3. 变量与赋值:Makefile 中的变量定义与使用
变量是 Makefile 的灵魂。没有变量,你的构建脚本就是一滩死水。我个人习惯把变量看作「构建系统的内存」,它们存储路径、编译选项、源文件列表——几乎所有可复用的信息。
但很多工程师栽在赋值运算符上。=、:=、?=、+= 这四个符号,看着差不多,实际行为天差地别。我见过一个项目,就因为用了 = 而不是 :=,导致编译选项被递归展开成几千个字符,最后 gcc 直接报参数过长。嗯,这种坑踩一次就记住了。
3.1 四种赋值运算符的核心差异
先看一张对比表,心里有个底:
| 运算符 | 名称 | 展开时机 | 典型场景 |
|---|---|---|---|
= |
递归展开赋值 | 使用时才展开 | 定义依赖其他变量的变量 |
:= |
简单展开赋值 | 定义时立即展开 | 固定路径、编译选项 |
?= |
条件赋值 | 仅当变量未定义时赋值 | 默认值、用户可覆盖配置 |
+= |
追加赋值 | 追加到已有值末尾 | 累积源文件列表、链接库 |
为什么会有这些区别?说白了,Make 的变量系统继承自古老的 make 设计哲学——它允许变量「延迟求值」。这在某些场景下很灵活,但也容易让人迷惑。
3.2 =:递归展开赋值
= 是 Makefile 最原始的赋值方式。它的特点是:变量在被实际使用的时候才展开。
CC = gcc
CFLAGS = -Wall -O2
TOOLCHAIN = $(CC) $(CFLAGS)
# 此时 TOOLCHAIN 的值还是 "gcc -Wall -O2" 的引用,不是字符串本身
我在项目中遇到过一个问题:有人用 = 定义了一个变量 SRC_DIR = src/$(ARCH),然后在文件末尾才定义 ARCH = arm。结果编译时 SRC_DIR 正确展开成了 src/arm。这就是递归展开的好处——你可以在任意位置定义依赖变量。
但坏处也很明显:性能差,容易死循环。
我曾经见过一个 Makefile,里面写了
A = $(B)、B = $(A)。Make 在展开时直接陷入无限递归,最后报错退出。用 = 时一定要确保依赖关系是 DAG(有向无环图),别搞成循环引用。
3.3 :=:简单展开赋值
:= 是我个人最常用的赋值方式。它在定义时立即展开所有引用的变量,结果就是一个固定的字符串。
ARCH = arm
SRC_DIR := src/$(ARCH) # 立即展开为 src/arm
ARCH = x86 # 修改 ARCH 不影响 SRC_DIR
# SRC_DIR 仍然是 src/arm
你想想看,这有什么好处?可预测。你不需要担心后续的变量修改会意外改变已有变量的值。在大型项目中,我几乎全部用 :=,只在少数需要延迟求值的场景才用 =。
路径、编译选项、工具链前缀——这些「定义后就不该变」的东西,一律用
:=。它让 Makefile 的行为更像普通编程语言,减少心智负担。
3.4 ?=:条件赋值
?= 的意思是「如果变量还没定义,就赋值;否则跳过」。说白了就是设置默认值。
CC ?= gcc
CFLAGS ?= -O2 -Wall
# 如果用户在命令行传了 CC=clang,那么 CC 保持为 clang
# 如果没传,就用 gcc
这个运算符在用户可配置的构建系统中特别有用。我做一个 SDK 时,会把所有可覆盖的变量都用 ?= 定义,用户只需要在顶层 Makefile 或命令行中设置一次,就能覆盖所有子模块的默认值。
?= 最适合定义「用户可覆盖的默认配置」。比如交叉编译工具链前缀、安装路径、调试等级等。它让 Makefile 既提供合理的默认值,又保留灵活性。
3.5 +=:追加赋值
+= 用于在变量现有值后面追加内容。它继承原变量的展开方式——如果原变量是用 = 定义的,追加后仍然是递归展开;如果原变量是用 := 定义的,追加后变成简单展开。
SRCS := main.c utils.c
SRCS += network.c # SRCS 变为 main.c utils.c network.c
CFLAGS = -Wall
CFLAGS += -O2 # CFLAGS 变为 -Wall -O2(递归展开)
我在项目中常用 += 来分模块累积源文件列表。比如每个子目录的 Makefile 只追加自己的源文件,最后主 Makefile 统一编译。
# 主 Makefile
SRCS :=
# 子模块 Makefile
include subdir/module.mk
# module.mk 中
SRCS += subdir/module.c
SRCS += subdir/helper.c
这样组织代码,清晰又解耦。你想想看,如果不用 +=,你就得手动维护一个巨大的源文件列表,改一个模块就得改主 Makefile——太脆弱了。
3.6 知识结构图
下面这张图总结了四种赋值运算符的核心区别和选择策略:
3.7 实战建议
说了这么多,到底怎么选?我总结几条经验:
- 默认用
:=。它最安全、最可预测。90% 的场景都适用。 - 只有明确需要延迟求值时用
=。比如变量 A 的值依赖变量 B,而 B 在 A 之后才定义。 - 所有用户可配置的变量用
?=。这样用户可以在命令行或环境变量中覆盖,不影响 Makefile 的默认行为。 - 分模块累积用
+=。但注意原变量的展开方式——如果原变量是:=,追加后也是简单展开,性能更好。
= 是「懒汉」,:= 是「实干家」,?= 是「备胎」,+= 是「收纳盒」。选对工具,你的 Makefile 才能跑得又快又稳。