可变参数函数
stdarg.h与可变参数
概述
可变参数函数是C语言中一类特殊的函数,它可以接受不确定数量的参数。最典型的例子就是标准库中的 printf 和 scanf,它们的参数个数和类型由格式字符串决定。C语言通过 <stdarg.h> 头文件提供了一套宏机制来支持可变参数的处理,使开发者能够自定义可变参数函数。
基础概念
可变参数的原理
在C语言中,函数参数按照从右到左的顺序压入栈中(取决于调用约定),因此只要知道第一个可变参数的地址,就可以依次访问后续参数。<stdarg.h> 中的宏正是基于这一原理实现的。
核心宏与类型
| 名称 | 说明 |
|---|---|
va_list | 可变参数列表的类型,本质上是一个指针 |
va_start | 初始化 va_list,使其指向第一个可变参数 |
va_arg | 获取当前参数并移动到下一个参数 |
va_end | 结束可变参数的遍历,执行清理工作 |
va_copy | 复制 va_list 的当前状态(C99新增) |
函数声明语法
可变参数函数的声明中,固定参数至少要有一个,省略号 ... 必须放在参数列表的最后:
// 正确:至少一个固定参数,省略号在末尾
返回类型 函数名(固定参数, ...);
// 错误:省略号不能单独出现
返回类型 函数名(...);
快速上手
下面是一个最简单的可变参数函数,计算任意个数整数的和:
#include <stdio.h>
#include <stdarg.h>
// count 表示后续可变参数的个数
int sum(int count, ...) {
va_list args; // 声明可变参数列表
va_start(args, count); // 初始化,从 count 之后开始
int total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(args, int); // 逐个获取 int 类型的参数
}
va_end(args); // 结束遍历
return total;
}
int main(void) {
printf("%d\n", sum(3, 1, 2, 3)); // 输出: 6
printf("%d\n", sum(5, 10, 20, 30, 40, 50)); // 输出: 150
return 0;
}
详细用法
va_start 的使用
va_start 接受两个参数:va_list 变量和最后一个固定参数的名称。它将 va_list 定位到第一个可变参数的位置:
va_list ap;
va_start(ap, last_fixed_param); // last_fixed_param 是最后一个固定参数
va_arg 的使用
va_arg 接受两个参数:va_list 变量和期望的参数类型。它返回当前参数的值,并将指针移动到下一个参数:
int val = va_arg(ap, int); // 获取一个 int 类型的参数
double d = va_arg(ap, double); // 获取一个 double 类型的参数
char *str = va_arg(ap, char *); // 获取一个 char* 类型的参数
注意:va_arg 的第二个参数必须是实际传入参数的类型,否则会导致未定义行为。
va_end 的使用
每次使用 va_start 之后,必须调用 va_end 进行清理。不调用 va_end 会导致未定义行为:
va_end(ap); // 必须在函数返回前调用
va_copy 的使用
va_copy 用于复制当前 va_list 的状态,适用于需要多次遍历可变参数的场景:
va_list ap, ap_copy;
va_start(ap, count);
va_copy(ap_copy, ap); // 复制当前状态
// 用 ap 遍历一次
for (int i = 0; i < count; i++) {
int val = va_arg(ap, int);
}
// 用 ap_copy 再遍历一次
for (int i = 0; i < count; i++) {
int val = va_arg(ap_copy, int);
}
va_end(ap);
va_end(ap_copy); // 每个 va_copy 都需要对应的 va_end
常见场景
场景一:实现自定义日志函数
#include <stdio.h>
#include <stdarg.h>
// 自定义日志函数,支持格式化输出
void log_message(const char *level, const char *fmt, ...) {
printf("[%s] ", level);
va_list args;
va_start(args, fmt);
vprintf(fmt, args); // vprintf 接受 va_list 参数
va_end(args);
printf("\n");
}
int main(void) {
log_message("INFO", "用户 %s 登录成功", "张三");
log_message("WARN", "磁盘使用率达到 %d%%", 85);
log_message("ERROR", "文件 %s 打开失败,错误码: %d", "data.txt", 2);
return 0;
}
场景二:实现可变参数的求平均值函数
#include <stdio.h>
#include <stdarg.h>
// 计算任意数量 double 的平均值
double average(int count, ...) {
if (count <= 0) return 0.0;
va_list args;
va_start(args, count);
double sum = 0.0;
for (int i = 0; i < count; i++) {
sum += va_arg(args, double); // 注意:float 会自动提升为 double
}
va_end(args);
return sum / count;
}
int main(void) {
printf("平均值: %.2f\n", average(4, 90.5, 85.0, 92.5, 78.0));
return 0;
}
场景三:封装格式化字符串函数
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
// 将格式化字符串写入动态分配的内存
char *format_string(const char *fmt, ...) {
va_list args;
// 第一次调用:计算所需长度
va_start(args, fmt);
int len = vsnprintf(NULL, 0, fmt, args);
va_end(args);
if (len < 0) return NULL;
// 分配内存
char *buf = malloc(len + 1);
if (!buf) return NULL;
// 第二次调用:实际写入
va_start(args, fmt);
vsnprintf(buf, len + 1, fmt, args);
va_end(args);
return buf;
}
int main(void) {
char *result = format_string("姓名: %s, 年龄: %d", "李四", 25);
if (result) {
printf("%s\n", result);
free(result);
}
return 0;
}
注意事项
默认参数提升
在可变参数中,小于 int 的整型会提升为 int,float 会提升为 double。因此 va_arg 中不能使用 char、short 或 float:
// 错误:char 和 short 会提升为 int
// char c = va_arg(ap, char); // 未定义行为
// short s = va_arg(ap, short); // 未定义行为
// 正确:使用提升后的类型
int c = va_arg(ap, int); // 传入的 char 会被提升为 int
double f = va_arg(ap, double); // 传入的 float 会被提升为 double
缺少参数个数信息
可变参数函数本身无法知道传入了多少个参数,必须通过其他方式告知:
- 使用固定参数指定个数(如
printf的格式字符串中%d的数量) - 使用哨兵值标记结束(如以
NULL结尾的指针列表)
// 方式一:用固定参数指定个数
void func(int count, ...);
// 方式二:用哨兵值标记结束
void print_strings(const char *first, ...) {
va_list ap;
va_start(ap, first);
const char *str = first;
while (str != NULL) { // NULL 作为结束标记
printf("%s\n", str);
str = va_arg(ap, const char *);
}
va_end(ap);
}
类型不匹配的危险
如果 va_arg 指定的类型与实际传入的类型不一致,会导致未定义行为,编译器通常不会报错:
// 危险:传入 int 但按 double 读取
int sum(int count, ...) {
va_list ap;
va_start(ap, count);
// 如果调用者传入 int,但这里按 double 读取,结果不可预测
double val = va_arg(ap, double); // 错误!
va_end(ap);
return (int)val;
}
va_start 和 va_end 必须成对出现
每次 va_start 必须有对应的 va_end,否则可能导致栈损坏或内存泄漏。
进阶用法
实现简易 printf
#include <stdio.h>
#include <stdarg.h>
// 简易版 printf,仅支持 %d、%s、%c、%f
void my_printf(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
while (*fmt) {
if (*fmt == '%') {
fmt++; // 跳过 %
switch (*fmt) {
case 'd': {
int val = va_arg(ap, int);
printf("%d", val);
break;
}
case 's': {
const char *str = va_arg(ap, const char *);
if (str) printf("%s", str);
else printf("(null)");
break;
}
case 'c': {
// char 提升为 int
int ch = va_arg(ap, int);
putchar(ch);
break;
}
case 'f': {
double val = va_arg(ap, double);
printf("%f", val);
break;
}
case '%':
putchar('%');
break;
default:
putchar('%');
putchar(*fmt);
break;
}
} else {
putchar(*fmt);
}
fmt++;
}
va_end(ap);
}
int main(void) {
my_printf("姓名: %s, 年龄: %d, 成绩: %f\n", "王五", 20, 95.5);
return 0;
}
可变参数与函数指针结合
#include <stdio.h>
#include <stdarg.h>
// 定义可变参数的函数指针类型
typedef void (*log_func_t)(const char *fmt, ...);
// 日志实现
void debug_log(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
printf("[DEBUG] ");
vprintf(fmt, ap);
printf("\n");
va_end(ap);
}
void error_log(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
printf("[ERROR] ");
vprintf(fmt, ap);
printf("\n");
va_end(ap);
}
int main(void) {
// 通过函数指针调用不同的日志函数
log_func_t logger = debug_log;
logger("变量 x = %d", 42);
logger = error_log;
logger("文件打开失败: %s", "config.txt");
return 0;
}
使用 va_copy 实现多次遍历
#include <stdio.h>
#include <stdarg.h>
// 先求和再求平均值,需要遍历两次
double sum_and_avg(int count, double *out_sum, ...) {
va_list ap, ap_copy;
double sum = 0.0;
// 第一次遍历:求和
va_start(ap, count);
va_copy(ap_copy, ap);
for (int i = 0; i < count; i++) {
sum += va_arg(ap, double);
}
va_end(ap);
// 第二次遍历:使用副本
// 这里演示 va_copy 的用法,实际场景中只需用 sum/count 即可
double sum2 = 0.0;
for (int i = 0; i < count; i++) {
sum2 += va_arg(ap_copy, double);
}
va_end(ap_copy);
*out_sum = sum;
return sum / count;
}
int main(void) {
double total;
double avg = sum_and_avg(&total, 3, 80.0, 90.0, 100.0);
printf("总分: %.1f, 平均: %.1f\n", total, avg);
return 0;
}