关于
本文涉及到代码,演示环境为:win10 + VS2017 ,ubuntu+clangclang版本:
参数入栈顺序
顺序
几种常见的函数参数入栈顺序,还有两种就不介绍了(__clrcall、__thiscall)
顺序 | 释义 |
---|---|
__cdecl | 函数参数按照从右到左的顺序入栈,并且由调用函数者把参数弹出栈以清理堆栈 |
__stdcall | 函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,函数参数个数固定 |
__fastcall | 使用内部寄存器ECX,EDX传递前两个DWORD 或者size更小的参数 |
__fastcall用的很少。 通常情况下: c/c++默认入栈方式:__cdel。Windows api使用的是__stdcall方式。 |
自定义参数入栈形式
可以自定义函数的入栈顺序。 常用形式如下
函数返回值 入栈规则 函数名(参数类型 参数名);
一个例子
int __cdecl get_name_index(const std::string& str_name);
为什么要从右往左入栈?
- 结论:为了确定函数参数个数。大胆猜想:统一定长参数函数和不定长参数函数处理函数入栈顺序。
- 有的函数参数个数是确定的,比如上面的函数,就一个参数,有的函数参数是不定长,学过C的都知道printf和scanf,当然也可以自定义变长参数,c++11引入了可变长参数。
- 定长参数的函数,当然,每个参数都有自己的地址,很容易就拿到了,but, 不定长参数呢,怎么知道每个参数的地址和压入多少个参数? 比如printf和scanf函数,第一个参数就很特别,一个格式化的字符串。printf是怎么知道有多少个参数呢? 通过格式格式字符串个数确定。有多少个格式字符串,就需要多少个参数
注意: 栈是先进后出,俗称后来居上。
- 显然,定长函数参数就不需要分析了,每个参数都有自己的地址,能确定下来,从左往右 对比 从右往左没有区别。
- 那不定长函数参数呢? 以 printf函数为例,
int a = 1;
int b = 2;
printf("%d, %d", a, b);
情况A: 从左往右。 那么上面的代码,先入栈的是"%d, %d", 再是a, 最后是b。printf需要知道压入了多少个参数,就需要检查格式化字符串"%d, %d", 但是,这个参数被压入了栈底。栈顶是b。肉眼当然知道压入了多少个参数,编译器需要通过判断条件才能知道,判断条件被压入栈底,这时,编译器就无法知道格式化字符串了,也就不知道参数个数了。
情况B:从右往左。 那么上面的代码,先入栈的是b,再是a,最后是"%d, %d". printf先检查第一个参数,栈顶是"%d, %d", 就可以知道压入了多少个参数了。
参数计算顺序
这个和编译器有关。不同编译器可能也相同。
一定要知道
Note: 期望输出 和 参数计算顺序 息息相关。
为什么? 代码参数的计算顺序决定了实际输出,这与期望输出是两码事。 而期望输出是主观预测输出结果。
我将使用下面的测试代码,观察VS和clang两种编译器的参数计算顺序。
int a = 2;
printf("%d, %d, %d", a, (a = (a + 2)), (a = (a + 3)));
win10 + VS2017
使用VS2017输出下面的代码,可知计算顺序。可自己先计算下输出结果是什么....
正确输出: 7, 7, 7
分析(个人理解): printf("%d, %d, %d", a, (a = (a + 2)), (a = (a + 3)));
可以拆解为下面的代码:
// printf("%d, %d, %d", a, (a = (a + 2)), (a = (a + 3)));
a = a + 3;
a = a + 2
a
printf函数先将表达式的结果计算出来,得到结算结果,再将计算结果压入栈。先计算a=a+3;, 此时, a的值从2->5, 同理,a=a+2;后,a的值从5->7。当执行函数printf时,变成了
printf("%d, %d, %d", 7, 7, 7);
- 按照上面的分析方法,再来一个例子
int a = 2;
int b = 3;
printf("%d, %d, %d", a, a = (a+b), b = (b-a));
输出结果
printf("%d, %d, %d", a, a = (a+b), b = (b-a));
// 等效下面的方式
1. b = b - a;// b = 3-2; b = 1
2. a = a +b; // a = 2 + 1; a = 3
3. printf("%d, %d, %d", 3, 3, 1);
ubuntu + clang
自己先计算结果,再看答案。
clang输出结果
可见,clang的计算顺序是从左至右,而VS的计算顺序是从右至左。
为什么clang的输出结果是这样的? 大胆猜想(个人意见)先计算a, 将a单独一个副本,再计算 a=a+2, 结果为4,再将a=a+2的结果4保存到另一个副本中,最后计算a=a+3=4+3 = 7,将(a=a+3)拷贝到下一个副本中。
结论
应该避免上述情况,即可实现同样的表达式,可以实现在不同的平台得到同样的结果。
踩坑
这点倒是深有体会,自己写公共服务模块时,就因为上面的情况出现了一些困扰。