C Variadic Functions with stdarg.h: Work with a Variable Number of Arguments

A variadic function is a C function that can accept a variable number of arguments. The standard header stdarg.h provides the tools needed to access those additional arguments safely according to the function's interface.

What Is a Variadic Function?

A normal C function has a fixed number of parameters. A variadic function has at least one named parameter followed by an ellipsis (...), allowing the caller to provide additional arguments.

C
#include <stdarg.h>

void printNumbers(int count, ...)
{
    /* Access variable arguments here. */
}

The named parameter is often used to tell the function how many additional arguments exist or what types should be expected.

The stdarg.h Header

The standard header stdarg.h defines the macros and type used to access variable arguments.

FeaturePurpose
va_listType used to represent the state needed to access variable arguments
va_startInitializes a va_list for accessing arguments
va_argRetrieves the next argument of a specified type
va_endFinishes use of a va_list
va_copyCreates a copy of an existing va_list

A Simple Variadic Function

A straightforward example is a function that receives a count followed by that many integers.

C
#include <stdarg.h>
#include <stdio.h>

void printNumbers(int count, ...)
{
    va_list args;
    va_start(args, count);

    for (int i = 0; i < count; i++)
    {
        int value = va_arg(args, int);
        printf("%d\n", value);
    }

    va_end(args);
}

int main(void)
{
    printNumbers(4, 10, 20, 30, 40);
    return 0;
}

Understanding va_list

va_list is an implementation-defined type used to keep track of the current position while traversing a function's variable arguments.

You should not assume that va_list is a pointer. It may be a pointer, an array type, a structure, or another implementation-specific representation.

Using va_start

va_start initializes a va_list before the function begins retrieving variable arguments.

C
void example(int count, ...)
{
    va_list args;

    va_start(args, count);

    /* Use va_arg here. */

    va_end(args);
}

The second argument to va_start is the last named parameter before the ellipsis.

Using va_arg

va_arg retrieves the next variable argument and interprets it as the type specified by its second argument.

C
int value = va_arg(args, int);
double number = va_arg(args, double);
const char *text = va_arg(args, const char *);

The Caller and Callee Must Agree on Types

C does not automatically provide runtime type information for arguments passed through the ellipsis. The function must know what type to retrieve.

C
void printValue(int count, ...)
{
    va_list args;
    va_start(args, count);

    int value = va_arg(args, int);

    va_end(args);

    printf("%d\n", value);
}

Calling this function with a value of an incompatible type and then retrieving it using the wrong type can result in undefined behavior. The interface must define the expected argument types.

Using va_end

Every va_list initialized with va_start or va_copy should eventually be passed to va_end before it is reused or the function returns.

C
va_list args;
va_start(args, count);

/* Process arguments. */

va_end(args);

Summing Variable Arguments

C
#include <stdarg.h>
#include <stdio.h>

int sum(int count, ...)
{
    int total = 0;
    va_list args;

    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("%d\n", sum(5, 1, 2, 3, 4, 5));
    return 0;
}

Using a Sentinel Value

Instead of passing a count, a variadic function can sometimes use a special sentinel value to mark the end of the argument list.

C
#include <stdarg.h>
#include <stdio.h>

void printStrings(const char *first, ...)
{
    va_list args;
    const char *text = first;

    va_start(args, first);

    while (text != NULL)
    {
        printf("%s\n", text);
        text = va_arg(args, const char *);
    }

    va_end(args);
}

int main(void)
{
    printStrings("C", "C++", "Rust", "Python", NULL);
    return 0;
}

A sentinel must have a value that can be represented unambiguously by the expected argument type. Pointer-based lists commonly use NULL as a terminator.

Why NULL Can Be Tricky

When using variadic arguments, integer and pointer representations can differ. For pointer sentinel interfaces, it is safer to use a correctly typed null pointer such as (const char *)NULL when necessary rather than relying on an untyped integer zero.

Variadic Functions and Default Argument Promotions

Arguments passed through the ellipsis undergo the default argument promotions. In particular, float is promoted to double, and integer types narrower than int are promoted according to the integer promotions.

C
#include <stdarg.h>
#include <stdio.h>

void showValue(int type, ...)
{
    va_list args;
    va_start(args, type);

    if (type == 1)
    {
        double value = va_arg(args, double);
        printf("%f\n", value);
    }

    va_end(args);
}

int main(void)
{
    float value = 3.5f;
    showValue(1, value);

    return 0;
}

The float argument is passed as a double, so retrieving it as float would be incorrect.

Integer Promotions

Types such as char and short are subject to integer promotions when passed through the ellipsis. This means they are commonly retrieved as int rather than as char or short.

C
#include <stdarg.h>
#include <stdio.h>

void showChar(char marker, ...)
{
    va_list args;
    va_start(args, marker);

    int value = va_arg(args, int);
    printf("%d\n", value);

    va_end(args);
}

int main(void)
{
    char letter = 'A';
    showChar('X', letter);

    return 0;
}

Using va_copy

If a function needs to traverse the same variable argument list more than once, va_copy can be used to create another va_list with an equivalent traversal state.

C
#include <stdarg.h>
#include <stdio.h>

void inspectArguments(int count, ...)
{
    va_list first;
    va_list second;

    va_start(first, count);
    va_copy(second, first);

    for (int i = 0; i < count; i++)
    {
        printf("first: %d\n", va_arg(first, int));
    }

    for (int i = 0; i < count; i++)
    {
        printf("second: %d\n", va_arg(second, int));
    }

    va_end(second);
    va_end(first);
}

Do not assume that assigning one va_list directly to another creates a usable independent traversal. Use va_copy when an independent copy is required.

