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时一般遵循这几条:
- 能用
:=就用:=。行为确定,好调试。 - 用
?=提供默认值。让用户能方便地覆盖。 - 用
+=分层构建。把基础选项和模块特有选项分开。 - 少用
=。除非你真的需要延迟求值,比如在规则中动态生成变量名。
我曾经接手过一个项目,Makefile里全是 =,变量之间互相引用,层层嵌套。改一个变量,得把整个文件读三遍才能确定影响范围。后来我花了一天时间,全部改成 :=,世界清静了。
最后送你一句话:变量是Makefile的基石,赋值方式选对了,后面就顺了。选错了,调试起来能让你怀疑人生。