3、变量与赋值:变量的定义与引用、自动变量($@、$<、$^)、赋值运算符(=、:=、?=、+=)的区别

变量这东西,说白了就是 Makefile 的灵魂。没有变量,你写出来的东西就是一堆硬编码的路径和文件名,改一次能让你改到怀疑人生。我刚开始用 Make 的时候,就吃过这个亏——项目路径一换,整个 Makefile 几乎重写。后来我才明白,变量用得好,维护成本能降一大半。

3.1 变量的定义与引用

定义变量很简单,基本格式就是:

变量名 = 值

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

举个例子:

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

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

你看,这样写的好处是,如果我想换编译器,只需要改 CC 的值,不用满文件去找 gcc 这个词。我在项目中遇到过好几次,客户要求换交叉编译工具链,这时候变量就派上大用场了。

小技巧:变量名建议用大写字母,这样一眼就能看出哪些是变量。当然,小写也行,但团队协作时最好统一风格。

3.2 自动变量:$@、$<、$^

自动变量是 Make 内置的「快捷方式」。它们不需要你定义,Make 会自动帮你填好值。嗯,这里要注意,自动变量只能在规则的命令行中使用,不能在依赖列表中用。

自动变量 含义 示例
$@ 目标文件的完整名称 如果目标是 myapp,$@ 就是 myapp
$< 第一个依赖文件的名称 如果依赖是 main.c utils.c,$< 就是 main.c
$^ 所有依赖文件的名称,用空格分隔 如果依赖是 main.c utils.c,$^ 就是 main.c utils.c

用自动变量改写上面的例子:

myapp: main.c utils.c
    gcc -Wall -O2 -o $@ $^

是不是简洁多了?我曾经在一个大型项目中,把几十个目标文件的编译规则全部用自动变量重构了一遍,Makefile 从 300 行缩到了 80 行。你想想看,这种爽感,谁用谁知道。

重点:自动变量能帮你写出「通用规则」。比如模式规则中,$@ 和 $< 几乎是标配。

3.3 赋值运算符的区别

这是新手最容易踩坑的地方。四种赋值运算符,各有各的脾气。

3.3.1 = (递归展开赋值)

这是最基本的赋值方式。它的特点是:延迟展开。也就是说,变量在引用时才会计算值。

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

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

为什么会这样?因为 B 在定义时并没有立即计算 $(A) 的值,而是等到你真正用到 B 的时候,才去查找 A 当前的值。我在项目中遇到过一个问题:用 = 赋值时,不小心形成了循环引用,Make 直接报错。所以,用 = 要小心,别把自己绕进去。

3.3.2 := (简单赋值)

这个就好理解了:立即展开。定义时就把值算好,后面再怎么改前面的变量,都不影响。

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

# 此时 B 的值是 "hello world",不会变

我个人习惯,能用 := 的地方尽量用 :=。为什么?因为行为更可预测,不容易出幺蛾子。特别是定义路径、编译器选项这些固定内容时,:= 是最安全的。

警告:如果你用 = 定义了一个变量,然后又用这个变量去定义另一个变量,最后再改第一个变量的值——嗯,你可能会得到意想不到的结果。我曾经调试过一个 bug,查了半小时才发现是 = 的延迟展开搞的鬼。

3.3.3 ?= (条件赋值)

这个运算符的意思是:如果变量没有被定义过,才赋值。如果已经定义过了,就忽略。

CC ?= gcc

# 如果外部没有传入 CC,就用 gcc
# 如果外部传了 CC=clang,那就用 clang

这个在写通用 Makefile 时特别有用。比如你想让用户可以在命令行覆盖某些配置:

make CC=clang CFLAGS="-O3"

这时候 ?= 就能保证用户传入的值不会被 Makefile 内部的赋值覆盖掉。

3.3.4 += (追加赋值)

这个最简单,就是在原有值后面追加内容。注意,它会自动加一个空格。

CFLAGS = -Wall
CFLAGS += -O2
CFLAGS += -g

# 最终 CFLAGS 的值是 "-Wall -O2 -g"

我在项目中经常用 += 来分层构建编译选项。比如基础选项放在一个地方,调试选项放在另一个地方,最后用 += 合并起来。这样代码清晰,也方便维护。

3.4 知识体系总览

下面这张图,是我梳理的本章核心逻辑。你看一眼,基本就能把变量和赋值这块串起来了。

Makefile 变量与赋值知识体系 变量与赋值 变量定义与引用 格式: 变量名 = 值 引用: $(变量名) 或 ${变量名} 建议大写命名,团队统一风格 自动变量 $@ : 目标文件 $< : 第一个依赖 $^ : 所有依赖 只能在命令行中使用 赋值运算符 = 递归展开(延迟) := 简单赋值(立即) ?= 条件赋值(未定义才赋) += 追加赋值(自动加空格) 核心原则: 固定内容用 :=,可覆盖配置用 ?=,分层构建用 +=

3.5 实战建议

说了这么多,最后给你几个我自己的经验:

  • 能用 := 就别用 =——除非你明确需要延迟展开的特性。我见过太多因为 = 导致的诡异 bug 了。
  • 自动变量是你的好朋友——写通用规则时,$@ 和 $^ 能让你少写 80% 的重复代码。
  • ?= 是给用户留的后门——如果你在写一个供他人使用的 Makefile,记得用 ?= 定义关键变量,让用户可以灵活覆盖。
  • += 适合分层管理——把基础配置、调试配置、发布配置分开写,最后用 += 组合,清晰又好维护。
我的习惯:写 Makefile 时,我会先列一个变量清单,把路径、编译器、选项这些「可能会变的东西」全部抽成变量。然后再写规则。这样后期改起来,真的省心很多。

好了,变量和赋值这块就聊到这儿。记住一句话:变量用得好,Makefile 写得少。下一节我们聊聊条件判断和函数,那又是另一番天地了。

可可精品资料库