A Variadic Average Function

C
#include <stdarg.h>
#include <stdio.h>

double average(int count, ...)
{
    if (count <= 0)
    {
        return 0.0;
    }

    va_list args;
    va_start(args, count);

    double total = 0.0;

    for (int i = 0; i < count; i++)
    {
        total += va_arg(args, double);
    }

    va_end(args);

    return total / count;
}

int main(void)
{
    printf("%.2f\n", average(4, 10.0, 20.0, 30.0, 40.0));
    return 0;
}

Variadic Functions with Different Types

A variadic interface can support different argument types when the caller supplies explicit type information.

C
#include <stdarg.h>
#include <stdio.h>

void printValues(int count, ...)
{
    va_list args;
    va_start(args, count);

    for (int i = 0; i < count; i++)
    {
        int type = va_arg(args, int);

        switch (type)
        {
            case 1:
            {
                int value = va_arg(args, int);
                printf("int: %d\n", value);
                break;
            }

            case 2:
            {
                double value = va_arg(args, double);
                printf("double: %.2f\n", value);
                break;
            }

            case 3:
            {
                const char *value = va_arg(args, const char *);
                printf("string: %s\n", value);
                break;
            }
        }
    }

    va_end(args);
}

int main(void)
{
    printValues(
        3,
        1, 42,
        2, 3.14,
        3, "Hello"
    );

    return 0;
}

Format Strings Are a Common Variadic Interface

The printf family is one of the most familiar examples of variadic functions. The format string describes how subsequent arguments should be interpreted.

C
printf("Name: %s, Age: %d, Score: %.2f\n",
       "Alice", 25, 98.5);

The format string acts as a form of runtime type description. A mismatch between a conversion specification and the supplied argument can produce undefined behavior.

Building a Simple Logger

C
#include <stdarg.h>
#include <stdio.h>

void logMessage(const char *format, ...)
{
    va_list args;
    va_start(args, format);

    vprintf(format, args);

    va_end(args);
}

int main(void)
{
    logMessage("User %s has score %d\n", "Alice", 95);
    return 0;
}

The vprintf family is specifically designed to accept a va_list, making it useful when creating wrappers around printf-style interfaces.

The vprintf Family

FunctionPurpose
vprintfFormatted output to stdout using va_list
vfprintfFormatted output to a FILE stream using va_list
vsnprintfFormatted output into a character buffer using va_list

A Safe String Formatting Wrapper

C
#include <stdarg.h>
#include <stdio.h>

int formatMessage(char *buffer, size_t size,
                  const char *format, ...)
{
    va_list args;
    va_start(args, format);

    int result = vsnprintf(buffer, size, format, args);

    va_end(args);

    return result;
}

int main(void)
{
    char buffer[100];

    formatMessage(buffer, sizeof buffer,
                  "User: %s, Score: %d", "Alice", 95);

    printf("%s\n", buffer);
    return 0;
}

A Variadic Function Cannot Discover Argument Count Automatically

The ellipsis does not provide a built-in count or type list. A variadic API therefore needs some way to determine where the arguments end and how each argument should be interpreted.

  • A count parameter
  • A sentinel value
  • A format string
  • Explicit type tags
  • A separate structure describing the arguments

Variadic Functions and Type Safety

Variadic arguments are less type-safe than ordinary function parameters because the compiler cannot generally verify the complete argument list against the function declaration.

For example, a function expecting an int followed by a double cannot automatically reject a caller that supplies incompatible arguments through the ellipsis. Good API design should make the expected argument types clear and, where possible, use compiler-supported format checking.

Common Mistakes

  • Calling va_arg with the wrong type
  • Forgetting va_end
  • Trying to determine the argument count automatically
  • Ignoring default argument promotions
  • Using an invalid sentinel value
  • Assuming va_list can always be copied with simple assignment
  • Using va_list after va_end
  • Reusing a va_list without reinitializing or copying it correctly

Best Practices

  • Always document the expected variable argument types
  • Use a count, sentinel, format string, or explicit type information to define the argument list
  • Remember that float becomes double through default argument promotions
  • Retrieve promoted integer types correctly
  • Call va_end for every initialized va_list
  • Use va_copy when an independent traversal is required
  • Prefer fixed-parameter APIs when a variadic interface does not provide a clear benefit
  • Use vprintf, vfprintf, or vsnprintf when wrapping printf-style functions

When Should You Use Variadic Functions?

  • Logging APIs
  • Formatted output functions
  • Flexible mathematical helpers
  • Functions accepting a variable number of similar values
  • APIs where a count or format string naturally describes the arguments

When Should You Avoid Them?

  • When a fixed parameter list is simple and sufficient
  • When strong compile-time type checking is important
  • When the argument types cannot be reliably described
  • When a structure or array would provide a clearer interface

Practice Exercises

  • Write a variadic function that calculates the sum of integers
  • Create a function that prints strings until a NULL sentinel
  • Build an average function for double values
  • Create a tagged variadic function that accepts int, double, and string values
  • Write a logging wrapper around vfprintf
  • Use va_copy to process the same argument list twice
  • Create a printf-style function using vsnprintf

Conclusion

Variadic functions give C programmers a flexible way to design APIs that accept a variable number of arguments. The stdarg.h facilities va_list, va_start, va_arg, va_end, and va_copy provide the core mechanism for traversing those arguments. Because the compiler has limited information about values passed through the ellipsis, careful type matching and a well-defined argument convention are essential.

Note: Note: Variadic argument handling depends on the C language's default argument promotions and the exact type expected by va_arg. Supplying or retrieving incompatible types can result in undefined behavior.