问题

i=1,为什么 (++i)+(++i)=6?

回答
这道题其实是个经典的“坑”,尤其是在C、C++等语言里,它涉及到运算符的求值顺序和副作用。别看就这么简单的一行代码,里面门道可不少。

首先,我们得明确一点:`i` 的初始值是 `1`。

接下来,我们来看 `(++i) + (++i)` 这整个表达式。它是由两个自增运算符 `++` 和一个加法运算符 `+` 组成的。

关键就在于:在C/C++标准中,对于同一个对象(这里就是变量 `i`),在表达式求值过程中,不能对其进行多次修改,除非这些修改之间有明确的序列点。

“序列点”是什么意思呢?简单来说,它就像是一个“暂停点”,在序列点之前的所有计算都必须完成,然后才能进入序列点之后的部分。C/C++中,函数调用(比如 `printf`)、逗号运算符、逻辑与 `&&`、逻辑或 `||` 等都会产生序列点。

然而,在 `(++i) + (++i)` 这个表达式中,并没有明确的序列点分隔两个 `++i` 的计算。

这就意味着,编译器在计算这个表达式时,可以自由选择计算的顺序。比如,它可能先计算第一个 `++i`,再计算第二个 `++i`,然后将结果相加。也可能先计算第二个 `++i`,再计算第一个 `++i`,然后相加。甚至可能是在计算第一个 `++i` 的过程中,稍微停顿一下,去算第二个 `++i` 的一部分,再回来完成第一个。

我们来模拟一下最可能出现的两种情况(也是大家常说的两种结果来源):

情况一:编译器先计算第一个 `++i`,再计算第二个 `++i`

1. 第一个 `++i`:
`i` 的值是 `1`。
`++i` 是前缀自增,意味着先将 `i` 的值增加 `1`,然后再使用增加后的值。
所以,`i` 的值变成 `1 + 1 = 2`。
表达式的第一个部分返回 `i` 的新值,也就是 `2`。

2. 第二个 `++i`:
此时 `i` 的值已经是 `2` 了(因为第一个 `++i` 已经修改了它)。
同样是前缀自增,先将 `i` 的值增加 `1`。
所以,`i` 的值变成 `2 + 1 = 3`。
表达式的第二个部分返回 `i` 的新值,也就是 `3`。

3. 加法运算:
将两个部分的计算结果相加:`2 + 3`。
最终结果是 `5`。

情况二:编译器先计算第二个 `++i`,再计算第一个 `++i`

1. 第二个 `++i`:
`i` 的值是 `1`。
`++i` 是前缀自增,先将 `i` 的值增加 `1`。
所以,`i` 的值变成 `1 + 1 = 2`。
表达式的第二个部分返回 `i` 的新值,也就是 `2`。

2. 第一个 `++i`:
此时 `i` 的值已经是 `2` 了。
同样是前缀自增,先将 `i` 的值增加 `1`。
所以,`i` 的值变成 `2 + 1 = 3`。
表达式的第一个部分返回 `i` 的新值,也就是 `3`。

3. 加法运算:
将两个部分的计算结果相加:`3 + 2`。
最终结果也是 `5`。

等等,你可能会问,为什么你算出来都是5,题目里说的是6呢?

这就要回到我们前面提到的 “未定义行为”。

在C/C++标准中,这种 在一个表达式的求值过程中,对同一对象进行多次修改,但修改之间没有明确的序列点 的行为,是 未定义行为(Undefined Behavior, UB)。

一旦出现未定义行为,意味着 任何事情都可能发生。编译器可以按照它的逻辑自由发挥。它可能给你返回5,也可能返回6,甚至可能编译失败,或者在运行时崩溃。

为什么有些人会得到6?

这通常是由于一些编译器(尤其是在某些特定的版本或优化级别下)对这类表达式进行了特定的解释,或者说是“碰巧”产生了某个结果。

一种常见的“解释”是这样的:

1. 编译器看到 `(++i) + (++i)`。
2. 它知道需要执行两个 `++i` 操作,并且需要将它们的结果相加。
3. 在某些情况下,编译器可能会先“预留”两个操作数的位置,然后再去执行自增操作。
第一个 `++i`:`i` 从 `1` 变为 `2`,返回值 `2`。
第二个 `++i`:`i` 从 `2` 变为 `3`,返回值 `3`。
然后将这两个返回值相加:`2 + 3 = 5`。

