3、变量与赋值:变量的定义与引用、=、:=、?=、+=的区别、变量的作用域、环境变量与Makefile变量
变量,是Makefile的灵魂。
说实话,我见过不少新手写Makefile,把所有路径、编译器、标志位都硬编码在规则里。改一次编译器版本,得翻遍整个文件。嗯,这其实是在给自己挖坑。今天我们就来聊聊Makefile里的变量,怎么定义、怎么用,以及那些让人头疼的赋值运算符到底有什么区别。
3.1 变量的定义与引用
定义变量很简单,就像这样:
CC = gcc
CFLAGS = -Wall -O2
TARGET = myapp
引用变量时,用$(变量名)或者${变量名}。我个人习惯用圆括号,看着更清晰:
$(CC) $(CFLAGS) -o $(TARGET) main.c
变量名大小写敏感。CC和cc是两个不同的变量。我建议统一风格,要么全大写表示全局常量,要么全小写表示局部变量。
1st_OBJ 是错的,obj_1st 没问题。
3.2 四种赋值运算符的区别
这是很多人的困惑点。=、:=、?=、+=,到底用哪个?
我当年刚学Makefile时,就被=和:=坑过一次。那次我定义了一个变量,后面又改了它的值,结果编译出来的东西完全不对。查了半天才发现是递归展开的问题。
3.2.1 = (递归展开赋值)
=是Makefile的默认赋值方式。它的特点是:延迟展开。也就是说,变量在定义时不会立即计算值,而是在使用时才展开。
A = hello
B = $(A) world
A = hi
# 最终 B 的值是 "hi world",而不是 "hello world"
为什么会这样?因为B在定义时只是记录了$(A) world这个表达式,等到真正用到B时,才去查找A的当前值。这时候A已经被改成hi了。
A = $(A) something 会让Make陷入死循环。我曾经在调试一个大型项目时遇到过这种情况,Make直接卡死,排查了半天才发现是变量自引用。
3.2.2 := (简单赋值)
:=是立即展开赋值。变量在定义时就会计算好值,后面再怎么改依赖的变量,它都不受影响。
A = hello
B := $(A) world
A = hi
# 最终 B 的值是 "hello world",保持不变
我个人强烈推荐在大多数场景下使用:=。原因很简单:行为可预测。你看到变量定义时是什么值,用的时候就是什么值,不会因为后面代码的改动而悄悄变化。
3.2.3 ?= (条件赋值)
?=的意思是:如果变量没有被定义过,才赋值。如果已经定义了,就跳过。
CC ?= gcc
# 如果 CC 没有被定义过,就设为 gcc
# 如果 CC 已经在命令行或环境变量中定义过,就保留原值
这个在写通用Makefile时特别有用。你可以让用户通过命令行覆盖默认值:
make CC=clang
这样CC就会用clang,而不是gcc。
3.2.4 += (追加赋值)
+=用于在变量原有值后面追加内容。它会自动在中间加一个空格。
CFLAGS = -Wall
CFLAGS += -O2
CFLAGS += -g
# 最终 CFLAGS 的值是 "-Wall -O2 -g"
这个在分模块定义编译选项时非常好用。比如基础选项放在一个地方,调试选项放在另一个地方:
BASE_CFLAGS = -Wall -Wextra
DEBUG_CFLAGS = -g -O0
RELEASE_CFLAGS = -O2 -DNDEBUG
# 根据模式选择
ifeq ($(MODE),debug)
CFLAGS = $(BASE_CFLAGS) $(DEBUG_CFLAGS)
else
CFLAGS = $(BASE_CFLAGS) $(RELEASE_CFLAGS)
endif
3.3 变量的作用域
Makefile里的变量作用域,说白了就两种:全局和局部。
全局变量:在Makefile顶层定义的变量,整个文件都能用。
局部变量:在特定规则或模式规则中定义的变量,只在该规则内有效。
# 全局变量
CC = gcc
# 局部变量:只在编译 .c 文件时有效
%.o: %.c
CC = clang # 这个 CC 只在这一条规则中生效
$(CC) -c $< -o $@
还有一种是通过target指定的变量:
# 只在编译 main.o 时生效
main.o: CFLAGS += -DMAIN_MODULE
main.o: main.c
$(CC) $(CFLAGS) -c main.c -o main.o
嗯,这里要注意:目标变量只对当前目标和它的依赖生效。如果你在main.o上设置了CFLAGS,但main.o依赖header.h,那header.h的规则里也能看到这个变量。
3.4 环境变量与Makefile变量
Makefile可以读取系统的环境变量。比如PATH、HOME、USER等。
# 直接使用环境变量
INSTALL_DIR ?= $(HOME)/local
# 或者用 export 把Makefile变量导出为环境变量
export MY_VAR = hello
但有个优先级问题:
- 命令行变量(
make VAR=value)优先级最高 - Makefile中定义的变量次之
- 环境变量优先级最低
举个例子:
# 假设环境变量 CC=gcc
# Makefile中:
CC = clang
# 最终 CC 是 clang,因为Makefile变量覆盖了环境变量
# 但如果用 make CC=gcc 命令行传入
# 最终 CC 是 gcc,命令行优先级最高
CFLAGS=-march=native的环境变量,而我的Makefile用了CFLAGS ?= -O2,结果?=没有覆盖环境变量,导致编译出来的二进制在另一台机器上跑不了。从那以后,我养成了一个习惯:在Makefile开头显式重置关键变量,或者用override强制覆盖。
3.5 知识体系总览
下面这张图,是我梳理的变量与赋值核心知识结构。你可以把它当作一个快速索引:
3.6 实战建议
说了这么多,我总结几条实战中的经验:
- 默认用
:=,除非你明确需要延迟展开。这样代码更可预测,调试更容易。 - 用
?=给用户留覆盖口。比如CC ?= gcc,用户想换编译器时直接make CC=clang就行。 - 用
+=分模块组织变量。把基础选项、平台选项、调试选项分开定义,最后再合并。 - 慎用环境变量。环境变量是隐式的,容易造成"在我机器上能编译"的问题。我建议在Makefile开头用
unexport清除不需要的环境变量,或者用override强制覆盖。 - 变量命名要有意义。
CC表示编译器,CFLAGS表示编译选项,LDFLAGS表示链接选项。别用A、B、tmp这种名字,三个月后你自己都看不懂。
$(info VAR=$(VAR)) 打印出来。或者用 make -p 查看所有变量的最终值。这两个方法帮我解决过不少疑难杂症。
好了,变量与赋值就聊到这里。记住一句话:变量用得好,Makefile写得少。下一节我们会聊条件判断和函数,到时候这些变量知识都会用上。
可可精品资料库