实际上,为什么不同的编译器会计算int x = ++ i + ++ i;的不同值?


165

考虑以下代码:

int i = 1;
int x = ++i + ++i;

我们假设编译器对此代码可能进行编译时有一些猜测。

  1. 两者都++i返回2,导致x=4
  2. 一个++i返回2,另一个返回3,导致x=5
  3. 两者都++i返回3,导致x=6

在我看来,第二次出现的可能性最大。使用++运算符执行两个运算符之一i = 1,将i其递增,然后2返回结果。然后,使用++来执行第二个运算符i = 2,将i递增,并3返回结果。然后23加在一起得到5

但是,我在Visual Studio中运行了这段代码,结果是6。我试图更好地理解编译器,并且想知道什么可能导致的结果6。我唯一的猜测是代码可以通过一些“内置”并发执行。++调用了两个运算符,每个运算符i在另一个返回之前先递增,然后都返回3。这将与我对调用堆栈的理解相矛盾,因此需要加以解释。

C++编译器可能会执行哪些(合理的)操作,从而导致结果4或结果或6

注意

该示例在Bjarne Stroustrup的《编程:使用C ++的原理和实践》中作为未定义行为的示例出现。

参见肉桂的评论


5
与pre / postincrement操作相比,C规范实际上并未涵盖=右侧的运算或评估的顺序,仅涵盖了左侧。
Cristobol Polychronopolis

2
建议您是否从Stroustrup的书中获得了该示例(如对其中一个答案的评论中所述),请在问题中给出引用。
丹尼尔·科林斯

4
@philipxy您建议的重复项不是此问题的重复项。问题是不同的。建议的重复项中的答案不能回答此问题。建议重复中的答案不是此问题已接受(或高票)答案的重复。我相信您误解了我的问题。我建议您重新阅读并重新考虑关闭投票。
肉桂

3
@philipxy“答案说编译器可以做任何事情……”这没有回答我的问题。“他们表明,即使您认为您的问题有所不同,也只是该问题的一种变体”。“尽管您不提供C ++版本”我的C ++版本与我的问题无关。我知道“因此语句所在的整个程序可以执行任何操作”,但是我的问题是关于特定的行为。“您的评论不反映那里答案的内容。” 我的评论反映了我的问题的内容,您应该重新阅读。
肉桂

2
回答标题;因为UB表示未定义bahavioir。在历史上的不同时期,由不同的人针对不同的体系结构制作了多个编译器,当要求他们在行外着色并进行真实世界的实现时,他们不得不在规范之外的这一部分中添加一些内容,因此人们正是这样做了他们使用了不同的蜡笔。因此,基本原则就是不要依赖UB
Toby

Answers:


200

编译器将您的代码分成非常简单的指令,然后以它认为最佳的方式重新组合和排列它们。

编码

int i = 1;
int x = ++i + ++i;

由以下说明组成:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

但是,尽管这是我编写方法的编号列表,但这里只有少数排序依存关系:1-> 2-> 3-> 4-> 5-> 10-> 11和1-> 6-> 7- > 8-> 9-> 10-> 11必须保持相对顺序。除此之外,编译器可以自由地重新排序,并可能消除冗余。

例如,您可以按以下顺序订购列表:

1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
4. store tmp1 in i
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

为什么编译器可以这样做?因为增量的副作用没有排序。但是现在编译器可以简化:例如,在4中有一个死存储:该值立即被覆盖。另外,tmp2和tmp4确实是同一回事。

1. store 1 in i
2. read i as tmp1
6. read i as tmp3
3. add 1 to tmp1
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

现在,与tmp1有关的所有操作都是无效代码:从未使用过。而且我的重读也可以消除:

1. store 1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
10. add tmp3 and tmp3, as tmp5
11. store tmp5 in x

看,这段代码要短得多。优化器很高兴。程序员不是,因为我只增加了一次。哎呀。

让我们看看编译器可以做的其他事情:让我们回到原始版本。

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
5. read i as tmp2
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

