一道簡單的題目引發的思考

xiumuzi003發表於2016-02-01

轉載:http://www.cnblogs.com/skynet/archive/2010/07/11/1775084.html

一道簡單的題目引發的思考

2010-07-11 05:42 by 吳秦, 5997 閱讀, 25 評論, 收藏編輯

——Don't believe in magic !Understand what your program do ,how they do .

引言

昨晚一時興起,我腦子就問自己下面的程式碼會輸出什麼,也不知道我腦子為什麼有這個程式碼模型,只是模糊的有些印象:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <stdlib.h>
 
int main(int argc,char** argv)
{
    int i=3,j;
    j=(i++)+(i++)+(++i);
    printf("i = %d, j = %d\n",i,j);
    exit(0);
}

您會怎樣考慮這個問題呢?您不執行這個程式能準確地說出答案嗎?我猜想肯定有大部分人不能肯定且準確地說出答案!如果您不能,這篇文章就是為你準備的,保證您看完之後豁然開朗!請細看下文,outline如下:

  • 1、諸君的回答
    • 1.1、A君的回答
    • 1.2、B君的回答
    • 1.3、C君的回答
    • 1.4、D君的回答
  • 2、編譯器的輸出
    • 2.1、Visual Studio的輸出
    • 2.2、GCC的輸出
    • 2.3、Visual C++ 2010的輸出
  • 3、分析
    • 3.1、gcc編譯器上的分析
    • 3.2、分析gcc編譯之後的彙編程式碼
    • 3.3、vs編譯器上的分析
    • 3.4、分析VS編譯之後的彙編程式碼
  • 4、擴散思維
    • 4.1、思維放射
    • 4.2、VS的輸出
    • 4.3、GCC的輸出
  • 5、感慨

1、諸君的回答

我那這道題目問了幾個人,他們的答案不盡相同。

1.1、A君的回答

因為i = 3,故依次i++=4,i++=5,++i=6,i最後輸出為i = 6;但是由於前面兩個++是後置++,最後一個++是前置++,故j = 3+4+6 = 13。

1.2、B君的回答

因為i = 3,故第一個i++後為4,第二個i++後為5,接著做i+i操作 = 5+5=10,最後與(++i)相加 = 10+6=16。

1.3、C君的回答

因為i = 3,故依次i++=4,i++=5,++i=6,i最後輸出為i = 6;但是第一i、第二個i的++是後置++,先進行i+i操作,然後進行兩次i++後置操作,故等價於(i)+(i) = 3+3=6,i++,i++,最後與++i=6相加等於12。

1.4、D君的回答

因為i = 3,故依次i++=4,i++=5,++i=6,i最後輸出為i = 6;但是前面兩個++都是後置++,故先做i+i+(++i)操作,然後才在i++,i++操作,第三個++是前置++,故等價於 i+i+(++i)=3+3+4=10,i++,i++。

到底哪個人說得對呢?

2、編譯器的輸出

首先讓我們先來看看編譯器會輸出什麼?

2.1、Visual Studio的輸出

執行環境:Win7+VS2005 or VS2010,輸出如下圖所示:

image

2.2、GCC的輸出

執行環境:Ubuntu 10.04+gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3,執行結果如下:

image

2.3、Visual C++的輸出

執行環境:Win7+VC2010,輸出和VS一樣,及i = 6 & j = 12

看到這裡你肯定想問why? why?? why???

3、分析

重編譯器的輸出結果來看貌似C君、D君的分析都是對的,這種差異跟編譯器有直接的關係,因為對於這個表示式怎麼編譯還沒有形成標準,編譯器的結合方向不同,答案因此會有所不同。而且當然還包括運算子的優先順序等。其實頂多算C君答對了一部分,其他幾個人的回答都是錯的,詳情見下面的分析。

3.1、gcc編譯器上的分析

