问题描述
一位朋友让我用简单的术语解释运算符优先级和求值顺序之间的区别。这就是我向他们解释的方式:-
举个例子-
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;
请注意,在任何步骤中,始终确保保持运算符优先级,即即使 b
在步骤 2 中被 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(),a()
or
c(),b()
or
c(),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 表达式...
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
的result除以c + d
的result,但这并不意味着a * b
必须在 before c + d
之前求值,反之亦然。
a
、b
、c
和 d
中的每一个都可以以任何顺序进行评估(包括同时进行评估,如果架构支持的话) )。类似地,a * b
和 c + d
中的每一个都可以按任何顺序计算,如果在程序中多次计算相同的表达式,则该顺序不必保持一致。显然,a
和 b
都必须在 a * b
之前被评估,而 c
和 d
都必须在 c + d
之前被评估可以评估,但这是您可以确定的唯一顺序。
有些运算符强制从左到右求值 - ||
、&&
、?:
和逗号运算符,但通常求值顺序是免费的 -全部。
不,你说
这里,x的最终值将变为1。这是因为首先将c和d的值相加(6+4),然后将a和b的值相乘(2*5 ),最后进行除法 (10/10),导致最终值变为 1,然后将其分配给 x。
求值顺序确定 6 + 4 将在除法完成之前求值...但不是编译器不能首先安排先求值 c * d
(因为乘法运算符是左结合的,这意味着--也--乘法将在除法之前进行)。您甚至不知道(除非您查看汇编器输出)编译器将选择哪种子表达式求值顺序。如前所述,完整的括号表达式为:
(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
的地址。