编译器可以像这样重新排序:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
9. read i as tmp4
10. add tmp2 and tmp4, as tmp5
11. store tmp5 in x

然后再次注意,我被阅读了两次,因此消除其中之一:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp3
7. add 1 to tmp3
8. store tmp3 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

很好,但是可以更进一步:它可以重用tmp1:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
6. read i as tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

然后可以消除6中i的重读:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
4. store tmp1 in i
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

现在4已死:

1. store 1 in i
2. read i as tmp1
3. add 1 to tmp1
7. add 1 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

现在3和7可以合并为一条指令:

1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
5. read i as tmp2
10. add tmp2 and tmp2, as tmp5
11. store tmp5 in x

消除最后一个临时:

1. store 1 in i
2. read i as tmp1
3+7. add 2 to tmp1
8. store tmp1 in i
10. add tmp1 and tmp1, as tmp5
11. store tmp5 in x

现在,您得到了Visual C ++给您的结果。

请注意,在两个优化路径中,重要的顺序依赖性都得以保留,只要不删除指令就什么​​都不做即可。


36
当前,这是唯一提到测序的答案。
下午20年

3
-1我认为这个答案不清楚。观察到的结果根本不依赖于任何编译器优化(请参阅我的答案)。
Daniel R. Collins

3
这假定读取-修改-写入操作。一些CPU,例如无处不在的x86,具有原子增量操作,这使情况变得更加复杂。
Mark

6
@philipxy“该标准对目标代码无话可说。” 该标准也无话可说,它是UB。这是问题的前提。OP想知道为什么在实践中,编译器会得出不同而奇怪的结果。另外,我的回答甚至没有说任何有关目标代码的内容。
塞巴斯蒂安·雷德尔

5
@philipxy我不理解您的反对意见。如前所述,问题是关于在存在UB的情况下编译器可能做什么,而不是C ++标准。在研究假设的编译器如何转换代码时,为什么使用目标代码不合适?实际上,除了目标代码外,其他任何东西都没有关系吗?
康拉德·鲁道夫

58

虽然这是UB(如OP所示),但以下是编译器可以获取3个结果的假设方法。x如果与不同的int i = 1, j = 1;变量而不是一个相同的变量一起使用,这三个变量都会给出正确的结果i

  1. 两者++ i都返回2,导致x = 4。
int i = 1;
int i1 = i, i2 = i;   // i1 = i2 = 1
++i1;                 // i1 = 2
++i2;                 // i2 = 2
int x = i1 + i2;      // x = 4
  1. 一个++ i返回2,另一个++ 3返回x = 5。
int i = 1;
int i1 = ++i;           // i1 = 2
int i2 = ++i;           // i2 = 3
int x = i1 + i2;        // x = 5
  1. 两者++ i都返回3,导致x = 6。
int i = 1;
int &i1 = i, &i2 = i;
++i1;                   // i = 2
++i2;                   // i = 3
int x = i1 + i2;        // x = 6

2
这比我希望的要好,谢谢。
肉桂

1
对于选项1,编译器可能已对preincrement进行了注释i。知道它只能发生一次,它只会发出一次。对于选项2,将代码从字面上转换为机器代码,就像大学编译器类项目可能做的那样。对于选项3,它类似于选项1,但是它制作了两个预增量副本。必须使用向量,而不是集合。:-)
Zan Lynx

@dxiv对不起,我不好,我混在一起了
Muru

22

在我看来,第二次出现的可能性最大。

我要选择选项4:两者++i同时发生。

较新的处理器朝着一些有趣的优化和并行代码评估迈进,这是编译器不断提高代码速度的另一种方式。我认为编译器向并行性迈进是一种实际的实现

我很容易看到由于相同的内存争用而导致的争用情况导致不确定的行为或总线故障-由于编码器违反了C ++合同,因此全部允许-因此UB。

我的问题是:C ++编译器可以执行哪些(合理的)操作,导致结果为4或结果为6?

可以,但不计入其中。

