printf 为什么可以接受任意数量参数?
一个神奇的函数
printf("Hello\n"); // 1个参数
printf("age = %d\n", 25); // 2个参数
printf("name = %s, age = %d, pi = %f\n", // 5个参数
"Alice", 25, 3.14);
同样的 printf,参数数量可以随意变化。用别的语言可能觉得正常,但 C 是静态类型语言——它是怎么做到的?
你以为的
编译器给 printf 做了特殊处理(printf 是编译器内建函数)。
实际情况:可变参数宏(va_list)
C 语言通过 stdarg.h 提供了一组宏来支持可变参数:
#include <stdarg.h>
void func(int count, ...) {
va_list args;
va_start(args, count); // 初始化,指向第一个可变参数
int val = va_arg(args, int); // 取出下一个参数
va_end(args); // 清理
}
原理(x86 32位简化版)
在 x86 cdecl 下,参数全部在栈上,且从右到左依次压入:
高地址
┌──────────────┐
│ 最后一个参数 │
├──────────────┤
│ ... │ ← va_arg 依次往上取
├──────────────┤
│ 第一个可变参数│ ← va_start(args, count) 指向这里
├──────────────┤
│ count (固定) │ ← 栈上的最后一个固定参数
├──────────────┤
│ 返回地址 │
低地址
va_start(args, last) 本质上就是:
args = (va_list)&last + sizeof(last)
而 va_arg(args, type) 就是:
type value = *(type*)args;
args = (va_list)((char*)args + sizeof(type));
在 x64 上情况更复杂——因为前6个参数用寄存器,va_list 需要同时保存寄存器和栈的状态。
动手验证
保存为 variadic.c:
#include <stdio.h>
#include <stdarg.h>
int my_printf(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
int count = 0;
const char *p = fmt;
while (*p) {
if (*p == '%') {
p++;
switch (*p) {
case 'd': {
int val = va_arg(args, int);
count += printf("[int %d]\n", val);
break;
}
case 'f': {
double val = va_arg(args, double);
count += printf("[float %f]\n", val);
break;
}
case 's': {
char *val = va_arg(args, char *);
count += printf("[str %s]\n", val);
break;
}
default:
count += putchar(*p);
break;
}
} else {
count += putchar(*p);
}
p++;
}
va_end(args);
return count;
}
void show_va_layout(int first, ...) {
printf("固定参数 first = %d, &first = %p\n", first, (void*)&first);
int *p = &first;
p++;
printf(" (int*)&first + 1 的值 = %d\n", *p);
printf(" (注意 x64 上这不是第二个参数的可靠方式)\n");
}
int main() {
printf("【自定义 my_printf】\n");
my_printf("Hello %s, age = %d, pi = %f\n", "Alice", 25, 3.14);
printf("\n【类型不匹配的陷阱】\n");
printf(" printf(\"%%d\", 3.14) = %d\n", 3.14);
printf(" printf(\"%%f\", 3.14) = %f\n", 3.14);
printf("\n【关于类型提升】\n");
printf(" 可变参数中的 float 会提升为 double\n");
printf(" char/short 会提升为 int\n");
return 0;
}
编译并运行:
$ gcc -o variadic variadic.c
$ ./variadic
命令解释:gcc -o variadic variadic.c 编译 C 源码。./variadic 执行。
运行结果:
【自定义 my_printf】
Hello [str Alice]
, age = [int 25]
, pi = [float 3.141590]
【类型不匹配的陷阱】
printf("%d", 3.14) = 2076451400
printf("%f", 3.14) = 3.140000
【关于类型提升】
可变参数中的 float 会提升为 double
char/short 会提升为 int
自己实现迷你 printf
上面的 my_printf 模拟了 printf 的核心逻辑:
- 遍历格式化字符串
- 遇到
%就看下一个字符 - 用
va_arg取出对应类型的参数 - 用标准
printf输出
类型提升规则
在可变参数中,整数类型有默认类型提升:
| 实际类型 | 提升后 |
|---|---|
float | double |
char | int |
short | int |
所以 va_arg(args, float) 是错误的,必须用 va_arg(args, double)。
类型不匹配为什么不会报错?
因为 va_arg 只是一个宏,它不知道栈/寄存器上的实际类型——你告诉它是什么,它就怎么解释:
va_arg(args, int) // 从当前位置取4字节
va_arg(args, double) // 从当前位置取8字节
如果你传了 double 却用 %d 取 int,它只会读取前4字节,不会报错——这就是未定义行为。
va_list 的实现细节
在 x86 上,va_list 就是一个 char*:
typedef char* va_list;
#define va_start(ap, last) (ap = (va_list)&last + sizeof(last))
#define va_arg(ap, type) ((type*)(ap += sizeof(type)))[-1]
但在 x64 上,va_list 是一个结构体,包含寄存器的保存区指针和栈指针:
typedef struct {
unsigned int gp_offset; // 下一个通用寄存器参数位置
unsigned int fp_offset; // 下一个浮点寄存器参数位置
void *overflow_arg_area; // 栈上参数的位置
void *reg_save_area; // 寄存器保存区的基址
} va_list[1];
核心启示
- 可变参数的底层是内存读取——
va_list是一个指针,va_arg按类型大小移动指针并读取 - 类型安全由程序员保证——如果格式串和参数不匹配,编译不会报错但运行会出问题
- 默认类型提升——
float → double,char/short → int - x64 的 va_list 是结构体——因为部分参数在寄存器中,需要特殊的处理逻辑
下期预告
“指向函数的指针”——函数真能像变量一样被传来传去吗?回调函数的底层实现到底是什么?
下一期《函数指针和回调底层是怎样的?》,看间接调用 call [rax] 和直接调用 call func 的区别。
更多内容详见专栏,关注不迷路
1192

被折叠的 条评论
为什么被折叠?



