4. 模式规则与静态模式

好,咱们今天聊聊 Makefile 里一个特别实用的东西——模式规则和静态模式。

说实话,我刚开始写 Makefile 的时候,也是一条一条规则手写。比如编译三个 .c 文件,我就写三条规则。后来项目大了,文件一多,手写就成噩梦了。你想想看,一个项目几十个源文件,每个都要写一条编译规则?那不得写到天亮。

这时候,模式规则就派上用场了。

4.1 %通配符:模式匹配的核心

模式规则里最核心的就是 % 这个通配符。它跟 shell 里的 * 有点像,但又不完全一样。% 在 Makefile 里专门用来做模式匹配。

它的工作方式是这样的:% 可以匹配任意长度的字符串(包括空字符串)。而且,同一个规则里,第一个 % 匹配到的内容,会传递给第二个 %。

举个例子:

%.o: %.c
    gcc -c $< -o $@

这条规则的意思是:任何以 .o 结尾的目标文件,都依赖于同名的 .c 文件。比如你要生成 main.o,Make 就会去找 main.c,然后执行编译命令。

关键点: % 匹配的是文件名中除去后缀的部分。在 %.o 里,% 匹配的是 main;在 %.c 里,% 匹配的也是 main。这就是「同名匹配」的原理。

我个人习惯把模式规则放在 Makefile 的前面,这样 Make 会优先尝试匹配模式规则。如果匹配不上,再去找显式规则。

4.2 静态模式规则:精准控制

模式规则虽然好用,但它有个问题:太「泛」了。它会匹配所有符合模式的目标。有时候,我只想对特定的几个文件应用规则,而不是全部。

这时候,静态模式规则就登场了。

静态模式规则的语法是这样的:

目标列表 : 目标模式 : 依赖模式
    命令

看个例子:

OBJS = main.o utils.o data.o

$(OBJS): %.o: %.c
    gcc -c $< -o $@

这条规则的意思是:只对 OBJS 列表里的文件应用模式规则。其他 .o 文件,就算存在对应的 .c 文件,也不会被这条规则处理。

我的经验: 我在项目中遇到过这样的情况——项目里既有需要编译的源文件,又有一些预编译的 .o 文件。如果用普通模式规则,Make 会尝试去编译所有 .o 文件,包括那些不需要重新编译的。用静态模式规则,就能精确控制哪些文件走编译流程。

静态模式规则的好处是:

  • 精准:只对指定目标生效
  • 可读性强:一眼就能看出哪些文件受规则影响
  • 避免误匹配:不会意外匹配到不该匹配的文件

4.3 多目标构建:一次搞定

有时候,我们需要同时构建多个目标。比如,一个库文件可能由多个 .o 文件组成。或者,一个项目需要生成多个可执行文件。

多目标构建,说白了就是让一条规则同时处理多个目标。

最直接的方式:

all: program1 program2 program3

program1: program1.o lib.a
    gcc program1.o lib.a -o program1

program2: program2.o lib.a
    gcc program2.o lib.a -o program2

program3: program3.o lib.a
    gcc program3.o lib.a -o program3

嗯,这样写虽然能工作,但太啰嗦了。三个程序还好,要是三十个呢?

更好的做法是用模式规则配合多目标:

PROGRAMS = program1 program2 program3

all: $(PROGRAMS)

$(PROGRAMS): %: %.o lib.a
    gcc $< lib.a -o $@

这里,%: %.o lib.a 是一个静态模式规则,它告诉 Make:对于 PROGRAMS 里的每个程序,都去找同名的 .o 文件,然后链接成可执行文件。

注意: 我曾经踩过一个坑——多目标规则里,如果多个目标共享同一个依赖文件,Make 可能会重复执行命令。比如上面的例子,如果 lib.a 更新了,Make 会重新链接所有程序。这没问题。但如果你不小心把命令写成了 gcc $< -o $@,而 $< 只代表第一个依赖,那就会出问题。

多目标构建的另一个常见场景是生成多个版本的库:

LIBS = libfoo.a libfoo.so libfoo.dylib

all: $(LIBS)

libfoo.a: foo.o
    ar rcs $@ $<

libfoo.so: foo.o
    gcc -shared -o $@ $<

libfoo.dylib: foo.o
    gcc -dynamiclib -o $@ $<

当然,你也可以用模式规则来简化:

LIBS = libfoo.a libfoo.so libfoo.dylib

all: $(LIBS)

%.a: %.o
    ar rcs $@ $<

%.so: %.o
    gcc -shared -o $@ $<

%.dylib: %.o
    gcc -dynamiclib -o $@ $<

4.4 知识体系图

下面这张图展示了模式规则、静态模式和多目标构建之间的关系:

Makefile 模式规则与多目标构建 % 通配符 模式匹配核心 静态模式规则 精准控制目标 多目标构建 批量处理 特性 • 匹配任意字符串 • 同名传递匹配 • 自动推导依赖 特性 • 限定目标列表 • 避免误匹配 • 可读性强 特性 • 批量生成目标 • 共享依赖管理 • 减少重复代码 组合使用:静态模式 + 多目标 $(OBJS): %.o: %.c → 精准编译指定文件

4.5 避坑指南

最后,分享几个我踩过的坑:

  • % 匹配的优先级:Make 会先尝试显式规则,再尝试模式规则。如果你同时写了显式规则和模式规则,显式规则优先。
  • 静态模式中的空格:目标列表和冒号之间不要有多余空格。我见过有人写 $(OBJS) : %.o: %.c,结果 Make 报错。正确的写法是 $(OBJS): %.o: %.c。
  • 多目标命令的重复执行:如果多个目标共享同一个命令,Make 会为每个目标执行一次命令。这通常是期望的行为,但如果你不小心,可能会造成不必要的重复编译。
  • 我曾经犯过一个错:在静态模式规则里用了 $^ 而不是 $<,结果把所有依赖都链接进去了,导致链接失败。记住,$< 是第一个依赖,$^ 是所有依赖。

小技巧: 如果你不确定模式规则是否匹配正确,可以用 make -p 查看 Make 的内部数据库,它会显示所有规则和变量的值。这招帮我排查过不少问题。

好了,模式规则和静态模式就讲到这里。这些东西看着简单,用好了能让你的 Makefile 精简不少。下次写 Makefile 的时候,试试用模式规则替代那些重复的显式规则,你会爱上这种感觉的。