为什么此代码打印 1 2 2 而不是预期的 3 3 1?

Why does this code print 1 2 2 and not the expected 3 3 1?

注意:这是一个self-Q/A更明显的针对《Let us C》一书宣扬的错误信息。另外,请让我们不要讨论 ,这个问题是关于 C.

我正在读 Yashwant Kanetkar 的书“让我们 C”。

书中有如下例子:

#include <stdio.h>

int main(void) {
    int a = 1;
    printf("%d %d %d", a, ++a, a++);
}

作者声称这段代码应该输出3 3 1:

Surprisingly, it outputs 3 3 1. This is because C’s calling convention is from right to left. That is, firstly 1 is passed through the expression a++ and then a is incremented to 2. Then result of ++a is passed. That is, a is incremented to 3 and then passed. Finally, latest value of a, i.e. 3, is passed. Thus in right to left order 1, 3, 3 get passed. Once printf( ) collects them it prints them in the order in which we have asked it to get them printed (and not the order in which they were passed). Thus 3 3 1 gets printed.

然而,当我用clang编译代码并运行时,结果是1 2 2,而不是3 3 1;这是为什么?

作者错了。不仅在 C 中未指定函数参数的求值顺序,而且求值之间的顺序也是无序的。雪上加霜的是,在独立表达式中没有中间序列点的情况下读取和修改同一对象(此处 a 的值在 3 个独立表达式中求值并在 2 中修改)具有 未定义的行为,因此编译器可以自由地生成 任何 它认为合适的代码。

详情见Why are these constructs using pre and post-increment undefined behavior?

C’s calling convention

这与调用约定无关!而且 C 甚至没有指定特定的调用约定——“cdecl”等是 x86 PC 的发明(与此无关)。正确且正式的 C 语言术语是 求值顺序

评估的顺序是未指定的行为(正式定义的术语),这意味着我们无法知道是否留给从右到左。编译器不需要对其进行记录,也不需要根据具体情况采用一致的顺序。

但是这里还有一个更严重的问题:so-called未测序side-effects。 C17 6.5/2 状态:

If a side effect on a scalar object is unsequenced relative to either a different side effect on the same scalar object or a value computation using the value of the same scalar object, the behavior is undefined. If there are multiple allowable orderings of the subexpressions of an expression, the behavior is undefined if such an unsequenced side effect occurs in any of the orderings.

这段文字对于普通人来说很难消化。从 language-lawyer 书呆子语言到简单英语的粗略简化翻译:

  • 如果表达式中使用的二元运算符1)没有明确说明操作数的执行顺序2)并且,
  • a side-effect,比如改变值,恰好是表达式中的一个变量,并且,
  • 同一变量在同一表达式的其他地方使用,

然后程序就坏了,可能会做任何事情。


1) 有 2 个操作数的运算符。
2) 大多数运营商不这样做,只有少数例外,如 || && , 运营商这样做。

我多年前在大学里读过(C step by Mitchell Waite),其中 c 编译器(某些编译器)通过将参数从右向左推(到说明符)然后弹出来为 printf 使用堆栈一张一张打印出来。

I write this code and the output is 3 3 1 : Online Demo.

根据堆栈中的书,我们有这样的东西

但在与专家(在评论中)进行了一些小挑战之后,我发现也许在某些编译器中这个序列是正确的,但并非对所有编译器都适用。

@Lundin provided this code and the output is 1 2 2 :Online Demo

and @Bob__ provided another example which output is totally different: Online Demo

它完全取决于编译器实现并且具有未定义的行为。

作者错了,书中有多次这样的错误陈述。

在 C 中,printf("%d %d %d", a, ++a, a++); 的行为是未定义的,因为函数参数的计算顺序是未指定的,并且因为在 2 个序列点之间多次修改同一个对象具有未定义的行为,只是为了命名这两个。

请注意,这本书被引用为 不要在 The Definitive C Book Guide and List 中使用 来为这个精确的例子提供不正确的建议。

另请注意,其他语言可能对此类语句有不同的看法,特别是 java 行为已完全定义。