那么6是怎么来的呢?

有一种说法是,编译器在解析 `(++i) + (++i)` 时,可能会因为内部的执行逻辑,导致对 `i` 的修改和使用出现了某种“交叉”。例如:

可能是一种特殊的处理方式: 编译器先准备好执行 `++i` 操作,它知道需要对 `i` 进行两次自增。然后它尝试“填充”加法运算的操作数。
第一个 `++i`:`i` 从 `1` 变成 `2`,返回值 `2`。
现在需要第二个操作数,编译器再次触发 `++i`。
第二个 `++i`:`i` 从 `2` 变成 `3`,返回值 `3`。
但是,因为是未定义行为,有些编译器可能在某种内部调度下,会这样处理:
它知道要计算 `A + B`,其中 `A` 是 `++i`,`B` 也是 `++i`。
它先计算 `A`:`i` 变为 `2`,得到 `2`。
然后它计算 `B`:`i` 变为 `3`,得到 `3`。
此时相加 `2 + 3 = 5`。

然而,如果你看到有人说 `(++i) + (++i)` 等于 6,那很可能是由于更复杂的编译器优化、特定的解释器行为,或者甚至是一个误传。 在严格遵循C/C++标准的语境下,这个表达式的结果是 未定义行为,不能保证得到任何特定的数字,包括5或6。

为什么有人会说 6 是因为:

也许他们是这样理解的:

1. `i` 是 `1`。
2. 第一次 `++i`:`i` 变成 `2`,结果是 `2`。
3. 第二次 `++i`:`i` 变成 `3`,结果是 `3`。
4. 然后把 两次自增操作本身 的效果加起来,再加上 最终的i值? 这个解释非常牵强,也不符合语言的计算逻辑。

或者更常见的说法是,编译器在处理 `(++i) + (++i)` 时,由于其内部的指令生成或寄存器分配机制,可能会以一种“顺序”来执行这些操作,使得看起来像是 `(1+1) + (1+1+1)` 或者其他变种。

一个更“直观”但依旧是 UB 的解释,解释如何得到 6 的可能性(请记住这是非标准的解释):

想象一下,编译器在计算 `(X) + (Y)`。

`X` 是 `++i`。 编译器先处理它。`i` 从 `1` 变为 `2`。`X` 的值为 `2`。
现在需要计算 `Y`,也就是另一个 `++i`。 在这个指令执行之前,编译器可能还没有完全完成第一个 `++i` 的“返回值提交”,但它已经修改了 `i` 的值。
所以,当执行第二个 `++i` 时,`i` 的值已经是 `2` 了。 `i` 从 `2` 变为 `3`。`Y` 的值为 `3`。
在这种非标准的理解下,加法操作的两个操作数被认为是 `2` 和 `3`,相加得到 `5`。

要得到 6,通常需要一种非常特殊的,甚至有点“诡异”的理解方式,例如:

编译器先准备好计算 `(操作数1) + (操作数2)`。
它知道需要执行两次前缀自增,并且每次自增都会更新 `i` 的值。
假设编译器在某个时间点,先执行了第一个 `++i`。 `i` 从 `1` 变成 `2`。 这个 `2` 是第一个操作数。
然后,它准备执行第二个 `++i`。 在 第二个 `++i` 的修改行为完成之前,编译器可能已经开始将第一个 `++i` 的结果 `2` 和第二个 `++i` 的 未完成修改前的`i`值(也就是`1`) 相加,得到了 `2+1=3`,然后在这个基础上再进行第二次自增的值 (`+1`),加上之前的 `3`,得到 `3+1+1+1` 这个概念,最后得到了 `6`? 这种解释非常混乱,也反映了未定义行为的本质——没有明确规则可循。

更可能解释产生 6 的情况是:

