Intuition The one core idea
A variadic function is a function that agrees to receive a pile of extra values it was never told the size of , and then reads them back one at a time with a moving cursor. Everything on this page exists to make one thing safe: walking a region of arguments whose length and contents nobody wrote down for the compiler.
Before you can read the parent note, you must own every word it leans on. This page builds them from zero — plain words first, then the picture, then why the topic needs it . Nothing here assumes you have written C before.
Definition Argument vs. parameter
A parameter is a name inside a function's header: the a in int square(int a).
An argument is the actual value you hand over when you call it: the 5 in square(5).
Picture: the parameter is a labelled cup on the counter; the argument is the water you pour into it.
The parent note keeps saying "named parameter" and "the extra arguments". The whole trick is that some arguments have cups (parameters) waiting for them, and some do not. Hold that image — everything below is about the argument that arrives with no cup .
last and ap — two names you will see everywhere
These two identifiers appear in every code sample; pin them down now.
last is whatever you named the last named parameter — in int sum(int n, ...) the value of last is simply n. It is not a keyword; it is a stand-in for "the final cup before the ...."
ap is the name conventionally given to the cursor variable — you write va_list ap; to declare it. "ap" just abbreviates a rgument p ointer; you could call it anything.
Look at the figure. The named parameter n (this is our last) gets a labelled cup. The three extra values 10, 20, 30 land on the counter with no label — that unlabelled region is the entire problem the <stdarg.h> toolkit solves.
Definition The call stack
When you call a function, the machine reserves a strip of memory — the stack — and lays the arguments into it in a known order. It is just a row of bytes at consecutive addresses.
row matters
Because the arguments sit in a predictable line , you can start at one end and step along it. "Step along" is the seed of everything: va_arg is nothing but take what's here, then move to the next slot.
(Some machines grow the stack toward higher addresses, some toward lower — that direction detail does not matter to you, because the <stdarg.h> macros already know it for your platform. You only ever move forward through the arguments .)
Read more in Calling Conventions and the Stack — but for this page you only need: arguments live in a line the macros can walk.
Common mistake "So the arguments are always on the stack?"
Feels right: the picture above shows them in a memory row, so surely that's where they live.
Truth: on many modern systems (e.g. the x86-64 SysV convention) the first several arguments are passed in CPU registers , not memory at all — only later ones spill onto the stack. So there is no single flat char* array to walk.
Fix: this is exactly why va_list exists. It abstracts over "register area + stack area" so your code never has to know which arguments came from where. See Calling Conventions and the Stack .
The red cursor in the figure is a pointer . That's the next symbol we must define.
A pointer is a variable whose value is a memory address — a house number, not the person living there. Written char *p, it means "p holds the address of a char."
Definition Two operations you must recognise
&x — the address-of operator: "give me the house number of x."
*p — the dereference operator: "go to house p and read who lives there."
Picture: &x points a finger at a box. *p reaches into the box p points at and pulls out its contents.
va_list is really just a char*?"
Feels right: everything above modelled it as one pointer.
Truth: that flat model is a teaching cartoon . On real platforms va_list is an opaque, implementation-specific object — often a small struct (or a one-element array of a struct) tracking both a register-save area and a stack pointer, plus counters of how many register slots are left. It is deliberately not a plain char*.
Fix: treat va_list as a black box you touch only through the macros — never assume its size or copy it with =.
sizeof(T)
sizeof(T) gives the number of bytes a value of type T occupies. sizeof(int) is commonly 4, sizeof(double) is 8.
Intuition The stride of the cursor
After reading an int, the cursor must advance to land exactly on the next value. Advance too little and you read half of the next thing; too much and you skip data. sizeof gives the width the macro must account for.
In the figure, each box is a different width because each type has a different sizeof. This is why va_arg needs you to name the type — the type tells it how wide the current box is .
ap += sizeof(T) is the whole story?"
Feels right: widths add up; just keep summing them.
Truth: real ABIs require alignment and padding — a double may have to sit at an address that is a multiple of 8, so the compiler inserts unused padding bytes between arguments. A naive char* increment by sizeof(T) would land inside the padding and misread everything after it.
Fix: never hand-roll the cursor. The standard macros know each platform's alignment rules and skip the padding for you — that is a core reason the mechanism is hidden behind macros. See Undefined Behavior in C .
A type is a label saying how many bytes a value uses and how to interpret those bytes (as a whole number, a decimal, an address...). The same bits mean different things read as int vs double.
Common mistake Why the wrong type is catastrophic here
Feels right: "I passed a double, I'll read a double... or was it an int? Whatever, they're numbers."
Truth: int and double have different sizes and different bit-layouts. Read 8 bytes as an int and you grab garbage and leave the cursor mis-positioned for every later read.
Fix: the type in va_arg(ap, type) must be the exact type actually sitting there. See Undefined Behavior in C .
Definition Default argument promotions
When a value is passed through ... (the variadic part), C automatically widens small types before they are handed over:
char, short → int
float → double
So there is no such thing as a char or float sitting in the variadic pile — it was already fattened.
Intuition Why this rule even exists
Old hardware moved everything in machine-word chunks; passing a lone byte was wasteful and inconsistent. Promotion makes every small value a uniform, predictable width — which is exactly what a blind cursor needs. See Default Argument Promotions .
This single rule is the source of the parent's biggest "Mistake" — you now understand why it happens.
A macro (#define NAME ...) is a text substitution the preprocessor performs before compiling — the compiler literally sees the replacement text. va_start, va_arg, va_end, va_copy are macros, not ordinary functions.
Intuition Why macros, not functions
va_arg(ap, int) takes a type name (int) as an argument. Ordinary C functions cannot accept a type as a parameter — only macros can splice a type into code. That is the deep reason the toolkit must be macros. They also need to modify ap in place, which a by-value function copy couldn't do reliably.
...
In a header like int sum(int n, ...), the three dots ... mean "zero or more additional arguments of unknown type follow. " The compiler stops type-checking anything after them.
va_list is called opaque because you are told what it does (holds the cursor) but never what it is made of internally (recall: it may be a struct spanning registers and stack). You may only touch it through the macros — never peek inside, never copy with =.
va_copy(dst, src) — the only safe duplicate
Because you may not copy a va_list with plain = (it might be an array/struct with internal state), C99 gives va_copy(dst, src): it makes dst an independent cursor sitting at the same position as src.
Why you'd want it: a va_list can be single-use — once you have walked it you cannot rewind. To read the arguments twice (say, one pass to find the max, another to sum), copy the cursor before consuming, then walk the copy separately. Each copy needs its own va_end.
va_copy in one glance
va_list ap, ap2;
va_start (ap, n);
va_copy (ap2, ap); // ap2 now sits where ap sits, independently
/* ...consume ap... */ va_end (ap);
/* ...consume ap2 for a second pass... */ va_end (ap2);
S–A–E : va_**S**tart → va_**A**rg (repeat) → va_**E**nd. And va_**copy** when you must walk the list twice (its copy gets its own va_end).
The Stack: args in a line
Pointer: address plus deref
Types: how to read the bytes
Default Argument Promotions
Registers plus stack: va_list abstracts both
Every arrow says "you need the box on the left before the box on the right makes sense." All roads end at Variadic Functions — the parent topic . Related deeper reading: C Standard Library , Function Pointers in C , Format String Vulnerabilities .
Test yourself — cover the right side.
An argument is... the actual value passed at the call site (the water), while a parameter is the named cup waiting for it.
In va_start(ap, last), last refers to... whatever you named the last named parameter before the ... (e.g. n in int sum(int n, ...)).
ap in the code samples is...the cursor variable you declared with va_list ap; — "ap" just abbreviates argument pointer; the name is your choice.
The stack gives us... a predictable line of memory holding (some of) the arguments, which lets a cursor step through them in order.
Are all variadic arguments always on the stack? No — many ABIs pass the first few in CPU registers; va_list abstracts over register area plus stack so you never care which.
A pointer holds... a memory address (a house number), not the value itself.
&x means...address-of: the location of x.
*p means...dereference: read the value stored at the address p holds.
sizeof(T) tells the cursor...how many bytes wide the current value is — though real ABIs also add alignment padding the macros must skip.
Why can't you hand-roll ap += sizeof(T)? alignment and padding rules differ per platform; a naive increment lands in padding and misreads. Let the macros handle it.
A type in va_arg is the promise that... this box is that many bytes and should be interpreted this way.
Default argument promotions turn char/short into...int, and float into double.
So you must never write va_arg(ap, float) but instead... va_arg(ap, double).
va_start, va_arg, va_end, va_copy are macros because...only macros can accept a type name as an argument and modify ap in place.
... (ellipsis) means...zero or more additional arguments of unknown type follow, un-type-checked.
va_list is opaque , so you may only...touch it through the macros — never inspect its size or =-copy it (use va_copy).
va_copy(dst, src) exists because...a va_list may be single-use and non-copyable with =; va_copy gives an independent cursor at the same spot so you can walk the list twice.