3. Make工具入门
变量是 Makefile 的灵魂。没有变量,你写出来的就是一坨硬编码的路径和命令,改一次要翻遍整个文件。我刚开始学 Makefile 时,就被这四种赋值符搞晕过——=、:=、?=、+=,长得差不多,行为却天差地别。今天咱们就把它们彻底掰扯清楚。
3.1 变量的定义与引用
先看最基本的用法。定义一个变量,然后引用它:
CC = gcc
CFLAGS = -Wall -O2
hello: hello.c
$(CC) $(CFLAGS) -o hello hello.c
引用变量用 $() 或 ${},我习惯用圆括号,看着更清爽。单字符变量可以省略括号,比如 $@、$<,但多字符必须加括号,否则 Make 会把它当成 $(C) 加一个字母 C,结果就是空值。
$CFLAGS,Make 解析成 $(C)FLAGS,变量 C 没定义,直接展开成空字符串。这种 bug 很难肉眼发现。
3.2 四种赋值运算符的区别
这四种赋值符的核心区别在于:什么时候展开。说白了,就是 Make 在解析时,是立刻计算右边的值,还是等到使用时再算。
| 运算符 | 名称 | 展开时机 | 典型场景 |
|---|---|---|---|
= |
递归展开赋值 | 使用时展开 | 需要延迟求值 |
:= |
简单展开赋值 | 定义时展开 | 固定值、避免递归 |
?= |
条件赋值 | 仅当未定义时赋值 | 默认值 |
+= |
追加赋值 | 取决于原变量类型 | 增量添加 |
3.3 =:递归展开赋值
这是 Makefile 的默认赋值方式。它的特点是:右值在变量被使用时才展开。你想想看,这意味着什么?
A = hello
B = $(A) world
A = hi
all:
@echo $(B) # 输出 "hi world"
为什么会这样?因为 B 在定义时并没有把 $(A) 展开成 hello,它只是记下了 $(A) world 这个表达式。等到 echo $(B) 执行时,A 已经被改成 hi 了,所以最终结果是 hi world。
我的经验: 递归展开适合定义那些依赖其他变量的变量。比如 OBJS = $(SRCS:.c=.o),你可以在后面再定义 SRCS,顺序无所谓。但小心别写出循环引用——A = $(B) 和 B = $(A) 会让 Make 无限递归,直接报错。
3.4 :=:简单展开赋值
这个就好理解了:定义时立即展开右边的所有变量。一旦赋值完成,右边的内容就固定死了。
A := hello
B := $(A) world
A := hi
all:
@echo $(B) # 输出 "hello world"
看到区别了吗?B 在定义时就把 $(A) 展开成了 hello,后面 A 怎么改都影响不到 B。
我个人习惯在绝大多数场景下用 :=。为什么?因为行为可预测,不会出现「变量值被后面代码偷偷改了」这种诡异问题。尤其是大型项目,几十个 Makefile 互相 include,用 := 能省掉很多调试时间。
:=。它更接近常规编程语言的赋值语义,心智负担小。
3.5 ?=:条件赋值
这个运算符的意思是:如果变量还没定义,就赋值;如果已经定义了,就跳过。说白了就是「设置默认值」。
CC ?= gcc
CFLAGS ?= -O2
all:
@echo $(CC) $(CFLAGS)
如果你在命令行传入了 make CC=clang,那么 CC ?= gcc 就不会执行,CC 保持为 clang。如果没传,就用 gcc 作为默认值。
我在项目中经常用 ?= 来让用户覆盖编译选项。比如:
INSTALL_DIR ?= /usr/local
DEBUG ?= 0
这样用户可以在命令行指定 make DEBUG=1,而不用改 Makefile 本身。
?= 只检查变量是否被定义过,不检查值是否为空。如果你之前写了 VAR =(赋空值),VAR ?= value 不会生效,因为 VAR 已经被定义了。
3.6 +=:追加赋值
这个最直观:在变量原有值后面追加内容,中间自动加一个空格。
CFLAGS := -Wall
CFLAGS += -O2
CFLAGS += -DNDEBUG
all:
@echo $(CFLAGS) # 输出 "-Wall -O2 -DNDEBUG"
但有个坑:+= 的展开行为取决于原变量是用 = 还是 := 定义的。
- 如果原变量用
:=定义,+=追加时右边立即展开。 - 如果原变量用
=定义,+=追加时右边延迟展开。
举个例子:
A = $(B)
A += $(B)
B = hello
all:
@echo $(A) # 输出 "hello hello"
因为 A 最初是递归展开的,+= 追加的 $(B) 也是延迟展开,最终两个 $(B) 都展开成 hello。
换成 := 试试:
A := $(B)
A += $(B)
B = hello
all:
@echo $(A) # 输出 " hello"
第一次赋值时 B 还没定义,A 是空。第二次追加时,$(B) 立即展开,但 B 仍然没定义,所以追加的还是空。最终 A 只有一个空格。
避坑指南: 我曾经在一个项目里混合使用 = 和 +=,结果变量值总是多出一些莫名其妙的内容。后来全部改成 := 加 +=,问题就消失了。记住:统一用 := 定义基础变量,然后用 += 追加,这样行为最可控。
3.7 四种赋值符的选择策略
说了这么多,到底怎么选?我总结了一个简单的决策流程:
3.8 实战中的常见模式
最后分享几个我在实际项目中常用的变量定义模式:
# 1. 用户可覆盖的配置
INSTALL_PREFIX ?= /usr/local
DEBUG ?= 0
# 2. 立即展开的固定值
CC := gcc
AR := ar
# 3. 依赖其他变量的路径
SRC_DIR := src
BUILD_DIR := build
CFLAGS := -I$(SRC_DIR) -Wall -O2
# 4. 增量追加
CFLAGS += -DDEBUG=$(DEBUG)
LDFLAGS := -lm
LDFLAGS += -lpthread
# 5. 延迟展开(特殊场景)
TARGET = $(BUILD_DIR)/$(PROJECT_NAME)
PROJECT_NAME := myapp
:=,所有「允许用户改」的变量用 ?=,所有「需要追加」的用 +=。至于 =,我只有在写递归变量时才用,比如 OBJS = $(SRCS:.c=.o) 这种。这样分工明确,看代码的人一眼就能知道每个变量的意图。
嗯,四种赋值符的区别就讲到这里。记住一句话:展开时机决定一切。理解了这一点,你就能驾驭 Makefile 的变量系统了。