【补充声明】此文完成于几年前回答 BCCN 论坛的网友提问,就问题本身而言,对于这个问题似乎是没必要深究的,因为这种代码在读取一个变量的值的过程中反复尝试修改它的值,其结果依赖编辑器的实现。这种代码当然也是不可能在现实应用中出现的。不过作为一个问题,如果他一定要问,某编译器为什么会给出这样的结果,那就必须了解编译器对这个代码的编译结果细节,这就是本文所论述的东西。本文只涉及到了 TC2, VC6, VC2005 几种编译器。而且后两者应该使用的 WIN32 DEBUG,通常 Release 版本和 Debug 版本是一种在运行结果表现上的等效关系,对这个具体问题在当时我并未有精力再去细分。此次更新编辑顺便修改了原文中的个别错别字。原文中的术语“堆栈”修改为“栈”。原文中的一些中间结论不够准确,但暂未删除,已经用删除线做了处理。
---- hoodlum1980 ,2012年9月10日。
首先我们来看一下这个问题的提出,来自于一个网友的提问:
http://bbs.bccn.net/thread-200774-1-1.html
----------------------------------------------------------------------------------------------------------
求教大家,简单问题,但为什么是这样的结果?(vc6.0)
很简单的程序
void main()
{
int i=8;
printf("%d,%d,%d,%d\n", ++i, --i, i++, i--);
}
但是结果为(8 7 8 8)无论是从左到右顺序求值还是从右到左顺序求值都不应该是这个结果吧?
我觉得从左到右应该是(9 8 8 9 )从右到左是(8 7 7 8),
是我的错还是编译器的原因?如果是从右到左顺序求值,为什么结果不是(8 7 7 8)而是(8 7 8 8)
请大家指点一下!
[ 本帖最后由 默默无纹 于 2008-2-24 21:04 编辑 ]
-----------------------------------------------------------------------------------------------------------
在这里我使用了VS.NET2005编译的结果是:8,8,7,8。用TC2.0编译的结果是:8,7,7,8。VC6.0我没有安装,所以没有试过,也没办法分析。
这里我们可以看到,由不同的编译器产生了不同结果,可见这个问题是依赖编译器的理解和实现的。换句话说,对于 i++ 和 ++i 的处理在这里是有歧义的,当然在自己应用中我相信也不会有任何人写出这样的代码。但是作为一个问题,我们有必要分析一下不同编译器究竟如何理解i++和++i操作符的。
我们在学习C的时候,应该已经大概知道了 i++ 和 ++i 两者的区别,即“++”符号在 i 之前还是之后,决定了 i 自增操作和他的语句的执行顺序的关系。即i++,理解为i在其语句中取原始值,++i在其语句中取自增后的新值。这一点是毫无疑义的。但是问题在于,网友的问题中又涉及到了 i++,++i 在作为参数时候的处理,所以这时候我们就会感到困惑,i++ 和 ++i 在作为参数的时候,和进入栈的顺序之间有何关系呢?根据前面的实验,可见TC2.0和VS.net2005的处理不同,可见两者对其处理不同,那么造成这种不同的结果的原因是什么呢?我们从代码上无法看到差异,因此我们必须看汇编语言才能知晓,编译器到底把我们的代码翻译成了什么样子。下面我采用IDA反汇编编译器把生成的.exe文件,结果如下:
VS.NET 2005的代码
mov [ebp+var_8], 8 //i=8
.text:004113E5 mov eax, [ebp+var_8] //
.text:004113E8 mov [ebp+var_D0], eax //i--之前把i的值保存到,temp[0]=8
.text:004113EE mov ecx, [ebp+var_8]
.text:004113F1 sub ecx, 1 //i--,(从右向左数第一个参数)
.text:004113F4 mov [ebp+var_8], ecx //i=7
.text:004113F7 mov edx, [ebp+var_8]
.text:004113FA mov [ebp+var_D4], edx //i++之前把i保存到:temp[1]=7 (这时候已经执行过i--了)
.text:00411400 mov eax, [ebp+var_8] //i++,(从右向左数第二个参数)
.text:00411403 add eax, 1
.text:00411406 mov [ebp+var_8], eax //i=8
.text:00411409 mov ecx, [ebp+var_8]
.text:0041140C sub ecx, 1 //--i, 无需保存修改前的值,直接改变实参i
.text:0041140F mov [ebp+var_8], ecx //i=7
.text:00411412 mov edx, [ebp+var_8]
.text:00411415 add edx, 1 //++i,
.text:00411418 mov [ebp+var_8], edx //i=8
.text:0041141B mov esi, esp
.text:0041141D mov eax, [ebp+var_D0] //压栈temp[0]=8
.text:00411423 push eax
.text:00411424 mov ecx, [ebp+var_D4] //压栈temp[1]=7
.text:0041142A push ecx
.text:0041142B mov edx, [ebp+var_8] //压栈i=8
.text:0041142E push edx
.text:0041142F mov eax, [ebp+var_8] //压栈i=8
.text:00411432 push eax
.text:00411433 push offset aDDDD ; "%d,%d,%d,%d\n" //压栈字符串"%d,%d,%d,%d\n"的地址
.text:00411438 call ds:printf //调用打印函数,输出8,8,7,8
.text:0041143E add esp, 14h
.text:00411441 cmp esi, esp
.text:00411443 call sub_41113B
.text:00411448 mov esi, esp
可见,++i和--i执行的时候直接改变了i的值,而i++和i--必须在所在的这个语句执行后才能改变i的值,所以i++作为参数时,实际上是这样的过程,
printf("%d",i++);
相当于下面的语句:
int temp=i;
i = (i+1);
printf("%d",temp);
因此上面的代码可以翻译为:
int i=8;
printf("%d,%d,%d,%d",++i,--i,i++,i--);
因此可以翻译为下面的等效代码:
i=8;
temp0=i; //temp0=8;
i--; //7
temp1=i; //temp1=7
i++; //8
--i; //7
++i; //i=8
printf("%d,%d,%d,%d",i,i,temp1,temp0);
所以打印结果是8,8,7,8
---------------------------------------------------------------------------------------------------------
我们再看在TC2.0下的反汇编代码:
TC2.0下面的反汇编代码
----------------------TC2.0反汇编结果--------------------
0:01FA sub_1FA proc near ; CODE XREF: start+11A