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 $? 的妙用:增量编译与后处理
$? 这个变量比较特殊,它只包含「比目标文件新」的依赖。我在项目中用它做过两件事:
- 增量打包: 只重新处理那些真正修改过的文件。
- 生成依赖文件: 配合
-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 在执行一条规则时,自动变量是如何被赋值的:
这张图想表达的是:你写的是抽象的模式规则,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 优化的第一步,也是最有效的一步。