4、隐式规则与模式匹配:Makefile 的隐式规则链,以及如何使用 % 通配符进行模式匹配
说实话,很多初学者写 Makefile 时,第一反应就是把所有编译命令一条条写死。我以前也这么干过。直到有一次维护一个上百个源文件的项目,改个编译选项要改几十行……嗯,那感觉太酸爽了。
其实 Make 自带了一套「潜规则」,也就是隐式规则。你只要告诉它「我要从 .c 文件生成 .o 文件」,它自己就知道该调用 $(CC) -c。再加上 % 通配符做模式匹配,就能写出非常优雅的通用规则。
4.1 隐式规则:Make 替你写好的规则
Make 内置了一套规则库。比如你写:
foo.o: foo.c
Make 会自动推导出编译命令:$(CC) -c foo.c -o foo.o。它甚至知道 CC 默认是 cc,CFLAGS 默认是空的。
我整理了一份常用的隐式规则表:
| 目标文件后缀 | 依赖文件后缀 | 默认命令 | 常用变量 |
|---|---|---|---|
.o |
.c |
$(CC) -c $(CFLAGS) $< -o $@ |
CC, CFLAGS |
.o |
.cpp / .cc |
$(CXX) -c $(CXXFLAGS) $< -o $@ |
CXX, CXXFLAGS |
.o |
.s (汇编) |
$(AS) -c $(ASFLAGS) $< -o $@ |
AS, ASFLAGS |
.a (静态库) |
.o |
$(AR) rcs $@ $^ |
AR, ARFLAGS |
| 可执行文件 | .o |
$(CC) $(LDFLAGS) $^ -o $@ |
CC, LDFLAGS, LDLIBS |
CC = gcc 和 CFLAGS = -Wall -O2,然后让隐式规则去干活。这样既保留了灵活性,又不用写一堆重复的编译命令。
4.2 隐式规则链:自动推导依赖关系
Make 的隐式规则可以「链式推导」。比如你只写:
app: foo.o bar.o
Make 会做以下推导:
- 需要
foo.o→ 找foo.c→ 用隐式规则编译 - 需要
bar.o→ 找bar.c→ 用隐式规则编译 - 链接
foo.o bar.o→ 用隐式规则生成app
你看,三行 Makefile 就能搞定一个 C 项目。但这里有个坑——隐式规则链只对「标准后缀」有效。如果你用 .cc 但系统默认只认 .cpp,链就断了。
.c 改成 .cc,但忘了设置 .SUFFIXES。结果 Make 死活找不到编译规则,报了一堆「no rule to make target」错误。排查了半天才发现是后缀名不匹配。
4.3 模式匹配:用 % 通配符写通用规则
% 是 Makefile 里的「万能通配符」。它匹配任意非空字符串,有点像 shell 里的 *,但更强大——它能在目标和依赖之间建立对应关系。
基本语法:
%.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
这条规则的意思是:任何 .o 文件,都依赖同名的 .c 文件。这里的 % 同时匹配目标和依赖中的「相同部分」。
来看个实际例子。假设项目结构是:
src/
main.c
utils.c
network.c
obj/
(空目录)
用模式匹配写 Makefile:
SRC = src/main.c src/utils.c src/network.c
OBJ = $(SRC:src/%.c=obj/%.o)
obj/%.o: src/%.c
$(CC) -c $(CFLAGS) $< -o $@
app: $(OBJ)
$(CC) $(LDFLAGS) $^ -o $@
这里 % 匹配了 main、utils、network 这些文件名。Make 会自动展开成三条规则:
obj/main.o: src/main.cobj/utils.o: src/utils.cobj/network.o: src/network.c
% 匹配的是「路径中的文件名部分」,不包括目录。所以 src/%.c 匹配的是 src/ 目录下所有 .c 文件。
4.4 模式匹配 vs 隐式规则:什么时候用哪个?
我个人的经验法则:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 标准 C/C++ 项目,源文件在同一目录 | 隐式规则 | 零配置,Make 自己搞定 |
| 源文件和目标文件在不同目录 | 模式匹配 | 需要显式指定路径映射 |
需要自定义编译参数(如 -DDEBUG) |
模式匹配 | 隐式规则不方便加额外参数 |
非标准后缀(如 .cc、.cxx) |
模式匹配 | 隐式规则可能不识别 |
| 多语言混合项目(C + 汇编 + 自定义代码生成) | 模式匹配 | 需要精确控制每条规则 |
make -p 查看 Make 内置的所有隐式规则。我经常用这个命令来确认某个后缀是否被支持。
4.5 模式匹配的高级用法:多模式与过滤器
有时候你需要匹配多种源文件类型。比如项目里既有 .c 也有 .s(汇编):
# 匹配 C 文件
obj/%.o: src/%.c
$(CC) -c $(CFLAGS) $< -o $@
# 匹配汇编文件
obj/%.o: src/%.s
$(AS) -c $(ASFLAGS) $< -o $@
Make 会按顺序尝试匹配。如果 foo.c 存在,就用第一条;如果不存在但 foo.s 存在,就用第二条。
再配合 filter 函数做文件分类:
SRC_C = $(filter %.c, $(SRC))
SRC_S = $(filter %.s, $(SRC))
OBJ_C = $(SRC_C:src/%.c=obj/%.o)
OBJ_S = $(SRC_S:src/%.s=obj/%.o)
这样就能分别处理不同类型的源文件了。
4.6 知识结构图:隐式规则与模式匹配的关系
下面这张图帮你理清 Makefile 规则体系的脉络:
4.7 实战:一个完整的模式匹配 Makefile
最后给一个我在实际项目中用过的模板。它支持 C 和汇编混合编译,目标文件放到独立目录:
# 项目配置
CC = gcc
AS = as
CFLAGS = -Wall -O2 -Iinclude
ASFLAGS =
LDFLAGS =
LDLIBS = -lm
# 源文件
SRC_C = $(wildcard src/*.c)
SRC_S = $(wildcard src/*.s)
OBJ_C = $(SRC_C:src/%.c=obj/%.o)
OBJ_S = $(SRC_S:src/%.s=obj/%.o)
OBJ = $(OBJ_C) $(OBJ_S)
# 目标
TARGET = bin/app
# 模式匹配规则
obj/%.o: src/%.c
@mkdir -p $(@D)
$(CC) -c $(CFLAGS) $< -o $@
obj/%.o: src/%.s
@mkdir -p $(@D)
$(AS) -c $(ASFLAGS) $< -o $@
# 链接
$(TARGET): $(OBJ)
@mkdir -p $(@D)
$(CC) $(LDFLAGS) $^ -o $@ $(LDLIBS)
# 清理
clean:
rm -rf obj bin
.PHONY: clean
这个 Makefile 有几个亮点:
$(wildcard ...)自动扫描源文件,新增文件不用改 Makefile@mkdir -p $(@D)自动创建目标目录,不用手动建obj/和bin/- 模式匹配同时处理
.c和.s,互不干扰
.c 和 .s 同名(比如 foo.c 和 foo.s),Make 会优先匹配第一条规则。所以一般把更通用的规则放前面。
嗯,隐式规则和模式匹配就讲到这里。说白了,它们就是 Makefile 的「语法糖」——让你用最少的代码,表达最多的构建逻辑。下次写 Makefile 时,不妨先想想:「这条规则能不能用 % 来概括?」