3、Makefile变量基础:变量的定义、赋值与引用,以及变量的作用域
说实话,变量是Makefile的灵魂。没有变量,你写的Makefile就是一堆硬编码的路径和命令,改一次要改半天。我刚开始学Makefile时,觉得变量不就是个占位符嘛,后来才发现,这里面的门道比我想象的多得多。
今天我们就来彻底搞懂Makefile里的变量。从怎么定义、怎么赋值、怎么引用,到变量到底在哪些地方能生效。这些东西搞明白了,你写Makefile的效率至少翻一倍。
3.1 变量的定义与引用
先看最基础的东西。Makefile里定义变量很简单,就是 变量名 = 值 这种格式。引用变量时用 $(变量名) 或者 ${变量名},我个人习惯用圆括号,看着更清晰。
CC = gcc
CFLAGS = -Wall -O2
TARGET = myapp
$(TARGET): main.c utils.c
$(CC) $(CFLAGS) -o $(TARGET) main.c utils.c
你看,这里定义了三个变量:CC、CFLAGS、TARGET。后面引用的时候,直接用 $(变量名) 就行。这样做的好处很明显——如果我想换编译器,只需要改第一行,不用满文件去找 gcc 这个词。
3.2 四种赋值方式
这里有个坑,我当年踩过。Makefile里赋值不是只有 = 一种,实际上有四种不同的赋值操作符。每种的行为都不一样,用错了会出大问题。
| 操作符 | 名称 | 特点 | 使用场景 |
|---|---|---|---|
= |
递归展开赋值 | 使用时才展开,引用后续定义的变量也能生效 | 需要延迟求值的场景 |
:= |
简单赋值 | 定义时立即展开,只使用当前已定义的变量 | 大多数情况,推荐使用 |
?= |
条件赋值 | 只有变量未定义时才赋值 | 提供默认值 |
+= |
追加赋值 | 在原有值后面追加内容,自动加空格 | 逐步构建变量值 |
3.2.1 递归展开赋值(=)
这是Makefile默认的赋值方式。它的特点是:等到真正使用变量的时候才去计算值。这意味着,你可以在定义变量A时引用变量B,即使B还没定义,只要在使用A之前B被定义了就行。
A = $(B) world
B = hello
all:
echo $(A) # 输出:hello world
为什么会这样?因为 echo $(A) 执行时,Make会去展开 A,发现 A = $(B) world,然后继续展开 $(B),此时 B 已经定义好了,所以结果是 hello world。
3.2.2 简单赋值(:=)
这个我强烈推荐大家多用。简单赋值在定义时就把值算好了,后面再怎么改引用的变量,都不会影响已经赋值的变量。
B = hello
A := $(B) world
B = hi
all:
echo $(A) # 输出:hello world,不是 hi world
你看,虽然 B 后来改成了 hi,但 A 在定义时就已经把 $(B) 展开成 hello 了。这种行为更符合大多数编程语言的直觉,不容易出意外。
3.2.3 条件赋值(?=)
这个很简单:如果变量还没定义,就赋值;如果已经定义了,就什么都不做。常用于提供默认值。
CC ?= gcc # 如果没定义 CC,就用 gcc
# 用户可以在命令行覆盖:
# make CC=clang
我在写通用Makefile时特别喜欢用 ?=,这样用户可以通过命令行传参覆盖默认值,灵活性很高。
3.2.4 追加赋值(+=)
在原有值后面追加内容,自动加空格分隔。这个在逐步构建编译选项时特别有用。
CFLAGS = -Wall
CFLAGS += -O2
CFLAGS += -I./include
# 最终 CFLAGS = -Wall -O2 -I./include
3.3 变量的作用域
变量不是在哪都能用的。Makefile里变量的作用域分三种:全局变量、目标级变量、模式级变量。搞混了的话,你会发现有些变量在某个规则里就是取不到值。
3.3.1 全局变量
在Makefile顶层定义的变量,默认就是全局的。整个Makefile文件以及它包含的其他Makefile都能访问到。
# 全局变量
PROJECT = myapp
include config.mk # config.mk 里也能用 $(PROJECT)
all:
echo $(PROJECT)
3.3.2 目标级变量
有时候你希望某个变量只对特定的目标生效。可以用 目标: 变量名 = 值 这种语法。
# 只对 debug 目标生效
debug: CFLAGS = -g -O0
debug:
$(CC) $(CFLAGS) main.c
# 这里的 CFLAGS 还是全局的值
release:
$(CC) $(CFLAGS) main.c
这个特性我在调试时经常用。正常编译用 -O2,调试时只想加 -g,用目标级变量就不用改全局配置。
3.3.3 模式级变量
模式级变量跟模式规则类似,用 % 匹配一类目标。比如让所有 .o 文件都带某个编译选项。
# 所有 .o 文件都加上这个选项
%.o: CFLAGS += -DDEBUG
main.o: main.c
$(CC) $(CFLAGS) -c main.c
utils.o: utils.c
$(CC) $(CFLAGS) -c utils.c
3.4 变量覆盖与命令行传参
Makefile里有个优先级的问题。变量的最终值取决于它在哪被定义的。优先级从高到低是这样的:
- 命令行参数:
make CC=clang,最高优先级 - Makefile中的赋值:普通定义
- 环境变量:从Shell继承来的变量,优先级最低
举个例子:
# 假设环境变量 CC=gcc
CC = g++
all:
echo $(CC) # 输出 g++
# 但如果执行 make CC=clang,输出就是 clang
这里有个小坑:如果你想禁止用户通过命令行覆盖某个变量,可以用 override 指令。
override CC = gcc # 命令行传参也改不了
不过我个人不太建议用 override,除非你有非常特殊的需求。剥夺用户的灵活性,有时候会带来麻烦。
3.5 知识结构图
下面这张图把变量相关的核心知识点串起来了,方便你对照复习。
3.6 实战建议
说了这么多,最后给几个我实际项目里总结出来的建议:
- 能用
:=就别用=:简单赋值的行为更可控,不容易出诡异的问题。 - 善用
?=提供默认值:这样你的Makefile既好用又灵活。 - 目标级变量用来做调试配置:比如
debug: CFLAGS = -g,干净利落。 - 别滥用
override:除非你有充分的理由,否则让用户能覆盖你的变量是好事。
核心要点:变量是Makefile的基石。搞懂了定义、赋值、引用和作用域,你就能写出可维护、可扩展的构建系统。记住四种赋值方式的区别,特别是 = 和 := 的差异,这是新手最容易踩坑的地方。