c/c++参数入栈顺序和参数计算顺序
内容提要
C/C++语言中函数参数的入栈顺序是从右至左的。参数入栈顺序由函数调用协议决定,常用的是__cdecl、__stdcall和__fastcall。自定义参数入栈顺序也是可行的。参数从右至左入栈的原因是方便处理不定长参数和确定参数个数。参数计算顺序由编译器决定,不同编译器可能有不同结果。在编码中应避免编写依赖参数计算顺序的程序。
延伸解读
调用约定决定入栈与清理方式
文章指出,参数入栈顺序并非语言层面随意规定,而是由调用约定决定。Visual C/C++ 支持 __cdecl、__stdcall、__fastcall 等多种约定,它们在参数传递顺序和堆栈清理责任上存在差异。例如 __cdecl 由调用者清理堆栈,__stdcall 由被调用者清理,__fastcall 则优先使用寄存器。理解这些差异有助于阅读底层代码或进行跨模块调用时判断栈平衡问题。
从右至左入栈与不定参数的关系
文章用 printf 说明从右至左入栈的原因:不定参数函数无法预先知道参数个数,若从左至右入栈,则需先找到 format 才能确定个数,而 format 的位置又依赖参数个数,形成矛盾。从右至左入栈时,format 最后入栈,位于栈顶,可先被取出并据此判断后续参数。这一设计使可变参数函数能够正确工作。
参数计算顺序因编译器而异
文章强调,C/C++ 标准未规定函数参数表达式的计算顺序,实际结果取决于编译器。示例中同一段代码在 VS2019 下输出 7,7,7,在 clang 下输出 2,4,7,说明不同编译器可能从左至右或从右至左计算。因此,编写依赖参数计算顺序的代码会带来可移植性问题,应避免类似 fun(++x, x+y) 的写法。
入栈前先计算,再统一压栈
文章进一步解释,参数入栈前编译器会先处理参数列表中的表达式,得到结果后再统一压栈。对于 a++ 这类操作,编译器可能开辟缓冲区保存当前值,最终入栈时从缓冲区取值,而非直接读取变量内存。这解释了为何参数计算顺序与入栈顺序可能不同,也提醒开发者不要将两者混为一谈。
Q&A
C/C++函数参数的入栈顺序是从左到右还是从右到左?
C/C++规定函数参数的入栈顺序是从右至左。
为什么C/C++参数要从右往左入栈?
主要是为了方便处理不定长参数和确定参数个数。如果从左至右入栈,不定长参数(如printf)将无法确定参数个数;而从右至左入栈时,参数个数最后入栈,只需取栈顶即可得到。
函数调用协议如何影响参数入栈?
函数调用协议决定了参数入栈方式、栈内数据清除方式以及编译器对函数名的修饰规则。例如__cdecl由调用者清理堆栈,参数从右到左压栈;__stdcall由被调用者清理,参数也从右到左压栈;__fastcall将参数存储在寄存器中,然后压入堆栈。
Visual C/C++默认使用哪种调用约定?
Visual C/C++默认使用__cdecl调用约定,即从右到左将参数压入堆栈,并由调用者清理堆栈。
参数的计算顺序由什么决定?不同编译器结果一样吗?
参数的计算顺序由编译器决定,不同编译器可能有不同结果。例如在VS2019中从右至左计算,而clang从左至右计算,因此同一段代码可能输出不同结果。
为什么在编码中要避免依赖参数计算顺序?
因为参数计算顺序依编译器实现而异,编写如fun(++x, x+y)这类依赖计算顺序的程序,在不同平台可能得到不同结果,所以应避免。