1. High-Level Concept

Normally, a C function has a fixed list of arguments. However, sometimes you need flexibility, such as a printing function that takes a format string and any number of variables to print. To do this, the function must rely on:

  1. A named argument: You need at least one fixed argument at the start (often a format string) to establish a reference point.
  2. The Ellipsis (...): This appears at the end of the argument list in the function declaration to indicate “more arguments follow.”
  3. Macros: Special tools to “walk” through the list of unnamed arguments.

2. The Mechanics: <stdarg.h>

To process these arguments, you use the type va_list and three macros defined in <stdarg.h>:

  • va_list: A special data type used to declare a variable (often named ap for “argument pointer”) that moves through the arguments.
  • va_start(ap, lastarg): Initializes the pointer. You must pass it your va_list variable and the name of the last named argument in your function declaration.
  • va_arg(ap, type): Returns the next argument. You must specify the type (like int or double) you expect effectively casting the raw data found at the pointer.
  • va_end(ap): Cleans up everything before the function returns.
3. Textbook Example: minprintf

The textbook provides a simplified version of printf, called minprintf, to demonstrate this. It processes a format string and prints arguments based on simple codes (%d, %f, %s).

Here is the code breakdown:

#include <stdarg.h>
#include <stdio.h>
 
/* minprintf: minimal printf with variable argument list */
void minprintf(char *fmt, ...)
{
    va_list ap; /* 1. Declare the argument pointer */
    char *p, *sval;
    int ival;
    double dval;
 
    va_start(ap, fmt); /* 2. Initialize ap to point to the first unnamed arg */
 
    /* Loop through the format string */
    for (p = fmt; *p; p++) {
        if (*p != '%') {
            putchar(*p);
            continue;
        }
        
        /* Switch on the character after '%' */
        switch (*++p) {
        case 'd':
            /* 3. Fetch the next argument assuming it is an int */
            ival = va_arg(ap, int);
            printf("%d", ival);
            break;
        case 'f':
            /* Fetch the next argument assuming it is a double */
            dval = va_arg(ap, double);
            printf("%f", dval);
            break;
        case 's':
            /* Fetch the next argument assuming it is a char pointer */
            for (sval = va_arg(ap, char *); *sval; sval++)
                putchar(*sval);
            break;
        default:
            putchar(*p);
            break;
        }
    }
    
    va_end(ap); /* 4. Clean up */
}
4. Key Details
  • Type Safety: The compiler cannot check the types of the unnamed arguments. If you pass a char * but use va_arg(ap, int), you will likely get garbage data or a crash. The function relies entirely on the logic you write (like parsing the % codes) to know what type to grab next.
  • Default Promotions: In variable argument lists, float values are automatically promoted to double, and char or short are promoted to int. This is why the example uses va_arg(ap, double) even if you might be thinking of a float, and va_arg(ap, int) for integers.
  • Initialization: You must call va_start before using va_arg, and you must call va_end before returning.

Practice Question

To test your understanding:

If you were writing a function sum(int count, …) that sums up a list of integers, how would the loop using va_arg look? (Hint: You know the number of arguments because of the count parameter).