浅谈C/C++中可变参数的原理

    要理解可变参数,首先要理解函数调用约定, 为什么只有__cdecl的调用约定支持可变参数,而__stdcall就不支持?

    实际上__cdecl和__stdcall函数参数都是从右到左入栈,它们的区别在于由谁来清栈,__cdecl由外部调用函数清栈,而__stdcall由被调用函数本身清栈, 显然对于可变参数的函数,函数本身没法知道外部函数调用它时传了多少参数,所以没法支持被调用函数本身清栈(__stdcall), 所以可变参数只能用__cdecll.

    另外还要理解函数参数传递过程中堆栈是如何生长和变化的,从堆栈低地址到高地址,依次存储 被调用函数局部变量,上一函数堆栈桢基址,函数返回地址,参数1, 参数2, 参数3...,

    有了上面的知识,我可以知道函数调用时,参数2的地址就是参数1的地址加上参数1的长度,而参数3的地址是参数2的地址加上参数2的长度,以此类推。

    于是我们可以自己写可变参数的函数了, 代码如下:

       
       
    1. int Sum(int nCount, ) 
    2.     int nSum = 0; 
    3.     int* p = &nCount; 
    4.     for(int i=0; i<nCount; ++i) 
    5.     { 
    6.         cout << *(++p) << endl; 
    7.         nSum += *p; 
    8.     } 
    9.  
    10.     cout << "Sum:" << nSum << endl << endl; 
    11.     return nSum; 
    12.  
    13. string  SumStr(int nCount, ) 
    14.     string str; 
    15.     int* p = &nCount; 
    16.  
    17.     for(int i=0; i<nCount; ++i) 
    18.     { 
    19.         char* pTemp = (char*)*(++p); 
    20.         cout <<  pTemp << endl; 
    21.         str += pTemp; 
    22.     } 
    23.  
    24.     cout << "SumStr:" << str << endl; 
    25.     return str; 

    在我们的测试函数中nCount表示后面可变参数的个数,int Sum(int nCount, )会打印后面的可变参数Int值,并且进行累加;string  SumStr(int nCount, ) 会打印后面可变参数字符串内容,并连接所有字符串。

    然后用下面代码进行测试:int main() 

       
       
    1.     Sum(3, 10, 20, 30); 
    2.     SumStr(5, "aa""bb""cc""dd""ff"); 
    3.      
    4.     system("pause"); 
    5.  
    6.     return 0; 

    测试结果如下:

    可以看到,我们上面的实现有硬编码的味道,也有没有做字节对齐,为此系统专门给我们封装了一些支持可变参数的宏:

        
        
    1. //typedef char *  va_list; 
    2. //#define _ADDRESSOF(v)   ( &reinterpret_cast<const char &>(v) ) 
    3. //#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) 
    4. //#define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) ) 
    5. //#define _crt_va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) 
    6. //#define _crt_va_end(ap)      ( ap = (va_list)0 ) 
    7. //#define va_start _crt_va_start 
    8. //#define va_arg _crt_va_arg 
    9. //#define va_end _crt_va_end 
    10. //#define _ADDRESSOF(v)   ( &reinterpret_cast<const char &>(v) ) 
    11. //#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) 
    12. //#define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) ) 
    13. //#define _crt_va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) 
    14. //#define _crt_va_end(ap)      ( ap = (va_list)0 ) 
    15. //#define va_start _crt_va_start 
    16. //#define va_arg _crt_va_arg 
    17. //#define va_end _crt_va_end 

    用系统的这些宏,我们的代码可以这样写了:

       
       
    1. //use va_arg, praram is int 
    2. int SumNew(int nCount, ) 
    3.     int nSum = 0; 
    4.     va_list vl = 0; 
    5.     va_start(vl, nCount); 
    6.  
    7.     for(int i=0; i<nCount; ++i) 
    8.     { 
    9.         int n = va_arg(vl, int); 
    10.         cout << n << endl; 
    11.         nSum += n; 
    12.     } 
    13.  
    14.     va_end(vl); 
    15.     cout << "SumNew:" << nSum << endl << endl; 
    16.     return nSum; 
    17.  
    18. //use va_arg,  praram is char* 
    19. string SumStrNew(int nCount, ) 
    20.     string str; 
    21.     va_list vl = 0; 
    22.     va_start(vl, nCount); 
    23.  
    24.     for(int i=0; i<nCount; ++i) 
    25.     { 
    26.         char* p = va_arg(vl, char*); 
    27.         cout <<  p << endl; 
    28.         str += p; 
    29.     } 
    30.  
    31.     cout << "SumStrNew:" << str << endl << endl; 
    32.     return str; 

    可以看到,其中 va_list实际上只是一个参数指针,va_start根据你提供的最后一个固定参数来获取第一个可变参数的地址,va_arg将指针指向下一个可变参数然后返回当前值, va_end只是简单的将指针清0.

    用下面的代码进行测试:

       
       
    1. int main()  
    2.     Sum(3, 10, 20, 30); 
    3.     SumStr(5, "aa", "bb", "cc", "dd", "ff"); 
    4.      
    5.     SumNew(3, 1, 2, 3); 
    6.     SumStrNew(3, "12", "34", "56"); 
    7.  
    8.     system("pause"); 
    9.  
    10.     return 0; 

    结果如下: 

    我们上面的例子传的可变参数都是4字节的, 如果我们的可变参数传的是一个结构体,结果会怎么样呢?

    下面的例子我们传的可变参数是std::string

        
        
    1. //use va_arg,  praram is std::string 
    2. void SumStdString(int nCount, ) 
    3.     string str; 
    4.     va_list vl = 0; 
    5.     va_start(vl, nCount); 
    6.  
    7.     for(int i=0; i<nCount; ++i) 
    8.     { 
    9.         string p = va_arg(vl, string); 
    10.         cout <<  p << endl; 
    11.         str += p; 
    12.     } 
    13.  
    14.     cout << "SumStdString:" << str << endl << endl; 
    15. int main()  
    16. Sum(3, 10, 20, 30); 
    17. SumStr(5, "aa""bb""cc""dd""ff"); 
    18. SumNew(3, 1, 2, 3); 
    19. SumStrNew(3, "12""34""56"); 
    20. string s1("hello "); 
    21. string s2("world "); 
    22. string s3("!"); 
    23. SumStdString(3, s1, s2, s3); 
    24. system("pause"); 
    25. return 0; 

    运行结果如下:

    可以看到即使传入的可变参数是std::string, 依然可以正常工作。

    我们可以反汇编下看看这种情况下的参数传递过程:

    很多时候编译器在传递类对象时,即使是传值,也会在堆栈上通过push对象地址的方式来传递,但是上面显然没有这么做,因为它要满足可变参数的调用约定,

    另外,可以看到最后在调用sumStdString后,由add esp, 58h来外部清栈。

    一个std::string大小是28, 58h = 88 = 28 + 28 + 28 + 4.

    从上面的例子我们可以看到,对于可变参数的函数,有2种东西需要确定,一是可变参数的数量, 二是可变参数的类型,上面的例子中,参数数量我们是在第一个参数指定的,参数类型我们是自己约定的。这种方式在实际使用中显然是不方便,于是我们就有了_vsprintf, 我们根据一个格式化字符串的来表示可变参数的类型和数量,比如C教程中入门就要学习printf, sprintf等。

    总的来说可变参数给我们提供了很高的灵活性和方便性,但是也给会造成不确定性,降低我们程序的安全性,很多时候可变参数数量或类型不匹配,就会造成一些不容察觉的问题,只有更好的理解它背后的原理,我们才能更好的驾驭它。

    原文链接:http://www.cnblogs.com/weiym/archive/2012/09/18/2689917.html

    • 0
      点赞
    • 0
      收藏
      觉得还不错? 一键收藏
    • 0
      评论
    C++ 可变参数和非可变参数的概念也存在。 非可变参数就是指函数在定义时,参数的数量和类型已经确定,调用时需要传入和定义时一样数量和类型的参数。例如: ```cpp int add(int x, int y) { return x + y; } int result = add(2, 3); std::cout << result << std::endl; // 输出 5 ``` 可变参数C++ 通常使用可变参数模板实现,即模板参数数量不确定,参数类型也不确定,需要在函数调用时确定具体类型和数量。C++11 引入了 `std::initializer_list`,可以方便地实现可变参数模板。 例如,下面是一个求和函数的可变参数实现: ```cpp #include <iostream> #include <initializer_list> template <typename T> T sum(std::initializer_list<T> args) { T result = 0; for (auto i : args) { result += i; } return result; } int main() { auto result1 = sum({2, 3}); auto result2 = sum({2, 3, 4, 5}); std::cout << result1 << std::endl; // 输出 5 std::cout << result2 << std::endl; // 输出 14 return 0; } ``` 需要注意的是,可变参数模板必须放在函数参数列表的最后,否则编译器无法正确推断类型。同时,可变参数模板也可以和非可变参数一起使用。例如: ```cpp template <typename T> T add(T x, T y) { return x + y; } template <typename T, typename... Args> T sum(T first, Args... args) { return add(first, sum(args...)); } int main() { auto result1 = sum(2, 3); auto result2 = sum(2, 3, 4, 5); std::cout << result1 << std::endl; // 输出 5 std::cout << result2 << std::endl; // 输出 14 return 0; } ``` 在这个例子,`sum` 函数的第一个参数是非可变参数,后面的参数是可变参数。函数的实现可变参数使用了递归调用,将第一个参数和后面的参数分别相加,最终得到总和。

    “相关推荐”对你有帮助么?

    • 非常没帮助
    • 没帮助
    • 一般
    • 有帮助
    • 非常有帮助
    提交
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值