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

Pet*_*ter 251 .net c#

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

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

结果i是0.我预计2(正如我的一些同事所做的那样).编译器可能会创建某种导致i为零的结构.

我期望2的原因是,在我的思路中,右手语句将首先被评估,用1递增i.比它被添加到i.因为我已经是1,所以它加1比1.所以1 + 1 = 2.显然这不是正在发生的事情.

你能解释一下编译器的作用或运行时会发生什么吗?为什么结果为零?

某种免责声明:我绝对知道你不会(也可能不会)使用这段代码.我知道我永远不会.尽管如此,我发现知道为什么它以这种方式行动以及究竟发生了什么是很有趣的.

Ode*_*ded 425

这个:

int i = 0;
i += i++
Run Code Online (Sandbox Code Playgroud)

你可以看到你做的事情(以下是粗略的过度简化):

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
Run Code Online (Sandbox Code Playgroud)

实际发生的事情比这更多 - 看一下MSDN,7.5.9后缀增量和减量运算符:

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;
Run Code Online (Sandbox Code Playgroud)

  • @Oded实际上是:`int i = 0; i = i + 1; (后缀)i = 0; (赋值)`.如果您在该声明的其他地方使用过i,那么它将在当时评估为1. (6认同)
  • 我不买这个.@yoriy的答案要准确得多.首先,在你的答案中,你说最后一行是"i + 1",而它应该是"i = i + 1".这不是"我的++"吗? (6认同)
  • @Oded在**语句的评估完成之前,'++`操作已完成**.所以`+ =`会覆盖这个值.这是怎么回事? (3认同)
  • 答案的第一部分是多余的.你的上一个代码示例可以完成它恕我直言.虽然+1. (3认同)

Mig*_*elo 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)
Run Code Online (Sandbox Code Playgroud)

等效代码

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

int i, tempVar1, tempVar2;
i = 0;
tempVar1 = i; // created due to postfix ++ operator
tempVar2 = i; // created due to += operator
++i;
i = tempVar1 + tempVar2;
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

打开反汇编窗口

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

在debuging时使用这个:

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

那么postfix ++会发生什么?

postfix ++告诉我们在评估之后我们想要增加操作数的值...每个人都知道......有些混淆是"评估后"的含义.

那么"评估后"意味着什么:

  • 操作数的其他用法,必须在同一行代码中受到影响:
    • a = i++ + i 第二个i受增量的影响
    • Func(i++, i) 第二个我受到了影响
  • 在同一行上对于短路操作者像其他用途||&&:
    • (false && i++ != i) || i == 0 第三个i不受i ++的影响,因为它没有被评估

那么含义是什么:i += i++;

它是一样的 i = i + i++;

评估顺序是:

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

并不是说丢弃了增量.

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

这与前一个示例不同.第三个i受增量影响.

评估顺序是:

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

  • + 1 ++ - 纯粹的核心解剖.查克诺里斯会很自豪:)我猜你确实假设OP在英特尔上,而不是单声道端口虽然...... (22认同)
  • C#具有明确定义的表达式评估顺序,而目标代码只是实现了该顺序.机器代码输出不是评估订单的原因或解释. (19认同)
  • 机器代码可以很容易地理解IMO如何实现评估顺序. (8认同)
  • @StuartLC我看到你在那里做了什么.虽然丢弃了upvote的耻辱. (5认同)
  • `a ++ + a`与`a + a ++`不同,因为这不再是纯数学.代数中的交换法没有考虑变量在表达式中途改变值的可能性.当编程是函数式编程时,数学只能巧妙地映射到编程.即便如此,由于代表性的限制.例如,浮点数有时表现为实数,有时则不然.即使没有副作用,适用于数学中实数的交换性和相关性定律也会突破浮点数. (2认同)

dtb*_*dtb 61

int i = 0;
i += i++;
Run Code Online (Sandbox Code Playgroud)

评估如下:

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
Run Code Online (Sandbox Code Playgroud)

ie i改变了两次:一次是i++表达,一次是+=声明.

+=声明的操作数是

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


Jon*_*ong 36

