跳到主要内容
🔍
1.1.4已发布intermediate · 约 4 课时 · P0

Makefile 基础与多目录工程

会写单文件/多文件/多目录 Makefile,理解增量编译与自动变量

  • 来源
  • 补充
  • 待确认

标记说明:【来源】来自上传资料 · 【补充】课程新编 · 【纠错】按勘误表修正 · 【更新】过时内容已现代化 · 【待确认】无法可靠还原

完成标准(本章)

  • 📖 已阅读:滚动 ≥ 80% 且有效阅读 ≥ 300
  • ✏️ 已练习:小练习正确率 ≥ 60%
  • 📝 已通过测验:分数 ≥ 60
  • 🛠️ 已掌握还需完成实践任务
学习状态:未开始

1.1.4 Makefile 基础与多目录工程

本章来源:规则三要素、增量编译、变量与自动变量、多目录概念来自《第一阶段讲义》4.x【来源】;原资料 Makefile 示例全部以截图嵌入、无法还原【待确认】——本章示例代码全部重写并本机实测(新编比例约 40%)【补充】;通俗解释与练习为新编【补充】。 平台适用性:universal(本机用 mingw32-make 实测——MinGW 无 make 命令,已实测确认;Linux 用 make;clean 的 del/rm 差异已在示例中处理,见迁移训练)。

① 学习目标

  1. 写出 Makefile 规则三要素(目标/依赖/命令),并说明命令必须以 TAB 开头的原因;
  2. 说明增量编译的时间戳原理,并能复现"第二遍 Nothing to be done";
  3. 正确使用变量与自动变量($@ / $^ / $<)改写链接规则;
  4. 写出带 clean 与 .PHONY 的完整 Makefile,并处理 Windows del / Linux rm 的平台差异;
  5. 独立完成多目录工程:顶层 Makefile 用 $(MAKE) -C 嵌套子目录构建(实践任务)。

② 前置知识

  • 必修:1.1.1(编译四阶段与 gcc 常用选项——Makefile 本质是把 gcc 命令自动化);
  • 建议:1.1.3(多文件编译与链接——本章把"编译 add.o → 打包 → 链接"的链自动化为一条 make)。

③ 核心概念【来源】

概念说明
规则三要素目标: 依赖 换行后跟命令;命令行必须以 TAB 开头(空格报 missing separator)
增量编译make 比较目标与依赖的时间戳:目标比所有依赖新就跳过重建(本机实测 Nothing to be done)
变量CC = gccCFLAGS = ...,用 $(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 allmingw32-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 而不执行清理。(判断)

    ◌ 未作答

⑨ 章节测验

章节测验

5 题题库 · 随机抽 5 题 · 及格线 60 分 · 前端判分(学习自测)
开始测验 →

⑩ 实战任务

实践任务

把多文件小项目改成多目录 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 --helpman 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)
通俗解释、练习/测验/实战任务/面试问题补充本章新编