Makefile变量:自定义变量、自动变量与赋值方式

变量这东西,在Makefile里可以说是灵魂般的存在。我刚开始学Makefile那会儿,总觉得变量不就是个占位符嘛,有啥好讲的?直到有一次我维护一个上千行的构建系统,被各种变量赋值搞得晕头转向,才意识到——嗯,这里面的门道还真不少。

今天咱们就把Makefile变量这块彻底掰扯清楚。说白了,变量就是给一段文本起个名字,方便复用。但Makefile里的变量,比你想的要灵活得多。

一、自定义变量:你想怎么叫都行

自定义变量,就是你自己定义的变量。语法很简单:

CC = gcc
CFLAGS = -Wall -O2
TARGET = myapp
SRC_DIR = src
OBJ_DIR = obj

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

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

变量名大小写敏感。CCcc是两个不同的变量。我建议你统一风格,要么全大写表示全局常量,要么全小写表示局部变量。别混着来,否则自己都容易看晕。

小技巧:变量名里可以用下划线、数字,但不能以数字开头。比如 SRC_1 可以,1_SRC 不行。

二、自动变量:Makefile里的魔法符号

自动变量是Makefile内置的,不需要你定义,直接用就行。它们会根据当前规则自动取值。我刚开始觉得这些符号看着像乱码,用熟了才发现——真香。

常用的自动变量就三个:

变量 含义 示例值
$@ 目标文件名 main.o
$^ 所有依赖文件,去重 main.c utils.c
$< 第一个依赖文件 main.c

看个例子就明白了:

main.o: main.c utils.h
    gcc -c $< -o $@

这里$<展开成main.c$@展开成main.o。你想想看,如果不用自动变量,你得写成:

main.o: main.c utils.h
    gcc -c main.c -o main.o

是不是麻烦多了?而且一旦改了目标名,还得跟着改命令。自动变量让规则变得通用,这也是Makefile的精髓之一。

重点:自动变量只能在规则的命令部分使用,不能在依赖列表里用。我刚开始就犯过这个错,在依赖里写 $@,结果展开成空字符串,编译报错找了半天。

还有几个自动变量也值得记住:

  • $?:比目标新的依赖文件列表。增量编译时特别有用。
  • $*:目标文件名去掉后缀的部分。比如目标main.o$*就是main
  • $%:当目标是归档文件(.a)时,表示其中的成员名。说实话,这个我用得不多。

三、变量的赋值方式:四种写法,各有千秋

这是Makefile变量里最容易踩坑的地方。四种赋值方式,行为完全不同。我曾经在一个项目里因为搞混=:=,导致编译出来的程序行为诡异,排查了整整一个下午。

1. = 递归展开赋值

这是最基本的赋值方式。变量在使用时才展开,而不是在定义时:

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

# B 最终的值是 "hi world"
# 因为 B 在使用时才去查找 A 的值

说白了,就是延迟求值。你定义B的时候,A还没确定,没关系,等到真正用B的时候,再去拿A的值。

这种方式的优点是灵活,但缺点也很明显——容易产生循环引用:

A = $(B)
B = $(A)
# 死循环了!

Make会检测到这种循环并报错。我在项目中遇到过这种情况,当时写了一个复杂的变量依赖链,结果Make直接卡住不动了。

2. := 简单赋值

这个我强烈推荐。变量在定义时就展开,之后不再改变:

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

# B 的值是 "hello world"
# 因为 B 在定义时就已经展开了

你想想看,这种方式更符合直觉吧?定义的时候是什么值,用的时候就是什么值。不会因为后面改了别的变量而受影响。

我的习惯:能用 := 的地方,我绝不用 =。除非我真的需要延迟求值。这样代码更可预测,调试起来也省心。

3. ?= 条件赋值

只有变量未定义时,才赋值。如果已经定义过了,就忽略:

CC ?= gcc
# 如果 CC 没定义过,就设为 gcc
# 如果 CC 已经定义过了,保持原值

这个在写通用Makefile时特别有用。用户可以在命令行覆盖变量:

make CC=clang

如果Makefile里用了CC ?= gcc,那么用户指定的clang会生效。如果用了CC = gcc,那用户的指定就被覆盖了。

注意:?= 只检查变量是否被定义,不检查值是否为空。如果变量被定义为空字符串,?= 也不会覆盖它。

4. += 追加赋值

在变量原有值后面追加内容,自动加一个空格:

CFLAGS = -Wall
CFLAGS += -O2
# CFLAGS 现在是 "-Wall -O2"

这个很好理解,就是拼接字符串。但要注意,+= 的展开方式取决于变量之前是用 = 还是 := 定义的:

  • 如果之前是 =,那么 += 也是递归展开
  • 如果之前是 :=,那么 += 也是简单展开

嗯,这里要注意,别混着用,否则行为会变得很诡异。

四、知识体系总览

下面这张图把Makefile变量的核心知识点串起来了,你可以对照着回顾一下:

Makefile 变量知识体系 Makefile 变量 自定义变量 用户定义的任意变量 如:CC, CFLAGS, TARGET 自动变量 内置魔法变量 $@, $^, $<, $?, $* 赋值方式 = 递归展开 := 简单赋值 ?= 条件赋值 += 追加赋值 核心:变量展开时机决定行为差异

五、实战建议

说了这么多,总结几条我自己的经验:

  1. 优先用 :=。代码行为可预测,调试方便。除非你真的需要延迟求值,才用 =
  2. 自动变量能不用就不用?错!恰恰相反,能用自动变量的地方尽量用。它们让规则更通用,减少重复。
  3. 变量命名要有意义。别用 ABC 这种名字。我看到过有人用 XYZ 做变量名,那Makefile简直没法维护。
  4. 善用 ?=。给用户留个口子,允许他们在命令行覆盖你的默认值。这是好习惯。

好了,Makefile变量这块就讲到这里。你把这些搞明白了,写起Makefile来会顺手很多。下次咱们接着聊Makefile里的函数,那又是另一片天地了。


公众号:蓝海资料掘金营,微信deep3321