首先,i++返回0.然后i增加1.最后i设置为初始值为i0加上i++返回的值,也是零.0 + 0 = 0.

  • 但它是`i + = i ++;`,而不是`i = i ++;`,所以`i ++`(0)的值被添加到`i`,而不是``i`被设置为值`i ++`返回".现在的问题是,当将"i ++"的值添加到"i"时,"i"是递增的值还是未递增的值?答案,我的朋友,写在规格中. (2认同)

Kaz*_*Kaz 32

这只是抽象语法树的从左到右,自下而上的评估.从概念上讲,表达式的树是从上到下走的,但随着递归从底部弹回树,评估展开.

// source code
i += i++;

// abstract syntax tree

     +=
    /  \
   i    ++ (post)
         \
         i
Run Code Online (Sandbox Code Playgroud)

评估从考虑根节点开始+=.这是表达的主要组成部分.+=必须对左操作数进行求值以确定存储变量的位置,并获得前一个值为零.接下来,必须评估右侧.

右侧是后递增++运算符.它有一个操作数,i既可以作为值的来源进行评估,也可以作为存储值的地方进行评估.操作员评估i,查找0并随后存储1到该位置.它根据返回先前值0的语义返回先前值.

现在控制权回到了+=运营商身上.它现在拥有完成其操作的所有信息.它知道存储结果的位置(存储位置i)以及先前值,并且它具有要添加到先前值的值,即0.所以,i最终得零.

与Java一样,C#通过修复评估顺序来清理C语言的一个非常狡猾的方面.从左到右,自下而上:编码员可能预期的最明显的顺序.


Yur*_*ich 30

因为i++首先返回值,然后递增它.但在i设置为1后,您将其设置为0.


Ash*_*nko 17

后增量方法看起来像这样

int ++(ref int i)
{
    int c = i;
    i = i + 1;
    return c;
}
Run Code Online (Sandbox Code Playgroud)

所以基本上当你调用时i++,i是递增但是在你的情况下返回原始值,它返回0.


Car*_*arl 12

i ++表示:返回i的值然后递增它.

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

现在,让我们添加i = 0作为起始条件.i + = i ++现在评估如下:

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

注意:在步骤2结束时,i的值实际为1.但是,在步骤3中,通过在i递增之前加载i的值来丢弃它.

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

因此,i + = ++我会给你1.


Pra*_*man 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;
Run Code Online (Sandbox Code Playgroud)


Adi*_*dil 11

后固定增量运算符,++为变量赋予表达式中的值,然后再将指定i的增量返回零(0)值再次覆盖增加的(1),这样就得零.您可以在++ Operator(MSDN)中阅读有关增量运算符的更多信息.


Wes*_*ick 8

i += i++;将等于零,因为它会在++事后发生.

i += ++i; 会以前做的

  • 如果它之后是'++`,我希望结果为'1`. (4认同)

Nat*_*ver 8

++ postfix i在递增之前进行求值,并且+=只计算i一次.

因此,0 + 0 = 0,正如i在递增之前评估和使用的那样,使用后缀格式++.i要先获得增量,请使用前缀form(++i).

(另外,只需注意:你应该得到1,因为0 +(0 + 1)= 1)

参考文献:http://msdn.microsoft.com/en-us/library/sa7629ew.aspx(+ =)
http://msdn.microsoft.com/en-us/library/36x43w8w.aspx(++)


Mig*_*elo 8

C#正在做什么,以及混乱的"原因"

我还预计价值为1 ......但对此问题的一些探索确实澄清了一些观点.

Cosider有以下几种方法:

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

    static int Inc(ref int a) { return a++; }
Run Code Online (Sandbox Code Playgroud)

我希望和我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
Run Code Online (Sandbox Code Playgroud)

但后来我得出了另一个结论...... 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; }
Run Code Online (Sandbox Code Playgroud)

调用之后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
Run Code Online (Sandbox Code Playgroud)

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

  • 请将此添加到您的原始答案中,将其作为单独的答案添加没有任何好处. (2认同)
  • 我这样做不是为了反驳另一个答案,因为它是关于反编译的代码......而在这一个中,我尝试了一种不同的方法来解释事情.你怎么看?我应该编辑其他答案并附加这个答案吗?也许,前置这一个...不知道!谢谢你的建议! (2认同)

Car*_*ten 7

我总是记得一个很好的助记符,如下:

如果++看台的表达,它返回它的值之前.所以下面的代码

int a = 1;
int b = a++;
Run Code Online (Sandbox Code Playgroud)

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

int a = 1;
int b = ++a;
Run Code Online (Sandbox Code Playgroud)

b 在这里是两个.

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

int i = 0;
i += (i++);
Run Code Online (Sandbox Code Playgroud)

i++返回0(如上所述),所以0 + 0 = 0.

i += (++i); // Here 'i' would become two
Run Code Online (Sandbox Code Playgroud)

Scott Meyers在"Effective C++ programming"中描述了这两个符号之间的区别.在内部,i++(postfix)记住值为i,并调用prefix-notation(++i)并返回旧值,i.这就是为什么你应该使用百达++ifor环(虽然我觉得所有的现代编译器转换i++++ifor环).


G.Y*_*G.Y 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++;
Run Code Online (Sandbox Code Playgroud)

如果我们写的话应该是一样的:

int i=0;
i = i + i ++;
Run Code Online (Sandbox Code Playgroud)

因此同样如下:

int i=0;
i= i + i;
i ++;
Run Code Online (Sandbox Code Playgroud)

因此同样如下:

int i=0;
i = i + i;
i = i + 1;
Run Code Online (Sandbox Code Playgroud)

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

现在,在我继续之前,即使是我给出的这组例子也没有得到任何人的支持或承认.但是,根据直觉和理性的方式,这应该是结果.

编码人员应该不知道如何编写或翻译程序集!

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

最后,我从维基百科,递增和递减运算符复制了它:
由于递增/递减运算符修改了它的操作数,因此在同一表达式中多次使用这样的操作数会产生未定义的结果.例如,在诸如x - ++ x之类的表达式中,不清楚应该以什么顺序执行减法和增量运算符.当编译器应用优化时,这样的情况会变得更糟,这可能导致操作的执行顺序与程序员的意图不同.

因此.

正确答案是不应该使用这个!(因为它没有定义!)

是的.. - 即使C#编译器试图以某种方式对其进行标准化,它也具有不可预测的结果.

我没有找到任何C#文档来描述你们所记录的行为,这些行为都是语言的正常或定义明确的行为.我找到的恰恰相反!

[ 从Postfix增量和减量运算符的MSDN文档中复制:++和 - ]

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

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

如果那个答案看起来很傲慢,请原谅我 - 我不是一个傲慢的人.我只想到成千上万的人来这里学习,我读到的答案会误导他们,并会损害他们对这个主题的逻辑和理解.