在 C 中,为什么 true && true \|\|假 && 假 == 真?

新手上路,请多包涵

我想知道是否有人知道编译器解释以下代码的方式:

 #include <iostream>
using namespace std;

int main() {
 cout << (true && true || false && false) << endl; // true
}

这是真的吗,因为 && 的优先级高于 ||或者因为 ||是短路运算符(换句话说,短路运算符是否忽略所有后续表达式,或仅忽略下一个表达式)?

原文由 Andrew 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 769
2 个回答

Caladain 的答案完全正确,但我想回复您对他的回答的评论之一:

如果短路 ||运算符出现并使第二个 && 表达式的执行短路,这意味着 ||运算符在第二个 && 运算符之前执行。这意味着 && 和 || 从左到右执行(不是 && 优先级)。

我认为您遇到的部分问题是优先级并不完全意味着您认为它的含义。确实 &&|| 具有更高的优先级,这正是您所看到的行为的原因。考虑使用普通算术运算符的情况:假设我们有 a * b + c * (d + e) 。优先级告诉我们如何插入括号:首先在 * 周围,然后在 + 周围。这给了我们 (a * b) + (c * (d + e)) ;在您的情况下,我们有 (1 && 1) || (infiniteLoop() && infiniteLoop()) 。然后,想象表达式变成 _树_。为此,请将每个运算符转换为一个节点,并将其两个参数作为子节点:

表达式树。

评估这棵树是短路的地方。在算术树中,您可以想象一种广度优先自下而上的执行方式:首先评估 DE = d + e ,然后 AB = a * bCDE = c * DE ,最终结果是 AB + CDE 。但请注意,您 同样 可以先评估 AB ,然后 DECDE 和最终结果;你无法区分。但是,由于 ||&& 是短路的,它们 必须 使用这个最左优先的评估。因此,为了评估 || ,我们首先评估 1 && 1 。既然这是真的, || 短路并忽略它的右手分支—— 即使 它已经评估过它,它也必须首先评估 infiniteLoop() && infiniteLoop()

如果有帮助,您可以将树中的每个节点视为一个函数调用,它会在第一种情况下产生以下表示形式 plus(times(a,b), times(c,plus(d,e))) ,在第二种情况下产生 or(and(1,1), and(infiniteLoop(),infiniteLoop()) 。短路意味着您必须全面评估 orand 的每个左侧函数参数; if it’s true (for or ) or false (for and ), then ignore the right-hand argument.

您的评论假定我们首先评估具有最高优先级的所有内容,然后是具有次高优先级的所有内容,依此类推,这对应于树的广度优先自下而上执行。相反,优先级告诉我们如何构建树。树的执行规则在简单的算术情况下是无关紧要的;然而,短路正是如何评估树的精确规范。


编辑1: 在您的其他评论之一中,您说

您的算术示例需要在最终加法之前评估两次乘法,这不是定义优先级的原因吗?

是的,这就是优先级的定义——但它并不完全正确。这在 C 中当然是完全正确的,但请考虑如何评估(非 C!)表达式 0 * 5 ^ 7 在你的脑海中,其中 5 ^ 7 = 57^ 更高优先于 * 。根据您的广度优先自下而上规则,我们需要评估 05 ^ 7 才能找到结果。但是你不会费心去评估 5 ^ 7 ;您只需说“好吧,因为 0 * x = 0 对于所有 x ,这必须是 0 ”,然后跳过整个右侧分支。换句话说,在评估最终乘法之前,我还没有完全评估双方;我短路了。同样,由于 false && _ == falsetrue || _ == true 对于任何 _ ,我们可能不需要触摸右侧;这就是操作员短路的意思。 C 不会短路乘法(尽管一种语言可以做到这一点),但它 短路 &&||

就像短路 0 * 5 ^ 7 不会改变通常的 PEMDAS 优先级规则一样,短路逻辑运算符也不会改变 && 的优先级高于 || 的事实 --- .这只是一个捷径。由于我们必须先选择运算符的 一侧进行计算,因此 C 承诺首先计算逻辑运算符的左侧;一旦完成此操作,就有一种明显(且有用)的方法可以避免评估某些值的右侧,并且 C 承诺会这样做。

你的规则——评估表达式广度优先自下而上——也是明确定义的,一种语言可以选择这样做。但是,它的缺点是不允许短路,这是一种有用的行为。但是如果你的树中的每个表达式都是明确定义的(没有循环)和纯粹的(没有修改变量、打印等),那么你就无法区分。只有在这些奇怪的情况下,“和”和“或”的数学定义没有涵盖,短路才是可见的。

另外,请注意,短路通过优先处理最左边的表达式来起作用这一事实并没有什么 _根本意义_。 One could define a language Ɔ, where ⅋⅋ represents and and \\ represents || , but where 0 ⅋⅋ infiniteLoop() and 1 \\ infiniteLoop() 会循环,而 infiniteLoop() ⅋⅋ 0infiniteLoop() \\ 1 分别是 false 和 true。这仅对应于选择先评估右侧而不是左侧,然后以相同的方式进行简化。

简而言之: 优先级告诉我们的是如何构建解析树。评估解析树的唯一合理顺序是那些表现得 好像 我们 在定义明确的纯值上 以广度优先自下而上(如您所愿)评估它的顺序。对于未定义或不纯的值,必须选择 一些 线性顺序。 1一旦选择了线性顺序,运算符一侧的某些值可以唯一地确定整个表达式的结果( _例如_, 0 * _ == _ * 0 == 0false && _ == _ && false == falsetrue || _ == _ || true == true )。因此,您可能无需完成对线性顺序之后出现的任何内容的评估就可以逃脱; C 承诺通过以从左到右的方式评估逻辑运算符 &&|| 来执行此操作,而不是为其他任何操作执行此操作。但是,由于优先级,我们 确实 知道 true || true && falsetrue 而不是 false :因为

  true || true && false
→ true || (true && false)
→ true || false
→ true

代替

  true || true && false
↛ (true || true) && false
→ true && false
→ false


1: 实际上,理论上我们也可以并行计算运算符的两边,但这现在并不重要,而且对 C 肯定没有意义。这产生了更灵活的语义,但是一个有问题的边-影响(它们何时发生?)。

原文由 Antal Spector-Zabusky 发布,翻译遵循 CC BY-SA 3.0 许可协议

“如果短路 || 运算符并短路第二个 && 表达式的执行,这意味着 || 运算符在第二个 && 运算符之前执行。这意味着 && 和 || 从左到右执行 (不是 && 优先级)。”

不完全的。

 (xx && yy || zz && qq)

会被这样评价:

  1. 检查第一个操作员。
  2. 评估 xx && yy
  3. 检查下一个运算符。
  4. 如果下一个运算符是 || 并且语句的第一部分为真,则跳过其余部分。
  5. 否则,检查 || 之后的下一个运算符,并评估它: zz && qq
  6. 最后,评估 ||

据我了解,C++ 的设计目的是在开始评估之前读取内容。毕竟,在我们的示例中,它不知道我们有第二个 && 检查 || 直到它读入,这意味着它必须读入 || 在它到达第二个之前 && 。因此,如果第一部分评估为真,它不会执行 || 之后的部分,但如果第一部分评估为假,那么它将执行第一部分,读入 || ,找到并评估第二部分,并使用第二部分的结果来确定最终结果。

原文由 Anonymoose 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题