`
chinamming
  • 浏览: 140539 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

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

 
阅读更多
以前只是知道可变参数怎么用,但是一直对它的原理是似懂非懂,现在对计算机有了比较深刻的认识之后,回头再看,豁然开朗。

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

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

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

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

于是我们可以自己写可变参数的函数了, 代码如下:
intSum(intnCount,)
{
intnSum=0;
int*p=&nCount;
for(inti=0;i<nCount;++i)
{
cout<<*(++p)<<endl;
nSum+=*p;
}

cout<<"Sum:"<<nSum<<endl<<endl;
returnnSum;
}

stringSumStr(intnCount,)
{
stringstr;
int*p=&nCount;

for(inti=0;i<nCount;++i)
{
char*pTemp=(char*)*(++p);
cout<<pTemp<<endl;
str+=pTemp;
}

cout<<"SumStr:"<<str<<endl;
returnstr;
}

在我们的测试函数中nCount表示后面可变参数的个数,intSum(intnCount,)会打印后面的可变参数Int值,并且进行累加;stringSumStr(intnCount,)会打印后面可变参数字符串内容,并连接所有字符串。
然后用下面代码进行测试:intmain()
{
Sum(3,10,20,30);
SumStr(5,"aa","bb","cc","dd","ff");

system("pause");

return0;
}

测试结果如下:


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

//#define_ADDRESSOF(v)(&reinterpret_cast<constchar&>(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

用系统的这些宏,我们的代码可以这样写了:
//useva_arg,praramisint
intSumNew(intnCount,)
{
intnSum=0;
va_listvl=0;
va_start(vl,nCount);

for(inti=0;i<nCount;++i)
{
intn=va_arg(vl,int);
cout<<n<<endl;
nSum+=n;
}

va_end(vl);
cout<<"SumNew:"<<nSum<<endl<<endl;
returnnSum;
}

//useva_arg,praramischar*
stringSumStrNew(intnCount,)
{
stringstr;
va_listvl=0;
va_start(vl,nCount);

for(inti=0;i<nCount;++i)
{
char*p=va_arg(vl,char*);
cout<<p<<endl;
str+=p;
}

cout<<"SumStrNew:"<<str<<endl<<endl;
returnstr;
}

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

用下面的代码进行测试:
intmain()
{
Sum(3,10,20,30);
SumStr(5,"aa","bb","cc","dd","ff");

SumNew(3,1,2,3);
SumStrNew(3,"12","34","56");

system("pause");

return0;
}

结果如下:


我们上面的例子传的可变参数都是4字节的, 如果我们的可变参数传的是一个结构体,结果会怎么样呢?
下面的例子我们传的可变参数是std::string
//useva_arg,praramisstd::string
voidSumStdString(intnCount,)
{
stringstr;
va_listvl=0;
va_start(vl,nCount);

for(inti=0;i<nCount;++i)
{
stringp=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等。

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

相关推荐

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

    要理解可变参数,首先要理解函数调用约定, 为什么只有__cdecl的调用约定支持可变参数,而__stdcall不支持?  实际上__cdecl和__stdcall函数参数都是从右到左入栈,它们的区别在于由谁来清栈,__cdecl由外部调用...

    关于C/C++中可变参数的详细介绍(va_list,va_start,va_arg,va_end)

    可变参数的函数原理其实很简单,而va系列是以宏定义来定义的,实现跟堆栈相关.我们写一个可变函数的C函数时,有利也有弊,所以在...如果在C++里,我们应该利用C++的多态性来实现可变参数的功能,尽量避免用C语言的方式来实现

    C语言参数个数可变函数详解

    exec*()系列函数,用于在程序中执行外部文件(main(int argc,char*argv[])算不算呢,与其说main()也是一个可变参数函数,倒不如说它是exec*()经过封装后的具备特殊功能和意义的函数,至少在原理这一级上有很多相似之...

    C++可变参数的实现方法

    可变参数的实现要解决三个问题: 1.如何调用带有可变参数的函数...printf和scanf是C语言标准库中最常见的可变参数函数, printf的签名是 代码如下:int printf(const char* format, …); 其中,… 表示可变参数,现在模仿

    C++可变参数的另一种实现

     ebp是帧指针寄存器,一般用来存取堆栈,有了堆栈结构,下面我们看看C可变参数的具体实现原理: #include enum { ptChar, ptInt, ptFloat, ptDouble, }; void printSum(unsigned

    C语言的可变参数表函数的设计

    首先在介绍可变参数表函数的设计之前,我们先来介绍一下最经典的可变参数表printf函数的实现原理。在C/C++中,对函数参数的扫描是从后向前的。C/C++的函数参数是通过压入堆栈的方式来给函数传参数的(堆栈是一种先进...

    c语言难点分析整理,C语言

    7. C中可变参数函数实现 38 8. C程序内存中组成部分 41 9. C编程拾粹 42 10. C语言中实现数组的动态增长 44 11. C语言中的位运算 46 12. 浮点数的存储格式: 50 13. 位域 58 14. C语言函数二维数组传递方法 64 15. ...

    免费下载:C语言难点分析整理.doc

    7. C中可变参数函数实现 38 8. C程序内存中组成部分 41 9. C编程拾粹 42 10. C语言中实现数组的动态增长 44 11. C语言中的位运算 46 12. 浮点数的存储格式: 50 13. 位域 58 14. C语言函数二维数组传递方法 64 15. ...

    史上最强的C语言资料

    7. C中可变参数函数实现 38 8. C程序内存中组成部分 41 9. C编程拾粹 42 10. C语言中实现数组的动态增长 44 11. C语言中的位运算 46 12. 浮点数的存储格式: 50 13. 位域 58 14. C语言函数二维数组传递方法 64 15. ...

    高级C语言详解

    7. C中可变参数函数实现 38 8. C程序内存中组成部分 41 9. C编程拾粹 42 10. C语言中实现数组的动态增长 44 11. C语言中的位运算 46 12. 浮点数的存储格式: 50 13. 位域 58 14. C语言函数二维数组传递方法 64 15. ...

    高级C语言 C 语言编程要点

    7. C中可变参数函数实现 38 8. C程序内存中组成部分 41 9. C编程拾粹 42 10. C语言中实现数组的动态增长 44 11. C语言中的位运算 46 12. 浮点数的存储格式: 50 13. 位域 58 14. C语言函数二维数组传递方法 64 15. ...

    C语言难点分析整理

    7. C中可变参数函数实现 38 8. C程序内存中组成部分 41 9. C编程拾粹 42 10. C语言中实现数组的动态增长 44 11. C语言中的位运算 46 12. 浮点数的存储格式: 50 13. 位域 58 14. C语言函数二维数组传递方法 64 15. ...

    高级进阶c语言教程..doc

    7. C中可变参数函数实现 38 8. C程序内存中组成部分 41 9. C编程拾粹 42 10. C语言中实现数组的动态增长 44 11. C语言中的位运算 46 12. 浮点数的存储格式: 50 13. 位域 58 14. C语言函数二维数组传递方法 64 15. ...

    C语言难点分析整理.doc

    7. C中可变参数函数实现 38 8. C程序内存中组成部分 41 9. C编程拾粹 42 10. C语言中实现数组的动态增长 44 11. C语言中的位运算 46 12. 浮点数的存储格式: 50 13. 位域 58 14. C语言函数二维数组传递方法 ...

    c语言va函数讲解.

    VA 函数(variable argument function),参数个数可变函数,又称可变参数 函数。C/C++编程中,系统提供给编程人员的va 函数很少。*printf()/*scanf() 系列函数,用于输入输出时格式化字符串;exec*()系列函数,用于...

    C++MFC教程

    而且C++本身所具备的超越C语言的特性都可以使开发者编写出更易用,更灵活的代码。 在MFC中对消息的处理利用了消息映射的方法,该方法的基础是宏定义实现,通过宏定义将消息分派到不同的成员函数进行处理。下面简单...

    C++网络爬虫项目

    由WebCrawler对象在从MultiIo对象中等到套接字描述符可读时动态创建,通 过Socket对象接收超文本传输协议响应。WEBCRAWLER 网络爬虫实训项目 9 2.3.4. 网络爬虫(WebCrawler) 代表整个应用程序的逻辑对象,构建并维护...

    Visual C++ 2005入门经典--源代码及课后练习答案

    5.5.1 接受数量可变实参的函数 242 5.5.2 main( )的实参 243 5.6 小结 244 5.7 练习 245 第6章 程序结构(2) 246 6.1 函数指针 246 6.1.1 声明函数指针 247 6.1.2 函数指针作为实参 250 6.1.3 函数...

    -C++参考大全(第四版) (2010 年度畅销榜

    3.1 C和C++中的真值和假值 3.2 选择语句 3.3 迭代语句 3.4 在选择和迭代语句内声明变量 3.5 跳转语句 3.6 表达式语句 3.7 块语句 第4章 数组和以null结束的字符串 4.1 一维数组 4.2 生成指向数组的指针 4.3 向函数...

Global site tag (gtag.js) - Google Analytics