4、变量基础:变量的定义与使用、自动变量($@, $<, $^)、变量的赋值方式(=, :=, ?=, +=)
说实话,很多初学者写 Makefile,最头疼的就是变量。我见过不少人,明明 C 代码写得挺溜,一碰到 Makefile 里的变量就懵了。其实没那么复杂,今天咱们就把这层窗户纸捅破。
4.1 变量是什么?为什么要用?
变量,说白了就是个“代号”。你想想看,如果在一个大项目里,你每次写编译命令都要把 gcc、源文件路径、头文件路径敲一遍,那得多累?而且万一哪天编译器路径变了,你还得满世界去找去改。
变量就是用来解决这个问题的。它让你把重复的内容“抽”出来,放到一个地方统一管理。
核心思想: 一次定义,多处使用。改一处,全局生效。
4.2 变量的定义与使用
定义变量很简单,格式就是:
变量名 = 值
使用变量时,用 $(变量名) 或者 ${变量名} 来引用。我个人习惯用 $(),看着更清晰。
来看个最简单的例子:
CC = gcc
CFLAGS = -Wall -g
hello: hello.c
$(CC) $(CFLAGS) -o hello hello.c
这里 CC 和 CFLAGS 就是变量。以后想换编译器?改一行 CC = clang 就行。我在项目中遇到过好几次,客户要求从 gcc 换成交叉编译器,要是没有变量,那改起来真是要命。
4.3 自动变量:$@, $<, $^
自动变量是 Makefile 里最“聪明”的变量。它们不需要你定义,Make 会自动帮你填好值。说白了,就是 Make 在干活的时候,顺手给你记下来的“小抄”。
| 自动变量 | 含义 | 举个例子 |
|---|---|---|
$@ |
目标文件的名称 | 如果目标是 hello,那 $@ 就是 hello |
$< |
第一个依赖文件的名称 | 如果依赖是 hello.c main.c,那 $< 就是 hello.c |
$^ |
所有依赖文件的名称(去重) | 如果依赖是 hello.c main.c,那 $^ 就是 hello.c main.c |
用自动变量改写上面的例子:
CC = gcc
CFLAGS = -Wall -g
hello: hello.c
$(CC) $(CFLAGS) -o $@ $<
你看,$@ 自动变成了 hello,$< 自动变成了 hello.c。这样写的好处是,规则变得通用。如果你有多个目标文件,只需要写一条通用规则就行。
我的小技巧: 写通用规则时,我几乎必用 $@ 和 $^。比如编译所有 .c 文件到 .o 文件:
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
这一条规则,就能处理所有源文件。省事吧?
4.4 变量的赋值方式
嗯,这里要注意。Makefile 里赋值方式有四种,很多人一开始会搞混。我当年也踩过坑,咱们一个一个说清楚。
4.4.1 = (递归展开赋值)
这是最基本的赋值方式。它的特点是:等到使用时才展开。什么意思呢?
A = hello
B = $(A) world
A = hi
all:
@echo $(B) # 输出:hi world
为什么会这样?因为 B 在定义时,并没有立刻把 A 的值拿过来。它只是记了个“引用”。等到 echo 真正要用 B 的时候,才去查 A 的值。而此时 A 已经被改成 hi 了。
我曾经踩过的坑: 用 = 赋值时,如果变量之间有循环引用,比如 A = $(B) 和 B = $(A),Make 会陷入死循环。所以,除非你清楚自己在做什么,否则尽量少用 =。
4.4.2 := (简单赋值)
这个就直白多了。定义时立即展开,值是什么就是什么,后面再改前面的变量也不影响。
A = hello
B := $(A) world
A = hi
all:
@echo $(B) # 输出:hello world
看到了吗?B 在定义时就把 A 当时的 hello 拿过来了,后面 A 怎么变,跟 B 没关系。
我建议: 绝大多数情况下,用 := 更安全。它的行为符合直觉,不容易出幺蛾子。我在写大型项目的 Makefile 时,几乎全部用 :=。
4.4.3 ?= (条件赋值)
这个很简单:如果变量没定义过,才赋值;如果已经定义了,就跳过。
CC ?= gcc
这行代码的意思是:如果外面没有传 CC 进来,我就用 gcc;如果用户在命令行里指定了 make CC=clang,那就用 clang。
这个在写通用 Makefile 时特别有用。你想想看,你写了一个 Makefile 给别人用,人家可能想用自己的编译器。用 ?= 就给了用户一个“覆盖”的机会。
4.4.4 += (追加赋值)
这个最好理解,就是在原有值后面追加内容。
CFLAGS = -Wall
CFLAGS += -g
CFLAGS += -O2
# 最终 CFLAGS 的值是:-Wall -g -O2
我在项目中经常这么用。比如基础编译选项写在前面,然后根据不同的平台或调试需求,再追加一些选项。这样代码很干净,也容易维护。
4.5 知识体系总览
说了这么多,咱们用一张图把变量这块的知识串起来。你一看就明白了。
4.6 实战小例子
最后,咱们把今天学的串起来,写一个完整的 Makefile:
# 编译器设置
CC := gcc
CFLAGS := -Wall -g
# 源文件和目标文件
SRCS := main.c utils.c
OBJS := $(SRCS:.c=.o)
TARGET := myapp
# 默认目标
all: $(TARGET)
# 链接
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
# 编译
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# 清理
clean:
rm -f $(OBJS) $(TARGET)
# 条件赋值:如果用户没指定,就用默认的
INSTALL_DIR ?= /usr/local
install: $(TARGET)
cp $(TARGET) $(INSTALL_DIR)
你看,这个 Makefile 里,:= 保证了变量不会被意外覆盖,$@ 和 $^ 让规则变得通用,?= 给了用户自定义安装路径的余地。这就是变量在实战中的用法。
好了,变量这块就讲到这里。记住一句话:用好变量,你的 Makefile 就成功了一半。