C Variadic Arguments - MarekBykowski/readme GitHub Wiki
C Variadic Arguments
Symbols
| Symbol | Role |
|---|---|
va_list |
opaque type holding current position in the variadic argument list |
va_start(args, last) |
initializes args; last is the last named parameter — used as anchor (see below) |
va_arg(args, type) |
pulls next argument as type and advances the internal pointer |
va_copy(dst, src) |
snapshots src into an independent dst at the same position |
va_end(args) |
invalidates args — mandatory before the function returns |
vprintf(fmt, args) |
printf variant that accepts a va_list instead of ... |
vfprintf(fp, fmt, args) |
fprintf variant that accepts a va_list |
vsnprintf(buf, n, fmt, args) |
snprintf variant that accepts a va_list |
va_start anchor — why last matters
va_start needs to know where the variadic args begin in memory. It finds
that by looking at the address of the last known named parameter and starting
just after it.
void log_msg(const char *file, int line, const char *fmt, ...)
// ^^^^ anchor
{
va_list args;
va_start(args, fmt); // "variadic args start right after fmt"
}
The named parameters are file, line, fmt. The ... starts after fmt,
so fmt is the correct anchor.
If you passed line instead:
va_start(args, line); // wrong — starts reading from fmt onward
// picks up fmt as if it were the first variadic arg
Rule: always pass the immediately preceding named parameter. Anything else is undefined behavior.
Lifecycle
void example(const char *fmt, ...) {
va_list args;
va_start(args, fmt); // initialize — args points at first variadic arg
vprintf(fmt, args); // consume — pointer walks through all args
va_end(args); // invalidate — required
}
What "consume" means
vprintf walks fmt left to right. For every specifier it finds, it calls
va_arg internally inside libc — you never write this yourself when using
vprintf. Each va_arg call does two things:
- reads the value at the current pointer position as the expected type
- advances the pointer by
sizeof(type)
For a concrete call:
vprintf("val=%d name=%s", args); // args contains: 99, "bar"
hits %d → reads 99 as int → pointer advances 4 bytes
hits %s → reads "bar" as char * → pointer advances 8 bytes
end of fmt → stops
After this, args is sitting past "bar" — nothing left to read, no way to
rewind. That is what consumed means.
va_arg — the lower-level primitive
va_arg is what vprintf uses under the hood. You would only write it
yourself if you were manually walking the argument list instead of delegating
to vprintf:
va_start(args, fmt);
int x = va_arg(args, int); // pull args one by one manually
char *s = va_arg(args, char *);
va_end(args);
When you use vprintf, va_arg is an implementation detail — not visible in
your code.
Reusing va_list
A va_list cannot be reused after consumption. Two patterns to handle this:
Double va_start — simpler, idiomatic for distant uses
va_start(args, fmt);
vprintf(fmt, args);
va_end(args);
va_start(args, fmt); // reset to beginning
vfprintf(fp, fmt, args);
va_end(args);
va_copy — cleaner when copy is needed close to va_start
va_list args, copy;
va_start(args, fmt);
va_copy(copy, args); // snapshot before consuming
vprintf(fmt, args);
va_end(args);
vfprintf(fp, fmt, copy); // copy still at the beginning
va_end(copy);
Format String & Conversion Specifiers
A format string is a char * containing literal text interleaved with
conversion specifiers — placeholders that tell printf which type to pull
from va_list and how to render it.
% [flags] [width] [.precision] [length modifier] conversion
%02ld → pad with zeros | width 2 | long | signed decimal
%03ld → pad with zeros | width 3 | long | signed decimal
%.2f → 2 decimal places | double | decimal float
%-10s → left-align | width 10 | string
Common conversions
| Specifier | Expected type | Output |
|---|---|---|
%d / %i |
int |
signed decimal |
%ld |
long |
signed decimal |
%u |
unsigned int |
unsigned decimal |
%lu |
unsigned long |
unsigned decimal |
%s |
char * |
null-terminated string |
%p |
void * |
pointer address in hex |
%x / %X |
unsigned int |
hex lowercase / uppercase |
%f |
double |
decimal float |
%zu |
size_t |
unsigned decimal |
Type mismatch between specifier and actual argument is undefined behavior. Compile with
-Wformatto catch most cases statically.