4. Make 构建工具:Makefile 基础语法、目标与依赖、变量与自动变量、伪目标(.PHONY)
说到 C/C++ 项目的构建,Make 绝对是个绕不开的老伙计。我从刚入行就开始用 Make,到现在十几年了,它依然是我工具箱里的常客。说白了,Make 就是个自动化构建引擎,你告诉它「怎么编译、先编译谁」,它就能帮你搞定一切。
这一节,咱们就把它最核心的几个概念掰开揉碎了讲清楚。
4.1 Makefile 基础语法
Makefile 的语法其实很简单,核心就是一条条规则。每条规则长这样:
目标: 依赖
<Tab>命令
注意那个 <Tab>,必须是 Tab 键,不能用空格代替。我见过不少新手在这上面栽跟头,编译报错半天找不到原因,结果就是 Tab 写成了空格。嗯,这里要特别提醒你。
来看个最简单的例子:
hello: hello.c
gcc -o hello hello.c
这条规则的意思是:要生成 hello 这个文件,需要依赖 hello.c。如果 hello.c 比 hello 新,就执行下面的 gcc 命令重新编译。
.ONESHELL:,这样整个规则的所有命令会在同一个 shell 里执行,避免变量传递的问题。
4.2 目标与依赖
目标(target)和依赖(prerequisite)是 Makefile 的灵魂。目标通常是你要生成的文件,依赖是生成目标所需要的源文件或其他目标。
咱们来看一个稍微复杂点的例子:
main: main.o utils.o
gcc -o main main.o utils.o
main.o: main.c main.h
gcc -c main.c
utils.o: utils.c utils.h
gcc -c utils.c
这里 main 是最终目标,它依赖 main.o 和 utils.o。而这两个 .o 文件又各自依赖对应的 .c 和 .h 文件。Make 会递归地检查所有依赖,只重新编译那些有变化的文件。
我在项目中遇到过一种情况:有人把所有源文件都写在一个规则里,每次改一个文件都要全部重新编译。一个项目编译半小时,改一行代码又得等半小时。你想想看,这多浪费时间?正确的做法就是像上面这样,把依赖关系拆细。
| 概念 | 说明 | 示例 |
|---|---|---|
| 最终目标 | make 命令默认构建的目标 | main |
| 中间目标 | 被其他目标依赖的中间产物 | main.o |
| 依赖文件 | 源文件或头文件 | main.c, utils.h |
4.3 变量与自动变量
写 Makefile 最怕什么?重复。一个项目几十个源文件,每个都写一遍 gcc 命令,累不累?这时候变量就派上用场了。
CC = gcc
CFLAGS = -Wall -O2
OBJS = main.o utils.o
main: $(OBJS)
$(CC) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
这里 CC、CFLAGS、OBJS 都是变量。用 $(变量名) 来引用。改编译器?改一行 CC = clang 就行。加编译选项?改 CFLAGS 就行。
自动变量才是真正的黑科技。上面例子里的 $@、$^、$< 就是自动变量,它们会自动匹配当前规则的目标和依赖。
| 自动变量 | 含义 | 示例值 |
|---|---|---|
$@ |
当前规则的目标 | main.o |
$^ |
所有依赖文件列表 | main.c main.h |
$< |
第一个依赖文件 | main.c |
$? |
比目标新的依赖文件 | main.c |
%.o: %.c 这条模式规则,可以匹配所有 .c 到 .o 的编译,不用为每个文件单独写规则。
4.4 伪目标(.PHONY)
伪目标这个概念,我第一次接触时也懵了一下。说白了,伪目标就是那些不代表实际文件的目标。比如 clean、all、install 这些。
为什么要用伪目标?假设你有个 clean 目标,用来删除编译产物:
clean:
rm -f *.o main
如果当前目录下恰好有个叫 clean 的文件,Make 会认为这个目标已经是最新的了,就不会执行 rm 命令。我曾经就因为这个吃过亏,明明改了代码,make clean 却什么都不做,排查了半天才发现是目录里多了个 clean 文件。
解决办法就是用 .PHONY 声明伪目标:
.PHONY: clean all install
clean:
rm -f *.o main
all: main
install: main
cp main /usr/local/bin/
声明为伪目标后,Make 就不会去检查同名文件了,每次都会执行对应的命令。
all(构建所有)、clean(清理)、install(安装)、test(测试)。建议把所有不生成文件的目标都加上 .PHONY 声明。
4.5 知识体系总览
下面这张图把 Makefile 的核心概念串起来了,你可以对照着理解它们之间的关系:
4.6 实战:一个完整的 Makefile
最后,咱们把上面讲的东西串起来,写一个完整的 Makefile。这个是我在实际项目中常用的模板,你拿过去改改就能用:
CC = gcc
CFLAGS = -Wall -O2 -Iinclude
LDFLAGS = -lm
SRCS = main.c utils.c network.c
OBJS = $(SRCS:.c=.o)
TARGET = myapp
.PHONY: all clean install test
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) -o $@ $^ $(LDFLAGS)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) $(TARGET)
install: $(TARGET)
cp $(TARGET) /usr/local/bin/
test: $(TARGET)
./$(TARGET) --test
这个 Makefile 里,$(SRCS:.c=.o) 是个变量替换技巧,把 .c 后缀换成 .o。模式规则 %.o: %.c 自动匹配所有源文件。伪目标 all、clean、install、test 都加了 .PHONY 声明。
我个人习惯把 all 放在第一个目标,这样直接执行 make 就会构建整个项目。另外,test 目标我建议也加上,每次改完代码跑一遍测试,能省不少调试时间。