引用
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的地址,那么也能够依次获知上面三个参数的地址,最后计算出结果:
但是,实际上的过程中,后续每一个参数的类型都不一样,我们需要增加的地址数也不一样,所以我们需要进行改进,使用void或者char指针替代明确的指针int*。
- va_list是一个指针,选择void或char;
- va_start将va_list指向最后一个具名参数后面的位置,即第一个不确定参数的位置;
- va_arg获取当前参数的值,同时将指针指向下一个参数;
- va_end将指针置为0。
所以,我们的宏这样定义:
注意:
va_start中获取到arg地址后还要再加sizeof(arg)才是第一个不确定参数的位置;
va_arg中先将ap+=sizeof(t),然后再减去sizeof(t)得到当前的参数,但不影响ap的递增。
例子如下: