关于C#:为什么这些构造使用前后增量未定义的行为?

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>

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

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

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

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

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

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

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


C具有未定义行为的概念,即某些语言结构在语法上是有效的,但在代码运行时您无法预测行为。

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

那么,考虑到这一点,为什么这些"问题"?语言清楚地表明某些事情会导致不明确的行为。没有问题,没有"应该"参与。如果所涉及的变量之一声明为volatile时未定义的行为发生了变化,则不会证明或更改任何内容。它是未定义的;您不能对该行为进行推理。

你最有趣的例子

1
u = (u++);

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


只要编译和反汇编你的代码行,如果你这么想知道它到底是什么,你就能得到你想要的。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ 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.

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


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

Between the previous and next sequence point an object shall have its stored value
modified at most once by the evaluation of an expression. Furthermore, the prior value
shall be read only to determine the value to be stored.

和6.5.16分配操作员,?4:

The order of evaluation of the operands is unspecified. If an attempt is made to modify
the result of an assignment operator or to access it after the next sequence point, the
behavior is undefined.


这种行为不能真正解释,因为它调用了未指定的行为和未定义的行为,所以我们不能对这段代码做任何一般性的预测,尽管如果您阅读了Olve Maudal的作品,例如Deep C和Unspecified and Undefined,有时您可以用特定的编译器和environm在非常特定的情况下做出很好的猜测。但是请不要在接近生产的地方这样做。

因此,在C99标准草案第11(0)段第3段(强调我的)中,继续讨论未指明的行为:

The grouping of operators and operands is indicated by the syntax.74) Except as specified
later (for the function-call (), &&, ||, ?:, and comma operators), the order of evaluation of subexpressions and the order in which side effects take place are both unspecified.

所以当我们有这样一条线时:

1
i = i++ + ++i;

我们不知道是否首先评估i++++i。这主要是为了给编译器提供更好的优化选项。

由于程序在序列点之间多次修改变量(iu等),我们在这里也有未定义的行为。根据标准草案6.5第2段(强调矿山):

Between the previous and next sequence point an object shall have its stored value
modified at most once by the evaluation of an expression. Furthermore, the prior value
shall be read only to determine the value to be stored.

它引用了以下未定义的代码示例:

1
2
i = ++i + 1;
a[i++] = i;

在所有这些示例中,代码试图在同一序列点中多次修改一个对象,在每种情况下都以;结束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
i = i++ + ++i;
^   ^       ^

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^

未指明的行为在3.4.4节的C99标准草案中定义为:

use of an unspecified value, or other behavior where this International Standard provides
two or more possibilities and imposes no further requirements on which is chosen in any
instance

未定义行为在3.4.3节中定义为:

behavior, upon use of a nonportable or erroneous program construct or of erroneous data,
for which this International Standard imposes no requirements

并指出:

Possible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message).


我们从这里引用标准答案C,这篇论文的结构①行为是未定义的。明白为什么这些结构的行为是未定义的术语,让我们先了解论文的标准:光明的C11

sequenced(5.1.2.3):

Given any two evaluations A and B, if A is sequenced before B, then the execution of A shall precede the execution of B.

unsequenced:

If A is not sequenced before or after B, then A and B are unsequenced.

评价两个东西:一个CAN

  • 计算出的价值,它的工作结果和表达;
  • 侧效果,这是修改的对象。

序列点:

The presence of a sequence point between the evaluation of expressions A and B implies that every value computation and side effect associated with A is sequenced before every value computation and side effect associated with B.

现在的问题来表达,这样

1
2
int i = 1;
i = i++;

标准说:

6.5表达式:

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. [...]

因此,上述两个侧invokes泛素表达,因为影响相同的对象是一iunsequenced相对对方。这不是一sequenced均值是否侧效应由分配到i将完成前或后,由++侧效应。取决于是否发生之前或增量赋值后,将产生不同的结果,这是与一个未定义的行为的情况下。

在左i重命名为学院的il和分配在分配权(在ir表达i++)加载的话,表达的样

1
2
il = ir++     // Note that suffix l and r are used for the sake of clarity.
              // Both il and ir represents the same object.

重要点是:对于++后缀运算符

just because the ++ comes after the variable does not mean that the increment happens late. The increment can happen as early as the compiler likes as long as the compiler ensures that the original value is used.

它意味il = ir++可以不被表达为

1
2
3
temp = ir;      // i = 1
ir = ir + 1;    // i = 2   side effect by ++ before assignment
il = temp;      // i = 1   result is 1

1
2
3
temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2

