4、Makefile自动变量:$@、$<、$^、$?、$* 等自动变量的使用场景
写Makefile的时候,最烦什么?我个人觉得,是重复写那些目标名、依赖名。明明前面刚写过,后面又得抄一遍。改个文件名,得改好几个地方。嗯,自动变量就是来解决这个痛点的。
说白了,自动变量是Make在解析规则时,帮我们自动填充的“占位符”。你不用手动赋值,它自己就知道当前规则的目标是谁、依赖是谁。我刚开始用Makefile那会儿,觉得这玩意儿可有可无。后来项目大了,规则多了,才发现没有自动变量,写出来的Makefile简直没法维护。
核心一句话:自动变量让Makefile规则变得通用、简洁、可复用。你只需要写一次模式,剩下的交给变量去匹配。
4.1 $@ —— 当前规则的目标文件
$@ 代表当前规则的目标。不管你的目标是 .o 文件、可执行文件,还是伪目标,$@ 都会自动替换成它。
举个例子,你写一个编译规则:
main.o: main.c
gcc -c main.c -o main.o
这里目标 main.o 出现了两次。用 $@ 改写:
main.o: main.c
gcc -c $< -o $@
是不是清爽多了?我在项目中经常用这种方式写编译规则,尤其是当目标文件很多的时候,$@ 能省掉大量重复的硬编码。
小技巧:如果你在链接阶段,$@ 就是最终生成的可执行文件名。比如 app: main.o utils.o,那 $@ 就是 app。
4.2 $< —— 第一个依赖文件
$< 代表规则中依赖列表的第一个文件。注意,是第一个,不是全部。
最常见的用法就是编译单个源文件:
%.o: %.c
gcc -c $< -o $@
这里 $< 自动匹配成对应的 .c 文件。比如 make main.o 时,$< 就是 main.c。
我曾经踩过一个坑:以为 $< 会包含所有依赖。结果写了个链接规则,想用 $< 把所有 .o 文件都链接进去,链接器报了一堆未定义符号。查了半天才发现,$< 只取了第一个文件。嗯,从那以后,链接阶段我再也不用 $< 了。
注意:当依赖列表有多个文件时,$< 只取第一个。如果你需要所有依赖,请用 $^。
4.3 $^ —— 所有依赖文件(去重)
$^ 代表当前规则的所有依赖文件,并且会自动去重。也就是说,同一个文件即使出现多次,也只会被展开一次。
链接阶段是 $^ 的主场:
app: main.o utils.o io.o
gcc $^ -o $@
这条命令展开后就是:gcc main.o utils.o io.o -o app。你想想看,如果不用 $^,你得把三个 .o 文件再写一遍。项目里如果有几十个 .o 文件,那画面太美我不敢看。
我个人习惯在链接规则里固定用 $^,因为它能保证所有依赖都被链接进去,而且不会因为重复依赖导致链接器报错。
4.4 $? —— 比目标新的依赖文件
$? 代表所有比目标文件“新”的依赖文件。说白了,就是那些在上次构建之后被修改过的文件。
这个变量在增量编译中特别有用。比如你只想重新编译那些修改过的源文件:
libapp.a: foo.o bar.o baz.o
ar rcs $@ $?
如果只有 foo.c 被修改了,那么 $? 就只包含 foo.o。这样打包静态库时,只更新必要的 .o 文件,效率高很多。
我记得有一次接手一个大型项目,全量编译要十几分钟。后来我在打包脚本里用了 $?,增量编译直接降到几秒钟。团队里的人都觉得我开了挂,其实只是用对了自动变量而已。
使用场景:除了打包静态库,$? 还常用于生成依赖文件列表、触发后续处理流程等。只要你想知道“哪些文件变了”,就用 $?。
4.5 $* —— 模式匹配的“主干”部分
$* 代表模式规则中 % 匹配到的部分。比如规则 %.o: %.c,当匹配到 main.o 时,$* 就是 main。
这个变量在生成依赖文件时特别好用:
%.d: %.c
gcc -MM $< > $@
sed -i 's,\($*\)\.o[ :]*,\1.o $@ : ,g' $@
这里 $* 帮我们提取了文件名的主干部分,然后拼接到依赖规则中。说实话,我第一次看到这种写法时觉得有点绕,但用熟了之后,发现它确实能写出非常优雅的自动依赖生成规则。
4.6 其他自动变量一览
除了上面几个常用的,还有几个自动变量也值得了解:
| 变量 | 含义 | 典型场景 |
|---|---|---|
$+ |
所有依赖文件(不去重) | 需要保留重复依赖的特殊场景 |
$% |
归档文件(如 .a)中的成员名 | 操作静态库中的目标文件 |
$(@D) |
目标文件的目录部分 | 创建输出目录 |
$(@F) |
目标文件的文件名部分 | 单独获取文件名 |
$(<D) |
第一个依赖的目录部分 | 处理跨目录依赖 |
$(<F) |
第一个依赖的文件名部分 | 单独获取依赖文件名 |
这些带 D 和 F 后缀的变量,说白了就是帮你从路径里拆出目录和文件名。我在处理多级目录的项目时经常用到,比如把 .o 文件统一放到 build/ 目录下:
build/%.o: src/%.c
mkdir -p $(@D)
gcc -c $< -o $@
这里 $(@D) 就是 build,mkdir -p build 确保目录存在。
4.7 自动变量的核心逻辑
为了让你更直观地理解这些自动变量之间的关系,我画了一张图:
4.8 实战:一个完整的编译规则
说了这么多,不如直接看一个完整的例子。这是我个人项目中常用的编译规则模板:
CC = gcc
CFLAGS = -Wall -O2
SRCS = main.c utils.c io.c
OBJS = $(SRCS:.c=.o)
TARGET = app
$(TARGET): $(OBJS)
$(CC) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) $(TARGET)
.PHONY: clean
你看,这里用了 $^ 做链接,$< 做编译,$@ 做输出。整个Makefile只有十几行,却能管理任意数量的源文件。你想想看,如果不用自动变量,每个规则都得把文件名写死,改一个源文件就得改好几处,那得多痛苦。
避坑指南:我曾经在写链接规则时,不小心把 $^ 写成了 $<,结果链接器只链接了第一个 .o 文件,其他函数全部未定义。查了半小时才找到原因。所以记住:链接用 $^,编译用 $<。
4.9 总结
自动变量是Makefile的“语法糖”,但绝不是可有可无的装饰。它们让规则变得通用、简洁、可维护。我个人建议,只要你写的规则里目标或依赖出现了两次以上,就应该考虑用自动变量替换。
记住这几个核心的:$@ 是目标,$< 是第一个依赖,$^ 是所有依赖,$? 是更新的依赖,$* 是模式匹配的主干。用好它们,你的Makefile会从“能用”变成“好用”。