什么是可变参数列表?以及可变参数列表是如何实现的?

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中执行了错误的类型,那么其后果是不可预测的。

猜你喜欢

转载自blog.csdn.net/DreamIPossible/article/details/108277473