#include <stdio.h>
int x (void) { puts(__func__); return 1; }
int r (void) { puts(__func__); return 2; }
int y (void) { puts(__func__); return 3; }
int w (void) { puts(__func__); return 3; }
int* z (void){ puts(__func__); static int foo; return &foo; }
#define x x()
#define r r()
#define y y()
#define w w()
#define z *z()
int main (void)
{
z = (x*x + r*r) - (y*y + w*w);
}
2条答案
按热度按时间hc2pp10m1#
C标准没有为大多数“结构独立”的操作指定求值顺序,甚至可能没有任何操作顺序,即一个操作可能部分开始,然后另一个操作可能部分开始,然后第一个操作可能完成,然后第二个操作。(例如,编译器可以使用32位操作序列实现64位整数算术。)
在您给予的表达式
z = (x*x + r*r) - (y*y + w*w)
中,如果x
、r
、y
和w
是普通变量,则=
右侧的运算顺序无关紧要-从内存加载x
的值不会影响加载r
的值或其他变量,所以先做哪一个并不重要。但是,如果这些变量用volatile
限定或用表达式替换(例如具有副作用的函数调用,例如打印到标准输出),则顺序可能很重要。在这些情况下,C标准并没有规定先计算哪一个,编译器可以先计算第二个r
,然后是第一个x
,然后是第一个w
,然后是第二个y
,以此类推。表达式中存在一些结构依赖项。必须先计算
x*x
的结果,然后才能将其与r*r
的结果相加,并且必须先计算x*x + r*r
的结果,然后才能从中减去y*y + w*w
的结果。在实践中,编译器很可能使用它已经拥有的值来计算表达式。例如,使用普通的非限定变量,如果最近的先前语句是
q = y*y - w*w
,一个好的编译器会保留y*y
和w*w
的值,以便在z = (x*x + r*r) - (y*y + w*w)
中重用。因此y
,w
,y*y
,并且w*w
将在x
、r
、x*x
和r*r
之前被评估。相反,不同的在先语句可以导致x
、r
、x*x
和r*r
被更早地评估,并且其他组合也是可能的。raogr8fs2#
在大多数情况下,编译器可以按照它喜欢的任何顺序对子表达式求值,只要在要使用该值的点完成所有求值。
旧的C99标准对此解释得最好,6.5:
(The明确提到的运算符具有特殊的求值规则顺序。)
除非后面指定(对于函数调用
()
、&&
、||
、?:
和逗号运算符),否则子表达式的求值顺序和副作用发生的顺序都未指定。这是 * 未指定的行为 *,这意味着:
在这个特定的例子中,程序员不应该期望操作数被求值/执行的特定顺序。
说明这一点的常用方法是用函数调用来代替算术操作数,我们可以用你的小等式来构造一个函数,函数名对应于每个变量,然后用脏宏来“模拟”一下:
这里表达式中的每个操作数都会导致一次函数调用,被调用函数的名字会被打印出来,当你在多个编译器上或者在同一个编译器上使用不同的优化选项时,你会发现它们的行为可能会有所不同。
以gcc为例
这个例子使用了gcc 12.2和gcc 5.1,编译器选项完全相同:https://godbolt.org/z/haPYefnb1
我们应该注意到C中的赋值运算符
=
要求在更新值之前计算正确的操作数,然而gcc 5.1选择先执行z
。它所做的是存储返回的地址,以便稍后使用。它可以自由地这样做,这是一致的行为。