3、变量与自动变量:让Makefile活起来的核心
说实话,我刚开始学Makefile的时候,最头疼的就是变量。那时候我写Makefile,每个目标文件都手动写一遍编译命令,一个项目下来能写上百行重复代码。后来我才明白——变量才是Makefile的灵魂。
今天咱们就来聊聊Makefile里的变量,特别是那些神奇的自动变量。这些东西用好了,你的Makefile能精简到原来的十分之一。
3.1 自定义变量:你的专属快捷键
先说说自定义变量。说白了,就是给一段文本起个名字,后面直接用名字代替。
# 定义变量
CC = g++
CFLAGS = -Wall -O2 -std=c++17
LIBS = -lQt5Core -lQt5Gui -lQt5Widgets
# 使用变量
main.o: main.cpp
$(CC) $(CFLAGS) -c main.cpp -o main.o $(LIBS)
我个人习惯把变量定义放在文件开头,这样一目了然。改编译器版本?改CFLAGS?只需要改一个地方。
变量定义规则:
- 变量名区分大小写,CC和cc是两个不同的变量
- 赋值用等号,两边可以有空格
- 引用变量用$(变量名)或${变量名}
- 变量值可以包含其他变量引用
3.2 变量赋值方式:不只是等号那么简单
这里有个坑,我当年踩过。Makefile里有好几种赋值方式,用错了会出大问题。
| 赋值方式 | 语法 | 特点 |
|---|---|---|
| 递归展开 | = | 使用时才展开,可以引用后面定义的变量 |
| 简单展开 | := | 定义时立即展开,效率高,推荐使用 |
| 条件赋值 | ?= | 只有变量未定义时才赋值 |
| 追加赋值 | += | 在原有值后面追加内容 |
举个例子你就明白了:
# 递归展开
A = hello
B = $(A) world
A = hi
# B的值是 "hi world"(因为使用时A已经是hi了)
# 简单展开
A := hello
B := $(A) world
A := hi
# B的值是 "hello world"(定义时A是hello,之后改A不影响B)
我在项目中遇到过一个问题:用等号定义变量,结果变量展开时引用了还没定义的变量,导致编译参数全乱了。从那以后,我基本只用 := 和 +=。
3.3 自动变量:Makefile的魔法符号
自动变量才是真正的神器。你想想看,每次写编译命令都要重复写目标文件名、依赖文件名,多烦人。自动变量就是来解决这个问题的。
常用的自动变量就这几个:
| 自动变量 | 含义 | 示例值 |
|---|---|---|
| $@ | 目标文件名 | main.o |
| $^ | 所有依赖文件,去重 | main.cpp utils.h |
| $< | 第一个依赖文件 | main.cpp |
| $? | 所有比目标新的依赖文件 | main.cpp |
| $* | 目标文件名去掉后缀 | main |
看个实际例子:
# 不用自动变量
main.o: main.cpp utils.h
g++ -c main.cpp -o main.o
# 用了自动变量
main.o: main.cpp utils.h
g++ -c $< -o $@
是不是清爽多了?$< 自动取第一个依赖(main.cpp),$@ 自动取目标(main.o)。
我的经验:写通用规则时,自动变量是必须的。比如模式规则里,你根本不知道具体文件名是什么,只能用自动变量。
3.4 模式规则 + 自动变量 = 终极武器
模式规则和自动变量搭配,效果翻倍。来看一个完整的Qt项目例子:
# 变量定义
CXX := g++
CXXFLAGS := -Wall -O2 -std=c++17 -fPIC
QT_LIBS := -lQt5Core -lQt5Gui -lQt5Widgets
MOC := moc
# 源文件
SOURCES := main.cpp widget.cpp dialog.cpp
OBJECTS := $(SOURCES:.cpp=.o)
# 模式规则:编译.cpp到.o
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
# 模式规则:生成moc文件
%.moc: %.h
$(MOC) $< -o $@
# 链接目标
app: $(OBJECTS)
$(CXX) $^ -o $@ $(QT_LIBS)
# 清理
clean:
rm -f *.o *.moc app
你看,这里用了 $< 取源文件,$@ 取目标文件,$^ 取所有.o文件。不管你有多少个源文件,规则就写一次。
3.5 特殊变量:Makefile的隐藏开关
除了自定义变量和自动变量,Makefile还有一些内置的特殊变量。这些我平时用得不多,但关键时刻能救命。
# MAKEFLAGS:传递给子make的参数
# MAKE:当前make命令的路径
# SHELL:使用的shell,默认/bin/sh
# .VARIABLES:所有已定义的变量列表
# 递归调用make时很有用
subsystem:
cd subdir && $(MAKE) $(MAKEFLAGS)
注意:MAKEFLAGS会传递所有命令行参数,包括 -j(并行数)。如果你在子目录里不想用并行编译,需要手动处理。
3.6 变量操作函数:让变量更灵活
Makefile还提供了一些函数来操作变量。我常用的有这几个:
# 替换后缀
OBJS := $(SRCS:.cpp=.o)
# 取目录名
DIR := $(dir src/main.cpp) # 结果是 "src/"
# 取文件名
FILE := $(notdir src/main.cpp) # 结果是 "main.cpp"
# 过滤文件
CPP_FILES := $(filter %.cpp, $(ALL_FILES))
# 条件判断
ifeq ($(DEBUG), 1)
CXXFLAGS += -g -DDEBUG
endif
我曾经在一个大型Qt项目里,用这些函数自动扫描源文件目录,生成编译规则。几百个源文件,Makefile只写了30行。
3.7 实战:一个完整的Qt库链接示例
最后,咱们把今天学的知识串起来,写一个完整的Qt项目Makefile:
# Qt库链接实战
CXX := g++
CXXFLAGS := -Wall -O2 -std=c++17 -fPIC
INCLUDES := -I/usr/include/qt5 -I/usr/include/qt5/QtWidgets
QT_LIBS := -lQt5Core -lQt5Gui -lQt5Widgets
LDFLAGS := -L/usr/lib/x86_64-linux-gnu
# 自动获取源文件
SRCDIR := src
SOURCES := $(wildcard $(SRCDIR)/*.cpp)
OBJECTS := $(SOURCES:.cpp=.o)
TARGET := myapp
# 默认目标
all: $(TARGET)
# 编译规则
$(SRCDIR)/%.o: $(SRCDIR)/%.cpp
$(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -o $@
# 链接规则
$(TARGET): $(OBJECTS)
$(CXX) $^ -o $@ $(LDFLAGS) $(QT_LIBS)
# 清理
clean:
rm -f $(OBJECTS) $(TARGET)
# 伪目标
.PHONY: all clean
这个Makefile里,我们用到了:
- 自定义变量:CXX、CXXFLAGS、INCLUDES等
- 自动变量:$<、$@、$^
- 函数:wildcard、替换后缀
- 模式规则:%.o: %.cpp
嗯,到这里你应该对Makefile的变量有了比较全面的了解。记住一句话:变量用得好,Makefile写得少。下次写Makefile的时候,多想想能不能用变量替代重复的文本,能不能用自动变量简化规则。
核心要点回顾:
- 自定义变量用 := 赋值,避免递归展开的坑
- $@ 是目标,$< 是第一个依赖,$^ 是所有依赖
- 模式规则 + 自动变量 = 通用编译规则
- 函数和条件判断让变量更灵活
可可精品资料库