作為一名老牌的編程語言,c++還是很值得人們來研究和學(xué)習(xí)的,該文章就其編譯器g++作出分析和論述。
g++為GNU的c++編譯器,其工作可分為編譯和鏈接,下面分別對這兩個過程進(jìn)行介紹。
編譯
編譯是指編譯器對源碼進(jìn)行預(yù)處理、編譯、和匯編的過程。在該過程中每一個.cpp文件(源文件)被編譯成對應(yīng)的.o文件(目標(biāo)文件),且是各自編譯各自的,互不打擾。也就是說在鏈接之前,每一個文件是沒有關(guān)系的。
預(yù)處理
該階段編譯器的工作為符號的替換,將.cpp文件中所有為方便程序員編程而設(shè)定的符號替換為與之對應(yīng)的可以被g++編譯的程序。主要內(nèi)容如下:
替換宏
消除注釋
插入頭文件對應(yīng)的源程序(即插入include的源文件)
該階段生成.i文件。是不是有點(diǎn)迷,莫慌,請看例子:
a.h文件:

a.cpp文件:

下面對a.cpp進(jìn)行預(yù)處理(執(zhí)行g(shù)++ -E a.cpp -o a.i),看看會發(fā)生什么:
(注:命令行中-E為預(yù)處理命令, -o為輸出命令,后接為輸出文件所賦的名稱)

仔細(xì)觀察一下,會發(fā)現(xiàn):a.h被插進(jìn)來了(紅框第一行開始),對應(yīng)的注釋消失了A()的函數(shù)體中宏Q被替換成了0
編譯
此階段將預(yù)處理好的.i文件編譯成.s匯編文件,實(shí)現(xiàn)命令為:g++ -s a.i -o a.s(-s為編譯命令),下面請看效果:

(注:這只是一部分程序,而非全部)
匯編
此階段將匯編文件(.s文件)匯編成目標(biāo)文件(二進(jìn)制文件),該階段輸出.o文件,實(shí)現(xiàn)命令為:g++ -c a.cpp -o a.o(-c為匯編命令),下面請看效果:

(注:這只是一部分程序,而非全部)
至此,源程序就轉(zhuǎn)換為了二進(jìn)制文件,但是從工程的角度來講,每一個文件之間還沒有聯(lián)系,需要將一個工程下的文件進(jìn)行連接(當(dāng)然也包括所需要的非工程的庫),才能被執(zhí)行。
鏈接
在一個工程下,會有很多文件,而這所有的文件的最終目的都是為main.cpp(或說main函數(shù))服務(wù)的。這些文件中包含的函數(shù)、變量、類、結(jié)構(gòu)體存在的使命就是被調(diào)用,而調(diào)用與被調(diào)用的過程其實(shí)就是將各個文件進(jìn)行聯(lián)系的過程,即鏈接。比如,在main.cpp使用了a.cpp的A函數(shù),這時候就需要將main.o與a.o進(jìn)行鏈接以完成調(diào)用A函數(shù)的過程,當(dāng)然前提示是main.cpp在使用A之前對其進(jìn)行了聲明,且聲明的方式有兩種,第一種就是引入頭文件#include"a.h",第二種就是將A聲明為外部函數(shù)。 (注:在編譯過程中不存在鏈接,也就不存在調(diào)用的概念)
從鏈接概念角度講,鏈接的過程就是將不同的庫鏈接至引用該庫的源程序中,以滿足源程序運(yùn)行條件,c++中存在靜態(tài)鏈接和動態(tài)鏈接兩種方式,之后會一一描述。
從實(shí)戰(zhàn)角度來講,將不同文件進(jìn)行鏈接一共有三種方式:
1,將不同文件同時編譯為一個輸出文件
2,靜態(tài)鏈接
3,動態(tài)鏈接
我們還是以上面的程序?yàn)槔贿^我們把a.cpp文件的中的main函數(shù)單獨(dú)拿出來打包成main.cpp文件,以方便觀察鏈接過程。這里給大家展示一下圖片,因?yàn)闀幸恍┳兞康穆暶髯鞒鲎儎樱蝗粫鹬囟x錯誤。
a.h

a.cpp

main.cpp

同時鏈接多個文件
將多個文件同時編譯為一個可執(zhí)行文件,其本質(zhì)上就是靜態(tài)編譯,將main.cpp與需要的庫在編譯階段就全部一起打包到可執(zhí)行文件中。 命令行:g++ main.cpp a.cpp -o main 執(zhí)行效果如下:

