Makefile 基础与多目录工程
会写单文件/多文件/多目录 Makefile,理解增量编译与自动变量
- 来源
- 补充
- 待确认
标记说明:【来源】来自上传资料 · 【补充】课程新编 · 【纠错】按勘误表修正 · 【更新】过时内容已现代化 · 【待确认】无法可靠还原
完成标准(本章)
- 📖 已阅读:滚动 ≥ 80% 且有效阅读 ≥ 300 秒
- ✏️ 已练习:小练习正确率 ≥ 60%
- 📝 已通过测验:分数 ≥ 60 分
- 🛠️ 已掌握还需完成实践任务
1.1.4 Makefile 基础与多目录工程
本章来源:规则三要素、增量编译、变量与自动变量、多目录概念来自《第一阶段讲义》4.x【来源】;原资料 Makefile 示例全部以截图嵌入、无法还原【待确认】——本章示例代码全部重写并本机实测(新编比例约 40%)【补充】;通俗解释与练习为新编【补充】。 平台适用性:universal(本机用 mingw32-make 实测——MinGW 无 make 命令,已实测确认;Linux 用 make;clean 的 del/rm 差异已在示例中处理,见迁移训练)。
① 学习目标
- 写出 Makefile 规则三要素(目标/依赖/命令),并说明命令必须以 TAB 开头的原因;
- 说明增量编译的时间戳原理,并能复现"第二遍 Nothing to be done";
- 正确使用变量与自动变量(
$@/$^/$<)改写链接规则; - 写出带 clean 与 .PHONY 的完整 Makefile,并处理 Windows del / Linux rm 的平台差异;
- 独立完成多目录工程:顶层 Makefile 用 $(MAKE) -C 嵌套子目录构建(实践任务)。
② 前置知识
- 必修:1.1.1(编译四阶段与 gcc 常用选项——Makefile 本质是把 gcc 命令自动化);
- 建议:1.1.3(多文件编译与链接——本章把"编译 add.o → 打包 → 链接"的链自动化为一条 make)。
③ 核心概念【来源】
| 概念 | 说明 |
|---|---|
| 规则三要素 | 目标: 依赖 换行后跟命令;命令行必须以 TAB 开头(空格报 missing separator) |
| 增量编译 | make 比较目标与依赖的时间戳:目标比所有依赖新就跳过重建(本机实测 Nothing to be done) |
| 变量 | CC = gcc、CFLAGS = ...,用 $(CC) 引用;一次定义多处复用 |
| 自动变量 | $@ 目标、$^ 所有依赖、$< 第一个依赖;链接行常用 $(CC) $^ -o $@ |
| wildcard / patsubst | 自动收集源文件并替换后缀(SRCS = $(wildcard src/*.c) → $(patsubst %.c,%.o,...)),文件多时避免手工列举 |
| .PHONY | 声明伪目标(clean 等不产文件的目标),避免与同名文件冲突、保证每次都执行 |
| 多目录嵌套 | 顶层 $(MAKE) -C src 进入子目录递归执行;用 $(MAKE) 而非 make 可传递 -j/-n 状态 |
| make -j | -j4 并行执行互不依赖的规则,缩短构建时间(依赖关系要写对才安全) |
④ 通俗解释【补充】
- 增量编译像厨师检查食材:每道菜(目标)都有食材清单(依赖)。厨师只会在"食材比菜新"(食材改过了)时才重新做这道菜;食材没动过,就直接端昨天的菜(Nothing to be done)。
- TAB 是 Makefile 的"合同格式":目标行说"这道菜要什么",TAB 开头的行才是"怎么做"。合同格式错一个字(空格代替 TAB),厨师直接罢工(missing separator)。
- 多目录像总店与分店:总店 Makefile 只喊"去 src 分店做菜"($(MAKE) -C src),具体菜谱(规则)都在分店手里;分店做完回话(Entering/Leaving directory 就是进出分店的记录)。
⑤ 示例代码
代码示例与验证记录
- examples/ex1-simple/Makefile单目录多文件 Makefile(TAB/变量/自动变量/clean/.PHONY/增量编译)✓ 已实测(mingw32-make 4.4.1 (GNU Make) + gcc 15.2.0 / Windows 11, 2026-08-15)编译:
mingw32-make clean all适用环境:LinuxWindows(MinGW)展开预期输出(实测)
第一遍 mingw32-make clean all(真实输出): del /Q main.o util.o ex1.exe ← 首次 clean 目录为空,"Could Not Find" 由命令前 - 前缀忽略 gcc -c main.c -o main.o -std=c11 -Wall -Wextra -Wpedantic gcc -c util.c -o util.o -std=c11 -Wall -Wextra -Wpedantic gcc main.o util.o -o ex1.exe -std=c11 -Wall -Wextra -Wpedantic 运行 ex1:max2(3, 7) = 7 第二遍 mingw32-make all(增量编译生效): mingw32-make: Nothing to be done for 'all'.
差异说明:本机 mingw32-make 4.4.1 (GNU Make) 真实执行:第一遍全量构建零警告;第二遍命中时间戳判断不重编;Linux 用 make 命令、产物无 .exe 后缀
完整源码见 /code 代码示例页
- examples/ex2-multidir/Makefile多目录工程(顶层 Makefile 嵌套 make -C src + include/ 头文件)✓ 已实测(mingw32-make 4.4.1 (GNU Make) + gcc 15.2.0 / Windows 11, 2026-08-15)编译:
mingw32-make clean all适用环境:LinuxWindows(MinGW)展开预期输出(实测)
第一遍 mingw32-make clean all(真实输出,目录路径按本机省略): mingw32-make.exe -C src clean mingw32-make[1]: Entering directory '.../src' del /Q main.o util.o ex2.exe mingw32-make[1]: Leaving directory '.../src' mingw32-make.exe -C src mingw32-make[1]: Entering directory '.../src' gcc -I../include -c main.c -o main.o -std=c11 -Wall -Wextra -Wpedantic gcc -I../include -c util.c -o util.o -std=c11 -Wall -Wextra -Wpedantic gcc main.o util.o -o ex2.exe -std=c11 -Wall -Wextra -Wpedantic mingw32-make[1]: Leaving directory '.../src' 运行 src/ex2:square(7) = 49 第二遍 mingw32-make all(增量编译生效): mingw32-make[1]: Nothing to be done for 'all'.
差异说明:本机 mingw32-make 4.4.1 (GNU Make) 真实执行零警告;嵌套调用显示 Entering/Leaving directory 标记;顶层只做分发、构建规则在 src/Makefile
完整源码见 /code 代码示例页
⑥ 编译与运行方法
- ex1/ex2 均本机真实执行(三处命令一致:examples.yaml、Makefile 注释、正文):
mingw32-make clean all (Windows / MinGW;Linux 用 make clean all)- 第一遍:全量编译链接、零警告;运行 ex1 →
max2(3, 7) = 7、ex2 →square(7) = 49; - 第二遍
mingw32-make all:mingw32-make: Nothing to be done for 'all'.——增量编译生效(本机实测); - 清理:
mingw32-make clean(首次 clean 目录为空时,del 的 "Could Not Find" 提示由命令前的-前缀忽略,属正常现象); - Linux 差异:命令名 make、产物无 .exe 后缀、clean 用 rm。
⑦ 常见错误
| 症状 | 原因 | 解决 |
|---|---|---|
makefile:4: *** missing separator. Stop. | 命令行用了空格而非 TAB | 编辑器关闭"Tab 转空格"(本机实测复现该报错) |
| 输入 make 提示"不是内部或外部命令" | MinGW 的命令名是 mingw32-make(make 不在 PATH,本机实测确认) | Windows 用 mingw32-make,Linux 用 make |
| make clean 提示 up to date 不执行 | 目录里有同名文件 clean 且未声明 .PHONY | 加 .PHONY: clean |
| 每次 make 都重新链接 | 目标名与产物名不一致(如目标写 ex1 而 gcc 产出 ex1.exe) | 目标名带平台后缀(TARGET = ex1$(EXE),本章初版实测踩坑后修正) |
| 改了 util.h 不重编 | 依赖行没把头文件写全 | 依赖写 main.o: main.c util.h,头文件在子目录则写相对路径 |
⑧ 小练习
小练习
学习自测:提交后才显示答案与解析(前端判分,不作为正式考试)ex-1-1-4-1.Makefile 中规则下方的命令行必须以什么开头?(单选)
◌ 未作答ex-1-1-4-2.自动变量 $^ 表示?(单选)
◌ 未作答ex-1-1-4-3.判断:目录里存在名为 clean 的文件、且 clean 未声明 .PHONY 时,make clean 可能提示 up to date 而不执行清理。(判断)
◌ 未作答
⑨ 章节测验
章节测验
⑩ 实战任务
实践任务
把多文件小项目改成多目录 Makefile 构建
把学生成绩管理系统(或其他自建多文件小项目)组织成"顶层 Makefile + 子目录构建"的多目录工程, 并完成一次全量构建与一次增量构建的验证。
输入与输出
输入:至少 2 个源文件 + 1 个头文件的小项目(可用本章 ex2 结构改造,或自建)。 交付物:完整工程目录 + Makefile + 一次 clean all 与一次增量构建的真实输出记录。
功能要求
- 顶层 Makefile 用 $(MAKE) -C 嵌套子目录 Makefile(或单目录多目标等价结构)
- 编译命令 -std=c11 -Wall -Wextra -Wpedantic 且零警告
- 规则命令行必须用 Tab 缩进
- clean 声明 .PHONY,并用 ifeq ($(OS),Windows_NT) 区分 del 与 rm
- 目标名带平台后缀(Windows 产物 .exe),保证增量编译判断成立
限制条件
- 本机 Windows 用 mingw32-make(MinGW 无 make 命令)
- 不要复制资料截图中的 Makefile(无法还原),按本章结构自己写
验收步骤(自检清单 0/4)
验收标准
- 构建与清理均一次通过(验收步骤 1/4)
- 能说明增量编译的时间戳判断依据并复现(验收步骤 2/3)
- Makefile 满足 requirements 全部条目
常见失败原因
- 命令行用空格缩进:missing separator. Stop.
- Windows 直接输 make:不是内部或外部命令(应 mingw32-make)
- clean 未 .PHONY 且目录有同名文件:make 提示 up to date
- 目标名不带 .exe 后缀:产物与目标名不符,每次全量重链接(本章初版实测踩坑)
- 改了头文件不重编:依赖行没把头文件写全
可选扩展
- 用 make -j4 对比并行构建时间(多文件项目)
- 用 wildcard/patsubst 自动收集源文件列表,避免手工列 OBJS
- 下一章预告:1.1.5 CMake——把工程描述再抽象一层
完成必要清单后才能计入"已完成实践"(学习状态自动推导,不提供一键完成)
⑪ 面试问题
面试问题
为什么 Makefile 的命令行必须以 Tab 开头?高频C/C++ · easy
要点:历史语法设计:make 用行首 Tab 区分"命令行"与"规则/变量等其他行",空格缩进会报 missing separator。
Makefile 语法里,规则由"目标: 依赖"行和后续命令行组成,make 靠行首 Tab 判断哪些行属于 该规则的命令(变量赋值、include、目标行等都不以 Tab 开头)。这是 1970 年代延续至今的 设计,也是新手最高频的错误:编辑器把 Tab 替换成空格后报 makefile:4: *** missing separator. Stop.(本机实测错误信息)。实践:编辑器关闭"Tab 转空格", 或用支持 Makefile 模式的编辑器高亮检查。
追问:- 追问:.RECIPEPREFIX 可以改这个字符吗?(GNU make 可以,但极少用,可提一句加分)
评分要点:- Tab 区分命令行与其他行
- missing separator 报错
make 的增量编译原理是什么?有什么局限?C/C++ · medium
要点:比较目标文件与依赖文件的时间戳,目标比所有依赖新就跳过;局限:依赖没写全会漏编、时间戳被回拨会误判、跨目录递归要正确传递依赖。
原理:对每条规则,make 比较目标与依赖的修改时间,只要有一个依赖比目标新就执行命令重建, 否则跳过(Nothing to be done)。局限:①依赖关系必须写全——改了头文件没写进依赖行就不会 重编,需 -MMD 自动生成依赖;②时间戳语义——系统时间回拨或时钟同步会导致漏编/误编; ③伪目标冲突——clean 这类不产文件的目标要 .PHONY,否则同名文件会让它 up to date; ④分布式构建需要更精确的依赖图(这也是 CMake/Ninja 出现的原因之一)。
追问:- 追问:为什么改了系统时间可能不重编?(时间戳比较失去单调性)
评分要点:- 时间戳比较机制
- 依赖缺失/时间回拨/伪目标三类局限
make 与直接写 shell 脚本构建、以及 CMake,有什么区别?各适合什么场景?高频C/C++ · medium
要点:shell 脚本顺序全量执行、无依赖分析;make 有依赖图与增量编译但工程大时手写维护难;CMake 是更上层的工程描述,生成 Makefile/Ninja 等后端,适合跨平台与大型工程。
三个层次:①shell 脚本——按顺序把命令跑一遍,最简单,但没有"只重编改动部分"的能力, 全量编译浪费时间;②Makefile——声明目标与依赖,make 自动推导增量编译、支持 -j 并行, 适合中小型 C/C++ 工程;③CMake——用 CMakeLists.txt 描述目标、依赖、选项,再生成 Makefile/Ninja/VS 工程,跨平台与依赖查找能力强,是现代大型工程主流。 面试要点:说明三者不是替代关系而是抽象层次递进,能举出"多文件手动 gcc → Makefile → CMake"的演进路径,并知道 make 仍是嵌入式 SDK(如 Linux 内核、U-Boot)的常见构建入口。
追问:- 追问:内核为什么还用 Makefile?(递归 make 与 kbuild 体系的历史与定制需求)
评分要点:- 依赖分析与增量能力
- 三者层次关系
- 场景选择
⑫ 延伸阅读
- GNU Make Manual(官方手册,只引名称:规则语法、自动变量、函数参考);
- 本地手册:
mingw32-make --help与man make(Linux); - 《跟我一起写 Makefile》(书名,只引名称不复制内容);
- 下一章预告:1.1.5 CMake 入门——把工程描述再抽象一层,用 CMakeLists.txt 生成构建脚本。
迁移训练(migration training)
把本章技能迁移到 Linux:
| 环节 | Windows(MinGW) | Linux |
|---|---|---|
| 命令 | mingw32-make(本机 4.4.1 实测) | make |
| 产物后缀 | .exe | 无后缀 |
| clean 删除命令 | del /Q(ifeq($(OS),Windows_NT) 自动选择) | rm -f |
| 并行 | make -j 同样可用 | make -j |
不变的:规则三要素、TAB 语法、自动变量、.PHONY、增量编译原理与嵌套 make -C;要改的:命令名、产物后缀、删除命令(本章示例已用 ifeq 处理,同一份 Makefile 双平台可用)。
内容来源映射
| 内容部分 | 资料 | 位置 | 标记 | 说明 |
|---|---|---|---|---|
| 规则三要素、增量编译、变量与自动变量、多目录嵌套概念 | 第一阶段讲义 | 4.x | 【来源】 | 概念重新组织表述;讲义该部分示例以截图嵌入无法还原 |
| 原资料 Makefile 示例代码 | 第一阶段讲义 | 4.x | 【待确认】 | 截图代码无法还原,本章示例全部重写并本机实测(mingw32-make clean all 真实执行) |
| 示例代码(ex1/ex2 Makefile 与配套源码) | 无 | 【补充】 | 全新编写;ex1/ex2 均本机真实执行并记录输出(见 examples.yaml) | |
| 通俗解释、练习/测验/实战任务/面试问题 | 无 | 【补充】 | 本章新编 |