这些使用前置和后置递增的结构为什么会产生未定义行为?

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

16
@Jarett,不需要,我只是需要一些关于“序列点”的指导。在工作中,我发现了一段代码 i=i++,我认为“这并没有修改i的值”。我进行了测试,然后开始疑惑。自那以后,我删除了这个语句,并将其替换为 i++。 - PiX
231
我觉得有趣的是每个人都总是假定像这样的问题被问是因为提问者想要在所讨论的结构中使用它。我的第一个假设是,PiX 知道这些是不好的,但是他/她很好奇为什么在他/她使用的编译器上它们会以这种方式运作... 而且就像 unWind 说的那样... 这是未定义的,它可以做任何事情... 包括 JCF(跳转并着火)。 - Brian Postow
40
为什么编译器不会在类似“u = u++ + ++u;”这样的语句中发出警告,即使结果未定义? - Learn OpenGL ES
5
无论加不加括号,(i++) 的值仍为1。 - Drew McGowen
3
无论 i = (i++); 的目的是什么,肯定有更清晰的写法。即使它被明确定义了,这也是正确的。即使在 Java 中定义了 i = (i++); 的行为,这仍然是糟糕的代码。只需编写 i++; 即可。 - Keith Thompson
显示剩余13条评论
15个回答

631

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

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

那么,考虑到这一点,为什么会有这些“问题”呢?语言明确表示某些事情会导致未定义的行为。没有问题,也没有“应该”的参与。如果当其中一个相关变量被声明为volatile时,未定义的行为发生变化,那并不能证明或改变任何东西。它是未定义的;你无法推理出行为。

你最有趣的例子是那个

u = (u++);

这是一个未定义行为的经典案例(请参阅维基百科关于序列点的条目)。


10
@PiX:事物未被定义可能由多种原因引起。这些原因包括:没有明确的“正确结果”,不同的机器架构会强烈偏向于不同的结果,现有的惯例并不一致,或超出了标准的范围(例如哪些文件名是有效的)。 - Richard
6
为了让大家困惑,C11中有些类似的例子现在已经定义得很好了,比如 i = ++i + 1; - M.M
4
阅读标准和发布的解释,就可以明白为什么UB的概念存在。标准从未打算完全描述一个C实现必须做到适用于任何特定目的的所有内容(参见“一个程序规则”的讨论),而是依赖于实现者的判断和愿望来产生有用的高质量实现。适用于低级系统编程的高质量实现将需要定义在高端数值计算应用中不需要的操作的行为。与其试图复杂化标准… - supercat
6
标准的作者通过详细说明哪些特殊情况被定义或未被定义,认识到实现者应该更好地判断哪种行为将被需要来支持所期望的程序类型。超现代编译器假装将某些操作视为未定义是为了暗示没有任何高质量的程序需要这些操作,但标准和理念与这种假设意图不一致。 - supercat
4
@jrh:在我意识到超现代主义哲学已经失控之前,我就写下了那个答案。让我感到不快的是,从“我们不需要正式承认这种行为,因为需要它的平台可以支持它”到“我们可以删除这种行为,而不提供可用的替代方案,因为它从未被承认,因此任何需要它的代码都已经失效”。许多行为早应该被废弃,并换成所有方面更好的替代方案,但这需要承认它们的合法性。 - supercat
显示剩余18条评论

99

大部分回答都引用了C标准并强调这些结构的行为是未定义的。为了理解为什么这些结构的行为是未定义的,让我们首先在C11标准的指导下理解这些术语:

顺序化:(5.1.2.3)

对于任何两个评估 AB,如果 AB 之前被顺序化,则 A 的执行必须在 B 之前。

非顺序化:

如果 A 既不在 B 之前也不在其之后,则 AB 非顺序化。

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

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

序列点:

在表达式 AB 的评估之间存在序列点,意味着与 A 相关的每个值计算副作用都在与 B 相关的每个值计算副作用之前。

现在回到这个问题,对于像

int i = 1;
i = i++;

标准规定:

6.5 表达式:

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

