3、变量与赋值:=、:=、?=、+=的区别,自动变量的妙用

说实话,Makefile 里最容易让人翻车的,就是变量赋值。

我刚入行那会儿,写过一段 Makefile,明明定义了一个变量,编译时却死活拿不到值。我盯着屏幕看了半小时,最后发现是赋值符号用错了。嗯,从那以后,我就把四种赋值方式刻在了脑子里。

今天咱们就把它们掰开揉碎,讲清楚。

3.1 四种赋值符号,到底怎么选?

Makefile 里有四种赋值方式:=、:=、?=、+=。它们长得像,脾气可完全不同。

符号 名称 核心特点
= 递归展开赋值 使用时才展开,延迟求值
:= 简单赋值 定义时立即展开,立即求值
?= 条件赋值 仅当变量未定义时才赋值
+= 追加赋值 在原有值后面追加内容

3.2 = 与 := 的区别——这是最坑的地方

= 是延迟求值。什么意思?就是 Make 在读到 = 时,不会立刻计算右边的值,而是等到真正用到这个变量时,才去展开。

举个例子你就明白了:

VAR1 = $(VAR2)
VAR2 = hello

all:
    @echo $(VAR1)   # 输出 hello

为什么会输出 hello?因为 VAR1 在 all 目标中被使用时,Make 才去展开 $(VAR2),而此时 VAR2 已经被赋值为 hello 了。

我个人习惯,在跨平台项目里尽量少用 =。因为它的行为不够直观,尤其是在变量互相引用时,容易搞出循环引用,Make 直接报错。

再看 :=:

VAR1 := $(VAR2)
VAR2 := hello

all:
    @echo $(VAR1)   # 输出空

看到区别了吧?:= 在定义时就把 $(VAR2) 展开了,但那时 VAR2 还没定义,所以 VAR1 是空的。之后你再给 VAR2 赋值,跟 VAR1 没关系了。

核心原则:如果你希望变量值在定义时就固定下来,用 :=。如果你希望变量值在使用时才动态计算,用 =。跨平台编译中,我几乎只用 :=,因为行为可预测。

3.3 ?= 和 += 的实用场景

?= 是我在写通用 Makefile 时的心头好。它的逻辑很简单:如果变量还没定义,就赋值;如果已经定义了,就跳过。

CC ?= gcc
CFLAGS ?= -O2 -Wall

这样用户可以在命令行覆盖:make CC=clang,而 Makefile 里默认值依然保留。我在做嵌入式项目时,经常用 ?= 给工具链路径设默认值,用户想换编译器,直接传参就行,不用改 Makefile。

+= 就更直白了——追加内容。注意,它继承左边变量的赋值风格。如果左边变量是用 := 定义的,那 += 也是立即展开追加。

CFLAGS := -O2
CFLAGS += -Wall -Wextra
# 结果:CFLAGS = -O2 -Wall -Wextra

小技巧:我习惯把基础编译选项用 := 定义,然后用 += 按模块追加。这样每个模块的 Makefile 只关心自己需要加的 flag,清爽得很。

3.4 自动变量——Makefile 里的魔法符号

自动变量是 Make 内置的、在规则执行时自动赋值的变量。说白了,就是不用你手动写文件名,Make 帮你填好了。

最常用的几个:

自动变量 含义
$@ 当前规则的目标文件名
$< 第一个依赖文件
$^ 所有依赖文件,去重
$+ 所有依赖文件,不去重
$* 目标文件名中 % 匹配的部分
$? 所有比目标新的依赖文件

看个实际例子:

obj/%.o: src/%.c
    @mkdir -p $(dir $@)
    $(CC) $(CFLAGS) -c $< -o $@

这里 $@ 是 obj/main.o,$< 是 src/main.c。你想想看,如果没有自动变量,你得手动写每个文件的名字,那 Makefile 得膨胀成什么样。

我在一个大型 SDK 项目里,用自动变量配合模式规则,只用了 20 行就搞定了 300 多个源文件的编译规则。当时同事看了直呼神奇。

注意:自动变量只能在规则的命令部分使用,不能在依赖列表里用。我曾经犯过这个错,把 $@ 写在依赖里,结果 Make 把它当成普通字符串处理了,排查了半天。

3.5 实战:跨平台编译中的变量设计

好了,理论讲完了,咱们来点实在的。下面是我在跨平台项目中常用的变量组织方式:

# 平台检测
UNAME_S := $(shell uname -s)

# 工具链——允许用户覆盖
CC      ?= gcc
CXX     ?= g++
AR      ?= ar
STRIP   ?= strip

# 基础编译选项——立即求值,固定下来
CFLAGS  := -Wall -Wextra -std=c11
CXXFLAGS:= -Wall -Wextra -std=c++17

# 平台特定选项——用条件判断追加
ifeq ($(UNAME_S),Linux)
    CFLAGS   += -DLINUX -fPIC
    LDFLAGS  += -lpthread
endif
ifeq ($(UNAME_S),Darwin)
    CFLAGS   += -DMACOS -fPIC
    LDFLAGS  += -framework CoreFoundation
endif
ifeq ($(OS),Windows_NT)
    CFLAGS   += -DWIN32
    LDFLAGS  += -lws2_32
endif

# 输出目录
BUILD_DIR := build/$(UNAME_S)
$(shell mkdir -p $(BUILD_DIR))

# 模式规则——自动变量大显身手
$(BUILD_DIR)/%.o: src/%.c
    $(CC) $(CFLAGS) -c $< -o $@

$(BUILD_DIR)/%.o: src/%.cpp
    $(CXX) $(CXXFLAGS) -c $< -o $@

这套结构我用了好几年,在 Linux、macOS、Windows 下都能跑。核心思路就是::= 定基础,?= 留接口,+= 做适配,自动变量省代码。

一句话总结:变量赋值决定了值什么时候确定,自动变量决定了你怎么少写代码。两者配合好,Makefile 就能写得既清晰又强大。

Makefile 变量与赋值知识体系 = 递归展开赋值 := 简单赋值 ?= 条件赋值 += 追加赋值 核心区别:求值时机 定义时求值 vs 使用时求值 自动变量(魔法符号) $@ → 目标文件 $< → 第一个依赖 $^ → 所有依赖(去重) $* → % 匹配部分 $? → 更新的依赖 $+ → 所有依赖(不去重) 实战原则::= 定基础,?= 留接口,+= 做适配

可可精品资料库