对于i = 0,为什么(i + = i ++)等于0?


253

采用以下代码(可用作控制台应用程序):

static void Main(string[] args)
{
    int i = 0;
    i += i++;
    Console.WriteLine(i);
    Console.ReadLine();
}

结果i为0。我期望为2(就像我的一些同事一样)。编译器可能会创建某种导致i零的结构。

我期望2的原因是,按照我的观点,将首先评估右手陈述,将i加1。然后将其加到i。因为我已经是1,所以它将1加1。所以1 + 1 =2。显然,这不是正在发生的事情。

您能解释一下编译器做什么或在运行时发生什么吗?为什么结果为零?

某种免责声明:我绝对知道您不会(也许不应该)使用此代码。我知道我永远不会。不过,我发现知道为什么以这种方式起作用以及正在发生的事情很有趣。


57
预期结果不应该是1吗?i(0)+ = i ++(1)因此0 + = 1 = 1
拍卖

11
它按预期工作
史蒂夫是一个d

177
这个问题将要问多少种变化?
mowwwalker

20
预增量将在执行操作之前增加值i + = ++ i将给您1
Pierluc SS 2012年

21
为什么每个人都专注于前增量还是后增量?的“奇怪”的事情是i上的左手侧+=的右侧前“缓存”进行评估。这是违反直觉的,例如,如果i是对象,则需要复制操作。(请不要0
误会

Answers:


425

这个:

int i = 0;
i += i++

可以在您做的时候看到(以下是过于简化的情况):

int i = 0;
i = i + i; // i=0 because the ++ is a postfix operator and hasn't been executed
i + 1; // Note that you are discarding the calculation result

实际发生的事情比这还涉及更多-看一下MSDN,7.5.9 Postfix递增和递减运算符

x ++或x--形式的后缀递增或递减操作的运行时处理包括以下步骤:

  • 如果x被分类为变量:

    • 计算x以产生变量。
    • x的值被保存。
    • 使用保存的x值作为参数调用选定的运算符。
    • 运算符返回的值存储在x求值给出的位置。
    • x的保存值成为运算结果。

请注意,由于优先顺序的原因,后缀++发生在之前 +=,但是结果最终未使用(因为使用了的先前值i)。


更透彻的分解,i += i++它是由零件都需要一个人知道这两个+=++不是原子(即,没有一个是单人操作),即使它们看起来像他们。这些实现的方式涉及临时变量,i即操作发生之前的副本-每个操作一个。(我将使用的名称iAddiAssign所使用的临时变量+++=分别)。

因此,更接近发生的事情是:

int i = 0;
int iAdd = i; // Copy of the current value of i, for ++
int iAssign = i; // Copy of the current value of i, for +=

i = i + 1; // i++ - Happens before += due to order of precedence
i = iAdd + iAssign;

3
@Oded该++操作在语句的评估完成之前完成。因此将+=覆盖值。这是发生了什么吗?
Anirudh Ramanathan 2012年

6
@Oded实际上是:int i = 0; i = i + 1; (postfix) i = 0; (assignment)。如果您在该语句的其他地方使用了i,则该时间值将为1。
drch 2012年

@Cthulhu-本质上。将答案通过DTB进入细节。
Oded 2012年

6
我不买这个。@yoriy的答案更加准确。首先,在回答中,您说的是最后一行,i+1而应该是i=i+1。那i++不是吗?
2012年

3
答案的第一部分是多余的。您的上一个代码示例可以做到这一点。+1。
corazza 2012年

194

反汇编运行代码:

int i = 0;
  xor         edx, edx
  mov         dword ptr i, edx         // set i = 0
i += i++;
  mov         eax, dword ptr i         // set eax = i (=0)
  mov         dword ptr tempVar1, eax  // set tempVar1 = eax (=0)
  mov         eax, dword ptr i         // set eax = 0 ( again... why??? =\ )
  mov         dword ptr tempVar2, eax  // set tempVar2 = eax (=0)
  inc         dword ptr i              // set i = i+1 (=1)
  mov         eax, dword ptr tempVar1  // set eax = tempVar1 (=0)
  add         eax, dword ptr tempVar2  // set eax = eax+tempVar2 (=0)
  mov         dword ptr i, eax         // set i = eax (=0)

等效代码

它编译为与以下代码相同的代码:

int i, tempVar1, tempVar2;
i = 0;
tempVar1 = i; // created due to postfix ++ operator
tempVar2 = i; // created due to += operator
++i;
i = tempVar1 + tempVar2;

反汇编第二个代码(只是为了证明它们是相同的)

int i, tempVar1, tempVar2;
i = 0;
    xor         edx, edx
    mov         dword ptr i, edx
tempVar1 = i; // created due to postfix ++ operator
    mov         eax, dword ptr i
    mov         dword ptr tempVar1, eax
tempVar2 = i; // created due to += operator
    mov         eax, dword ptr i
    mov         dword ptr tempVar2, eax
++i;
    inc         dword ptr i
i = tempVar1 + tempVar2;
    mov         eax, dword ptr tempVar1
    add         eax, dword ptr tempVar2
    mov         dword ptr i, eax

打开拆卸窗口

大多数人不知道,甚至不记得他们可以使用Visual Studio Disassembly窗口看到最终的内存中汇编代码。它显示正在执行的机器代码,而不是CIL。

在调试时使用它:

Debug (menu) -> Windows (submenu) -> Disassembly

那么postfix ++会发生什么呢?

postfix ++告诉我们,我们希望在求值后增加操作数的值...大家都知道...有点困惑的是“求值后”的含义。

那么“评估后”是什么意思:

  • 必须影响同一行代码上操作数的其他用法:
    • a = i++ + i 第二个我受增量影响
    • Func(i++, i) 我第二次受到影响
  • 同一条线上的其他用法尊重短路运算符,例如||&&
    • (false && i++ != i) || i == 0 第三个我不受i ++的影响,因为未评估

那是什么意思i += i++;呢?

与...相同 i = i + i++;

评估顺序为:

  1. 存储i + i(即0 + 0)
  2. 增量i(我变为1)
  3. 将步骤1的值分配给i(i变为0)

并不是说增量被丢弃了。

是什么意思i = i++ + i;

这与前面的示例不同。第三项i受增量影响。

评估顺序为:

  1. 储存我(即0)
  2. 增量i(我变为1)
  3. 存储步骤1 + i的值(即0 + 1)
  4. 将步骤3的值分配给i(i变为1)

22
+ 1 ++-用于纯粹的硬核解剖。查克·诺里斯(Chuck Norris)会为此感到骄傲:)我想您确实假设OP在Intel上,而不是Mono端口上……
StuartLC 2012年

19
C#对表达式有明确定义的求值顺序,而目标代码仅实现该顺序。机器代码输出不是评估顺序的原因或解释。
卡兹(Kaz)2012年

8
机器代码使您很容易理解IMO如何执行评估顺序。
凯文(Kevin)

5
@StuartLC我明白你在那里做了什么。可惜的是,那丢掉的upvote。
Steffan Donal

2
a++ + a不一样,a + a++因为这不再是纯粹的数学。代数中的可交换性定律未考虑变量在表达式中途更改值的可能性。当编程是功能编程时,数学仅能巧妙地映射到编程。而且由于代表性的限制,甚至还没有。例如,浮点数有时表现得像实数,有时却不然。即使没有副作用,在数学中应用于实数的可交换性和关联性定律也会超过浮点数。
卡兹(Kaz)

61
int i = 0;
i += i++;

评估如下:

Stack<int> stack = new Stack<int>();
int i;

// int i = 0;
stack.Push(0);                   // push 0
i = stack.Pop();                 // pop 0 --> i == 0

// i += i++;
stack.Push(i);                   // push 0
stack.Push(i);                   // push 0
stack.Push(i);                   // push 0
stack.Push(1);                   // push 1
i = stack.Pop() + stack.Pop();   // pop 0 and 1 --> i == 1
i = stack.Pop() + stack.Pop();   // pop 0 and 0 --> i == 0

i两次更改:一次是通过i++表达式,一次是通过+=语句。

但是+=语句的操作数是

  • i的评价之前i++(的左手侧+=)和
  • i的评价之前i++(的右侧+=)。

啊,这是一个很棒的解释。让我想起了当我使用反向抛光表示法在基于堆栈的计算器上工作时的情况。
弥敦道

36

首先,i++返回0。然后i递增1。最后i,将其初始值设置i为0加上i++返回的值(也为零)。0 + 0 = 0。


2
但是它i += i++;不是i = i++;,因此将i++(0)的值添加到i,而不是“ i设置为i++返回的值”。现在的问题是,将i++返回的值相加时i,将i是增加的值还是未增加的值?答案是我的朋友,写在规范中。
丹尼尔·菲舍尔

是的,我会解决。但是,因为反正i = 0一开始,i += something就相当于i = 0 + somethingi = something
2012年

32

这是抽象语法树的从左到右,自下而上的评估。从概念上讲,表达式的树是从上向下行走的,但是当递归从底部弹出时,评估就展开了。

// source code
i += i++;

// abstract syntax tree

     +=
    /  \
   i    ++ (post)
         \
         i

评估从考虑根节点开始+=。那是表达的主要成分。+=必须评估的左操作数,以确定我们存储变量的位置,并获取为零的先验值。接下来,必须评估右侧。

右侧是后递增++运算符。它具有一个操作数,i该操作数既可以作为值的来源,也可以作为要存储值的位置进行评估。操作员评估i,找到0,然后将a存储1到该位置。它返回先前值,0根据返回的先前值的其语义。

现在,控制权交还给+=操作员。现在,它具有所有信息以完成其操作。它知道存储结果的位置(的存储位置i)以及先验值,并且具有要添加到先验值的值,即0。因此,i最终为零。

与Java一样,C#通过固定求值顺序对C语言的非常精简的方面进行了消毒。从左到右,自下而上:编码人员可能期望的最明显的顺序。


+1:我同意你的看法,不同之处在于每个编码人员都希望...与以下内容相同:SetSum(ref i, Inc(ref i))with int SetSum(ref int a, int b) { return a += b; }int Inc(ref int a) { return a++; }...当然,我不再期望了。
Miguel Angelo

另外,我的预期是不一致的!这将不等于Set(ref i, Sum(i, Inc(ref i)))int Set(ref int a, int b) { return a = b; }int Sum(int a, int b) { return a + b; }
Miguel Angelo

谢谢; 您暗示我的答案有缺陷/不完整,我必须解决。
卡兹(Kaz)

问题SetSum在于它不评估左操作数i,而只接受其地址,因此它不等于对操作数进行完整的从左至右评估。您需要类似的东西SetSum(ref i, i, PostInc(ref i))。的第二个参数SetSum是要添加的值,在这里我们仅用于i指定的先验值iSetSum就是int SetSum(ref int dest, int a, int b) { return dest = a + b; }
卡兹(Kaz)2012年

+ =运算符会引起混淆(至少对我而言),因为赋值运算符具有从右到左的评估值(例如a = b = c = d)...因此,可以想象+ =遵循相同的规则,作为原子操作(就像我对SetSum方法所做的那样)...但是实际上发生的是C#转换a += ba = a + b...表明+ =运算符不是原子的……只是语法糖。
Miguel Angelo


17

后增量方法看起来像这样

int ++(ref int i)
{
    int c = i;
    i = i + 1;
    return c;
}

因此,基本上,当您调用时i++,它i是增量,但在您的情况下将返回原始值,即返回0。


12

简单的答案

int i = 0;
i += i++;
// Translates to:
i = i + 0; // because post increment returns the current value 0 of i
// Before the above operation is set, i will be incremented to 1
// Now i gets set after the increment,
// so the original returned value of i will be taken.
i = 0;

12

i ++的意思是:返回i的值,然后递增它。

i + = i ++表示:取i的当前值。添加i ++的结果。

