如果我們是在Linux下開發,那Makefile肯定要知道,不懂Makefile,面對較大的工程項目的時候就會比較麻煩,懂得利用開發工具將會大大提高我們的開發效率,也可以說Makefile是必須掌握的一項技能。
一、了解什么是 Makefile
一個大型工程中的源文件不計其數,各個功能或者模塊分別放在不同的目錄下,手動敲命令去編譯就帶來很大的麻煩,那么Makefile可以定義一系列的編譯規則,哪些文件需要先編譯,哪些文件需要后編譯,哪些文件需要重新編譯,甚至進行更復雜的功能操作,Makefile帶來的好處就是——“自動化編譯”,一旦寫好,只需要一個make命令,整個工程完全自動編譯,極大的提高軟件開發的效率。
make?是一個命令工具,是一個解釋Makefile中指令的命令工具,一般來說,大多數的IDE都有這個命令,比如:Linux下GNU的make、Visual C++的nmake、Delphi的make。可見,Makefile都成為了一種在工程方面的編譯方法。當然,不同產商的make各不相同,也有不同的語法,但其本質都是在 “文件依賴性” 上做文章。
二、明白編譯鏈接過程
在編寫Makefile之前,還是要先了解清楚程序編譯鏈接過程,無論是c、c++,首先要把源文件編譯成中間代碼文件,在Windows下也就是 .obj 文件,Unix/Linux下是 .o 文件,即 Object File,這個動作叫做編譯(compile)。然后再把大量的Object File合成執行文件,這個動作叫作鏈接(link)。
編譯時,編譯器需要的是語法的正確,函數與變量的聲明的正確。對于后者,通常是你需要告訴編譯器頭文件的所在位置(頭文件中應該只是聲明,而定義應該放在C/C++文件中),只要所有的語法正確,編譯器就可以編譯出中間目標文件。一般來說,每個源文件都應該對應于一個中間目標文件(O文件或是OBJ文件)。
鏈接時,主要是鏈接函數和全局變量,所以,我們可以使用這些中間目標文件(O文件或是OBJ文件)來鏈接我們的應用程序。鏈接器并不管函數所在的源文件,只管函數的中間目標文件(Object File),在大多數時候,由于源文件太多,編譯生成的中間目標文件太多,而在鏈接時需要明顯地指出中間目標文件名,這對于編譯很不方便,所以,我們要給中間目標文件打個包,在Windows下這種包叫“庫文件”(Library File),也就是 .lib 文件,在Unix/Linux下是Archive File,也就是 .a 文件,也叫靜態庫文件。
總結一下,編譯鏈接的過程如下:
源文件首先會生成中間目標文件,再由中間目標文件生成執行文件。
在編譯時,編譯器只檢測程序語法,和函數、變量是否被聲明。如果函數未被聲明,編譯器會給出一個警告,但可以生成Object File。
在鏈接程序時,鏈接器會在所有的Object File中找尋函數的實現,如果找不到,那就會報鏈接錯誤碼(Linker Error),在VC下,這種錯誤一般是:Link 2001錯誤,意思是說,鏈接器未能找到函數的實現。你需要指定函數的Object File。
三、編寫一個簡單的 Makefile
1. Makefile 的基本語法規則:
?
目標?...?:?依賴?... ????????實現目標的具體表達式(命令) ????????... ????????...
?
目標(target):就是一個目標文件,可以是Object 文件,也可以是執行文件,還可以是一個標簽(Label);
依賴(prerequisites):就是要生成那個target所需要的文件或是目標;
命令(command):Shell命令,也就是make工具需要執行的命令。
【總結】:通過依賴(prerequisites)中的一些文件生成目標(target)文件,目標文件要按照命令(command)中定義的規則來生成。
2. 來看一個簡單的示例代碼
簡單寫三個方法文件(openFile.c、readFile.c、writeFile.c)、一個頭文件(operateFile.h)和一個主函數文件(main.c),代碼如下:
?
//?openFile.c #include?"operateFile.h" void?openFile() { ????printf("open?file........... "); }
//?readFile.c #include?"operateFile.h" void?readFile() { ????printf("read?file........... "); }
//?writeFile.c #include?"operateFile.h" void?writeFile() { ????printf("write?file........... "); }
//?operateFile.h #ifndef?__OPERATEFILE_H__ #define?__OPERATEFILE_H__ #include?void?openFile(void); void?readFile(void); void?writeFile(void); #endif
//?main.c #include?#include?"operateFile.h" int?main() { ????openFile(); ????readFile(); ????writeFile(); ???? ????return?0; }
?
3. 根據上面的語法規則及編譯鏈接過程編寫一個Makefile文件
?
main:main.o?openFile.o?readFile.o?writeFile.o??#?main生成所需要的.o文件 ????gcc?-o?main?main.o?openFile.o?readFile.o?writeFile.o??#?生成main的規則
?
main.o:main.c??#?mian.o文件生成所需要的mian.c文件
????gcc?-c?main.c openFile.o:openFile.c ????gcc?-c?openFile.c readFile.o:readFile.c ????gcc?-c?readFile.c writeFile.o:writeFile.c ????gcc?-c?writeFile.c
clean:????#?需要手動調用
????rm?*.o?main
注意:Makefile的注釋符號是 ‘#’。
4. 編寫完成后,執行make命令,make會在當前目錄下找到名字為Makefile或makefile的文件,程序就會自動運行,產生相應的中間文件和可執行文件
a. 如果執行make出現如下信息,那就是命令行(makefile中的gcc或者rm)前面沒有用tab鍵縮進,不能用空格:
b. 如果執行make出現如下信息,那就是你的代碼沒有修改過,Makefile拒絕你的請求:
這里還會有一種情況就是如果只修改過其中一個文件,那么重新編譯就可以看到只編譯修改的那個文件,沒有編譯其他未修改的文件,避免了重復編譯。這里可以想象在一個大型源碼的工程或者一個內核源碼,里面的源文件上千或上萬個,如果只修改了一個小問題,就要全部重新編譯,就會花費大量編譯的過程,Makefile就可以避免這個問題,而且支持多線程并發操作,可以減少很多編譯的時間,提高工作效率。
那么Makefile是如何判斷文件是否有修改過呢??
Makefile是通過對比時間戳,當我們生成中間文件或可執行文件之后,他們的創建時間肯定要比 .c文件最后修改的時間晚,如果某個 .c文件有新修改過,它的時間戳肯定會比原來生成中間文件或可執行文件的時間戳晚,這樣就判斷這個 .c文件有被更新過,就會重新編譯它。
5. 正常運行后,執行可執行文件輸入 ./main 即可,就能看到代碼執行的結果
6. 在makefile文件的最后可以看到有個clean,這個clean就是前面所說的標簽,它不是一個文件,所以make無法生成它的依賴關系和決定它是否要執行,只能通過顯示指定這個目標才可以 ,通過make clean的指令就可以執行clean下面的命令。
到這里,一個基礎版的Makefile就完成了。
四、Makefile的優化
學會了編寫基礎版的Makefile后,就可以對剛剛寫的Makefile進行優化。
? 優化1:省略命令
我們將上面寫的基礎版Makefile改成下面這樣的省略版:
?
main:main.o?openFile.o?readFile.o?writeFile.o??? ????gcc?-o?main?main.o?openFile.o?readFile.o?writeFile.o?? clean:????? ????rm?*.o?main執行make后的結果:
?
可以看到,這些文件都在同一目錄下的時候,省略版和基礎版的結果是一樣的,省略版的makefile中去掉了生成main.o、openFile.o、readFile.o和writeFile.o這些目標的依賴和生成命令,這就是make的隱含規則,make會試圖去自動推導產生這些目標的依賴和生成命令,這個行為就是隱含規則的自動推導。
優化2:引入變量
這里引入變量的意思有點像使用宏替換,改成$(變量名),$是格式:
?
TARGET?=?main OBJS?=?main.o?openFile.o?readFile.o?writeFile.o CC?=?gcc $(TARGET):$(OBJS) ????$(CC)?-o?$(TARGET)?$(OBJS) clean:????? ????rm $(OBJS)?$(TARGET)
?
優化3:引入函數 格式:$(函數名? 實參列表)
#?函數1 $(wildcard??*.c)? ??#?表示當前路徑下的所有的?.c
#?函數2 $(patsubst?%.c,?%.o,?所有的.c文件)????#?生成中間文件?.o
#?函數3 $(notdir?xxx)???#?去除xxx文件的絕對路徑,只保留文件名引入函數后的Makefile版本可以改寫成:
TARGET?=?main? SOURCE?=?$(wildcard?*.c) OBJS?=?$(patsubst?%.c,?%.o,?$(SOURCE)) CC?=?gcc $(TARGET):$(OBJS) ????$(CC)?-o?$(TARGET)?$(OBJS) ???? clean:????? ????rm $(OBJS)?$(TARGET)
?
優化4:對文件進行分類管理 在一個實際工程項目中程序文件比較多,我們就會對文件按照文件類型進行分類,分為頭文件、源文件、目標文件和可執行文件,分別放在不同的目錄中,由Makefile統一管理這些文件,將生產的目標文件放在目標目錄下,可執行文件放到可執行目錄下,分類目錄如下圖:
bin目錄:放可執行文件
include目錄:放頭文件
obj目錄:放中間目標文件
src目錄:放源文件
可見原來那些文件都不在同一目錄下了,那么這時候如果還用之前的Makefile,make就沒法處理了,自動推導也會無法進行,就需要改成如下:
?
INC_DIR?=?./include BIN_DIR?=?./bin SRC_DIR?=?./src OBJ_DIR?=?./obj ? SRC?=?$(wildcard?$(SRC_DIR)/*.c)? ? ? # /*/ OBJ?=?$(patsubst?%.c,?$(OBJ_DIR)/%.o,?$(notdir?$(SRC))) ? TARGET?=?main BIN_TARGET?=?$(BIN_DIR)/$(TARGET) ? CC?=?gcc ? $(BIN_TARGET):$(OBJ) ????$(CC)?$(OBJ)?-o?$@ ? $(OBJ_DIR)/%.o:$(SRC_DIR)/%.c ????$(CC)?-I$(INC_DIR)?-c?$-o?$@ ? clean: ????find?$(OBJ_DIR)?-name?*.o?-exec?rm?-rf?{}?;??#?刪除?.o?文件 ????rm?$(BIN_TARGET)???#?刪除可執行文件main在Makefile中,最終要生成可執行文件main我們把它叫做終極目標,其它所有的 .o 文件本身也是一個目標,也需要編譯生成,工程里面許多的 .c 就會生成許多的 .o,每一個 .c 都寫一遍目標依賴命令顯然是不可行的,于是就有了類似for循環的東西,把所有目標變成一個集合,但不是真正用for循環,而是使用一些抽象的符號表示,解釋如下:
?
%.o:所有 .o 結尾的文件
%.c:所有 .c 結尾的文件
$@:表示目標文件
$<:表示第一個依賴文件,也叫初級依賴
$^:表示所有的依賴文件,也叫終極依賴
? 當然,不止只有這些符號,只是列舉了上面出現的或者常見的。
執行make后的結果:
? make執行后bin目錄里面已經生成了可執行文件main,obj目錄里面已經生成了中間目標文件 main.o、openFile.o、readFile.o、writeFile.o,最后執行main后的結果也是和前面基礎版的Makefile的結果是一樣的。 ?
? ok,看到這里應該對Makefile有了一定的了解,可以動手敲一敲用起來!
?
審核編輯:湯梓紅
評論
查看更多