由一个自增引发的问题

今天晚上,舍友把我叫过去(他用的是 VC++ 6)问:“这里为什么输出的是16?”

程序非常简单:

#include <stdio.h>

int main() {
int i, j;
i = 3;
j = (++i) + (++i) + (++i);
printf("%d\n", j);
return 0;
}

我看了一下,回答说:“我觉得是15吧。”

然而,运行输出的不是预计的15(=4+5+6),而是16。我尝试去查找原因,却发现这似乎是编译器挖的一个大大的坑。

我先开始做实验,尝试捕捉中间结果。

#include <stdio.h>

int main() {
int i, j, a, b, c;
i = 3;
j = (a = (++i)) + (b = (++i)) + (c = (++i));
printf("%d\na=%d,b=%d,c=%d", j, a, b, c);
return 0;
}

然而,这次的输出又是预料中的:

15
a=4,b=5,c=6

也就是说,这几个理论上不会改变代码执行流程的赋值影响了执行结果。后来发现是第一个赋值引起的,后面两个不影响,所以 a 的值不好捕获(不信可以尝试一下)。

然而,如果是两个自增,结果会为10,其中第二个自增(b)的值为5。

我在出去买夜宵的路上在想,也许是编译器的问题?两次访问 i,实际上只是通过地址?(如下面的伪代码)

add ptr[i], 1
add ptr[i], 1
stack = ptr[i] + ptr[i]

但是优先级被忽略了?第三个自增为什么没有发生这种情况?我认为解释不通。

再一想,不管怎么样,这是编译器说了算,还是直接看编译的结果吧。回来后我就在他的 VC++ 6 中设了一个断点,看看三个自增的情况下会被编译成什么样子,然后就出现了意料之外的结果:

前两个自增虽然用寄存器进行了缓存,却真的出现了 temp = ptr[i] + ptr[i] 的情况(和语义预期不同);第三个自增的结果也用寄存器缓存,不过独立地和前面的结果相加,相当于 temp = temp + exx(和语义预期相同)。也就是说,这三个自增,出现了两种执行方式!

我有时间补上 VC++ 6 的三自增反汇编结果,毕竟我这里已经不再用 VC++ 6 了;舍友都睡觉了。

然后,我在 Visual Studio 2015 上尝试同样的测试(两个自增、三个自增),结果更让我吃惊。

首先是两个自增,结果是10而不是预计的9:

int main() {
int i, j
i = 3;
j = (++i) + (++i);
return 0;
}

反汇编后是这样的:

mov dword ptr [ebp-8],3 ; ebp-8 是 i 的地址
mov eax,dword ptr [ebp-8]
add eax,1
mov dword ptr [ebp-8],eax
mov ecx,dword ptr [ebp-8]
add ecx,1
mov dword ptr [ebp-8],ecx
mov edx,dword ptr [ebp-8]
add edx,dword ptr [ebp-8] ; 注意上面这两行,是谁和谁在相加
mov dword ptr [ebp-14h],edx ; ebp-14h 是 j 的地址

不难看出10是怎么得到的,上面的核心代码和以下的等价:

i = 3;
++i;
++i;
j = i + i;

当然如果开了反汇编窗口中的“显示符号”选项的话,ebp+* 会被换为更清晰易读的形式(ij),不过我这里就用最原始的方式来展现了。

然后是三个自增的情况。代码只是一句略有变化:

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

猜猜这次的结果是多少?是18,不是预期的15,也不是 VC++ 6 中得到的16。看看反汇编之后的代码吧:

mov dword ptr [ebp-8],3
mov eax,dword ptr [ebp-8]
add eax,1
mov dword ptr [ebp-8],eax
mov ecx,dword ptr [ebp-8]
add ecx,1
mov dword ptr [ebp-8],ecx
mov edx,dword ptr [ebp-8]
add edx,1
mov dword ptr [ebp-8],edx
mov eax,dword ptr [ebp-8]
add eax,dword ptr [ebp-8]
add eax,dword ptr [ebp-8]
mov dword ptr [ebp-14h],eax

可以看到,这次的代码和下面一段等价:

i = 3;
++i;
++i;
++i;
j = i + i + i;

这真是超出预料的。在行为上,不仅是各个版本的编译器行为不同,还都和从语法角度推断的结果不一样。真是不知道这是一个 bug,还是我应该再去读一下 C++ 语言规范了。

总之在有定论之前,不要作大死地使用 (++i) + (++i) 这种人机共愤的语句


另外,我并没有在 Visual Studio 2015 中发现明显的控制“反汇编”窗口显隐的菜单项,或许只是因为我的默认界面设置是以 VB 为标准的。在“监视”窗口和“变量”窗口,右键单击都有“转至反汇编”的菜单项,但是按下去没有反应;在“命令”窗口执行 disasm 命令提示无法执行。查阅 MSDN 后发现,要先在[选项]-[调试]-[常规]中打开“地址级调试”选项。选中后就可以正常查看反汇编结果了。

分享到 评论