C Variadic Functions
A variadic function is a C function that can receive a variable number of arguments. Functions such as printf use this feature so one call can print one value, while another call can print several values of different types.
Variadic functions are powerful, but they remove some of the compiler’s normal checking for the extra arguments. To use them safely, you must provide a way for the function to know how many arguments exist and what type each argument has.
Overview: How Variadic Functions Work
A normal C function has a fixed parameter list. If a function is declared as int add(int a, int b), every call must pass exactly two int arguments. A variadic function still has at least one fixed, named parameter, but it ends its parameter list with an ellipsis: .... The ellipsis means more arguments may follow at the call site.
The C standard library provides the tools for reading those unnamed arguments in <stdarg.h>. The main type is va_list, which stores the current position while walking through the extra arguments. You start it with va_start, read each value with va_arg, and clean it up with va_end.
Under the hood, the caller passes all arguments according to the platform’s calling convention. The called function has direct names only for the fixed parameters. The unnamed arguments are not an array, do not carry a length, and do not carry runtime type tags. The va_list machinery knows how to move from one argument slot to the next, but it relies on your code to request the correct type.
That is why every good variadic function has a contract. Some use a count, such as sum(4, 10, 20, 30, 40). Some use a sentinel value, such as a final NULL pointer. Format functions such as printf use a format string; each conversion specifier tells the function which type to read next. Without a contract like this, the function cannot know where the argument list ends.
Default argument promotions also matter. In the unnamed part of a variadic call, float is promoted to double, and small integer types such as char and short are promoted to int or unsigned int. Because of that, you do not read a variadic float with va_arg(args, float); you read it as double.
Syntax
This is the basic pattern for a variadic function that receives a count followed by that many int values.
#include <stdarg.h>
#include <stdio.h>
int sum_numbers(int count, ...)
{
va_list args;
int total = 0;
va_start(args, count);
for (int i = 0; i < count; i++) {
total += va_arg(args, int);
}
va_end(args);
return total;
}
int main(void)
{
printf("Total: %d\n", sum_numbers(4, 5, 10, 15, 20));
return 0;
}
Output:
Total: 50
| Part | Meaning |
|---|---|
#include <stdarg.h> |
Provides va_list, va_start, va_arg, va_end, and va_copy. |
int count, ... |
The function has one named parameter and then an optional variable argument list. |
va_start(args, count) |
Initializes args. The second argument must be the last named parameter before .... |
va_arg(args, int) |
Reads the next unnamed argument as an int and advances the list. |
va_end(args) |
Finishes use of the argument list. Call it once for each successful va_start or va_copy. |
Examples
Summing Any Number Of Integers
#include <stdarg.h>
#include <stdio.h>
int sum_numbers(int count, ...)
{
va_list args;
int total = 0;
va_start(args, count);
for (int i = 0; i < count; i++) {
total += va_arg(args, int);
}
va_end(args);
return total;
}
int main(void)
{
printf("Three values: %d\n", sum_numbers(3, 4, 8, 12));
printf("Five values: %d\n", sum_numbers(5, 1, 2, 3, 4, 5));
return 0;
}
Output:
Three values: 24
Five values: 15
The first parameter tells the function exactly how many extra arguments to read. Each loop iteration calls va_arg(args, int), so every unnamed argument must be an int after promotion. Passing fewer values than the count says, or passing a different type, makes the behavior undefined.
Averaging Double Values
#include <stdarg.h>
#include <stdio.h>
double average(int count, ...)
{
va_list args;
double total = 0.0;
va_start(args, count);
for (int i = 0; i < count; i++) {
total += va_arg(args, double);
}
va_end(args);
if (count == 0) {
return 0.0;
}
return total / count;
}
int main(void)
{
printf("Average: %.2f\n", average(3, 82.5, 91.0, 76.5));
printf("Empty: %.2f\n", average(0));
return 0;
}
Output:
Average: 83.33
Empty: 0.00
This example reads the unnamed values as double. A call may pass values written with decimal notation, and a float variable would also arrive as double because of default argument promotions. The function handles count == 0 explicitly to avoid division by zero.
A Small Format-Style Logger
#include <stdarg.h>
#include <stdio.h>
void log_message(const char *level, const char *format, ...)
{
va_list args;
printf("[%s] ", level);
va_start(args, format);
vprintf(format, args);
va_end(args);
printf("\n");
}
int main(void)
{
log_message("INFO", "Loaded %d records", 42);
log_message("WARN", "%s is %.1f%% full", "Disk", 91.5);
return 0;
}
Output:
[INFO] Loaded 42 records
[WARN] Disk is 91.5% full
The logger uses a format string as its contract, just like printf. It does not read each argument itself; instead, it forwards the initialized va_list to vprintf, the standard-library version of printf that accepts a va_list. This pattern is common when wrapping formatted output functions.
Finding A Maximum With A Required First Value
#include <stdarg.h>
#include <stdio.h>
int max_int(int first, int count_after_first, ...)
{
va_list args;
int best = first;
va_start(args, count_after_first);
for (int i = 0; i < count_after_first; i++) {
int value = va_arg(args, int);
if (value > best) {
best = value;
}
}
va_end(args);
return best;
}
int main(void)
{
printf("Max: %d\n", max_int(17, 4, 3, 28, 11, 22));
printf("Single: %d\n", max_int(9, 0));
return 0;
}
Output:
Max: 28
Single: 9
This design requires at least one real value, so the maximum is always well-defined. The second fixed parameter says how many values follow the first one. Notice that va_start uses count_after_first, because it is the last named parameter before the ellipsis.
How It Works Step By Step
- The caller evaluates and passes the fixed arguments and the unnamed arguments.
- The variadic function starts with its named parameters available like ordinary parameters.
- A
va_listvariable is declared to track the current position in the unnamed arguments. va_start(list, last_named_parameter)initializes that tracking state.- Each
va_arg(list, type)reads the next argument as the requested type and advances the state. - The function must stop based on its own contract: a count, a sentinel, or a format string.
va_end(list)releases any state associated with the traversal before the function returns.
A va_list is a one-way traversal object. After you read an argument, you normally cannot move backward. If you must scan the same arguments twice, use va_copy to create a second va_list, and call va_end on both lists when finished.
Common Mistakes
Reading The Wrong Type
The type passed to va_arg must match the type that actually arrived after default promotions. A common mistake is trying to read a float from the unnamed arguments. In a variadic call, float is promoted to double, so the corrected function reads double:
#include <stdarg.h>
#include <stdio.h>
void print_measurements(int count, ...)
{
va_list args;
va_start(args, count);
for (int i = 0; i < count; i++) {
double value = va_arg(args, double);
printf("%.1f\n", value);
}
va_end(args);
}
int main(void)
{
float first = 2.5f;
print_measurements(2, first, 7.0);
return 0;
}
Output:
2.5
7.0
The same idea applies to char and short: read them as int unless a wider unsigned type is required by the promotion rules. The function’s documentation should state exactly which argument types callers must pass.
Losing The End Condition
The ellipsis does not include a hidden length. If a function expects a sentinel, every caller must provide it. This corrected string printer stops when it reads a NULL pointer:
#include <stdarg.h>
#include <stdio.h>
void print_words(const char *first, ...)
{
va_list args;
const char *word = first;
va_start(args, first);
while (word != NULL) {
printf("%s\n", word);
word = va_arg(args, const char *);
}
va_end(args);
}
int main(void)
{
print_words("red", "green", "blue", NULL);
return 0;
}
Output:
red
green
blue
If the caller forgot the final NULL, the loop would continue reading memory that is not part of the intended argument list. For count-based functions, the equivalent mistake is giving a count that does not match the number of supplied arguments.
Forgetting va_end
Always pair each successful va_start or va_copy with va_end in the same function. On many systems va_end looks harmless, but portable C requires it because some implementations need cleanup for the argument traversal state.
Best Practices
- Use variadic functions only when a fixed-parameter function, array parameter, or struct would be awkward.
- Always include at least one named parameter before
...;va_startneeds the last named parameter. - Define a clear contract: count, sentinel, or format string.
- Document the expected types of unnamed arguments, including promotion rules.
- Read
floatvalues asdouble, and read promoted small integer types asintor the correct promoted type. - Never call
va_argmore times than the contract allows. - Call
va_endbefore every return afterva_start. - Prefer forwarding to
vprintf,vsnprintf, or similarv...functions when wrapping formatted output. - Consider passing an array plus a length when all extra values have the same type; it is often easier to check and test.
Practice Exercises
- Write
product(int count, ...)that multipliescountinteger values. Decide what it should return whencountis0. - Write
print_labels(const char *first, ...)that prints strings until it seesNULL. Test it with one label and with several labels. - Write a wrapper named
error_messagethat printsERROR:before a formatted message. Useva_listandvprintf.
Summary
- A variadic function uses
...to accept additional unnamed arguments. - The tools for reading those arguments are in
<stdarg.h>. va_startbegins traversal,va_argreads one value, andva_endfinishes traversal.- The function must have its own way to know how many arguments to read and what types to read.
- Default argument promotions affect the types you request with
va_arg. - Wrong counts, missing sentinels, wrong
va_argtypes, and missingva_endare the most important mistakes to avoid.