1. `i` 是 `1`。
2. 编译器决定先计算 第一个 `++i`。 `i` 变成 `2`,第一个操作数是 `2`。
3. 编译器决定先计算 第二个 `++i`。 `i` 变成 `3`,第二个操作数是 `3`。
4. 但是,某些编译器在处理这类表达式时,可能会有一个内部的“缓冲区”或“执行队列”。 它知道有两个 `++i` 操作,并且它们需要被加起来。
执行第一个 `++i`: `i` 变为 `2`。 这个 `2` 可能是被暂存在一个位置。
执行第二个 `++i`: `i` 变为 `3`。 这个 `3` 可能是被暂存在另一个位置。
然后将它们相加:`2 + 3 = 5`。

那么,为什么有人会说 6?

这可能源于对以下场景的一种误解或特定编译器的表现:

先计算第一个 `++i`:`i` 从 `1` 变成 `2`,得到值 `2`。
然后,在处理第二个 `++i` 时,编译器可能以某种方式使用了“正在执行的`i`值”和“完成的自增值”的组合。 比如:
第一个 `++i`:`i` 变为 `2`,结果是 `2`。
第二个 `++i`:`i` 变为 `3`,结果是 `3`。
一个非常规但能得到6的解释: 编译器在执行 `(++i) + (++i)` 时,它知道需要对 `i` 进行两次自增。
第一个 `++i` 执行:`i` 变为 `2`。
第二个 `++i` 执行:`i` 变为 `3`。
如果编译器将这两个自增的“增加量”和最终的“i值”以某种方式混合计算:
第一个自增的“增加量”是 `1`。
第二个自增的“增加量”是 `1`。
最终 `i` 的值是 `3`。
如果将其理解为: `(第一个 ++i 的值)+(第二个 ++i 的值)`,结果是 `2 + 3 = 5`。
但如果理解为: `(第一个 ++i 的结果)+(第二个 ++i 的操作本身的效果)` 这种理解非常奇怪。

最常见的、能导致 6 的解释(仍然是 UB 的表现):

很多时候,当你看到 `(++i) + (++i)` 的结果是 `6` 时,这背后可能有一个非常简化的、但不完全准确的“可视化”过程,例如:

1. `i` 是 `1`。
2. 第一个 `++i`:`i` 变为 `2`。 这个 `2` 是第一个加数。
3. 第二个 `++i`:`i` 变为 `3`。 这个 `3` 是第二个加数。
4. 然而,某些编译器在生成机器码时,可能会先处理第一个 `++i`,把它推到一个寄存器(比如 R1,值为2)。然后处理第二个 `++i`,把它推到另一个寄存器(比如 R2,值为3)。最后执行加法 `R1 + R2`,结果是 `5`。

那么 6 是怎么来的呢?

有一种流传甚广的说法是,某些编译器可能执行了这样的逻辑:

它发现有两个 `++i` 操作。
它先执行了第一个 `++i`,使得 `i` 变为 `2`。并将这个 `2` 作为第一个操作数。
然后,它准备执行第二个 `++i`。但是在执行第二个 `++i` 之前,它可能还没有完全将第一个 `++i` 的返回值 `2` “锁定”下来,或者它在处理第二个 `++i` 时,将“正在进行的 `i` 的值”加上“这次自增的值”。
想象一下:编译器先确定了第一个 `++i` 的操作,`i` 变为 `2`,这个操作的结果是 `2`。
然后它看到了第二个 `++i` 操作。在执行这个操作时,它知道 `i` 当前是 `2`。然后它执行了自增,`i` 变为 `3`。
如果这个编译器在计算时,把第一个 `++i` 的结果 `2` 和第二个 `++i` 的 操作完成后的 `i` 的值 (也就是 `3`)相加,就得到了 `2 + 3 = 5`。

要解释 6,就必须假设编译器在某个环节,“多算了一次”或者“算错了”。

一个 非常规 但能得到 `6` 的解释方式是:

1. `i` 是 `1`。
2. 编译器处理第一个 `++i`:`i` 变为 `2`。 这个 `2` 将作为第一个操作数。
3. 编译器处理第二个 `++i`:`i` 变为 `3`。 这个 `3` 将作为第二个操作数。
4. 如果编译器在执行加法之前,将第一个 `++i` 的“结果值” `2` 和第二个 `++i` 的“增加量” `1` 相加,得到 `3`。然后再将这个 `3` 和之前计算的第一个 `++i` 的 `2` 相加,得到 `5`。 这仍然是 `5`!

