4、自动变量:$@、$<、$^、$? 等自动变量的妙用,减少重复代码

写 Makefile 最怕什么?我个人觉得,最怕的就是重复。你想想看,一个项目几十个源文件,每个都要写一条编译规则,那 Makefile 本身就成了一个体力活。而且一旦要改编译器选项,你得挨个改,改漏一个就等着编译报错吧。

嗯,自动变量就是来解决这个问题的。说白了,它们是一些预定义的符号,能自动帮我们填充规则中的目标、依赖等关键信息。用好它们,你的 Makefile 会变得又短又清晰。

4.1 自动变量速查表

先给一张速查表,方便你随时查阅。我在项目中经常用到的就是前四个:

自动变量 含义 示例值(规则 foo.o: foo.c foo.h)
$@ 目标文件名 foo.o
$< 第一个依赖文件 foo.c
$^ 所有依赖文件(去重) foo.c foo.h
$? 比目标新的依赖文件 如果 foo.h 更新了,则为 foo.h
$* 目标文件名去掉后缀 foo
$(@D) 目标文件的目录部分 .(当前目录)
$(@F) 目标文件的文件名部分 foo.o
我的小习惯: 我一般只记 $@、$<、$^ 这三个。剩下的用到时再查表,完全够用。

4.2 模式规则 + 自动变量 = 黄金搭档

模式规则(%.o: %.c)本身已经能匹配一类文件了,但如果没有自动变量,你还是要写死文件名。两者一结合,效果立竿见影。

来看一个对比。这是没有自动变量的写法:

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

是不是很崩溃?每加一个源文件,就要复制粘贴一行。而且如果编译器从 gcc 换成 clang,你得改三处。

用了自动变量之后:

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

就这一条规则,搞定所有 .c 到 .o 的编译。$< 自动取到 foo.c、bar.c…… $@ 自动取到对应的 foo.o、bar.o……

核心思想: 模式规则负责「匹配」,自动变量负责「填充」。两者缺一不可。

4.3 链接场景中的 $^ 用法

编译阶段我们用 $< 和 $@,链接阶段则轮到 $^ 大显身手了。

链接器需要把所有目标文件拼在一起。如果手动写:

myapp: foo.o bar.o baz.o
    gcc foo.o bar.o baz.o -o myapp

依赖列表写了两遍,冗余不说,还容易漏掉。用 $^ 就清爽了:

myapp: foo.o bar.o baz.o
    gcc $^ -o $@

$^ 自动展开成 foo.o bar.o baz.o,而且会去重。万一你不小心写了两次 foo.o,它也只出现一次,不会让链接器报「重复定义」的错误。

注意: $^ 和 $+ 的区别。前者去重,后者不去重。我几乎只用 $^,因为去重更安全。除非你明确需要重复的依赖,否则别碰 $+。

4.4 $? 的妙用:增量编译与后处理

$? 这个变量比较特殊,它只包含「比目标文件新」的依赖。我在项目中用它做过两件事:

  1. 增量打包: 只重新处理那些真正修改过的文件。
  2. 生成依赖文件: 配合 -MM 选项,只对修改过的源文件重新生成 .d 文件。

举个例子,假设你有一个归档操作:

archive.a: foo.o bar.o baz.o
    ar rcs $@ $?

如果只有 bar.o 重新编译了,$? 就只包含 bar.o,归档命令只会更新这一个文件,而不是全部重新打包。对于大型项目,这个优化能省不少时间。

避坑指南: 我曾经在链接阶段用过 $?,结果发现链接器报了一堆未定义符号。后来才意识到,链接器需要所有目标文件,不能只链接新的。所以 $? 只适合那些「可以增量处理」的场景,比如归档、代码生成、文档构建。

4.5 自动变量与目录处理

当你的源文件分散在不同子目录时,自动变量也能帮上忙。比如:

obj/%.o: src/%.c
    gcc -c $< -o $@

这里 $< 是 src/foo.c,$@ 是 obj/foo.o。自动变量完整保留了路径信息,不需要你手动拼接。

如果你需要提取目录或文件名,可以用 $(@D) 和 $(@F)。比如:

obj/%.o: src/%.c
    mkdir -p $(@D)
    gcc -c $< -o $@

这个规则会自动创建 obj/ 目录(如果不存在的话)。$(@D) 取到 obj,$(@F) 取到 foo.o。

4.6 一张图看懂自动变量的工作流

下面这张 SVG 图展示了 Make 在执行一条规则时,自动变量是如何被赋值的:

规则定义 %.o: %.c gcc -c $< -o $@ Make 匹配具体目标 具体规则(匹配后) foo.o: foo.c $@ = foo.o $< = foo.c 展开后的命令 gcc -c foo.c -o foo.o 自动变量在规则匹配后自动赋值,无需手动指定

这张图想表达的是:你写的是抽象的模式规则,Make 在匹配到具体文件后,自动变量才被赋予实际值。整个过程是自动的,你只管写规则,剩下的交给 Make。

4.7 实战:一个完整的编译模板

最后,给你一个我常用的模板。它把上面讲的自动变量都用上了:

CC      = gcc
CFLAGS  = -Wall -O2
LDFLAGS = -lm

SRCS    = main.c utils.c parser.c
OBJS    = $(SRCS:.c=.o)
TARGET  = myapp

# 编译规则:$< 取源文件,$@ 取目标文件
%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

# 链接规则:$^ 取所有目标文件,$@ 取最终目标
$(TARGET): $(OBJS)
    $(CC) $^ $(LDFLAGS) -o $@

# 清理
clean:
    rm -f $(OBJS) $(TARGET)

这个模板里,你只需要改 SRCS 这一行,其他都不用动。加一个源文件,就加在 SRCS 里,编译和链接规则自动适配。

总结一下: 自动变量不是锦上添花,而是 Makefile 的基石。没有它们,你写的就不是 Makefile,而是批处理脚本。用好 $@、$<、$^ 这三个,你的 Makefile 就能从「能用」变成「好用」。

嗯,自动变量就讲到这里。记住一点:凡是重复的地方,就想想能不能用自动变量替代。这是 Makefile 优化的第一步,也是最有效的一步。