1、首先什么是可变参数列表?
对于一般的函数而言,参数列表都是固定的,而且各个参数之间用逗号进行分开。这种函数在调用的时候,必须严格按照参数列表中参数的个数进行传参,否则编译器就会报错。
int add(int a, int b) //该函数定义时,参数有两个,所以在调用时只能传入两个参数
{
int c = a + b;
return c;
}
int main()
{
int sum1 = 0;
int sum2 = 0;
int sum3 = 0;
sum1 = add(1); //报错:error C2198: “add”: 用于调用的参数太少
sum2 = add(1, 2);
sum3 = add(1, 2, 3); //报错:warning C4020: “add”: 实参太多
return 0;
}
我们应该都注意到,库函数 printf(); 的参数并不是固定的,传入的参数个数不同,但该函数仍然可以成功执行。如下:
int a = 20;
int b = 30;
printf("10\n");
printf("%d\n",a);
printf("%d %d\n",a, b);
//printf函数的定义如下:
//int __cdecl printf(_In_z_ _Printf_format_string_ const char * _Format, ...);
//可以看出参数列表中的参数并没有完全给出
所以,具有可变参数列表的函数就是:函数定义时,参数列表中的的参数不完全定义;调用该函数时,可以根据实际情况传入多个参数,且可以成功完成其函数功能。而该函数的参数列表就是可变参数列表。
2、那么通过一个简单的函数,详细了解一下具有可变参数列表的函数时如何实现函数功能的
#include<stdio.h>
#include<stdarg.h>
int average(int n, ...) //该函数的功能为:求出任意个数参数的平均值
{
int i = 0;
int sum = 0;
va_list arg; //即:char* arg;
va_start(arg, n); //即:arg = (char*)&n + 4;
//初始化arg为位置参数列表中的第一个参数的地址
for(i=0; i<n; i++)
{
sum += va_arg(arg, int); //即: sum += (*(int *)(arg += 4) - 4);
//此时已经将arg重新赋值为可变参数列表中第二个参数的地址,
//但是此处保留的仍然是上一个参数的地址,然后对保留地址进行
//强制类型转换之后解以用得到内容(参数)
}
return sum/n;
va_end(arg); //即:arg = char* 0; //把arg置为NULL
}
int main()
{
int a = 10;
int b = 20;
int c = 30;
int avg1 = average(2, a, b);
int avg2 = average(3, a, b,c);
printf("avg1 = %d\n",avg1);
printf("avg2 = %d\n",avg2);
return 0;
}
上述代码的执行结果:。
可以看到函数中定义出了几个之前未见过的符号,我们转到定义看看到底是什么:
typedef char * va_list; //类型重定义:将char* 定义为va_list
#define va_start _crt_va_start
#define va_arg _crt_va_arg
#define va_end _crt_va_end //这三个都是#define 定义的符号,不清楚是什么,再次转到定义:
#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define _crt_va_end(ap) ( ap = (va_list)0 )
//原来是三个宏,对其中不明白的符号再次转到定义:
#define _ADDRESSOF(v) ( &(v) )
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
//仍然是两个宏
哦,这时候先进行替换,方便理解:
va_list arg; 相当于 char* arg; //arg是个字符指针呀
va_start(arg, n); 相当于 _crt_va_start(arg, n);相当于(arg = (va_list)_ADDRESSOF(n) + _INTSIZEOF(n))
相当于 (arg = (char*)&n + ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
其中 ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) ;求出n的字节数,然后向上取整为4的倍数(换句话说: _INTSIZEOF(n)整个做的事情就是将n的长度化为int长度的整数倍)
所以在上述代码中相当于 arg = (char*)&n + 4;
//初始化arg为位置参数列表中的第一个参数的地址(如调用这个函数时用的参数为(int 3, 1,2,6),那么此时arg指向的是1所在的地址)
va_arg(arg, int); 相当于 _crt_va_arg(arg,int)
相当于 ( *(int *)((arg += _INTSIZEOF(int)) - _INTSIZEOF(int)) )
所以在上述代码中相当于 *(int *)(arg += 4) - 4;
//此时已经将arg重新赋值为可变参数列表中第二个参数的地址,但是此处保留的仍然是上一个参数的地址,然后对保留地址进行强制类型转换之后解以用得到内容(参数)
va_end(arg); 相当于 (arg = (va_list)0)
所以在上述代码中相当于 arg = char* 0; //把arg置为NULL
回到原来代码中进行替换。
为了更好的理解,我们把该程序的反汇编拷贝出来解析一下:(仅以int avg1 = average(2, a, b);本次调用为例)
int main() //调用main函数之前已经调用了其他函数
{
01154770 push ebp //将当前ebp的地址压栈到当前的栈顶,每次压栈esp都更新
01154771 mov ebp,esp //将当前的ebp移动到当前esp的位置处
01154773 sub esp,0FCh //将esp向低地址处移动OFCh,为main()开辟空间
01154779 push ebx //将寄存器ebx压入栈顶
0115477A push esi //将寄存器esi压入栈顶
0115477B push edi //将寄存器edi压入栈顶
0115477C lea edi,[ebp-0FCh] //将为main开辟空间时的esp的地址加载进入edi中
01154782 mov ecx,3Fh //ecx中放入3Fh
01154787 mov eax,0CCCCCCCCh //eax中放入0CCCCCCCCh
0115478C rep stos dword ptr es:[edi] //从edi中所存的地址处开始,用0CCCCCCCCh始化3Fh次
0115478E mov ecx,offset _FC008D77_test@c (0115C003h)
01154793 call @__CheckForDebuggerJustMyCode@4 (01151212h)
int a = 10;
01154798 mov dword ptr [a],0Ah //创建变量,dword ptr [a]中放入0Ah
int b = 20;
0115479F mov dword ptr [b],14h //创建变量,dword ptr [b]中放入14h
int c = 30;
011547A6 mov dword ptr [c],1Eh //创建变量,dword ptr [c]中放入1Eh
int avg1 = average(2, a, b); //调用函数前先进行传参(参数列表中从右向左)
011547AD mov eax,dword ptr [b] //eax中放入dword ptr [b]的内容
011547B0 push eax //调用average函数前将要用的形参放入寄存器并压栈
011547B1 mov ecx,dword ptr [a] //eax中放入dword ptr [b]的内容
011547B4 push ecx //调用average函数前将要用的形参放入寄存器并压栈
011547B5 push 2 //将参数2也压栈
011547B7 call _average (01151398h) //开始调用average函数(将此地址进行压栈/保护现场)
011547BC add esp,0Ch //esp回到原来的位置,将开辟的栈帧回收
011547BF mov dword ptr [avg1],eax //将eax中存的平均值放入avr1中准备打印
int average(int n, ...)
{
01151810 push ebp
01151811 mov ebp,esp
01151813 sub esp,0E4h
01151819 push ebx
0115181A push esi
0115181B push edi
0115181C lea edi,[ebp-0E4h]
01151822 mov ecx,39h
01151827 mov eax,0CCCCCCCCh
0115182C rep stos dword ptr es:[edi] //average()函数的栈帧开辟以及初始化过程,同main()
0115182E mov ecx,offset _FC008D77_test@c (0115C003h)
01151833 call @__CheckForDebuggerJustMyCode@4 (01151212h)
int i = 0;
01151838 mov dword ptr [i],0 //创建局部变量i并初始化为0;
int sum = 0;
0115183F mov dword ptr [sum],0 //创建局部变量sum并初始化为0;
va_list arg;
va_start(arg, n);
01151846 lea eax,[ebp+0Ch] //将当前的ebp+0Ch处的地址存放到eax中
01151849 mov dword ptr [arg],eax //将eax中的内容赋值给arg
for (i = 0; i < n; i++)
0115184C mov dword ptr [i],0 //进入循环,先把i用0赋值
01151853 jmp average+4Eh (0115185Eh) //跳转到0115185Eh处,执行eax,dword ptr [i]
01151855 mov eax,dword ptr [i] //由0115187Bh跳转过来
01151858 add eax,1 //执行++
0115185B mov dword ptr [i],eax //并重新赋值给i,继续进行循环
0115185E mov eax,dword ptr [i] //由01151853h跳转过来,将此时的i加载到eax中
01151861 cmp eax,dword ptr [n] //eax的值与变量n的值进行比较
01151864 jge average+6Dh (0115187Dh)
{
sum += va_arg(arg, int); //即: sum += (*(int *)(arg += 4) - 4);
01151866 mov eax,dword ptr [arg]
01151869 add eax,4
0115186C mov dword ptr [arg],eax //此时arg存放的是下一个参数(第3个参数)的地址
0115186F mov ecx,dword ptr [arg]
01151872 mov edx,dword ptr [sum]
01151875 add edx,dword ptr [ecx-4] //sum的值加上一次arg所指地址处的内容(第2个参数)
01151878 mov dword ptr [sum],edx //将求的和继续放在sum中
}
0115187B jmp average+45h (01151855h) //跳转到0115185Eh处,执行准备执行i++
return sum / n;
0115187D mov eax,dword ptr [sum] //将for循环结束之后的sum放入eax中
01151880 cdq
01151881 idiv eax,dword ptr [n] //用eax中的数值除以变量n的内容,得到平均值
01151884 jmp average+7Dh (0115188Dh)
va_end(arg); //即:arg = char* 0; //把arg置为NULL
01151886 mov dword ptr [arg],0
}
0115188D pop edi
0115188E pop esi
0115188F pop ebx //弹出栈顶的各种寄存器
01151890 add esp,0E4h //回收为average开辟的栈帧
01151896 cmp ebp,esp
01151898 call __RTC_CheckEsp (0115121Ch)
0115189D mov esp,ebp
0115189F pop ebp
011518A0 ret //最终将栈顶的元素取出作为一个地址跳转到该地址处回到main()函数)
再以一个简单的图表示一下:
int avg1 = average(2, a, b); 该语句调用average() 函数时,其参数列表中有三个参数,而且是从右向左依次压栈的。
因为参数列表中最左侧的参数表示的就是该参数之后参数的个数,所以压栈顺序它在函数栈帧的最上方。当调用函数时,读取这个参数就能在此基础上明确参数的总个数,在利用for循环和arg指针就能准确的完成该函数的功能。
所以average(); 函数不管有几个参数都可以按照上面的函数栈帧的运行规律正确读取参数个数并成功完成功能。
3、总结一下:
3.1 实现参数列表可变的函数的前提是:函数栈帧的压栈规律(参数列表中的参数是从右向左以此压栈的,同时函数栈帧是先使用高地址再使用低地址的)。
3.2 具有可变参数列表的函数在定义时,参数列表中的参数是不完全给定的,只给出第1个参数,后面用“...”表示,而且第1个参数必须是int类型,用于调用该函数时确定本次调用该函数所传的参数的个数(若传递n个参数,则第1个参数为n-1)。
3.3 一般都要用到 va_list arg; va_start(arg, n); va_arg(arg, int); va_end(arg)
3.4 有一点必须注意:va_arg(arg, int) 每使用一次这个函数,arg指针都会指向下一个参数,所以必须合理使用
4、可变参数的限制:
4.1 可变参数必须从头到尾依次访问,在依次访问中可以在中间某一参数处停止,但不能直接访问参数列表中间的参数。
4.2 参数列表中至善有一个命名参数,否则无法使用va_start。
4.3 上述用到的宏是无法直接判断实际存在的参数的数量。
4.4 如果在va_arg中执行了错误的类型,那么其后果是不可预测的。