【问题标题】:What is the correct answer for cout << a++ << a;?cout << a++ << a; 的正确答案是什么?
【发布时间】:2012-05-28 10:09:54
【问题描述】:

最近在面试中有一个客观类型的问题。

int a = 0;
cout << a++ << a;

答案:

一个。 10
湾。 01
C。未定义的行为

我回答了选项 b,即输出为“01”。

但令我惊讶的是,后来面试官告诉我正确答案是选项 c:未定义。

现在,我确实知道 C++ 中序列点的概念。以下语句的行为未定义:

int i = 0;
i += i++ + i++;

但根据我对声明 cout &lt;&lt; a++ &lt;&lt; a 的理解,ostream.operator&lt;&lt;() 将被调用两次,首先是 ostream.operator&lt;&lt;(a++),然后是 ostream.operator&lt;&lt;(a)。

我还检查了VS2010编译器的结果,它的输出也是'01'。

【问题讨论】:

  • 您要求解释了吗?我经常采访潜在的候选人,并且对接受问题很感兴趣,这表明了兴趣。
  • @jrok 这是未定义的行为。实施所做的任何事情(包括以您的名义向您的老板发送侮辱性电子邮件)都是符合要求的。
  • 这个问题急需一个没有提及序列点的 C++11(C++ 的 current 版本)答案。不幸的是,我对 C++11 中序列点的替换了解不够。
  • 如果它不是未定义的,它肯定不可能是10,它可能是01 或00。 (c++ 将始终评估为值 c 有 before 被递增)。即使它不是未定义的,它仍然会令人非常困惑。
  • 你知道,当我看到标题“cout 非常 比 C 差很多。:)

标签: c++ c++-faq


【解决方案1】:

你可以想到:

cout << a++ << a;

作为:

std::operator<<(std::operator<<(std::cout, a++), a);

C++ 保证先前评估的所有副作用都将在sequence points 执行。函数参数评估之间没有序列点,这意味着参数a 可以在参数std::operator&lt;&lt;(std::cout, a++) 之前或之后进行评估。所以上面的结果是不确定的。


C++17 更新

在 C++17 中,规则已更新。特别是:

在移位运算符表达式E1&lt;&lt;E2 和E1&gt;&gt;E2 中,E1 的每个值计算和副作用都排在E2 的每个值计算和副作用之前。

这意味着它需要代码产生结果b,它输出01。

更多详情请见P0145R3 Refining Expression Evaluation Order for Idiomatic C++。

【讨论】:

  • @Maxim:感谢您的解释。通过您解释的电话,这将是未定义的行为。但是现在,我还有一个问题(可能是一个更无聊的问题,我错过了一些基本的东西并大声思考)你是如何推断出 std::operator 的原因
  • @Maxim 并不是说​​它有什么不同,但是由于c 具有int 类型,所以这里的operator&lt;&lt; 是成员函数。
  • @pravs:operator&lt;&lt; 是成员函数还是独立函数都不会影响序列点。
  • C++ 标准中不再使用“序列点”。它不精确,已替换为“先排序/后排序”关系。
  • So the result of the above is undefined. 你的解释只适用于unspecified,不适用于undefined。 JamesKanze 解释了它是如何更该死的 undefined in his answer though.
【解决方案2】:

从技术上讲,总的来说这是未定义的行为。

但是,答案有两个重要方面。

代码声明:

std::cout << a++ << a;

被评估为:

std::operator<<(std::operator<<(std::cout, a++), a);

该标准没有定义函数参数的评估顺序。
所以要么:

  • std::operator&lt;&lt;(std::cout, a++) 首先评估或
  • a首先被评估或
  • 它可能是任何实现定义的顺序。

根据标准,此订单未指定[Ref 1]。

[参考 1]C++03 5.2.2 函数调用
第 8 段

参数的评估顺序未指定。参数表达式评估的所有副作用在输入函数之前生效。后缀表达式和参数表达式列表的求值顺序未指定。

此外,在对函数的参数求值之间没有序列点,但仅在对所有参数求值后才存在序列点[Ref 2]。

[参考 2]C++03 1.9 程序执行 [intro.execution]:
第 17 段:

当调用函数时(无论函数是否内联),在函数体中的任何表达式或语句执行之前,在对所有函数参数(如果有)求值之后都会有一个序列点。

请注意,此处c 的值被多次访问而没有中间序列点,对此标准表示:

[Ref 3]C++03 5 表达式 [expr]:
第 4 段:

....
在前一个序列点和下一个序列点之间,一个标量对象的存储值最多只能通过表达式的计算修改一次。此外,应仅访问先前值以确定要存储的值。对于完整的子表达式的每个允许排序,都应满足本段的要求。 表达; 否则行为未定义。

