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

  • 相关阅读:
    软件开发术语定义
    软件开发流程纲要及各个阶段产生的文档
    Java封装自己的Api
    Java中如何使封装自己的类,建立并使用自己的类库?
    Struts2中ActionContext和ServletActionContext
    TP-Link 无线路由器设置图文教程----怎么设置TP-Link无线路由器图解
    数据库(第一范式,第二范式,第三范式)
    ORACLE配置tnsnames.ora文件实例
    Windows下64位Apache服务器的安装
    公司内部Oracle RAC测试环境的简单使用说明.
  • 原文地址:https://www.cnblogs.com/UnknowCodeMaker/p/11114138.html
Copyright © 2011-2022 走看看