您的位置:首页 > 编程语言 > C语言/C++

C/C++函数参数的入栈顺序,计算顺序和可变参数的实现

2013-04-14 00:10 671 查看
函数参数入栈顺序

#include
void foo(int x, int y, int z)
{
printf("x = %d at [%X]\n", x, &x);
printf("y = %d at [%X]\n", y, &y);
printf("z = %d at [%X]\n", z, &z);
}
int main(int argc, char *argv[])
{
foo(100, 200, 300);
return 0;
}


运行结果是:

x = 100 at [...60]
y = 200 at [...64]
z = 300 at [...68]
这是由于,C程序栈的内存生长方式是往低地址内存生长,这也说明为什么局部变量无法申请太大内存,因为栈内容有限。此外,这个例子说明,函数参数的入栈的顺序是从右往左的!。参数入栈顺序具体的还与编译器相关,涉及到C语言中调用约定所采用的方式:
C调用约定在返回前,要作一次堆栈平衡,也就是参数入栈了多少字节,就要弹出来多少字节.这样很安全.
有一点需要注意:stdcall调用约定如果采用了不定参数,即VARARG的话,则和C调用约定一样,要由调用者来作堆栈平衡.
(1)_stdcall是 Pascal方式清理C方式压栈,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。 int f(void *p) -->> _f@4(在外部汇编语言里可以用这个名字引用这个函数)在WIN32
API中,只有少数几个函数,如wspintf函数是采用C调用约定,其他都是stdcall
(2)C调用约定(即用 __cdecl关键字说明)(The C default calling convention)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数 vararg的函数(如printf)只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。
_cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函 数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。
(3)__fastcall调用的主 要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传 送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。__fastcall方式的函数采用寄存器传递参数,VC将
函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的字节数。
(4)thiscall仅仅应用于"C++"成员函数。this指针存放于CX/ECX寄存器中,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。
(5)naked call。 当采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。
综上,其实只有PASCAL调用约定的从左到右入栈的.而且PASCAL不能使用不定参数个数,其参数个数是一定的。

支持可变参数的__cdecl调用其实可以理解的。C方式入栈顺序从右往左,那么在栈底的元素就是可变参数的最右边一个,我们只需要知道所有明确参数里的最左边一个参数在栈中的位置,剩下到栈底的都是可变参数了,反之如果从左往右入栈,则无法知道最右边的可变参数在栈中的位置。在具体实现中,也可观察到其中的原理,包括,需要调用者手动清栈。

float averge(int n_values, ...)
{
va_list var_arg;
//准备访问可变参数
va_start(var_arg, n_values);//第一个参数是va_list变量的名字,第2个参数是省略号前最后一个有名字的参数
//取值
for(::)
sum += va_arg(var_arg, int);//第二个参数是参数的类型
//完成处理可变参数,手动清栈
va_end(var_arg);
}

结论很简单:如果支持可变参数的函数,那么参数进栈的顺序几乎必然是自右向左 的。并且,参数出栈也不能由函数自己完成,而应该由调用者完成。

函数参数计算顺序
主要想说明的是,函数的参数压栈顺序和参数计算顺序不是一个概念。一个函数带有多个参数的时,C++语言没有规定函数调用时实参的求值顺序。这个是编译器自己规定的。

比方说int z = add(++x,x+y);不同编译器可能产生不同结果。

最后一点:千万要注意,C不支持默认参数
References
[1]http://blog.sina.com.cn/s/blog_54f82cc2010133mn.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: