C Funções Variádicas

Você já chamou o printf com um argumento (printf("Olá\n")), com dois (printf("%d\n", idade)) e com cinco. Mas uma função em C tem uma lista fixa de parâmetros (veja Parâmetros). Como o printf aceita qualquer quantidade? Ele é uma função variádica: uma função que recebe um número variável de argumentos. Nesta aula você vai escrever as suas, com as ferramentas do cabeçalho stdarg.h, e vai ver por que elas são poderosas e perigosas ao mesmo tempo.

Você vai aprender
  • O que significam as reticências ... na lista de parâmetros.
  • As quatro peças do stdarg.h: va_list, va_start, va_arg e va_end.
  • Os três jeitos de a função saber quantos argumentos chegaram: um contador, uma sentinela e uma string de formato.
  • Por que va_arg(args, float) e va_arg(args, char) estão errados (as promoções).
  • O va_start de um argumento só, novidade do C23.
  • Como escrever uma função de log que funciona como o printf, com vprintf.

Todos os programas desta aula foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c/variadicas, com o comando padrão do guia: gcc -std=c23 -Wall -Wextra arquivo.c -o programa (veja Compilar com gcc). Você pode repetir cada um no editor do guia, que compila C de verdade no navegador.


A ideia: uma sacola sem etiquetas

Imagine que a Ana entrega uma sacola de compras para o filho guardar. Dentro há uma quantidade qualquer de coisas, uma atrás da outra. O filho só consegue tirar um item por vez, na ordem em que foram colocados, e os itens não têm etiqueta. Para guardar tudo direito, ele precisa de um bilhete da mãe dizendo duas coisas:

  1. O que é cada item ("primeiro um pacote de arroz, depois uma garrafa de leite…"), para saber como pegar e onde guardar.
  2. Quando parar ("são 3 itens", ou "pare quando achar o papel escrito FIM").

Uma função variádica é o filho. Os argumentos extras chegam na sacola, sem etiqueta de tipo. O bilhete é o que a função recebe antes das reticências: no printf, é o formato. Em printf("%s tem %d anos\n", nome, idade), o "%s tem %d anos\n" diz: "tire um texto, depois um int, e acabou". Se o bilhete mentir, o filho guarda o leite no armário de panelas, e ninguém avisa.


1. As reticências: ...

Uma função variádica se declara com ... (três pontos) no fim da lista de parâmetros. É assim que o printf está declarado dentro do stdio.h (simplificado):

int printf(const char *formato, ...);
const char *formato
Um parâmetro nomeado, normal: o texto com os %d, %s…. É o "bilhete".
...
"E mais quantos argumentos você quiser, de qualquer tipo". O nome técnico em português é reticências; em inglês, ellipsis. A palavra "variádica" vem do inglês variadic, "de quantidade variável".

As reticências sempre vêm por último. int f(..., int x) não compila. Até o C17, a função precisava ter pelo menos um parâmetro nomeado antes do ...; o C23 liberou isso (seção 5).


2. Sua primeira função variádica: somar N números

Para ler os argumentos que vieram nas reticências, use o cabeçalho stdarg.h (standard arguments, "argumentos padrão"). Esta função soma quantos números você passar. O primeiro argumento diz quantos vêm depois:

#include <stdarg.h>
#include <stdio.h>

int soma(int quantos, ...) {
    va_list args;
    va_start(args, quantos);
    int total = 0;
    for (int i = 0; i < quantos; i++) {
        total += va_arg(args, int);
    }
    va_end(args);
    return total;
}

int main(void) {
    printf("%d\n", soma(3, 10, 20, 30));
    printf("%d\n", soma(5, 1, 2, 3, 4, 5));
    printf("%d\n", soma(0));
    return 0;
}
$ gcc -std=c23 -Wall -Wextra soma.c -o soma
$ ./soma
60
15
0

A mesma função recebeu 3 números, depois 5, depois nenhum. Linha por linha:

