#include <stdio.h>

int main(void)
{
   int i = 0;
   i = i++ + ++i;
   printf("%d\n", i); // 3

   i = 1;
   i = (i++);
   printf("%d\n", i); // 2 Should be 1, no ?

   volatile int u = 0;
   u = u++ + ++u;
   printf("%d\n", u); // 1

   u = 1;
   u = (u++);
   printf("%d\n", u); // 2 Should also be one, no ?

   register int v = 0;
   v = v++ + ++v;
   printf("%d\n", v); // 3 (Should be the same as u ?)

   int w = 0;
   printf("%d %d\n", ++w, w); // shouldn't this print 1 1

   int x[2] = { 5, 8 }, y = 0;
   x[y] = y ++;
   printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}

当前回答

ISO W14站点的文档n1188提供了关于这种计算的一个很好的解释。

我解释这些想法。

适用于这种情况的ISO 9899标准的主要规则是6.5p2。

在前一个序列点和下一个序列点之间,通过表达式求值,对象的存储值最多修改一次。此外,前面的值只能被读取,以确定要存储的值。

像i=i++这样的表达式中的序列点在i=i之前和i++之后。

In the paper that I quoted above it is explained that you can figure out the program as being formed by small boxes, each box containing the instructions between 2 consecutive sequence points. The sequence points are defined in annex C of the standard, in the case of i=i++ there are 2 sequence points that delimit a full-expression. Such an expression is syntactically equivalent with an entry of expression-statement in the Backus-Naur form of the grammar (a grammar is provided in annex A of the Standard).

所以盒子里的指令顺序没有明确的顺序。

i=i++

可以解释为

tmp = i
i=i+1
i = tmp

或者是

tmp = i
i = tmp
i=i+1

因为解释代码i=i++的所有这些形式都是有效的,并且都生成不同的答案,所以行为是未定义的。

因此,序列点可以从组成程序的每个方框的开始和结束处看到(方框是C语言中的原子单位),并且在方框中,指令的顺序并不在所有情况下都是定义的。改变顺序有时会改变结果。

编辑:

其他解释这种歧义的很好的来源是c-faq网站(也出版了一本书)的条目,即这里、这里和这里。

其他回答

我认为C99标准的相关部分是6.5表达式,§2

在前一个序列点和下一个序列点之间,一个对象应该有它的存储值 由表达式求值最多修改一次。此外,先验值 应该是只读的,以确定要存储的值。

和6.5.16赋值操作符,§4:

操作数的求值顺序未指定。如果试图修改 赋值运算符的结果或在下一个序列点之后访问它的结果 行为是未定义的。

虽然任何编译器和处理器实际上都不太可能这样做,但在C标准下,编译器使用以下序列实现"i++"是合法的:

In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value

虽然我不认为有任何处理器支持这样的硬件,可以有效地完成这样的事情,人们可以很容易地想象这样的行为会使多线程代码更容易(例如,它将保证如果两个线程同时尝试执行上面的序列,I将增加2),而且未来的某些处理器可能会提供类似的功能,这并不是完全不可想象的。

If the compiler were to write i++ as indicated above (legal under the standard) and were to intersperse the above instructions throughout the evaluation of the overall expression (also legal), and if it didn't happen to notice that one of the other instructions happened to access i, it would be possible (and legal) for the compiler to generate a sequence of instructions that would deadlock. To be sure, a compiler would almost certainly detect the problem in the case where the same variable i is used in both places, but if a routine accepts references to two pointers p and q, and uses (*p) and (*q) in the above expression (rather than using i twice) the compiler would not be required to recognize or avoid the deadlock that would occur if the same object's address were passed for both p and q.

这个问题通常是与代码相关的问题的重复链接,比如

printf("%d %d\n", i, i++);

or

printf("%d %d\n", ++i, i++);

或者类似的变体。

虽然正如前面所述,这也是未定义的行为,但当涉及printf()时,与如下语句进行比较时,会有细微的差异:

x = i++ + i++;

在以下声明中:

printf("%d %d\n", ++i, i++);

printf()中参数的求值顺序未指定。这意味着,表达式i++和++i可以以任何顺序求值。C11标准对此有一些相关的描述:

附件J,未指明的行为

函数指示符、参数和 参数中的子表达式在函数调用中求值 (6.5.2.2)。

3.4.4,未指定的行为

使用未指定的值,或其他行为 国际标准提供了两种或两种以上的可能性 在任何情况下都没有进一步的要求。 未指定的行为的一个例子是 函数的参数被求值。

未指定的行为本身不是问题。想想这个例子:

printf("%d %d\n", ++x, y++);