因此,由于对同一对象 i 有两个副作用是无序的,所以上述表达式引发了未定义行为。这意味着赋值对 i 的副作用完成的顺序在 ++ 的副作用之前或之后是不确定的。
根据赋值操作在增量操作之前还是之后发生的顺序,将产生不同的结果,这是未定义行为之一

让我们将赋值操作左边的 i 重命名为 il,将赋值操作右边的 i(在表达式 i++ 中)重命名为 ir,则表达式如下:

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

有关后缀 ++ 运算符的一个重要点是:

仅仅因为变量后面有 ++ 并不意味着增量会晚执行。 增量可以在编译器任何时候发生,只要编译器确保使用原始值

这意味着表达式 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,这取决于赋值和++的副作用顺序,因此会引发未定义行为。

82

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

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

和6.5.16赋值运算符, §4:

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


2
上述是否意味着 'i=i=5;' 将是未定义行为? - supercat
2
据我所知,i=i=5 也是未定义行为。@supercat - dhein
3
@Zaibis: 我通常使用"大多数地方规则适用性原理"。假设在多处理器平台上,可以将A=B=5;实现为“写入锁A;写入锁B;将5存储到A;将5存储到B;解锁B;解锁A;”,而像C=A+B;这样的语句可以实现为“读锁A;读锁B;计算A+B;解锁A和B;写入锁C;存储结果;解锁C;”。这将确保如果一个线程执行了A=B=5;,而另一个线程执行了C=A+B;,后者线程将看到两个写入都已经发生或都没有发生。这可能是一个有用的保证。然而,如果一个线程执行了I=I=5;... - supercat
2
如果编译器没有注意到两个写操作是针对同一位置的(如果其中一个或两个lvalue涉及指针,则可能很难确定),则生成的代码可能会死锁。我不认为任何真实世界的实现都将这种锁定作为其正常行为的一部分实现,但在标准下是允许的,如果硬件可以便宜地实现这些行为,那么它可能是有用的。在今天的硬件上,这种行为作为默认行为实现起来成本太高,但这并不意味着它将永远如此。 - supercat
1
@supercat但是,仅使用C99的序列点访问规则是否足以将其声明为未定义行为?因此,无论技术上硬件可以实现什么都没有关系。 - dhein
显示剩余4条评论

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


5
@ronnieaka gcc evil.c -c -o evil.bingdb evil.bin反汇编 evil,或者 Windows 上的等效命令 :) - badp
最后,-0x4(%ebp)的值是否为4? - kchoi
29
这个回答并没有真正回答“为什么这些结构是未定义行为”的问题。 - Shafik Yaghmour
12
顺便提一下,使用 gcc -S evil.c 编译成汇编代码会更容易,这就是所需的全部。组装和反汇编只是一个绕远路的方法。 - Kat
61
记录一下,如果您想知道某个特定的结构是做什么用的——尤其是如果有任何怀疑它可能是未定义行为的情况——那么“只需使用您的编译器尝试一下并查看结果”这个古老的建议可能会带来相当大的危险。最多,您将了解到在此版本的编译器下,在这些情况下,今天它会做些什么。您将不能获得有关它保证要做什么的任何信息。总的来说,“只需使用您的编译器尝试一下”会导致非可移植性程序,这些程序仅适用于您的编译器。 - Steve Summit
显示剩余5条评论

64

这种行为实际上无法解释,因为它同时引用了未指明的行为未定义的行为,所以我们无法对此代码做出任何一般性的预测,尽管如果你阅读Olve Maudal的作品,比如Deep C未指明和未定义,有时候你可以在特定编译器和环境下做出很好的猜测,但请不要在生产环境中这样做。

接下来谈论未指明的行为,在c99标准草案的第6.5节第3段中说(我强调):

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

因此,当我们有这样一行代码:

i = i++ + ++i;

我们不知道i++++i哪个会先被评估。这主要是为了让编译器有更好的优化选项

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

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

它引用以下代码示例作为未定义:

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节中定义为:

在使用非可移植或错误的程序构造或错误数据时的行为,此国际标准不对其施加任何要求。

并指出:

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