代码修改c 不止一次,没有干预序列点,并且没有被访问以确定存储对象的值。这明显违反了上述条款,因此标准规定的结果是Undefined Behavior[Ref 3]。

【讨论】:

  • 我的意思是,c 只修改了一次,所以程序可以合法地打印 01 或 10,但不能做一些奇怪的事情。我的理解正确吗?
  • 从技术上讲,行为是未定义的,因为修改了一个对象,并且在没有中间序列点的情况下在其他地方访问它。未定义 not 未指定;它给实施留下了更大的余地。
  • @Als 是的。我没有看到你的编辑(尽管我对 jrok 的声明做出反应,即程序不能做一些奇怪的事情——它可以)。就目前而言,您的编辑版本很好,但在我看来,关键词是部分排序;序列点仅引入部分排序。
  • 新的 C++0x 标准说的基本相同,但在不同的部分和不同的措辞:) Quote: (1.9 Program Execution [intro.execution], par 15): "If相对于同一标量对象上的另一个副作用或使用同一标量对象的值的值计算,标量对象上的副作用是无序的,行为未定义。”
  • 我相信这个答案有一个错误。 “std::cout指定了行为和执行顺序。
【解决方案3】:

序列点只定义一个部分排序。在你的情况下,你有 (一旦完成重载决议):

std::cout.operator<<( a++ ).operator<<( a );

a++ 和第一次调用之间有一个序列点 std::ostream::operator&lt;&lt; 之间有一个序列点 第二次a 和第二次调用std::ostream::operator&lt;&lt;,但是有 在a++ 和a 之间没有序列点;唯一的订购 约束是 a++ 被充分评估(包括副作用) 在第一次致电operator&lt;&lt; 之前,第二次致电a 完全 在第二次调用 operator&lt;&lt; 之前进行评估。 (还有 因果排序约束:第二次调用operator&lt;&lt; 不能 在第一个之前,因为它需要第一个的结果作为 论点。)§5/4 (C++03) 规定:

除非另有说明,否则 评估单个运算符的操作数和子表达式 个别表达,以及副作用发生的顺序, 未指定。在上一个和下一个序列点之间有一个标量 对象的存储值最多由 表达式的评估。此外,先验值应为 仅访问以确定要存储的值。的要求 本款应满足每个允许的订购 完整表达式的子表达式;否则行为是 不明确的。

您的表达式允许的顺序之一是a++, a, first 致电operator&lt;&lt;,第二次致电operator&lt;&lt;;这修改了 a (a++) 的存储值,并访问它而不是确定 新值(第二个a),行为未定义。

【讨论】:

  • 您对标准的引用中的一个要点。 IIRC 在处理重载运算符时包含一个异常,它将运算符视为一个函数,因此在第一次和第二次调用 std::ostream::operator
  • @ChristopherSmith 重载运算符的行为类似于函数调用。如果c 是一个用户类型,其中用户定义了++,而不是int,则结果将是未指定的,但不会有未定义的行为。
  • @ChristopherSmith 你在哪里看到foo(foo(bar(c)), c) 中两个c 之间的序列点?函数被调用和返回时有一个序列点,但在两个c 的评估之间不需要函数调用。
  • @ChristopherSmith 如果c 是一个UDT,则重载的运算符将 是函数调用,并且会引入一个序列点,因此不会未定义行为。但是仍然不确定子表达式c是在c++之前还是之后评估的,所以你是否得到了递增的版本将不会被指定(理论上,每个都不必相同时间)。
  • @ChristopherSmith 序列点之前的所有内容都将发生在序列点之后的任何内容之前。但是序列点只定义了部分排序。例如,在有问题的表达式中,子表达式c 和c++ 之间没有序列点,因此两者可能以任意顺序出现。至于分号......它们只会在它们是完整表达式的情况下产生一个序列点。其他重要的序列点是函数调用:f(c++) 将在f 中看到递增的c,而逗号运算符&amp;&amp;、|| 和?: 也会导致序列点。
【解决方案4】:

正确的答案是质疑问题。这种说法是不可接受的,因为读者看不到明确的答案。另一种看待它的方式是,我们引入了副作用 (c++),使语句更难解释。简洁的代码很棒,只要它的意思清楚。

【讨论】:

  • 这个问题可能表现出糟糕的编程习惯(甚至是无效的 C++)。但是答案应该回答问题,指出什么是错的以及为什么是错的。对问题的评论不是答案,即使它们是完全有效的。充其量,这可以是评论,而不是答案。
猜你喜欢
  • 2021-12-23
  • 2022-11-13
  • 2015-09-02
  • 2018-07-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-06-24
  • 1970-01-01
相关资源
最近更新 更多