运算符优先级与求值顺序

5

一个朋友问我用简单的方式解释运算符优先级和运算顺序的区别。这是我向他们解释的内容:

我们来看一个例子 -

int x;
int a = 2;
int b = 5;
int c = 6;
int d = 4;

x = a * b / (c + d);

在这里,x 的最终值将变成1。这是因为首先会将cd的值相加(6+4),然后将ab的值相乘(2*5),最后进行除法运算(10/10),结果导致最终值变为1,并分配给x
所有这些都由运算符优先级指定。在此示例中,括号强制先执行加法,而不是乘法和除法,即使加法的优先级较低。另外,乘法先于除法执行,因为乘法和除法具有相同的优先级,并且它们都具有从左到右的结合性。
现在是重要的部分,即该表达式的评估顺序。
在某个系统上,表达式的计算顺序可能如下 -
/* Step 1 */   x = a * b / (c + d);
/* Step 2 */   x = a * 5 / (c + d);
/* Step 3 */   x = a * 5 / (c + 4);
/* Step 4 */   x = a * 5 / (6 + 4);
/* Step 5 */   x = a * 5 / 10;
/* Step 6 */   x = 2 * 5 / 10;
/* Step 7 */   x = 10 / 10;
/* Step 8 */   x = 1;

请注意,在任何一步中,始终确保操作符的优先级得以保持,即使在第二步中将b替换为5,乘法也不会在第七步之前进行。因此,尽管不同系统的计算顺序不同,但操作符的优先级始终得以保持。
在另一个系统上,求值的顺序可能如下:
/* Step 1 */   x = a * b / (c + d);
/* Step 2 */   x = a * b / (6 + d);
/* Step 3 */   x = a * b / (6 + 4);
/* Step 4 */   x = a * b / 10;
/* Step 5 */   x = 2 * b / 10;
/* Step 6 */   x = 2 * 5 / 10;
/* Step 7 */   x = 10 / 10;
/* Step 8 */   x = 1;

再次强调,运算符优先级得到保留。

在上面的例子中,整个行为是明确定义的。其中一个原因是所有变量都是不同的。 从技术角度来看,在此示例中行为是明确定义的原因是没有对任何变量进行无序修改。 因此,在任何系统上,x 最终总是被赋值为 1

现在,让我们将上面的例子改成这样:

int x;
int y = 1;

x = ++y * y-- / (y + y++);

这里,最后被分配给x的值因系统而异,导致行为未定义。

在某些系统中,评估顺序可能如下 -

/* Step 1 */   x = ++y * y-- / (y + y++);   // (y has value 1)
/* Step 2 */   x = ++y * y-- / (1 + y++);   // (y still has value 1)
/* Step 3 */   x = ++y * 1 / (1 + y++);     // (y now has value 0)
/* Step 4 */   x = 1 * 1 / (1 + y++);       // (y now has value 1)
/* Step 5 */   x = 1 * 1 / (1 + 1);         // (y now has value 2)
/* Step 6 */   x = 1 * 1 / 2;
/* Step 7 */   x = 1 / 2;
/* Step 8 */   x = 0;

再次强调,运算符优先级得到维护。

在其他系统上,求值顺序可能是这样的:

/* Step 1 */   x = ++y * y-- / (y + y++);   // (y has value 1)
/* Step 2 */   x = ++y * y-- / (y + 1);     // (y now has value 2)
/* Step 3 */   x = ++y * 2 / (y + 1);       // (y now has value 1)
/* Step 4 */   x = ++y * 2 / (1 + 1);       // (y still has value 1)
/* Step 5 */   x = ++y * 2 / 2;             // (y still has value 1)
/* Step 6 */   x = 2 * 2 / 2:               // (y now has value 2)
/* Step 7 */   x = 4 / 2;
/* Step 8 */   x = 2;

再次强调,运算符优先级得以维持。

如何改进这个解释?


1
嗯,就我所看到的,它是正确的。但是,嗯,它确实会引发未定义行为,因此您无法保证评估是否真正发生。 - klutt
2
我会使用函数调用而不是变量。在我看来,这样会更清楚地表明涉及到一个评估过程。 - Support Ukraine
2
最终分配给x的值因系统而异,使行为未定义。嗯,情况恰恰相反。行为是未定义的,因此它可以在系统之间变化,编译器可以随意处理。当它是未定义的时候,任何事情都可能发生。编译器可以按照自己的方式计算。您可以编写“然后编译器将格式化硬盘”或“编译器将生成恶魔”,这将与在未定义行为的情况下进行任何评估顺序一样正确。 - KamilCuk
1
@KushagrJaiswal 只需阅读我发布的链接。特别是,子表达式的顺序通常不关心恰好位于操作数之间的运算符是什么。当然除了所有特殊情况,如&&等。 - Lundin
3
如果你试图区分优先级和操作顺序,那么其中关键的事情之一就是避免使用时间和/或排序术语(例如“first”和“before”)来描述优先级的影响。优先级关乎于什么,而不是何时 - John Bollinger
显示剩余9条评论
4个回答

9
我更倾向于使用函数调用来进行解释。函数调用可以很明显地表明在应用运算符之前需要“评估某些内容”。
基本示例:
int x = a() + b() * c();

必须计算为

temp = result_of_b_func_call * result_of_c_func_call
x = result_of_a_func_call + temp

由于乘法比加法具有更高的优先级,因此计算机会先处理乘法。

但是,这3个函数调用的评估顺序是未指定的,即函数可以以任何顺序调用。例如:

a(), b(), c()
or
a(), c(), b()
or
b(), a(), c()
or
b(), c(), a()
or
c(), a(), b()
or
c(), b(), a()

另一个基本的例子是解释运算符的结合性,例如:

int x = a() + b() + c();

必须进行计算

temp = result_of_a_func_call + result_of_b_func_call
x = temp + result_of_c_func_call

由于加法的从左到右结合性,输出结果受其影响。但是三个函数调用的顺序仍未知。
如果无法进行函数调用,我更倾向于以下方法。
x = a * b + c / d

很明显这里有两个子表达式,即a * bc / d。由于运算符优先级,这些子表达式必须在加法之前求值,但是求值顺序未指定,也就是说我们无法确定先执行乘法还是除法。

因此,可能会有以下两种情况:

temp1 = a * b
temp2 = c / d
x = temp1 + temp2

或者它可以是

temp2 = c / d
temp1 = a * b
x = temp1 + temp2

我们只知道这个附加必须是最后完成的。

4
6.5表达式
...
3     操作符和操作数的分组由语法表示。85) 除非另有规定,否则子表达式的副作用和值计算是无序的。86)
85) 语法指定了在表达式评估中操作符的优先级,它与此子条款的主要子条款的顺序相同,最高优先级首先。因此,例如,作为二元+运算符(6.5.6)的操作数允许的表达式是在6.5.1到6.5.6中定义的表达式。有一些例外情况,即强制类型转换表达式(6.5.4)作为一元运算符(6.5.3)的操作数,以及包含在以下任何一对运算符之间的操作数:分组括号()(6.5.1),下标方括号[](6.5.2.1),函数调用括号()(6.5.2.2)和条件运算符?:(6.5.15)。在每个主要子条款内,运算符具有相同的优先级。左结合性或右结合性在其中每个子条款中均通过所讨论的表达式的语法来表示。

86) 在程序执行过程中多次评估的表达式中,其子表达式的未排序和不确定排序评估在不同的评估中不必一致进行。
C 2011 Online Draft

优先级和结合性只控制表达式的解析以及将哪些操作符与哪些操作数分组。它们不控制子表达式计算的顺序。

针对您的示例:

x = a * b / (c + d);

优先级和结合性导致表达式被解析为

(x) = ((a * b) / (c + d))

乘法运算符*和除法运算符/具有相同的优先级且是从左往右结合的,因此a * b / (c + d)被解析为(a * b) / (c + d)(与a * (b / (c + d))不同)。
这告诉我们的是a * b的结果被c + d的结果所除,但这并不意味着a * b必须在c + d之前或之后进行评估。
每个变量abcd都可以以任何顺序进行评估(包括同时评估,如果架构支持的话)。同样,a * bc + d中的每个表达式也可以以任何顺序进行评估,并且如果同一表达式在程序中评估多次,则该顺序不必保持一致。显然,在a * b被评估之前,必须评估ab,在c + d被评估之前,必须评估cd,但这是您可以确定的唯一顺序。
存在可以强制左到右评估的运算符-||&&?:和逗号运算符,但通常评估顺序是一个自由竞争。

2

不能简单地说“括号强制加法在乘法和除法之前进行”。你可以在代码的反汇编中看到这一点(使用gcc 10.2.0):

x = a * b / (c + d);
   1004010b6:   8b 45 fc                mov    -0x4(%rbp),%eax
   1004010b9:   0f af 45 f8             imul   -0x8(%rbp),%eax
   1004010bd:   8b 4d f4                mov    -0xc(%rbp),%ecx
   1004010c0:   8b 55 f0                mov    -0x10(%rbp),%edx
   1004010c3:   01 d1                   add    %edx,%ecx
   1004010c5:   99                      cltd
   1004010c6:   f7 f9                   idiv   %ecx

先进行乘法,然后加法,最后除法。

2

不对,你说错了

在这里,x的最终值将变为1。这是因为首先将c和d的值相加(6+4),然后将a和b的值相乘(2*5),最后进行除法运算(10/10),导致最终值变为1,然后将其赋给x。

求值顺序确立了在执行除法之前将计算6 + 4...但并不意味着编译器不能先安排先计算 c * d (因为乘法运算符是左结合的,这也意味着乘法将在除法之前进行)。你甚至不知道(除非查看汇编输出),编译器将选择哪个子表达式的求值顺序。如所述,完全带括号的表达式将是:

(x = ((a * b) / (c + d)));

因此,编译器将决定首先从a * bc + d开始执行。然后它会执行另一个操作,接着进行除法运算,最后才是赋值。但要注意,因为赋值需要x的地址而不是其值(它是一个左值),所以可以在任何时候计算x的地址,但必须在进行赋值之前。最后,赋值的(未使用)值被抛弃。
可能的顺序包括:
- 计算a * b - 计算x的地址 - 计算c + d - 计算除法(a*b)/(c+d) - 将结果存储在位置&x 不同的顺序是:
- 计算c + d - 计算a * b - 计算除法(a*b)/(c+d) - 计算x的地址 - 将结果存储在位置&x 但也可以在第一步中计算x的地址。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接