4、隐式规则与模式匹配:Makefile 的隐式规则链,以及如何使用 % 通配符进行模式匹配

说实话,很多初学者写 Makefile 时,第一反应就是把所有编译命令一条条写死。我以前也这么干过。直到有一次维护一个上百个源文件的项目,改个编译选项要改几十行……嗯,那感觉太酸爽了。

其实 Make 自带了一套「潜规则」,也就是隐式规则。你只要告诉它「我要从 .c 文件生成 .o 文件」,它自己就知道该调用 $(CC) -c。再加上 % 通配符做模式匹配,就能写出非常优雅的通用规则。

核心思想: 隐式规则是 Make 内置的「常识」,模式匹配是你自定义的「套路」。两者结合,能省掉 80% 的重复代码。

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
我的习惯: 我会在项目根 Makefile 里显式设置 CC = gcc 和 CFLAGS = -Wall -O2,然后让隐式规则去干活。这样既保留了灵活性,又不用写一堆重复的编译命令。

4.2 隐式规则链:自动推导依赖关系

Make 的隐式规则可以「链式推导」。比如你只写:

app: foo.o bar.o

Make 会做以下推导:

  1. 需要 foo.o → 找 foo.c → 用隐式规则编译
  2. 需要 bar.o → 找 bar.c → 用隐式规则编译
  3. 链接 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.c
  • obj/utils.o: src/utils.c
  • obj/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 规则体系的脉络:

Makefile 规则体系:隐式规则与模式匹配 Makefile 规则体系 规则类型 显式规则 隐式规则 模式匹配 内置规则库(.c→.o, .cpp→.o) 链式推导(自动找依赖) 变量控制(CC, CFLAGS 等) % 通配符匹配文件名 路径映射(src/%.c → obj/%.o) 多模式匹配(.c + .s 混合) 选择建议 标准项目 → 优先用隐式规则(省事) 复杂路径 / 自定义参数 → 用模式匹配(灵活) 两者可以混用,Make 会优先匹配显式规则

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 时,不妨先想想:「这条规则能不能用 % 来概括?」