Visual walkthrough — Variadic functions — va_list, va_start, va_arg, va_end
We assume you know only one thing: a computer's memory is a long row of numbered mailboxes, each holding one byte, and each with an address (its house number). Everything else we build.
Step 1 — What "arguments on the stack" actually means
WHY this picture: you cannot "walk a cursor" over something invisible. Before any macro makes sense, we must see the row of bytes the cursor walks over. We use a simplified all-on-the-stack model (real CPUs also use registers — that complexity is exactly why the macros hide it, see Calling Conventions and the Stack).
PICTURE: each int is 4 bytes wide. So the four arguments occupy addresses in a strip. The named parameter n is the very first block; the ... values follow.

Step 2 — A cursor is just one number
WHY a pointer and not, say, an index? Because the arguments live at real memory addresses, and a pointer is C's native tool for "hold an address and read what's there." An index would need a base to add to; a pointer already is base+offset in one value. That is the minimal tool for the job.
PICTURE: think of as a little arrow floating above the memory strip, parked over one byte.

- — the cursor's current position (an address, i.e. a number).
- There is no length stored anywhere. knows where it is, never how far it may go. Remember this — it is the whole reason we need a count or sentinel.
Step 3 — va_start: park the cursor after the last named arg
WHY "after n"? The named parameters are the slots the compiler did reserve and name. n is the last one you gave a name. Everything you want to read is beyond it. So the cursor's starting line is the finish line of the named region.
PICTURE: the address just past n is . In our conceptual model:

The green arrow now sits at the first ... value (10). We haven't read anything yet — we've only aimed.
Step 4 — va_arg: read here, then step forward
WHY read-then-advance (and why do YOU state the type)? The bytes in memory are just bits — they carry no label saying "I am an int" or "I am a pointer." Someone must decide how to interpret them. The compiler stopped checking at ..., so you supply the type. va_arg(ap, int) means "trust me: the next 4 bytes are an int."
PICTURE — pulling 10: the cursor is over the 10 block. We copy those 4 bytes out as an int, then slide the cursor right by sizeof(int) = 4.

- — "treat as pointing at an int."
- — the star means "give me the value living at that address."
- — after reading, move the cursor forward by the width just consumed, so the next
va_argstarts fresh.
Call va_arg(ap, int) twice more and you harvest 20, then 30, the cursor stepping 4 bytes each time.
Step 5 — Why the type must be the promoted type
WHY: before a value even reaches the ..., C applies Default Argument Promotions: every char/short is widened to int, and every float is widened to double. The narrow value is never actually placed in the arguments — a fattened copy is. So the byte-strip really holds ints and doubles, no matter what you wrote at the call.
PICTURE: the caller's char c = 'A' becomes a 4-byte int slot; float x becomes an 8-byte double slot. If your cursor tries to read only 1 or 4 bytes, it lands mid-value and every later read is misaligned.

Step 6 — The degenerate case: reading one too many
WHY it's fatal, not just wrong: past the last argument is whatever the stack happened to hold — a return address, saved registers, garbage. Interpreting it as an int gives a nonsense number; the program may print junk or crash. This is textbook Undefined Behavior in C.
PICTURE: the strip ends after 30. A fourth va_arg lands in the red "no-man's-land."

This is why every variadic function needs a stop rule:
- a count (
sum(3, ...)— read exactly 3), or - a sentinel (a final
NULL/-1that means "STOP").
Nothing in the machinery enforces it — the discipline is entirely yours.
Step 7 — va_end: put the cursor down
WHY bother if we're returning anyway? Because the standard requires it, and on some calling conventions va_start allocates temporary state that leaks or corrupts if never released. Treat va_start and va_end as a matched pair, like opening and closing a bracket.
PICTURE: the arrow is retired; the strip is untouched.

The one-picture summary
Everything above, on one strip: park after the last named arg → read+step, read+step, read+step → stop at the agreed boundary → retire the cursor.

Recall Feynman: the whole walk in plain words
Picture a row of parcels on a shelf, left to right. The first parcel is labelled n — that one you were told about. Your job is to open the unlabelled parcels after it, but nobody wrote what's inside or how many there are.
va_start puts your finger just to the right of the n parcel — on the first mystery parcel.
va_arg(int) means "I bet this parcel holds a number of a certain size; open it, take the number, and shuffle my finger right by exactly that size." You must know the size, because the parcels are unlabelled — and the shelf secretly fattens tiny parcels (a char becomes a full-size int box, a float becomes a double box) before putting them out, so always bet on the fat size.
The shelf has no end marker. If you open one parcel too many, you're grabbing at empty air past the shelf — that's the crash. So either you were told "there are 3," or the last parcel is a special empty STOP box.
va_end is lowering your hand when the job's done. Start, then Arg-Arg-Arg, then End. That's the entire dance.
Active Recall
Where does va_start park the cursor?
... argument.Why does va_arg need you to name the type?
What two things does va_arg do?
Why is reading one extra va_arg undefined behavior?
Why must you write double/int even if you passed float/char?
float→double and char/short→int before they ever reach ....What does va_end do and why is it required?
va_start that must be released.