Makefile 入門
掃描二維碼
隨時隨地手機看文章
點擊上方藍字關(guān)注我哦~
01
前言
今天抽空研究了下 Makefile,在這里整理一下各處搜到的資料,以備將來復習時快速上手,同時也幫助和我一樣的初學者們節(jié)約時間。
02
準備工作
首先,假設我們有如下幾個代碼文件:main.cpp functions.h function1.cpp function2.cpp
--- functions.h ---
// functions.h
void print_hello();
int factorial(int n);
--- function1.cpp ---
// function1.cpp
int factorial(int n){
if (n!=1)
return n*factorial(n-1);
else return 1;
}
--- function2.cpp ---
//function2.cpp
void print_hello()
{
std::cout << "hello world" << std::endl;
}
--- main.cpp ---
//main.cpp
int main(){
print_hello();
std::cout << "this is main" << std::endl;
std::cout << "The factorial of 5 is " << factorial(5) << std::endl;
return 0;
}
03
不用makefile如何編譯
如果不用 makefile,則需要按照下面的方式編譯上述代碼:
g++ -c function1.cpp
g++ -c function2.cpp
g++ -c main.cpp
g++ -o hello main.o function1.o function2.o
其中,g++ -c function1.cpp 會將源碼編譯成名為 function1.o 對象文件。如果不想采用默認的命名,也可以自定義文件名,例如:
g++ -c function1.cpp -o fun1.o
也可以用一行命令整合編譯、鏈接的步驟:
g++ -o hello main.cpp function1.cpp function2.cpp
這種方式有很多弊端,例如:
每次編譯、鏈接都需要手動敲的很多命令。
當工程量很大時,編譯整個工程需要花很久。而我們往往并不是每次都修改了所有源文件,因此希望程序自動編譯那些被修改的源碼,而沒被修改的部分不要浪費時間重新編譯。
為了解決上述第一個問題,我們可以把所有編譯需要的命令保存到文件中,編譯時一鍵執(zhí)行。針對第二個問題,我們希望有一個軟件,自動檢測哪些源文件被修改過,然后自動把它們挑出來選擇性地編譯。而 make 命令通過檢測代碼文件的時間戳,決定是否編譯它。
04
使用Makefile編譯
第一版Makefile
首先需要確定 Makefile 的名字,需要設置成 Makefile 或者 makefile,而不能是其它版本(MakeFile, Make_file, makeFile,... )。其次,需要注意的是 Makefile 是縮進敏感的,在行首一定不能隨便打空格。下面我們看一下第一版 Makefile。
# Makefile (#號為注釋)
all:
g++ -o hello main.cpp function1.cpp function2.cpp
clean:
rm -rf *.o hello
(注意上面代碼片段的縮進,是一個<tab>而不是4個或者8個空格。)
其中 all 、clean的術(shù)語為 target,我也可以隨意指定一個名字,例如 abc,真正執(zhí)行編譯的是它下面縮進行的命令。我們可以看到,這個命令和我們在命令行中手動敲的沒有任何區(qū)別。因此,通過這個簡單的 Makefile,就可以省去了每次手動敲命令的痛苦:只需要在命令行敲下 make 回車,即可完成編譯。
clean 表示清除編譯結(jié)果,它下方就是普通的命令行刪除文件命令。命令行輸入 make 將默認執(zhí)行第一個 target (即 all)下方的命令;如要執(zhí)行清理操作,則需要輸入 make clean,指定執(zhí)行 clean 這個 target 下方的命令。
這個 Makefile 雖然可以省去敲命令的痛苦,卻無法選擇性編譯源碼。因為我們把所有源文件都一股腦塞進了一條命令,每次都要編譯整個工程,很浪費時間。第二版 Makefile 將解決這個問題。
第二版Makefile
既然我們希望能夠選擇性地編譯源文件,就不能像上一節(jié)那樣把所有源文件放在一條命令里編譯了,而是要分開寫:
all: hello
hello: main.o function1.o function2.o
g++ main.o function1.o function2.o -o hello
main.o: main.cpp
g++ -c main.cpp
function1.o: function1.cpp
g++ -c function1.cpp
function2.o: function2.cpp
g++ -c function2.cpp
clean:
rm -rf *.o hello
上面的 Makefile 包含了一條重要的語法:<target>:<dependencies>。即,目標:目標依賴的文件。
順著代碼捋一下邏輯:
命令行輸入 make ,將默認執(zhí)行 all 這個 target;
而 all 這個 target 依賴于 hello,hello 在當前目錄下并不存在,于是程序開始往下讀取命令..……終于找到了 hello 這個 target;
正待執(zhí)行 hello 這個 target 的時候,卻發(fā)現(xiàn)它依賴于 main.o,function1.o,function2.o 這三個文件,而它們在當前目錄下都不存在,于是程序繼續(xù)向下執(zhí)行;
-
遇到 main.o target,它依賴于 main.cpp。而 main.cpp 是當前目錄下存在的文件,終于可以編譯了,生成 main.o 對象文件。后面兩個函數(shù)以此類推,都編譯好之后,再回到 hello target,連接各種二進制文件,生成 hello 文件。
第一次編譯的時候,命令行會輸出:
g++ -c main.cpp
g++ -c function1.cpp
g++ -c function2.cpp
g++ main.o function1.o function2.o -o hello
證明所有的源碼都被編譯了一遍。假如我們對 main.cpp 做一點修改,再重新 make(重新 make 前不要 make clean),則命令行只會顯示:
g++ -c main.cpp
g++ main.o function1.o function2.o -o hello
這樣,我們就發(fā)揮出 Makefile 選擇性編譯的功能了。下面,將介紹如何在 Makefile 中聲明變量(declare variable)
第三版Makefile
我們希望將需要反復輸入的命令整合成變量,用到它們時直接用對應的變量替代,這樣如果將來需要修改這些命令,則在定義它的位置改一行代碼即可。
CC = g++
CFLAGS = -c -Wall
LFLAGS = -Wall
all: hello
hello: main.o function1.o function2.o
$(CC) $(LFLAGS) main.o function1.o function2.o -o hello
main.o: main.cpp
$(CC) $(CFLAGS) main.cpp
function1.o: function1.cpp
$(CC) $(CFLAGS) function1.cpp
function2.o: function2.cpp
$(CC) $(CFLAGS) function2.cpp
clean:
rm -rf *.o hello
上面的 Makefile 中,開頭定義了三個變量:CC,CFLAGS,和 LFLAGS。其中 CC 表示選擇的編譯器(也可以改成 gcc);CFLAGS 表示編譯選項,-c 即 g++ 中的 -c,-Wall 表示顯示編譯過程中遇到的所有 warning;LFLAGS 表示鏈接選項,它就不加 -c 了。這些名字都是自定義的,真正起作用的是它們保存的內(nèi)容,因此只要后面的代碼正確引用,將它們定義成阿貓阿狗都沒問題。容易看出,引用變量名時需要用 $() 將其括起來,表示這是一個變量名。
第四版Makefile
第三版的 Makefile 還是不夠簡潔,例如我們的 dependencies 中的內(nèi)容,往往和 g++ 命令中的內(nèi)容重復:
hello: main.o function1.o function2.o
$(CC) $(LFLAGS) main.o function1.o function2.o -o hello
我們不想敲那么多字,能不能善用 <target>:<dependencies> 中的內(nèi)容呢?這就需要引入下面幾個特殊符號了(也正是這些特殊符號,把 Makefile 搞得像是天書,嚇退了很多初學者):
$@ ,$<,$^
例如我們有 target: dependencies 對:all: library.cpp main.cpp
$@ 指代 all ,即 target
$< 指代 library.cpp, 即第一個 dependency
$^ 指代 library.cpp 和 main.cpp,即所有的 dependencies
因此,本節(jié)開頭的 Makefile 片段可以改為:
hello: main.o function1.o function2.o
$(CC) $(LFLAGS) $^ -o $@
而第四版 Makefile 就是這樣的:
CC = g++
CFLAGS = -c -Wall
LFLAGS = -Wall
all: hello
hello: main.o function1.o function2.o
$(CC) $(LFLAGS) $^ -o $@
main.o: main.cpp
$(CC) $(CFLAGS) $<
function1.o: function1.cpp
$(CC) $(CFLAGS) $<
function2.o: function2.cpp
$(CC) $(CFLAGS) $<
clean:
rm -rf *.o hello
但是手動敲文件名還是有點麻煩,能不能自動檢測目錄下所有的 cpp 文件呢?此外 main.cpp 和 main.o 只差一個后綴,能不能自動生成對象文件的名字,將其設置為源文件名字后綴換成 .o 的形式?
第五版Makefile
想要實現(xiàn)自動檢測 cpp 文件,并且自動替換文件名后綴,需要引入兩個新的命令:patsubst 和 wildcard。
wildcard
wildcard 用于獲取符合特定規(guī)則的文件名,例如下面的代碼:
SOURCE_DIR = . # 如果是當前目錄,也可以不指定
SOURCE_FILE = $(wildcard $(SOURCE_DIR)/*.cpp)
target:
@echo $(SOURCE_FILE)
make 后發(fā)現(xiàn),輸出的為當前目錄下所有的 .cpp 文件:
./function1.cpp ./function2.cpp ./main.cpp
其中 @echo 前加 @是為了避免命令回顯,上文中 make clean 調(diào)用了 rm -rf 會在 terminal 中輸出這行命令,如果在 rm 前加了 @ 則不會輸出了。
patsubst
patsubst 應該是 pattern substitution 的縮寫。用它可以方便地將 .cpp 文件的后綴換成 .o。它的基本語法是:$(patsubst 原模式,目標模式,文件列表)。運行下面的示例:
SOURCES = main.cpp function1.cpp function2.cpp
OBJS = $(patsubst %.cpp, %.o, $(SOURCES))
target:
@echo $(SOURCES)
@echo $(OBJS)
輸出的結(jié)果為:
main.cpp function1.cpp function2.cpp
main.o function1.o function2.o
綜合上述兩個命令,我們可以升級到第五版 Makefile:
OBJS = $(patsubst %.cpp, %.o, $(wildcard *.cpp))
CC = g++
CFLAGS = -c -Wall
LFLAGS = -Wall
all: hello
hello: $(OBJS)
$(CC) $(LFLAGS) $^ -o $@
main.o: main.cpp
$(CC) $(CFLAGS) $< -o $@
function1.o: function1.cpp
$(CC) $(CFLAGS) $< -o $@
function2.o: function2.cpp
$(CC) $(CFLAGS) $< -o $@
clean:
rm -rf *.o hello
然而這一版的 Makefile 還有提升空間,它的 main.o,function1.o,function2.o 使用的都是同一套模板,不過換了個名字而已。第六版的 Makefile 將處理這個問題。
第六版Makefile
這里要用到 Static Pattern Rule,其語法為:
targets: target-pattern: prereq-patterns
其中 targets 不再是一個目標文件了,而是一組目標文件。而 target-pattern 則表示目標文件的特征。例如目標文件都是 .o 結(jié)尾的,那么就將其表示為 %.o,prereq-patterns (prerequisites) 表示依賴文件的特征,例如依賴文件都是 .cpp 結(jié)尾的,那么就將其表示為 %.cpp。
通過上面的方式,可以對 targets 列表中任何一個元素,找到它對應的依賴文件,例如通過 targets 中的 main.o,可以鎖定到 main.cpp。
下面是第六版的 Makefile:
OBJS = $(patsubst %.cpp, %.o, $(wildcard *.cpp))
CC = g++
CFLAGS = -c -Wall
LFLAGS = -Wall
all: hello
hello: $(OBJS)
$(CC) $(LFLAGS) $^ -o $@
$(OBJS):%.o:%.cpp
$(CC) $(CFLAGS) $< -o $@
clean:
rm -rf *.o hello
05
其它
看到有的 Makefile 設置了 -lm 的 flag,查閱資料發(fā)現(xiàn)表示連街 math 庫,因為代碼中可能
例如
g++ -o out fun.cpp -lm
CC = g++
LIBS = -lm
out: fun.cpp
$(CC) -o $@ $^ $(LIBS)
/ The End /
本文介紹了如何寫 Makefile,主要的知識點有:
在 Makefile 中定義變量并引用
$^,$@,$< 的含義
wildcard,patsubst 的用法
-
static pattern rule:targets: target-pattern: prereq-patterns
公眾號回復:"Makefile" 獲取本文源文件下載鏈接。
推薦閱讀:
免責聲明:本文轉(zhuǎn)載自知乎,版權(quán)歸原作者所有。如涉及作品版權(quán)問題,請與我聯(lián)系刪除。
掃碼關(guān)注我們
看更多嵌入式案例
喜歡本篇內(nèi)容請給我們點個在看
免責聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!