Standards C and C ++ do not impose any requirements for its work. The compiler-compiler may decide to create chained lists, std::stack<boost::any> or even magic dust from a pony (according to @Xeo's comment) under the hood.
However, this is usually implemented as follows, even if transformations, such as embedding or passing arguments in CPU registers, may not leave any of the code discussed.
Also note that this answer specifically describes the stack growing down in the figures below; In addition, this answer is a simplification to demonstrate the scheme (see https://en.wikipedia.org/wiki/Stack_frame ).
How to call a function with a fixed number of arguments
This is possible because the underlying architecture of the machine has a so-called "stack" for each thread. The stack is used to pass arguments to functions. For example, when you have:
foobar("%d%d%d", 3,2,1);
Then it is compiled into assembler code like this (example and schematic, real code may look different); note that the arguments are passed from right to left:
push 1 push 2 push 3 push "%d%d%d" call foobar
These push operations populate the stack:
[] // empty stack ------------------------------- push 1: [1] ------------------------------- push 2: [1] [2] ------------------------------- push 3: [1] [2] [3] // there is now 1, 2, 3 in the stack ------------------------------- push "%d%d%d":[1] [2] [3] ["%d%d%d"] ------------------------------- call foobar ... // foobar uses the same stack!
The bottom element of the stack is called Top Stack, often abbreviated as TOS.
The foobar function will now access the stack, starting with TOS, that is, a format string, which, as you recall, was passed last. Imagine that stack is your stack pointer, stack[0] is the value in TOS, stack[1] is one over TOS, and so on:
format_string <- stack[0]
... and then parses the format string. During parsing, it recognizes %d tokens and for each, loads another value from the stack:
format_string <- stack[0] offset <- 1 while (parsing): token = tokenize_one_more(format_string) if (needs_integer (token)): value <- stack[offset] offset = offset + 1 ...
This, of course, is a very incomplete pseudo-code that demonstrates how a function should rely on passed arguments to find out how much it should load and remove from the stack.
Security
This dependency on user-provided arguments is also one of the biggest security concerns (see https://cwe.mitre.org/top25/ ). Users can easily mistakenly use a function with a variable number of arguments, either because they have not read the documentation, or have forgotten to set up a format string or argument list, or because they are simply evil, or something like that. See also Formatting attack on strings .
Implementation C
In C and C ++, variadic functions are used in conjunction with the va_list interface. Although insertion on the stack is an integral part of these languages ( in K + RC you can even declare a function forward without specifying its arguments , but at the same time call it with any number and any arguments), reading from this list of unknown arguments is conjugated via va_... -macros and va_list -type, which mostly abstract low-level stack frame access.