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

14 回答 14

603

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

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

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

你最有趣的例子,那个

u = (u++);

是未定义行为的教科书示例(请参阅维基百科关于序列点的条目)。

于 2009-06-04T09:20:59.763 回答
83

这里引用的大多数答案都来自 C 标准,强调这些构造的行为是未定义的。为了理解为什么这些构造的行为是 undefined,让我们首先根据 C11 标准来理解这些术语:

排序:(5.1.2.3)

给定任意两个求值A,并且B如果A在之前排序B,则 的执行A应先于 的执行B

未排序:

如果A在 之前或之后没有排序B,那么A和是未排序B的。

评估可以是以下两种情况之一:

  • 值计算,计算出表达式的结果;和
  • 副作用,即对象的修改。

序列点:

在表达式的求值之间存在一个序列点AB意味着与关联的每个值计算和副作用都在与 关联的每个计算副作用之前进行A排序。B

现在来到这个问题,对于像这样的表达

int i = 1;
i = i++;

标准说:

6.5 表达式:

如果标量对象的副作用相对于同一标量对象的不同副作用或使用同一标量对象的值的值计算是未排序的,则行为未定义。[...]

因此,上面的表达式调用了 UB,因为对同一对象的两个副作用i相对于彼此是无序的。这意味着分配 to 的副作用是在副作用 byi之前还是之后完成并没有顺序++
根据赋值发生在增量之前还是之后,会产生不同的结果,这就是未定义行为的情况之一。

让我们重命名i赋值左侧的为 beil和赋值右侧(在表达式中i++)为 be ir,然后表达式为

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

关于 Postfix运算符的重要一点++是:

仅仅因为++变量出现在变量之后并不意味着增量发生得很晚只要编译器确保使用原始值,增量就可以在编译器喜欢的时候发生。

这意味着表达式il = ir++可以被评估为

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

或者

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

导致两个不同的结果12这取决于赋值的副作用顺序,++因此调用 UB。

于 2015-06-27T00:27:48.540 回答
79

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

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

和 6.5.16 赋值运算符,§4:

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

于 2009-06-04T09:35:47.813 回答
76

只需编译和反汇编您的代码行,如果您如此倾向于知道它是如何准确地得到您所得到的。

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

$ 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 指令是某种编译器优化?)

于 2010-05-24T13:26:05.523 回答
61

该行为无法真正解释,因为它同时调用了未指定的行为未定义的行为,因此我们无法对此代码做出任何一般性预测,尽管如果您阅读Olve Maudal 的作品,例如Deep CUnspecified 和 Undefined有时您可以做得很好使用特定编译器和环境在非常特定的情况下进行猜测,但请不要在生产附近的任何地方这样做。

因此,继续讨论未指定的行为,在草案 c99 标准部分6.53段中说(强调我的):

运算符和操作数的分组由语法指示。74) 除稍后指定(对于函数调用 ()、&&、||、?: 和逗号运算符)外,子表达式的求值顺序和中的顺序发生哪些副作用都未指定。

所以当我们有这样的一行时:

i = i++ + ++i;

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

我们在这里也有未定义的行为,因为程序在序列点之间不止一次地修改变量(,,等i)。从标准草案第2段(强调我的):u6.5

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

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

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

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

i = i++ + ++i;
^   ^       ^

i = (i++);
^    ^

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

u = (u++);
^    ^

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

未指定的行为在c99 标准草案中定义3.4.4为:

使用未指定的值,或本国际标准提供两种或多种可能性的其他行为,并且在任何情况下都没有强加进一步的要求

未定义的行为在部分定义3.4.3为:

使用不可移植或错误程序结构或错误数据时的行为,本国际标准对此没有要求

并指出:

可能的未定义行为范围从完全忽略具有不可预测结果的情况,到在翻译或程序执行期间以环境特征的记录方式表现(有或没有发出诊断消息),到终止翻译或执行(有发出的诊断消息)。

于 2013-08-15T19:25:21.450 回答
44

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