不要使用++i + ++i也不期望有明智的结果。


如果我既可以接受这个答案,也可以接受@dxiv的支持。感谢您的答复。
肉桂

4
@UriRaz:根据编译器的选择,处理器甚至可能不会注意到存在数据危险。例如,编译器可能会分配i给两个寄存器,将两个寄存器递增,然后将它们全部写回。处理器无法解决该问题。根本问题是C ++和现代CPU都不是严格顺序的。C ++显式具有在发生之前和之后发生的顺序,默认情况下允许同时发生。
MSalters

1
但是我们知道,使用Visual Studio的OP并非如此。包括x86和ARM在内的大多数主流ISA是按照完全顺序执行模型定义的,其中一条机器指令在另一条机器指令开始之前完全完成。超标量无序必须对单个线程保持这种错觉。(不能保证读取共享内存的其他线程可以按程序顺序查看事物,但是OoO exec的基本规则是不破坏单线程执行。)
Peter Cordes

1
这是我最喜欢的答案,因为它是唯一提到在CPU级别执行并行指令的人。顺便说一句,很高兴在答案中提到,由于竞争条件,cpu线程将被暂停,以等待在相同内存位置上的互斥锁解锁,因此这在并发模型中非常不理想。第二-由于相同的竞争条件,实际答案可以是45-,具体取决于cpu线程执行模型/速度,因此,这本质上就是UB。
Agnius Vasiliauskas

1
@AgniusVasiliauskas也许,然而,考虑到当今处理器的简单化,“实际上,为什么不同的编译器为什么会计算不同的值”正在寻找更多易于理解的东西。但是,编译器/处理器方案的范围要远远大于所提到的几种答案。您的有用见解是另一回事。IMO,并行性是未来,因此,尽管以抽象的方式,但答案仍集中在那些方面,因为未来仍在发展。在IAC中,该帖子已变得很流行,并且易于掌握的答案也得到了最好的奖励。
chux-恢复莫妮卡

17

我认为,简单明了的解释(无需对编译器优化或多线程进行任何尝试)就是:

  1. 增量 i
  2. 增量 i
  3. 添加i+i

i递增两次,它的值是3,并且当加在一起时,和为6。

为了进行检查,请将其视为C ++函数:

int dblInc ()
{
    int i = 1;
    int x = ++i + ++i;
    return x;   
}

现在,这是我使用旧版本的GNU C ++编译器(win32,gcc版本3.4.2(mingw-special))通过编译该函数获得的汇编代码。这里没有花哨的优化或多线程发生:

__Z6dblIncv:
    push    ebp
    mov ebp, esp
    sub esp, 8
    mov DWORD PTR [ebp-4], 1
    lea eax, [ebp-4]
    inc DWORD PTR [eax]
    lea eax, [ebp-4]
    inc DWORD PTR [eax]
    mov eax, DWORD PTR [ebp-4]
    add eax, DWORD PTR [ebp-4]
    mov DWORD PTR [ebp-8], eax
    mov eax, DWORD PTR [ebp-8]
    leave
    ret

请注意,局部变量i仅位于一个位置的堆栈上:address [ebp-4]。该位置增加了两次(在汇编函数的第5-8行中;包括该地址中显然有的冗余负载eax)。然后在第9-10行,将该值加载到中eax,然后添加到中eax(即,计算当前i + i)。然后将其冗余复制到堆栈中,并eax作为返回值返回(显然将为6)。

查看C ++标准(此处为旧标准:ISO / IEC 14882:1998(E))可能会对您有所帮助,该标准在“表达式”第5.4节中说:

除非另有说明,否则未指定各个运算符的操作数和各个表达式的子表达式的求值顺序以及发生副作用的顺序。

带有脚注:

运算符的优先级不是直接指定的,但是可以从语法中得出。

此时给出了两个未指定行为的示例,都涉及增量运算符(其中一个是:)i = ++i + 1

现在,如果愿意,可以:创建一个整数包装器类(如Java Integer);重载函数operator+operator++以便它们返回中间值对象;从而编写++iObj + ++iObj并获取它以返回一个持有5的对象。(为了简洁起见,此处未包含完整的代码。)

就个人而言,我很感兴趣是否有一个著名的编译器示例,该示例以上述顺序以外的任何其他方式完成了该工作。在我看来,最直接的实现inc是在执行加法运算之前仅对原始类型执行两个汇编代码。


2
增量运算符确实具有非常明确定义的“返回”值
edc65

@philipxy:我已经编辑了答案,以删除您反对的文章。此时,您可能同意也可能不同意答案。
Daniel R. Collins

2
这些不是“未指定行为的两个例子”,不是两个未定义行为的例子,这是一个非常不同的野兽,源于标准中的不同段落。我看到C ++ 98曾经在脚注示例的文本中说“未指定”,与规范性文本相反,但后来已解决。
库比

@Cubbi:此处引用的标准中的文本和脚注均使用短语“未指定”,“未直接指定”,并且似乎与定义1.3.13中的术语匹配。
丹尼尔·科林斯

1
@philipxy:我看到您对这里的许多答案重复了相同的评论。似乎您的主要批评实际上更多地是关于OP的问题本身,其范围不仅仅是关于抽象标准。
Daniel R. Collins

7

编译器可以做的合理的事情是Common Subexpression Elimination。这已经是编译器中的常见优化:如果子表达式(x+1)在较大的表达式中多次出现,则只需要计算一次即可。例如,在a/(x+1) + b*(x+1)x+1子表达式可以计算一次。

当然,编译器必须知道可以通过这种方式优化哪些子表达式。拨打rand()两次应给两个随机数。因此,非内联函数调用必须免于CSE。正如您所注意到的,没有规则说明i++应如何处理两次出现的情况,因此没有理由将其从CSE中豁免。

结果可能确实int x = ++i + ++i;是优化为int __cse = i++; int x = __cse << 1。(CSE,然后反复降低强度)


该标准对目标代码无话可说。这与语言定义无关或与语言定义无关。
philipxy

1
@philipxy:该标准对任何形式的未定义行为均无话可说。这是问题的前提。
MSalters

7

实际上,您正在调用未定义的行为。任何事情都有可能发生,不只是东西,你认为“合理”,而且往往事情发生,你不考虑合理。按照定义,一切都是“合理的”。

一个非常合理的编译是,编译器观察到执行一条语句将调用未定义的行为,因此该语句无法执行,因此将其翻译为一条指令,有意使您的应用程序崩溃。那是很合理的。

下载者:GCC非常反对您。


当标准将某项行为表征为“未定义的行为”时,这意味着该行为不在标准的管辖范围之内。由于该标准没有试图判断其管辖范围之外事物的合理性,也没有试图禁止合规实施可能无故无用的所有方式,因此该标准未在特定情况下强加要求并不表示任何判断可能的动作同样是“合理的”。
超级猫

6

有没有合理的事情编译器能做得到的6的结果,但它是可能的,合法的。4的结果是完全合理的,我认为5边界的结果是合理的。所有这些都是完全合法的。

嘿,等等!不清楚会发生什么吗?加法需要两个增量的结果,因此显然这些必须首先发生。我们从左到右走,所以...啊!如果只是那么简单。不幸的是,事实并非如此。我们不会从左到右走,这就是问题所在。

对于编译器来说,将内存位置读取到两个寄存器中(或从相同的文字初始化它们,优化往返内存)是一件非常合理的事情。这将有效地具有有隐蔽在两个效果不同的变量,每个具有2的值,这将最终被添加到为4的结果。这是“合理的”,因为它是快速和有效的,而且它是根据两个标准和代码。

类似地,该存储位置可以被读取一次(或从立即数初始化的变量)并增加一次,此后可以递增另一个寄存器中的卷影副本,这将导致2和3加在一起。我认为这是合理的边界,尽管完全合法。我认为这是合理的,因为它不是一个。它既不是“合理的”优化方法,也不是“合理的”完全学究的方法。它有点在中间。

