2、变量与宏:Makefile中的变量定义、自动变量、环境变量、递归展开与简单展开变量

说实话,变量和宏是Makefile里最容易被低估的东西。

很多人写Makefile,上来就是硬编码路径、硬编码编译器参数。我刚开始做嵌入式开发时也这样,直到有一次项目要同时编译三个平台——ARM、MIPS、x86——我才发现,不改变量设计,Makefile根本没法复用。

这一章,咱们就把变量这块彻底讲透。

2.1 变量定义:你其实每天都在用

Makefile里的变量,说白了就是个名字替身。你定义一个变量,后面用到的地方自动替换成它的值。

CC = gcc
CFLAGS = -Wall -O2

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

这里 CC 和 CFLAGS 就是变量。用 $() 或者 ${} 引用它们。

我个人习惯用 $(),因为跟Shell的 ${} 区分开,看着更清晰。

小技巧:变量名建议全大写,单词间用下划线。比如 MY_PROJECT_DIR,而不是 myProjectDir。这是Makefile社区的惯例,一眼就能认出是变量。

2.2 自动变量:Makefile里的"魔法符号"

自动变量这东西,我第一次接触时觉得挺玄乎。后来用多了才发现,它其实就是Make帮你预定义好的一些快捷方式。

最常用的几个:

自动变量 含义 示例值
$@ 目标文件名 hello.o
$< 第一个依赖文件 hello.c
$^ 所有依赖文件(去重) hello.c utils.c
$? 比目标新的依赖文件 utils.c
$* 去掉后缀的目标文件名 hello

举个例子你就明白了:

%.o: %.c
    $(CC) -c $< -o $@

这条规则的意思是:
$< 是 hello.c,$@ 是 hello.o。
实际执行的就是 gcc -c hello.c -o hello.o。

我在项目中遇到过一个问题:有人用 $^ 代替 $<,结果链接时把同一个.o文件传了两遍。嗯,$^ 虽然去重,但用在编译阶段会出问题。记住:编译用 $<,链接用 $^。

注意:自动变量只能在规则的命令部分使用。你不能在依赖列表里写 $@,那玩意儿是未定义的。

2.3 环境变量:Makefile与系统的桥梁

Makefile能读取系统的环境变量。比如 PATH、HOME、USER 这些。

all:
    @echo "当前用户:$(USER)"
    @echo "家目录:$(HOME)"

但这里有个坑——环境变量可以被Makefile里的同名变量覆盖。

# 假设系统环境变量 CC=gcc
CC = clang

all:
    @echo "编译器:$(CC)"  # 输出 clang,不是 gcc

我曾经在一个团队项目里踩过这个坑。有人把 CC 在Makefile里硬编码成 gcc,但CI环境里 CC 指向的是 clang。结果本地编译没问题,CI上死活过不了。

我的建议是:不要依赖环境变量来做关键决策。如果一定要用,用 ?= 来赋值:

CC ?= gcc  # 只有没定义时才用 gcc

这样既保留了环境变量的灵活性,又有默认值兜底。

2.4 递归展开与简单展开:两种变量,两种命运

这是Makefile变量里最核心、也最容易搞混的概念。

简单说:

  • 递归展开变量(用 = 定义):使用时才展开,延迟求值
  • 简单展开变量(用 := 定义):定义时就展开,立即求值

看个例子:

# 递归展开
A = hello
B = $(A) world
A = hi

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

为什么会这样?因为 B 在定义时并没有展开 $(A),它只是记下了"等我被用到时,再去查 A 的值"。等到 echo 执行时,A 已经被改成 hi 了。

再看简单展开:

# 简单展开
A = hello
B := $(A) world
A = hi

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

B 在定义时就把 $(A) 展开成了 hello,后面 A 怎么变都跟它没关系。

核心区别:
  • = 是"引用",用的时候才去查
  • := 是"复制",定义时就定死了

我个人习惯:能用 := 就用 :=。原因很简单——避免意外。递归展开虽然灵活,但容易产生循环引用,而且调试时很难追踪值的变化。

你想想看,如果一个变量被引用了三层,每层都是递归展开,中间某个地方改了值,你找bug得找到什么时候?

实战建议:
  • 路径、文件名等固定信息用 :=
  • 需要延迟求值的(比如根据目标平台动态变化)用 =
  • 条件赋值用 ?=
  • 追加赋值用 +=

2.5 知识体系总览

下面这张图,把变量与宏的核心脉络串起来了:

Makefile 变量与宏 变量定义 CC = gcc CFLAGS = -Wall $(CC) $(CFLAGS) 自动变量 $@ 目标文件 $< 第一个依赖 $^ 所有依赖 $? 更新的依赖 环境变量 $(PATH) $(HOME) CC ?= gcc 可被覆盖 展开方式 递归展开 = 延迟求值 简单展开 := 立即求值 条件赋值 ?= 追加赋值 +=

这张图把变量定义、自动变量、环境变量、展开方式串在了一起。你写Makefile时,脑子里要有这个结构——变量不是孤立的,它们互相影响。

2.6 避坑指南

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

  • 递归展开的循环引用:A = $(B) 和 B = $(A) 会导致无限递归。Make会报错,但排查起来很痛苦。
  • 自动变量在依赖列表里无效:我见过有人写 $@.c 当依赖,结果Make把它当成空字符串处理。
  • 环境变量污染:CI环境里经常有各种环境变量,一不小心就覆盖了你的Makefile变量。用 ?= 能规避大部分问题。
  • 空格陷阱:CC = gcc 和 CC=gcc 不一样。前者 CC 的值是 gcc(带前导空格),后者是 gcc。我因为这个浪费过半天时间。
曾经有个血泪教训:我在一个大型C++项目里用了递归展开变量来拼接编译选项,结果因为某个条件分支改变了中间变量的值,导致整个项目的编译选项全乱了。从那以后,我所有新项目都默认用 :=,除非有明确的延迟求值需求。

变量这东西,看着简单,用好了能让你的Makefile优雅十倍。用不好,就是给自己挖坑。记住:明确、简单、可预测,这是变量设计的三个原则。

可可精品资料库