Variadic Functions with stdarg
stdarg provides the standard interface for implementing variadic functions in C. Conceptually, va_list can be understood as a cursor over the unnamed arguments. The exact representation is ABI-dependent, so it is not always literally a plain pointer, but the mental model is still useful when reasoning about a small implementation.
va_start receives two inputs: the va_list object and the last named parameter. Once the implementation knows the position and size of the last fixed parameter, it can initialize the argument cursor so that it points to the first variadic argument.
va_arg also needs two pieces of information: the current argument cursor and the type to read. The type is necessary because the implementation must know how many bytes to fetch and how far to advance the cursor afterward.
There is no magic operation that suddenly discovers every argument. A variadic function must know what to read, and it usually gets that information from an explicit protocol.
How Does a Variadic Call Know When to Stop?
For sprintf, the protocol is the format string. The implementation scans the format string, recognizes conversion specifiers such as %d, and uses those specifiers to decide:
- whether it needs to read another variadic argument;
- what type that argument should be read as;
- how the value should be converted into text.
Without such a protocol, the callee cannot reliably know how many unnamed arguments were passed.
Converting Numbers for %d
In a small implementation, if there is no existing itoa, one option is to implement a specialized conversion helper. My version used an itoa_special-style function whose job was to insert the decimal representation of num into a target string at the current output position.
The output position is controlled by an iterator or cursor. The function must handle the important edge cases explicitly:
- negative numbers;
- zero;
- advancing and returning the updated cursor position.
This keeps number formatting separate from the main format-string scanning loop.
The Main sprintf Strategy
The second parameter of sprintf is const char *fmt because the implementation reads from the format string. It should not modify fmt; it should copy or convert characters into the output buffer.
A minimal strategy looks like this:
- scan
fmtcharacter by character; - copy ordinary characters directly into
str; - when a percent sign is found, enter a conversion-handling branch;
- for a known specifier such as
%d, read the next variadic argument and format it; - for unsupported noise such as
%m, copy the percent sign and following character as literal output or handle it according to the chosen policy; - carefully update both the input cursor and the output cursor.
Most bugs in this kind of implementation come from cursor movement: either reading too far from the format string or writing to the wrong position in the output buffer.
Default Argument Promotions
When reading character values from a variadic argument list, it is easy to run into a promotion warning or an outright error.
In C, arguments passed through ... undergo default argument promotions:
charandshortare promoted toint;floatis promoted todouble.
Therefore this is wrong:
1 | char tmp = va_arg(args, char); |
The value must be read as int and then cast back:
1 | char tmp = (char)va_arg(args, int); |
This rule matters because va_arg must be called with the promoted type. Otherwise the implementation reads the argument list with the wrong size and type expectation.
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !