【C/C++】C语言可变参数实现原理
C语言可变参数
前言:在定义某些函数时,函数参数的个数可能无法确定例如printf,这时需要函数支持传递多个参数。可变参数的函数至少需要一个参数,其余用...来表示可变参数。
举个例子,定义求一个求平均值的函数。参数n_values表示需要计算的数值个数,这个值是必须有的,否则无法确定传入了多少个参数。
double average(int n_values, ...) {
va_list var_arg; // 定义一个va_list变量
int count;
float sum = 0;
va_start(var_arg, n_values); // 执行var_arg=(va_list)&n_values + _INTSIZEOF(n_values),var_arg指向参数n_values之后那个参数的地址,即var_arg指向第一个可变参数在堆栈的地址
for (count = 0; count < n_values; count++) {
sum += va_arg(var_arg, int); // (*(t *)((var_arg += _INTSIZEOF(int)) - _INTSIZEOF(int)) ) 取出当前var_arg指针所指的值,并使var_arg指向下一个参数
}
va_end(var_arg); // 清空va_list var_arg
return sum / n_values;
}
根据以上代码可以看到获取可变参数的流程如下:
- 定义
va_list - 调用
va_start,并传入第一个参数 - 调用
va_arg获取可变参数的值 - 最后调用
va_end
函数调用栈
在讲解原理之前,需要了解函数调用栈帧的结构,可变参数就是利用了函数调用栈来实现的。
函数栈帧是在函数被调用时栈上的布局,例如函数蚕食,函数返回值,局部变量等是如何存储的。栈的增长方向是从高到低。
在栈帧中,有两个重要的寄存器,esp和ebp。esp始终指向栈顶,当有输入入栈时该指针就会向下移动,ebp指向当前栈帧的栈底,它里面的值是上一个函数栈的ebp,当函数调用结束可以通过这个值可以恢复上一个函数的现场。
不同的编译器有着不同的函数调用约定,比如有的参数从右到左进栈,有的从左到右进栈;在参数出栈时有的是调用者清栈,有的是被调用者清栈。下面我们统一以 cdecl 为标准,即 c 语言默认的调用约定来讲述。它将从右向左进栈,调用者清栈。
下图是一个函数栈帧的示意图:

当调用一个函数时,会先将参数压栈,然后是返回地址,再就是函数内部的局部变量。
实现原理
可变参数就是利用了cdecl的调用惯例
- 参数从右向左入栈,则一个参数是最后入栈的。其他可变参数相对于第一个参数往高地址找即可。
- 调用者清理栈。只有调用者知道可变参数到底传了几个,被调用函数是不知道可变参数的个数的,所以调用者清栈更合适。
假设用如下方式调用average函数
void main(int argc, char *argv[]) {
double ret = 0;
ret = average(3, 4, 5, 6);
return;
}
那么average函数的调用栈大致如下所示:

参数3、4、5、6依次从有向左入栈,即6、5、4、3。黄色箭头标记的是第一个参数3的地址。
那么va_**之类的宏到底是如何实现可变参数?如果了解了栈帧的布局,那理解其原理也就很简单了。
va_list
va_list的本质是定义了一个char *指针,并且这个指针需要指向第一个可变参数的地址。因为参数的个数是可变的,所以用char *是最合适的。
// 定义 char *指针类型
#define va_list char *
举个例子:
va_list var_arg;
同样的可以将其转换成如下代码,本质是定义了一个var_arg指针:
char *var_arg;
va_start
这是个宏函数,将上面的指针va_arg指向第一个卡变参数。
// 指向可变参数
#define va_start(ap, param) (ap = (va_list)¶m + sizeof(param))
从以上代码可以看到,取了第一个参数的地址,并计算出可变参数的其实地址。这就是为什么可变参数至少需要一个固定参数的原因,需用通过这个固定参数找到可变参数的其实地址。
举个例子:
va_start(var_arg, n_values);
以上代码可转换为:
var_arg = (char *)&n_value + sizeof(n_value);
va_arg
获取可变参数的值,并将指针指向下一个参数的地址。
// ap 自增 sizeof(t),然后减去 sizeof(t),顺序获取参数的值
#define va_arg(ap, t) (*(t *)((ap = (ap + sizeof(t))) - sizeof(t)))
ap首先增加了sizeof(t),然后又减了sizeof(t)。第一次增加ap的值变了,第二季减ap的值没有变,是为了取当前参数的值。
举个例子:
int value = va_arg(var_arg, int)
以上代码可转换为:
var_arg = var_arg + sizeof(int)
int value = *(int *)(var_arg - sizeof(int))
va_end
最后一步就是清理指针
// 清理指针
#define va_end(ap) (ap = ((va_list)0))
举个例子:
va_end(var_arg);
以上代码可以转换为:
var_arg = (char *)0;
因此文章开始的代码可以转换为:
double average2(int n_values, ...) {
char *var_arg;
int count;
float sum = 0;
var_arg = (char*)&n_values + sizeof(n_values);
for (count = 0; count < n_values; count++) {
var_arg = var_arg + sizeof(int);
int value = *(int *)(var_arg - sizeof(int));
sum += value;
}
var_arg = (char *)0;
return sum / n_values;
}

浙公网安备 33010602011771号