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 NULL para 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_assert ou static_assert para verificar tamanhos de tipos, se necessário.
  • Documente claramente na interface da função quais tipos de argumentos são esperados.

Referências

Exercícios

  1. Crie uma função variádica media que recebe um inteiro n e n valores double, 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;
    }
  2. Implemente uma função printf_variavel que aceita uma string de formato com %d e %s e imprime os argumentos correspondentes. Use va_arg para 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);
    }
  3. Escreva uma função variádica soma_varios que 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_arg até 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;
    }
  4. Crie uma função maximo_double que recebe um inteiro n e n valores double, 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;
    }
  5. Explique por que é necessário chamar va_end após usar va_list e 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);
    }