這篇備忘是由同學發的一個疑問,確實我也忘了我在學的時候遇到這個問題麼有,主要是很少這麼用過,而且純數學計算也沒有怎麼寫過。因為相對來說,用matlab會更好。
其實C語言是門精美的語言,也是我認為最為舒服的語言,只是沒有面向對象,擴展後的C++語法復雜性爆炸增長,而且各種庫也比較蛋疼,MFC也成了昨日黃花,不知道Object-C如何,想必蘋果用的東西應該還可以。要是哪天牛逼到自己寫個C的面向對象擴展超集多好,按照自己理解來,語言名字想好了(這是最簡單的工作),可惜沒那本事,誰叫我編譯原理學的很差呢。
閒言少敘,開始。
裡面東西很淺顯,匯編之類的很多年都沒用過了,生疏的很,希望大牛們不要笑話,只是自己做個備忘。
不過這個疑問確實很好,我研究了一下。
程序如下,非常簡單:
#include<stdio.h>
#include<stdlib.h>
intmain()
{
int a=1,b=3,c=0;
a=(++b)+(++b)+(++b);
printf("a=%d\nb=%d\n",a,b);
return0;
}
准確說這是故意為了明白自加運算符而做的程序,實際上這是很糟糕的一段代碼,盡管它有一點的效率提升,為什麼糟糕,原因在於不同的編譯器的解釋是不一樣的。
我開始看到同學在VC下的運行結果我吃了一斤(也沒胖),應該說我在學TC時候也應該接觸類似的程序,但是並沒有發現什麼特殊的結果,但是確實沒在VC下運行過。
於是我在GCC下運行了一次:發現跟VC結果一樣的,當然,這兩個編譯器是不同的。
老大用C#運行了一次,結果是正常人理解的15。
GCC是多少呢?答案是16;自加後b的值都是一樣的。
如果我們按照平常的理解,似乎是4+5+6=15;但是為什麼GCC下是16呢?而且VC下也是16;而我要告訴你的是TC下是18;
剛才也試了剛學的python,發現這玩意沒有自增運算。
我試了半天,也沒理解這是怎麼回事,算了,看看匯編代碼把。
看一下匯編代碼,說實話,LINUX沒有用過匯編,學的8086匯編是基於Intel的,我們知道匯編是與硬件緊密聯系的語言,不同平台上語法存在不同,偽代碼也有所區別。
匯編代碼有點多,在VC下也可以看,相對來說,代碼要簡潔多了,主要是屏蔽了一些底層的東西。
我們知道一段C代碼,經過語法分析,預處理,編譯,鏈接,最後成為可執行文件。在內存中,除了你編寫的代碼,還有堆棧段等一系列數據結構。作用不一而足。
我們看到關鍵的部分:a=(++b)+(++b)+(++b);
首先先解釋下匯編,經過查閱,在LINUX下用的是AT&T匯編(我說一開始看這玩意怎麼有點奇怪),與Intel幾個不同點,大部分的偽命令是一致的;
加法,移動等操作,右邊是目標操作數,左邊是源操作數,與Intel正相反;
ADDL----剛開始有點發蒙,難道是加到左邊?其實就是ADD,“L”表示操作數是32bit的LONG類型,我擦;
$0x3----0x麼,16精制數好解釋,前面美元符啥意思?取這個數的地址?後來查了一下,是立即數的表示,尼瑪,就是Intel下面的mov esp 0x3
%esp-----esp麼,寄存器,前面%,哎,不解釋,還是一種表示記號,AT&T下面寄存器就是以%開頭,esp等共有8個32bit寄存器,還有edx之類的。
我的能力也就能解釋一下a=(++b)+(++b)+(++b)這段了:
1,首先是addl$0x1,0x1c(%esp),就是加1到右邊的寄存器,0x1c似乎是地址標示
2,一樣的語句;
3,mov語句,將自加後的esp值放到eax寄存器中;
4,add,將eax中數自加到本身;
5,addl,將esp再自加1,看到沒有
6,現在再將esp加到eax寄存器中;
7,最後把eax中的值放入變量a中;
我們看到了這個表達式的執行過程,首先是將變量b自加了兩次!!!然後相加,最後在自加一次b,再和前面的和相加得出最後結果。
怎麼會自加兩次呢?我們知道++b是先自加後使用,關鍵是我們怎麼去理解“使用”這個詞語?
a=(++b)+(++b)+(++b);
C語言中,語法分析是采用最大識別原則,就是從左向右,不斷讀進字符,直到無法解釋為止。
那麼對(++b)+(++b),顯然括號的等級最高,把左邊(++b)讀到棧裡面,先加了1,然後讀進中間的”+”號,發現右邊出現左括號,故繼續讀入字符,注意這時候“+運算”並沒有執行,那麼接著運算第二個(++b),這裡面就有問題,到底是5呢,還是4呢?編譯器直接在變量上自加,所以,是5,而且當+右方的()運算完成後才開始計算加法,也就是“使用”,但不是4+5,而是5+5,因為b已經是5了,也就是,編譯器把b變量統一為最後自加結果。所以編譯器的解釋是5+5+6=16!!!
是不是可以這樣理解,(++b)+(++b)認為是“使用”,畢竟相加了麼,
即:(++b)+(++b)為一次運算,算出為5+5,然後b變量在5基礎上自加一次,故有5+5+6=16;
很不幸,這樣理解不對,我們看下這個例子:a=(++b)+(b++)+(b++),如果我們按照上述邏輯思考的話,應該是4+4+5=13,意即在(++b)+(b++)完成後,可以算是使用了,b++執行,所以b為4+1=5;可惜啊,答案是12;也就是編輯器是以表達式為單位來理解“使用”這個詞語。但是這樣理解似乎對a=(++b)+(++b)+(++b)又無法解釋,如果以表達式為單位算使用,那麼似乎應該是先做完自加,然後在相加,(這是從人的角度解釋的)所以結果是6+6+6=18,但是GCC下不是,但是我要說的是,TC下編譯器是這麼理解的!!!
我們看下a=(++b)+(b++)+(b++)的情況:
從匯編上我們可以清晰看出執行流程。
似乎已經有點眉目:編譯器!!
如果我們把程序修改如下:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int a=1,b=3,c=++b;
a=c+(++b)+(++b);
printf("a=%d\nb=%d\n",a,b);
return0;
}
其實大多數人理解的是這個意思,這個避免了自增的一個b=4丟失的問題,僅對三個有用,多了還是上面的解釋。
似乎我們有了點答案,再玩玩把,我們看看a=(b++)+(b++)+(b++)會有什麼結果。
有沒有覺得非常犀利!!
看一下匯編語句:
三個自加操作,是在最後完成的!!!
也就是等於a=1+1+1,然後做三次自加運算。
那麼試一下:a=(++b)+(b++)+(++b)+(++b)結果是多少呢?
前面兩個似乎容易啊:
4+4=8,對呢,後面怎麼玩呢?是先都自加還是一個個來呢?前面說過了,C語言是“最大口徑”讀入,從做到右一次完成運算(針對GCC編譯器規則)。
所以,算出8以後,讀入“+”,再讀入右邊(++b),運算出結果8+5=13,然後b+1=6;故而最後結果是13+6=19!
那麼請問b=???
呵呵,一開始會說6吧,其實b=7,為什麼,忘了還有個b++了吧,這是放在最後運算的部分。
如果是a=(++b)+(++b)+(++b)+(b++)+(++b)+(++b);如此變態的表達式!我擦,也能寫的出來。
結果是(GCC):a=37;b=9!!!其實主要是前兩個++的理解:(++b)+(++b),要注意,++b並不是4,人們往往以為第一個是4,然後4+5,計算機並沒有額外存儲4這個數字,那麼在都到下一個(++b)後,b=5,然後運算b+b=10,懂了吧?人類往往把4額外存儲起來,就像這個式子表達的一樣c=++b;a=c+(++b)+(++b);上面我已經做了演示。
下面我們看下TC的編譯器理解:
TC下面執行b=3;a=(++b)+(++b)+(++b)是多少呢?答案是18;
可以看出TC編譯器對此的解釋是先全部做完自加運算得出最後的b值,然後再做加法運算,
本人嘗試將TC反匯編一下,但是代碼的可讀性非常差。找了半天找到了關鍵部分:
[html] * Referenced by a CALL at Address:
|:0001.011A
|
:0001.01FA 55 push bp *把基址壓倒堆棧
:0001.01FB 8BEC mov bp, sp *把堆棧偏移地址放入bp
:0001.01FD 56 push si
:0001.01FE 57 push di
:0001.01FF BF0100 mov di, 0001 a
:0001.0202 BE0300 mov si, 0003 b
:0001.0205 46 inc si ++b
:0001.0206 46 inc si ++b
:0001.0207 46 inc si ++b
:0001.0208 8BFE mov di, si
:0001.020A 03FE add di, si
:0001.020C 03FE add di, si
:0001.020E 56 push si
:0001.020F 57 push di
:0001.0210 B89401 mov ax, 0194
:0001.0213 50 push ax
:0001.0214 E8B206 call 08C9
:0001.0217 83C406 add sp, 0006
:0001.021A E85410 call 1271
:0001.021D 33C0 xor ax, ax
:0001.021F EB00 jmp 0221
* Referenced by a CALL at Address:
|:0001.011A
|
:0001.01FA 55 push bp *把基址壓倒堆棧
:0001.01FB 8BEC mov bp, sp *把堆棧偏移地址放入bp
:0001.01FD 56 push si
:0001.01FE 57 push di
:0001.01FF BF0100 mov di, 0001 a
:0001.0202 BE0300 mov si, 0003 b
:0001.0205 46 inc si ++b
:0001.0206 46 inc si ++b
:0001.0207 46 inc si ++b
:0001.0208 8BFE mov di, si
:0001.020A 03FE add di, si
:0001.020C 03FE add di, si
:0001.020E 56 push si
:0001.020F 57 push di
:0001.0210 B89401 mov ax, 0194
:0001.0213 50 push ax
:0001.0214 E8B206 call 08C9
:0001.0217 83C406 add sp, 0006
:0001.021A E85410 call 1271
:0001.021D 33C0 xor ax, ax
:0001.021F EB00 jmp 0221
看到沒有,si寄存器保存了b=3變量值,並且先自增了三次,變為6了,然後做了兩次加法,和存在di中。這個與GCC編譯器解釋不同吧,哎,大約6年沒用匯編了,看了很生疏,很多都忘了,抽空看看把。
總結:
編寫代碼,效率要考慮,但是要避免有歧義,費解的表達方式,程序還有個可讀性要求,畢竟你寫的代碼以後要維護。
對於自加這種運算,要注意使用條件,有時你確實少寫了那麼一點代碼,提高了那麼一丁點的效率;但是往往會帶來意想不到的錯誤。而且問題是不同編譯器會做優化,所以實際執行順序與你理解的可能並不一樣。不過想必也沒有人會在生產環境中寫這樣的代碼。這篇文章也只是從匯編的角度來闡釋了處理流程,我看到有些文章是從運算符結合和優先順序來解釋的,其實本質上是編譯器的選擇過程。
我試圖能講的很深入,發現很多東西都還給老師了,慚愧,哎,抽空復習復習。
拙文一篇,僅做拋磚引玉。
摘自 DesignLab