为什么这些构造使用前后增量未定义的行为?

时间:2009-06-04 09:17:52

标签: c increment undefined-behavior operator-precedence sequence-points

#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 %d\n", w++, ++w, w); // shouldn't this print 0 2 2

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

14 个答案:

答案 0 :(得分:543)

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

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

那么,考虑到这一点,为什么这些“问题”?该语言明确指出某些事情会导致undefined behavior。没有问题,没有“应该”参与。如果未声明的行为在声明其中一个涉及的变量volatile时发生更改,则不会证明或更改任何内容。它是 undefined ;你无法推理这种行为。

你最有趣的例子,

u = (u++);

是未定义行为的教科书示例(请参阅维基百科在sequence points上的条目)。

答案 1 :(得分: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指令是某种编译器优化?)

答案 2 :(得分:58)

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

  

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

和6.5.16赋值运算符,§4:

  

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

答案 3 :(得分:48)

行为无法真正解释,因为它会同时调用unspecified behaviorundefined behavior,因此我们无法对此代码进行任何一般性预测,但如果您阅读 Olve Maudal的 Deep CUnspecified and Undefined这样的工作有时您可以在特定的情况下使用特定的编译器和环境做出正确的猜测,但请不要在生产附近的任何地方做到这一点。

继续未指明的行为,在draft c99 standard部分6.5 3 中说(强调我的) :

  

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

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

i = i++ + ++i;

我们不知道是先评估i++还是++i。这主要是为了给编译器better options for optimization

我们这里也有未定义的行为,因为程序在sequence points之间不止一次地修改变量(iu等...) 。草稿标准部分6.5 2 强调我的):

  

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

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

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

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

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

i = (i++);
^    ^

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

u = (u++);
^    ^

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

未指定的行为3.4.4部分3.4.3中定义为:

  

使用未指定的值或本国际标准提供的其他行为   两种或更多种可能性,并且没有任何进一步的要求   实例

未定义的行为在{{1}}部分中定义为:

  

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

并注意到:

  

可能的未定义行为包括完全忽略具有不可预测结果的情况,在转换或程序执行期间以环境特征(有或没有发出诊断消息)的文档方式执行,终止翻译或执行(发布诊断信息)。

答案 4 :(得分:48)

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

已排序:(5.1.2.3)

  

鉴于任何两个评估AB,如果AB之前排序,那么A的执行应在执行B之前执行1}}。

<强>未测序

  

如果在A之前或之后未对B进行排序,则AB将无法排序。

评估可以是两件事之一:

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

序列点:

  

表达式AB的评估之间存在一个序列点意味着每个值计算副作用与{相关联{1}}在与A关联的每个值计算副作用之前排序。

现在提出问题,对于像

这样的表达式
B

标准说:

6.5表达式:

  

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

因此,上面的表达式调用UB,因为对同一对象int i = 1; i = i++; 的两个副作用相对于彼此没有排序。这意味着,i在副作用之前或之后是否会通过分配到i的副作用进行排序。 根据分配是在增量之前还是之后发生,将产生不同的结果,这是未定义行为的情况之一。

允许将作业左侧的++重命名为i,并在作业的右侧(在il表达式中)重命名为i++,然后表达式为< / p>

ir
关于Postfix il = ir++ // Note that suffix l and r are used for the sake of clarity. // Both il and ir represents the same object. 运算符的

An important point是:

  

只是因为变量之后的++并不意味着增量发生在后期。只要编译器确保使用原始值,就可以在编译器喜欢之前发生增量。

这意味着表达式++可以评估为

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 1,这取决于通过赋值和2的副作用的顺序,因此调用UB。

答案 5 :(得分:31)

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

关于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上讨论像这样的表达式和为什么它们是未定义的。我试着真正解释原因的两个较长的答案都存档在网上:

答案 6 :(得分:22)

虽然任何编译器和处理器都不太可能实际这样做,但在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的情况下,编译器几乎肯定会检测到问题,但是如果例程接受对两个指针pq的引用,并在上面的表达式中使用(*p)(*q)(而不是两次使用i),编译器不需要识别或避免在同一对象的地址出现时发生的死锁已为pq传递。

答案 7 :(得分:22)

这个问题经常被链接为与

等代码相关的问题的副本
printf("%d %d\n", i, i++);

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

或类似的变体。

虽然这也是undefined behaviour已经说明,但在与{1}}进行比较时,如果与以下语句进行比较,则存在细微差别:

printf()

在以下声明中:

   x = i++ + i++;

printf("%d %d\n", ++i, i++); 中的order of evaluation个参数为unspecified。这意味着,可以按任何顺序评估表达式printf()i++C11 standard对此有一些相关的描述:

附件J,未指明的行为

  

函数指示符,参数和的顺序   参数中的子表达式在函数调用中计算   (6.5.2.2)。

3.4.4,未指明的行为

  

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

     

示例未指定行为的示例是其中的顺序   评估函数的参数。

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

++i

这也有未指定的行为,因为printf("%d %d\n", ++x, y++); ++x的评估顺序未指定。但这是完全合法和有效的陈述。此语句中有 no 未定义的行为。因为修改(y++++x)是针对不同的对象完成的。

什么呈现以下声明

y++

作为未定义的行为是这两个表达式修改相同的对象printf("%d %d\n", ++i, i++); 而没有干预sequence point的事实。

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

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

i

逗号运算符从左到右计算其操作数,并仅生成最后一个操作数的值。因此,在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++);增加++ii6会产生分配给i++i)的旧值到6。由于后期增加,j变为i

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

7

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

对于那些不熟悉未定义行为的人将从阅读What Every C Programmer Should Know About Undefined Behavior中受益,以了解C中未定义行为的概念和许多其他变体。

这篇文章:Undefined, unspecified and implementation-defined behavior也很重要。

答案 8 :(得分:14)

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

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

等违反了这条规则。该标准还表示行为未定义且未指定。有些编译器会检测到这些并产生一些结果,但这不符合标准。

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

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

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

答案 9 :(得分:13)

虽然a = a++a++ + a++等表达式的语法合法,但这些结构的行为未定义因为不遵守C标准中的 C99 6.5p2

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

footnote 73进一步澄清

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

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

    允许

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

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

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

         
        
    • 在函数调用和函数调用中的实际参数的评估与实际调用之间。 (6.5.2.2)。
    •   
    • 在以下运算符的第一个和第二个操作数的评估之间:逻辑AND&amp;&amp; (6.5.13);逻辑OR || (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);返回语句中的(可选)表达式(6.8.6.4)。
    •   
    • 在库函数返回之前(7.1.4)。
    •   
    • 与每个格式化输入/输出函数转换说明符(7.21.6,7.29.2)关联的操作之后。
    •   
    • 在每次调用比较函数之前和之后,以及对比较函数的任何调用和作为参数传递给该调用的对象的任何移动之间(7.22.5)。
    •   
  2.   

同一paragraph in C11的措辞是:

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

您可以通过例如使用最新版本的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

重要的是要知道what a sequence point is -- and what is a sequence point and what isn't。例如,逗号运算符是一个序列点,所以

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

是明确定义的,并且会将i递增1,产生旧值,丢弃该值;然后在逗号操作员,解决副作用;然后将i增加1,结果值就变成了表达式的值 - 也就是说,这只是一种人为的j = (i += 2)的写法,这又是一个聪明的&#34;写作的方式

i += 2;
j = i;

但是,函数参数列表中的,而不是一个逗号运算符,并且在不同参数的计算之间没有序列点;相反,他们的评价对彼此没有考虑;所以函数调用

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

未定义的行为,因为在函数参数中的i++++i的评估与{的值之间没有序列点因此{1}}在前一个和下一个序列点之间由ii++修改两次。

答案 10 :(得分:9)

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增量不能保证全部完成。实际上,不同的编译器在这里会得到不同的结果。在您提供的示例中,执行了前2个++i,然后读取了k[]的值,然后是最后++i然后k[]

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

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

答案 11 :(得分:5)

您的问题可能不是,“为什么这些构造在C语言中未定义行为?”。您的问题可能是,“为什么这段代码(使用++不能给我期望的值?”,有人将您的问题标记为重复,然后发送给您。

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

我假设您现在已经听说了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语言不能保证它们会做什么。这是一个奇怪且令人惊讶的结果,因为您可能认为只要编写并运行该程序,任何可以编写的程序都会生成唯一的,定义明确的输出。但是对于未定义的行为,情况并非如此。

是什么使表达式不确定?包含++--的表达式是否始终未定义?当然不是:它们是有用的运算符,如果正确使用它们,它们的定义就很好。

对于表达式,我们正在讨论的是使它们不确定的原因是:一次执行的事情太多,不确定何时会发生什么顺序,而是何时顺序对结果产生影响。 / p>

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

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

问题是,在调用printf之前,编译器是否先计算xx++++x的值?但事实证明,我们不知道。 C中没有规则说函数的参数从左到右或从右到左或以其他顺序求值。因此,我们无法确定编译器是否先执行x,然后执行++x,然后执行x++,还是先执行x++,然后执行++x,然后执行x或其他顺序。但是顺序显然很重要,因为根据编译器使用的顺序,我们显然会得到printf打印的不同结果。

这个疯狂的表情呢?

x = x++ + ++x;

该表达式的问题在于它包含三种不同的尝试来修改x的值:(1)x++部分试图将x加1,将新值存储在x中,并返回x的旧值; (2)++x部分尝试将x加1,将新值存储在x中,然后返回新值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的值,并对其进行修改。

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

答案 12 :(得分:4)

来自n1188的文档the ISO W14 site中提供了关于此类计算中发生的事情的一个很好的解释。

我解释了这些想法。

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

  

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

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

在我上面引用的论文中,解释了你可以把程序想象成由小方框组成,每个方框包含2个连续序列点之间的指令。序列点在标准的附录C中定义,在i=i++的情况下,有2个序列点限定了完整表达。这样的表达式在语法上等同于语法的Backus-Naur形式的expression-statement条目(语法的附录A中提供了语法)。

因此,框内的指令顺序没有明确的顺序。

i=i++

可以解释为

tmp = i
i=i+1
i = tmp

tmp = i
i = tmp
i=i+1

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

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

编辑:

解释此类含糊不清的其他好消息来源是c-faq网站(也已发布as a book)的条目,即herehere以及here。< / p>

答案 13 :(得分:3)

原因是程序正在运行未定义的行为。问题在于评估顺序,因为根据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
    

    }

海湾合作委员会如何运作?它以右手边(RHS)从左到右的顺序计算子表达式,然后将值分配给左侧(LHS)。这正是Java和C#的行为和定义标准的方式。 (是的,Java和C#中的等效软件已经定义了行为)。它按照从左到右的顺序在RHS声明中逐个评估每个子表达式;对于每个子表达式:首先计算++ c(预增量),然后将值c用于操作,然后使用后增量c ++)。

根据GCC C++: Operators

  

在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 
}

视觉工作室如何工作,它采用另一种方法,它在第一遍中评估所有预增量表达式,然后在第二遍中使用操作中的变量值,在第三遍中从RHS分配到LHS,然后最后传递它在一次传递中计算所有后增量表达式。

因此,C ++定义的行为中的等价物与Visual 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处声明:

  

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