3、Makefile变量进阶:自动变量、预定义变量与模式规则

说实话,刚接触Makefile那会儿,我觉得变量就是用来偷懒的——少打几个字而已。后来在项目里被坑了几次,才明白变量这东西,用好了能让你少掉一半头发。

这一节我们聊点进阶的。自动变量、预定义变量、模式规则、静态模式规则。嗯,名字听着唬人,其实说白了就是Makefile里的一些「快捷方式」和「套路」。我一个个给你拆开讲。

3.1 自动变量:$@、$<、$^ 到底是个啥?

先看个例子。你写一个编译规则:

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

这代码没问题,但不够聪明。如果我把目标文件改成 app.o,或者源文件改成 app.c,你得手动改两处。自动变量就是来解决这个问题的。

我常用的三个自动变量:

变量 含义 例子
$@ 目标文件名 在 main.o: main.c 中,$@ 就是 main.o
$< 第一个依赖文件 在 main.o: main.c 中,$< 就是 main.c
$^ 所有依赖文件(去重) 在 main.o: main.c defs.h 中,$^ 就是 main.c defs.h

用自动变量改写上面的规则:

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

你看,现在不管目标叫什么,源文件叫什么,我都不用改命令行了。我在项目里见过有人写了上百条类似的规则,每条都硬编码文件名。后来改成自动变量,代码量直接砍掉三分之二。

我的习惯:只要规则里有多个文件参与,我第一反应就是用自动变量。省事,还不会写错。

3.2 预定义变量:Makefile自带的「快捷键」

除了自动变量,Makefile还内置了一些预定义变量。这些变量你不用定义就能直接用。我挑几个最常用的:

变量 含义 默认值
CC C编译器 cc
CFLAGS C编译器选项 空
LDFLAGS 链接器选项 空
RM 删除命令 rm -f

举个例子:

CC = gcc
CFLAGS = -Wall -O2

main.o: main.c
    $(CC) $(CFLAGS) -c $< -o $@

为什么要用预定义变量?说白了就是「约定大于配置」。别人看你的Makefile,一眼就知道编译器是 CC,选项是 CFLAGS。不用猜。

我曾经踩过的坑:有一次我直接用了 CC 但没给它赋值,结果系统默认用了 cc。而 cc 在我那台机器上是个软链接,指向了老版本的gcc。编译出来的程序跑起来各种诡异。从那以后,我每次写Makefile都会显式指定 CC 的值。

3.3 模式规则:一条规则搞定一类文件

假设你有三个源文件:main.c、utils.c、io.c。按之前的写法,你得写三条规则:

main.o: main.c
    gcc -c main.c -o main.o
utils.o: utils.c
    gcc -c utils.c -o utils.o
io.o: io.c
    gcc -c io.c -o io.o

烦不烦?模式规则就是来救场的。它用 % 作为通配符,匹配任意文件名:

%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

这条规则的意思是:任何 .o 文件,都由同名的 .c 文件编译而来。一条规则,搞定所有源文件。

我刚开始用模式规则的时候,总觉得不踏实——万一匹配错了怎么办?后来发现,Makefile的匹配机制其实很严谨。它只会匹配那些确实存在的依赖文件。如果 foo.o 没有对应的 foo.c,这条规则就不会被触发。

3.4 静态模式规则:更精准的匹配

模式规则虽然好用,但它太「泛」了。有时候我只想对某几个文件应用规则,而不是所有文件。这时候就该静态模式规则上场了。

语法长这样:

目标列表 : 目标模式 : 依赖模式
    命令

看个例子:

OBJS = main.o utils.o io.o

$(OBJS): %.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

这里 $(OBJS) 指定了目标列表,只有这三个文件会匹配这条规则。其他 .o 文件就算存在,也不会被这条规则处理。

什么时候用静态模式规则?我个人习惯是:当项目里有多个子目录,每个子目录的编译选项不同时,静态模式规则特别好用。比如:
SRC_DIR1_OBJS = dir1/a.o dir1/b.o
SRC_DIR2_OBJS = dir2/c.o dir2/d.o

$(SRC_DIR1_OBJS): %.o: %.c
    $(CC) $(CFLAGS) -I dir1 -c $< -o $@

$(SRC_DIR2_OBJS): %.o: %.c
    $(CC) $(CFLAGS) -I dir2 -c $< -o $@
这样每个目录的源文件用各自的编译选项,互不干扰。

3.5 知识体系一览

说了这么多,我画张图帮你理一理思路:

Makefile变量进阶知识体系 Makefile变量进阶 自动变量 $@ 目标文件 $< 第一个依赖 $^ 所有依赖 预定义变量 CC、CFLAGS LDFLAGS、RM 模式规则 %.o: %.c 通配匹配所有文件 静态模式规则 $(OBJS): %.o: %.c 精准匹配指定文件 从通用到精准:自动变量 → 预定义变量 → 模式规则 → 静态模式规则

你看这张图,从左到右、从上到下,其实是一个从「通用」到「精准」的递进关系。自动变量是最基础的快捷方式,预定义变量是约定俗成的命名规范,模式规则帮你批量处理同类文件,静态模式规则则让你能精确控制哪些文件走哪条规则。

一个小建议:刚开始学的时候,别急着把所有技巧都用上。先从自动变量和预定义变量入手,写几个小项目练练手。等用熟了,再引入模式规则和静态模式规则。一口吃不成胖子,Makefile也是。

嗯,这一节的内容就到这儿。记住,变量和规则是Makefile的骨架,用好了它们,你的构建脚本会变得既简洁又强大。下次写Makefile的时候,不妨试试用 $@ 和 $< 替换掉那些硬编码的文件名——你会爱上这种清爽的感觉。

可可精品资料库