<stdarg.h> lets you write functions that accept a variable number of arguments, like printf. The caller passes extra values after a fixed parameter; inside the function you use va_start, va_arg, and va_end to walk through them.
01
va_list
Arg walker.
02
va_start
Begin list.
03
va_arg
Next value.
04
va_end
Cleanup.
05
...
Ellipsis.
06
count
Know arity.
Fundamentals
Definition and Usage
A variadic function ends its parameter list with ... (ellipsis). At least one named parameter must come before ...—often a count or format string that tells the function how many or what types follow.
The compiler does not type-check arguments after .... You must call va_arg with the correct type each time. Wrong types cause undefined behavior.
💡
Beginner Tip
Always call va_end before returning from the function (every path). The old reference labeled va_list as a macro—it is a type; va_start / va_arg / va_end are the macros.
Foundation
📝 Syntax
Include the header:
C
#include <stdarg.h>
Variadic function declaration
C
int sum(int count, ...); /* count is last fixed arg before ... */
Macro and type reference
va_list — type for the argument pointer (e.g. va_list ap;).
va_start(ap, last) — initialize ap; last is the last named parameter.
va_arg(ap, type) — fetch next argument as type.
va_end(ap) — end traversal; required cleanup.
va_copy(dest, src) — copy state (C99); call va_end on both when done.
Default argument promotion
char and short → use va_arg(ap, int).
float → use va_arg(ap, double).
float literals in variadic calls are promoted to double.
Headers and linking
#include <stdarg.h> — no special link flag.
Compile: gcc program.c -std=c11 -o program
Cheat Sheet
⚡ Quick Reference
Step
Macro
Purpose
1
va_list ap;
Declare walker
2
va_start(ap, last)
Point at first ... arg
3
va_arg(ap, int)
Read next arg
4
va_end(ap)
Cleanup
Arity
count or NULL
Know when to stop
Start
va_start(ap, count)
After fixed args
Read
va_arg(ap, int)
Match real type
End
va_end(ap)
Always call
float
va_arg(ap, double)
Promotion rule
Hands-On
Examples Gallery
Compile with gcc file.c -std=c11 -o out. Every variadic function needs a rule for how many arguments to read and of what type.
📚 Getting Started
Count-based variadic sum from the reference.
Example 1 — Sum Variable Integers with a Count
Classic pattern: first argument is how many int values follow.
C
#include <stdarg.h>
#include <stdio.h>
int sum(int count, ...) {
int total = 0;
va_list args;
int i;
va_start(args, count);
for (i = 0; i < count; i++) {
total += va_arg(args, int);
}
va_end(args);
return total;
}
int main(void) {
printf("Sum of 3, 5, 7: %d\n", sum(3, 3, 5, 7));
printf("Sum of 1..5: %d\n", sum(5, 1, 2, 3, 4, 5));
return 0;
}
📤 Output:
Sum of 3, 5, 7: 15
Sum of 1..5: 15
How It Works
va_start(args, count) uses count as the anchor parameter. The loop calls va_arg(args, int) exactly count times—no more, no less.
Example 2 — Average of doubles
Same count pattern with double (remember float args promote to double).
C
#include <stdarg.h>
#include <stdio.h>
double average(int count, ...) {
va_list args;
double sum = 0.0;
int i;
va_start(args, count);
for (i = 0; i < count; i++) {
sum += va_arg(args, double);
}
va_end(args);
return count > 0 ? sum / count : 0.0;
}
int main(void) {
printf("Avg: %.2f\n", average(4, 10.0, 20.0, 30.0, 40.0));
return 0;
}
📤 Output:
Avg: 25.00
How It Works
Passing 10.0 etc. as double matches va_arg(args, double). If callers passed bare float, you would still read with double due to promotion.
📈 Practical Patterns
Sentinel values, logging, and max-finding.
Example 3 — Strings Until NULL Sentinel
No count parameter—stop when you see a NULL pointer.
[INFO] User Alice logged in (id=42)
[WARN] Retries left: 3
How It Works
Real printf parses fmt to know types. Here we delegate to vprintf from <stdio.h>—the standard helper that accepts a va_list.
Example 5 — Maximum of N Integers
Requires at least one value; returns the largest.
C
#include <stdarg.h>
#include <stdio.h>
int max_int(int count, ...) {
va_list args;
int i, m;
if (count <= 0) {
return 0;
}
va_start(args, count);
m = va_arg(args, int);
for (i = 1; i < count; i++) {
int v = va_arg(args, int);
if (v > m) {
m = v;
}
}
va_end(args);
return m;
}
int main(void) {
printf("Max: %d\n", max_int(5, 3, 9, 1, 9, 4));
return 0;
}
📤 Output:
Max: 9
How It Works
Seed m with the first va_arg, then compare each subsequent value. Validate count > 0 before touching the list.
Applications
🚀 Common Use Cases
printf / scanf family — format-driven variadic I/O.
String builders — concatenate unknown number of C strings.
Testing frameworks — assertion macros with optional messages.
🧠 How stdarg.h Works
1
Caller pushes extras
Arguments after the last fixed param go on stack per calling convention.
Call site
2
va_start locates them
Uses address of last fixed arg to find first ... value.
Initialize
3
va_arg reads each
Advances pointer by size of requested type.
Iterate
=
💬
va_end finishes
Cleanup; function returns result built from the args.
Important
📝 Notes
You cannot pass ... to another function directly—use va_list and helpers like vprintf.
Wrong va_arg type corrupts the walk and may crash.
Reading past the last real argument is undefined behavior.
C++ has safer alternatives (std::initializer_list, templates); C uses stdarg.
va_end must run on every path, including early return.
Variadic macros in C99 use __VA_ARGS__—different feature, often paired with logging.
Performance
⚡ Optimization
Variadic calls add slight overhead versus fixed parameters. For hot paths, pass an array and length instead of ten separate arguments. For logging, the cost is dominated by I/O, not va_arg itself.
Wrap Up
Conclusion
<stdarg.h> powers flexible APIs like printf. Declare ..., anchor with a fixed parameter, then va_start / va_arg / va_end. Document how callers specify arity and types.
When type safety matters more than flexibility, use fixed arrays, structs, or counted buffers instead of variadic functions.
stdarg.h provides macros to read variable arguments in functions declared with ... (ellipsis). va_start initializes a va_list, va_arg fetches each argument by type, and va_end cleans up. printf and scanf use this machinery internally.
va_start(ap, last) needs the name of the last fixed parameter before the ... arguments. The implementation uses it to find where the variable args begin on the stack. It must be the parameter immediately before ...
printf reads the format string (%d, %f, %s) to decide which type to pass to va_arg each time. Your functions must use a similar convention—pass a count, a format string, or a sentinel—because the compiler does not type-check ... arguments.
In variable argument lists, char and short are promoted to int, and float is promoted to double. Always use va_arg(ap, int) for promoted char/short and va_arg(ap, double) for float values passed through ...
va_list is a type (typedef), not a macro. va_start, va_arg, va_end, and va_copy (C99) are macros. You declare va_list ap; then call va_start(ap, last_fixed_arg).
Use them for logging, printf-style APIs, or generic wrappers where arity varies. For most application code, fixed parameters, arrays, or structs are safer. Prefer stdarg when you are building a small library API that truly needs flexible arity.
Did you know?
The C standard library exposes vprintf, vfprintf, and vsnprintf so wrapper functions can forward a va_list without re-parsing .... That is how a one-line #define log(...) my_log(__VA_ARGS__) can still reach a shared variadic core.