变长参数其实是C语言的特殊参数形式
例如printf()函数和scanf()函数等:
int printf(const char * format,...);
如此声明,printf函数除了第一个函数类型为const char 之外,其后可以追加任意数量、任意类型的参数。
如何实现呢?在函数的实现部分,可以使用stdarg.h里的多个宏来访问各个额外的参数:
假设lastarg是变长参数函数的最后一个具名参数(如printf里面的format)
在函数内部定义类型为va_list的变量:
va_list ap;
该变量以后会一次指向各个可变参数。ap首先必须用宏 va_start 初始化一次,其中lastarg必须是函数的最后一个具名的参数。
va_start( ap , lastarg );
此后可以用 va_arg宏获得下一个不定参数(假设已知其类型为type):
type next = va_arg( ap, type ) ;
在函数结束之前,还必须用宏va_end来收拾清理现场。
在这里我们可以讨论一下几个宏的实现细节。
变长函数的实现得益于C默认的cdecl调用惯例的自右向左压栈的参数传递方式(不明白的可以先去了解一下函数调用时,栈的现场保护以及参数压栈的实现等基础)
设想如下的函数:
int sun(unsigned num , ...);
其语意如下:
第一个参数传递一个整数num,紧接着后面会传递num个整数,返回num个整数的和。
当我们调用:
int n = sum(3, 16 , 15 , 26);
参数会在栈上形成如下布局:
TOP
…
26
15
16
3
在函数的内部,函数可以使用num来访问数字3,但没法使用名称去访问其他的几个不定参数。但从以上栈的排列可看出,其他的几个参数恰好在参数num的高地址方向(栈向高地址伸展),因此可以简单的通过num的地址计算出其他参数的地址。
sum 函数实现如下:
int sum(unsigned num){
int *p = &num + 1;
int ret = 0;
while(num--){
ret += *p++;
}
retur nret;
}
从以上我们可以观察到两个事实:
1. sum函数获取参数的量取决与num函数的值,因此,如果num参数的值不等于实际传递的不定参数的数量,那么sum函数可能获取到错误的或者不足的参数。可能会访问到栈上的其他局部变量,引起程序的崩溃。
2. cdecl调用惯例保证了可变参数的正确取用。
各个宏:
//访问可变参数流程
va_list args; //定义一个可变参数列表
va_start(args,arg);//初始化args指向强制参数arg的下一个参数;
va_arg(args,type);//获取当前参数内容并将args指向下一个参数
...//循环获取所有可变参数内容
va_end(args);//释放args
所以以上的sum函数可如下定义:
int sum( unsigned int n,...)
{
int sum = 0 ;
va_list args ;
va_start(args , n);
while(n > 0)
{
//通过va_arg(args,int)依次获取参数的值
sum += va_arg(args,int);
n --;
}
va_end(args);
return sum;
}
其实只要了解了 cdecl调用惯例 和函数调用的具体到汇编代码的实现,我们就可以很好地理解可变参数函数的实现原理。
我们在编写可变参数函数的时候,一定要注意输入参数和你写的可变参数函数的获取参数相同,不然也会引起以上所说的越界情况。