3. 变量与赋值:Makefile 中的变量定义、递归展开与简单展开的区别
变量是 Makefile 的灵魂。没有变量,你的 Makefile 就是一堆硬编码的路径和选项,改一次要翻半天。但 Makefile 里的变量赋值,跟 C 语言里的变量赋值完全是两码事。这里有个坑,我当年刚接触时踩过不止一次。
说白了,Makefile 的变量赋值分两种:递归展开和简单展开。它们最大的区别在于——变量是在什么时候被展开的。
3.1 变量定义的基本语法
先看最基础的。Makefile 里定义一个变量,写法很简单:
# 变量名 = 值
CC = gcc
CFLAGS = -Wall -O2
TARGET = myapp
引用变量时用 $(变量名) 或者 ${变量名}。我个人习惯用圆括号,因为跟函数调用的风格一致,看着顺眼。
$(CC) $(CFLAGS) -o $(TARGET) main.c
嗯,这里有个细节:变量名区分大小写。CC 和 cc 是两个不同的变量。我见过有人因为大小写写错,排查了半天。
3.2 递归展开赋值(=)
= 是 Makefile 里最常用的赋值方式。但它有个特点:延迟展开。什么意思?变量在被引用时才计算它的值,而不是在定义时。
看个例子:
A = hello
B = $(A) world
A = hi
all:
@echo $(B)
猜猜输出什么?
输出是 hi world。为什么会这样?因为 B 的值是在 echo $(B) 这一行才被展开的。此时 A 已经被重新赋值为 hi 了,所以 $(A) 展开成 hi。
我曾经踩过的坑:用 = 定义了一个变量,后面又用同一个变量名做拼接,结果陷入了无限递归。比如 A = $(A) extra,Make 会一直展开下去,直到栈溢出。这种错误在日志里很难看出来,因为报错信息只是说"递归变量展开"。
递归展开的好处是灵活,你可以在后面修改依赖变量的值,所有引用都会自动更新。但坏处也很明显——性能差,而且容易出意外。
3.3 简单展开赋值(:=)
:= 是另一种赋值方式。它的行为更接近 C 语言:立即展开。变量在定义时就把值计算好,后面再怎么改依赖变量,它都不受影响。
还是刚才的例子,换成 :=:
A = hello
B := $(A) world
A = hi
all:
@echo $(B)
这次输出是 hello world。因为 B 在定义时,$(A) 就被展开成 hello 了。后面 A 再怎么变,跟 B 没关系。
我的建议:在大多数场景下,优先用 :=。它的行为可预测,不会因为后面改了某个变量就莫名其妙地影响前面的定义。尤其是在大型项目中,变量定义分散在多个文件里,用 := 能减少很多心智负担。
3.4 条件赋值(?=)
?= 的意思是:如果变量没有被定义过,才赋值;否则跳过。这个在给默认值的时候特别好用。
CC ?= gcc
CFLAGS ?= -O2
如果用户在命令行传了 CC=clang,那么 Makefile 里的 CC ?= gcc 就不会生效。这给了用户覆盖默认配置的能力。
我在项目中经常这样用:
# 允许外部传入,否则用默认值
INSTALL_DIR ?= /usr/local
BUILD_TYPE ?= release
3.5 追加赋值(+=)
+= 用于在变量后面追加内容。它比较智能——如果变量之前是用 := 定义的,追加时也是简单展开;如果之前是用 = 定义的,追加时也是递归展开。
CFLAGS = -Wall
CFLAGS += -O2 # 现在 CFLAGS 是 "-Wall -O2"
嗯,这里要注意:+= 会自动在中间加一个空格,你不需要手动加。
3.6 四种赋值方式的对比
| 运算符 | 名称 | 展开时机 | 典型场景 |
|---|---|---|---|
= |
递归展开 | 使用时展开 | 需要延迟求值,或依赖后续变量定义 |
:= |
简单展开 | 定义时展开 | 大多数场景,行为可预测 |
?= |
条件赋值 | 仅当未定义时 | 提供默认值,允许外部覆盖 |
+= |
追加赋值 | 继承原变量的展开方式 | 逐步构建编译选项、链接库列表 |
3.7 实战中的选择策略
说了这么多理论,到底怎么选?我总结了几条经验:
- 路径、文件名、工具链:用
:=。这些值一旦确定就不会变,立即展开更安全。 - 编译选项列表:用
+=逐步追加。比如先加通用选项,再加平台相关选项。 - 允许用户覆盖的变量:用
?=。比如PREFIX、CC、CFLAGS。 - 需要引用后面定义的变量:用
=。但这种情况很少见,我个人基本不用。
核心原则:能用 := 的地方就别用 =。简单展开让你少踩一半的坑。
3.8 变量展开的底层逻辑
为了让你更直观地理解这两种展开方式的区别,我画了一张流程图:
3.9 常见陷阱与避坑指南
最后,分享几个我实际项目中遇到的坑:
- 陷阱一:递归展开导致性能问题。如果一个变量被引用了上千次,每次引用都要重新展开,编译速度会明显变慢。用
:=可以避免这个问题。 - 陷阱二:条件赋值被误覆盖。用
?=时,如果前面已经用=或:=定义过同一个变量,?=不会生效。所以?=最好放在文件开头。 - 陷阱三:追加赋值与空格。用
+=时,如果变量原本是空的,它不会在开头加多余的空格。但如果变量原本有值,它会加一个空格再追加。这个行为大多数时候是你要的,但偶尔会出问题。
一个小技巧:如果你不确定某个变量最终会展开成什么,可以在 Makefile 里加一行 $(info VAR = $(VAR)) 来打印它的值。这比猜要快得多。
变量赋值看起来简单,但选对了能让你的 Makefile 更健壮、更容易维护。记住:默认用 :=,需要覆盖用 ?=,逐步构建用 +=,尽量少用 =。这个原则能帮你避开 90% 的变量相关的问题。