-
Notifications
You must be signed in to change notification settings - Fork 0
C Variadic Arguments
| 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 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 argRule: always pass the immediately preceding named parameter. Anything else is undefined behavior.
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
}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 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.
A va_list cannot be reused after consumption. Two patterns to handle this:
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_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);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
| 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.