4. 规则与目标

写 Makefile 的时候,最烦什么?我个人觉得,是重复。

你想想看,每个目标文件都要写一遍完整的编译命令,改个源文件名还得改规则。这种体力活,我干了三天就受不了了。后来我才发现,Make 早就给我们准备了一套「自动变量」,专门解决这种问题。

自动变量是什么?说白了,就是 Make 在匹配规则时自动填充的占位符。你不用手动指定文件名,Make 会根据当前正在处理的目标和依赖,自动帮你填进去。

核心思想:自动变量 = 让规则通用化,一份规则匹配 N 个文件。

4.1 五个最常用的自动变量

先看一张表,把这五个变量记住,你就能应付 90% 的场景。

变量 含义 典型值示例
$@ 当前规则的目标文件名 main.o
$< 第一个依赖文件 main.c
$^ 所有依赖文件(去重) main.c utils.c
$? 比目标新的依赖文件 utils.c
$* 目标文件名去掉后缀的部分 main

嗯,这里要注意:$^ 会自动去重,而 $? 只保留「比目标新」的依赖。这两个特性在实际项目中非常有用。

4.2 实战场景一:编译单个源文件

这是最基础的应用。没有自动变量的时候,你得这么写:

main.o: main.c
    gcc -c main.c -o main.o

utils.o: utils.c
    gcc -c utils.c -o utils.o

每个文件写一遍,累不累?用自动变量,一行搞定:

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

这里 $< 自动变成 main.c 或 utils.c,$@ 自动变成 main.o 或 utils.o。模式匹配 + 自动变量,天生一对。

我的习惯:所有编译规则我都用 $< 和 $@ 写。这样即使以后改了源文件后缀,规则也不用动。

4.3 实战场景二:链接多个目标文件

链接的时候,依赖文件通常不止一个。这时候 $^ 就派上用场了。

app: main.o utils.o
    gcc $^ -o $@

$^ 展开后是 main.o utils.o,不用你一个一个列出来。而且如果某个 .o 文件在依赖列表里出现了两次,$^ 会自动去重,不会导致链接错误。

我记得有一次,项目里因为头文件依赖写重复了,链接报了一堆符号重定义。排查了半天,最后发现是依赖列表里同一个 .o 写了两次。从那以后,链接规则我全用 $^,再没出过这种问题。

4.4 实战场景三:增量编译与 $?

$? 这个变量,我愿称之为「增量编译神器」。

它的值是「所有比目标新的依赖文件」。什么意思?就是只有那些被修改过的源文件对应的 .o,才会被重新编译。

libapp.a: main.o utils.o
    ar rcs $@ $?

假设你只改了 utils.c,那么 $? 只包含 utils.o。打包静态库时,只更新这一个 .o 文件,效率极高。

我曾经踩过的坑:用 $? 做增量打包时,如果某个 .o 被删除了,$? 不会包含它。结果静态库里缺了文件,链接时符号找不到。所以 $? 适合「只增不改」的场景,全量打包还是用 $^ 更安全。

4.5 实战场景四:生成依赖文件与 $*

$* 代表目标文件名去掉后缀的部分。在生成 .d 依赖文件时特别好用。

%.d: %.c
    gcc -MM $< -o $*.d

这里 $* 展开为 main、utils 等,生成的 .d 文件就是 main.d、utils.d。你想想看,如果没有 $*,你得用 $(basename ...) 函数去截取,多麻烦。

4.6 自动变量对比与选择

什么时候用哪个?我整理了一个决策表:

场景 推荐变量 原因
编译单个源文件 $< + $@ 依赖只有一个,$< 最直接
链接多个 .o $^ + $@ 自动去重,不会漏文件
增量打包静态库 $? + $@ 只更新修改过的文件,速度快
生成 .d 依赖文件 $* + $< 自动提取文件名前缀,省去字符串处理
复制/移动文件 $< + $@ 源和目标一一对应,简单明了

4.7 自动变量知识体系

下面这张图,帮你把自动变量的关系理清楚:

自动变量知识体系 自动变量 $@ 目标文件 $< 首个依赖 $^ 所有依赖 $? 更新依赖 $* 文件名前缀 应用场景速查 编译:$< + $@ 链接:$^ + $@ 使用原则 • 编译规则:优先用 $< 和 $@,依赖单一,语义清晰 • 链接规则:用 $^ 确保所有依赖都被包含,自动去重 • 增量场景:用 $? 只处理变更文件,提升构建速度

4.8 避坑指南

自动变量虽好,但有几个坑我不得不提:

  • 不要在依赖列表里用自动变量——自动变量只在命令部分生效,依赖列表里写 $@ 是无效的。
  • $^ 去重是好事也是坏事——如果你故意写了重复依赖(比如某些特殊场景),$^ 会帮你「纠正」掉,可能不是你想要的。
  • $? 只适用于增量场景——全量编译时别用,否则第一次构建可能漏文件。
  • 模式规则中慎用 $*——如果目标文件名没有后缀(比如 lib),$* 会变成空字符串,导致命令出错。

我的建议:刚开始用自动变量时,先加一行 @echo $@ $< $^ 看看展开结果。确认无误后再去掉调试信息。这个习惯帮我省了不少排查时间。

自动变量是 Makefile 从「手写脚本」走向「工程化构建」的第一步。掌握了它们,你的 Makefile 就能从几十行缩到几行,而且更健壮、更易维护。