4. 自动变量与模式匹配:$@, $^, $< 等自动变量,以及 % 模式规则的使用
写 Makefile 最怕什么?我个人觉得,最怕的就是「重复」。一个项目里几十个 .c 文件,要是每个文件都写一条编译规则,那 Makefile 比源代码还长,维护起来简直要命。
还好,Make 提供了两样好东西来解决这个问题:自动变量和模式规则。说白了,它们就是 Makefile 里的「循环」和「模板」。今天我们就来彻底搞懂它们。
核心思想:自动变量让你在规则里引用「当前目标」和「当前依赖」,模式规则让你用通配符 % 匹配一类文件。两者结合,一条规则就能处理成千上万个文件。
4.1 自动变量:规则里的「代词」
先看一个最基础的编译规则:
main.o: main.c
gcc -c main.c -o main.o
这条规则只针对 main.o。如果我有 10 个 .c 文件呢?写 10 遍?
自动变量就是来解决这个问题的。它们像英语里的代词,指代规则中的「目标」和「依赖」。最常用的有三个:
| 自动变量 | 含义 | 举例(规则 foo.o: foo.c bar.h) |
|---|---|---|
$@ |
规则的目标文件名 | foo.o |
$^ |
所有依赖文件列表(去重) | foo.c bar.h |
$< |
第一个依赖文件 | foo.c |
用自动变量改写上面的规则:
main.o: main.c
gcc -c $< -o $@
你看,这条规则现在可以「套用」到任何 .o 文件上。只要把目标名和依赖名换掉,命令部分完全不用改。
我的习惯:编译单个 .c 到 .o 时,我几乎只用 $< 和 $@。$^ 更多用在链接阶段,因为链接需要所有 .o 文件。
再看链接阶段的例子:
app: main.o utils.o
gcc $^ -o $@
这里 $^ 展开成 main.o utils.o,$@ 展开成 app。简洁明了。
注意:$^ 会自动去除重复的依赖。如果你需要保留重复项,用 $+。不过说实话,我在项目中很少遇到需要保留重复依赖的场景。
除了这三个,还有几个用得相对少一些的:
$?:所有比目标新的依赖文件列表。增量编译时有用。$*:目标文件名中去除后缀的部分。比如目标 foo.o,$*就是 foo。$%:当目标是归档文件(.a)时,表示其中的成员名。嵌入式开发中偶尔用到。
4.2 模式规则:用 % 匹配一类文件
自动变量解决了命令部分的重复问题,但规则本身还是得一条条写。模式规则就是来消灭规则重复的。
% 是 Makefile 里的通配符,它匹配任意非空字符串。看一个最经典的模式规则:
%.o: %.c
gcc -c $< -o $@
这条规则的意思是:任何 .o 文件,如果它对应的 .c 文件存在,就用这条规则来编译。
比如 Make 需要生成 main.o,它会找 main.c,找到了就执行 gcc -c main.c -o main.o。需要生成 utils.o?同样适用。
模式规则里的 % 会匹配相同的部分。在 %.o: %.c 中,两个 % 匹配的是同一个字符串。
模式规则 vs 普通规则:模式规则是「模板」,普通规则是「实例」。Make 会先找普通规则,找不到才用模式规则匹配。
4.3 多依赖模式规则
有时候一个 .o 文件依赖多个文件,比如头文件。模式规则也能处理:
%.o: %.c %.h
gcc -c $< -o $@
但这里有个问题:如果某个 .c 文件没有对应的 .h 文件,这条规则就匹配不上。所以更常见的做法是:
%.o: %.c
gcc -c $< -o $@
main.o: main.h
utils.o: utils.h config.h
模式规则负责编译,普通规则负责声明头文件依赖。两者配合,既简洁又准确。
我曾经在一个项目中看到有人把所有头文件依赖都写在模式规则里,结果项目里 30 多个 .c 文件,有一半因为缺少对应的 .h 文件而编译失败。嗯,从那以后我就坚持「模式规则只写编译,头文件依赖单独列」的原则。
4.4 静态模式规则:精准匹配
模式规则匹配所有符合条件的文件,有时候范围太大了。静态模式规则可以指定只对某些目标应用模式:
objects = main.o utils.o
$(objects): %.o: %.c
gcc -c $< -o $@
这条规则只对 objects 列表里的文件生效。其他 .o 文件即使有对应的 .c 文件,也不会被这条规则匹配。
静态模式规则的语法是:
目标列表: 模式: 依赖模式
命令
我个人觉得,静态模式规则在大型项目中特别有用。你可以精确控制哪些文件用哪种编译选项,避免误伤。
4.5 自动变量与模式规则实战
来看一个完整的例子。假设项目结构如下:
project/
├── src/
│ ├── main.c
│ ├── utils.c
│ └── protocol.c
├── inc/
│ ├── utils.h
│ └── protocol.h
└── Makefile
Makefile 可以这样写:
CC = gcc
CFLAGS = -I inc -Wall
SRC_DIR = src
OBJ_DIR = obj
SRCS = $(wildcard $(SRC_DIR)/*.c)
OBJS = $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRCS))
all: app
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) -c $< -o $@
app: $(OBJS)
$(CC) $^ -o $@
clean:
rm -rf $(OBJ_DIR) app
这里用到了 wildcard 和 patsubst 函数,配合模式规则和自动变量,实现了全自动编译。新增一个 .c 文件?什么都不用改,直接 make 就行。
避坑指南:我曾经在模式规则里忘记加 @mkdir -p,结果 obj 目录不存在时编译失败。后来我养成了习惯:只要输出目录不是源码目录,就在规则里先创建目录。
4.6 知识结构图
下面这张图总结了自动变量和模式规则的核心关系:
4.7 常见陷阱与避坑
用自动变量和模式规则时,有几个坑我踩过,分享给你:
- 模式规则中的 % 只能出现一次:比如
%.%.o: %.c是错的。一个模式规则里只能有一个%。 - 自动变量只在命令部分生效:你不能在依赖部分用
$@或$<。依赖部分在规则匹配时就已经确定了。 - 模式规则的优先级:Make 会按 Makefile 中出现的顺序匹配模式规则。如果多个模式规则都能匹配同一个目标,第一个生效。
- 空模式规则:
%.o:没有依赖的模式规则会取消所有内置的 .o 编译规则。慎用。
我曾经踩过的坑:有一次我在模式规则里用了 $^ 而不是 $<,结果编译命令变成了 gcc -c main.c main.h -o main.o。gcc 看到 .h 文件直接报错。从那以后,编译 .c 到 .o 我坚持只用 $<。
4.8 小结
自动变量和模式规则是 Makefile 的「左膀右臂」。自动变量让你在命令里引用目标和依赖,模式规则让你用一条规则匹配一类文件。两者结合,Makefile 的代码量能减少 80% 以上。
记住几个关键点:
$@是目标,$^是所有依赖,$<是第一个依赖%匹配任意非空字符串,前后要一致- 模式规则负责通用编译,普通规则负责特殊依赖
- 静态模式规则可以精准控制匹配范围