看一段代码:
bash
printf("hello");
printf("%d", 42);
printf("%d %s %f", 42, "pi", 3.14);
printf("a", "b", "c", "d", "e");
同一个 printf,每次传的参数数量都不一样。编译器不报错,程序能跑,结果也正确。
大多数早期语言没这个能力------函数有几个参数是固定的,少传编译报错,多传也报错。C 凭什么可以?它是怎么办到的?
先说答案:C 的变长参数是通过让调用方把参数全压到栈上,然后让被调用方自己去栈上取来实现的。 没有类型检查,没有数量验证,全靠约定。
你调用 printf("%d %d", 10, 20) 时,编译器生成的汇编代码把参数按顺序压到栈上------先压 20,再压 10,最后压格式串的地址。printf 的代码拿到格式串,挨个解析 % 占位符,每遇到一个就从栈上取一个参数。
这就是为什么 printf 能接受任意参数------它不是"接收"这么多参数,是拿到格式串后自己去栈上"取"的。 编译器只是负责把参数按约定顺序摆好,printf 自己去拿。
不拿白不拿,拿了就要负责。
C 里提供了一套标准宏来安全地做这个事,定义在 <stdarg.h> 里:
bash
#include <stdarg.h>
void my_print(int count, ...) {
va_list args;
va_start(args, count);
for (int i = 0; i < count; i++) {
int val = va_arg(args, int);
printf("%d ", val);
}
va_end(args);
}
va_start 让 args 指向第一个变参,va_arg 每次取一个参数并推进指针,va_end 清理。
这背后做的事就是栈指针操作------没有任何黑魔法。
但变长参数有个很深的坑:类型提升。
看这段代码:
bash
printf("%d", 'A'); // 输出 65 ------ char 被提升成了 int
printf("%f", 3.14f); // 输出错误 ------ float 被提升成了 double
C 标准规定,变长参数里的 char 和 short 会自动提升成 int(默认参数提升),float 会提升成 double。
所以 printf 用 %f 匹配 float 时,实际上栈上摆的是 double。你告诉它按 float 去取,它会取错字节数------导致输出垃圾值,或者连带后面参数都偏移。
这就是为什么 printf("%f", 3.14f) 有时候输出莫名其妙的结果。不是数字不对,是类型不匹配导致取字节的位置错了。
%hd 和 %hhd 可以用来打印 short 和 char,但传参的时候它们已经被提升成 int 了,printf 内部再把它截断回来。这个过程不是自动的,你要自己负责任。
变长参数还有另一个问题:没有类型安全。
bash
printf("%s", 42);
这一行编译不报错,运行时必崩------printf 以为栈上是个字符串地址,拿着 42 去内存地址 42 读数据,段错误是运气好的结果。
Java、Python、Go 等后来语言要么不支持变长参数,要么做了严格的类型检查。C 的选择是不管你,你自己传错了自己负责。
到现在为止,每个星期都有人在 Stack Overflow 上问"为什么 printf 输出奇怪的值",答案是"你的格式化符号跟参数类型对不上"。
变长参数是一个典型 C 语言式的设计:给你极大的灵活性,也给你极大的犯错空间。
你有没有被 printf 的变长参数坑过?比如 %d 跟 %ld 混用、%f 传了 int 结果输出 0.000000------评论区聊聊。