(i++)+(i++)+(++i) <=> i+i+(++i); i++; i++;即如果表示式中含有i++,一律替換成i,然後在表示式之後進行i++操作。

這樣的話上面的程式碼就可以很好的理解了,即3+3+4=10。

3.2、分析gcc編譯之後的彙編程式碼

可以對gcc編譯之後的執行檔案進行反編譯分析驗證正確性。在Linux下面可以用objdump –d xxx(執行檔案)命令反彙編執行檔案。反編譯之後可以看到如下圖所示的程式碼:

gcc反彙編之後的程式碼

說明:Linux下采用的是AT&T的彙編語法格式,Windows下面採用的是Intel彙編語法格式。二者的主要區別在於:

  1. 指令運算元的賦值方向是不同的 
       Intel:第一個是目的運算元,第二個是源運算元 
       AT&T:第一個是源運算元,第二個是目的運算元
  2. 指令字首 
       AT&T:暫存器前邊要加上%,立即數前要加上$ 
       Intel:沒有這方面的要求
  3. 記憶體單元運算元 
       Intel:基地址使用[] 
       AT&T:  基地址使用() 
      比如:intel中  mov  ax,[bx] 
                  AT&T中 movl (%eax),%ebx
  4. 操作碼的字尾 
         AT&T中操作碼後面有一個字尾字母:“l” 32位,“w” 16位,“b” 8位 
         Intel卻使用了在運算元前面加dword ptr, word ptr, byte ptr的格式 
       例如:mov al,bl (Intel) 
                 movb %bl %al (AT&T)
  5. AT&T中跳轉指令標號後的字尾 表示跳轉方向,“f”表示向前,“b”表示向後

下面我們重點分析紅框中的程式碼:

movl  $0x3 ,0x1c(%esp):將3賦給i,即i=3 
mov   0x1c(%esp) ,%eax:將esp中的i放到eax中 
add     %eax ,%eax:進行i+i操作,即3+3 
addl    $0x1 ,0x1c(%esp):對i進行加1操作,即表示式中的(++i) 
add     0x1c(%esp),%eax:將eax中i+i的結果6,加上++i之後的i,即6+4=10 
addl    $0x1 ,0x1c(%esp):對i進行加1操作,即表示式中的(i++) 
addl    $0x1 ,0x1c(%esp):對i進行加1操作,即表示式中的(i++)

至此關鍵程式碼已經分析完成,由此可見我們之前對gcc編譯器上的分析是正確的。

3.3、vs編譯器上的分析

(i++)+(i++)+(++i) <=>(++i)+i+i; i++; i++;即如果表示式中含有前置++i,首先執行++i操作;表示式中的i++,一律換成i,然後執行加法操作;最後在進行i++操作。

這樣的話上面的程式碼就可以很好的理解而來,即首先執行++i,i變為4了;然後進行i+i+i=4+4+4;i++,i++。

其實對於VS/VC2010編譯器中的可以總結為:當用於四則運算時,前置++/--的運算優先順序最高,後置++/--的運算優先順序最小,其它的居中。(跟你書上看到是不是不同!)

3.4、分析VS編譯之後的彙編程式碼

用W32Dasm反彙編vs編譯生成的exe檔案,追蹤程式碼。我們可以看到如下圖所示的程式碼:

反彙編後的程式碼

下面重點分析一下框中程式碼:

