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

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

专注于为中小企业提供做网站、网站建设服务,电脑端+手机端+微信端的三站合一,更高效的管理,为中小企业石城免费做网站提供优质的服务。我们立足成都,凝聚了一批互联网行业人才,有力地推动了上1000家企业的稳健成长,帮助中小企业通过网站建设实现规模扩充和转变。

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

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

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

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

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

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

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

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

测试结果如下:

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

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

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

用下面的代码进行测试:

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

结果如下:

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

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

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

运行结果如下:

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

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

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

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

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

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

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

标题名称:浅谈C/C++中可变参数的原理
本文URL:http://www.hantingmc.com/qtweb/news27/507877.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联