3. 变量与赋值

各位同学,今天我们来聊聊Makefile里最基础、也最容易踩坑的部分——变量与赋值。

说实话,我见过不少工程师,C语言写得飞起,一到Makefile就懵了。尤其是那几个赋值符号:=、:=、?=、+=,看着差不多,用起来天差地别。我自己刚入行那会儿,就因为搞混了=和:=,导致一个项目编译了半小时,结果链接的全是旧对象文件……嗯,那种感觉,你懂的。

所以今天咱们就把这块彻底讲透。你想想看,如果连变量赋值都搞不清楚,后面那些条件判断、函数调用、多目录构建,根本没法玩。

3.1 变量的定义与引用

先说说最基本的。Makefile里的变量,说白了就是一个名字对应一串文本。定义很简单:

CC = gcc
CFLAGS = -Wall -O2
TARGET = myapp

引用变量时,用$(变量名)或者${变量名}。我个人习惯用圆括号,看着更清晰:

$(CC) $(CFLAGS) -o $(TARGET) main.c

这里有个细节要注意:变量名可以包含字母、数字、下划线,但不能以数字开头。我在项目中遇到过有人用1st_OBJ这种名字,结果make直接报错——嗯,这种坑踩一次就记住了。

3.2 四种赋值符号的区别

好,重头戏来了。这四个符号,咱们一个一个拆开讲。

3.2.1 = —— 递归展开赋值

=是Makefile里最原始的赋值方式。它的特点是:延迟展开。什么意思呢?就是变量的值在定义时并不立即计算,而是在使用时才去展开。

A = hello
B = $(A) world
A = hi

all:
    @echo $(B)   # 输出:hi world

看到了吗?B定义的时候,A还是hello,但最后输出却是hi world。因为B的值是在echo时才展开的,那时候A已经被改成hi了。

警告:这种延迟展开很容易导致意料之外的结果。我曾经在大型项目中看到有人用=定义了一堆互相依赖的变量,结果调试了一整天,最后发现是变量展开顺序出了问题。所以我的建议是:除非你明确需要延迟展开,否则尽量少用=。

3.2.2 := —— 简单赋值

:=是我个人最常用的赋值方式。它的特点是:立即展开。变量在定义时就计算好值,后面再怎么改都不影响。

A := hello
B := $(A) world
A := hi

all:
    @echo $(B)   # 输出:hello world

这里B在定义时就把$(A)展开成了hello,所以后面A怎么变,B都不受影响。

技巧:在大多数场景下,我建议优先使用:=。它的行为更接近C语言里的变量赋值,容易理解,也容易调试。特别是定义编译选项、路径这些不会变的值时,用:=最安全。

3.2.3 ?= —— 条件赋值

?=的意思是:如果变量没有被定义过,才赋值;否则忽略。

CC ?= gcc
CFLAGS ?= -O2

all:
    @echo $(CC) $(CFLAGS)

如果你在命令行里传入了CC=clang,那么Makefile里的CC ?= gcc就不会生效。这个特性在写通用Makefile时特别有用——用户可以通过命令行覆盖默认值。

我记得有一次帮团队写一个跨平台的构建脚本,Linux上用gcc,macOS上用clang。我就用?=定义了默认编译器,用户只要在命令行里指定一下就行,Makefile本身不用改。嗯,这种设计思路,说白了就是「给用户留个后门」。

3.2.4 += —— 追加赋值

+=用于在变量原有值后面追加内容。它会自动在中间加一个空格。

CFLAGS := -Wall
CFLAGS += -O2
CFLAGS += -DDEBUG

all:
    @echo $(CFLAGS)   # 输出:-Wall -O2 -DDEBUG

这里要注意一点:+=的展开方式取决于变量最初的定义方式。如果变量最初是用:=定义的,那么+=也是立即展开;如果最初是用=定义的,那么+=也是延迟展开。

核心要点:

  • =:延迟展开,使用时才计算
  • :=:立即展开,定义时就计算
  • ?=:条件赋值,仅当变量未定义时生效
  • +=:追加内容,展开方式继承原变量

3.3 实战场景对比

光讲理论不行,咱们来看几个实际项目中会遇到的情况。

场景 推荐符号 原因
定义编译器路径 := 路径不会变,立即展开更安全
定义可被用户覆盖的选项 ?= 用户可以在命令行传入自己的值
逐步构建编译选项 += 分模块追加,代码更清晰
定义依赖其他变量的复杂表达式 = 需要延迟展开时使用,但要小心

举个例子,我在一个嵌入式项目中写过这样的Makefile:

# 用户可覆盖的配置
CROSS_COMPILE ?= arm-none-eabi-
CC := $(CROSS_COMPILE)gcc
LD := $(CROSS_COMPILE)ld

# 编译选项逐步构建
CFLAGS := -mcpu=cortex-m4 -mthumb
CFLAGS += -Wall -Wextra
CFLAGS += -O2
CFLAGS += -I./include

# 链接选项(延迟展开,因为后面可能追加库路径)
LDFLAGS = -T $(LINKER_SCRIPT)
LDFLAGS += -L./lib

你看,这里CC和LD用:=,因为交叉编译工具链一旦确定就不会变。CFLAGS用+=逐步追加,方便阅读和维护。LDFLAGS用=,因为LINKER_SCRIPT可能在后面才被定义。

3.4 知识体系总览

为了让你对整个变量赋值体系有个直观的认识,我画了一张图:

Makefile变量赋值体系 变量赋值 = 递归展开 := 简单赋值 ?= 条件赋值 += 追加赋值 特性 • 延迟展开 • 使用时才计算 • 容易产生递归 特性 • 立即展开 • 定义时计算 • 行为可预测 特性 • 仅首次赋值 • 可被外部覆盖 • 适合默认值 特性 • 追加内容 • 自动加空格 • 继承展开方式 使用建议 ✅ 优先使用 :=,行为清晰可控 ✅ 需要用户覆盖时用 ?= ✅ 逐步构建选项时用 += ⚠️ 谨慎使用 =,除非明确需要延迟展开

3.5 避坑指南

最后,分享几个我亲身踩过的坑:

  • 不要用=定义递归变量:比如A = $(A) foo,这会导致无限递归,make会直接报错。我曾经在代码审查时看到有人这么写,当时就愣住了——嗯,后来发现是新手把shell脚本的习惯带过来了。
  • 注意+=的展开方式:如果变量最初是用=定义的,那么+=追加的内容也是延迟展开的。这有时候会带来意想不到的结果。
  • 命令行传参优先级最高:不管Makefile里用哪种赋值方式,命令行传入的变量都会覆盖Makefile里的定义。只有?=能「感知」到这一点,其他赋值方式都会被覆盖。

我的个人习惯:在写Makefile时,我会先用:=定义所有「固定」的变量,比如编译器路径、源文件列表。然后用?=定义那些允许用户覆盖的变量,比如编译选项。最后用+=在条件判断中追加内容。至于=,我只有在写一些复杂的、需要延迟展开的表达式时才会用到,而且一定会加注释说明原因。

好了,关于变量与赋值,今天就讲到这里。记住一句话:赋值符号选对了,Makefile就成功了一半。下次写Makefile时,多想想你用的是哪个符号,为什么用这个符号——养成这个习惯,你会少踩很多坑。


可可精品资料库