#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?
}

当前回答

虽然像a = a++或a++ + a++这样的表达式的语法是合法的,但这些结构的行为是未定义的,因为在C标准中不遵守shall。C99 6.5 p2:

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

脚注73进一步澄清

本段给出了未定义的语句表达式,如 I = ++ I + 1; A [i++] = i; 同时允许 I = I + 1; A [i] = i;

各序列点列于C11(和C99)的附件C:

The following are the sequence points described in 5.1.2.3: Between the evaluations of the function designator and actual arguments in a function call and the actual call. (6.5.2.2). Between the evaluations of the first and second operands of the following operators: logical AND && (6.5.13); logical OR || (6.5.14); comma , (6.5.17). Between the evaluations of the first operand of the conditional ? : operator and whichever of the second and third operands is evaluated (6.5.15). The end of a full declarator: declarators (6.7.6); Between the evaluation of a full expression and the next full expression to be evaluated. The following are full expressions: an initializer that is not part of a compound literal (6.7.9); the expression in an expression statement (6.8.3); the controlling expression of a selection statement (if or switch) (6.8.4); the controlling expression of a while or do statement (6.8.5); each of the (optional) expressions of a for statement (6.8.5.3); the (optional) expression in a return statement (6.8.6.4). Immediately before a library function returns (7.1.4). After the actions associated with each formatted input/output function conversion specifier (7.21.6, 7.29.2). Immediately before and immediately after each call to a comparison function, and also between any call to a comparison function and any movement of the objects passed as arguments to that call (7.22.5).

C11同一段的措词是:

如果标量对象上的副作用相对于同一标量对象上的不同副作用或使用同一标量对象的值进行的值计算没有排序,则行为未定义。如果一个表达式的子表达式有多个允许的顺序,那么如果这种未排序的副作用出现在任意一个顺序中,则该行为是未定义的。


您可以在程序中检测此类错误,例如使用带有-Wall和-Werror的最新版本的GCC,然后GCC将直接拒绝编译您的程序。gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005的输出如下:

% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function ‘main’:
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
    i = i++ + ++i;
    ~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
    i = (i++);
    ~~^~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
    u = u++ + ++u;
    ~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
    u = (u++);
    ~~^~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
    v = v++ + ++v;
    ~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors

重要的部分是知道什么是序列点,什么是序列点,什么不是。例如,逗号操作符是一个序列点,所以

j = (i ++, ++ i);

定义良好,并将I加1,得到旧值,丢弃旧值;然后在逗号运算符,解决副作用;然后将I加1,结果值就变成了表达式的值——也就是说,这只是一种写j = (I += 2)的人为方法,这也是一种“聪明”的写法

i += 2;
j = i;

然而,函数参数列表中的,不是逗号操作符,并且在不同参数的计算之间没有序列点;相反,他们对彼此的评价是没有顺序的;函数调用

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

具有未定义的行为,因为在函数参数中i++和++i的计算之间没有序列点,因此i的值在前一个序列点和下一个序列点之间被i++和++i修改了两次。

其他回答

只要编译和反汇编你的代码行,如果你如此倾向于知道它是如何得到你所得到的。

这是我从我的机器上得到的,以及我认为正在发生的事情:

$ cat evil.c
void evil(){
  int i = 0;
  i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
   0x00000000 <+0>:   push   %ebp
   0x00000001 <+1>:   mov    %esp,%ebp
   0x00000003 <+3>:   sub    $0x10,%esp
   0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   $0x1,-0x4(%ebp)  // i++     i = 1
   0x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 1
   0x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 2
   0x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 3
   0x00000019 <+25>:  addl   $0x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave  
   0x0000001e <+30>:  ret
End of assembler dump.

(我…假设0x00000014指令是某种编译器优化?)

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

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语言中未定义行为的概念和许多其他变体。

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

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

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

和6.5.16赋值操作符,§4:

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

C有未定义行为的概念,也就是说,一些语言结构在语法上是有效的,但你不能预测代码运行时的行为。

据我所知,该标准并没有明确说明为什么存在未定义行为的概念。在我看来,这只是因为语言设计者想在语义上有一些余地,而不是要求所有实现以完全相同的方式处理整数溢出,这很可能会带来严重的性能损失,他们只是让行为未定义,所以如果你编写的代码导致整数溢出,任何事情都可能发生。

那么,考虑到这一点,为什么这些是“问题”呢?语言清楚地表明,某些事情会导致未定义的行为。没有什么问题,没有什么“应该”牵涉其中。如果当其中一个涉及的变量被声明为volatile时,未定义的行为发生了变化,这并不能证明或改变任何东西。它是没有定义的;你不能对这种行为进行推理。

你最有趣的例子就是

u = (u++);

是教科书上未定义行为的例子(见维基百科关于序列点的条目)。

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网站(也出版了一本书)的条目,即这里、这里和这里。