4. Makefile模式规则与静态模式:%通配符的使用,静态模式规则的写法,常见应用场景。

好,咱们接着聊Makefile。前面几章我们讲了变量、自动变量、隐式规则这些基础。说实话,这些已经能应付不少场景了。但如果你写过稍微复杂一点的项目,比如一个目录下有几十个.c文件要编译,你肯定不想一个一个去写规则。

这时候,%通配符就该登场了。我个人觉得,这是Makefile里最优雅的设计之一。

4.1 %通配符:模式规则的灵魂

先看一个最简单的例子。假设你有三个.c文件:foo.c、bar.c、baz.c。你想把它们都编译成.o文件。

传统写法是这样的:

foo.o: foo.c
    gcc -c foo.c -o foo.o

bar.o: bar.c
    gcc -c bar.c -o bar.o

baz.o: baz.c
    gcc -c baz.c -o baz.o

嗯,三个文件还好。那三十个呢?三百个呢?你想想看,这得写到什么时候去。

用模式规则,一行就搞定:

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

%在这里是个通配符,它匹配文件名中除去后缀的部分。比如foo.o匹配%.o时,%就代表foo。然后它会把foo代入到依赖中,变成foo.c。

说白了,这就是个「模式匹配」的机制。Make会拿着目标文件名,去和模式规则做匹配,匹配上了就按规则执行。

核心要点:%可以出现在目标和依赖中。同一个规则里,多个%匹配到的内容是相同的。

我在项目中遇到过一种情况:有人把%和*搞混了。*是shell的通配符,在Makefile里用$(wildcard *.c)才能展开。而%是Make自己的模式匹配,两者完全不是一回事。

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

模式规则虽然好用,但它有个问题:它会匹配所有符合模式的目标。有时候你只想对特定的文件列表应用规则,而不是所有文件。

举个例子。假设你的项目里有main.c、utils.c、test.c,但test.c是测试文件,你不想用同样的编译选项。这时候静态模式规则就派上用场了。

写法是这样的:

OBJS = main.o utils.o

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

看清楚语法:目标列表 : 目标模式 : 依赖模式。Make会拿OBJS里的每个文件,去匹配%.o这个模式,然后生成对应的依赖。

这样test.o就不会被这条规则影响。你可以单独为它写另一条规则。

小技巧:静态模式规则比普通模式规则更安全。它不会意外匹配到你不想处理的文件。我个人的习惯是,只要文件列表是明确的,就用静态模式规则。

4.3 常见应用场景

光说不练假把式。咱们来看几个实际项目中会用到的场景。

场景一:编译所有.c文件到.o文件

这是最基础也是最常见的用法:

SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)

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

这里用$(wildcard *.c)自动获取所有.c文件,然后用替换引用的语法$(SRCS:.c=.o)把后缀换成.o。最后用静态模式规则编译。

我曾经见过有人手动列出所有文件名,结果加了个新文件忘了更新Makefile,编译出来缺东西。用wildcard就省心多了。

场景二:生成不同目录下的目标文件

项目大了,源文件通常会分目录存放。比如src/目录放源码,obj/目录放编译产物。

SRC_DIR = src
OBJ_DIR = obj
SRCS = $(wildcard $(SRC_DIR)/*.c)
OBJS = $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRCS))

$(OBJS): $(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
    gcc -Wall -O2 -c $< -o $@

这里用了$(patsubst ...)来做路径替换。模式规则里的%匹配的是文件名(不含路径),所以src/foo.c匹配$(SRC_DIR)/%.c时,%就是foo。

注意:如果目标目录不存在,编译会失败。记得在规则里加上mkdir -p $(OBJ_DIR)。我曾经因为这个bug排查了半天,最后发现是目录没创建。

场景三:生成多个可执行文件

有时候一个项目会生成多个可执行文件,每个对应一个源文件。比如tool1.c生成tool1,tool2.c生成tool2。

TOOLS = tool1 tool2 tool3

$(TOOLS): %: %.c
    gcc -Wall -O2 $< -o $@

这个规则的意思是:对于tool1,它依赖tool1.c,编译命令用$<和$@。简洁明了。

4.4 模式规则与隐式规则的优先级

嗯,这里有个坑。Make内置了很多隐式规则,比如%.o: %.c就是内置的。如果你自己写了同样的模式规则,你自己的会覆盖内置的。

但如果你写了静态模式规则,它的优先级比普通模式规则高。Make会先尝试匹配静态模式规则,匹配不上再去找普通模式规则。

我建议你养成一个习惯:只要是自己定义的规则,都用静态模式规则。这样行为更可控,不会莫名其妙被内置规则干扰。

4.5 知识体系梳理

说了这么多,咱们用一张图来总结一下模式规则和静态模式的核心逻辑。

Makefile 模式规则知识体系 模式规则 普通模式规则 %.o: %.c 匹配所有符合模式的目标 全局生效,无法精确控制 静态模式规则 $(OBJS): %.o: %.c 只对指定列表中的目标生效 精确控制,避免意外匹配 % 通配符是核心

这张图把两种模式规则的关系理清楚了。左边是普通模式规则,右边是静态模式规则。它们的核心都是%通配符,但应用范围和控制粒度不同。

4.6 避坑指南

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

  • % 不能匹配路径中的斜杠。比如%.o: %.c匹配不了src/foo.o,因为%不会跨目录匹配。需要写成src/%.o: src/%.c。
  • 静态模式规则中的目标列表不能为空。如果OBJS是空的,Make会报错。建议用$(if $(OBJS), ...)做保护。
  • 同一个文件不能匹配多个模式规则。如果有两条规则都能匹配同一个目标,Make会选择第一条。这可能导致意想不到的行为。

嗯,模式规则和静态模式就讲到这里。说白了,它们就是让你少写重复代码的利器。用好了,Makefile能精简不少。下次你写编译规则的时候,不妨试试用%来偷个懒。

可可精品资料库