C语言可变参函数分析
可变参函数指的是参数总个数不固定,只有调用时才知道到底有多少个参数
下面实现一个任意多个整数求和的可变参函数:
int Sum(int nItemCount, ...) { int i = 0, sum = 0; va_list vp; va_start(vp, nItemCount); for (i = 0; i < nItemCount; i++) { sum += va_arg(vp, int); } va_end(vp); return sum; } int main() { Sum(6, 1, 2, 3, 4, 5, 6); return 0; }
先铺垫一下:
该函数调用约定没有指明,VC++编译器默认为_cdecl,那么其传参方向从右到左,nItemCount最后入栈,
栈帧如下:
VC++中va_list,va_start,va_arg,va_end定义如下:
typedef char * va_list; #define va_start _crt_va_start #define va_arg _crt_va_arg #define va_end _crt_va_end #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) #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 ) #ifdef __cplusplus #define _ADDRESSOF(v) ( &reinterpret_cast<const char &>(v) ) #else /* __cplusplus */ #define _ADDRESSOF(v) ( &(v) ) #endif /* __cplusplus */
va_list其实就是char*的别名,我这里编译的是Windows 32位程序,所以VC++将参数变量和局部变量的起始
地址都安排在能够模4(对4取余结果为0)地地址,那么在可变参函数内要正确的取出参数,则需要计算每一个
参数的起始地址,_crt_va_start和_crt_va_arg宏就实现了取出参数的功能,先来看看_crt_va_start:
_crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
_crt_va_start宏的功能就是取出可变参数列表("..."表示的那一部分参数)中的第一个参数(对于上面例子来说,也就是函数Sum(int nItemCount, ...)中紧挨着nItmecount的下一个参数)因为可变参函数的第一个参数是明确的(对于上面例子来说也就是Sum函数的nItemCount参数),所以可变参数列表中的第一个参数的内存地址为:
nItemCount的内存地址+nItemCount模4对齐后的大小
而_INTSIZEOF宏就是计算一个参数类型模4对齐后的大小,分析一下它:
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
它用到了这样一个数学技巧:一个整数M要想成为整数N的最小整数倍,则需要用S=((M+N-1)/N)*N计算,S就是大于等于M
的整数中能够模N的最小的那个整数
例如: 3对4取余不为0,S=((3+4-1)/4)*4=4,这里4就是大于3的整数中能够模4的最小整数
5对4取余不为0,S=((5+4-1)/4)*4=8,这里8就是大于5的整数中能够模4的最小整数
对于这个公式:S=((M+N-1)/N)*N,当N为2的n次方的时候,可以用移位代替除法:((M+N-1)>>n)<<n,而_INTSIZEOF
中,是对sizeof(int)进行对齐,所以公式就变为((M+N-1)>>2)<<2,也就是将(M+N-1)的先右移两个二进制位,在左移
两个二进制位,其实相当于清零了(M+N-1)的低两位,sizeof(int) - 1结果为3,转换成二进制就是0x00000011,在用
"~"取反的到结果0x11111100,在和 ( (sizeof(n) + sizeof(int) - 1) 进行位与运算即可将其低两位清零,从而
达到目的。
再来看看_crt_va_arg(ap,t)这个宏,这个宏的作用就是取出当前参数并将指针指向下一个参数,这个宏原理_crt_va_start一样,都需要上一个参数的起始内存地址和上一个参数的类型,才能推导出下一个参数的内存地址,
分析一下:
ap += _INTSIZEOF(t)使得ap指向下一个参数,(ap += _INTSIZEOF(t)) - _INTSIZEOF(t)将得到一个指向上
一个参数的指针(这是个右值),((t)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))将这个指针(右值)强转为t类
型的指针,然后在取值
最后_crt_va_end(ap)宏进行收尾工作,将指针ap置为NULL
一般可变参函数第一个参数都是记录可变参数的个数,例如printf和scanf的第一个参数就是格式化字符串,其内部通过
解析格式化字符串中的"%c,%d"等格式说明符来判断参数的个数以及类型,从而正确的取出对应参数。