Funções com número variável de argumentos
LidoNesta aula, você aprenderá a criar funções em C que aceitam um número variável de argumentos, usando o cabeçalho stdarg.h e as macros va_list, va_start, va_arg e va_end. Vamos também entender como printf funciona internamente e ver exemplos práticos de implementação.
Em C, é possível criar funções que recebem uma quantidade variável de argumentos, como printf e scanf. Essa capacidade é essencial para funções de propósito geral que precisam lidar com entradas de tamanho dinâmico. Nesta aula, você vai dominar o uso do cabeçalho stdarg.h, que fornece as ferramentas necessárias para implementar tais funções de forma segura e eficiente.
O suporte a argumentos variáveis é uma característica poderosa da linguagem, mas exige cuidado: o compilador não verifica os tipos nem a quantidade de argumentos passados, então o programador deve usar mecanismos como um contador explícito ou uma string de formato para informar quantos argumentos esperar e de que tipos eles são. Vamos explorar esses conceitos em detalhes.
stdarg.h
O cabeçalho stdarg.h define macros e tipos que permitem acessar os argumentos adicionais de uma função variádica. Ele é parte da biblioteca padrão do C e está disponível em qualquer compilador conforme o padrão ANSI C (C89/C90) e posteriores.
Para usar argumentos variáveis, a função deve ter pelo menos um parâmetro fixo (ou seja, declarado normalmente) e, após ele, uma elipse (...) na lista de parâmetros. Por exemplo:
int soma(int n, ...);Aqui, n é um parâmetro fixo que pode ser usado para informar quantos argumentos variáveis serão passados. O ... indica que a função aceita um número não especificado de argumentos adicionais.
O cabeçalho stdarg.h define quatro macros principais: va_list, va_start, va_arg e va_end. Elas são usadas em conjunto para percorrer a lista de argumentos variáveis. Além disso, a macro va_copy (disponível desde C99) permite copiar o estado de uma lista para outra, útil em situações que exigem múltiplas varreduras.
va_list, va_start, va_arg
A macro va_list é um tipo de dado que representa a lista de argumentos variáveis. Geralmente é um ponteiro para um registro que contém as informações necessárias para acessar os argumentos na pilha. Você declara uma variável desse tipo para manipular a lista.
A macro va_start inicializa a lista de argumentos. Ela recebe dois argumentos: a variável do tipo va_list e o último parâmetro fixo da função. Isso permite que a macro saiba onde os argumentos variáveis começam. Por exemplo, em void f(int n, ...), chamamos va_start(ap, n).
A macro va_arg extrai o próximo argumento da lista. Ela recebe a variável va_list e o tipo esperado do argumento (por exemplo, int, double, char *). Cada chamada a va_arg avança a lista para o próximo argumento. É crucial que o tipo informado corresponda ao tipo real do argumento, pois não há verificação automática.
Por fim, va_end deve ser chamado após o processamento, para realizar a limpeza necessária (em algumas plataformas, pode ser uma operação nula, mas é obrigatória por padrão). Não chamar va_end pode causar comportamento indefinido.
Um exemplo simples de função que soma um número variável de inteiros:
#include <stdarg.h>
int soma(int n, ...) {
va_list ap;
int soma = 0;
va_start(ap, n);
for (int i = 0; i < n; i++) {
soma += va_arg(ap, int);
}
va_end(ap);
return soma;
}Neste exemplo, n indica quantos argumentos serão somados. A função percorre a lista usando va_arg com tipo int e acumula o resultado.
Como printf funciona
A função printf é um exemplo clássico de função com número variável de argumentos. Sua assinatura é int printf(const char *format, ...). O primeiro argumento é uma string de formato que especifica quantos e quais tipos de argumentos são esperados.
Internamente, printf percorre a string de formato caractere por caractere. Quando encontra um especificador de conversão (como %d, %f, %s), ele usa va_arg para obter o argumento correspondente, com o tipo indicado pelo especificador. Por exemplo, %d espera um int, %f espera um double (mas note que em argumentos variáveis, float é promovido a double), e %s espera um char *.
Essa abordagem é flexível, mas perigosa: se o número ou tipo dos argumentos não corresponder à string de formato, o comportamento é indefinido, podendo causar falhas de segmentação ou leitura de memória incorreta. Compiladores modernos podem emitir avisos se a string de formato for uma constante e houver incompatibilidade.
Para ilustrar, uma implementação simplificada de uma função similar a printf (apenas para inteiros e strings) poderia ser:
#include <stdio.h>
#include <stdarg.h>
void minha_printf(const char *formato, ...) {
va_list ap;
va_start(ap, formato);
for (const char *p = formato; *p != '\0'; p++) {
if (*p == '%') {
p++;
switch (*p) {
case 'd': {
int valor = va_arg(ap, int);
printf("%d", valor);
break;
}
case 's': {
const char *s = va_arg(ap, const char *);
printf("%s", s);
break;
}
default:
putchar('%');
putchar(*p);
}
} else {
putchar(*p);
}
}
va_end(ap);
}Essa implementação ignora muitos detalhes, como largura de campo, precisão e flags, mas mostra o princípio básico de como printf funciona.
Exemplos
Vamos explorar mais alguns exemplos práticos para consolidar o uso de funções variádicas.
Exemplo 1: Função para encontrar o maior número
A função abaixo recebe uma quantidade variável de inteiros e retorna o maior deles. O primeiro argumento fixo é a quantidade de números.
#include <stdarg.h>
int maior(int n, ...) {
va_list ap;
va_start(ap, n);
int max = va_arg(ap, int); // primeiro valor
for (int i = 1; i < n; i++) {
int valor = va_arg(ap, int);
if (valor > max) max = valor;
}
va_end(ap);
return max;
}Uso:
#include <stdio.h>
int main() {
printf("%d\n", maior(5, 10, 20, 5, 30, 15)); // imprime 30
return 0;
}Exemplo 2: Concatenar strings
Vamos criar uma função que concatena várias strings em um buffer fornecido. O primeiro argumento fixo é o buffer de destino, o segundo é o número de strings a concatenar, e os seguintes são as strings.
#include <stdarg.h>
#include <string.h>
void concatenar(char *dest, size_t tam, int n, ...) {
va_list ap;
va_start(ap, n);
dest[0] = '\0'; // esvazia o buffer
for (int i = 0; i < n; i++) {
const char *s = va_arg(ap, const char *);
strncat(dest, s, tam - strlen(dest) - 1);
}
va_end(ap);
}Uso:
#include <stdio.h>
int main() {
char buffer[100];
concatenar(buffer, sizeof(buffer), 3, "Olá, ", "mundo ", "!");
puts(buffer); // imprime "Olá, mundo !"
return 0;
}Exemplo 3: Função para imprimir vários valores com separador
Esta função imprime uma sequência de inteiros separados por um caractere fornecido. O primeiro argumento é o separador, o segundo é o número de inteiros, e os demais são os inteiros.
#include <stdio.h>
#include <stdarg.h>
void imprimir_separado(char sep, int n, ...) {
va_list ap;
va_start(ap, n);
for (int i = 0; i < n; i++) {
if (i > 0) putchar(sep);
printf("%d", va_arg(ap, int));
}
putchar('\n');
va_end(ap);
}Uso:
int main() {
imprimir_separado(',', 4, 1, 2, 3, 4); // imprime 1,2,3,4
return 0;
}Esses exemplos mostram a versatilidade das funções variádicas. No entanto, lembre-se de que a responsabilidade de passar os argumentos corretos é do chamador; a função não pode verificar a quantidade ou os tipos.
Boas práticas
- Sempre forneça um mecanismo para o chamador indicar quantos argumentos serão passados: um contador explícito, uma string de formato ou um valor sentinela (como
NULLpara ponteiros). - Use
va_copy(disponível em C99) se precisar processar a lista mais de uma vez. - Evite usar funções variádicas em código crítico de segurança, pois a falta de verificação de tipos pode causar vulnerabilidades.
- Considere usar macros como
_Static_assertoustatic_assertpara verificar tamanhos de tipos, se necessário. - Documente claramente na interface da função quais tipos de argumentos são esperados.
Referências
- cppreference: Variadic functions
- cppreference: printf
- POSIX: stdarg.h
- GNU C Library: Variadic Functions
- Microsoft C++: va_arg, va_copy, va_end, va_start
Exercícios
Crie uma função variádica
mediaque recebe um inteironenvaloresdouble, e retorna a média aritmética deles. Teste com alguns valores.✓ Resposta:#include <stdarg.h> double media(int n, ...) { va_list ap; va_start(ap, n); double soma = 0.0; for (int i = 0; i < n; i++) { soma += va_arg(ap, double); } va_end(ap); return soma / n; }Implemente uma função
printf_variavelque aceita uma string de formato com%de%se imprime os argumentos correspondentes. Useva_argpara extrair os valores.✓ Resposta:#include <stdio.h> #include <stdarg.h> void printf_variavel(const char *formato, ...) { va_list ap; va_start(ap, formato); for (const char *p = formato; *p != '\0'; p++) { if (*p == '%') { p++; switch (*p) { case 'd': printf("%d", va_arg(ap, int)); break; case 's': printf("%s", va_arg(ap, const char *)); break; default: putchar('%'); putchar(*p); } } else { putchar(*p); } } va_end(ap); }Escreva uma função variádica
soma_variosque recebe um número variável de inteiros, mas termina quando o argumento for igual a 0 (sentinela). A função deve retornar a soma. Dica: use um loop que lêva_argaté encontrar 0.✓ Resposta:#include <stdarg.h> int soma_varios(int primeiro, ...) { va_list ap; va_start(ap, primeiro); int soma = 0; int valor = primeiro; while (valor != 0) { soma += valor; valor = va_arg(ap, int); } va_end(ap); return soma; }Crie uma função
maximo_doubleque recebe um inteironenvaloresdouble, e retorna o maior deles. Teste com valores positivos e negativos.✓ Resposta:#include <stdarg.h> double maximo_double(int n, ...) { va_list ap; va_start(ap, n); double max = va_arg(ap, double); for (int i = 1; i < n; i++) { double v = va_arg(ap, double); if (v > max) max = v; } va_end(ap); return max; }Explique por que é necessário chamar
va_endapós usarva_liste o que pode acontecer se você não chamar. Dê um exemplo de código com e sem.✓ Resposta:va_endé necessário para realizar qualquer limpeza que a implementação exija. Em algumas plataformas, pode não fazer nada, mas é obrigatório por padrão. Se você não chamar, pode ocorrer comportamento indefinido, como vazamento de recursos ou corrupção de memória. Exemplo com e sem:// Com va_end (correto) void f(int n, ...) { va_list ap; va_start(ap, n); // ... va_end(ap); } // Sem va_end (incorreto) void g(int n, ...) { va_list ap; va_start(ap, n); // ... // falta va_end(ap); }