但是标准调用约定(如"_cdecl"和"_stdcall")的定义指出(书中),参数从右到左进行计算。函数参数没有特定的计算顺序
现在我对这两个定义感到困惑,一个根据未定义行为的规定,另一个则符合调用约定的定义。请解释这两种情况的正当性。
但是标准调用约定(如"_cdecl"和"_stdcall")的定义指出(书中),参数从右到左进行计算。函数参数没有特定的计算顺序
正如Graznarak的答案所正确指出的,参数计算的顺序与参数传递的顺序是不同的。
ABI通常仅适用于参数传递的顺序,例如使用哪些寄存器和/或将参数值推送到堆栈的顺序。
C标准规定评估的顺序是未指定的。例如(请记住printf
返回一个int
结果):
some_func(printf("first\n"), printf("second\n"));
C标准规定这两个消息将以某种顺序打印(评估不交错),但明确指出不说选择哪个顺序。它甚至可以在一个调用和下一个调用之间变化,而不违反C标准。甚至可以先评估第一个参数,然后评估第二个参数,然后将第二个参数的结果压入堆栈,再将第一个参数的结果压入堆栈。
ABI可能会指定用于传递这两个参数的寄存器,或者在堆栈上推送值的确切位置,这完全符合C标准的要求。
但是,即使ABI实际上要求评估按指定顺序进行(例如,打印"second\n"
,然后是"first\n"
将违反ABI),这仍与C标准一致。
C标准所说的是,C标准本身并没有定义评估的顺序。一些次要的标准仍然可以这样做。
顺便说一句,这本身并不涉及未定义行为。有些情况下,未指定的评估顺序可以导致未定义的行为,例如:
printf("%d %d\n", i++, i++); /* undefined behavior! */
i==0
,所有结果(0,0),(0,1)和(1,0)都是可能的。 - CiaPan&&
,||
,?:
)之外,也没有指定参数或操作数的评估顺序。即使对于使用堆栈的实现,参数的求值顺序与它们在内存中出现的顺序之间也没有必然关系。优化编译器可以并且确实会违反您的假设。建议阅读:comp.lang.c FAQ的第3节。 - Keith Thompson_cdecl
和 _stdcall
仅仅指定参数按照从右到左的顺序压入堆栈,并不意味着参数会按照这个顺序被求值。如果调用约定像_cdecl
, _stdcall
和pascal
改变了参数求值的顺序,那么你必须知道所调用函数的调用约定,才能理解自己的代码行为。如果这样,就会发生数据泄漏。在某些你没有编写的头文件里面,就会有一个神秘的密钥来理解你正在编写的一行代码;但是你有几十万行,每行的行为都不同吗?这将是疯狂的。
我觉得 C89 中许多未定义的行为都源于标准是在存在多个冲突实现之后编写的。他们可能更关心达成大多数实现者能够接受的基线,而不是定义所有行为。我想所有 C 中未定义的行为只是一群聪明而充满激情的人们达成不同意见的地方,但我不在场。
我现在很想分叉一个 C 编译器,并使其评估函数参数,就好像我正在运行广度优先遍历的二叉树一样。在未定义的行为上,您永远无法玩得太过瘾!
请查看您提到的书籍,看看是否有任何关于“序列点”的参考资料,因为我认为这就是您想要了解的内容。
基本上,序列点是一个点,一旦到达该点,您可以确定所有前面的表达式都已完全评估,并且其副作用肯定不再存在。
例如,初始化器的末尾就是一个序列点。这意味着在:
bool foo = !(i++ > j);
你可以肯定的是,i
将会等于它初始值加一,并且 foo
已经被赋值为 true
或者 false
。另一个例子:
int bar = i++ > j ? i : j;
i
的当前值大于j
,并在此比较之后将i
加一(问号是一个序列点,因此在比较之后,i
被递增),则将i
(新值)分配给bar
,否则将j
分配给bar
。这是因为三元运算符中的问号也是一个有效的序列点。printf("%d, %d and %d\n", i++, i++, i--);
1, 2, 3//left to right
1, 0, 1//evaluated i-- first
//or
1, 2, 1//evaluated i-- second
i
的新值将为2。但上述所有值理论上都是同样有效的,并且符合100%的标准。1、2和3
,也可能输出"666, 666 and 666"
。由于C标准未规定参数求值的顺序,每个编译器实现都可以采用任意一种顺序。这就是为什么编写类似foo(i++)
这样的代码是完全疯狂的原因 - 当切换编译器时,您可能会得到不同的结果。
还有一个重要的事情没有在这里强调 - 如果您喜欢的ARM编译器按从左到右的顺序评估参数,则它将对所有情况和所有后续版本都执行此操作。对于编译器来说,参数的读取顺序只是一种惯例...
foo(i++)
不同的结果?这是一个完全定义良好的函数调用,因此只有错误的编译器才会与其他编译器给出不同的结果。 - Jonathan Lefflerfoo(i++)
是完全可预测的。i
的值被设置到一边,传递到 foo
,并且在调用之后的时间给 i
赋值为 i+1
。它在标准中描述:此处序列点是“函数调用后,在参数已经被评估之后”。你正在序列点之间修改一个对象一次,并且你正在进行后自增 i
,所以其行为是完全定义的。如果你的编译器对此进行了“优化”成 foo(++i)
,那么我会说你的编译器有严重的问题。 - Elias Van Ootegemi
的初始值需要被设置。我用int i, j = 123; i = foo(j++); printf("i = %d, j=%d\n", i, j);
进行了测试,其中foo
只是执行printf("value passed: %d\n", x); return x;
将其接收到的值分配给i
。输出结果为:Value passed 123 和 _i = 123, j=124_,正如我们所期望的一样。但是我应该说,在这个答案的示例中,i++
是在值被设置并传递给foo
之后递增的。 - Elias Van Ootegem最终我找到了答案...是的。
这是因为参数在评估后传递。因此,传递参数与评估完全不同。由于C编译器通常构建以最大化速度和优化为目标,可以以任何方式评估表达式。
因此,参数传递和评估都是完全不同的故事。
_cdecl
和_stdcall
约定不是C标准的一部分,它们属于微软。这些约定可能定义了函数调用时参数在栈上放置的顺序/顺序(我认为它们也与哪个代码在函数调用后恢复堆栈指针有关),但即使如此也不必决定求值顺序。 - Jonathan Leffler