Skip to content

C Variadic Arguments

MarekBykowski edited this page Jun 12, 2026 · 1 revision

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:

  1. reads the value at the current pointer position as the expected type
  2. 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 -Wformat to catch most cases statically.

Clone this wiki locally