在我的书中,第一个关于 , 的片段i = i++ + ++i显然很疯狂。没有人会在真正的程序中编写它,它的作用并不明显,没有任何可以想象的算法有人试图编写会导致这种特殊的人为操作序列的编码。而且由于对你和我来说它应该做什么并不明显,如果编译器也无法弄清楚它应该做什么,那在我的书中也没关系。

第二个片段,i = i++,更容易理解。显然有人试图增加 i,并将结果分配回 i。但是在 C 中有几种方法可以做到这一点。将 1 加到 i 并将结果分配回 i 的最基本方法几乎​​在任何编程语言中都是相同的:

i = i + 1

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

i++

这意味着,“将 1 加到 i,并将结果分配回 i”。因此,如果我们构建两者的大杂烩,通过编写

i = i++

我们真正要说的是“将 1 加到 i,并将结果分配回 i,并将结果分配回 i”。我们很困惑,所以如果编译器也感到困惑,我也不会太困扰。

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

我们曾经在 comp.lang.c 上花费无数时间讨论此类表达式以及它们为何未定义。我试图真正解释原因的两个较长的答案已在网络上存档:

另请参阅问题 3.8和C 常见问题列表第 3 节中的其余问题。

于 2015-06-18T11:55:45.607 回答
31

通常这个问题被链接为与代码相关的问题的副本,例如

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

或者

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++未指定。但这是完全合法有效的声明。此语句中没有未定义的行为。因为修改 (++xy++) 是针对不同的对象进行的。

什么使以下陈述

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的旧值。然后变成由于后增量。6i++i6ji7

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

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

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


对于那些不熟悉未定义行为的人来说,阅读每个 C 程序员都应该了解的关于未定义行为 的知识,以了解 C 中未定义行为的概念和许多其他变体,将会从中受益。

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

于 2015-12-30T20:26:30.360 回答
23

尽管任何编译器和处理器实际上都不太可能这样做,但根据 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),并且某些未来的处理器可能会提供类似的功能并不是完全不可想象的。

如果编译器按照上述说明进行编写i++(根据标准是合法的),并且在整个表达式的评估过程中散布上述指令(也是合法的),并且如果它没有碰巧注意到发生了其他指令之一要访问i,编译器有可能(并且合法)生成会死锁的指令序列。可以肯定的是,编译器几乎肯定会i在两个地方都使用相同变量的情况下检测到问题,但是如果例程接受对两个指针p和的引用,并在上面的表达式中q使用(*p)和(而不是使用(*q)i两次)编译器不需要识别或避免如果为p和传递相同对象的地址时会发生的死锁q

于 2012-12-05T18:30:27.523 回答
18

虽然像or这样的表达式的语法是合法的,但这些构造的行为未定义的,因为不遵守 C 标准中的shallC99 6.5p2a = a++a++ + a++

  1. 在前一个和下一个序列点之间,对象的存储值最多只能通过表达式的评估修改一次。[72] 此外,应仅读取先验值以确定要存储的值 [73]

脚注73进一步阐明

  1. 本段呈现未定义的语句表达式,例如

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

    同时允许

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

各种序列点列在C11(和C99)的附件 C 中:

  1. 以下是5.1.2.3中描述的顺序点:

    • 在函数调用和实际调用中的函数指示符和实际参数的评估之间。(6.5.2.2)。
    • 在以下运算符的第一个和第二个操作数的计算之间:逻辑与 && (6.5.13);逻辑或 || (6.5.14);逗号 , (6.5.17)。
    • 在条件的第一个操作数的评估之间?: 运算符以及计算第二个和第三个操作数中的任何一个(6.5.15)。
    • 完整声明符的结尾:声明符(6.7.6);
    • 在完整表达式的评估和要评估的下一个完整表达式之间。以下是完整的表达式: 不属于复合文字(6.7.9)的初始化器;表达式语句中的表达式 (6.8.3);选择语句(if 或 switch)的控制表达式(6.8.4);while 或 do 语句的控制表达式 (6.8.5);for 语句 (6.8.5.3) 的每个(可选)表达式;return 语句 (6.8.6.4) 中的(可选)表达式。
    • 紧接在库函数返回之前 (7.1.4)。
    • 在与每个格式化输入/输出函数转换说明符(7.21.6、7.29.2)关联的操作之后。
    • 在每次调用比较函数之前和之后,以及在对比较函数的任何调用与作为参数传递给该调用的对象的任何移动之间(7.22.5)。

