程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C >> 關於C >> 關於不同編譯器下C語言中自加(++)運算符的解釋

關於不同編譯器下C語言中自加(++)運算符的解釋

編輯:關於C

這篇備忘是由同學發的一個疑問,確實我也忘了我在學的時候遇到這個問題麼有,主要是很少這麼用過,而且純數學計算也沒有怎麼寫過。因為相對來說,用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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved