运算符优先级与评估顺序
Operator precedence versus order of evaluation
有朋友让我简单解释一下运算符优先级和求值顺序的区别。这就是我向他们解释的方式:-
举个例子-
int x;
int a = 2;
int b = 5;
int c = 6;
int d = 4;
x = a * b / (c + d);
这里,x
的最终值会变成1
。这是因为首先,c
和 d
的值将被加在一起(6+4
),然后 a
和 b
的值将被相乘(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;
请注意,在任何步骤中,始终确保保持运算符优先级,即即使在步骤 2 中 b
被 5
替换,乘法直到步骤7. 因此,即使不同系统的评估顺序不同,运算符优先级始终保持不变。
在另一个系统上,求值顺序可能是这样的-
/* 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;
同样,运算符优先级保持不变。
我怎样才能改进这个解释?
我更喜欢使用函数调用的解释。函数调用使得“在应用运算符之前需要评估某些东西”变得非常明显。
基本示例:
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
由于加法的从左到右的结合性。但是再次调用 3 个函数的顺序是未知的。
如果函数调用不是一个选项,我更喜欢
x = a * b + c / d
这里很明显有两个子表达式,即a * b
和c / d
。由于运算符优先级,这两个子表达式都必须在加法之前求值,但求值顺序未指定,即我们无法判断先做乘法还是先除法
所以可以
temp1 = a * b
temp2 = c / d
x = temp1 + temp2
或可以
temp2 = c / d
temp1 = a * b
x = temp1 + temp2
我们只知道加法必须在最后。
“括号强制加法在乘法和除法之前进行”的说法不一定正确。您可以在代码 (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
先乘法,后加法,再除法
6.5 Expressions
...
3 The grouping of operators and operands is indicated by the syntax.85) Except as specified
later, side effects and value computations of subexpressions are unsequenced.86)
85) The syntax specifies the precedence of operators in the evaluation of an expression, which is the same
as the order of the major subclauses of this subclause, highest precedence first. Thus, for example, the
expressions allowed as the operands of the binary + operator (6.5.6) are those expressions defined in
6.5.1 through 6.5.6. The exceptions are cast expressions (6.5.4) as operands of unary operators
(6.5.3), and an operand contained between any of the following pairs of operators: grouping
parentheses () (6.5.1), subscripting brackets [] (6.5.2.1), function-call parentheses () (6.5.2.2), and
the conditional operator ? : (6.5.15).
Within each major subclause, the operators have the same precedence. Left- or right-associativity is
indicated in each subclause by the syntax for the expressions discussed therein.
86) In an expression that is evaluated more than once during the execution of a program, unsequenced and
indeterminately sequenced evaluations of its subexpressions need not be performed consistently in
different evaluations.
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
的结果除以[=18=的结果 ],但这并不意味着 a * b
必须在 之前 c + d
进行评估,反之亦然。
a
、b
、c
和 d
中的每一个都可以以任何顺序 求值(包括同时如果体系结构支持它)。类似地,a * b
和 c + d
中的每一个都可以按任何顺序求值,如果同一个表达式在程序中被求值多次,则该顺序不必保持一致。显然 a
和 b
都必须在 a * b
之前计算,并且 c
和 d
都必须在 c + d
之前计算可以评估,但这是唯一可以确定的顺序。
有些运算符强制从左到右计算 - ||
、&&
、?:
和逗号运算符,但通常计算顺序是自由的- for-all.
不,你说
Here, the final value of x will become 1. This is because first, the values of c and d will be added together (6+4), then the values of a and b will be multiplied together (2*5), and finally, the division will take place (10/10), resulting in the final value becoming 1, which is then assigned to x.
评估顺序确定 6 + 4 将在除法完成之前评估...但不是说编译器不能先安排先评估 c * d
(因为乘法运算符是左关联的,并且这也意味着乘法将在除法之前进行)。您甚至不知道(除非您查看汇编器输出)编译器 select 的子表达式求值顺序。如前所述,完整的括号表达式为:
(x = ((a * b) / (c + d)));
所以,编译器会模糊地决定先从a * b
还是c + d
开始。然后它会做其他操作,然后它会做除法,最后是赋值。但要注意,因为赋值需要 x
的地址而不是它的值(它是一个左值),所以 x
的地址可以在任何时候计算,但在赋值之前。最后,抛出赋值的(未使用的)值。
可能的顺序是:
- 计算
a * b
- 计算
x
的地址
- 计算
c + d
- 计算除法
(a*b)/(c+d)
- 将结果存储在位置
&x
。
一个不同的:
- 计算
c + d
- 计算
a * b
- 计算除法
(a*b)/(c+d)
- 计算
x
的地址
- 将结果存储在位置
&x
。
但是你也可以在第一步中计算出x
的地址。
有朋友让我简单解释一下运算符优先级和求值顺序的区别。这就是我向他们解释的方式:-
举个例子-
int x;
int a = 2;
int b = 5;
int c = 6;
int d = 4;
x = a * b / (c + d);
这里,x
的最终值会变成1
。这是因为首先,c
和 d
的值将被加在一起(6+4
),然后 a
和 b
的值将被相乘(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;
请注意,在任何步骤中,始终确保保持运算符优先级,即即使在步骤 2 中 b
被 5
替换,乘法直到步骤7. 因此,即使不同系统的评估顺序不同,运算符优先级始终保持不变。
在另一个系统上,求值顺序可能是这样的-
/* 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;
同样,运算符优先级保持不变。
我怎样才能改进这个解释?
我更喜欢使用函数调用的解释。函数调用使得“在应用运算符之前需要评估某些东西”变得非常明显。
基本示例:
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
由于加法的从左到右的结合性。但是再次调用 3 个函数的顺序是未知的。
如果函数调用不是一个选项,我更喜欢
x = a * b + c / d
这里很明显有两个子表达式,即a * b
和c / d
。由于运算符优先级,这两个子表达式都必须在加法之前求值,但求值顺序未指定,即我们无法判断先做乘法还是先除法
所以可以
temp1 = a * b
temp2 = c / d
x = temp1 + temp2
或可以
temp2 = c / d
temp1 = a * b
x = temp1 + temp2
我们只知道加法必须在最后。
“括号强制加法在乘法和除法之前进行”的说法不一定正确。您可以在代码 (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
先乘法,后加法,再除法
6.5 ExpressionsC 2011 Online Draft
...
3 The grouping of operators and operands is indicated by the syntax.85) Except as specified later, side effects and value computations of subexpressions are unsequenced.86)
85) The syntax specifies the precedence of operators in the evaluation of an expression, which is the same as the order of the major subclauses of this subclause, highest precedence first. Thus, for example, the expressions allowed as the operands of the binary + operator (6.5.6) are those expressions defined in 6.5.1 through 6.5.6. The exceptions are cast expressions (6.5.4) as operands of unary operators (6.5.3), and an operand contained between any of the following pairs of operators: grouping parentheses () (6.5.1), subscripting brackets [] (6.5.2.1), function-call parentheses () (6.5.2.2), and the conditional operator ? : (6.5.15). Within each major subclause, the operators have the same precedence. Left- or right-associativity is indicated in each subclause by the syntax for the expressions discussed therein.
86) In an expression that is evaluated more than once during the execution of a program, unsequenced and indeterminately sequenced evaluations of its subexpressions need not be performed consistently in different evaluations.
优先级和结合性仅控制表达式的解析方式以及哪些运算符与哪些操作数分组。它们不控制计算子表达式的顺序。
以你为例
x = a * b / (c + d);
优先级和结合性导致表达式被解析为
(x) = ((a * b) / (c + d))
乘法运算符 *
和 /
具有相同的优先级并且是左结合的,因此 a * b / (c + d)
被解析为 (a * b) / (c + d)
(相对于 a * (b / (c + d))
).
所以这告诉我们a * b
的结果除以[=18=的结果 ],但这并不意味着 a * b
必须在 之前 c + d
进行评估,反之亦然。
a
、b
、c
和 d
中的每一个都可以以任何顺序 求值(包括同时如果体系结构支持它)。类似地,a * b
和 c + d
中的每一个都可以按任何顺序求值,如果同一个表达式在程序中被求值多次,则该顺序不必保持一致。显然 a
和 b
都必须在 a * b
之前计算,并且 c
和 d
都必须在 c + d
之前计算可以评估,但这是唯一可以确定的顺序。
有些运算符强制从左到右计算 - ||
、&&
、?:
和逗号运算符,但通常计算顺序是自由的- for-all.
不,你说
Here, the final value of x will become 1. This is because first, the values of c and d will be added together (6+4), then the values of a and b will be multiplied together (2*5), and finally, the division will take place (10/10), resulting in the final value becoming 1, which is then assigned to x.
评估顺序确定 6 + 4 将在除法完成之前评估...但不是说编译器不能先安排先评估 c * d
(因为乘法运算符是左关联的,并且这也意味着乘法将在除法之前进行)。您甚至不知道(除非您查看汇编器输出)编译器 select 的子表达式求值顺序。如前所述,完整的括号表达式为:
(x = ((a * b) / (c + d)));
所以,编译器会模糊地决定先从a * b
还是c + d
开始。然后它会做其他操作,然后它会做除法,最后是赋值。但要注意,因为赋值需要 x
的地址而不是它的值(它是一个左值),所以 x
的地址可以在任何时候计算,但在赋值之前。最后,抛出赋值的(未使用的)值。
可能的顺序是:
- 计算
a * b
- 计算
x
的地址
- 计算
c + d
- 计算除法
(a*b)/(c+d)
- 将结果存储在位置
&x
。
一个不同的:
- 计算
c + d
- 计算
a * b
- 计算除法
(a*b)/(c+d)
- 计算
x
的地址
- 将结果存储在位置
&x
。
但是你也可以在第一步中计算出x
的地址。