C11中同一段的措辞是:

  1. 如果标量对象的副作用相对于同一标量对象的不同副作用或使用同一标量对象的值的值计算是未排序的,则行为未定义。如果一个表达式的子表达式有多个允许的排序,则如果在任何排序中出现这种未排序的副作用,则行为是未定义的。84)

您可以通过例如使用带有-Walland的最新版本的 GCC 来检测程序中的此类错误-Werror,然后 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一,产生旧值,丢弃该值;然后在逗号运算符处,解决副作用;然后加i一,结果值变成表达式的值——也就是说,这只是一种人为的写法j = (i += 2),又是一种“聪明”的写法

i += 2;
j = i;

但是,,in 函数参数列表不是逗号运算符,并且不同参数的计算之间没有序列点;取而代之的是,它们的评估彼此之间是无序的;所以函数调用

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

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

于 2017-03-26T14:58:07.470 回答
15

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

i = i++;
i = i++ + ++i;

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

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

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

以上是复制/分析字符串时的常见编码实践。

于 2014-09-11T12:36:41.063 回答
11

https://stackoverflow.com/questions/29505280/incrementing-array-index-in-c中,有人询问了如下声明:

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计算之前全部完成。事实上,不同的编译器在这里会得到不同的结果。在您提供的示例中,++i执行前 2 个,然后读取 的值k[],然后读取最后一个++ithen k[]

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

现代编译器将对此进行很好的优化。事实上,可能比您最初编写的代码更好(假设它按照您希望的方式工作)。

于 2015-04-08T03:20:31.417 回答
10

您的问题可能不是“为什么这些构造在 C 中是未定义的行为?”。您的问题可能是,“为什么这段代码(使用++)没有给我预期的价值?”,有人将您的问题标记为重复,并将您发送到这里。

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

我假设您现在已经听说过 C++--运算符的基本定义,以及前缀形式++x与后缀形式有何不同x++。但是这些运算符很难考虑,所以为了确保你理解,也许你写了一个很小的测试程序,涉及到类似的东西

int x = 5;
printf("%d %d %d\n", x, ++x, x++);

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

或者,也许您正在查看一个难以理解的表达式,例如

int x = 5;
x = x++ + ++x;
printf("%d\n", x);

也许有人给了你那个代码作为一个谜题。这段代码也毫无意义,特别是如果你运行它——如果你在两个不同的编译器下编译和运行它,你可能会得到两个不同的答案!那是怎么回事?哪个答案是正确的?(答案是两者都是,或者两者都不是。)

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

是什么让表达式未定义?表达式是否涉及++并且--总是未定义?当然不是:这些都是有用的操作符,如果你正确使用它们,它们的定义是完美的。

对于我们正在讨论的表达式,使它们不确定的原因是当一次发生太多事情时,当我们无法确定事情将按照什么顺序发生时,但是当顺序对我们将得到的结果很重要时。

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

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

问题是,在实际调用之前printf,编译器是否计算了xfirst、orx++或 maybe的值++x?但事实证明我们不知道。C 中没有规定函数的参数从左到右、从右到左或以其他顺序计算。所以我们不能说编译器是否会x先执行,然后执行++x,然后执行x++,或者执行x++然后执行,或者其他顺序。但是顺序显然很重要,因为根据编译器使用的顺序,我们会清楚地打印出一系列不同的数字。++xx

这个疯狂的表情是怎么回事?

x = x++ + ++x;

这个表达式的问题在于它包含三种不同的尝试来修改 的值x: (1)x++部分尝试取x的值,加 1,将新值存储在 中x,并返回旧值;(2)++x部分尝试取x' 的值,加 1,将新值存入x,并返回新值;(3) 该x =部分试图将其他两个的总和分配回x。这三个尝试的任务中的哪一个会“获胜”?这三个值中的哪一个将实际决定 的最终值x?同样,也许令人惊讶的是,C 中没有规则可以告诉我们。

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


