陳賢敏 湯海晨 陳治帆
摘要:同一個C語言的自增、自減表達式為什么在不同集成系統(tǒng)開發(fā)環(huán)境中產(chǎn)生不一樣的結果。使用GCC和Clang兩種不同的編譯器來驗證此實例表達式的值,并把兩種編譯好的C語言程序反編譯成匯編語言,再來分析代碼,目的是讓大家真正從底層了解為什么相同的表達式值會產(chǎn)生不同的結果。
關鍵詞:自增自減;GCC;Clang
中圖分類號:TP311? ? ? 文獻標識碼:A
文章編號:1009-3044(2023)31-0059-02
開放科學(資源服務)標識碼(OSID)
0 引言
C語言中的自增自減運算符(++和--)簡潔、緊湊、靈活。學生在做練習中遇到由自增自減運算符組成的復雜算術表達式,如p=(i++) +(i++) +(i++) 時,在不同的編譯器中可能會得到不同的運行結果[1],會讓用戶產(chǎn)生茫然疑惑。
國內對于C語言中的++、--用例有許多針對性的探討和研究,朱恩亮[2]在《Visual C ++與Turbo C處理自增自減運算表達式的區(qū)別》一文中列舉了在Visual C ++、Turbo C編程語言環(huán)境中,通過操作符的連接,可以構造出具有不同復雜程度的表達式。分析了i=3,表達式s1=(++y)+(++y)+(++y)值分別是s1=5+5+6=16和s1=6+6+6=18,產(chǎn)生不同結果是由于在C語言中操作符存在著優(yōu)先級與結合性,并構造表達式后,不同語言系統(tǒng)的編譯會產(chǎn)生不同的結果。
夏超群[3]在《淺析C語言自增自減運算符的使用》一文,其例2的語中p=(i++) +(i++) (i++) 從理論上分析該語句中表達式(i++) +(i++) +(i++) 的值應為5+6+7,實際卻是VC++6.0環(huán)境下運算結果都是5+5+5。文中說明后置自增自減運算符的“先用后變”的“變”是指在下一條語句執(zhí)行前統(tǒng)一改變,而不是剛用完就變。故該語句等價于:p=i+i+i;i=i+1;i=i+1;i=i+1。造成這種結果是因為高級語言的一條語句經(jīng)編譯解釋成若干條機器指令,這若干條機器指令的順序最終決定該等價高級語言語句的執(zhí)行結果。
為什么編譯系統(tǒng)會產(chǎn)生這兩種結果,以上作者的文中并未從源頭給出答案。因此本文通過把C程序代碼編譯后轉換成匯編語言并分析代碼的執(zhí)行順序,真正從底層了解不同的編譯器為什么會產(chǎn)生不同的結果。
1 C語言實例
#include <stdio.h>
int main() {
int accumulate,i=5;
accumulate=(++y)+(++y)+(++y);
printf("accumulate=%d,i=%d",accumulate,y);
return 0;
}
大家看到此實例中的表達式accumulate=(++y)+(++y)+(++y),第一反應給出的答案為6+7+8,結果值為21,y的值為8,那是否正確呢?其實還可以給出第二種答案accumulate的值為22,y的值為8。
此實例C語言中的表達式accumulate=(++y)+(++y)+(++y)的背后運算到底是怎樣的一個過程,為什么此實例程序中的變量y等于5,最終程序運行會產(chǎn)生兩種不同的結果,分別是22和21。下面通過GCC編譯器模式和Clang編譯器模式驗證此實例在不同的編譯器得出的結果值是不一樣的。
1.1 實驗1
實驗環(huán)境操作系統(tǒng)是CentOS Linux,GCC編譯器模式,值為accumulate=22,y=8。通過GCC反編譯實例后如圖1,為了便于理解,圖2根據(jù)圖1一一對應寫成C語言格式,使讀者更好地理解匯編語句。
可以看出,表達式accumulate=(++y)+(++y)+(++y),語句(1) 先把[rbp-4]當成i變量,理解成把5賦給[rbp-4]變量,add DWORD PTR [rbp-4],1;語句(2) 相當于表達式中的第一項(++y) ,加1執(zhí)行后[rbp-4]變量值6,i變量自然也是6了,執(zhí)行語句(3) 匯編語句,相當于表達式中的第二項(++y) ,在原來[rbp-4]的基礎上又加1,得[rbp-4]值為7,自然i的值為7。語句(4) 簡單點說,就是把7賦給eax。難道繼續(xù)加1是i的值被替換成8嗎?顯然不是,語句(5) lea edx,[rax+rax] ,理解為edx=7+7=14此時才明白為什么在C語言中前兩項 (++y)+(++y)相加,也就是accumulate=7+7+(++y)。到這里讀者有點不理解了,這個rax為什么等于7。簡單解釋一下,32位和64位寄存器不是分開的寄存器,它們是重疊的:64位的rax,具有eax作為其底部的32位。因此,對32位寄存器的修改會反映在相應的64位寄存器中,反之亦然。繼續(xù)看語句(6) 中[rbp-4]是多少呢,它在執(zhí)行語句(3) 后值沒有改變過,依舊是7,因此執(zhí)行(6) add DWORD PTR [rbp-4],1。相當于表達式中的第三項(++y) ,[rbp-4]值為8,語句(7) [rbp-4]賦給了eax,eax值為8。繼續(xù)執(zhí)行語句(8) add eax,edx ,可以把該語句轉換成熟悉的語句eax=eax+edx,已知文中語句(5) 已得出edx=14,因此可得出eax=eax+edx=8+14=22,如圖3所示。
1.2 實驗2
分析完了GCC編譯器模式,現(xiàn)以值為accumulate=21,y=8,按照同樣的方式分解一下由Clang編譯器模式編譯的程序,就會發(fā)現(xiàn)情況有所不同。
通過圖4、圖5可以看出,表達式accumulate=(++y)+(++y)+(++y),先通過語句(1) 、(2) ,把5賦給eax,語句(3) 累加1后把值賦給eax寄存器,eax值為6,相當于表達式第一項(++y)的值,注意先把第一項值6保留在eax寄存器,通過語句(4) 、(5) 把eax值為6賦給ecx,執(zhí)行語句(6) ecx累加1后,值為7,相當于表達式第二項(++y),把第二項值也是獨立保存在ecx寄存器中。語句(8) eax=eax+ecx,得eax=6+7=13,從語句(9) 、(10) 可得出表達式第三項(++y)值為ecx=7+1=8,語句(12) 的結果為eax=eax+ecx=13+8=21,如圖6所示。
2 實驗結果分析
現(xiàn)在才明白實驗1中編譯器前兩項自增后,GCC編譯是前兩次y,(++y)+(++y)+(++y),原因是首先掃描求解前半部分,即(++y)+(++y)的值,先對y變量進行兩次自增運算,y的值變?yōu)?,再計算y+y的值為7+7=14,然后再求解后半部分,即14+(++y)的值,先對變量y自增1次,y的值變?yōu)?,再計算14+8=22,因此最終accumulate的值為22,y的值為8。
實驗2中,用Clang編譯器,再反編譯為匯編語言。通過匯編語言可以看出,前兩項y的值都分別獨立賦給不同的寄存器eax和ecx,因此前兩項(++y) 中的y就不會累加后再賦給相同的寄存器。而實驗1中(2) 、(3) 中兩條語句是對y進行累加后賦給,由原來的初值5變化7后,再賦給寄存器eax。
文中的實例accumulate=(++y)+(++y)+(++y)只是來證明這樣的式子在不同的編譯系統(tǒng)中會產(chǎn)生不同的值,因此在實際的工作生產(chǎn)和教學中不建議這樣來使用。任何類似(y++)+(y++)、(++j)+(++j)這樣的表達式,都屬于未定義行為??梢赃@么說,在C語言標準中沒有規(guī)定這樣的表達式應該如何計算,完全由編譯器自行決定,說到底編譯器也是由程序員來開發(fā)出來,當然不同的人有不同的邏輯理解,這樣也就導致不同的集成開發(fā)環(huán)境編譯器下自增和自減處理邏輯的不同。也進一步說明,由于自增自減表達式結果的不確定性,也就不具有可移植性,是十分不友好的表達式。
通過以上實例驗證,相同的表達式運行結果得出的值不一樣是由不同集成開發(fā)環(huán)境中的不同編譯器編譯造成的。實驗1、實驗2論證了使用GCC、Clang兩種不同的編譯器對同一段C語言源代碼進行編譯,得到了不同的值,它們按照各自思路轉化為了匯編代碼,通過對匯編語言的分析,最終找到了相同源代碼在不同的集成開發(fā)環(huán)境下結果是不一致的,這也是寫本篇論文的目的和任務。也通過上述實驗發(fā)現(xiàn)Clang的編譯器更能符合正常人的思維邏輯,在今后的學習工作中更推薦使用Clang編譯器的開發(fā)環(huán)境。
3 結束語
C語言中自增運算符為“++”,其作用是使變量的值增1;自減運算符為“--”,其作用是使變量的值減1[4]。當初制定這兩個C語言運算符是為了方便程序使用,但在使用y++、--y不恰當時會造成混淆,給剛入門學習C語言的人員帶來混亂。
當然,像文中的實例代碼移植性差,需要大家盡量避免,分析這樣的運算順序也沒有任何意義。建議大家盡量不要去研究這樣的表達式,也更不要在實際編程中寫出這樣的表達式。所以,在編寫程序時,有選擇地小心謹慎使用自增(自減)運算符來簡化程序,在一些容易出錯的地方可以用其他方法代替,從而保證程序的執(zhí)行萬無一失[5]。當然在實際項目中,考慮到項目遷移等問題,建議不要使用連續(xù)自增、自減進行運算,這種邏輯問題在項目維護過程中很難被發(fā)現(xiàn)和維護。
參考文獻:
[1] 袁玲.在DEV C++環(huán)境下C語言自加自減運算符使用分析[J].電腦知識與技術,2016,12(27):248-249.
[2] 朱恩亮.Visual C+ +與Turbo C處理自增自減運算表達式的區(qū)別[J].鹽城工學院學報(自然科學版),2003,16(3):27-28,31.
[3] 夏超群.淺析C語言自增自減運算符的使用[J].武漢工程職業(yè)技術學院學報,2010,22(3):47-49.
[4] 周亮.淺談C語言中自增自減運算符的應用[J].電腦知識與技術,2010,6(17):4714-4715.
[5] 唐婷,呂浩音.C語言自增(自減)運算符運算規(guī)律的探討[J].隴東學院學報,2016,27(5):8-11.
【通聯(lián)編輯:謝媛媛】