将内存位置增加两次(结果为3),然后将该值加到自身以得到最终结果6,这是合理的,但由于进行内存往返并不十分有效,因此不太合理。尽管在具有良好存储转发功能的处理器上,这样做也可能是“合理的”,因为存储应该基本上是不可见的...
当编译器“知道”它位于相同的位置时,它也可能选择递增在寄存器中将该值两次,然后再将其添加到自身。两种方法都可以得到6的结果。

按照标准的措辞,编译器可以为您提供任何这样的结果,尽管我个人认为6是Obnoxious Department的“ fuck you”备忘录,因为这是相当意外的事情(无论是否合法,尝试总是给自己最少的惊喜是一件好事!)。尽管看到未定义行为是如何涉及的,可悲的是,人们不能真正争论“意外”,嗯。

那么,实际上,您在编译器中拥有的代码是什么?让我们问一声clang,它将向我们展示我们是否做得很好(调用-ast-dump -fsyntax-only):

ast.cpp:4:9: warning: multiple unsequenced modifications to 'i' [-Wunsequenced]
int x = ++i + ++i;
        ^     ~~
(some lines omitted)
`-CompoundStmt 0x2b3e628 <line:2:1, line:5:1>
  |-DeclStmt 0x2b3e4b8 <line:3:1, col:10>
  | `-VarDecl 0x2b3e430 <col:1, col:9> col:5 used i 'int' cinit
  |   `-IntegerLiteral 0x2b3e498 <col:9> 'int' 1
  `-DeclStmt 0x2b3e610 <line:4:1, col:18>
    `-VarDecl 0x2b3e4e8 <col:1, col:17> col:5 x 'int' cinit
      `-BinaryOperator 0x2b3e5f0 <col:9, col:17> 'int' '+'
        |-ImplicitCastExpr 0x2b3e5c0 <col:9, col:11> 'int' <LValueToRValue>
        | `-UnaryOperator 0x2b3e570 <col:9, col:11> 'int' lvalue prefix '++'
        |   `-DeclRefExpr 0x2b3e550 <col:11> 'int' lvalue Var 0x2b3e430 'i' 'int'
        `-ImplicitCastExpr 0x2b3e5d8 <col:15, col:17> 'int' <LValueToRValue>
          `-UnaryOperator 0x2b3e5a8 <col:15, col:17> 'int' lvalue prefix '++'
            `-DeclRefExpr 0x2b3e588 <col:17> 'int' lvalue Var 0x2b3e430 'i' 'int'

如您所见,相同的lvalue Var 0x2b3e430前缀++在两个位置应用,并且这两个前缀在树中的同一节点之下,这恰好是一个非常特殊的运算符(+),对序列等没有特别说明。为什么这很重要?好吧,继续读下去。

请注意以下警告:“对i进行了多个无序的修改”。哦,听起来不好。这是什么意思?[basic.exec]告诉我们有关副作用和排序的信息,并且告诉我们(第10段),默认情况下,除非另有明确说明,否则对单个运算符的操作数和各个表达式的子表达式的求值是无序列的。好吧,该死的就是这样operator+-别无其他说法,所以...

但是,我们是否关心先序列,不确定序列或未序列?谁想知道?

同一段还告诉我们,未排序的求值可能会重叠,并且当它们引用相同的内存位置时(就是这种情况!),并且该值不是潜在的并发,则行为是不确定的。这是真正的丑陋之处,因为那意味着您一无所知,也无法保证自己“合理”。不合理的事情实际上是完全可以容许和“合理的”。


使用“合理的”只是为了防止任何人说“编译器可以做任何事情,甚至发出'set x to 7'的单个指令。”也许我应该澄清一下。
肉桂

@cinnamon很多年前,当我还没有经验的时候,Sun的编译器工程师告诉我说,他们的编译器为发生当时我认为不合理的不确定行为提供了绝对合理的代码。学过的知识。
gnasher729

该标准对目标代码无话可说。这是零散的,并且不清楚您建议的实现如何由语言定义合理化或与语言定义相关。
philipxy

@philipxy:该标准定义了格式正确和定义明确的内容,哪些不是。对于此Q,它将行为定义为未定义。除了合法之外,编译器还可以合理地生成有效的代码。是的,您是对的,标准不要求是这种情况。但是,这是一个合理的假设。
戴蒙

@Damon:该标准指定了所有实现都必须按照定义对待的操作,以及不需要实现将那些按照定义对待的操作。因为某些任务比其他任务需要更广泛的语义,所以Standard未能定义某些动作的行为并不意味着无论如何,定义它的实现将比不执行的任务更适合某些任务。 ,也没有定义失败的行为不会使实现比某些确实定义了实现的对象更不适合实现。
supercat

1

有一条规则

在上一个序列点与下一个序列点之间,一个标量对象最多必须通过表达式的求值修改其存储值,否则行为是不确定的。

因此,即使x = 100也是可能的有效结果。

对我而言,该示例中最合乎逻辑的结果是6,因为我们将i的值增加了两倍,并且它们将其加到了自身上。在“ +”的两边计算值之前很难进行加法运算。

但是编译器开发人员可以实现任何其他逻辑。


0

看起来++ i返回左值,但是i ++返回右值。
这样的代码就可以了:

int i = 1;
++i = 10;
cout << i << endl;

这不是:

int i = 1;
i++ = 10;
cout << i << endl;

以上两个语句与VisualC ++,GCC7.1.1,CLang和Embarcadero一致。
这就是为什么您在VisualC ++和GCC7.1.1中的代码类似于以下代码的原因

int i = 1;
... do something there for instance: ++i; ++i; ...
int x = i + i;

查看反汇编时,它首先递增i,然后重写i。尝试添加时,它执行相同的操作,将i递增并重写。然后将我添加到我。 我注意到CLang和Embarcadero的行为有所不同。因此,它与第一个语句不一致,在第一个++ i之后,它将结果存储在一个右值中,然后将其添加到第二个i ++中。
在此处输入图片说明


“看起来像一个左值”的问题在于,您是从C ++标准的角度而不是编译器的角度进行交谈。
MSalters

@MSalters该语句与VisualStudio 2019,GCC7.1.1,clang和Embarcadero以及第一段代码一致。因此规格是一致的。但是对于第二段代码,它的工作方式有所不同。第二段代码与VisualStudio 2019和GCC7.1.1一致,但与clang和Embarcadero不一致。
armagedescu

3
好吧,答案中的第一段代码是合法的C ++,因此实现显然与规范一致。与问题相比,您的“执行操作”以分号结尾,使其成为完整的陈述。这样就创建了C ++标准所要求的序列,但该序列不存在于问题中。
MSalters

@MSalters我想将其作为等效的伪代码来实现。但是我不确定如何重新设置它
armagedescu

0

我个人绝不会期望编译器在您的示例中输出6。您的问题已经有很好且详细的答案。我会尝试一个简短的版本。

++i在这种情况下,基本上是一个两步过程:

  1. 增值 i
  2. 读取的值 i

在添加的++i + ++i两个方面,可以根据标准以任何顺序评估添加。这意味着这两个增量被认为是独立的。而且,两个术语之间没有依赖性。因此,增量和读取i可以交错。这给出了潜在的顺序:

  1. i左操作数的增量
  2. i正确的操作数的增量
  3. 读回i左操作数
  4. 读回i正确的操作数
  5. 两者之和:收益6

现在,我考虑一下,按照标准,6是最有意义的。对于4的结果,我们需要一个CPU,该CPU首先i独立读取,然后递增并将该值写回到同一位置。基本上是比赛条件 对于5值,我们需要一个引入临时变量的编译器。

但是,该标准说++i在返回变量之前(即在实际执行当前代码行之前)先递增变量。求和运算符+需要i + i在应用增量后求和。我想说C ++需要处理变量而不是值语义。因此,对我来说6是最有意义的,因为它依赖于语言的语义而不是CPU的执行模型。


0
#include <stdio.h>


void a1(void)
{
    int i = 1;
    int x = ++i;
    printf("i=%d\n",i);
    printf("x=%d\n",x);
    x = x + ++i;    // Here
    printf("i=%d\n",i);
    printf("x=%d\n",x);
}


void b2(void)
{
    int i = 1;
    int x = ++i;
    printf("i=%d\n",i);
    printf("x=%d\n",x);
    x = i + ++i;    // Here
    printf("i=%d\n",i);
    printf("x=%d\n",x);
}


void main(void)
{
    a1();
    // b2();
}

欢迎使用stackoverflow!您能否在答案中提供任何限制,假设或简化。在此链接上查看有关如何回答的更多详细信息:stackoverflow.com/help/how-to-answer
Usama Abdulrehman

0

因为答案取决于编译器解码语句的方式。使用两个不同的变量++ x和++ y来创建逻辑是一个更好的选择。注意:输出取决于ms visual Studio中语言的最新版本(如果已更新),因此如果规则已更改,则输出也将更改


0

尝试这个

int i = 1;
int i1 = i, i2 = i;   // i1 = i2 = 1
++i1;                 // i1 = 2
++i2;                 // i2 = 2
int x = i1 + i2;      // x = 4

0

从此评估链接顺序

未指定任何C运算符的操作数的求值顺序,包括函数调用表达式中的函数参数的求值顺序,以及任何表达式中的子表达式的求值顺序(以下说明除外)。编译器将按任何顺序对它们进行求值,并在再次求同一个表达式时可以选择其他顺序

从引号可以明显看出,评估顺序未由C标准指定。不同的编译器执行不同的评估顺序。编译器可以自由地以任何顺序求值此类表达式。这就是为什么不同的编译器为问题中提到的表达式提供不同的输出的原因。

但是,如果在子表达式Exp1和Exp2之间存在序列点,则在计算Exp2的每个值和副作用之前,先对Exp1的值计算和副作用进行排序。


这个报价和您的陈述与回答问题有什么关系?(修辞。)无论如何,给定的代码具有未定义的行为,如此处其他地方所述,因此您的观点不适用。同样,询问者试图(严重地)询问在代码未定义的情况下,其实现的哪些方面会导致其执行某种操作。他们也不会接受,如果不澄清自己的问题,编译器所做的任何事情都是“合理的”。此外,此帖子还没有为此处已发布的帖子添加任何内容。
philipxy

您的评论未涉及我的任何问题。包括代码未定义和未指定。
philipxy

引号没有提到未定义,而是没有指定。我受够了。
philipxy

是的,它没有被说明。这就是为什么不同的编译器会执行不同的评估顺序的原因。这就是为什么您将为不同的编译器获得不同的输出的原因。
克里希纳(Krishna Kanth)Yenumula


-4

实际上,您正在调用未定义的行为。任何事情都有可能发生,不只是东西,你认为“合理”,而且往往事情发生,你不考虑合理。按照定义,一切都是“合理的”。

一个非常合理的编译是,编译器发现执行一条语句将调用未定义的行为,因此该语句无法执行,因此将其翻译为一条指令,有意使您的应用程序崩溃。那是很合理的。毕竟,编译器知道这种崩溃永远不会发生。


1
我相信您误解了这个问题。问题是关于可能导致特定结果(x = 4、5或6的结果)的一般或特定行为。如果您不喜欢我对“合理的”一词的使用,请直接转到上面的评论,您的回答是:“使用“合理的”只是要阻止任何人说“编译器可以做任何事情,甚至发出单一指令'“ set x to 7”“'”“如果您对保留总体思路的问题的措词更好,我愿意接受。此外,您似乎已重新发布了答案。
肉桂

2
建议删除您的两个答案之一,因为它们都非常相似
MM
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.