
自动化构建工具:make 与 Makefile
1. 什么是 make / Makefile?
make 是一个自动化构建工具,它根据一个叫做 Makefile 的文件中的规则,自动编译和链接程序。更本质一点说,make是一个命令,makefile是一个文件 。
Makefile 定义了文件之间的依赖关系以及如何生成目标文件。
注意:
Makefile文件名首字母可以大写也可以小写,但通常使用Makefile或makefile。
2. Makefile 的基本结构
依赖关系与依赖方法
一个典型的规则如下:
makefile
target: dependencies
recipe
- 第一行 :
target是要生成的目标文件,dependencies是生成该目标所依赖的文件。 - 第二行 :
recipe是生成目标的命令(必须以 Tab 键开头)。
示例:
makefile
myproc:myproc.c
gcc -o myproc myproc.c

myproc是目标文件,依赖myproc.c。- 依赖方法:
gcc -o myproc myproc.c。
make 的执行规则
make命令默认扫描当前目录下的Makefile,并执行 第一个 目标。- 如果依赖文件的时间戳比目标文件新(即内容被修改过),则重新生成目标;否则跳过。
- 这样设计可以避免重复编译未修改的代码,提高构建效率。
3. 如何判断文件是否被修改?
之前说过,文件=内容+属性
文件的三个时间戳(stat 命令)

在 Linux 中,每个文件都有三个时间戳:
| 时间戳 | 含义 |
|---|---|
| Access | 最近一次访问时间(读文件) |
| Modify | 最近一次内容修改时间 |
| Change | 最近一次属性修改时间 |
注意:
- 修改文件内容时,Modify 和 Change 都会更新(因为文件大小等属性变了)。
- 单独修改属性(如
chmod)只会更新 Change。- Access 时间不会每次都更新(为了减少磁盘 I/O),通常访问多次后才刷新一次。
make 的判断依据
make 只根据 Modify 时间来判断文件内容是否发生了更改 。
如果依赖文件的 Modify 时间晚于目标文件,则重新生成。
使用 touch 修改时间
touch 命令除了可以创建空文件,还可以将所有时间戳更新为当前时间:
bash
touch filename # 将 filename 的 Access、Modify、Change 都改为当前时间

利用这个特性,可以强制让 make 认为文件被修改过,从而总是执行某个目标。
touch可以统一更改时间到当前时刻
为什么讲这些呢?
因为这些是关键字PHONY的原理
PHONY可以使程序可以总是被执行,下图中的clean就是如此,原理就是通过touch修改时间
所以我们使用make clean的时候可以总是被执行

如果改成下图这样,make也可以总是被执行,但为了节省编译时间,通常不会这样做

4. 伪目标(.PHONY)
为什么需要伪目标?
有时候我们想执行一些不生成具体文件的操作(例如 clean 删除中间文件)。但 clean 本身不是一个真实的文件,如果恰好当前目录下有一个名为 clean 的文件,make clean 就会认为目标已经存在且无依赖,从而跳过执行。
使用 .PHONY 声明伪目标
makefile
.PHONY: clean
clean:
rm -f *.o hello
.PHONY告诉 make:clean不是一个真实文件,无论是否存在,都执行它的命令。- 伪目标的原理本质上是 make 强制把它的依赖时间视为"总是比目标新",从而总是执行。
能否让普通目标也总是执行?
可以,但不推荐。例如:
makefile
hello: hello.c
gcc hello.c -o hello
touch hello # 更新 hello 的时间戳,这样下次 make 时会认为 hello 比依赖新,从而不编译
这样会导致每次 make 都重新编译,失去了增量编译的优势。因此,一般只对 clean 等辅助目标使用 .PHONY。
5. Makefile 的推导规则(自动推导)
拆分写法(不常用,仅用于理解)
上述的命令可以拆分成下图(一般不这样写,为了讲解原理而拆分)
makefile会从上向下扫描,没有就入栈,有就出栈执行

在实际开发中,我们一般这样写

其中(BIN) 和 (SRC)替换成(SRC) 替换成(SRC)替换成@和^
$@表示目标文件。$^表示所有依赖文件。

如果不想回显命令,可以在前边加上@

如果想看起来直观一点,可以在命令后输出一行,指定内容
make 会从上向下扫描,发现需要 hello.o,则先去执行生成 hello.o 的规则,再返回执行 hello 的规则。
简化写法(常用)
实际开发中,我们通常会使用变量和自动化变量来简化:
makefile
BIN = hello
SRC = hello.c
$(BIN): $(SRC)
gcc -o $@ $^
隐藏命令回显
在命令前加 @ 可以阻止 make 输出该命令本身:
makefile
$(BIN): $(SRC)
@gcc $^ -o $@
@echo "编译完成"
处理多个源文件

%.o: %.c是一个模式规则,表示所有.o文件都依赖对应的.c文件。$<表示第一个依赖文件(即对应的.c文件)。
6. Makefile 的终极形态(通用模板)
说明:
wildcard函数:展开当前目录下所有.c文件。SRC:.c=.o是变量替换语法,将所有.c后缀替换为.o。- 这个模板可以自动适应任意数量的
.c文件,无需手动列出。

makefile
SRC=$(shell ls *.c)
SRC=$(wildcard *.c) #两种方式都可以 显示当前目录下的所有.c文件
OBJ=$(SRC:.c=.o) #把所有SRC内部的.c文件替换成.o文件
总结
| 概念 | 说明 |
|---|---|
| 依赖关系 | 目标文件依赖哪些源文件或中间文件 |
| 依赖方法 | 生成目标的具体命令,前面必须有一个 Tab 键 |
| 时间戳 | make 根据文件的 Modify 时间判断是否需要重新编译 |
| 伪目标 | 用 .PHONY 声明,总是执行其命令(如 clean) |
| 自动化变量 | $@(目标)、$^(所有依赖)、$<(第一个依赖) |
| 模式规则 | %.o: %.c 表示所有 .o 依赖同名 .c |
| 函数 | wildcard、shell 等用于动态获取文件列表 |
掌握这些内容,你就可以编写灵活、高效的 Makefile,实现项目的自动化构建。
新编译 |
| 伪目标 | 用 .PHONY 声明,总是执行其命令(如 clean) |
| 自动化变量 | $@(目标)、$^(所有依赖)、$<(第一个依赖) |
| 模式规则 | %.o: %.c 表示所有 .o 依赖同名 .c |
| 函数 | wildcard、shell 等用于动态获取文件列表 |
掌握这些内容,你就可以编写灵活、高效的 Makefile,实现项目的自动化构建。