对同一个程序在VC6和VS2005中结果不一样的分析

杨全胜

最近,一个学生在网上问到一道C语言题目如下
#include <conio.h>
#include <stdio.h>

int add(int x,int y){
return(x+y);
}

int main( )
{
int x=2;
int y=2;
int a=add(add(x++,y++),add(--x,--y));
printf("%d\n",a);
printf("%d\n",x);
_getch();
return 0;
}

     做这道题其实不难,关键是要要弄清楚C语言调用函数的方法以及自增和自减运算的规则就不难做出来。C语言在进行函数调用的时候,对于参数的处理是通过堆栈来进行参数传递的,但与Pascal不同的是,C语言是从最右边的参数开始,将参数压入堆栈,而Pascal是从左边的参数开始的。因此,在做a=add(add(x++,y++),add(--x,--y));这一句的时候,是先处理--y,然后处理--x,接着调用add(--x,--y);由于这里两个参数都做前缀自减运算,所以,x和y先做减一运算,然后再带入到函数的参数中,这样一来add(--x,--y)执行完,函数的结果是1+1=2;而x=1,y=1;之后,考察y++和x++,由于都是后缀自加运算,所以要先使用x与y做参数调用add(x++,y++),然后再对x和y做加一运算,因此add(x++,y++)函数的结果是1+1=2;而调用过后x=2,y=2;这样最后的add调用的时候,得出的结果就是2+2=4,加数和被加数的两个2就是前面两次调用add的结果。因此最终的结果a=4;x=2。

    如果我们在Turbo C或者VC6中编译和执行这段程序,会看到结果和我们分析的是一样的。然而如果你在VS2005(包括VS2002)中执行这段程序,却发现得到的结果是a=6,x=2。结果居然不一样,到底问题出在哪里?让我们来分析一下VC6和VS2005各自对int a=add(add(x++,y++),add(--x,--y))这条语句的翻译结果吧。

    下面是VC6翻译的结果,后面的注释为本人所加:

14: int x=2;
00401068 mov dword ptr [ebp-4],2        ;存x(2)
15: int y=2;
0040106F mov dword ptr [ebp-8],2        ;存y(2)
16: int a = add(add(x++,y++),add(--x,--y));
00401076 mov eax,dword ptr [ebp-8]    ;取出y
00401079 sub eax,1                                ;y-1
0040107C mov dword ptr [ebp-8],eax    ;从新保存y,这几句完成了--y,y此时等于1
0040107F mov ecx,dword ptr [ebp-8]    ;将做过--y的y值(1)取出
00401082 push ecx                                  ;新y值压栈(作为参数传递)
00401083 mov edx,dword ptr [ebp-4]    ;取出x
00401086 sub edx,1                                ;x-1
00401089 mov dword ptr [ebp-4],edx    ;从新保存x,这几句完成了--x,x此时等于1
0040108C mov eax,dword ptr [ebp-4]]    ;将做过--x的x值(1)取出
0040108F push eax                                  ;新x值压栈(作为参数传递)
00401090 call @ILT+15(add) (00401014);调用add(--x,--y)
00401095 add esp,8                                  ;调整堆栈(原来的参数退出但不保留)
00401098 push eax                                    ;add(--x,--y)的返回值2在eax中,将其压栈
00401099 mov ecx,dword ptr [ebp-8]       ;取出新y值(1)
0040109C mov dword ptr [ebp-10h],ecx   ;转存新y值,因为增1是后缀运算符,所以先不加
0040109F mov edx,dword ptr [ebp-10h]
004010A2 push edx                                    ;新y值压栈,作为函数参数传递
004010A3 mov eax,dword ptr [ebp-4]       ;取出新x值(1)
004010A6 mov dword ptr [ebp-14h],eax   ;转存新x值,因为增1是后缀运算符,所以先不加
004010A9 mov ecx,dword ptr [ebp-14h]
004010AC push ecx                                    ;新x值压栈,作为函数参数传递
004010AD mov edx,dword ptr [ebp-4]       ;压栈后,x值再加1,完成x++
004010B0 add edx,1
004010B3 mov dword ptr [ebp-4],edx
004010B6 mov eax,dword ptr [ebp-8]       ;压栈后,x值再加1,完成x++
004010B9 add eax,1
004010BC mov dword ptr [ebp-8],eax
004010BF call @ILT+15(add) (00401014);调用add(x++,y++)
004010C4 add esp,8                                  ;调整堆栈(原来的参数退出但不保留)
004010C7 push eax                                    ;add(x++,y++)的返回值2在eax中,将其压栈
004010C8 call @ILT+15(add) (00401014);调用add(add(x++,y++),add(--x,--y))
004010CD add esp,8
004010D0 mov dword ptr [ebp-0Ch],eax;存放返回值(4)

从上面的分析可以看到,其整个过程和我们上面的分析是基本一样的。下面我们来分析VS2005翻译的情况。

