3. 变量与赋值:Makefile 的灵魂所在
说实话,我刚接触 Makefile 那会儿,觉得变量就是个花架子。不就是存个字符串嘛,有啥好学的?直到我在一个嵌入式项目里,手动改了 37 处路径,结果漏了 2 处,编译出来一堆莫名其妙的错误……嗯,从那以后,我再也不敢小看变量了。
今天咱们就好好聊聊 Makefile 里的变量。说白了,它就是你的“代码复用神器”。你想想看,一个项目里源文件路径、编译器选项、链接库,这些东西到处重复出现,改一次得改一堆地方,多痛苦。变量就是来解决这个问题的。
3.1 变量的定义与引用
定义变量很简单,基本语法就一行:
变量名 = 值
引用变量时,用 $() 或者 ${} 把变量名包起来。我个人习惯用 $(),看着更清爽。
CC = gcc
CFLAGS = -Wall -O2
hello: hello.c
$(CC) $(CFLAGS) -o $@ $^
这里 $(CC) 展开成 gcc,$(CFLAGS) 展开成 -Wall -O2。你看,改编译器或者加编译选项,只需要改一处就行。
3.2 自动变量:$@, $^, $<
自动变量是 Makefile 里最让我觉得“真香”的特性。它们不需要你定义,Make 会自动帮你填好值。我刚开始学的时候觉得这玩意儿有点玄学,用多了才发现——太方便了。
| 自动变量 | 含义 | 举个例子 |
|---|---|---|
$@ |
目标文件的完整名称 | 规则 hello.o: hello.c 中,$@ 就是 hello.o |
$^ |
所有依赖文件的列表(去重) | 规则 main.o: main.c defs.h 中,$^ 就是 main.c defs.h |
$< |
第一个依赖文件的名称 | 规则 main.o: main.c defs.h 中,$< 就是 main.c |
看个实际例子:
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
这条规则的意思是:把每个 .c 文件编译成对应的 .o 文件。$< 是源文件(比如 hello.c),$@ 是目标文件(比如 hello.o)。
$^ 和 $< 搞混了。结果链接的时候把第一个依赖文件当成了所有依赖文件,链接器报了一堆未定义引用。排查了半小时才发现是这里写错了。记住:$^ 是全部,$< 是第一个。
3.3 赋值运算符:=, :=, ?=, +=
这部分是很多人的“噩梦”。四种赋值方式,看着差不多,用起来天差地别。我当年也是踩了不少坑才搞明白的。
3.3.1 递归展开赋值(=)
= 是 Makefile 最原始的赋值方式。它的特点是:延迟展开。也就是说,变量在定义时不会立即计算值,而是在使用时才展开。
A = hello
B = $(A) world
A = hi
all:
@echo $(B) # 输出:hi world
为什么会这样?因为 B 在定义时只是记下了 $(A) world 这个表达式,等到 echo 执行时,才去查找 A 的值。此时 A 已经被改成 hi 了。
A = $(A) something,这会让 Make 陷入死循环。我见过有人这么写,然后 Make 卡住不动了。
3.3.2 简单赋值(:=)
:= 是我个人最常用的赋值方式。它是立即展开的——变量定义时就把值计算好,后面再怎么改都不影响。
A := hello
B := $(A) world
A := hi
all:
@echo $(B) # 输出:hello world
你看,B 在定义时就已经把 $(A) 展开成 hello 了,后面 A 改成 hi 跟它没关系。
:=。它的行为更符合直觉,不容易出 bug。
3.3.3 条件赋值(?=)
?= 的意思是:如果变量没定义过,才赋值。如果已经定义过了,就啥也不干。
CC ?= gcc
CC ?= clang
all:
@echo $(CC) # 输出:gcc
这个在写通用 Makefile 时特别有用。比如你想让用户可以在命令行覆盖默认值:
# 用户执行:make CC=clang
CC ?= gcc
# 此时 CC 的值是 clang,因为用户在命令行已经定义了
3.3.4 追加赋值(+=)
+= 就是给变量追加内容。它会在原有值后面加一个空格,然后加上新内容。
CFLAGS := -Wall
CFLAGS += -O2
CFLAGS += -g
all:
@echo $(CFLAGS) # 输出:-Wall -O2 -g
这个在逐步构建编译选项时特别好用。比如调试版本加 -g,发布版本加 -O2,用 += 可以灵活组合。
3.4 四种赋值运算符对比
| 运算符 | 名称 | 展开时机 | 典型场景 |
|---|---|---|---|
= |
递归展开赋值 | 使用时 | 需要延迟求值,比如根据条件动态变化 |
:= |
简单赋值 | 定义时 | 绝大多数情况,推荐使用 |
?= |
条件赋值 | 定义时(仅当未定义) | 提供默认值,允许用户覆盖 |
+= |
追加赋值 | 取决于原变量的赋值方式 | 逐步构建列表,如编译选项、源文件列表 |
3.5 知识体系总览
下面这张图是我自己整理的 Makefile 变量知识体系,帮你理清思路:
3.6 实战经验总结
说了这么多,最后分享几个我自己的实战心得:
- 默认用 :=:除非你明确需要延迟展开,否则
:=是最安全的选择。它的行为跟大多数编程语言一样,不容易出幺蛾子。 - 自动变量是神器:写规则时尽量用
$@、$^、$<,这样规则更通用,复制到其他项目也能直接用。 - ?= 用于提供默认值:比如
CC ?= gcc,用户可以在命令行用make CC=clang覆盖,非常灵活。 - += 用于逐步构建:比如编译选项、源文件列表,用
+=可以分模块添加,代码更清晰。