这也具有未指定的行为,因为++x和y++的求值顺序是未指定的。但这是完全合法有效的声明。在这个语句中没有未定义的行为。因为修改(++x和y++)是对不同的对象进行的。

是什么呈现下面的语句

printf("%d %d\n", ++i, i++);

作为未定义的行为是这两个表达式修改相同的对象I没有中间序列点的事实。


另一个细节是printf()调用中涉及的逗号是分隔符,而不是逗号操作符。

这是一个重要的区别,因为逗号操作符确实在它们的操作数求值之间引入了一个序列点,这使得以下行为合法:

int i = 5;
int j;

j = (++i, i++);  // No undefined behaviour here because the comma operator 
                 // introduces a sequence point between '++i' and 'i++'

printf("i=%d j=%d\n",i, j); // prints: i=7 j=6

逗号操作符从左到右计算其操作数,只产生最后一个操作数的值。所以在j = (++i, i++);中,++i将i增加到6,i++产生i(6)的旧值,该值被分配给j。然后由于后增量,i变成7。

如果函数调用中的逗号是逗号操作符,那么

printf("%d %d\n", ++i, i++);

不会有问题的。但是它调用了未定义的行为,因为这里的逗号是分隔符。


对于那些不熟悉未定义行为的人来说,阅读《每一个C程序员都应该知道关于未定义行为的事情》可以帮助他们理解C语言中未定义行为的概念和许多其他变体。

本文:未定义、未指定和实现定义的行为也是相关的。

ISO W14站点的文档n1188提供了关于这种计算的一个很好的解释。

我解释这些想法。

适用于这种情况的ISO 9899标准的主要规则是6.5p2。

在前一个序列点和下一个序列点之间,通过表达式求值,对象的存储值最多修改一次。此外,前面的值只能被读取,以确定要存储的值。

像i=i++这样的表达式中的序列点在i=i之前和i++之后。

In the paper that I quoted above it is explained that you can figure out the program as being formed by small boxes, each box containing the instructions between 2 consecutive sequence points. The sequence points are defined in annex C of the standard, in the case of i=i++ there are 2 sequence points that delimit a full-expression. Such an expression is syntactically equivalent with an entry of expression-statement in the Backus-Naur form of the grammar (a grammar is provided in annex A of the Standard).

所以盒子里的指令顺序没有明确的顺序。

i=i++

可以解释为

tmp = i
i=i+1
i = tmp

或者是

tmp = i
i = tmp
i=i+1

因为解释代码i=i++的所有这些形式都是有效的,并且都生成不同的答案,所以行为是未定义的。

因此,序列点可以从组成程序的每个方框的开始和结束处看到(方框是C语言中的原子单位),并且在方框中,指令的顺序并不在所有情况下都是定义的。改变顺序有时会改变结果。

编辑:

其他解释这种歧义的很好的来源是c-faq网站(也出版了一本书)的条目,即这里、这里和这里。

回答这个问题的另一种方法是,不要陷入序列点和未定义行为的神秘细节中,而是简单地问,它们应该是什么意思?程序员试图做什么?

第一个片段,i = i++ + ++i,在我的书中很明显是疯狂的。没有人会把它写进真正的程序中,它的功能并不明显,没有人会尝试编写的算法会导致这种特殊的人为操作序列。因为你我都不清楚它应该做什么,所以在我的书中,如果编译器不能弄清楚它应该做什么也没关系。

第二个片段i = i++比较容易理解。很明显,有人试图增加i,并将结果赋值回给i。但是在c中有几种方法可以做到这一点。将1加到i,并将结果赋值回给i,这在几乎任何编程语言中都是相同的:

i = i + 1

当然,C有一个方便的快捷方式:

i++

这意味着,“将1加到i,并将结果赋给i”。所以如果我们通过写作来构造一个两者的大杂烩

i = i++

我们真正说的是“给i加1,然后把结果赋给i,再把结果赋给i”我们感到困惑,所以如果编译器也感到困惑,也不会太困扰我。

实际上,只有当人们将这些疯狂的表达式用作c++应该如何工作的人为示例时,才会写出这些疯狂的表达式。当然,理解++的工作原理也很重要。但使用++的一个实际规则是,“如果使用++的表达式的含义不明显,就不要写它。”

我们曾经在comp.lang.c上花了无数个小时讨论这样的表达式以及为什么它们是未定义的。我的两个较长的回答,试图真正解释为什么,被存档在网络上:

为什么标准没有定义它们的作用? 运算符的优先级不是决定求值的顺序吗?

请参见问题3.8和C常见问题列表第三部分的其他问题。