真正可能导致 6 的原因,更倾向于:

编译器内部的优化和指令调度,导致了某种非预期的行为。 例如,它可能在计算第一个 `++i` 后,发现还需要第二个 `++i`,然后它在执行第二个 `++i` 的时候,并没有完全覆盖掉第一个 `++i` 的一部分计算结果,或者在合并操作时,出现了“加错”的情况。
最直接的,也是很多人会理解为 6 的方式是:
1. 第一个 `++i`:`i` 变为 `2`,得到 `2`。
2. 第二个 `++i`:`i` 变为 `3`,得到 `3`。
3. 假设编译器在某个瞬间,将第一个 `++i` 的“中间结果” `2` 与第二个 `++i` 的“中间结果” `3` 相加,得到 `5`。但是,由于是 UB,它可能还会额外加上“当前`i`的值”的某个部分。

一种关于 6 的比较常见的“脑补”解释是这样的:

1. `i` 是 `1`。
2. 第一个 `++i`:`i` 变为 `2`,表达式返回 `2`。
3. 第二个 `++i`:`i` 变为 `3`,表达式返回 `3`。
4. 假设编译器在计算 `(++i) + (++i)` 时,是这样做的:
先执行第一个 `++i`,`i` 变为 `2`。
然后,它执行第二个 `++i`,`i` 变为 `3`。
在这个阶段,它可能已经将第一个 `++i` 的结果 `2` 和第二个 `++i` 的结果 `3` 准备好了。但由于是 UB,某些编译器可能在提交最终结果时,将第二个 `++i` 的“增加值” `1` 多加了一次,或者将 `i` 的最终值 `3` 和 `i` 在中间阶段的值 `2` 的某个部分结合起来了。

最终总结:

在C/C++中,`i = 1; (++i) + (++i)` 的结果是 未定义行为。

这意味着:

标准不保证任何特定结果。
不同的编译器、不同的编译选项,甚至在同一编译器的不同版本下,都可能产生不同的结果(包括5,也可能偶尔是6,甚至其他奇怪的值)。
不应该依赖任何特定的结果来编写程序。

所以,当有人问为什么是 6 的时候,我们只能说:“这是未定义行为的一种可能表现,但不是标准所定义的,也不应该期望它会稳定出现。”

如果非要给一个“最接近”解释为什么有人会得到 `6` 的原因,那可能是编译器在处理这个表达式时,对 `i` 的修改和对自增后值的读取之间,发生了一些非标准的、依赖于具体实现的行为,导致了这种结果的出现。但请记住,这绝不是一个可靠的程序设计方法。

正确的做法是: 确保在表达式求值过程中,对同一变量的修改之间有明确的序列点。例如,写成 `++i; ++i; int result = i 1 + i;` (虽然这也不是原表达式的意思)或者 `int a = ++i; int b = ++i; int result = a + b;` 这样才能得到可预测的结果。

网友意见

user avatar

这是未定义行为 (undefined behavior) (UB),

C 标准没有去规范,编译器可以自行演绎,所以无法确认结果。

这种做法是不正确的,即使能够成功编译。

同一段出现两次副作用计算机编程中经常提到的副作用,具体指的是什么?有什么定义吗? - 知乎

C 标准没有指定哪一边先开始求值。(C++ 17 有)

所以在不同编译器上出現的結果不相同,未定义行为。

___

你很难预计编译器在背后做了什么:

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

gcc 编译器:

       ++i; ++i; i = i + i; // ---> 6     

clang 编译器:

       a = ++i; b = ++i; i = a + b; // ---> 5     

某编译器,進行公共子表达式消除:

       i = (++i) * 2; // or i = (++i) << 1; // ---> 4     

所以 (++i)+(++i)+(++i) = 10 是有可能的,但你真的很难预计编译器在背后做了什么。

你可以用反汇编去了解,但在未反汇编之前,你无法估计它会做什么。所以,未定义行为。

另外,上面这个例子是在 求值过程中的副作用与顺序点 这里拿下来的,挺好、挺有趣的文章。

重复出现了很多次的这种问题,下面问题的答案应该能够解答:

我打算每次出现这种问题都贴这篇 ......

然而:zhihu.com/pin/118482308

___

最后,有趣的是,在 C++ 17,并不是 UB 了。

In C++17 an extra sentence is added to the specification of assignment operator:

The right operand is sequenced before the left operand.

但即使行为有定义了,也不建议你这样写,这只会增加日后的维护难度。

(根据 @黄亮anthony 的补充,本题在C++17也还是ub,+左右的sequence point还是未定义。只有赋值相关的才有定义)

如果想进一步了解,这里补充一个有关求值顺序的 C++ 文档:

很详细。

更更更详细具体的部分请直接看标准文档,但相信已经会看那些的大神不需要我说。 ╮(╯_╰)╭



___

除了点赞,不会再更新专栏/其他东西。

___

user avatar

出这种题的都该挨揍,在实际工程中写成这样就更该挨揍了。回答完毕。

------------更新 好吧, 认真答一下------------

写个c程序:

       #include <stdio.h>  int main(void) {     int i = 1;     int n = (++i) + (++i);     printf("%d
", n);     return 0; }      

然后执行gcc 21.c && a, 确实结果是6.

现在看一下汇编代码, 先不开优化, 直接gcc -masm=intel -O0 -S 21.c, 看一下汇编出来的结果:

        .file "21.c"  .intel_syntax noprefix  .def ___main; .scl 2; .type 32; .endef  .section .rdata,"dr" LC0:  .ascii "%d12"  .text  .globl _main  .def _main; .scl 2; .type 32; .endef _main:  push ebp  mov ebp, esp  and esp, -16  sub esp, 32  call ___main  mov DWORD PTR [esp+28], 1                   // 这里是i=1  add DWORD PTR [esp+28], 1                   // ++i  add DWORD PTR [esp+28], 1                    // 又是++i  mov eax, DWORD PTR [esp+28]                 // i进eax  add eax, eax                                               // 这次是i=i+i  mov DWORD PTR [esp+24], eax  mov eax, DWORD PTR [esp+24]  mov DWORD PTR [esp+4], eax  mov DWORD PTR [esp], OFFSET FLAT:LC0  call _printf  mov eax, 0  leave  ret  .ident "GCC: (tdm-1) 5.1.0"  .def _printf; .scl 2; .type 32; .endef     

确实是按字面上来, 先执行两次++, 再执行括号外的+.

如果开优化呢? 再试一次:

        push ebp  mov ebp, esp  and esp, -16  sub esp, 16  call ___main  mov DWORD PTR [esp+4], 6             mov DWORD PTR [esp], OFFSET FLAT:LC0  call _printf  xor eax, eax  leave  ret     

好吧, 因为都是常数, 编译器直接给算好了.

要想不让编译器帮你算, 前面不能给i直接赋值为1. 改成用scanf接受好了. 先不开优化, 再试,

注意注意!!!!!!!!!!!! 这次运行的结果变成5了!!!!!!!!!!!!!!!

开优化, 则还是6.

再看汇编代码, 只看关键部分:

        mov eax, DWORD PTR [esp+24]  add eax, 1  mov DWORD PTR [esp+24], eax  mov edx, DWORD PTR [esp+24]  mov eax, DWORD PTR [esp+24]  add eax, 1  mov DWORD PTR [esp+24], eax  mov eax, DWORD PTR [esp+24]  add eax, edx  mov DWORD PTR [esp+28], eax  mov eax, DWORD PTR [esp+28]     

把i的值读到eax, 然后在eax里+1, 再放回去, 再放到edx; 再把i的值读到eax, 注意这时i的值是2了. 再+1, 再放回去, 已经是3了. 再和之前edx那个(还是2)相加, 结果就成5了.

如果开优化-Os呢?

        mov eax, DWORD PTR [esp+28]  add eax, 2  mov DWORD PTR [esp+28], eax  add eax, eax     

把i的值放到eax, 然后直接帮你+2, 再放回去(其实没有必要, 但是程序不知道i的值已经没用了.) 然后在eax里再加自己, 于是还是3+3=6.