靜態(tài)鏈接
靜態(tài)鏈接是指將main文件需要的庫在編譯階段直接復(fù)制過來,和main文件一起打包成可執(zhí)行文件,所以在生成可執(zhí)行文件之后,及時刪除原靜態(tài)庫文件,也不會影響可執(zhí)行文件的執(zhí)行過程,因?yàn)樾枰撵o態(tài)庫已經(jīng)被復(fù)制到可執(zhí)行文件中了,這一點(diǎn)也是靜態(tài)鏈接和動態(tài)鏈接的本質(zhì)區(qū)別。靜態(tài)鏈接庫的后綴名為.a,其本質(zhì)就是將幾個編譯好的.o文件壓縮打包而成,所以說前面的同時鏈接多個文件屬于靜態(tài)鏈接的一種。一般情況下,g++會自動在環(huán)境變量與默認(rèn)地址中尋找需要的靜態(tài)庫,當(dāng)然也可以手動指定路徑。
生成靜態(tài)庫命令:ar cr libname.a name.o靜態(tài)庫名的格式為lib+庫名+.a所以第三個參數(shù)是要成生成的靜態(tài)庫的名字,name.o指要壓縮進(jìn)靜態(tài)庫的文件,可以指定多個但是注意一定是.o文件否則后面出錯,下面進(jìn)行實(shí)操,輸入命令行ar cr liba.a a.o,查看當(dāng)前目錄,發(fā)現(xiàn)liba.a文件,見圖:

下面利用該靜態(tài)庫生成可執(zhí)行文件,輸入命令行:g++ main.cpp -L. -la -o main該命令行的主要作用為告訴g++所需要的靜態(tài)庫在哪里(不在默認(rèn)路徑下),-L是路徑,.表示在當(dāng)前路徑下,-l接庫名,a是lib.a的庫名(*注:庫名要將lib與.a去掉),查看執(zhí)行效果:

動態(tài)編譯
上文講到靜態(tài)編譯是在編譯階段將需要的庫文件復(fù)制過來和main.o一起打包成可執(zhí)行文件,這樣做優(yōu)點(diǎn)是可以免受靜態(tài)庫變化的影響,但是缺點(diǎn)也很明顯,第一浪費(fèi)資源和空間,一個庫文件可能被多個可執(zhí)行文件引用,那樣就需要將該庫文件復(fù)制多次,浪費(fèi)空間。第二更新成本大,如果某一個庫文件更新的話,需要將每一個可執(zhí)行文件中的該庫文件更新,成本太大。
基于以上內(nèi)容,我們引入動態(tài)鏈接,動態(tài)鏈接是指,源文件在編譯階段不引入引用的庫而是在執(zhí)行階段引入,且不將庫文件復(fù)制到可執(zhí)行文件中,只是在執(zhí)行時進(jìn)行調(diào)用(注:每一次執(zhí)行時都需要引入動態(tài)庫),這樣一個動態(tài)庫就可以被多個可執(zhí)行文件引用了,所以動態(tài)庫又稱為共享庫。這樣做還有一個好處,就是如果某庫需要更新,直接更新庫文件,執(zhí)行文件在執(zhí)行階段就會自動更新了。
生成動態(tài)庫命令:g++ -shared -fPIC -o libname.so name.o動態(tài)庫的后綴為.so命名格式與靜態(tài)類似,-shared -fPIC是生成位置不固定的動態(tài)庫命令。這里需要注意的有兩點(diǎn),1,一定是.o文件,否則出錯,2,如果執(zhí)行出錯,可能是因?yàn)橹吧傻?code>.o文件位置固定,加上-fPIC在生成一次,即運(yùn)行g++ -c name.cpp -fPIC -o name.o在生成一次.o文件即可。
下面運(yùn)行命令g++ -shared -fPIC -o liba.so a.o,查看當(dāng)前目錄,發(fā)現(xiàn)liba.so:

下面利用動態(tài)庫生成可執(zhí)行文件,運(yùn)行命令g++ main.cpp -L. -la -o main(與靜態(tài)庫類似),執(zhí)行成功,生成可執(zhí)行文件main,但是運(yùn)行時會報錯說找不到動態(tài)庫lib.so,為什么會這樣呢,還記得靜態(tài)鏈接和動態(tài)鏈接的區(qū)別嗎,因?yàn)閯討B(tài)鏈接沒有將庫文件復(fù)制到可執(zhí)行文件中,故每一次執(zhí)行main需要去鏈接動態(tài)庫,而g++ main.cpp -L. -la -o main命令只是告訴編譯器在當(dāng)前目錄有liba.so,可以生成main,所以生成main是成功的,但是執(zhí)行時g++還會去默認(rèn)目錄下去找liba.a,發(fā)現(xiàn)沒有所以報錯。
運(yùn)行mv liba.so /usr/lib將liba.so轉(zhuǎn)移到/usr/lib下(動態(tài)庫默認(rèn)存儲地址),再次運(yùn)行,成功:

(也可以這樣:g++ main.cpp / home/zhdr/test/libmyhello.so -o main就不用 mv libmyhello.so /usr/lib)
以上為g++工作流程的簡單描述,后面如果有時間會對extern關(guān)鍵字以及可能發(fā)生的重定義錯誤進(jìn)行闡述,敬請期待。