文章目錄
- 一、gcc/g++介紹
- 二、gcc編譯選項
- 預處理
- 編譯
- 匯編
- 鏈接
- 三個細節
- 三、動靜態庫感性認識
- 動靜態庫的優缺點
- 四、自動化構建-make/Makefile
- 背景知識
- 初步上手Makefile
- makefile的推導過程
- makefile語法
一、gcc/g++介紹
我們之前介紹了編輯器vim,可以讓我們在linux上linux系統里都默認安裝了gcc和g++。gcc只能編譯C語言,g++主要編譯C++,也可以編譯C語言。
gcc編譯文件后默認會生成可執行文件a,out,示例如下:
但是gcc編譯文件的最佳實踐不是生成默認的a.out,而是用-o選項,生成我們自己命名的可執行文件。
當我們需要編譯多個文件生成一個可執行程序時,可以用下面的格式,反正記住-o選項后面緊跟可執行文件名就不會錯了。
二、gcc編譯選項
因為gcc支持分步驟編譯代碼,所以我們可以通過實踐來看各階段編譯器做了什么,這里就需要我們通過gcc的不同的編譯指令來查看。gcc編譯文件格式大致如上,小編來挨個介紹:
預處理
(進?宏替換/去注釋/條件編譯/頭?件展開等)
-E選項是指從現在開始gcc開啟翻譯工作,當把預處理進行完畢后就停下來,所以hello.i文件里的內容就是hello.c文件經過預處理后的結果,下面我們通過vim來分析預處理階段編譯器會完成的工作:
頭文件展開就是指gcc把已經在系統里安裝好了的頭文件內容拷貝一份到指定文件中,不如stdio.h就在系統的如下路徑:
條件編譯本質就是通過宏的定義與否對代碼進行裁剪。操作系統的內核裁剪就可以通過條件編譯來實現。
編譯
(將C語言翻譯為匯編語言)
-S選項是指從現在開始gcc開啟編譯工作,當把編譯進行完畢后就停下來
匯編
(將匯編語言翻譯為機器可識別二進制代碼)
-c選項是指從現在開始gcc開啟匯編工作,當把匯編進行完畢后就停下來
這里的hello.o叫做可重定位目標二進制文件,因為它還無法被直接執行,因為頭文件只包了相關庫函數的聲明(如printf),庫函數是實現在對應的庫里,所以經過匯編后的文件還需要在鏈接期間和相關庫進行鏈接,才能形成可執行文件。
所以在鏈接之前編譯器只會翻譯我們自己寫的代碼。
鏈接
(?成可執??件或庫?件)
鏈接指令不需要加額外的選項,直接-o就行了。
我們前面已經初步介紹了.o文件不可執行的原因,因為還需要鏈接這個步驟,那么接下來小編就來介紹鏈接到底做了什么讓.o文件變成了可執行文件。首先.o文件里除了庫函數以外的文件都已經被翻譯成了二進制代碼,而庫函數只是在頭文件里有聲明,沒有具體實現,所以在.o文件里庫函數調用那里是沒有填要調用函數的地址的,只有在鏈接期間.o文件和庫函數鏈接上了庫函數那里才能填上具體要調用庫函數在庫里的地址,因為庫是一組預先編譯好的代碼集合,所以填上地址后當代碼執行到對應庫函數時會跳轉到庫里對應的地址實現庫函數然后再返回繼續執行后續的代碼。所以這里也可以解釋為什么.o文件叫做可重定位二進制文件。
三個細節
1、有了上面的認識,所以我們以后要編寫代碼,必須要提前下載好頭文件和庫,linux下系統會自動幫我們下好,windows下我們在下載IDE時需要勾選相關選項下載。
2、庫的出現是為了讓程序員直接使用,提高開發效率。C++11 C++17 之類的更新本質就是在庫里添加了新的方法或者改變了編譯器的語法規則。
3、庫的分類如下圖所示,庫的名字一般是去掉前綴lib,去掉后綴.so或.a后,剩下的就是庫的名字。
三、動靜態庫感性認識
靜態庫是指編譯鏈接時,把庫?件的代碼全部加?到可執??件中,因此?成的?件?較?,但在運?時也就不再需要庫?件了。其后綴名?般為“.a”
動態庫與之相反,在編譯鏈接時并沒有把庫?件的代碼加?到可執??件中,?是在程執?時由運?時鏈接?件加載庫,這樣可以節省系統的開銷。
動靜態庫和動靜態鏈接之間的聯系:庫是鏈接的對象,無論是靜態庫還是動態庫,都是在鏈接過程中為程序提供所需的代碼和功能,鏈接是將庫與應用程序結合起來的操作。
前面我們認識到鏈接分為靜態鏈接和動態鏈接,我們看下面這個案例可以知道編譯器默認采用動態鏈接的方式生成可執行程序。我們用file來查看編譯生成的可執行文件的詳細屬性。
ldd可用來查看一個可執行程序依賴的庫。
如果我們想讓編譯器采用靜態鏈接的方式生成可執行程序,需要加一個-static選項。
但是實踐操作系統會提示編譯失敗,因為我們的系統里沒有安裝c/c++的靜態庫,在cnetos下安裝靜態庫的指令如下:
sudo yuminstall -y glibc-static
sudo yum install -y libstdc++-static
動靜態庫的優缺點
動態鏈接:(類比網吧)
需要的方法需要跳轉到庫里去執行
優點:節省資源
缺點:一但丟失,程序無法直接運行
稍微慢一點
靜態鏈接:(類比一人一臺電腦)
把你想要的方法拷貝到你的可執行程序中。
優點:不依賴任何庫,程序自己能獨立運行
缺點:體積大,占據資源多(占據磁盤空間,內存空間)
加載速度慢
四、自動化構建-make/Makefile
背景知識
1、會不會寫makefile,從?個側?說明了?個?是否具備完成?型?程的能?。
2、?個?程中的源?件不計數,其按類型、功能、模塊分別放在若?個?錄中,makefile定義了?系列的規則來指定,哪些?件需要先編譯,哪些?件需要后編譯,哪些?件需要重新編譯,甚?于進?更復雜的功能操作。
3、makefile帶來的好處就是?“?動化編譯”,?旦寫好,只需要?個make命令,整個?程完全?動編譯,極?的提?了軟件開發的效率。
4、make是?個命令?具,是?個解釋makefile中指令的命令?具,?般來說,?多數的IDE都有這個命令,?如:Delphi的make,VisualC++的nmake,Linux下GNU的make。可?,makefile都成為了?種在?程??的編譯?法。
5、make是?條命令,makefile是?個?件,兩個搭配使?,完成項??動化構建。
初步上手Makefile
(注釋Makefile里的內容用#)
首先我們要明確make是linux系統內置的命令,Makefile是一個需要我們自己創建的文件。我們先來感受一下make的便捷:
這上面的操作要建立在我們自己創建出makefile文件的基礎上,下面小編來介紹如何創建makefile(makefile本質就是編寫依賴關系和依賴方法):
我們看上面的樣例,前兩行就是編譯源文件需要在makefile寫的依賴方法。
后三行表示移除生成的可執行文件(項目清理),.PHONY表示跟在它后面的被修飾的目標是一個偽目標,偽目標的特點是總是會被執行,而且依賴文件列表為空。那么什么是不會總是被執行呢,比如我們的code.exe就不是偽目標,當它被make構建出來后,如果源代碼code.c沒更新make編譯器就不會再執行把code.c編一遍,這樣做是為了讓編譯工作高效一些,因為重復編譯會浪費資源。所以這里的最佳實踐是可執行程序不需要被.PHONY修飾,clean需要被.PHONY修飾。因為清理工作效率很高,而且清理需要清理徹底。
這里又有一個問題,make是怎么知道code.c是否被編譯過呢?這里就涉及文件的修改時間,我們來看下面:
一般只要文件內容改變,文件屬性會跟著改變,因為涉及文件大小的變化,即便文件大小沒變,modify本身也是文件屬性,修改文件內容modify一定會被更改,所以change會隨著modify的改變而改變。如果我們修改文件權限的話就只有change改變。
編譯器判斷源文件是否被編譯過就是比較源文件和它的可執行文件的文件內容修改時間哪個更新,如果源文件更新編譯器就會編譯源文件,如果可執行文件更新說明源文件已經被編譯過了,而且源文件沒有被修改,那么就不會重新編譯。因為一開始只有源文件,所以兩者的modify時間一定有先有后,不可能重疊。
所以被.PHONY修飾過的文件總是被執行的原理就是讓make忽略源文件和可執行文件之間modify時間的對比,只要make就會執行依賴方法。
make指令默認只會形成一個目標文件,就是make在makefile里從上到下掃描遇到的第一個目標文件,如果我們要指定make其他文件就需要在make指令后面跟上其他目標文件的文件名。我們在工程中的最佳實踐是把要生成的可執行文件放前面,clean放后面。
makefile的推導過程
make根據依賴關系對makefile進行正向解析,解析過程中將依賴方法依次入棧,解析到末尾依次將依賴方法出棧并執行依賴方法,就可完成單次可執行程序的翻譯。
但是我們實際上是不會把編譯過程拆分的這么細的,我們平時的最佳實踐是把多個源文件都翻譯成.o文件,然后將多個.o文件與庫做鏈接生成最后的可執行程序。我們之前的例子只有一個源文件,所以直接將源文件翻譯成可執行程序也沒有任何問題。
補充:gcc的-S -c選項如果后面不跟-o指定文件的話會默認生成同名的.s和.o文件,如code.c會生成code.o。
makefile語法
前面我們編寫makefile是直接將方法和文件寫在makefile里,不過在實際工程中喜歡將它們設定義為變量,所以就會涉及一系列語法,小編來一一介紹。
- @
(關閉指令回顯)
我們前面在make 和 make clean之后系統會把具體指令顯示出來,如果我們不想讓他顯示就在makefile的指令前面加上@
- $
(取這個符號后面的括號里變量的內容)
有了這個符號我們在makefile里編寫指令就不用再出現具體的文件和指令了,我們只用寫出通用的makefile,只要想改變某個變量的內容一鍵替換就行了,類似于宏和全局變量。注意每個$()之間要用空格隔開。示例如下:
- $@、$^
($@代表:左邊的內容,$^代表:右邊所有內容)
$^可以包括:右邊所有文件,只是這里只有一個文件。
第一次優化:
我們之前介紹說先把源文件翻譯成目標文件然后再整體鏈接成可執行文件,所以我們這里也按這個思路優化一下makefile:
- wildcard函數
(獲取當前目錄下所有以.c結尾的源文件)
如果我們有很多源文件時,SRC=后面要把源文件挨個寫出來,很麻煩,所以這里引入wildcard函數(touch file{1…10}.c 表示一次性創建file1.c到file10.c 10個文件):
第二次優化:
我們已經可以把一次性所有,c文件獲取到全部翻譯成.o文件了,可是我們目前還無法一次性獲取所有.o文件,那么我們要把所有.o文件鏈接為可執行程序就要挨個將.o文件寫一遍嗎?并非如此,下面這條指令可以簡化我們的工作:
它可以將SRC里的所有.c 文件替換為.o文件然后將所有.o文件放到OBJ里。
一步登天:
這是我們要構建多個文件時makefile的最終形態,我們先提前科普幾個概念:
$(OBJ): $(SRC):
$(OBJ) 中每個.o 文件的生成,都依賴于 $(SRC) 中所有 .c 文件,大部分場景這種粗暴的依賴方式都用不到,而鏈接時是$(BIN): $(OBJ),需要由多個$(OBJ)里的目標文件鏈接成一個在$(BIN)里的可執行程序,所以鏈接時符合這個依賴關系。
%.o:%.c:
任何 .o 文件的生成,都依賴于同名的 .c 文件,這個就正好符合我們后面會介紹的將源文件翻譯成目標文件的場景。
$^:
將:右邊的所有文件一次性交給gcc來編譯。
$<:
將:右邊的所有文件一個一個的依次交給gcc來編譯,gcc一次只會編譯一個文件。
因為編譯源文件到目標文件需要一個一個文件單獨編譯,所以不能用$(BIN): $(OBJ)
依賴關系和$^,它們是用來一次性make多個文件的。最后鏈接操作符合我們上面介紹的規則。依賴方法執行完后還可以echo一句提示來讓用戶知道make具體做了哪些工作。
以上就是小編分享的全部內容了,如果覺得不錯還請留下免費的關注和收藏如果有建議歡迎通過評論區或私信留言,感謝您的大力支持。
一鍵三連好運連連哦~~