int x=2;
004113FE mov dword ptr [x],2        ;存x(2)
int y=2;
00411405 mov dword ptr [y],2        ;存x(2)
int a=add(add(x++,y++),add(--x,--y));
0041140C mov eax,dword ptr [y]
0041140F sub eax,1                                     ;y减1,做--y
00411412 mov dword ptr [y],eax
00411415 mov ecx,dword ptr [x]
00411418 sub ecx,1                                     ;x减1,做--x
0041141B mov dword ptr [x],ecx
0041141E mov edx,dword ptr [y]
00411421 mov dword ptr [ebp-0E8h],edx    ;减过1的y值暂存
00411427 mov eax,dword ptr [y]
0041142A add eax,1                                     ;减过1的y值,做y++
0041142D mov dword ptr [y],eax                 ;注意,y值已经提前被加1了---<1>
00411430 mov ecx,dword ptr [x]
00411433 mov dword ptr [ebp-0ECh],ecx    ;减过1的x值暂存
00411439 mov edx,dword ptr [x]
0041143C add edx,1                                     ;减过1的x值,做x++
0041143F mov dword ptr [x],edx                 ;注意,x值已经提前被加1了---<1>
00411442 mov eax,dword ptr [y]                 ;
00411445 push eax                                        ;这里压栈的是做过y++以后的y值---<2>
00411446 mov ecx,dword ptr [x]
00411449 push ecx                                        ;这里压栈的是做过x++以后的x值---<2>
0041144A call add (41109Bh)                      ;调用add(--x,--y) ;就是这里使用的参数有问题-<2>
0041144F add esp,8
00411452 push eax
00411453 mov edx,dword ptr [ebp-0E8h] ;此时才把--y的结果拿出来做下一次调用的参数--<3>
00411459 push edx
0041145A mov eax,dword ptr [ebp-0ECh] ;此时才把--x的结果拿出来做下一次调用的参数--<3>
00411460 push eax
00411461 call add (41109Bh)                      ;调用add(x++,y++) ;这里使用的参数还是有问题-<3>
00411466 add esp,8
00411469 push eax
0041146A call add (41109Bh)                      ;调用add(add(x++,y++),add(--x,--y))
0041146F add esp,8
00411472 mov dword ptr [a],eax

     上面的翻译有几个很“意外”的结果,<1>处y和x都被提前加1了,也就是说,这里应该是后缀增1运算的,但却做了前缀增1运算。<2>处,调用的是add(--x,--y),却使用了add(x++,y++)的参数,而<3>处调用的是add(x++,y++),却用了add(--x,--y) 的参数。这样错综复杂的情况交织在一起,就形成了完全不一样的结果。至于<2>处是不是调用的add(--x,--y),而<3>处调用的是不是add(x++,y++),我们可以通过下面这段稍加改动的程序的输出就可以确认是先调用的add(--x,--y)。

#include <stdio.h>
#include <conio.h>

int add(int x,int y,int e){
printf("e=%d\n",e);
return(x+y);
}

int main( )
{
int x=2;
int y=2;
int a=add(add(x++,y++,0),add(--x,--y,1),2);
printf("%d\n",a);
printf("%d\n",x);
_getch();
return 0;
}

输出是:
e=1
e=0
e=2
6
2

输出结果可以看到三个add函数的调用顺序。

进一步的研究发现,当执行add(add(x++,y++),add(x--,y--))和add(add(++x,++y),add(x--,y--))的时候,VS2005的编译没有问题。但当执行add(add(++x,++y),add(--x,--y))的时候(注意右边的add函数又是做前缀自增运算),VS2005的编译又出问题。下面是翻译后的代码:

int x=2;
0041360E mov dword ptr [x],2
int y=2;
00413615 mov dword ptr [y],2
int a=add(add(++x,++y),add(--x,--y));
0041361C mov eax,dword ptr [y]
0041361F sub eax,1
00413622 mov dword ptr [y],eax
00413625 mov ecx,dword ptr [x]
00413628 sub ecx,1
0041362B mov dword ptr [x],ecx
0041362E mov edx,dword ptr [y]
00413631 add edx,1                            ;y自减后没有保存就做自增运算
00413634 mov dword ptr [y],edx
00413637 mov eax,dword ptr [x]
0041363A add eax,1                            ;x自减后没有保存就做自增运算
0041363D mov dword ptr [x],eax
00413640 mov ecx,dword ptr [y]        ;取出准备压入栈的y实际上已经是做过自减与自增运算的y
00413643 push ecx
00413644 mov edx,dword ptr [x]        ;取出准备压入栈的x实际上已经是做过自减与自增运算的x
00413647 push edx
00413648 call add (4111D6h)
0041364D add esp,8
00413650 push eax
00413651 mov eax,dword ptr [y]        ;这里取出要压栈的y和上一个y居然是同一个值
00413654 push eax
00413655 mov ecx,dword ptr [x]        ;这里取出要压栈的x和上一个x居然是同一个值
00413658 push ecx
00413659 call add (4111D6h)
0041365E add esp,8
00413661 push eax
00413662 call add (4111D6h)
00413667 add esp,8
0041366A mov dword ptr [a],eax

VS2005在处理上述情况的时候是将自增和自减运算提前计算后再进行函数调用,但似乎在有些情况下却没有处理好中间结果的保存,除非微软对C++有新的理解。

CopyRight © 2003