C++学习---变长参数(stdarg.h)的实现原理

时间:2022-12-04 13:58:18

引用

C++ 中对stdarg.h头文件进行了封装,该头文件实现了函数变长参数,能够在定义函数时不必完全指定参数个数,而编译器能够在代码编译时,拿到所有的参数,并进行相应的处理。

stdarg.h中定义了va_list类型,va_start/va_arg/va_end/va_copy4个宏,我们具体探究一下其实现原理。

变长参数定义与原理

定义

变长参数是C语言中的特殊参数形式,函数声明如下:

​int printf(const char* format, ...)​​ 这样的声明表示,该函数除了第一个参数类型为const char*之外,后面可以追加任意数量和类型的参数;

在函数实现的过程中,我们使用stdarg.h中的宏来依次访问后续额外的参数。

使用方式

假设变长参数函数的最后一个具名参数(如上面的format)为lastarg,那么我们需要在函数实现中定义va_list类型的变量如下:

​valist ap;​

该变量后续将会指向每一个未知参数,首先我们需要使用va_start对ap进行初始化,注意,这里会用到lastarg:

​va_start(ap, lastarg);​

然后,我们使用va_arg来依次获取下一个不确定的参数,假设该参数的类型为T:

​T next = va_arg(ap, T);​

在函数结束之前,我们使用va_end清理现场:

​va_end(ap)​

实现原理

C语言中默认的cdecl调用惯例是自右往左压栈传递函数参数,例如函数(后面跟着count个整数,计算它们的和)

​int sum(int count,...);​

那么对应调用​​sum(3,1,2,5)​​,此时,在栈上面地址由低到高是3,1,2,5,这样,我们知道了第一个参数count的地址,那么也能够依次获知上面三个参数的地址,最后计算出结果:

int sum(int count,...){
int* p = &count + 1;
int ret_sum = 0;
while(count--)
ret_sum += *p++;
return ret_sum;
}

但是,实际上的过程中,后续每一个参数的类型都不一样,我们需要增加的地址数也不一样,所以我们需要进行改进,使用void或者char指针替代明确的指针int*。

  • va_list是一个指针,选择void或char;
  • va_start将va_list指向最后一个具名参数后面的位置,即第一个不确定参数的位置;
  • va_arg获取当前参数的值,同时将指针指向下一个参数;
  • va_end将指针置为0。

所以,我们的宏这样定义:

#define va_list char*
#define va_start(ap,arg) (ap=(va_list)&arg+sizeof(arg))
#define va_arg(ap,t) (*(t*)((ap+=sizeof(t))-sizeof(t)))
#define va_end(ap) (ap=(va_list)0)

注意:

va_start中获取到arg地址后还要再加sizeof(arg)才是第一个不确定参数的位置;

va_arg中先将ap+=sizeof(t),然后再减去sizeof(t)得到当前的参数,但不影响ap的递增。

例子如下:

void PrintFloats (int n, ...){
int i;
double val;
printf ("Printing floats:");
va_list vl;
va_start(vl,n);
for (i=0;i<n;i++)
{
val=va_arg(vl,double);
printf (" [%.2f]",val);
}
va_end(vl);
printf ("\n");
}

int main (){
PrintFloats (3,3.14159,2.71828,1.41421);
return 0;
}
//测试结果:
Printing floats: [3.14] [2.72] [1.41]