浅谈C/C++中可变参数的原理
要理解可变参数,首先要理解函数调用约定, 为什么只有__cdecl的调用约定支持可变参数,而__stdcall就不支持?
实际上__cdecl和__stdcall函数参数都是从右到左入栈,它们的区别在于由谁来清栈,__cdecl由外部调用函数清栈,而__stdcall由被调用函数本身清栈, 显然对于可变参数的函数,函数本身没法知道外部函数调用它时传了多少参数,所以没法支持被调用函数本身清栈(__stdcall), 所以可变参数只能用__cdecll.
另外还要理解函数参数传递过程中堆栈是如何生长和变化的,从堆栈低地址到高地址,依次存储 被调用函数局部变量,上一函数堆栈桢基址,函数返回地址,参数1, 参数2, 参数3...,相关知识可以参考我的这篇堆栈桢的生成原理
有了上面的知识,我可以知道函数调用时,参数2的地址就是参数1的地址加上参数1的长度,而参数3的地址是参数2的地址加上参数2的长度,以此类推。
于是我们可以自己写可变参数的函数了, 代码如下:
- int Sum(int nCount, )
- {
- int nSum = 0;
- int* p = &nCount;
- for(int i=0; i<nCount; ++i)
- {
- cout << *(++p) << endl;
- nSum += *p;
- }
- cout << "Sum:" << nSum << endl << endl;
- return nSum;
- }
- string SumStr(int nCount, )
- {
- string str;
- int* p = &nCount;
- for(int i=0; i<nCount; ++i)
- {
- char* pTemp = (char*)*(++p);
- cout << pTemp << endl;
- str += pTemp;
- }
- cout << "SumStr:" << str << endl;
- return str;
- }
在我们的测试函数中nCount表示后面可变参数的个数,int Sum(int nCount, )会打印后面的可变参数Int值,并且进行累加;string SumStr(int nCount, ) 会打印后面可变参数字符串内容,并连接所有字符串。
然后用下面代码进行测试:int main()
- {
- Sum(3, 10, 20, 30);
- SumStr(5, "aa", "bb", "cc", "dd", "ff");
- system("pause");
- return 0;
- }
测试结果如下:
可以看到,我们上面的实现有硬编码的味道,也有没有做字节对齐,为此系统专门给我们封装了一些支持可变参数的宏:
- //typedef char * va_list;
- //#define _ADDRESSOF(v) ( &reinterpret_cast<const char &>(v) )
- //#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 )
- //#define va_start _crt_va_start
- //#define va_arg _crt_va_arg
- //#define va_end _crt_va_end
- //#define _ADDRESSOF(v) ( &reinterpret_cast<const char &>(v) )
- //#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 )
- //#define va_start _crt_va_start
- //#define va_arg _crt_va_arg
- //#define va_end _crt_va_end
用系统的这些宏,我们的代码可以这样写了:
- //use va_arg, praram is int
- int SumNew(int nCount, )
- {
- int nSum = 0;
- va_list vl = 0;
- va_start(vl, nCount);
- for(int i=0; i<nCount; ++i)
- {
- int n = va_arg(vl, int);
- cout << n << endl;
- nSum += n;
- }
- va_end(vl);
- cout << "SumNew:" << nSum << endl << endl;
- return nSum;
- }
- //use va_arg, praram is char*
- string SumStrNew(int nCount, )
- {
- string str;
- va_list vl = 0;
- va_start(vl, nCount);
- for(int i=0; i<nCount; ++i)
- {
- char* p = va_arg(vl, char*);
- cout << p << endl;
- str += p;
- }
- cout << "SumStrNew:" << str << endl << endl;
- return str;
- }
可以看到,其中 va_list实际上只是一个参数指针,va_start根据你提供的最后一个固定参数来获取第一个可变参数的地址,va_arg将指针指向下一个可变参数然后返回当前值, va_end只是简单的将指针清0.
用下面的代码进行测试:
- int main()
- {
- Sum(3, 10, 20, 30);
- SumStr(5, "aa", "bb", "cc", "dd", "ff");
- SumNew(3, 1, 2, 3);
- SumStrNew(3, "12", "34", "56");
- system("pause");
- return 0;
- }
结果如下:
我们上面的例子传的可变参数都是4字节的, 如果我们的可变参数传的是一个结构体,结果会怎么样呢?
下面的例子我们传的可变参数是std::string
- //use va_arg, praram is std::string
- void SumStdString(int nCount, )
- {
- string str;
- va_list vl = 0;
- va_start(vl, nCount);
- for(int i=0; i<nCount; ++i)
- {
- string p = va_arg(vl, string);
- cout << p << endl;
- str += p;
- }
- cout << "SumStdString:" << str << endl << endl;
- }
- int main()
- {
- Sum(3, 10, 20, 30);
- SumStr(5, "aa", "bb", "cc", "dd", "ff");
- SumNew(3, 1, 2, 3);
- SumStrNew(3, "12", "34", "56");
- string s1("hello ");
- string s2("world ");
- string s3("!");
- SumStdString(3, s1, s2, s3);
- system("pause");
- 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
【编辑推荐】
相关文章
- Java要抛弃祖宗的基业,Java程序员危险了!
- 十大 Java 语言特性
- JVM 三色标记算法,原来是这么回事!
- 聊聊 Spring 事务控制策略以及 @Transactional 失效问题避坑
- 写给 Java 程序员的前端 Promise 教程
- 写给 Java 程序员的前端 Promise 教程,你学会了吗?
- Java 中为什么不全部使用 Static 方法?
- Java 池化技术你了解多少?
- Java 服务 Docker 容器化优秀实践
- Spring Boot + EasyExcel导入导出,简直太好用了!
- 我们一起聊聊 Java 内存泄漏
- CentOS 下安装 Docker 极简教程
- JDK 19 功能集冻结:Java 19 只有七个新特性
- 关于 CMS 垃圾回收器,你真的懂了吗?
- 为什么会有这么多编程语言?
- 改善Java代码的八个建议
- 接口流量突增,如何做好性能优化?
- Java 以编程方式创建JAR文件
- POJO、Java Bean是如何定义的
- Spring 的 Bean 明明设置了 Scope 为 Prototype,为什么还是只能获取到单例对象?