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

聊到Makefile,变量这东西是绕不开的。我刚开始写Makefile那会儿,总觉得变量就是用来偷懒的——少打几个字而已。后来项目大了才发现,变量用得好不好,直接决定了你的构建系统是「优雅」还是「灾难」。

今天咱们就把Makefile变量这块掰开揉碎了讲。嗯,内容不少,但都是干货。

2.1 变量的定义与引用

先说说最基本的。Makefile里定义变量,语法很简单:

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

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

$(TARGET): main.o utils.o
    $(CC) $(CFLAGS) -o $(TARGET) main.o utils.o

这里有个坑——变量名大小写敏感。CC 和 cc 是两个不同的变量。我在项目中见过有人混用大小写,结果编译了半天找不到命令,排查了半小时才发现是变量名写错了。

注意:变量名中不要包含空格、冒号、# 等特殊字符。虽然Make允许,但容易引发诡异问题。

2.2 自动变量

自动变量是Makefile里最实用的特性之一。说白了,就是Make帮你预定义好的一些变量,专门用在规则的命令行里。

常用的自动变量有这些:

变量 含义 示例值
$@ 目标文件名 main.o
$< 第一个依赖文件 main.c
$^ 所有依赖文件(去重) main.c utils.c
$+ 所有依赖文件(不去重) main.c utils.c
$? 比目标新的依赖文件 utils.c
$* 模式匹配中的主干部分 main

举个例子你就明白了:

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

这条规则的意思是:把每个.c文件编译成对应的.o文件。$< 自动取到当前匹配的.c文件,$@ 自动取到目标.o文件。你想想看,如果没有自动变量,你得写多少重复代码?

我的习惯:在编译规则里,我几乎只用 $@ 和 $<。链接规则里才用 $^。这样分工明确,不容易乱。

2.3 环境变量

Makefile可以读取系统的环境变量。比如 PATH、HOME、USER 这些。但这里有个容易踩的坑——环境变量和Makefile变量同名时,谁说了算?

默认情况下,Makefile里定义的变量会覆盖环境变量。但如果你用 -e 选项运行make,环境变量就会覆盖Makefile里的定义。

我曾经在一个项目里遇到过:某位同事在CI环境里设置了 CC=clang,但Makefile里写的是 CC=gcc。结果本地编译好好的,一上CI就报错。排查了半天才发现是环境变量在捣鬼。

我的建议是:

  • 不要依赖环境变量来传递关键构建参数
  • 如果必须用,用 ?= 来定义,这样只有变量未定义时才赋值
CC ?= gcc   # 如果环境变量没设CC,就用gcc

2.4 递归展开变量与简单展开变量

这部分是Makefile变量的核心,也是很多人搞不清楚的地方。

简单展开变量用 := 赋值。它的特点是:在定义时就立即计算值。

X := hello
Y := $(X) world
X := hi

# 此时 Y 的值是 "hello world",不是 "hi world"

递归展开变量用 = 赋值。它的特点是:在使用时才计算值。

X = hello
Y = $(X) world
X = hi

# 此时 Y 的值是 "hi world",因为引用Y时才去展开X

为什么会这样?说白了,= 只是记了个公式,等到真正用的时候才去算。:= 则是当场就算好,以后不管变量怎么变,都不影响。

核心区别:
  • =(递归展开):延迟计算,可能造成无限递归
  • :=(简单展开):立即计算,性能更好,行为可预测

我在项目中遇到过最经典的坑:

CFLAGS = -I$(INCDIR)
INCDIR = /usr/local/include

# 用 = 没问题,因为使用CFLAGS时才展开INCDIR

# 但如果写成这样:
CFLAGS := -I$(INCDIR)
INCDIR = /usr/local/include

# CFLAGS 就变成了 "-I",因为定义时INCDIR还没定义

所以我的建议是:

  • 大部分场景用 :=,行为更可控
  • 只有明确需要延迟展开时,才用 =
  • 绝对不要在 = 里引用自身,会死循环

2.5 知识体系总览

下面这张图帮你理清Makefile变量的整体脉络:

Makefile变量 定义与引用 $(变量名) / ${变量名} 大小写敏感 自动变量 $@ $< $^ $? 规则中的快捷方式 环境变量 PATH HOME USER ?= 安全赋值 展开方式 递归展开 (=) 使用时才计算 可能无限递归 简单展开 (:=) 定义时立即计算 行为可预测 推荐使用

2.6 实战建议

最后,分享几个我在实际项目中的经验:

  • 变量命名要有规律:我习惯用大写表示工具链相关(CC, LD, AR),小写表示项目相关(sources, objects, target)
  • 多用条件赋值:?= 是你的好朋友,既保留灵活性,又有默认值兜底
  • 善用追加赋值:+= 可以在不破坏已有值的情况下追加内容,特别适合CFLAGS这种需要逐步构建的变量
  • 复杂逻辑用函数:当变量赋值里出现 $(wildcard ...)、$(patsubst ...) 时,考虑用 := 提前计算好,避免每次使用都重复执行

嗯,Makefile变量这块就讲到这里。记住一句话:变量是Makefile的骨架,用好了事半功倍,用不好寸步难行。

一句话总结:能用 := 就别用 =,能用 ?= 就别硬编码,自动变量能省则省。

可可精品资料库