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最后入栈,
栈帧如下:
可变参函数栈帧.png

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"等格式说明符来判断参数的个数以及类型,从而正确的取出对应参数。

 

转载于:https://www.cnblogs.com/UnknowCodeMaker/p/11114138.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值