3. Makefile变量基础:变量的定义、引用与赋值方式(=、:=、?=、+=)

说到Makefile,变量这东西,说白了就是给一串文本起个名字。你想想看,一个项目里编译选项、源文件路径、目标文件名,这些玩意儿到处重复出现,改一次得翻遍整个文件。变量就是来解决这个痛点的。

我个人习惯,写Makefile第一件事就是先把变量规划好。就像写代码前先设计数据结构一样,变量定义清楚了,后面的事情就顺了。

3.1 变量的定义与引用

定义变量很简单,就一行:

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

引用变量用 $() 或者 ${},我一般用圆括号,看着顺眼:

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

嗯,这里要注意:变量名区分大小写。CC 和 cc 是两个不同的变量。我曾经见过有人因为大小写写错,排查了半天。

变量名里可以用字母、数字、下划线。别用特殊字符,也别用Makefile的保留字,不然有你哭的。

小技巧: 变量名尽量用大写,这是约定俗成的规矩。一眼就能看出来这是个变量,不是目标名。

3.2 四种赋值方式

Makefile里有四种赋值操作符,每种行为都不一样。我刚开始学的时候也搞混过,后来踩了几个坑才彻底明白。

操作符 名称 展开时机 典型场景
= 递归展开赋值 使用时展开 需要延迟求值
:= 简单赋值 定义时展开 大多数情况
?= 条件赋值 仅当未定义时 提供默认值
+= 追加赋值 追加到已有值 增量添加

3.3 =:递归展开赋值

这是Makefile最原始的赋值方式。它的特点是:用到的时候才展开。

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

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

为什么会这样?因为 B 在定义时并没有立即把 $(A) 替换掉,而是等到 echo 执行时才去查 A 的值。这时候 A 已经被改成 hi 了。

避坑指南: 我曾经在项目里用 = 定义了一堆互相依赖的变量,结果展开时形成了无限递归,Make直接报错。如果你发现变量值莫名其妙,先检查是不是用了 =。

3.4 :=:简单赋值

这个就直白多了:定义的时候立即展开。我90%的场景都用这个。

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

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

看到区别了吧?B 在定义那一刻就把 $(A) 替换成了 hello,后面 A 怎么改都不影响它。

我个人习惯,能用 := 的地方绝不用 =。原因很简单:行为可预测,不容易出幺蛾子。你想想看,调试的时候看到一个变量,你肯定希望它的值在定义时就确定了,而不是等到使用时才去猜。

3.5 ?=:条件赋值

这个操作符的意思是:如果变量还没定义,就赋值;如果已经定义了,就跳过。

CC ?= gcc
CFLAGS ?= -O2

# 如果用户没有在命令行指定 CC,就用 gcc
# 如果用户指定了 CC = clang,那就用 clang

这个在写通用Makefile时特别有用。用户可以在命令行覆盖你的默认值:

make CC=clang CFLAGS="-O3 -march=native"

嗯,这里要注意:?= 只检查变量是否被定义过,不关心它的值是不是空。如果变量被定义成空字符串,?= 也不会覆盖它。

3.6 +=:追加赋值

这个最简单,就是在原有值后面追加内容,中间自动加空格。

CFLAGS = -Wall
CFLAGS += -O2
CFLAGS += -I/usr/local/include

# 最终 CFLAGS = -Wall -O2 -I/usr/local/include

我在项目中经常用它来分层构建编译选项。比如基础选项放一个地方,不同模块再追加自己的特殊选项:

BASE_CFLAGS := -Wall -Wextra -std=c99
MODULE_A_CFLAGS := $(BASE_CFLAGS) -DDEBUG
MODULE_B_CFLAGS := $(BASE_CFLAGS) -O2 -march=native
核心要点:
  • := 是「现在就要」——定义时展开
  • = 是「到时候再说」——使用时展开
  • ?= 是「你没给?那我给」——仅当未定义时赋值
  • += 是「再加点料」——追加到已有值后面

3.7 实战中的变量使用建议

说了这么多理论,来点实际的。我写Makefile时一般遵循这几条:

  1. 能用 := 就用 :=。行为确定,好调试。
  2. 用 ?= 提供默认值。让用户能方便地覆盖。
  3. 用 += 分层构建。把基础选项和模块特有选项分开。
  4. 少用 =。除非你真的需要延迟求值,比如在规则中动态生成变量名。

我曾经接手过一个项目,Makefile里全是 =,变量之间互相引用,层层嵌套。改一个变量,得把整个文件读三遍才能确定影响范围。后来我花了一天时间,全部改成 :=,世界清静了。

最后送你一句话:变量是Makefile的基石,赋值方式选对了,后面就顺了。选错了,调试起来能让你怀疑人生。

Makefile变量赋值方式对比 = 递归展开赋值 := 简单赋值 ?= 条件赋值 += 追加赋值 使用时展开 延迟求值 可能递归 慎用 定义时展开 立即求值 行为确定 推荐使用 未定义才赋值 提供默认值 用户可覆盖 通用场景 追加到末尾 自动加空格 分层构建 增量添加 核心原则:能用 := 就用 :=,用 ?= 提供默认值,用 += 分层构建 少用 =,除非你真的需要延迟求值

可可精品资料库