A variadic function's last declared parameter is ..., accepting any number of additional arguments after the fixed ones. Inside the function, va_start begins accessing the variable arguments, va_arg retrieves each one in order (requiring the caller to specify its type explicitly), and va_end cleans up. Critically, C has no automatic mechanism telling the function how many arguments were actually passed or what their types are — printf works around this by parsing its own format string, meaning a mismatched format specifier (like %d for a value that's actually a double) is undefined behavior, not a caught error.
Writing a variadic function with stdarg.h
Since C doesn't automatically track argument count, this function needs its own way to know when to stop — here, a sentinel value (-1) marks the end of the list, one of a few common conventions for this problem.
#include <stdarg.h>
int sum(int count, ...) { // count tells us how many follow — one common convention
va_list args;
va_start(args, count); // start reading arguments AFTER count
int total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(args, int); // must specify the TYPE explicitly each time
}
va_end(args); // required cleanup
return total;
}
printf("%d\n", sum(3, 10, 20, 30)); // 60 — count=3 tells sum() how many ints followWhy printf trusts the format string, dangerously
printf has no way to verify the argument's actual type matches %d — if you pass a double where %d expects an int, the behavior is undefined, not a caught error; this is exactly why compilers like GCC add special format-string checking specifically for printf-family functions, since the language itself provides none.
double pi = 3.14159;
printf("%d\n", pi); // UNDEFINED BEHAVIOR: %d expects int, pi is double — likely garbage output
// gcc -Wall specifically flags this exact mistake via -Wformat, worth always enablingThe NULL/sentinel-terminated alternative
Rather than passing an explicit count, some variadic functions use a special terminating value in the argument list itself to know when to stop — the caller must remember to include it, another manual responsibility C doesn't enforce.
#include <stdarg.h>
#include <stdio.h>
void printStrings(const char *first, ...) {
va_list args;
va_start(args, first);
const char *s = first;
while (s != NULL) {
printf("%s\n", s);
s = va_arg(args, const char *);
}
va_end(args);
}
printStrings("a", "b", "c", NULL); // NULL marks the end — easy to forget it