目录
- 1. 什么是可变参数
-
- 1.1.printf
- 1.2 scanf
- 1.3 sprintf
-
- 补充 snprintf
- 2. 核心头文件与标准宏
-
- 2.1 可变参数的类型
- 2.2 核心宏
- 3. 实现原理
-
- 3.1 参数压栈规则(cdecl调用约定)
- 3.2 宏的底层实现逻辑
- 4. 使用方法与示例
-
- 4.1 使用步骤
-
- 示例:固定类型的求和函数
- 5. 关键特性与进阶用法
-
- 5.1 默认参数提升
- 5.2 标准v系列格式化函数
- 5.3 多次遍历参数:va_copy
1. 什么是可变参数
可变参数函数(Variadic Function)是指参数的数量、类型可以在调用时动态变化的函数。最典型的例子是标准库中的 printf、scanf、sprintf 等格式化函数,以及 execl 等系统调用。
1.1.printf
int printf(const char *format, ...);
1.2 scanf
int scanf(const char *format, ...);
1.3 sprintf
int sprintf(char *str, const char *format, ...);
- 功能:把格式化字符串输出到字符缓冲区 str(内存字符串,非屏幕)
- 返回值:生成字符串的字符数(不含'\\0');出错返回负数
不做缓冲区越界检查,优先用 snprintf
补充 snprintf
int snprintf(char *str, size_t size, const char *format, ...);
- size:缓冲区总大小,最多写入 size‑1 个字符,末尾自动补\\0 用法。
2. 核心头文件与标准宏
C标准规定,可变参数的实现依赖 <stdarg.h> 头文件,其中定义了一个专用类型和4个核心宏(C99新增va_copy)。
2.1 可变参数的类型
va_list 是用于保存可变参数列表状态的类型,本质上是一个指向栈内存的指针,用来遍历栈上的可变参数。
2.2 核心宏
| va_start(ap, last) | 初始化可变参数列表 | ap:va_list 类型变量;last:函数最后一个固定参数的名称 |
| va_arg(ap, type) | 获取下一个可变参数 | ap:va_list 变量;type:当前参数的类型(如 int、double、char*) |
| va_end(ap) | 清理可变参数列表 | ap:va_list 变量,使用完毕必须调用 |
| va_copy(dest, src) | 复制可变参数列表状态(C99新增) | dest:目标 va_list;src:源 va_list |
3. 实现原理
要理解可变参数的本质,需要先了解C语言函数的栈帧结构和调用约定。
3.1 参数压栈规则(cdecl调用约定)
在x86等主流架构下,C语言默认使用 cdecl 调用约定:
- 函数参数从右往左依次压入栈中;
- 栈内存从高地址向低地址生长(栈顶指向低地址);
- 由调用者负责栈空间的清理(被调用者不需要知道参数总数)。
举个例子,调用 func(1, 2.5, "hello") 时,栈上的参数布局(从低地址到高地址):
低地址 ← 栈顶方向 高地址
+——–+——–+——–+
| 1 | 2.5 | "hello"|
+——–+——–+——–+
第一个参数 最后一个参数
最左侧的固定参数地址最低,越靠右的参数地址越高,可变参数全部位于固定参数的右侧(高地址方向)。
3.2 宏的底层实现逻辑
基于栈布局,<stdarg.h> 的宏本质就是通过最后一个固定参数的地址,逐步偏移遍历后续参数。
// 按int大小向上对齐,保证栈地址对齐
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) – 1) & ~(sizeof(int) – 1))
typedef char* va_list;
// 初始化:指向最后一个固定参数的下一个位置(第一个可变参数)
#define va_start(ap, last) (ap = (va_list)&last + _INTSIZEOF(last))
// 取参数:先偏移到下一个参数,再返回当前参数的值
#define va_arg(ap, type) (*(type*)((ap += _INTSIZEOF(type)) – _INTSIZEOF(type)))
// 清理:置空指针
#define va_end(ap) (ap = (va_list)0)
// 复制:直接拷贝指针状态
#define va_copy(dest, src) (dest = src)
注意:实际标准库的实现会因架构、编译器不同而有所差异,切勿手动计算地址来遍历参数,务必使用标准宏以保证代码的可移植性。
4. 使用方法与示例
使用可变参数函数必须遵循固定的步骤,且函数必须满足两个前提:
4.1 使用步骤
示例:固定类型的求和函数
第一个参数为参数个数,后续为若干个 int 类型参数,返回它们的和。
#include <stdio.h>
#include <stdarg.h>
// count:可变参数的个数,后续为n个int
int sum(int count, ...) {
va_list ap; // 定义参数列表变量
int total = 0;
va_start(ap, count); // 初始化,以count为基准定位可变参数
for (int i = 0; i < count; i++) {
// 依次取出int类型的参数
total += va_arg(ap, int);
}
va_end(ap); // 清理,必须调用
return total;
}
int main() {
printf("3个数求和:%d\\n", sum(3, 10, 20, 30)); // 输出60
printf("5个数求和:%d\\n", sum(5, 1, 2, 3, 4, 5)); // 输出15
return 0;
}
5. 关键特性与进阶用法
5.1 默认参数提升
在可变参数列表中,会发生默认参数提升
- 所有小于 int 的整型类型(char、signed char、unsigned char、short、unsigned short)都会被提升为 int;
- float 类型会被提升为 double。
因此,绝对不能用 va_arg 直接获取 char、short、float 类型,必须使用提升后的类型 int、double
错误示例:
char c = va_arg(ap, char); // 错误!char被提升为int
float f = va_arg(ap, float); // 错误!float被提升为double
正确示例:
char c = (char)va_arg(ap, int); // 用int接收后强转
double f = va_arg(ap, double); // 直接用double
这也是为什么 printf 中 %f 对应 double,传入 float 也能正常工作的原因。
5.2 标准v系列格式化函数
C标准库提供了一组以 v 开头的格式化函数,专门接收 va_list 类型参数,用于封装自定义可变参数函数,避免重复实现格式解析逻辑:
- vprintf:输出到标准输出
- vfprintf:输出到指定文件流
- vsprintf:输出到字符串缓冲区
- vsnprintf:安全版字符串输出(带长度限制)
示例:封装一个带时间戳的日志函数
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
void log_info(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
// 先打印时间戳
time_t t = time(NULL);
struct tm *tm_info = localtime(&t);
printf("[%04d-%02d-%02d %02d:%02d:%02d] [INFO] ",
tm_info->tm_year + 1900, tm_info->tm_mon + 1, tm_info->tm_mday,
tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec);
// 直接调用vprintf输出格式化内容
vprintf(fmt, ap);
va_end(ap);
}
int main() {
log_info("用户%s登录成功,ID:%d\\n", "admin", 1001);
return 0;
}
5.3 多次遍历参数:va_copy
如果需要多次遍历同一个可变参数列表,不能重复使用同一个 va_list 直接从头遍历,也不能多次调用 va_start。正确做法是用 va_copy 复制一份状态。
示例:先打印所有参数,再求平均值
double avg_and_print(int count, ...) {
va_list ap_read, ap_copy;
double sum = 0;
va_start(ap_read, count);
va_copy(ap_copy, ap_read); // 复制当前状态到ap_copy
// 第一次遍历:打印
printf("参数列表:");
for (int i = 0; i < count; i++) {
int val = va_arg(ap_read, int);
printf("%d ", val);
}
printf("\\n");
// 第二次遍历:求和
for (int i = 0; i < count; i++) {
sum += va_arg(ap_copy, int);
}
va_end(ap_read);
va_end(ap_copy); // 复制的va_list也必须调用va_end
return sum / count;
}