LinhaO que faz
#include <stdarg.h>Traz as quatro peças abaixo. Sem ele, nada disso existe.
int soma(int quantos, ...)Um parâmetro nomeado (quantos, o "bilhete") e as reticências.
va_list args;Cria a "mão que tira itens da sacola". va_list = variable argument list, "lista de argumentos variáveis". É um tipo especial; você nunca olha dentro dele.
va_start(args, quantos);Põe a mão na sacola, logo depois do último parâmetro nomeado (quantos). Tem de vir antes de qualquer va_arg.
va_arg(args, int)Tira o próximo argumento, dizendo que ele é um int, e avança. Cada chamada pega um; não dá para voltar.
va_end(args);Tira a mão da sacola. Todo va_start tem o seu va_end, na mesma função, como todo malloc tem o seu free.
va_start, va_arg e va_end não são funções

São macros (veja Macros com Parâmetros): o compilador as troca por um código especial, que sabe onde os argumentos ficaram guardados. É por isso que va_arg(args, int) pode receber um tipo (int) como argumento, coisa que uma função normal não aceita.

Experimente: rode. Depois acrescente uma chamada soma(4, 2, 4, 6, 8) e mostre o resultado. Tente também soma(1, 99).

#include <stdarg.h>
#include <stdio.h>

int soma(int quantos, ...) {
    va_list args;
    va_start(args, quantos);
    int total = 0;
    for (int i = 0; i < quantos; i++) {
        total += va_arg(args, int);
    }
    va_end(args);
    return total;
}

int main(void) {
    printf("%d\n", soma(3, 10, 20, 30));
    printf("%d\n", soma(2, 7, 8));
    return 0;
}

3. Como a função sabe quando parar

Dentro da função, não existe jeito de perguntar "quantos argumentos vieram?" nem "de que tipo é o próximo?". Quem chama tem de contar. Há três jeitos de combinar isso:

JeitoComo funcionaExemplo
ContadorUm parâmetro nomeado diz quantos argumentos vêm depois.soma(3, 10, 20, 30)
SentinelaUm valor especial marca o fim, como o '\0' de uma string.lista("arroz", "café", NULL)
FormatoUm texto diz quantos argumentos vêm e de que tipo.printf("%s: %d\n", nome, n)

A sentinela

A palavra vem do soldado que fica de guarda no fim do acampamento. Para uma lista de textos, a sentinela natural é NULL (veja Ponteiro Nulo), porque nenhum texto de verdade mora no endereço nulo:

#include <stdarg.h>
#include <stdio.h>

void lista_compras(const char *primeiro, ...) {
    va_list args;
    va_start(args, primeiro);
    int n = 1;
    for (const char *item = primeiro; item != NULL; item = va_arg(args, const char *)) {
        printf("%d. %s\n", n, item);
        n++;
    }
    va_end(args);
}

