C Variable Arguments — Variadic Functions with stdarg.h Explained
In this tutorial, you will learn about C Variable Arguments. We cover key concepts, practical examples, and best practices to help you master this topic.
C variable arguments allow functions to accept a varying number of arguments using the stdarg.h macros (va_list, va_start, va_arg, va_end), enabling flexible interfaces like printf and scanf.
What You Will Learn
- The stdarg.h interface: va_list, va_start, va_arg, va_end
- Writing variadic functions with a fixed leading parameter
- Type safety challenges and how to address them
- Implementing a simple printf-like formatter
- The vprintf family of functions for forwarding variadic arguments
- Common pitfalls with undefined behavior
Why It Matters
Variadic functions are essential for logging, formatting, error reporting, and generic interfaces. Every C program that uses printf, scanf, or fprintf relies on variadic arguments. Understanding how they work lets you write your own logging frameworks, assertion helpers, and format converters. In Durga Antivirus Pro, a custom log_error variadic function formats error messages with file, line, and severity level before writing them to the audit log.
Real-World Use
A logging library needs to accept a format string and any number of additional arguments, just like printf. The logging function prefixes the message with a timestamp and severity level (INFO, WARN, ERROR), then forwards the formatted message to a file. This cannot be done without variadic arguments because the number of log values changes with every call.
Learning Path
flowchart LR A[Recursion] --> B[Variable Arguments\nYou are here] B --> C[Inline Functions] style B fill:#f90,color:#fff
Basics of stdarg.h
A variadic function requires at least one fixed parameter. The ... in the parameter list marks where variable arguments begin.
#include <stdio.h>
#include <stdarg.h>
// Sum a variable number of integers
// The count parameter tells the function how many arguments follow
int sum(int count, ...) {
va_list args;
int total = 0;
// Initialize va_list to point to the first variable argument
va_start(args, count);
for (int i = 0; i < count; i++) {
total += va_arg(args, int);
}
// Clean up
va_end(args);
return total;
}
int main() {
printf("Sum of 3, 5, 7: %d\n", sum(3, 3, 5, 7));
printf("Sum of 10, 20: %d\n", sum(2, 10, 20));
printf("Sum of 1 number: %d\n", sum(1, 42));
return 0;
}
Output:
Sum of 3, 5, 7: 15
Sum of 10, 20: 30
Sum of 1 number: 42
The Four Macros
The stdarg.h interface has four macros:
va_list: A type that holds information needed to retrieve variable arguments. Think of it as a cursor pointing to the current argument.va_start(ap, last_fixed): Initializesapso it points to the first variable argument.last_fixedis the name of the last fixed parameter before the....va_arg(ap, type): Retrieves the current argument as the specified type and advancesapto the next argument. The type must match what was actually passed.va_end(ap): Cleans up theva_list. Must be called after processing arguments. On most platforms it does nothing, but failing to call it is undefined behavior.
Building a Logger
A practical variadic logging function:
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
typedef enum { INFO, WARN, ERROR } LogLevel;
const char* level_name(LogLevel level) {
switch (level) {
case INFO: return "INFO";
case WARN: return "WARN";
case ERROR: return "ERROR";
default: return "UNKNOWN";
}
}
void log_message(LogLevel level, const char *format, ...) {
time_t now = time(NULL);
struct tm *tm_info = localtime(&now);
char timestamp[20];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info);
printf("[%s] [%s] ", timestamp, level_name(level));
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
printf("\n");
}
int main() {
log_message(INFO, "System initialized on port %d", 8080);
log_message(WARN, "Disk usage at %.1f%%", 85.3);
log_message(ERROR, "Failed to open file: %s", "config.cfg");
return 0;
}
Output:
[2026-06-28 10:30:00] [INFO] System initialized on port 8080
[2026-06-28 10:30:00] [WARN] Disk usage at 85.3%
[2026-06-28 10:30:00] [ERROR] Failed to open file: config.cfg
The vprintf function takes a va_list directly, letting you forward variadic arguments to printf without re-Parsing them.
Type Safety Challenges
The biggest danger of variadic functions is type unsafety. The compiler does not check that the types of variable arguments match what the function expects.
#include <stdio.h>
#include <stdarg.h>
void print_values(int count, ...) {
va_list args;
va_start(args, count);
for (int i = 0; i < count; i++) {
double val = va_arg(args, double); // WARNING: expects double
printf("Value %d: %f\n", i, val);
}
va_end(args);
}
int main() {
// BUG: passing int where double is expected
print_values(2, 10, 20); // Undefined behavior!
return 0;
}
If the caller passes int but the function reads double, the behavior is undefined. The function reads garbage bits. Always ensure the type you pass in va_arg matches the actual argument type after default argument promotions (float becomes double, char/short become int).
Finding the Minimum of Variable Arguments
#include <stdio.h>
#include <stdarg.h>
#include <limits.h>
int min(int count, ...) {
va_list args;
va_start(args, count);
int minimum = INT_MAX;
for (int i = 0; i < count; i++) {
int val = va_arg(args, int);
if (val < minimum) {
minimum = val;
}
}
va_end(args);
return minimum;
}
int main() {
printf("min(42, 17, 8, 99, 3) = %d\n", min(5, 42, 17, 8, 99, 3));
printf("min(-5, 0, 100) = %d\n", min(3, -5, 0, 100));
printf("min(1) = %d\n", min(1, 999));
return 0;
}
Output:
min(42, 17, 8, 99, 3) = 3
min(-5, 0, 100) = -5
min(1) = 999
Forwarding Variadic Arguments
Sometimes you need to receive variadic arguments and pass them to another variadic function. Use the v... versions of printf functions:
#include <stdio.h>
#include <stdarg.h>
void debug_log(const char *file, int line, const char *fmt, ...) {
fprintf(stderr, "[DEBUG] %s:%d: ", file, line);
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
fprintf(stderr, "\n");
}
#define LOG_DEBUG(fmt, ...) \
debug_log(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
int main() {
int status = -1;
LOG_DEBUG("Connection failed with status %d", status);
return 0;
}
Output:
[DEBUG] log_example.c:25: Connection failed with status -1
The ##__VA_ARGS__ extension in GCC/Clang removes the trailing comma when no variable arguments are passed, allowing LOG_DEBUG("hello") to work without extra arguments.
Common Mistakes
Calling va_arg with the wrong type: The type in
va_arg(args, type)must exactly match what was passed (after promotion). Reading anintthat was passed asdoubleproduces garbage.Forgetting to call va_end: While va_end often does nothing, failing to call it is undefined behavior. Always pair va_start with va_end in the same function scope.
Using va_arg without checking the count: If the caller passes fewer arguments than you read, you read past the valid arguments into undefined memory. Always know how many arguments to expect.
Passing float to variadic functions:
floatis promoted todoublein variadic argument lists. Always read withva_arg(args, double), neverva_arg(args, float).Nested va_list traversal: You cannot traverse the same va_list twice without reinitializing. Use
va_copyto create a copy if you need to Process arguments in multiple passes.Using va_list after va_end: Once you call va_end, the va_list is invalid. Reinitialize with va_start before using it again.
Forgetting the required fixed parameter: A variadic function must have at least one named parameter before the
....void bad(...)is not valid C.
Practice Questions
- Why must a variadic function have at least one fixed parameter?
- What happens if you read
va_arg(args, double)when the argument is actually an int? - How does the
vprintffamily of functions differ fromprintf? - Why does
va_arg(args, float)produce undefined behavior? - Challenge: Write a function
void print_csv(FILE *stream, int count, ...)that prints the variable arguments as comma-separated values. Support int, double, and string types by using a format string like "ids,values,names".
Mini Project
Build a unit test assertion framework using variadic macros:
- Define
TEST_ASSERT(condition, fmt, ...)that prints the file and line number, the formatted message, and the condition if it fails - Define
TEST_EQUAL(a, b, fmt, ...)that checks equality and prints both values on failure - Define
RUN_TEST(name)that prints "PASS" or "FAIL" with timing - Write three test cases: one that passes, one that fails, and one that tests string equality
- The framework should count total tests, passes, and failures, and print a summary
FAQ
What is Next
Proceed to Inline Functions to learn how to eliminate function call overhead with inline expansion. Then explore Function Pointers for callback-based programming patterns.