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 就能写得既清晰又强大。
可可精品资料库