以前只是知道可变参数怎么用,但是一直对它的原理是似懂非懂,现在对计算机有了比较深刻的认识之后,回头再看,豁然开朗。
要理解可变参数,首先要理解函数调用约定, 为什么只有__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++可变参数函数的参数传递机制剖析 #### 摘要 本文深入探讨了C/C++语言中可变参数函数的参数传递机制,并提出了一种更加精确且灵活的设计方法来处理这类函数。通过分析,我们不仅理解了如何在函数内部访问...
在C/C++编程语言中,可变参数函数是一种允许开发者传递不同数量参数的函数,它在许多场景下非常有用,比如在打印日志、格式化输出或者错误处理时。本篇将深入剖析C/C++中可变参数函数的参数传递机制。 首先,我们...
C/C++语言可变参数表深层探索 C/C++支持函数接受可变数量的参数,这对于创建灵活的接口非常有用。通过使用`va_list`、`va_start`、`va_arg`和`va_end`宏,程序员可以定义和处理具有不确定参数数量的函数,这对于...
C/C++支持可变参数列表,允许函数接受不确定数量的参数。这在某些场景下非常有用,如日志记录函数、打印函数等。通过`va_list`、`va_start`、`va_arg`和`va_end`宏,可以访问和处理可变参数列表中的元素。 #### 七...
要理解可变参数,首先要理解函数调用约定, 为什么只有__cdecl的调用约定支持可变参数,而__stdcall不支持? 实际上__cdecl和__stdcall函数参数都是从右到左入栈,它们的区别在于由谁来清栈,__cdecl由外部调用...
通过上述分析,我们了解到C语言中可变参数函数的实现原理及其在编译器层面的具体处理方式。理解这些概念有助于开发者更好地设计和使用可变参数函数,特别是在需要处理数量不确定的参数时。需要注意的是,可变参数...
7. **C/C++语言可变参数表深层探索** - **可变参数列表**:了解如何使用 `va_list`、`va_start` 和 `va_end` 等宏来处理可变数量的参数。 8. **C/C++数组名与指针区别深层探索** - **数组名与指针的区别**:虽然...
在C/C++编程中,可变参数和默认参数是两种重要的特性,它们允许开发者编写更加灵活的函数。本文主要探讨了这两种特性的概念、工作原理以及如何在实际编程中使用。 首先,C语言中的可变参数功能使得函数能够接受一个...
OSIP库包含了解析器、构建器和事务管理器等组件,使得在C/C++中实现SIP功能变得更加便捷。例如,通过`osip_message_init()`初始化一个SIP消息对象,然后使用`osip_message_parse()`解析接收到的SIP报文,`osip_...
最优化算法是计算机科学和...在实际应用中,需要根据问题的特性和需求选择合适的最优化算法,并在C/C++中实现和调整参数以获得最佳效果。同时,理解这些算法的原理并进行调试,可以帮助我们更好地理解和改进优化过程。
在编程领域,C和C++语言因其高效、灵活性和广泛的应用而备受青睐。然而,由于它们的特性,如果不遵循一定的编码规范,代码可能会变得难以理解和维护。本篇将深入探讨"C/C++编码规范",旨在提供一套通用的指导原则,...
在C/C++编程中,可变参数是一种处理不定数量参数的方法。这主要通过一组预定义的宏来实现,包括`va_list`、`va_start`、`va_arg`和`va_end`,这些宏定义在`stdarg.h`头文件中。下面我们将详细探讨这些宏的用途和工作...
C/C++的`va_list`、`va_start`、`va_end`和`va_arg`宏提供了处理可变参数列表的能力,允许函数在运行时根据传入的参数个数和类型动态地处理数据。 #### 8. C/C++数组名与指针区别深层探索 虽然在许多情况下数组名...
4. **可变参数宏**:`__VA_ARGS__`是预定义的宏,用于接收可变数量的参数,如`LOG(format, ...)`例子,它允许类似于函数的宏调用,如`LOG("%s %d", str, count)`。 5. **递归宏**:当一个宏在自己的定义中调用自身...
C++14作为C++11的补充,继续优化和扩展,例如增加了通用初始化、变长模板参数、constexpr函数的更多用法等。C++17则在C++14的基础上进一步增强,如引入了if constexpr、std::variant、std::optional等新工具,提高了...
- **C/C++语言可变参数表深层探索**:探讨C/C++中如何实现可变参数列表,以及它们在函数设计中的作用。 - **C/C++数组名与指针的区别**:解释数组名与指针之间的差异,并通过示例展示它们的不同之处。 - **C/C++...