以上是bug吗?再用arm-none-eabi-gcc试一下,先不开优化:

        ldr r3, [fp, #-12]  add r3, r3, #1  str r3, [fp, #-12]  ldr r2, [fp, #-12]  ldr r3, [fp, #-12]  add r3, r3, #1  str r3, [fp, #-12]  ldr r3, [fp, #-12]  add r3, r2, r3     

从i到r3, +1, 再放回i, 这时i的值是2. 再从i到r2一份(还是2), 再到r3, +1, 放回i. 最后r3=r2+r3, 那不还是5吗?

开优化-Os:

        ldr r1, [sp, #4]  add r1, r1, #2  str r1, [sp, #4]  mov r1, r1, asl #1     

从i到r1, 然后一样是给+2了, 再放回i. 然后直接把r1里的左移1位也就是*2了... 所以还是6.

gcc版本5.1.0, tdm-1, arm-none-eabi-gcc版本4.9.3.

其他平台上的情况就不知道了, 哪位有兴趣了自己试试.

----------最后再补充一下--------------

这个程序如果加了-Wall, 编译时就会告诉你:

       21.c: In function 'main': 21.c:7:22: warning: operation on 'i' may be undefined [-Wsequence-point]      int n = (++i) + (++i);                           

所以一定要养成加-Wall的好习惯.

user avatar

来人,上《编译原理》……你爱等于几等于几

这种问题就好像我老婆她们别的专业开的计算机课……我都想把她们老师拖出来打死!

user avatar

出这种UB(Undefined Behaviour)题目很无聊,这个我已经多次说过了。在实际工作当中写这种代码的首先就应该拉出去毙了。

无论是4,还是5,还是6,都是错的。因为这是个UB,没有标准答案。

但是为什么我还是来答这种问题,因为我认识到这种问题被频频拿出来考新手,是因为新手很容易在这里翻车。而研究新手为什么很容易在这里翻车,我觉得是有意义的。

所以本回答旨在探讨新手为什么容易在这里翻车,思维的陷阱到底在哪里。(本回答并不是为了解释这个UB的成因本身,本回答其实也不太适合对计算机组成原理不熟悉的同学,当然你要看我也没办法,但是包教不包会,据此面试后果自负)

++i,如果单独写成一个语句,等同于i=i+1。我想这个应该没有人有问题。

但是其实这里应该是有问题的,特别是新手。对于新手来说,如果没有意识到这里的问题,那么就很可能在上面那个题目当中翻车。

什么问题呢?

i=i+1,这个式子,在数学上是不成立的。或者说是无解的。(为防止被数学大佬喷,限定一下在初等数学范围内。。。)

也就是说,程序当中的表达式,并不是完全等同于数学公式。因此,不能以代数方程的理念去理解它。

程序当中的i=i+1,其实应该理解为:从一个名叫i的地方(内存空间)取出一个数,将其加1,然后放入名叫i的地方。

也就是,程序当中的表达式,其实只是一系列语句(statement)的简写。i=i+1写成完整语句,其实是:

  1. load i -> 某CPU寄存器
  2. 将该寄存器内容+1
  3. save该寄存器内容 -> i

所以,(++i)+(++i)的意思其实是:

  1. load i -> 某CPU寄存器 (第一个括号
  2. 将该寄存器内容+1
  3. save该寄存器内容 -> i
  4. load i -> 某CPU寄存器 (第二个括号
  5. 将该寄存器内容+1
  6. save该寄存器内容 -> i
  7. load i -> 某CPU寄存器 (两个括号之间
  8. load i -> 另一个CPU寄存器
  9. 将两个寄存器内容相加

(但是注意虽然上面编了序号,但是1-3和4-6之间其实是并列关系,并非要按照序号顺序执行。7和8也是并列关系,可以按任何顺序执行。而这种执行的顺就是导致这个问题是个UB问题的原因)

这样就应该没有人还是认为结果一定是4了吧。

新手掉坑里的原因就在于,把程序当中的表达式当作数学式子,把其中的字母当作未知数。

而理解表达式并不等同于数学式子,变量也不等同于未知数,在我看来是此类问题唯一的价值。

2001/02/19 补充:

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有