3. 变量与赋值:Makefile 的灵魂所在

说实话,我刚接触 Makefile 那会儿,觉得变量就是个花架子。不就是存个字符串嘛,有啥好学的?直到我在一个嵌入式项目里,手动改了 37 处路径,结果漏了 2 处,编译出来一堆莫名其妙的错误……嗯,从那以后,我再也不敢小看变量了。

今天咱们就好好聊聊 Makefile 里的变量。说白了,它就是你的“代码复用神器”。你想想看,一个项目里源文件路径、编译器选项、链接库,这些东西到处重复出现,改一次得改一堆地方,多痛苦。变量就是来解决这个问题的。

3.1 变量的定义与引用

定义变量很简单,基本语法就一行:

变量名 = 值

引用变量时,用 $() 或者 ${} 把变量名包起来。我个人习惯用 $(),看着更清爽。

CC = gcc
CFLAGS = -Wall -O2

hello: hello.c
    $(CC) $(CFLAGS) -o $@ $^

这里 $(CC) 展开成 gcc,$(CFLAGS) 展开成 -Wall -O2。你看,改编译器或者加编译选项,只需要改一处就行。

我的小习惯:变量名尽量用大写,这样一眼就能看出它是变量。比如 CC、CFLAGS、LDFLAGS,这是 Makefile 社区约定俗成的写法。

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 变量知识体系,帮你理清思路:

Makefile 变量与赋值知识体系 变量与赋值 变量定义与引用 变量名 = 值 引用:$(变量名) 或 ${变量名} 自动变量 $@:目标文件 $^:所有依赖 $<:第一个依赖 赋值运算符 =:递归展开(延迟) :=:简单赋值(立即) ?=:条件赋值(默认值) +=:追加赋值 核心原则:能用 := 就别用 =,自动变量能省则省

3.6 实战经验总结

说了这么多,最后分享几个我自己的实战心得:

  • 默认用 :=:除非你明确需要延迟展开,否则 := 是最安全的选择。它的行为跟大多数编程语言一样,不容易出幺蛾子。
  • 自动变量是神器:写规则时尽量用 $@、$^、$<,这样规则更通用,复制到其他项目也能直接用。
  • ?= 用于提供默认值:比如 CC ?= gcc,用户可以在命令行用 make CC=clang 覆盖,非常灵活。
  • += 用于逐步构建:比如编译选项、源文件列表,用 += 可以分模块添加,代码更清晰。
最后说一句:变量用得好,Makefile 就成功了一半。我见过太多人把 Makefile 写成“面条代码”,几百行全是硬编码的路径和命令。用变量把它们抽象出来,你的 Makefile 会变得又短又清晰,维护起来也轻松得多。

可可精品资料库