C 和 C++ 标准对其工作方式没有任何要求。一个符合要求的编译器很可能会决定在引擎盖下发出链表、std::stack<boost::any> 甚至是神奇的小马尘(根据@Xeo 的评论)。
但是,它通常按如下方式实现,即使在 CPU 寄存器中内联或传递参数等转换可能不会留下任何讨论的代码。
另请注意,此答案在下面的视觉效果中专门描述了向下增长的堆栈;此外,此答案只是为了演示该方案而进行的简化(请参阅https://en.wikipedia.org/wiki/Stack_frame)。
如何使用不固定数量的参数调用函数
这是可能的,因为底层机器架构对每个线程都有一个所谓的“堆栈”。堆栈用于将参数传递给函数。例如,当您有:
foobar("%d%d%d", 3,2,1);
然后编译成这样的汇编代码(示例性和示意性,实际代码可能看起来不同);请注意,参数是从右向左传递的:
push 1
push 2
push 3
push "%d%d%d"
call foobar
那些推入操作会填满堆栈:
[] // 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!
栈底元素称为“栈顶”,通常缩写为“TOS”。
foobar 函数现在将访问堆栈,从 TOS 开始,即格式字符串,您记得它是最后推送的。想象stack 是你的堆栈指针,stack[0] 是 TOS 的值,stack[1] 是 TOS 之上的值,以此类推:
format_string <- stack[0]
... 然后解析格式字符串。在解析时,它会识别%d-tokens,并为每一个从堆栈中加载一个值:
format_string <- stack[0]
offset <- 1
while (parsing):
token = tokenize_one_more(format_string)
if (needs_integer (token)):
value <- stack[offset]
offset = offset + 1
...
这当然是一个非常不完整的伪代码,它演示了函数必须如何依赖传递的参数来找出它必须加载和从堆栈中删除多少。
安全
这种对用户提供的参数的依赖也是目前最大的安全问题之一(请参阅https://cwe.mitre.org/top25/)。用户可能很容易错误地使用可变参数函数,或者因为他们没有阅读文档,或者忘记调整格式字符串或参数列表,或者因为它们很邪恶,或者其他原因。另见Format String Attack。
C 实现
在 C 和 C++ 中,可变参数函数与 va_list 接口一起使用。虽然压入堆栈是这些语言所固有的(in K+R C you could even forward-declare a function without stating its arguments,但仍然使用任意数量和种类的参数调用它),从这样一个未知的参数列表中读取是通过 va_...-macros 和 va_list-type 接口的,它基本上抽象了低级堆栈帧访问。