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的时候,多想想能不能用变量替代重复的文本,能不能用自动变量简化规则。

核心要点回顾:

  • 自定义变量用 := 赋值,避免递归展开的坑
  • $@ 是目标,$< 是第一个依赖,$^ 是所有依赖
  • 模式规则 + 自动变量 = 通用编译规则
  • 函数和条件判断让变量更灵活
Makefile变量体系结构图 Makefile变量 自定义变量 CC = g++ CFLAGS := -Wall -O2 LIBS += -lQt5Core 自动变量 $@ → 目标文件名 $^ → 所有依赖文件 $< → 第一个依赖文件 $? → 更新的依赖文件 特殊变量与函数 MAKE、MAKEFLAGS wildcard、filter函数 ifeq条件判断 三者结合 → 高效、可维护的Makefile

可可精品资料库