现在,让我们添加i = 0作为起始条件。我+ =我+ +现在这样评估:

  1. i的当前值是多少?它是0。存储它,以便我们可以将i ++的结果添加到它。
  2. 评估i ++(评估为0,因为那是i的当前值)
  3. 加载存储的值,并将步骤2的结果添加到其中。(将0加到0)

注意:在步骤2的末尾,i的值实际上是1。但是,在步骤3中,可以通过在i的值递增之前加载它来丢弃它。

与i ++相反,++ i返回递增的值。

因此,i + = ++ i会给您1。


这是完全的帮助
sonsha

11

修正后增量运算符,++在表达式中为变量提供一个值,然后进行增量您分配返回零(0)值,以i覆盖递增的一(1),因此您将获得零。您可以在++运算符(MSDN)中阅读有关增量运算符的更多信息。




8

C#在做什么以及混乱的“原因”

我也期望该值为1 ...但是对此事的一些探索确实阐明了一些要点。

考虑以下方法:

    static int SetSum(ref int a, int b) { return a += b; }

    static int Inc(ref int a) { return a++; }

我期望i += i++与相同SetSum(ref i, Inc(ref i))。该语句之后的i值为1

int i = 0;
SetSum(ref i, Inc(ref i));
Console.WriteLine(i); // i is 1

但是后来我得出了另一个结论…… i += i++实际上与i = i + i++…… 相同,因此我使用这些函数创建了另一个类似的示例:

    static int Sum(int a, int b) { return a + b; }

    static int Set(ref int a, int b) { return a = b; }

调用此Set(ref i, Sum(i, Inc(ref i)))值后,i的值为0

int i = 0;
Set(ref i, Sum(i, Inc(ref i)));
Console.WriteLine(i); // i is 0

这不仅解释了C#在做什么……而且还解释了为什么很多人对此感到困惑……包括我在内。


2
请将其添加到原始答案中,将其作为单独的答案并没有任何好处。
casperOne 2012年

2
我这样做是为了避免污染其他答案,因为这与反编译的代码有关……而在此代码中,我尝试了另一种方法来解释问题。你怎么看?我应该编辑另一个答案并追加这个答案吗?也许,在这之前...不知道!感谢您的建议!
Miguel Angelo 2012年

7

我一直记得的一个好的记忆法是:

如果++站在的表达,它返回它的值之前。所以下面的代码

int a = 1;
int b = a++;

1,因为a是1 之前,它得到了增加了++站立 a。人们称此后修复符号。还有一个修复符号,其中的事情是完全相反的:如果++支架之前,表达式返回它的值的操作:

int a = 1;
int b = ++a;

b 这里有两个。

所以对于您的代码,这意味着

int i = 0;
i += (i++);

i++返回0(如上所述),因此0 + 0 = 0

i += (++i); // Here 'i' would become two

Scott Meyers在“有效的C ++编程”中描述了这两种表示法之间的区别。在内部,i++(后缀)会记住该值i,并调用前缀符号(++i)并返回旧值i。这就是为什么你应该使用百达++ifor环(虽然我觉得所有的现代编译器转换i++++ifor环)。


1
我测试了int i = 0; i += (++i),并将i其设置为1而不是2。这对我也很有意义,因为使用前缀而不是后缀不会改变以下事实:如果您写信i += (++i)i = i + (++i)i则在之前评估++i,导致i = 0 + (++i)最终i = 0 + 1
Wutz

6

唯一正确的问题答案是:因为未定义。

好吧,在你们把我烧死之前..

你们都回答了为什么i+=i++可以合理的结果i=0

我很想对您的每一个答案都投下反对票,但是我计算出的名声很高。

为什么我对你们这么生气?不是因为你的答案解释了..
我的意思是,我读的每个答案都做出了巨大的努力来解释不可能的事,我鼓掌!

但是结果如何呢?它是直观的结果-是可接受的结果吗?

你们每个人都看到了“裸露的国王”,并以某种方式接受了它作为理性的国王。

你们都错了!

i+=i++;结果0未定义。

