我想知道是否有人知道编译器解释以下代码的方式:
#include <iostream>
using namespace std;
int main() {
cout << (true && true || false && false) << endl; // true
}
这是真的吗,因为 && 的优先级高于 ||或者因为 ||是短路运算符(换句话说,短路运算符是否忽略所有后续表达式,或仅忽略下一个表达式)?
原文由 Andrew 发布,翻译遵循 CC BY-SA 4.0 许可协议
Caladain 的答案完全正确,但我想回复您对他的回答的评论之一:
我认为您遇到的部分问题是优先级并不完全意味着您认为它的含义。确实
&&
比||
具有更高的优先级,这正是您所看到的行为的原因。考虑使用普通算术运算符的情况:假设我们有a * b + c * (d + e)
。优先级告诉我们如何插入括号:首先在*
周围,然后在+
周围。这给了我们(a * b) + (c * (d + e))
;在您的情况下,我们有(1 && 1) || (infiniteLoop() && infiniteLoop())
。然后,想象表达式变成 _树_。为此,请将每个运算符转换为一个节点,并将其两个参数作为子节点:评估这棵树是短路的地方。在算术树中,您可以想象一种广度优先自下而上的执行方式:首先评估
DE = d + e
,然后AB = a * b
和CDE = c * DE
,最终结果是AB + CDE
。但请注意,您 同样 可以先评估AB
,然后DE
、CDE
和最终结果;你无法区分。但是,由于||
和&&
是短路的,它们 必须 使用这个最左优先的评估。因此,为了评估||
,我们首先评估1 && 1
。既然这是真的,||
短路并忽略它的右手分支—— 即使 它已经评估过它,它也必须首先评估infiniteLoop() && infiniteLoop()
。如果有帮助,您可以将树中的每个节点视为一个函数调用,它会在第一种情况下产生以下表示形式
plus(times(a,b), times(c,plus(d,e)))
,在第二种情况下产生or(and(1,1), and(infiniteLoop(),infiniteLoop())
。短路意味着您必须全面评估or
或and
的每个左侧函数参数; if it’strue
(foror
) orfalse
(forand
), then ignore the right-hand argument.您的评论假定我们首先评估具有最高优先级的所有内容,然后是具有次高优先级的所有内容,依此类推,这对应于树的广度优先自下而上执行。相反,优先级告诉我们如何构建树。树的执行规则在简单的算术情况下是无关紧要的;然而,短路正是如何评估树的精确规范。
编辑1: 在您的其他评论之一中,您说
是的,这就是优先级的定义——但它并不完全正确。这在 C 中当然是完全正确的,但请考虑如何评估(非 C!)表达式
0 * 5 ^ 7
在你的脑海中,其中5 ^ 7 = 57
和^
更高优先于*
。根据您的广度优先自下而上规则,我们需要评估0
和5 ^ 7
才能找到结果。但是你不会费心去评估5 ^ 7
;您只需说“好吧,因为0 * x = 0
对于所有x
,这必须是0
”,然后跳过整个右侧分支。换句话说,在评估最终乘法之前,我还没有完全评估双方;我短路了。同样,由于false && _ == false
和true || _ == true
对于任何_
,我们可能不需要触摸右侧;这就是操作员短路的意思。 C 不会短路乘法(尽管一种语言可以做到这一点),但它 会 短路&&
和||
。就像短路
0 * 5 ^ 7
不会改变通常的 PEMDAS 优先级规则一样,短路逻辑运算符也不会改变&&
的优先级高于||
的事实---
.这只是一个捷径。由于我们必须先选择运算符的 某 一侧进行计算,因此 C 承诺首先计算逻辑运算符的左侧;一旦完成此操作,就有一种明显(且有用)的方法可以避免评估某些值的右侧,并且 C 承诺会这样做。你的规则——评估表达式广度优先自下而上——也是明确定义的,一种语言可以选择这样做。但是,它的缺点是不允许短路,这是一种有用的行为。但是如果你的树中的每个表达式都是明确定义的(没有循环)和纯粹的(没有修改变量、打印等),那么你就无法区分。只有在这些奇怪的情况下,“和”和“或”的数学定义没有涵盖,短路才是可见的。
另外,请注意,短路通过优先处理最左边的表达式来起作用这一事实并没有什么 _根本意义_。 One could define a language Ɔ, where
⅋⅋
representsand
and\\
represents||
, but where0 ⅋⅋ infiniteLoop()
and1 \\ infiniteLoop()
会循环,而infiniteLoop() ⅋⅋ 0
和infiniteLoop() \\ 1
分别是 false 和 true。这仅对应于选择先评估右侧而不是左侧,然后以相同的方式进行简化。简而言之: 优先级告诉我们的是如何构建解析树。评估解析树的唯一合理顺序是那些表现得 好像 我们 在定义明确的纯值上 以广度优先自下而上(如您所愿)评估它的顺序。对于未定义或不纯的值,必须选择 一些 线性顺序。 1一旦选择了线性顺序,运算符一侧的某些值可以唯一地确定整个表达式的结果( _例如_,
0 * _ == _ * 0 == 0
、false && _ == _ && false == false
或true || _ == _ || true == true
)。因此,您可能无需完成对线性顺序之后出现的任何内容的评估就可以逃脱; C 承诺通过以从左到右的方式评估逻辑运算符&&
和||
来执行此操作,而不是为其他任何操作执行此操作。但是,由于优先级,我们 确实 知道true || true && false
是true
而不是false
:因为代替
1: 实际上,理论上我们也可以并行计算运算符的两边,但这现在并不重要,而且对 C 肯定没有意义。这产生了更灵活的语义,但是一个有问题的边-影响(它们何时发生?)。