5.1.29C Programming

Variadic functions — va_list, va_start, va_arg, va_end

2,077 words9 min readdifficulty · medium

WHY do we even need this?

The cost of this power: the compiler stops checking you. You alone decide how many args there are and what type each is. Promise wrong → undefined behavior.


WHAT are the four pieces?


HOW does it work, derived from first principles

When you call f(a, b, c, d), the arguments are placed somewhere the callee can reach (on many ABIs: the stack and/or registers, in a known order). The named parameters consume the first slots. The remaining bytes are still there but unnamed.


Figure — Variadic functions — va_list, va_start, va_arg, va_end

Worked Example 1 — sum of n integers (count method)

#include <stdarg.h>
#include <stdio.h>
 
int sum(int n, ...) {          // n tells us HOW MANY follow
    va_list ap;
    va_start(ap, n);           // cursor starts after 'n'
    long total = 0;
    for (int i = 0; i < n; i++)
        total += va_arg(ap, int);   // pull each as int
    va_end(ap);
    return (int)total;
}
 
int main(void){ printf("%d\n", sum(3, 10, 20, 30)); }  // 60
Step Code Why this step?
1 int sum(int n, ...) Need a named param n so va_start has an anchor and so we know the count.
2 va_start(ap, n) Point cursor just after n — at the first ... argument.
3 va_arg(ap, int) in loop Read & advance, exactly n times; type int matches what we passed.
4 va_end(ap) Required cleanup before returning.

Worked Example 2 — sentinel-terminated string concatenator

#include <stdarg.h>
#include <string.h>
 
size_t total_len(const char *first, ...) {
    va_list ap;
    va_start(ap, first);
    size_t len = strlen(first);
    const char *s;
    while ((s = va_arg(ap, const char*)) != NULL)  // stop at NULL
        len += strlen(s);
    va_end(ap);
    return len;
}
// total_len("ab", "cde", "f", NULL) == 6
Step Why this step?
first is named Gives the anchor; also it's natural here (a list always has a first item).
loop until NULL No count was passed, so a sentinel marks the end. Caller must pass NULL.
va_arg(ap, const char*) We promised pointers — type must match the args.

Worked Example 3 — re-scanning with va_copy

double average(int n, ...) {
    va_list ap, ap2;
    va_start(ap, n);
    va_copy(ap2, ap);          // duplicate cursor BEFORE consuming
 
    long sum = 0;
    for (int i = 0; i < n; i++) sum += va_arg(ap, int);
    va_end(ap);
 
    // ... could re-walk ap2 for a second pass ...
    va_end(ap2);
    return n ? (double)sum / n : 0.0;
}

Common Mistakes (Steel-man each)


Recall Feynman: explain to a 12-year-old

Imagine a conveyor belt carrying boxes, and you're standing at the start. You don't know how many boxes are coming. va_start says "go to the first box." va_arg(int) says "open the next box, expecting a number, take it, and step to the next box." But here's the catch: the belt doesn't tell you when it ends and the boxes aren't labeled with what's inside. So you must agree beforehand — either someone tells you "there are 3 boxes," or the last box is a special empty "STOP" box. If you guess the count or the contents wrong, you grab thin air. va_end is putting your hands down when you're done.


Active Recall

What header declares the variadic macros?
<stdarg.h>
What is va_list?
An opaque type holding the current position (cursor) in the argument list.
What does va_start(ap, last) do?
Initializes ap to point just after the last named parameter last.
What does va_arg(ap, type) do?
Returns the next argument interpreted as type, and advances the cursor.
Why must you call va_end?
It performs required cleanup of the va_list before the function returns.
Why must a variadic function have ≥1 named parameter?
va_start needs the name of the last named parameter as its anchor point.
Two ways to know how many variadic args there are?
A count argument, or a sentinel terminator value (e.g. NULL/-1).
What promotion happens to float arguments?
They are promoted to double, so use va_arg(ap, double).
What promotion happens to char/short arguments?
They are promoted to int, so use va_arg(ap, int).
What is va_copy(dst, src) for?
Portably duplicating a va_list so the arguments can be scanned more than once.
Why does mismatching va_arg type cause UB?
The bytes are interpreted as the wrong type / wrong size, reading garbage memory.
Can you rewind a va_list?
No — it's effectively single-pass; copy it with va_copy first if you need a second pass.

Connections

Concept Map

motivates

uses

requires

includes

passed to

initializes

read by

advances

closed by

needs stop rule

cost

type mismatch causes

C99 duplicate

Unknown arg count at runtime

Variadic function

stdarg.h macros

≥1 named parameter

va_list cursor

va_start ap last

va_arg ap type

va_end ap

Count or sentinel

Compiler stops checking

Undefined behavior

va_copy

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Dekho, variadic function matlab aisa function jiska argument count fixed nahi hota — jaise printf, jisme tum 1 ya 10 values pass kar sakte ho. Normal function me compiler har argument ke liye naam jaanta hai, par yahan ... (ellipsis) ke baad jitne bhi arguments aaye, unka koi naam nahi hota. Toh C tumhe <stdarg.h> ke 4 magic tools deta hai: va_list (ek cursor/pointer jaisa), va_start (cursor ko shuru karo, last named parameter ke baad), va_arg (agla argument nikalo, type batake), aur va_end (kaam khatam, cleanup).

Sabse important baat: compiler ab tumhari checking nahi karta. Tum khud decide karte ho kitne arguments hain aur har ek ka type kya hai. Isliye tumhe ya toh ek count pass karna padta hai (jaise sum(3, ...) me 3), ya ek sentinel rakhna padta hai (jaise list ke end me NULL). Galat count ya galat type diya — toh UB (undefined behavior), program crash ya garbage value.

Ek bada trap yaad rakho: default argument promotions. Jab tum char ya short pass karte ho, woh automatically int ban jaata hai, aur float ban jaata hai double. Isliye chahe tumne float diya ho, hamesha va_arg(ap, double) likho, aur chote integers ke liye va_arg(ap, int). Agar tum va_arg(ap, float) likhoge toh galat bytes read honge.

Yaad rakhne ka tareeka: S-A-E — Start, Arg (baar baar), End. Aur ek named parameter zaroori hai kyunki va_start ko anchor chahiye. Agar dobaara list scan karni ho toh va_copy use karo, kyunki ek va_list ko rewind nahi kar sakte. Bas itna samajh lo, toh printf ka andar ka jaadu clear ho jaayega!

Go deeper — visual, from zero

Test yourself — C Programming

Connections