C编译器如何读取这个操作?

nbysray5  于 2023-03-12  发布在  其他
关注(0)|答案(2)|浏览(109)

C编译器如何读取此操作,即操作的顺序是什么?

z = (x*x + r*r) - (y*y + w*w)

我搞不清楚它是先从左括号开始,还是同时对两个括号都有效。

hc2pp10m

hc2pp10m1#

C标准没有为大多数“结构独立”的操作指定求值顺序,甚至可能没有任何操作顺序,即一个操作可能部分开始,然后另一个操作可能部分开始,然后第一个操作可能完成,然后第二个操作。(例如,编译器可以使用32位操作序列实现64位整数算术。)
在您给予的表达式z = (x*x + r*r) - (y*y + w*w)中,如果xryw是普通变量,则=右侧的运算顺序无关紧要-从内存加载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*yw*w的值,以便在z = (x*x + r*r) - (y*y + w*w)中重用。因此ywy*y,并且w*w将在xrx*xr*r之前被评估。相反,不同的在先语句可以导致xrx*xr*r被更早地评估,并且其他组合也是可能的。

raogr8fs

raogr8fs2#

在大多数情况下,编译器可以按照它喜欢的任何顺序对子表达式求值,只要在要使用该值的点完成所有求值。
旧的C99标准对此解释得最好,6.5:
(The明确提到的运算符具有特殊的求值规则顺序。)
除非后面指定(对于函数调用()&&||?:和逗号运算符),否则子表达式的求值顺序和副作用发生的顺序都未指定。
这是 * 未指定的行为 *,这意味着:

  • 编译器可能以它喜欢的任何方式来做事情。
  • 编译器不需要记录它是如何工作的。
  • 编译器不需要在整个程序中保持一致的行为,即使遇到几个相同的代码段也是如此。
  • 程序员不应该期望任何确定的结果或编写依赖于它的程序。

在这个特定的例子中,程序员不应该期望操作数被求值/执行的特定顺序。
说明这一点的常用方法是用函数调用来代替算术操作数,我们可以用你的小等式来构造一个函数,函数名对应于每个变量,然后用脏宏来“模拟”一下:

#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);
}

这里表达式中的每个操作数都会导致一次函数调用,被调用函数的名字会被打印出来,当你在多个编译器上或者在同一个编译器上使用不同的优化选项时,你会发现它们的行为可能会有所不同。
以gcc为例
这个例子使用了gcc 12.2和gcc 5.1,编译器选项完全相同:https://godbolt.org/z/haPYefnb1
我们应该注意到C中的赋值运算符=要求在更新值之前计算正确的操作数,然而gcc 5.1选择先执行z。它所做的是存储返回的地址,以便稍后使用。它可以自由地这样做,这是一致的行为。

相关问题