因此,有了所有这些背景和介绍,如果你想确保你的所有程序都是明确定义的,你可以写哪些表达式,哪些不能写?

这些表达都很好:

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++;

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

x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);

最后一个问题是,你怎么知道哪些表达式是明确定义的,哪些表达式是未定义的?

正如我之前所说,未定义的表达式是那些同时发生的事情太多,你不能确定事情发生的顺序,以及顺序很重要的地方:

  1. 如果有一个变量在两个或多个不同的地方被修改(分配给),你怎么知道哪个修改首先发生?
  2. 如果有一个变量在一个地方被修改,并且在另一个地方使用它的值,你怎么知道它是使用旧值还是新值?

作为 #1 的示例,在表达式中

x = x++ + ++x;

有3次尝试修改x

作为 #2 的示例,在表达式中

y = x + x++;

我们都使用 的值x,并对其进行修改。

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


还有一件事。您可能想知道如何“修复”我通过呈现这个答案开始的未定义表达式。

在 的情况下printf("%d %d %d\n", x, ++x, x++);,这很容易——只需将其编写为三个单独的printf调用:

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

现在该行为已被完美定义,您将获得合理的结果。

x = x++ + ++x另一方面,在 的情况下,没有办法修复它。没有办法编写它以保证其行为符合您的期望——但这没关系,因为x = x++ + ++x无论如何您永远不会像在真实程序中那样编写表达式。

于 2018-08-16T11:54:35.787 回答
6

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

我解释这些想法。

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

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

表达式中的序列点i=i++是 beforei=和 after i++

在我上面引用的论文中,解释说您可以将程序视为由小盒子组成,每个盒子包含两个连续序列点之间的指令。序列点在标准附件C中定义,如果i=i++有2个序列点界定一个完整的表达式。这样的表达式在语法上与expression-statementBackus-Naur 形式的语法中的条目等价(语法在标准的附件 A 中提供)。

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

i=i++

可以解释为

tmp = i
i=i+1
i = tmp

或作为

tmp = i
i = tmp
i=i+1

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

因此,可以通过组成程序的每个框的开头和结尾看到一个序列点[这些框是 C 中的原子单元],并且在一个框内,指令的顺序并不是在所有情况下都定义的。改变这个顺序有时会改变结果。

编辑:

解释这种歧义的其他好来源是来自c-faq网站的条目(也作为一本书出版),即hereherehere

于 2017-10-13T13:58:04.977 回答
2

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

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

使用Nuwen MinGW 15 GCC 7.1,您将获得:

 #include<stdio.h>
 int main(int argc, char ** argv)
 {
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 2

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

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

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

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

GCC 是如何工作的?它以从左到右的顺序计算右侧 (RHS) 的子表达式,然后将值分配给左侧 (LHS)。这正是 Java 和 C# 的行为方式和定义其标准的方式。(是的,Java 和 C# 中的等效软件已定义行为)。它以从左到右的顺序对 RHS 语句中的每个子表达式逐一求值;对于每个子表达式:首先计算 ++c(预增量),然后将值 c 用于操作,然后是后增量 c++)。

根据GCC C++:运算符

在 GCC C++ 中,运算符的优先级控制各个运算符的求值顺序

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

#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\n", i); // 2

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

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

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

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

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

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 3

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

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

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

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

Visual Studio 是如何工作的,它采用另一种方法,它在第一遍评估所有预增量表达式,然后在第二遍操作中使用变量值,在第三遍从 RHS 分配给 LHS,然后在最后一遍评估所有一次通过后增量表达式。

因此,Visual C++ 所理解的定义行为 C++ 中的等价物:

#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\n", i); // 3

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

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

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

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

正如 Visual Studio 文档在Precedence and Order of Evaluation 中所述

当多个运算符一起出现时,它们具有相同的优先级并根据它们的关联性进行评估。表中的运算符在以后缀运算符开头的部分中进行了描述。

于 2017-06-10T22:56:26.007 回答