产生两种不同的结果,这取决于位置和21序列的赋值和++侧效果和,因此invokes UB。


另一种方式回答这个,而不是bogged下比在arcane序列点和未定义的行为细节,是简单的问,他们是什么意思?《程序员》试图做什么?

第一个片段是足够的,i = i++ + ++i很疯狂,我在我的书。没有人会永远写在一个真正的IT计划,它是什么是显而易见的,没有人可以conceivable算法一直试图进入代码,这是会contrived的序列的操作。因为它不是明显的你和我是它应该做的,很好的书如果编译器不能找出是什么,它应该做的,无论是。

第二i = i++片段,是一个小更容易明白。有人显然是试图增加I,和分配的结果回的。但有一对夫妇的方式这样做。最基本的方式添加1到i,和i赋值的结果回的是相同的,在几乎任何编程语言:

1
i = i + 1

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

1
i++

这意味着,"添加1到i,和i赋值的结果回"。所以如果我们构建一个hodgepodge勋章,由写作

1
i = i++

我们说的是真的"添加1到i,和i的结果回的分配,和分配的结果回到我"。我们明白,它不打扰我太多,如果编译器变得困惑,太。

realistically,唯一的时间是当书面表达的命题得到疯狂的人使用的是他们作为知识+人工的例子是应该的。和当然它是理解作品的重要知识+ +。但实际使用+ +一个规则是,"如果它是在表达上使用均值+ +,不写它。"

我们以前无数的时间花在我们的论文comp.lang.c表达样和为什么他们是未定义的。我的前两个答案,试图去解释为什么,是Web上的存档:

  • 为什么不使用标准的论文做什么?
  • 不确定的precedence阶算子的评价?


这个问题通常被链接为与类似代码相关的问题的副本

1
2
printf("%d %d
"
, i, i++);

1
2
printf("%d %d
"
, ++i, i++);

或类似的变体。

虽然这也是上文所述的未定义行为,但与以下陈述相比,涉及printf()时存在细微差异:

1
   x = i++ + i++;

在以下声明中:

1
2
printf("%d %d
"
, ++i, i++);

printf()中,参数的计算顺序未指明。也就是说,表达式i++++i可以按任何顺序进行计算。C11标准对此有一些相关说明:

附录J,未指明行为

The order in which the function designator, arguments, and
subexpressions within the arguments are evaluated in a function call
(6.5.2.2).

3.4.4、未指明的行为

Use of an unspecified value, or other behavior where this
International Standard provides two or more possibilities and imposes
no further requirements on which is chosen in any instance.

EXAMPLE An example of unspecified behavior is the order in which the
arguments to a function are evaluated.

未指明的行为本身不是问题。考虑这个例子:

1
2
printf("%d %d
"
, ++x, y++);

这也有不明确的行为,因为对++xy++的评估顺序不明确。但这是完全合法和有效的声明。这句话中没有未定义的行为。因为修改(++xy++是对不同的对象进行的。

什么导致了下面的陈述

1
2
printf("%d %d
"
, ++i, i++);

由于未定义的行为,这两个表达式修改同一对象i,而没有中间的序列点。

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

这是一个重要的区别,因为逗号运算符在对其操作数的计算之间引入了一个序列点,这使得以下内容合法:

1
2
3
4
5
6
7
8
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
"
,i, j); // prints: i=7 j=6

逗号运算符从左到右计算其操作数,并仅生成最后一个操作数的值。因此,在j = (++i, i++);中,++ii增加到6中,i++生成i的旧值(6分配给j。然后,由于后增量,i变为7

所以,如果函数调用中的逗号是逗号运算符,那么

1
2
printf("%d %d
"
, ++i, i++);

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

对于那些刚接触到未定义行为的人来说,通过阅读每个C程序员应该了解的关于未定义行为的内容,可以了解C中未定义行为的概念和许多其他变体。

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


虽然任何编译器和处理器都不可能真正做到这一点,但根据C标准,编译器实现"i++"的顺序是合法的:

1
2
3
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将增加两个),而不是全部增加。很难想象未来的处理器会提供这样的功能。

如果编译器按照上述说明编写i++(在标准下是合法的),并在整个表达式的评估过程中散布上述指令(也是合法的),并且如果没有注意到另一个指令中的一个发生在访问i的过程中,那么对于mpiler生成一系列死锁的指令。当然,在两个地方都使用同一个变量i的情况下,编译器几乎肯定会检测到问题,但如果一个例程接受对两个指针pq的引用,并在上述表达式中使用(*p)(*q)(而不是使用i两次),编译器将如果同一对象的地址同时传递给pq,则不需要识别或避免出现死锁。