mov [ebp-08],3:將3賦給i,即i=3 
mov eax,dword ptr [ebp-08]:將ebp中的i的值放到eax中,是"累加器"(accumulator), 它是很多加法乘法指令的預設暫存器。dword ptr表示這是一個雙字指標,即所要定址的資料是一個雙字(4位元組) 
add eax,1:對eax中的i進行加1操作 
mov dword ptr [ebp-08] ,eax:將eax中的i賦給ebp中i,即將i加1之後的值賦給i,也即達到i=i+1的效果 
mov ecx,dword ptr [ebp-08]:將ebp中的i放到ecx中 
add ecx,dword ptr [ebp-08]:將ebp中的值加上i,即4+4 
add ecx,dword ptr [ebp-08]:將ebp中的值加上i,即4+4+4 
mov dword ptr [ebp-14],ecx:將ecx中的值賦給j 
mov edx,dword ptr [ebp-08]:將i放到edx中 
add edx,1:對edx中的i進行加1操作 
mov dword ptr [ebp-08] ,edx:將edx中的i賦給ebp中i,即將i加1之後的值賦給i,也即達到i=i+1的效果 
mov eax,dword ptr [ebp-08]:將i放到eax中 
add eax,1:對eax中的i進行加1操作 
mov dword ptr [ebp-08] ,eax:將eax中的i賦給ebp中i,即將i加1之後的值賦給i,也即達到i=i+1的效果

至此,上面表示式的關鍵運算部分已經分析完成。從這裡可以知道,上面我們地VS編譯器的分析是正確的。

4、發散思維

可以說通過上面那麼篇幅的介紹,我們對涉及前置++和後置++的加法運算表示式的計算過程有了一個清楚的認識,下面就我們發散一下我們的思維,釋放我們的能量。

4.1、思維放射

您看下面的程式碼會輸出什麼,現在知道了吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <stdlib.h>
 
int main(int argc,char** argv)
{
    int i=3,j=3,k=3,l=3,m=3,n=3,result1,result2,result3,result4,result5,result6;
    result1=(++i)+(++i);
    printf("i = 3\n");
    printf("result1= (++i)+(++i) = %d\n\n",result1);
 
    result2=(j++)+(j++);   
    printf("j = 3\n");
    printf("result2= (j++)+(j++) = %d\n\n",result2);
 
    result3=(++k)+(++k)+(++k);
    printf("k = 3\n");
    printf("result3= (++k)+(++k)+(++k) = %d\n\n",result3);
 
    result4=(++l)+(++l)+(l++);
    printf("l = 3\n");
    printf("result4= (++l)+(++l)+(l++) = %d\n\n",result4);
 
    result5=(m++)+(m++)+(m++);
    printf("m = 3\n");
    printf("result5=(m++)+(m++)+(m++) = %d\n\n",result5);
 
    result6=(n++)+(++n)+(n++);
    printf("n = 3\n");
    printf("result6=(n++)+(++n)+(n++) = %d\n\n",result6);
    exit(0);
}

請不看結果先自己分析一下,然後和結果對比!

4.2、VS的輸出

執行環境:Win7+VS2005 or VS2010,輸出如下圖所示:

image

4.3、GCC的輸出

執行環境:Ubuntu 10.04+gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3,執行結果如下:

image

根據前面我們挖掘到的規則,我們可以得到result3之外所有其它答案。最後,還有一點要說明的是:gcc中的加法運算表達死中,是按照從左到右按順序,如果運算子兩邊有++i運算元,就先進行++i操作,然後進行加法運算;vs中的加法運算表示式中,則不一樣,只要表示式中有++i運算元,就要先計算,最後才是進行加法運算。這也是為什麼result3不同的原因!加法運算可以擴充套件到減法、乘法、除法運算和前置--、後置--。但是如果是四則混合運算還要考慮加、減、乘、除的優先順序問題。

 

 

 

5、感慨

通過這麼多分析,我們可以算得上是對涉及++、--的運算表示式計算過程有了透徹理解!我在挖掘這個計算過程的路上,可是化了不少功夫也在剛開始分析彙編程式碼時遇到了一些困難,但這顆求知的心,推動著我堅持要去弄清楚它!最後我想說:請不要寫這種語句!理由很簡單,它既不好理解又不好維護,最重要的是它的結果會因編譯器的不同而不同。


作者:吳秦
出處:http://www.cnblogs.com/skynet/
本文基於署名 2.5 中國大陸許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名吳秦(包含連結).

相關文章