C 语言可变参数与格式化
更新时间:2026-08-26。本文是
languages/c/主题专家层文档。printf为什么能接收任意数量的参数?这就是可变参数(variadic function)——用va_list系列宏实现。本文用"自定义 printf"的实测,讲清这套机制和它的类型安全陷阱。
本文要回答的问题
printf怎么知道传了几个参数?va_list怎么工作?- 怎么自己写一个接收可变参数的函数?
- 可变参数有什么类型安全陷阱?为什么容易出错?
一、va_list 四个宏
可变参数的核心是四个宏:
| 宏 | 作用 |
|---|---|
va_start(ap, last) | 初始化 ap,指向第一个可变参数 |
va_arg(ap, type) | 取出下一个参数(按 type 解析) |
va_end(ap) | 清理 |
va_copy(dst, src) | 复制 va_list(遍历两次用) |
二、实测:可变参数求和
c
#include <stdarg.h>
int sum(int count, ...) { // ... 表示可变参数
va_list ap;
va_start(ap, count); // 从 count 之后开始
int total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(ap, int); // 逐个取出
}
va_end(ap);
return total;
}
sum(4, 1, 2, 3, 4); // 返回 10实测:sum(4, 1,2,3,4) = 10。
关键:可变参数函数必须能知道参数个数——sum 靠第一个参数 count,printf 靠格式串里 % 的数量。
三、自定义 printf
c
void my_printf(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
for (const char *p = fmt; *p; p++) {
if (*p != '%') { putchar(*p); continue; }
switch (*++p) { // % 后的转换符
case 'd': printf("%d", va_arg(ap, int)); break;
case 's': printf("%s", va_arg(ap, char *)); break;
default: putchar('%'); putchar(*p);
}
}
va_end(ap);
}
my_printf("自定义: %d + %s = %d\n", 3, "foo", 7);
// 输出:自定义: 3 + foo = 7printf 的工作原理就是:遍历格式串,遇到 % 就按转换符的类型 va_arg 取一个参数。这就是格式化函数的核心。
四、可变参数的栈布局
可变参数在底层是依次压栈的:

va_start 定位到第一个可变参数,va_arg 按类型大小步进到下一个。这也是为什么必须知道类型——不知道类型就不知道步进多少字节。
注意:x86-64 上,前 6 个整型参数用寄存器传(见 调用约定),所以 va_list 在寄存器/栈之间有一套"溢出区"机制,比"纯压栈"复杂。
五、类型安全陷阱
| 陷阱 | 说明 |
|---|---|
| 默认参数提升 | float 传给可变参数会提升成 double,char/short 提升成 int |
| 类型不匹配 | va_arg(ap, double) 但实际传了 int,读到垃圾值 |
| 参数个数不匹配 | 格式串 %d %d 但只传一个参数,读越界 |
| 漏 va_end | 不调 va_end,某些平台上资源泄漏 |
c
// 坑:float 会提升成 double
void f(int n, ...) {
float x = va_arg(ap, float); // 错误!应该用 double
double x2 = va_arg(ap, double); // 正确
}这就是为什么 printf 的 %f 实际对应 double(float 被提升了)。
六、现代替代
可变参数是"类型不安全"的(编译器无法检查),现代 C 有替代:
- 用固定参数 + 数组代替(传指针 + 长度)
- C11 的
_Generic(编译期类型分发) - 编译器的 format 检查(
__attribute__((format(printf, ...)))让编译器检查格式串)
c
// 让编译器帮你检查格式串和参数匹配
void my_log(const char *fmt, ...) __attribute__((format(printf, 1, 2)));七、与本站主线衔接
- 调用约定:可变参数的寄存器/栈传递机制,见 内存布局与 ABI。
- 栈帧:可变参数的栈布局,见 内存布局与 ABI。
- 类型提升:默认参数提升规则,见 数据类型与内存表示。
- 未定义行为:va_arg 类型不匹配是 UB,见 未定义行为与优化。
一句话总结
可变参数靠 va_list 四件套(va_start/va_arg/va_end/va_copy),按类型步进栈取参数,printf 就是遍历格式串按 % 取参;它类型不安全(float 提升 double、个数靠约定),现代代码尽量用数组/_Generic/编译器 format 检查替代。