Skip to content

C Variable Arguments — Variadic Functions with stdarg.h Explained

DodaTech Updated 2026-06-28 8 min read

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): Initializes ap so it points to the first variable argument. last_fixed is the name of the last fixed parameter before the ....
  • va_arg(ap, type): Retrieves the current argument as the specified type and advances ap to the next argument. The type must match what was actually passed.
  • va_end(ap): Cleans up the va_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

  1. Calling va_arg with the wrong type: The type in va_arg(args, type) must exactly match what was passed (after promotion). Reading an int that was passed as double produces garbage.

  2. 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.

  3. 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.

  4. Passing float to variadic functions: float is promoted to double in variadic argument lists. Always read with va_arg(args, double), never va_arg(args, float).

  5. Nested va_list traversal: You cannot traverse the same va_list twice without reinitializing. Use va_copy to create a copy if you need to Process arguments in multiple passes.

  6. Using va_list after va_end: Once you call va_end, the va_list is invalid. Reinitialize with va_start before using it again.

  7. 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

  1. Why must a variadic function have at least one fixed parameter?
  2. What happens if you read va_arg(args, double) when the argument is actually an int?
  3. How does the vprintf family of functions differ from printf?
  4. Why does va_arg(args, float) produce undefined behavior?
  5. 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

Can I use variable arguments in a macro?

Yes, variadic macros (C99+) use VA_ARGS. Example: #define LOG(fmt, ...) printf(fmt, ##VA_ARGS). The ## removes the trailing comma when no arguments are passed.

How does printf know how many arguments there are?

printf counts format specifiers (%d, %s, etc.) in the format string. Mismatches between specifiers and arguments cause undefined behavior.

Can I pass an array to a variadic function?

Yes, but the array decays to a pointer. You must either pass the length separately or use a sentinel value to mark the end.

What is the maximum number of variable arguments?

The C standard does not specify a maximum. In practice, limits depend on stack size and ABI constraints. Several hundred are usually safe.

Is there any type safety for variadic functions in C?

The _Generic keyword (C11) can dispatch to different functions based on argument type, providing a limited form of type safety. Some compilers also provide format string checking with attribute((format(printf,...))).

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.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse C