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 知识结构图

下面这张图总结了自动变量和模式规则的核心关系:

自动变量与模式规则知识结构 自动变量 $@ → 目标文件名 $^ → 所有依赖(去重) $< → 第一个依赖 $? → 更新的依赖 $* → 去除后缀的文件名 模式规则 %.o: %.c → 通用编译模板 % 匹配任意非空字符串 静态模式规则:精准匹配 $(objects): %.o: %.c 模式规则 + 普通规则配合 结合使用 实战应用 自动变量 + 模式规则 → 一条规则编译所有文件 配合 wildcard/patsubst → 全自动项目管理 核心原则 模式规则提供模板,自动变量填充细节,两者缺一不可

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% 以上。

记住几个关键点:

  • $@ 是目标,$^ 是所有依赖,$< 是第一个依赖
  • % 匹配任意非空字符串,前后要一致
  • 模式规则负责通用编译,普通规则负责特殊依赖
  • 静态模式规则可以精准控制匹配范围