语言评估机制中的错误,如果您愿意,甚至更糟!设计中的错误。

需要证明吗?当然要!

int t=0; int i=0; t+=i++; //t=0; i=1

现在,这是直观的结果!因为我们首先对t赋值的值进行了评估,然后才进行赋值和赋值,所以我们才进行后期操作-是合理的吗?

i=i++i=i得出相同的结果是否合理i

同时t=i++t=i对于i

语句评估后应执行后操作。
因此:

int i=0;
i+=i++;

如果我们写道:

int i=0;
i = i + i ++;

因此与:

int i=0;
i= i + i;
i ++;

因此与:

int i=0;
i = i + i;
i = i + 1;

任何不是的结果 1如果我们进行理性的思考,表示编译器中的错误或语言设计中的错误-但是MSDN和许多其他来源都告诉我们“嘿-这是未定义的!”

现在,在继续之前,任何人都不会支持或认可我给出的这组示例。但是,按照直觉和理性的方式,这应该是结果。

编码人员应该不知道汇编的编写或翻译方式!

如果以不遵守语言定义的方式编写-这是一个错误!

最后,我从Wikipedia,Increment和decrement运算符复制了此代码:
由于increment / decrement运算符会修改其操作数,因此在同一表达式中多次使用此类操作数会产生不确定的结果。例如,在诸如x − ++ x的表达式中,不清楚减法和增量运算符应按什么顺序执行。当编译器应用优化时,这种情况甚至变得更糟,这可能导致操作的执行顺序与程序员的意图不同。

因此。

正确答案是:不应使用此功能!(因为它是未定义的!)

是的。-即使C#编译器试图以某种方式对其进行标准化,其结果也无法预测。

我没有找到任何有关C#的文档,这些文档将所有行为描述为该语言的正常行为或定义明确的行为。我的发现恰恰相反!

[ 从MSDN文档复制,用于Postfix递增和递减运算符:++和- ]

将后缀运算符应用于函数参数时,在传递给函数之前,不能保证参数的值要递增或递减。有关更多信息,请参见C ++标准中的1.9.17节。

注意那些词不能保证 ...

原谅我,如果这个答案看起来很自大-我不是一个自大的人。我只是认为成千上万的人来这里学习,而我读到的答案会误导他们,并损害他们的逻辑和对该主题的理解。


我不确定我是否100%关注,但是您参考了C ++文档,但是我的问题是关于C#的。有关该文档的信息在这里
彼得

我在回答中指的是C#。通过您提供的链接:x ++或x--的结果是运算之前x的值,而++ x或--x的结果是运算之后x的值。无论哪种情况,x本身在运算后都具有相同的值。清楚地表明测试时不是这种情况,因为i=++i它将提供与截然不同的结果i=i++。因此,我的回答是正确的。
GY

好的,但是当您参考C ++文档时会感到困惑。那么,您是说该规范未正确实施?
彼得

不,我要说的是根据规范未定义,使用undefined将导致不确定的结果。
GY 2014年

在C ++中未定义,但是C#表示在操作后它应该是相同的值,不是吗?这与未定义的不一样(但是我同意您不应该使用它,请参阅我的免责声明,我只是想了解发生了什么事情)。
彼得

4

变量后的++运算符使它成为后缀增量。增量发生在语句中的其他所有内容之后,即添加和赋值。如果相反,您将++放在变量之前,它将在评估我的值之前发生,并为您提供预期的答案。


2
++不发生+=声明中,它发生 的执行+=语句。这就是为什么++get 的效果会被覆盖的原因+=
dtb

使用++ i实际上会得到1,而不是2(我最初的“预期答案”)。
彼得

看起来由于表达式的前增加或后增加,分配+= 覆盖了修改。
史蒂文·卢

4

计算步骤为:

  1. int i=0 //初始化为0
  2. i+=i++ //方程
  3. i=i+i++ //在通过编译器简化方程式之后
  4. i=0+i++ // i值替换
  5. i=0+0 // i ++为0,如下所述
  6. i=0 //最终结果i = 0