50
另一个回答问题的方法,而不是陷入序列点和未定义行为的晦涩细节中,就是简单地问:“它们应该意味着什么?”“程序员在试图做什么?”
首先被提到的 i=i++ + ++i 这一片段,在我看来显然是疯狂的。没有人会在真正的程序中编写它,它的作用不明确,也没有人能够想到某种算法,使其成为这个特定的人为操作序列。既然我们无法确定它的作用,那么如果编译器也无法理解它的作用,那么这是可以接受的。
第二个片段 i=i++ 稍微容易理解一些。看起来像是有人想要递增 i ,并将结果赋回给 i 。但是,C语言可以有几种方法实现这个功能。在几乎任何编程语言中,最基本的方法是取 i 的值,加1,然后将结果赋回给 i
i = i + 1

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

i++

这也意味着,“取出i的值,加1,并将结果赋回给i”。因此,如果我们将两者混合起来写成:
i = i++

我们真正想说的是“取出i的值,加1,把结果赋值回i,再把结果赋值回i”。我们很困惑,所以如果编译器也感到困惑,那么这也不会让我太烦恼。

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

我们曾经花费无数的时间在comp.lang.c上讨论像这样的表达式以及它们为什么未定义。我写了两篇较长的答案,试图真正解释其中的原因,并已存档在网上:

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


