目录:
- [1. make/Makefile的使用](#1. make/Makefile的使用)
- [2. make 与 Makefile 的关系](#2. make 与 Makefile 的关系)
- [2.1 角色分工](#2.1 角色分工)
- [2.2 工作流程](#2.2 工作流程)
- [3. Makefile 的核心](#3. Makefile 的核心)
- [4. make 的工作](#4. make 的工作)
- [5. 伪目标 .PHONY](#5. 伪目标 .PHONY)
- [6. 变量](#6. 变量)
- [6.1. 链接规则](#6.1. 链接规则)
- [6.2. 模式匹配规则,编译.c 生成.o](#6.2. 模式匹配规则,编译.c 生成.o)
- [7. 函数](#7. 函数)
- [8. 使用示例](#8. 使用示例)
1. make/Makefile的使用
在写 C/C++ 项目时,如果每次改一个文件都要手敲:
gcc -c main.c -o main.o
gcc -c utils.c -o utils.o
gcc main.o utils.o -o app
文件一多,命令又长又重复,还容易漏掉某个文件、忘记重新编译。更麻烦的是:改了一个 .c 文件,其实只需要重新编译它,再链接一次就行,但手敲时往往全量重编,浪费时间。
make 就是解决这个问题的工具:它是一个自动化构建工具 ,读一个叫 Makefile 的脚本,按照里面写的规则,自动判断哪些文件需要重新编译,然后执行对应命令。
2. make 与 Makefile 的关系
一句话概括:
make 是可执行程序(命令工具),Makefile 是给 make 读取的配置脚本文件。make 读 Makefile,然后根据里面描述的规则,自动执行编译、构建等工作。
2.1 角色分工
- make :程序,Linux 自带的构建工具。 它本身不知道怎么编译代码,只会去读取规则文件,解析规则,对比文件时间戳,执行 shell 命令 。 你在终端敲的
make,就是运行这个程序。 - Makefile :文本文件(文件名固定,首字母大写,也可以小写 makefile)。 里面写好:目标文件、依赖关系、要执行的命令。 它是给 make 看的 "说明书" ,没有 Makefile,直接敲 make 会报错:
No makefile found。
| 名称 | 说明 |
|---|---|
make |
可执行程序,构建工具本身 |
Makefile / makefile |
描述构建规则的文本文件,make 读取它 |
| 规则(rule) | Makefile 的基本单位,一条规则 = 目标 + 依赖 + 命令 |
2.2 工作流程
终端输入 make
↓
make程序 寻找当前目录 Makefile / makefile
↓
make 解析Makefile,读取里面的规则(目标:依赖 + 命令)
↓
比较【目标文件】和【依赖文件】的时间戳
- 如果依赖更新(修改过源码):执行命令,重新生成目标
- 如果目标最新:跳过,不执行(增量构建,只编译改动文件)
执行时直接在项目目录敲
make
make 会自动在当前目录找 Makefile 或 makefile(也可以 make -f 自定义文件名)。
3. Makefile 的核心
Makefile 最基本的结构:
目标(target): 依赖(prerequisites)
命令(recipe)
关键点:命令前面必须是一个 Tab,不能用空格!
一个最简单的例子:
app: main.o utils.o
gcc main.o utils.o -o app
main.o: main.c
gcc -c main.c -o main.o
utils.o: utils.c
gcc -c utils.c -o utils.o
含义:
-
app依赖main.o、utils.o,用gcc链接出app -
main.o依赖main.c,编译出main.o -
utils.o依赖utils.c,编译出utils.o

4. make 的工作
make 的核心逻辑其实就两条:
-
依赖关系:目标依赖哪些文件?要生成目标,先保证依赖都就绪。
-
时间戳判断 :如果目标的修改时间比任一依赖旧,说明依赖更新了,目标过期,需要重新执行命令;否则跳过。
举例:app 依赖 main.o、utils.o。
-
第一次
make:app不存在 → 先递归生成main.o、utils.o→ 再链接app。 -
只改了
utils.c后make:utils.c比utils.o新 → 重新编译utils.o;main.o没变 → 不重编;utils.o比app新 → 重新链接app。
这就是 增量构建:只做必要的重编译,效率高。
小实验:连续敲两次
make,第二次会提示make: 'app' is up to date.,因为目标比依赖新。

改 utils.c 再 make:

屏幕只执行两条命令:

5. 伪目标 .PHONY
有些"目标"并不是真的文件,比如 clean、all、run。如果目录里恰好有个叫 clean 的文件,make 会以为目标已存在而不执行命令
所以要用 .PHONY 声
.PHONY: clean all
all: app
clean:
rm -f *.o app
6. 变量
手写一堆 gcc、文件名容易重复,用变量维护
# 通常放在最顶部
TARGET = app # 最终生成的可执行程序名
CC = gcc # 编译器
CFLAGS = -Wall -g # 编译参数:开启警告 + 加入调试信息
SRCS = main.c foo.c # 源码文件列表
OBJS = $(SRCS:.c=.o)# 后缀替换,把 .c 全部换成 .o → main.o foo.o
示例:
CC = gcc
CFLAGS = -Wall -g
TARGET = app
OBJS = main.o utils.o
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -f $(OBJS) $(TARGET)
6.1. 链接规则
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET)
-
$(TARGET):读取变量,代表最终可执行文件(比如app) -
$(OBJS):所有的目标文件(main.o foo.o) 含义:可执行文件TARGET依赖所有.o文件 。 当任意.o文件更新,执行下面命令:gcc main.o foo.o -o app
作用:链接阶段 ,把多个.o目标文件合并,生成最终程序。
6.2.模式匹配规则,编译.c 生成.o
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
%:Makefile 的通配符,匹配任意文件名%.o: %.c:代表:任何 xxx.o 文件,依赖同名的 xxx.c$(CC):编译器 gcc$(CFLAGS):编译选项(警告、调试选项)-c:只编译,不链接 ,生成.o目标文件$<:自动变量 ,代表规则里第一个依赖文件 (也就是xxx.c)$@:自动变量 ,代表当前规则的目标文件 (也就是xxx.o)
示例:当需要main.o,make 会自动匹配这条规则,执行:
gcc -Wall -g -c main.c -o main.o
6.3 伪目标 clean,清理编译产物
.PHONY: clean
clean:
rm -f $(OBJS) $(TARGET)
.PHONY: clean 声明 clean 是伪目标:它不是磁盘上的文件,只是一个动作。
如果当前目录恰好存在一个名叫
clean的文件,不加.PHONY时,make clean会判断:文件已经存在且最新,不会执行删除命令。加上就可以避免这个 bug。
clean: 目标 执行 make clean 触发下面 shell 命令:
rm -f main.o foo.o app
rm:删除命令-f:强制删除,文件不存在时不会报错 作用:删除所有.o中间文件和可执行程序,清空编译产物。
变量总结:
| 符号 | 含义 |
|---|---|
$(TARGET) |
最终可执行程序(自定义变量) |
$(OBJS) |
所有.o 目标文件(自定义变量) |
$(CC) |
编译器(一般 gcc) |
$(CFLAGS) |
编译选项 |
% |
通配符,匹配任意文件名 |
$< |
第一个依赖文件 |
$@ |
当前规则的目标文件 |
7. 函数
Makefile 自带一些函数,最常用的几个:
SRCS = $(wildcard *.c) # 找出所有 .c 文件
OBJS = $(SRCS:.c=.o) # 把 .c 后缀替换成 .o
-
$(wildcard *.c):通配符展开 -
$(SRCS:.c=.o):后缀替换
这样新加 .c 文件不用改 Makefile:
CC = gcc
CFLAGS = -Wall -g
TARGET = app
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
$(TARGET): $(OBJS)
$(CC) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -f $(OBJS) $(TARGET)
8. 使用示例
BIN=xin.exe
#SRC=$(shell ls *.c)
SRC=$(wildcard *.c) # wildcard函数,获取当前目录下的所有的原文件
OBJ=$(SRC:.c=.o)
$(BIN):$(OBJ)
@gcc -o $@ $^
@echo "linking $^ to $@ ... done"
%.o:%.c
@gcc -c $<
@echo "compiling $< to $@ ... done"
.PHONY:clean
clean:
.PHONY:test
test:
@echo "Debug-------"
@echo $(SRC);
@echo "Debug-------"
@echo $(OBJ);
@echo "Debug-------"