int main(void) {
    lista_compras("arroz", "feijão", "café", NULL);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra sentinela.c -o sentinela
$ ./sentinela
1. arroz
2. feijão
3. café
const char *item = primeiro
O laço começa pelo parâmetro nomeado, que já é o primeiro item.
item != NULL
Continua enquanto não achar a sentinela.
item = va_arg(args, const char *)
Tira o próximo argumento da sacola, como um texto (um const char *; veja Ponteiros e Strings).
NULL
Na chamada, o último argumento. Esquecê-lo é um erro grave (veja Quando dá errado).

Experimente: acrescente mais dois itens à lista (antes do NULL) e rode.

#include <stdarg.h>
#include <stdio.h>

void lista_compras(const char *primeiro, ...) {
    va_list args;
    va_start(args, primeiro);
    int n = 1;
    for (const char *item = primeiro; item != NULL; item = va_arg(args, const char *)) {
        printf("%d. %s\n", n, item);
        n++;
    }
    va_end(args);
}

int main(void) {
    lista_compras("arroz", "feijão", "café", NULL);
    return 0;
}

O formato

É o jeito do printf e do scanf: a função lê o texto, e cada % diz "tire mais um argumento, deste tipo". O %d vira um va_arg(args, int); o %s, um va_arg(args, char *); o %f, um va_arg(args, double). É o jeito mais flexível, e o printf de verdade é feito assim. Você vai usar esse poder na seção 6, sem precisar reescrever o printf.


4. Os tipos que chegam: as promoções

Quando um argumento entra pelas reticências, o C aplica as promoções padrão de argumentos (default argument promotions), porque a função não diz que tipo espera:

Você passaChega na sacola comoEntão leia com
char, signed char, unsigned charintva_arg(args, int)
short, unsigned shortintva_arg(args, int)
boolintva_arg(args, int)
floatdoubleva_arg(args, double)
int, long, double, ponteiros…o mesmo tipoo mesmo tipo

É por isso que o printf usa o mesmo %f para float e para double: os dois chegam como double (veja Formatos do printf). E é por isso que va_arg(args, float) está errado: não existe float na sacola.

#include <stdarg.h>
#include <stdio.h>

double media(int quantos, ...) {
    va_list args;
    va_start(args, quantos);
    double total = 0;
    for (int i = 0; i < quantos; i++) {
        total += va_arg(args, float);
    }
    va_end(args);
    return total / quantos;
}

int main(void) {
    float a = 7.5f, b = 8.5f;
    printf("Média: %.2f\n", media(2, a, b));
    return 0;
}
$ gcc -std=c23 -Wall -Wextra media.c -o media
In file included from media.c:1:
media.c: In function ‘media’:
media.c:9:31: warning: ‘float’ is promoted to ‘double’ when passed through ‘...’
    9 |         total += va_arg(args, float);
      |                               ^
media.c:9:31: note: (so you should pass ‘double’ not ‘float’ to ‘va_arg’)
media.c:9:31: note: if this code is reached, the program will abort
$ ./media
Instrução ilegal         (imagem do núcleo gravada) ./media
$ echo $?
132

O gcc foi bem claro, em três frases:

‘float’ is promoted to ‘double’ when passed through ‘...’
"float é promovido a double quando passa pelas reticências".
(so you should pass ‘double’ not ‘float’ to ‘va_arg’)
"(então você deveria passar double, e não float, para o va_arg)".
if this code is reached, the program will abort
"se este código for executado, o programa vai abortar". E abortou mesmo: o gcc pôs ali uma instrução proibida de propósito, e o sistema matou o programa com Instrução ilegal (sinal SIGILL; código 132 = 128 + 4).

O Clang (o compilador do editor) avisa com outras palavras e não aborta: deixa o programa ler lixo.

$ clang -std=c23 -Wall -Wextra media.c -o media-clang
media.c:9:31: warning: second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double' [-Wvarargs]
    9 |         total += va_arg(args, float);
      |                               ^~~~~
1 warning generated.
$ ./media-clang
Média: 0.00

Second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double' = "o segundo argumento do va_arg é de um tipo promovível, float; este va_arg tem comportamento indefinido, porque os argumentos serão promovidos a double". No laboratório saiu Média: 0.00; no editor do guia sai outro número qualquer. Comportamento indefinido de verdade (veja Comportamento Indefinido).

Missão: rode e veja o aviso e a média errada. Conserte o va_arg para a média sair Média: 8.00, sem nenhum aviso. (Não precisa mudar o main: os float continuam chegando como double.)

#include <stdarg.h>
#include <stdio.h>

double media(int quantos, ...) {
    va_list args;
    va_start(args, quantos);
    double total = 0;
    for (int i = 0; i < quantos; i++) {
        total += va_arg(args, float);
    }
    va_end(args);
    return total / quantos;
}

int main(void) {
    float a = 7.5f, b = 8.5f;
    printf("Média: %.2f\n", media(2, a, b));
    return 0;
}
#include <stdarg.h>
#include <stdio.h>

double media(int quantos, ...) {
    va_list args;
    va_start(args, quantos);
    double total = 0;
    for (int i = 0; i < quantos; i++) {
        total += va_arg(args, double);
    }
    va_end(args);
    return total / quantos;
}

int main(void) {
    float a = 7.5f, b = 8.5f;
    printf("Média: %.2f\n", media(2, a, b));
    return 0;
}

Com char acontece o mesmo: va_arg(args, char) dá o aviso ‘char’ is promoted to ‘int’ when passed through ‘...’ no gcc, e o programa também morre com Instrução ilegal. O certo é ler um int e guardar num char: char c = (char) va_arg(args, int);.

Cuidado com o tipo dos números na chamada

A promoção acontece só com os tipos pequenos. Se a função lê va_arg(args, int) e você passa 1.5 (um double), ou se ela lê double e você passa 8 (um int), ninguém converte nada: a função lê lixo. Na chamada de uma função que lê double, escreva 8.0, e não 8.


5. va_start com um argumento só C23

Até o C17, o va_start precisava de dois argumentos: a va_list e o nome do último parâmetro nomeado. O C23 simplificou: o segundo virou opcional.

#include <stdarg.h>
#include <stdio.h>

int soma(int quantos, ...) {
    va_list args;
    va_start(args);
    int total = 0;
    for (int i = 0; i < quantos; i++) {
        total += va_arg(args, int);
    }
    va_end(args);
    return total;
}

int main(void) {
    printf("%d\n", soma(3, 10, 20, 30));
    return 0;
}
$ gcc -std=c23 -Wall -Wextra c23.c -o c23
$ ./c23
60
$ gcc -std=c17 -Wall -Wextra c23.c -o c23
c23.c: In function ‘soma’:
c23.c:6:18: error: macro ‘va_start’ requires 2 arguments, but only 1 given
    6 |     va_start(args);
      |                  ^
In file included from c23.c:1:
/usr/lib/gcc/x86_64-linux-gnu/15/include/stdarg.h:50:9: note: macro ‘va_start’ defined here
   50 | #define va_start(v,l)   __builtin_va_start(v,l)
      |         ^~~~~~~~
c23.c:6:5: error: ‘va_start’ undeclared (first use in this function)
    6 |     va_start(args);
      |     ^~~~~~~~
c23.c:6:5: note: each undeclared identifier is reported only once for each function it appears in

No C23 funciona; com -std=c17, o erro diz macro ‘va_start’ requires 2 arguments, but only 1 given ("a macro va_start exige 2 argumentos, mas só 1 foi dado"). O editor do guia (Clang, -std=c23) também aceita a forma nova. Se o seu código precisa compilar em compiladores antigos, use a forma de dois argumentos, que continua valendo no C23.

O C23 trouxe junto outra novidade: a função pode ter só as reticências, sem nenhum parâmetro nomeado, como void mostra(...). Com o va_start(args) de um argumento, ela consegue ler a sacola mesmo assim. O gcc 15 e o editor aceitam. Mas lembre: sem parâmetro nomeado, não há "bilhete", e a função continua precisando saber quantos argumentos ler (com uma sentinela, por exemplo).


6. vprintf: uma função de log como o printf

Um uso muito comum de função variádica é o log (o "diário de bordo" do programa): uma função que escreve mensagens com um prefixo, como [LOG], e aceita formatos como o printf. Você não precisa reescrever o printf: existe uma versão dele que recebe a sacola inteira, uma va_list, em vez dos argumentos soltos. É o vprintf (o v é de va_list):

#include <stdarg.h>
#include <stdio.h>

void registrar(const char *formato, ...) {
    va_list args;
    va_start(args, formato);
    fprintf(stderr, "[LOG] ");
    vfprintf(stderr, formato, args);
    fprintf(stderr, "\n");
    va_end(args);
}

int main(void) {
    int estoque = 12;
    registrar("programa começou");
    registrar("estoque de %s: %d unidades", "café", estoque);
    registrar("preço médio: R$ %.2f", 7.9);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra log.c -o log
$ ./log
[LOG] programa começou
[LOG] estoque de café: 12 unidades
[LOG] preço médio: R$ 7.90
registrar(const char *formato, ...)
A mesma "cara" do printf: um formato e as reticências.
fprintf(stderr, "[LOG] ")
Escreve o prefixo na saída de erros, stderr, o lugar certo para mensagens de diagnóstico (veja stdin, stdout e stderr).
vfprintf(stderr, formato, args)
Faz o trabalho do printf: lê o formato e tira da sacola args um argumento por %. É a versão v do fprintf.
va_end(args)
Quem chamou va_start chama va_end. O vfprintf usa a sacola, mas não a fecha.

Cada função da família printf tem a sua versão v:

Com argumentos soltosCom uma va_listEscreve em
printf(fmt, ...)vprintf(fmt, args)a tela (stdout)
fprintf(arq, fmt, ...)vfprintf(arq, fmt, args)um arquivo ou stderr
snprintf(buf, n, fmt, ...)vsnprintf(buf, n, fmt, args)uma string, sem passar de n bytes
scanf(fmt, ...)vscanf(fmt, args)(lê da entrada)

O compilador não confere o seu formato… a menos que você peça

O gcc confere os %d e %s de um printf (o aviso -Wformat). Mas na sua função ele não sabe que o primeiro parâmetro é um formato. Veja um erro passar calado:

#include <stdarg.h>
#include <stdio.h>

void registrar(const char *formato, ...) {
    va_list args;
    va_start(args, formato);
    printf("[LOG] ");
    vprintf(formato, args);
    printf("\n");
    va_end(args);
}

int main(void) {
    registrar("estoque: %d unidades", "doze");
    return 0;
}
$ gcc -std=c23 -Wall -Wextra logsem.c -o logsem          « nenhum aviso
$ ./logsem
[LOG] estoque: -1248849909 unidades

O %d pediu um int, mas veio um texto; o vprintf leu lixo. O gcc e o Clang aceitam um atributo que diz "esta função funciona como o printf": [[gnu::format(printf, 1, 2)]]. O 1 é a posição do formato; o 2, a posição onde começam as reticências. Ele não faz parte do padrão C (o gnu:: avisa que é uma extensão do GNU; veja Atributos), mas os dois compiladores mais usados entendem:

#include <stdarg.h>
#include <stdio.h>

[[gnu::format(printf, 1, 2)]]
void registrar(const char *formato, ...) {
    va_list args;
    va_start(args, formato);
    printf("[LOG] ");
    vprintf(formato, args);
    printf("\n");
    va_end(args);
}

int main(void) {
    registrar("estoque: %d unidades", "doze");
    return 0;
}
$ gcc -std=c23 -Wall -Wextra logerro.c -o logerro
logerro.c: In function ‘main’:
logerro.c:15:26: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘char *’ [-Wformat=]
   15 |     registrar("estoque: %d unidades", "doze");
      |                         ~^            ~~~~~~
      |                          |            |
      |                          int          char *
      |                         %s

Agora o erro aparece na hora de compilar, com a sugestão de usar %s. No editor, o Clang avisa format specifies type 'int' but the argument has type 'char *'.


Formas diferentes: qual usar

FormaBom paraCuidado
Contador (soma(3, a, b, c))Argumentos todos do mesmo tipo, em pouca quantidadeSe o número não bater com os argumentos, a função lê lixo
Sentinela (..., NULL)Listas de ponteiros (textos)Esquecer o NULL faz a função ler além do fim
Formato + vprintfMensagens, logs, relatóriosUse [[gnu::format(printf, …)]] para o compilador conferir
Array + tamanho (soma(v, n))Muitos valores do mesmo tipoNenhum dos problemas acima: o compilador confere tudo

Qual usar: quando todos os valores são do mesmo tipo, prefira um array e o tamanho, como int soma(const int v[], int n) (veja Ponteiros e Arrays). O compilador confere o tipo de cada coisa, e não há sacola sem etiqueta. Guarde as funções variádicas para quando os tipos mudam de chamada para chamada, como num log com formato. É o caso em que elas brilham.


Quando dá errado

As funções variádicas são uma das poucas partes do C em que o compilador não consegue conferir os tipos. Quase todo erro aqui é comportamento indefinido, sem mensagem.

O contador não bate com os argumentos
int main(void) {
    printf("%d\n", soma(4, 10, 20, 30));   // diz 4, passa 3
    printf("%d\n", soma(2, 1.5, 2.5));     // passa double, a função lê int
    return 0;
}
$ gcc -std=c23 -Wall -Wextra faltou.c -o faltou     « nenhum aviso
$ ./faltou
1931552444
707198992
$ ./faltou
1537287868
1258110992                     « outro lixo a cada execução

O que significa: a primeira chamada mandou tirar 4 itens de uma sacola com 3; a segunda mandou tirar int onde havia double. Nenhum compilador avisa. No editor do guia, a primeira linha pode até sair 60, por sorte.

Como consertar: confira cada chamada. Se a função lê int, passe inteiros. Se você se pega errando a contagem, troque por um array e o tamanho.

Esqueceu a sentinela
    lista_compras("arroz", "feijão", "café");   // faltou o NULL
$ gcc -std=c23 -Wall -Wextra semsent.c -o semsent     « nenhum aviso
$ ./semsent
1. arroz
2. feijão
3. café
4. feijão
5. �?��u
6.
7.
8. ����
9. �@c��a
10. ~���
Falha de segmentação     (imagem do núcleo gravada) ./semsent

O que significa: sem o NULL, a função continuou tirando "textos" da memória depois dos argumentos: lixo, até cair num endereço proibido. No editor do guia, o mesmo código pode parar certinho no item 3, por sorte.

Como consertar: ponha o NULL no fim de toda chamada. É fácil esquecer; por isso a sentinela combina melhor com poucas chamadas, ou com uma macro que acrescente o NULL sozinha.

warning: ‘float’ is promoted to ‘double’ when passed through ‘...’ e depois Instrução ilegal

O que significa: um va_arg(args, float) (ou char, short, bool). Esses tipos nunca chegam pela sacola: viram double ou int. O gcc troca a linha por uma instrução que derruba o programa (código 132). No Clang: second argument to 'va_arg' is of promotable type.

Como consertar: leia o tipo promovido: va_arg(args, double) para float, va_arg(args, int) para char, short e bool.

error: unknown type name ‘va_list’
$ gcc -std=c23 -Wall -Wextra seminclude.c -o seminclude
seminclude.c: In function ‘soma’:
seminclude.c:4:5: error: unknown type name ‘va_list’
    4 |     va_list args;
      |     ^~~~~~~
seminclude.c:2:1: note: ‘va_list’ is defined in header ‘<stdarg.h>’; this is probably fixable by adding ‘#include <stdarg.h>’
    1 | #include <stdio.h>
  +++ |+#include <stdarg.h>
    2 |
seminclude.c:5:5: error: implicit declaration of function ‘va_start’ [-Wimplicit-function-declaration]
    5 |     va_start(args, quantos);
      |     ^~~~~~~~
…

O que significa: "nome de tipo desconhecido: va_list". Faltou o #include <stdarg.h>, e o próprio gcc sugere a linha (this is probably fixable by adding = "isto provavelmente se conserta acrescentando"). No Clang: use of undeclared identifier 'va_list'.

Como consertar: acrescente #include <stdarg.h> no topo.

error: ‘va_start’ used in function with fixed arguments

O que significa: "va_start usado numa função com argumentos fixos": a função não tem ... na lista de parâmetros, então não há sacola para abrir. No Clang: 'va_start' used in function with fixed args.

Como consertar: acrescente , ... no fim dos parâmetros, ou tire o va_start se a função não precisa de argumentos extras.

error: macro ‘va_start’ requires 2 arguments, but only 1 given

O que significa: você usou o va_start(args) do C23 compilando com um padrão antigo (-std=c17, -std=c11…).

Como consertar: compile com -std=c23 ou escreva a forma antiga, va_start(args, ultimo_parametro), que funciona em todas as versões.


Referência rápida

EscritaO que faz
#include <stdarg.h>Traz va_list e as macros abaixo
tipo f(tipo ultimo, ...)Declara uma função variádica (as reticências sempre no fim)
va_list args;Cria a variável que percorre os argumentos extras
va_start(args, ultimo);Começa a leitura depois do último parâmetro nomeado
va_start(args); C23O mesmo, sem repetir o nome
va_arg(args, tipo)Devolve o próximo argumento como tipo e avança (use int e double, nunca char, short, bool ou float)
va_end(args);Termina a leitura; um para cada va_start
va_copy(copia, args); C99Copia a posição atual, para percorrer a sacola duas vezes (a cópia também precisa de va_end)
vprintf(fmt, args)printf que recebe uma va_list (também vfprintf, vsnprintf, vscanf)
[[gnu::format(printf, 1, 2)]]Extensão do gcc e do Clang: confere os formatos da sua função como confere os do printf

Exercícios

1. Começar a leitura

Complete a linha que começa a leitura dos argumentos extras de int maior(int quantos, ...), na forma que funciona em todas as versões do C:

(args, quantos);

2. O tipo certo

A função recebe notas que foram guardadas em variáveis float. Complete o tipo no va_arg:

double nota = va_arg(args, );

3. A versão com va_list

Qual função faz o trabalho do fprintf, mas recebe uma va_list?

(stderr, formato, args);

4. Dentro de uma função variádica, como ela descobre quantos argumentos recebeu?

5. Você chama letras(3, 'A', 'n', 'a'). Como a função deve ler cada letra?

6. A função soma(int quantos, ...) lê int. O que acontece com soma(2, 1.5, 2.5)?

Missão final: o log da lojinha

Missão: a função registrar da lojinha mostra o formato cru, com os %s e %d sem trocar. Reescreva-a com va_list, va_start, vprintf e va_end, para a saída ser [LOG] Ana vendeu 3 cafés e [LOG] caixa fechou com R$ 23.70. Não mude o main.

#include <stdarg.h>
#include <stdio.h>

void registrar(const char *formato, ...) {
    printf("[LOG] %s\n", formato);
}

int main(void) {
    registrar("%s vendeu %d cafés", "Ana", 3);
    registrar("caixa fechou com R$ %.2f", 23.7);
    return 0;
}
#include <stdarg.h>
#include <stdio.h>

void registrar(const char *formato, ...) {
    va_list args;
    va_start(args, formato);
    printf("[LOG] ");
    vprintf(formato, args);
    printf("\n");
    va_end(args);
}

int main(void) {
    registrar("%s vendeu %d cafés", "Ana", 3);
    registrar("caixa fechou com R$ %.2f", 23.7);
    return 0;
}

Resumo

  • Uma função variádica tem ... no fim dos parâmetros e aceita qualquer quantidade de argumentos extras. O printf é a mais famosa.
  • Para ler os extras: va_list args;, va_start(args, ultimo);, um va_arg(args, tipo) por argumento e va_end(args); no fim.
  • A função não sabe quantos argumentos vieram nem de que tipo: quem chama diz, com um contador, uma sentinela (NULL) ou um formato. Se a informação mentir, é comportamento indefinido, sem aviso.
  • Pelas reticências, char, short e bool chegam como int, e float chega como double. Nunca use va_arg(args, float): no gcc, o programa morre com Instrução ilegal.
  • No C23, va_start(args) aceita um argumento só e a função pode ter só ....
  • Para uma função "tipo printf", passe a va_list para vprintf/vfprintf/vsnprintf e marque-a com [[gnu::format(printf, 1, 2)]] para o compilador conferir os formatos.
  • Quando os valores são todos do mesmo tipo, um array e o tamanho são mais seguros.