1
关于未定义行为(Undefined Behavior),一个相当令人讨厌的问题是,尽管在99.9%的编译器中使用*p=(*q) ++;表示if (p!= q)*p=(*q)++; else *p= __ARBITRARY_VALUE;曾经是安全的,但现在不再是这样了。超现代的C需要编写类似于后者的公式(虽然没有一种标准的方式来表示代码不关心*p中的内容),以实现编译器以前提供的前者的效率水平(需要else子句才能允许编译器优化掉可能需要的if)。 - supercat
@supercat我现在相信,任何可以执行那种优化的“智能”编译器都必须足够聪明,以便窥视assert语句,使得程序员可以在问题行之前加上简单的assert(p != q)。(当然,采取这种方法也需要重写<assert.h>以便在非调试版本中不要完全删除断言,而是将它们转换成类似于__builtin_assert_disabled()的东西,让编译器可以看到,并且不生成代码。) - Steve Summit
2
@RobertSsupportsMonicaCellio 所写的方式确实有点令人困惑。将其理解为“将从i获取的值加1,将结果分配回i,并将结果再次分配回`i”即可。 - Steve Summit
使用 int j = i++,我期望 j 能够得到 i 的前缀自增值。如果 i = i++ 不是未定义行为,我期望 i 同样能够以其前缀自增值结束,而不是“明显尝试递增 i,并将结果分配回 i”。 - chux - Reinstate Monica
1
@chux 是的,但是你懂C语言,并且你对自增运算符的正确定义感到舒适。那些对这些运算符感到困惑的人不是这样的!特别是,如果你想象一下他们想象++ii++基本上都是i + 1的快捷方式,那么理解一些初学者的误解会变得更容易。尽管如此,我已经缓和了你评论的那句话。 - Steve Summit
显示剩余3条评论

32
往往这个问题被链接为与代码相关的重复问题,比如:
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 standard对此有一些相关描述:

附录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++); 中,++ii 增加到 6,而 i++ 产生 i 的旧值 (6),该值被赋给 j。然后,由于后增量,i 变为 7

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

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

这不会成为问题。但是,因为这里的逗号是一个分隔符,所以它会引发未定义行为


对于那些不熟悉“未定义行为”的人来说,阅读《每个C程序员应该知道的关于未定义行为的事情》可以帮助理解C语言中的概念和许多其他变体的未定义行为。
此帖子:未定义、未指定和实现定义行为也相关。

这个程序片段 int a = 10, b = 20, c = 30; printf("a=%d b=%d c=%d\n", (a = a + b + c), (b = b + b), (c = c + c)); 看起来表现稳定(在gcc v7.3.0中从右到左进行参数评估,结果为 "a=110 b=40 c=60")。这是因为赋值被视为 'full-statements',从而引入了一个序列点吗?这不应该导致从左到右的参数/语句评估吗?还是说这只是未定义行为的体现? - kavadias
1
@kavadias 那个printf语句涉及到未定义的行为,原因如上所述。您正在将bc分别写入第3和第4个参数中,并在第2个参数中读取。但是这些表达式之间没有顺序(第2、3和4个参数)。gcc / clang有一个选项-Wsequence-point,可以帮助找到这些问题。 - P.P

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将增加两个)。而且,未来某些处理器可能会提供类似于这样的功能。

如果编译器按照上述方式(在标准下是合法的)编写i++,并且在整个表达式的求值过程中插入上述指令(也是合法的),如果它没有注意到其他指令恰好访问了i,那么编译器生成的指令序列可能会造成死锁(这也是合法的)。当然,编译器几乎肯定会在同一个变量i在两个位置使用的情况下检测到问题,但如果一个程序接受两个指针pq的引用,并在上述表达式中使用(*p)(*q)(而不是两次使用i),则编译器不需要识别或避免发生死锁,如果相同对象的地址被传递给pq


18
虽然像 a = a++a++ + a++ 这样的表达式的语法是合法的,但这些结构的行为是未定义的,因为 C 标准中的一个规定没有被遵守。C99 6.5p2

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

脚注 73 进一步澄清了此规定。

  1. This paragraph renders undefined statement expressions such as

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

    while allowing

    i = i + 1;
    a[i] = i;
    
各种序列点列在C11(和C99)的附录C中:
以下是5.1.2.3中描述的序列点: 1. 在函数调用中,函数设计者和实际参数的计算之间以及实际调用之间。(6.5.2.2) 2. 在逻辑AND && (6.5.13)、逻辑OR || (6.5.14)和逗号, (6.5.17)的第一和第二操作数的计算之间。 3. 在条件? :运算符的第一个操作数的计算和第二个和第三个操作数中的任何一个被计算之间。(6.5.15) 4. 完整声明符的结尾:声明符(6.7.6)。 5. 在完整表达式和下一个要计算的完整表达式之间。以下是完整表达式:不是复合文字的初始化程序(6.7.9);表达式语句中的表达式(6.8.3);选择语句(if或switch)的控制表达式(6.8.4);while或do语句的控制表达式(6.8.5);for语句的每个(可选的)表达式(6.8.5.3);返回语句中的(可选的)表达式(6.8.6.4)。 6. 在库函数返回之前立即执行(7.1.4)。 7. 在与每个格式化输入/输出函数转换说明符相关的操作之后立即执行(7.21.6, 7.29.2)。 8. 在每次调用比较函数之前和之后立即执行,并且在调用比较函数和传递给该调用的对象之间的任何移动之间也要执行(7.22.5)。

相同的C11段落的措辞如下:

  1. 如果标量对象上的副作用与同一标量对象上的另一个副作用或使用同一标量对象的值计算相对无序,则行为未定义。 如果表达式的子表达式具有多个允许的排序方式,并且在任何排序中出现了这样的无序副作用,则行为未定义。84)

你可以通过使用最新版本的GCC并使用-Wall-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递增1,并将结果作为表达式的值 - 这实际上只是一种编写j = (i += 2)的做法,而这又是一种“聪明”的写法。
i += 2;
j = i;

然而,在函数参数列表中的逗号,不是逗号运算符,且在不同参数的评估之间没有序列点;相反,它们的评估与彼此无序;因此函数调用。
int i = 0;
printf("%d %d\n", i++, ++i, i);

由于函数参数中i++++i之间没有序列点,因此i的值被两次修改(分别由i++++i),在前一个序列点和下一个序列点之间存在未定义行为。请注意保留HTML标签。

15

C标准规定变量在两个序列点之间最多只能被赋值一次,例如分号就是一个序列点。
因此,每个形式如下的语句:

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

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

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

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

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


当然,它不适用于一个表达式中的不同变量。如果是这样,那就是完全的设计失误了!在第二个例子中,你只需要在语句结束和下一个语句开始之间对两者进行递增,这是有保证的,正是因为序列点的概念位于所有这些内容的中心位置。 - underscore_d

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接