C标准规定一个变量最多只能在两个序列点之间分配一次。例如,分号就是一个序列点。所以每一个形式的陈述:

1
2
i = i++;
i = i++ + ++i;

等等违反了这个规则。标准还指出行为是未定义的,而不是未指定的。有些编译器确实检测到这些并产生一些结果,但这不符合标准。

然而,两个不同的变量可以在两个序列点之间递增。

1
while(*src++ = *dst++);

在复制/分析字符串时,上述是一种常见的编码实践。


虽然像a = a++a++ + a++这样的表达式的语法是合法的,但是这些构造的行为是不定义的,因为在c标准中不遵守a。C99~65P2:

  • Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression. [72] Furthermore, the prior value shall be read only to determine the value to be stored [73]
  • 脚注73进一步阐明了

  • This paragraph renders undefined statement expressions such as

    1
    2
    i = ++i + 1;
    a[i++] = i;

    while allowing

    1
    2
    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中相同段落的措辞为:

  • 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.84)
  • 您可以通过使用最新版本的gcc和-Wall-Werror来检测程序中的此类错误,然后gcc将直接拒绝编译您的程序。以下是GCC(Ubuntu 6.2.0-5Ubuntu12)6.2.0 20161005的输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    % 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

    重要的是要知道序列点是什么——什么是序列点,什么不是序列点。

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

    定义很好,会将i增加一个,产生旧值,丢弃该值;然后在逗号运算符处,解决副作用;然后将i增加一个,得到的值就成为表达式的值,即,这只是一种编写j = (i += 2)的人为方法,这又是一种"聪明"的方法,它可以使仪式

    1
    2
    i += 2;
    j = i;

    但是,函数参数列表中的,不是逗号运算符,并且不同参数的计算之间没有序列点;相反,它们的计算相互之间没有排序;因此函数调用

    1
    2
    3
    int i = 0;
    printf("%d %d
    "
    , i++, ++i, i);

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


    在https://stackoverflow.com/questions/29505280/increming-array-index-in-c中,有人询问了如下语句:

    1
    2
    3
    4
    5
    int k[] = {0,1,2,3,4,5,6,7,8,9,10};
    int i = 0;
    int num;
    num = k[++i+k[++i]] + k[++i];
    printf("%d", num);

    打印7…OP预计它将打印6。

    ++i增量不能保证在其余计算之前全部完成。事实上,不同的编译器在这里会得到不同的结果。在您提供的示例中,首先执行2个++i,然后读取k[]的值,然后读取最后一个++i,然后读取k[]

    1
    2
    num = k[i+1]+k[i+2] + k[i+3];
    i += 3

    现代编译器会很好地优化这一点。实际上,可能比您最初编写的代码要好(假设它按照您希望的方式工作)。


    您的问题可能不是,"为什么这些构造在C中是未定义的行为?"。您的问题可能是,"为什么这段代码(使用++)没有给我预期的值?"有人把你的问题标记为副本,然后把你送到这里。好的。

    这个答案试图回答这个问题:为什么您的代码没有给出您期望的答案,以及您如何学习识别(并避免)无法按预期工作的表达式。好的。

    我假设你已经听过c的++--操作符的基本定义,以及前缀形式++x与后缀形式x++的区别。但是这些操作人员很难思考,所以为了确保你理解,也许你写了一个小的测试程序好的。

    1
    2
    3
    int x = 5;
    printf("%d %d %d
    "
    , x, ++x, x++);

    但是,令你惊讶的是,这个程序并没有帮助你理解——它打印了一些奇怪的、意外的、无法解释的输出,这表明++可能做了一些完全不同的事情,根本不像你想象的那样。好的。

    或者,也许你在看一个难以理解的表达,比如好的。

    1
    2
    3
    4
    int x = 5;
    x = x++ + ++x;
    printf("%d
    "
    , x);

    也许有人把代码当成拼图给了你。这段代码也没有意义,特别是如果您运行它——并且如果您在两个不同的编译器下编译和运行它,您可能会得到两个不同的答案!怎么了?哪个答案是正确的?(答案是两者都是,或者两者都不是。)好的。

    正如您现在听到的,所有这些表达式都是未定义的,这意味着C语言无法保证它们将做什么。这是一个奇怪而令人惊讶的结果,因为您可能认为任何可以编写的程序,只要它编译并运行,都会生成一个独特的、定义良好的输出。但在未定义行为的情况下,情况并非如此。好的。

    什么使表达式未定义?涉及++--的表达式是否总是未定义?当然不是:这些是有用的操作符,如果您正确地使用它们,它们是完全定义好的。好的。

    对于表达式,我们讨论的是什么使得它们不明确,当一次发生太多事情时,当我们不确定事情将以什么顺序发生时,但是当顺序关系到结果时,我们得到。好的。

    让我们回到我在这个答案中使用的两个例子。当我写好的。型

    1
    2
    printf("%d %d %d
    "
    , x, ++x, x++);

    问题是,在调用printf之前,编译器是否首先计算x的值,或者x++的值,或者可能是++x的值?但事实证明我们不知道。在C语言中,没有规则规定函数的参数可以从左到右、从右到左或以其他顺序进行计算。所以我们不能说编译器是先执行x,然后执行++x,然后执行x++,或者执行x++,然后执行++x,然后执行x,或者执行其他命令。但顺序显然很重要,因为根据编译器使用的顺序,我们将清楚地得到printf打印的不同结果。好的。型

    这个疯狂的表情呢?好的。型

    1
    x = x++ + ++x;

    这个表达式的问题在于,它包含三种不同的修改x值的尝试:(1)x++部分尝试将1加到x,将新值存储在x中,并返回x的旧值;(2)++x部分尝试将1加到x,将新值存储在x中,并返回ed的新值。ocx1〔1〕和(3)x =部分试图将另外两个任务的总和分配回x。这三个试图分配的任务中哪一个会"赢"?这三个值中的哪一个将实际分配给x?再一次,也许令人惊讶的是,在C语言中没有任何规则可以告诉我们。好的。型

    您可能会想象优先级、关联性或从左到右的评估会告诉您事情发生的顺序,但事实并非如此。你可能不相信我,但请相信我的话,我会再说一遍:优先级和关联性并不能决定C中表达式的计算顺序的每一个方面。特别是,如果在一个表达式中有多个不同的点,我们试图将一个新值赋给诸如x之类的东西,优先级和关联性。这些尝试中的哪一个发生在第一个、最后一个或任何事情上,并不能告诉我们。好的。型

    因此,如果您想确保所有程序都定义良好,可以编写哪些表达式,也可以不编写哪些表达式,那么就不要再使用这些背景和介绍了。好的。型

    这些表达都很好:好的。型

    1
    2
    3
    4
    5
    6
    7
    8
    y = x++;
    z = x++ + y++;
    x = x + 1;
    x = a[i++];
    x = a[i++] + b[j++];
    x[i++] = a[j++] + b[k++];
    x = *p++;
    x = *p++ + *q++;

    这些表达式都是未定义的:好的。型

    1
    2
    3
    4
    5
    6
    7
    x = x++;
    x = x++ + ++x;
    y = x + x++;
    a[i] = i++;
    a[i++] = i;
    printf("%d %d %d
    "
    , x, ++x, x++);

    最后一个问题是,你怎么知道哪些表达式是定义良好的,哪些表达式是未定义的?好的。型

    正如我之前所说,未定义的表达式是指那些同时进行得太多、无法确定事物发生的顺序以及顺序的重要性的表达式:好的。型百万千克1如果有一个变量在两个或多个不同的地方被修改(分配给),您如何知道哪个修改首先发生?百万千克1百万千克1如果有一个变量在一个地方被修改,而它的值在另一个地方被使用,你如何知道它是使用旧值还是使用新值?百万千克1

    作为1的示例,在表达式中好的。型

    1
    x = x++ + ++x;

    有三次尝试修改'x。好的。型

    作为2的一个例子,在表达式中好的。

    1
    y = x + x++;

    我们都使用EDOCX1的值(0),并对其进行修改。好的。

    这就是答案:确保在您编写的任何表达式中,每个变量最多修改一次,如果修改了某个变量,您也不会尝试在其他地方使用该变量的值。好的。好啊。


    ISO W14网站的文件N1188中提供了此类计算中发生的情况的良好解释。

    我解释这些想法。

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

    Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be read only to determine the value to be stored.

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

    在我上面引用的那篇文章中,解释说你可以把程序看成是由小盒子组成的,每个盒子包含两个连续序列点之间的指令。序列点在本标准附录C中定义,对于i=i++,有2个序列点界定完整表达式。这种表达在句法上等同于文法巴科斯-诺尔形式的expression-statement条目(文法见本标准附录A)。

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

    1
    i=i++

    可以解释为

    1
    2
    3
    tmp = i
    i=i+1
    i = tmp

    或作为

    1
    2
    3
    tmp = i
    i = tmp
    i=i+1

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

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

    编辑:

    解释这种模棱两可的另一个好来源是来自C-FAQ网站(也作为一本书出版)的条目,即这里、这里和这里。


    原因是程序正在运行未定义的行为。问题在于评估顺序,因为根据C++ 98标准不需要序列点(根据C++ 11术语,没有一个操作在一个或另一个之前被排序)。

    但是,如果你坚持使用一个编译器,你会发现这种行为是持久的,只要你不添加函数调用或指针,这会使行为更加混乱。

    • 因此,首先是GCC:使用Nuwen Mingw 15 GCC 7.1,您将获得:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      #include<stdio.h>
      int main(int argc, char ** argv)
      {
      int i = 0;
      i = i++ + ++i;
      printf("%d
      "
      , i); // 2

      i = 1;
      i = (i++);
      printf("%d
      "
      , i); //1

      volatile int u = 0;
      u = u++ + ++u;
      printf("%d
      "
      , u); // 2

      u = 1;
      u = (u++);
      printf("%d
      "
      , u); //1

      register int v = 0;
      v = v++ + ++v;
      printf("%d
      "
      , v); //2

      }

    GCC如何工作?它以从左到右的顺序为右侧(rhs)计算子表达式,然后将值赋给左侧(lhs)。这正是Java和C语言的行为和定义它们的标准。(是的,爪哇和C的等效软件定义了行为)。它以左到右的顺序在RHS语句中逐个评估每个子表达式;对于每个子表达式:首先计算++C(预增量),然后使用值C来操作,然后增加后增量C++。

    根据GCC C++:算子

    In GCC C++, the precedence of the operators controls the order in
    which the individual operators are evaluated

    GCC中定义的行为C++中的等效代码理解:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    #include<stdio.h>
    int main(int argc, char ** argv)
    {
        int i = 0;
        //i = i++ + ++i;
        int r;
        r=i;
        i++;
        ++i;
        r+=i;
        i=r;
        printf("%d
    "
    , i); // 2

        i = 1;
        //i = (i++);
        r=i;
        i++;
        i=r;
        printf("%d
    "
    , i); // 1

        volatile int u = 0;
        //u = u++ + ++u;
        r=u;
        u++;
        ++u;
        r+=u;
        u=r;
        printf("%d
    "
    , u); // 2

        u = 1;
        //u = (u++);
        r=u;
        u++;
        u=r;
        printf("%d
    "
    , u); // 1

        register int v = 0;
        //v = v++ + ++v;
        r=v;
        v++;
        ++v;
        r+=v;
        v=r;
        printf("%d
    "
    , v); //2
    }

    然后我们去Visual Studio。Visual Studio 2015,您可以获得:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    #include<stdio.h>
    int main(int argc, char ** argv)
    {
        int i = 0;
        i = i++ + ++i;
        printf("%d
    "
    , i); // 3

        i = 1;
        i = (i++);
        printf("%d
    "
    , i); // 2

        volatile int u = 0;
        u = u++ + ++u;
        printf("%d
    "
    , u); // 3

        u = 1;
        u = (u++);
        printf("%d
    "
    , u); // 2

        register int v = 0;
        v = v++ + ++v;
        printf("%d
    "
    , v); // 3
    }

    Visual Studio是如何工作的,它采用另一种方法,在第一遍中计算所有的预增量表达式,然后在第二遍中使用变量值,在第三遍中从rhs分配给lhs,然后在最后一遍中计算所有的后增量表达式。

    因此,在定义行为C++中,等价于VisualC++的理解:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    #include<stdio.h>
    int main(int argc, char ** argv)
    {
        int r;
        int i = 0;
        //i = i++ + ++i;
        ++i;
        r = i + i;
        i = r;
        i++;
        printf("%d
    "
    , i); // 3

        i = 1;
        //i = (i++);
        r = i;
        i = r;
        i++;
        printf("%d
    "
    , i); // 2

        volatile int u = 0;
        //u = u++ + ++u;
        ++u;
        r = u + u;
        u = r;
        u++;
        printf("%d
    "
    , u); // 3

        u = 1;
        //u = (u++);
        r = u;
        u = r;
        u++;
        printf("%d
    "
    , u); // 2

        register int v = 0;
        //v = v++ + ++v;
        ++v;
        r = v + v;
        v = r;
        v++;
        printf("%d
    "
    , v); // 3
    }

    由于Visual Studio文档规定了评估的优先级和顺序:

    Where several operators appear together, they have equal precedence and are evaluated according to their associativity. The operators in the table are described in the sections beginning with Postfix Operators.