一个基本的Makefile
1 | hello: |
使用make执行这个Makefile 结果是
1 | echo "hello world!" |
创建一个hello文件后再make结果是
1 | make: 'hello' is up to date. |
这是 make
的经典行为:
只要目标文件(
hello
)已经存在,并且所有依赖为最新,make
就不会重复执行。
但现在 hello
目标没有任何依赖,所以:
make
只看有没有hello
这个文件- 如果已经存在这个文件,并且没有任何依赖需要更新它
- 就会判断它是 up to date(已经是最新的)
Makefile语法
一个Makefile由一系列rule组成,一个rule的基本结构为:
1 | targets: prerequisites |
- 目标(targets)是文件名,使用空格分隔。通常每条规则只包含一个目标。
- 命令(commands)是一系列步骤,通常用于生成目标文件。每条命令前必须以一个制表符(Tab 字符)开头,而不是空格。
- 前置条件(prerequisites)也是文件名,使用空格分隔。在执行生成目标的命令之前,这些文件必须已经存在。前置条件也被称为依赖(dependencies)。
Make的本质
创建一个新的Makefile
1 | blah: |
在第一次make时结果为
1 | cc blah.c -o blah |
第二次make的结果为
1 | make: 'blah' is up to date. |
然而在我们修改blah.c文件后再make时,结果依然是第二次make时的结果。似乎与我们想要的结果不一样。
要解决这个问题,我们只需要将Makefile修改为
1 | blah: blah.c |
这样,当blah.c发生改变时,blah也会重新生成。
当我们再次运行 make
时,会发生以下一系列步骤:
首先会选择第一个目标,因为第一个目标是默认目标;
这个目标有一个先决条件(prerequisite),比如
blah.c
;make
接下来会判断是否需要执行blah
目标。它只会在以下两种情况下执行:blah
这个文件不存在;或者
blah.c
的修改时间比blah
更新。
最后这一步非常关键,它是 make
的核心。make
试图判断自从上次编译 blah
以来,blah
的先决条件(这里是 blah.c
)是否发生了变化。也就是说,如果你修改了 blah.c
,那么运行 make
时应该重新编译这个文件。反过来,如果 blah.c
没有变,就不应该重新编译。
为了实现这一点,make
利用文件系统的时间戳作为是否变更的判断依据。这个方法在大多数情况下是合理的,因为文件的时间戳通常只会在文件被修改时才更新。
但重要的是你要明白:这个方法并不总是准确的。比如说,你可以手动修改一个文件,然后把它的修改时间改成一个更早的时间。如果你这么做了,make
会错误地认为这个文件没有变化,于是就不会重新编译。
Make clean
clean
通常用作一个目标,用于删除其他目标的输出,但它在 Make 中并不是一个特殊的词。你可以通过运行 make
和 make clean
来创建和删除 some_file
。
需要注意的是,clean
在这里做了两件新事情:
- 它是一个不是默认的第一个目标,也不是一个先决条件的目标。这意味着除非你显式地运行
make clean
,否则它不会执行。 - 它并不是一个文件名。如果恰好存在一个名为
clean
的文件,那么这个目标就不会执行,这是我们不希望的。
变量
变量只能是字符串,值通常为文件名。有 := 和 = 赋值。
✅ =
(延迟展开/递归赋值)
1 | FOO = $(BAR) |
- 当
FOO
被使用时,才会去解析$(BAR)
。 - 所以
FOO
的值是hello
。 - 如果你在后面修改了
BAR
,FOO
的值也会随之改变。
✔适用于:需要在使用时动态计算值的场景。
✅ :=
(立即展开/简单赋值)
1 | FOO := $(BAR) |
- 当这一行被解析时,
$(BAR)
立刻被展开(即“立即求值”)。 - 此时
BAR
还没有被赋值,所以FOO
是空的。
✔适用于:只想计算一次,或者确保变量在赋值时就固定不变的情况。
建议使用和${}
和$()
来引用变量。
targets
当规则有多个目标时,将针对每个目标运行命令。$@是一个包含目标名称的自动变量。
1 | all: f1.o f2.o |
通配符
在 Makefile 中,*
是一种通配符(wildcard),它和 shell 中的意思类似,用于匹配任意长度的字符串,但它的行为取决于使用的上下文。下面来具体讲讲它的几种用法:
✅ 1. 在规则的依赖或目标中使用 *
这时候的 *
是由 Make 自动解释 的,可以与模式规则(Pattern Rule)一起使用:
例子:
1 | # 规则:把所有 .c 文件编译成 .o 文件 |
%.o
和%.c
是模式匹配,%
相当于一个通用“变量”,匹配文件名中的任意部分。- 例如:当
main.c
存在时,%.c -> main.c
,就会自动生成对应的main.o
✅ 2. 在变量中使用 *
通配符(通常结合 wildcard
函数)
此时 *
不会自动展开,需要用 $(wildcard ...)
函数显式展开:
1 | SRC := $(wildcard src/*.c) |
src/*.c
中的*
匹配src/
目录下所有以.c
结尾的文件。
✅ 3. 在命令中使用 *
(shell通配符)
如果你在命令里直接写 *
,Make 会调用 shell 解释它,就像你在终端里输入的一样。
1 | clean: |
- 这里的
*
是由 shell 解释的,删除所有.o
文件。
🔍 总结:Makefile 中的 *
用法一览
场景 | 是否需要函数 | 作用 |
---|---|---|
模式规则(如 %.o: %.c ) |
❌ | 匹配任意文件名片段 |
变量中使用(如 SRC := *.c ) |
✅ 需要 wildcard |
获取实际匹配的文件名 |
shell 命令中(如 rm *.o ) |
❌ | 由 shell 展开 |
以下是一个例子:
1 | 打印所有的.c文件 |
这是个很关键的问题,Makefile 中通配符未匹配到任何文件时的行为确实值得细讲。下面我分几种情况来给你讲清楚:
🔍 1. 直接写通配符(未展开)时:
示例:
1 | target: *.o |
如果当前目录下没有任何 .o
文件,**\*.o
不会被展开为空,而是保持字面字符串 "\*.o"
**。
❗结果:
- Make 会把
"*.o"
当成一个 真实的文件名(叫*.o
)。 - 如果没有叫
*.o
的文件存在,Make 会认为这个依赖是 缺失的,然后:- 报错:
No rule to make target '*.o', needed by 'target'.
- 或者:尝试按默认规则找如何生成
*.o
,但几乎不会成功。
- 报错:
🔍 2. 使用 $(wildcard \*.o)
时:
示例:
1 | FILES := $(wildcard *.o) |
如果没有 .o
文件,$(wildcard *.o)
会被展开为 空字符串。
✅结果:
- 依赖变成
target:
,即没有依赖 - Make 会继续执行
target
的命令(如果你有写命令) - 不会报错,也不会尝试构建
*.o
这个文件
✅ 推荐写法:始终用 $(wildcard ...)
包裹通配符
1 | OBJS := $(wildcard *.o) |
这样即使没有 .o
文件,也不会出错,OBJS
就是空,target
的命令仍会正常执行(取决于是否需要依赖)。
⚠️ 顺便提醒一个坑:用 wildcard
后别滥用 ?
如果你写了:
1 | target: $(wildcard *.o) |
此时 $?
是空的,不会报错,但什么都不会输出,也不会编译任何文件。
🔚 总结
情况 | 没有匹配时的行为 |
---|---|
*.o 直接用 |
被当成一个叫 *.o 的文件,找不到就报错 |
$(wildcard *.o) |
展开为空字符串,依赖为空,不会报错 ✅ |
你这段关于 %
的描述相当准确!它说出了 %
在 Makefile 中的两种核心用法:匹配(matching) 和 替换(replacing)。下面我来帮你具体拆解一下这两个模式的用法和差异,并配合实际例子说明 %
是怎么工作的。
🧠 %
的本质是什么?
在 Make 中,%
是一个通配符,但它不是文件系统通配符,而是 Makefile 专属的模式匹配符号,用于规则或函数中。
✅ 用法一:匹配模式(Matching) — 找出“stem”
📌 使用场景:规则定义中的目标依赖关系
1 | %.o: %.c |
🧩 含义:
%
匹配任意字符(至少一个),称为 stem(词干)- 比如
main.o
和main.c
,stem = main
- Make 会自动将
%.o
和%.c
配对,构建规则
✅ 自动变量回顾:
变量 | 含义 |
---|---|
$@ |
目标,比如 main.o |
$< |
第一个依赖,比如 main.c |
$* |
stem,即 main |
✅ 用法二:替换模式(Replacing) — 使用 stem 替换路径
📌 使用场景:函数中进行字符串变换
1 | SRCS := main.c foo.c bar.c |
🧩 含义:
%.c=%.o
表示把所有.c
文件的stem
替换成.o
main.c
→main.o
foo.c
→foo.o
这是 %
在 :=
或 $(...)
变量表达式中非常强大的替换用法。
💡 进阶:模式规则和替换配合使用
你可以组合使用 %
来实现自动编译流程:
1 | SRCS := $(wildcard *.c) |
- 利用
wildcard
获取.c
文件 - 利用
%.c=%.o
替换生成.o
文件列表 - 利用
%.o: %.c
写模式规则自动构建
这个模板非常常用,在实际项目中非常好使。
✅ 总结表格:%
的两种用法对比
用法 | 场景 | 示例 | 说明 |
---|---|---|---|
匹配 | 规则模式匹配 | %.o: %.c |
把 main.c 编成 main.o ,stem = main |
替换 | 字符串变换 | $(SRCS:%.c=%.o) |
把 .c 文件换成 .o |
自动变量
1 | hey: one two |
1 | target: dep1 dep2 |
有趣的规则
隐式规则
1 | CC = gcc # Flag for implicit rules |
变量 | 含义 | 示例值(可设定) |
---|---|---|
CC |
编译 C 的命令 | 默认是 cc ,常设为 gcc |
CXX |
编译 C++ 的命令 | 默认是 g++ |
CFLAGS |
给 C 编译器的额外参数 | -Wall -O2 |
CXXFLAGS |
给 C++ 编译器的额外参数 | -std=c++17 -O2 |
CPPFLAGS |
给预处理器的参数 | -Iinclude -DMY_DEFINE |
LDFLAGS |
链接器参数 | -Llib/ |
LDLIBS |
链接时要用到的库 | -lm -lpthread |
目标文件 | 来源 | 默认命令(隐式规则) |
---|---|---|
n.o |
n.c |
$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@ |
n.o |
n.cpp / n.cc |
$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@ |
n (可执行文件) |
n.o |
$(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@ |
静态模式规则
📜 静态模式规则的语法
1 | targets...: target-pattern: prereq-patterns ... |
语法解释:
- **targets…**:多个目标文件
- target-pattern:目标的模式(通常是文件名模式)
- prereq-patterns:依赖的模式(也可以是文件名模式)
- commands:执行的命令
1 | objects = foo.o bar.o all.o |