Makefile 是用于 make
工具的配置文件,它定義了如何編譯和鏈接你的項目,讓構建過程自動化。
一、核心概念
make
的核心思想是 “目標”(Target) 和 “依賴”(Dependencies):
- 目標 (Target):你想要生成的文件(比如可執行文件
myapp
)或者一個偽目標(比如clean
)。 - 依賴 (Dependencies):生成這個目標所必須的文件(比如源代碼
.cpp
文件、頭文件.h
文件)。 - 命令 (Commands):當依賴文件比目標文件更新(或目標不存在)時,需要執行的 shell 命令(比如
g++ -c main.cpp
)。
基本語法:
目標: 依賴1 依賴2 ...
<TAB> 命令1
<TAB> 命令2
...
關鍵點:
- 命令前面必須是
TAB
鍵,不能是空格!這是 Makefile 最常見的錯誤之一。 make
會檢查依賴文件的時間戳。如果任何一個依賴文件比目標文件新,make
就會執行對應的命令來更新目標。
二、一個簡單的例子
假設你有一個 C++ 項目,包含兩個源文件:
main.cpp
utils.cpp
utils.h
(被main.cpp
和utils.cpp
包含)
目標: 生成一個名為 myapp
的可執行文件。
Step 1: 最簡單的 Makefile
# Makefile# 目標: myapp
# 依賴: main.o 和 utils.o (編譯后的目標文件)
# 命令: 鏈接兩個 .o 文件生成 myapp
myapp: main.o utils.og++ main.o utils.o -o myapp# 目標: main.o
# 依賴: main.cpp 和 utils.h (因為 main.cpp 包含了它)
# 命令: 編譯 main.cpp 生成 main.o
main.o: main.cpp utils.hg++ -c main.cpp -o main.o# 目標: utils.o
# 依賴: utils.cpp 和 utils.h
# 命令: 編譯 utils.cpp 生成 utils.o
utils.o: utils.cpp utils.hg++ -c utils.cpp -o utils.o# 偽目標: clean (清理編譯產物)
# .PHONY 告訴 make 這不是一個真實文件,避免和同名文件沖突
.PHONY: clean
clean:rm -f myapp main.o utils.o# 偽目標: all (默認目標,通常放在最前面)
# 當你只輸入 'make' 時,會執行這個目標
.PHONY: all
all: myapp
如何使用:
- 將以上內容保存為
Makefile
(注意大小寫和無后綴)。 - 在終端中,進入包含
Makefile
和源代碼的目錄。 - 輸入
make
。make
會找到all
目標,并執行myapp
目標,從而編譯整個項目。 - 輸入
make clean
可以刪除編譯生成的文件。
三、使用變量讓 Makefile 更靈活
直接在命令里寫編譯器和選項很不方便。我們可以用變量。
# Makefile - 使用變量# 定義變量
CC = g++ # 編譯器
CFLAGS = -Wall -g # 編譯選項: -Wall 顯示所有警告, -g 生成調試信息
LDFLAGS = # 鏈接選項 (這里為空):鏈接動靜態庫
TARGET = myapp # 最終可執行文件名
OBJS = main.o utils.o # 所有目標文件# 鏈接目標
$(TARGET): $(OBJS)$(CC) $(LDFLAGS) $(OBJS) -o $(TARGET)# 編譯目標文件 (依賴關系保持不變)
main.o: main.cpp utils.h$(CC) $(CFLAGS) -c main.cpp -o main.outils.o: utils.cpp utils.h$(CC) $(CFLAGS) -c utils.cpp -o utils.o# 清理
.PHONY: clean
clean:rm -f $(TARGET) $(OBJS)
現在,如果你想換編譯器(比如用 clang++
),只需要修改 CC = clang++
這一行。
四、使用自動推導規則 (Implicit Rules)
make
內置了一些常識性的規則。例如,它知道如何從 .cpp
文件生成 .o
文件。
我們可以利用這一點,簡化編譯規則:
# Makefile - 使用內置規則CC = g++
CFLAGS = -Wall -g
TARGET = myapp
# 注意: 這里 OBJS 仍然需要明確列出,因為 make 不知道你的源文件是哪些
OBJS = main.o utils.o.PHONY: all
all: $(TARGET)$(TARGET): $(OBJS)$(CC) $(LDFLAGS) $(OBJS) -o $(TARGET)# 刪除了 main.o 和 utils.o 的顯式規則!
# make 會自動使用內置規則: $(CC) $(CFLAGS) -c source.cpp -o source.o.PHONY: clean
clean:rm -f $(TARGET) $(OBJS)
只要 CFLAGS
設置正確,make
就會自動用 g++ -Wall -g -c main.cpp -o main.o
這樣的命令。
五、自動依賴生成 (推薦)
手動維護頭文件依賴(如 main.o: main.cpp utils.h
)非常容易出錯且麻煩。我們可以讓編譯器幫我們生成。
# Makefile - 自動依賴生成CC = g++
CFLAGS = -Wall -g
TARGET = myapp
SRCS = main.cpp utils.cpp # 源文件列表
OBJS = $(SRCS:.cpp=.o) # 將 .cpp 替換為 .o, 得到 main.o utils.o
DEPS = $(SRCS:.cpp=.d) # 依賴文件列表, 如 main.d, utils.d.PHONY: all
all: $(TARGET)$(TARGET): $(OBJS)$(CC) $(LDFLAGS) $(OBJS) -o $(TARGET)
解釋:
%.o: %.cpp
是一個模式規則,%
是通配符。它告訴make
如何將任意.cpp
文件編譯成.o
文件。
六、Makefile
常用的幾個自動變量
👍 make
提供了幾個非常有用的自動變量 (Automatic Variables),它們可以在規則的命令中使用,讓 Makefile 更加簡潔和通用。
核心自動變量
這些變量在每個規則的命令執行時,會根據當前規則的上下文自動展開。
-
$@
- 目標文件名 (Target)-
含義:代表當前規則的目標 (Target)。
-
用途:當你有多個規則,且目標名各不相同時,用
$@
可以避免重復寫目標名。 -
例子:
# 假設當前規則是: program: main.o utils.o # 那么 $@ 就代表 "program" program: main.o utils.o$(CC) $^ -o $@ # 等價于 $(CC) main.o utils.o -o program
-
-
$^
- 所有依賴文件名列表 (All Prerequisites)-
含義:代表當前規則中列出的所有依賴項 (Dependencies),用空格分隔。如果有重復的依賴,
$^
會包含重復項。 -
用途:在鏈接或編譯命令中,一次性引用所有依賴。
-
例子:
# 規則: program: main.o utils.o # 那么 $^ 就代表 "main.o utils.o" program: main.o utils.o$(CC) $^ -o $@ # 等價于 $(CC) main.o utils.o -o program
-
-
$<
- 第一個依賴文件名 (First Prerequisite)-
含義:代表當前規則中的第一個依賴項。
-
用途:在模式規則中非常有用,比如從
.cpp
編譯.o
時,$<
就是.cpp
文件。 -
例子:
# 模式規則: %.o: %.cpp # 當 make 處理 main.cpp -> main.o 時 # $< 代表 "main.cpp", $@ 代表 "main.o" %.o: %.cpp$(CC) $(CFLAGS) -c $< -o $@ # 等價于 g++ -c main.cpp -o main.o
-
為什么 $^
和 $<
有區別?(重要!)
雖然在很多簡單情況下 $^
和 $<
看起來一樣(比如只有一個依賴),但當依賴有多個或有重復時,它們就不同了。
$^
包含所有依賴,包括重復項。$<
只包含第一個依賴。
例子:
# 假設有一個奇怪的規則(實際很少見)
program: main.o utils.o main.o # main.o 被列了兩次@echo "Dependencies: $^" # 輸出: Dependencies: main.o utils.o main.o@echo "First dep: $<" # 輸出: First dep: main.o
在鏈接命令中,你通常希望傳遞所有目標文件,所以用 $^
。而在編譯單個源文件時,你只需要當前的源文件,所以用 $<
。
使用自動變量優化之前的 Makefile
讓我們用這些自動變量來簡化之前的例子:
# Makefile - 使用自動變量CC = g++
CFLAGS = -Wall -g
TARGET = myapp
SRCS = main.cpp utils.cpp
OBJS = $(SRCS:.cpp=.o)
DEPS = $(SRCS:.cpp=.d).PHONY: all
all: $(TARGET)# 鏈接規則: 使用 $^ (所有 .o 文件) 和 $@ (目標可執行文件)
$(TARGET): $(OBJS)$(CC) $^ -o $@.PHONY: clean
clean:rm -f $(TARGET) $(OBJS) $(DEPS)
優點:
- 更簡潔:
$(CC) $^ -o $@
比$(CC) $(LDFLAGS) $(OBJS) -o $(TARGET)
短且清晰。 - 更通用:這個鏈接規則可以用于任何由
.o
文件鏈接而成的目標,你不需要修改命令。 - 更可靠:在模式規則中,
$<
確保只傳遞當前正在編譯的源文件。
總結
$@
= 目標 (Target) - 你要生成的東西。$^
= 所有依賴 (All Prerequisites) - 生成目標需要的所有輸入文件(鏈接時用)。$<
= 第一個依賴 (First Prerequisite) - 通常用于編譯單個源文件時的源文件名。