在这里,最初的值i是0。WKT,i++无非是:首先使用该i值,然后将该i值加1。因此,它使用i值0,同时进行计算i++,然后将其增加1。因此,得出一个值的0。


3

有两种选择:

第一种选择:如果编译器按如下方式读取该语句,

i++;
i+=i;

那么结果是2。

对于

else if
i+=0;
i++;

结果是1。


5
两者都不是实际结果。
史蒂文·卢

3

请非常小心:请阅读C FAQ:您试图做的事情(混合分配和++相同变量)不仅未指定,而且也是未定义的(意味着编译器可以做任何事情)在求值时!),不仅要“合理”的结果)。

请阅读第3节。整个部分非常值得一读!尤其是3.9,这说明了未指定的含义。第3.3节为您简要概述了使用“ i ++”之类可以做和不能做的事情。

根据编译器的内部情况,您可能会得到0或2或1,甚至其他任何值!而且因为它是未定义的,所以他们可以这样做。


糟糕,C#...我被“ gcc”抛弃了,有人经过一些反汇编来分解代码。
奥利维尔·杜拉克

1
我也错过了它也是C#,但还是喜欢答案。
伊恩·柯林斯

1
@Iain:谢谢,我也相信这是值得保留的回答后,许多人不知道这个(或有关,大多数用在subjcet知识的人都去同一个常见问题,从用户网的最佳时间进行更新的地方)
Olivier Dulac 2012年

3

上面的答案中有很多很好的推理,我只是做了一个小测试,想与您分享

int i = 0;
i+ = i++;

在这里,结果i显示0结果。现在考虑以下情况:

情况1:

i = i++ + i; //Answer 1

早些时候,我认为上面的代码类似于此,所以乍一看答案是1,而我对此的真正答案是1。

情况2:

i = i + i++; //Answer 0 this resembles the question code.

在这里,增量运算符不会出现在执行路径中,这与以前的情况不同,在以前的情况下,i ++有机会在加法之前执行。

我希望这能有所帮助。谢谢


2

希望从C编程101类型的角度来回答这个问题。

在我看来,它是以这种顺序发生的:

  1. i被评估为0,导致i = 0 + 0递增操作i++“已排队”,但是0的分配i也尚未发生。
  2. i++发生增量
  3. i = 0上面的分配发生了,有效地覆盖了#2(后增量)将要完成的所有操作。

现在,#2可能永远不会真正发生(可能不会吗?),因为编译器可能意识到它将毫无用处,但这可能取决于编译器。无论哪种方式,其他更丰富的答案都表明结果是正确的,并且符合C#标准,但是并没有定义C / C ++会发生什么。

方式和原因超出了我的专业知识,但是先前评估的右侧分配发生在后增量之后的事实,这可能使这里感到困惑。

而且,除非您做了++i而不是i++相信,否则您不会期望结果为2 。


1
预增量版本2使用C ++ 生成结果:ideone.com/8dH8tf
Steven Lu

这就说得通了。但是,增量前的情况比增量后的情况稍微复杂一些。
gkimsey 2012年

2

简单的说,

i ++,将在“ + =”运算符完成后将1加到“ i”。

您想要的是++ i,因此它将在执行“ + =”运算符之前将1加到“ i”。


0
i=0

i+=i

i=i+1

i=0;

然后将1加到i

i + = i ++

因此,在将1加到之前ii将值设为0。只有在将1加到之前,才将值设为i0。

i+=++i

i=2

-4

答案是i1

让我们看看如何:

原来 i=0;

然后,i +=i++;根据的值进行计算时,我们将得到0 +=0++;,因此根据运算符优先级0+=0将首先执行,结果将是0

然后,增量运算符将应用为0++0+1而的值i将为1


3
这个答案是错误的。您将不会得到1,因为当您执行0 += 0++;赋值操作时是在增量之后,++但i的值之前是++(因为是后置运算符。)
PhoneixS 2012年

2
抱歉,这是不正确的。阅读我的问题,你会看到我说,结果为0如果您运行的代码,你会看到它实际上是0
彼得·
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.