第9课Linux开发工具四make与makefile一、为什么我们需要 Makefile1.1 IDE 背后的秘密在使用 Visual Studio 等 IDE 时我们只需按下 F5 或点击编译按钮程序就会自动完成编译、链接并运行。但我们从未关心过这个过程是如何发生的。IDE 实际做的事情将每个源文件.c/.cpp分别编译成目标文件Windows 下是 .objLinux 下是 .o将所有目标文件和库文件链接在一起形成最终的可执行程序.exe核心结论当项目只有1-2个源文件时手动编译没问题。但当项目有100个、1000个甚至上万个源文件时手动输入编译命令就变得不现实了。1.2 什么是构建老师不喜欢抽象名词用大白话解释构建 把你的所有源文件走一遍程序翻译的完整流程最终形成可执行程序的整个过程。1.3 Windows vs Linux 构建方式对比系统自动化构建工具特点WindowsVisual Studio 集成环境完全自动化用户无感知Linuxmake Makefile工具独立需要开发者自己维护构建规则核心结论make 是一条命令Makefile 是一个文件。两者配合完成 Linux 下的项目自动化构建。二、Makefile 的核心灵魂依赖关系与依赖方法2.1 第一个最简单的 Makefile假设我们有一个test.c源文件要生成test.exe可执行程序。步骤在当前目录下新建一个文件文件名必须是Makefile或makefile推荐首字母大写在文件中写入以下内容test.exe: test.c gcc -o test.exe test.c在命令行中执行make命令即可自动编译生成test.exe2.2 核心概念详解Makefile 的每一条规则都由两部分组成依赖关系目标文件: 依赖文件列表冒号左侧要生成的目标文件冒号右侧生成目标文件所需要的所有依赖文件依赖方法以Tab 键开头的命令行说明如何从依赖文件生成目标文件易错警告依赖方法必须以 Tab 键开头不能用空格代替这是 Makefile 的语法强制要求没有为什么。2.3 生活化理解为什么需要两者同时存在老师用了一个非常形象的例子月底你给爸爸打电话“爸我是你儿子。”只表明了依赖关系你爸会很奇怪“这小子是不是疯了”正确的做法是“爸我是你儿子今天中午12点前给我农行卡打1000块钱。”同时表明了依赖关系和依赖方法结论任何事情的完成都必须同时具备合理的依赖关系和可行的依赖方法。Makefile 也不例外。三、Makefile 的工作原理推导栈与递归思想3.1 完整的程序翻译过程为了理解 Makefile 的推导过程我们先回顾 C 程序的完整翻译步骤test.c → 预处理 → test.i → 编译 → test.s → 汇编 → test.o → 链接 → test.exe3.2 模拟完整翻译过程的 Makefile我们可以写出一个展示完整翻译过程的 Makefiletest.exe: test.o gcc -o test.exe test.o test.o: test.s gcc -c test.s -o test.o test.s: test.i gcc -S test.i -o test.s test.i: test.c gcc -E test.c -o test.i3.3 make 命令的解析过程当我们执行make命令时make 会从 Makefile 的第一个目标开始解析这里是test.exe检查目标文件是否存在以及所有依赖文件是否都是最新的如果某个依赖文件不存在或者比目标文件更新就会去查找该依赖文件的生成规则这个过程会一直持续下去直到找到一个已经存在的源文件这里是test.c然后从最底层开始依次执行依赖方法生成上一层的文件直到最终生成目标文件核心比喻这个过程就像一个栈结构先进后出或者函数递归。test.c就是递归的出口。四、项目清理与伪目标 .PHONY4.1 为什么需要清理项目一个完整的项目不仅要能构建还要能清理。清理就是删除所有生成的中间文件.i、.s、.o和最终的可执行程序。4.2 初步的清理目标我们可以在 Makefile 中添加一个clean目标clean: rm -f test.exe test.i test.s test.o执行make clean命令就会自动删除这些文件。4.3 问题出现了如果当前目录下恰好有一个名为clean的文件那么执行make clean时make 会认为clean文件已经是最新的不会执行任何命令。4.4 解决方案伪目标 .PHONY.PHONY是 Makefile 中的一个特殊关键字用来声明伪目标。语法.PHONY: clean clean: rm -f test.exe test.i test.s test.o核心结论被.PHONY修饰的目标总是会被执行无论当前目录下是否存在同名文件也不会进行时间对比。五、Makefile 高效编译的秘密文件时间对比5.1 为什么第二次 make 不会重新编译当我们第一次执行make生成test.exe后再次执行make会看到提示make: test.exe is up to date.这是因为 make 会比较源文件和目标文件的修改时间如果源文件的修改时间比目标文件晚 → 源文件被修改过需要重新编译如果目标文件的修改时间比源文件晚 → 源文件没有被修改不需要重新编译核心价值这种机制可以大大提高大型项目的编译效率。当你只修改了一个源文件时make 只会重新编译这一个文件然后重新链接而不是编译整个项目。5.2 Linux 文件的三个时间属性使用stat 文件名命令可以查看文件的详细时间信息时间属性英文全称含义AccessAccess Time最近一次访问文件内容的时间ModifyModify Time最近一次修改文件内容的时间ChangeChange Time最近一次修改文件属性的时间如权限、大小等关键区别修改文件内容 → Modify 时间和 Change 时间都会更新因为内容改变会导致文件大小等属性改变只修改文件属性如chmod命令→ 只有 Change 时间会更新5.3 为什么 Access 时间不会每次访问都更新老师提出了一个深刻的问题为什么我们多次cat同一个文件Access 时间却不一定更新原因系统中读操作的频率远远高于写操作如果每次访问文件都更新 Access 时间就意味着每次读操作都要伴随一次写磁盘操作更新文件属性磁盘 IO 是计算机系统中最慢的操作之一频繁的写磁盘会严重降低系统性能解决方案现代 Linux 系统会对 Access 时间的更新进行优化通常是每隔一段时间或累计一定次数的访问后才更新一次。六、Makefile 语法进阶6.1 禁止命令回显符号默认情况下make 会把它执行的每一条命令都打印到终端上。如果我们不想看到这些命令可以在命令前加上符号。示例test.exe: test.c echo 开始编译代码... gcc -o $ $^ echo 编译完成 .PHONY: clean clean: echo 清理工程... rm -f test.exe echo 清理完毕执行make时只会看到我们自定义的提示信息不会看到实际执行的 gcc 和 rm 命令。6.2 自定义变量当 Makefile 变得复杂时使用变量可以让代码更简洁、更易维护。定义变量BIN test.exe SRC test.c使用变量$(BIN): $(SRC) gcc -o $(BIN) $(SRC) .PHONY: clean clean: rm -f $(BIN)注意等号两侧可以有空格定义变量时等号左右两侧允许加空格并且强烈推荐加上以提高代码的可读性如BIN test.exe。千万警惕“尾随空格”致命陷阱Makefile 中的所有变量本质上都是字符串。它会自动忽略等号前后的空格但会把变量值后面的所有空格当成值的一部分例如如果写成 BIN test.exe 末尾不小心敲了几个空格那么 BIN 的实际值就是 test.exe 。这会导致后续编译器找不到对应的文件而报错。区分 Shell 脚本如果你在写 Shell 脚本.sh等号两边才是绝对不能加空格的必须写成 BINtest.exe不要和 Makefile 搞混。6.3 内置自动变量Makefile 提供了一些非常有用的内置自动变量它们会根据当前的规则自动展开自动变量含义$表示规则中的目标文件$表示规则中的第一个依赖文件$^表示规则中的所有依赖文件以空格分隔使用示例test.exe: test.c gcc -o $ $ # $ 展开为 test.exe$ 展开为 test.c七、处理多文件项目7.1 问题100个源文件怎么办如果我们的项目有100个源文件main.c、src1.c、src2.c…src100.c难道我们要在 Makefile 中把它们一个个列出来吗7.2 解决方案一使用 shell 命令获取源文件列表SRC $(shell ls *.c)$(shell 命令)会执行括号中的 shell 命令并将命令的输出结果作为变量的值。这里ls *.c会列出当前目录下所有的 .c 文件。7.3 解决方案二使用 wildcard 函数推荐Makefile 内置的wildcard函数专门用来获取符合特定模式的文件名SRC $(wildcard *.c)7.4 将 .c 后缀替换为 .o 后缀我们需要将所有的 .c 源文件编译成对应的 .o 目标文件。可以使用 Makefile 的变量替换功能OBJ $(SRC:.c.o)这行代码的意思是将SRC变量中所有以.c结尾的字符串替换为以.o结尾。7.5 模式规则%.o: %.c现在我们需要一个通用的规则告诉 make 如何将任意一个 .c 文件编译成对应的 .o 文件%.o: %.c gcc -c $ echo 编译 $ 完成这里的%是一个通配符它会匹配任意字符串。例如当 make 需要生成main.o时它会自动匹配这条规则将%替换为main然后执行gcc -c main.c。7.6 多文件项目的完整 MakefileBIN bite.exe SRC $(wildcard *.c) OBJ $(SRC:.c.o) $(BIN): $(OBJ) echo 链接所有目标文件... gcc -o $ $^ echo 生成可执行文件 $ 完成 %.o: %.c echo 编译 $ ... gcc -c $ .PHONY: clean clean: echo 清理工程... rm -f $(OBJ) $(BIN) echo 清理完毕7.6.1 符号详解1. 变量操作符号 (,$())对应行1, 2, 3, 5, 7 等(赋值号)最基本的变量赋值。如第 1 行BIN bite.exe将右边的字符串赋值给左边的变量。$()(取值/展开符)用来获取变量的值或者调用 Makefile 的内置函数。例如$(BIN)就是把BIN的值bite.exe提取出来。在 Makefile 中只要想使用变量就必须用$()包裹它单字符变量除外但建议全包。2. 内置函数与文本处理对应行2, 3wildcard(通配符函数)* 语法$(wildcard 匹配模式)解释第 2 行$(wildcard *.c)的意思是去当前目录下找所有以.c结尾的文件并把它们的名字用空格拼成一长串字符串赋值给SRC比如main.c utils.c。:.c.o(模式替换语法)语法$(变量名:原后缀新后缀)解释第 3 行$(SRC:.c.o)是一个非常巧妙的文本替换。它会把SRC变量里所有的.c结尾的字符串全部替换成.o。如果SRC是main.c utils.c那么OBJ就会自动变成main.o utils.o。3. 规则定义与模式匹配 (:,%)对应行5, 10:(规则分隔符)语法目标 : 依赖解释告诉 make左边的文件是怎么来的依赖于右边的文件。%(模式通配符)解释第 10 行%.o: %.c称为“模式规则” (Pattern Rule)。这里的%就像一个占位符Stem。含义它告诉 Make 一个通用的道理——“任何一个.o文件都依赖于和它同名的.c文件”。当 Make 需要生成main.o时它会自动套用这条规则把%替换成main去寻找main.c。这比你手动一行行写main.o: main.c要聪明得多。4. 自动变量 ($,$,$^)对应行7, 8, 11, 12$代表冒号左边的目标文件。在第 7 行里它就是bite.exe在第 12 行里它就是当时匹配到的那个.o文件。$^代表冒号右边的所有依赖文件。在第 7 行里它代表所有的.o文件。所以gcc -o $ $^实际上是在把所有.o文件链接成一个bite.exe。$代表冒号右边的第一个依赖文件。在第 12 行的编译命令gcc -c $中它代表当前正在被编译的那个.c源文件。5. 命令控制符号 ()对应行6, 7, 8, 11 等所有缩进的执行命令(静默执行符)* 解释默认情况下Make 在执行命令前会先把这行命令原原本本地打印到屏幕上。如果在命令前面加上Make 就只会执行命令而不会在屏幕上回显命令本身。作用让终端输出更干净。比如你只想看到echo打印出来的中文提示语而不想看到系统把echo 链接所有目标文件...这句代码本身也打印一遍。6. 特殊伪目标 (.PHONY)对应行14.PHONY(声明伪目标)解释第 14 行.PHONY: clean告诉 Makeclean不是一个真正的文件名它只是一个“动作的代号”Pseudo-target。为什么要加假设你的目录下刚好新建了一个叫clean的文件如果你没加.PHONY当你敲make clean时Make 会发现“咦clean文件已经存在了且没有依赖项需要更新”它就会罢工不再执行下面的删除命令。加上.PHONY后无论有没有同名文件Make 都会强制执行clean标签下的命令。总结这个 Makefile 的工作流收集当前所有的.c文件第 2 行。推导出需要生成的.o文件列表第 3 行。发现终极目标bite.exe需要所有的.o文件第 5 行。于是自动利用模式规则第 10 行把一个个.c编译成.o。所有.o都准备好后把它们链接成最终的bite.exe第 7 行。7.6.2 增量编译疑问为什么不能像链接那样把所有.c一次性全编译了这个想法在底层是可以实现的。gcc确实支持你一口气传入所有的源文件比如执行gcc -c main.c utils.c它也会乖乖吐出main.o和utils.o。但是如果在 Makefile 里这么写就完全毁了 Makefile 的“灵魂”Makefile 存在的最大意义叫作增量编译Incremental Build。如果按照这个设想写成“全包”的形式# 假设我们这么写反面教材 $(OBJ): $(SRC) echo 一次性编译所有文件... gcc -c $^ # 把所有 .c 一起喂给 gcc致命后果假设你的项目有 1000 个.c文件。今天你只修改了其中1 个文件比如utils.c的一行代码。当你敲下make时由于所有文件被绑在了一起Make 会把这 1000 个文件全部重新编译一遍本来 0.1 秒能搞定的事你要等 5 分钟。现在的写法分离式%.o: %.cMake 为每一个.o建立了独立的依赖关系。当你只修改了utils.c时Make 会检查时间戳main.c比main.o旧 - 不需要重新编译。utils.c比utils.o新因为你刚改了它 -只触发utils.o: utils.c这一条规则只重新编译utils.c。最后再把现成的main.o和刚生成的utils.o重新链接成.exe。总结链接打包必须大家一起上$^但编译加工必须拆开单干$就是为了“谁改了就只编译谁”极大提高大型项目的编译速度。八、通用 Makefile 最佳实践8.1 进一步变量化为了让 Makefile 更加通用我们可以把编译器、命令等也都变量化BIN bite.exe SRC $(wildcard *.c) OBJ $(SRC:.c.o) # 编译器 CC gcc # 回显命令 ECHO echo # 删除命令 RM rm -rf $(BIN): $(OBJ) $(ECHO) linking $^ to $ ... done $(CC) -o $ $^ %.o: %.c $(ECHO) compiling $ to $ ... done $(CC) -c $ .PHONY: clean clean: $(ECHO) cleaning project ... $(RM) $(OBJ) $(BIN) $(ECHO) clean done!8.2 这个通用 Makefile 的优势高度通用将这个 Makefile 复制到任何 C 语言项目目录下基本都能直接使用易于修改如果需要切换到 C 编译器只需将CC gcc改为CC g清晰易读所有的配置都集中在文件开头一目了然高效编译只重新编译被修改过的源文件8.3 调试技巧添加 test 目标在编写 Makefile 的过程中我们经常需要查看变量的值是否正确。可以添加一个test伪目标来帮助调试.PHONY: test test: echo SRC $(SRC) echo OBJ $(OBJ)执行make test命令就可以看到SRC和OBJ变量的实际值。
第9课:Linux开发工具(四):make与makefile
发布时间:2026/5/15 21:47:18
第9课Linux开发工具四make与makefile一、为什么我们需要 Makefile1.1 IDE 背后的秘密在使用 Visual Studio 等 IDE 时我们只需按下 F5 或点击编译按钮程序就会自动完成编译、链接并运行。但我们从未关心过这个过程是如何发生的。IDE 实际做的事情将每个源文件.c/.cpp分别编译成目标文件Windows 下是 .objLinux 下是 .o将所有目标文件和库文件链接在一起形成最终的可执行程序.exe核心结论当项目只有1-2个源文件时手动编译没问题。但当项目有100个、1000个甚至上万个源文件时手动输入编译命令就变得不现实了。1.2 什么是构建老师不喜欢抽象名词用大白话解释构建 把你的所有源文件走一遍程序翻译的完整流程最终形成可执行程序的整个过程。1.3 Windows vs Linux 构建方式对比系统自动化构建工具特点WindowsVisual Studio 集成环境完全自动化用户无感知Linuxmake Makefile工具独立需要开发者自己维护构建规则核心结论make 是一条命令Makefile 是一个文件。两者配合完成 Linux 下的项目自动化构建。二、Makefile 的核心灵魂依赖关系与依赖方法2.1 第一个最简单的 Makefile假设我们有一个test.c源文件要生成test.exe可执行程序。步骤在当前目录下新建一个文件文件名必须是Makefile或makefile推荐首字母大写在文件中写入以下内容test.exe: test.c gcc -o test.exe test.c在命令行中执行make命令即可自动编译生成test.exe2.2 核心概念详解Makefile 的每一条规则都由两部分组成依赖关系目标文件: 依赖文件列表冒号左侧要生成的目标文件冒号右侧生成目标文件所需要的所有依赖文件依赖方法以Tab 键开头的命令行说明如何从依赖文件生成目标文件易错警告依赖方法必须以 Tab 键开头不能用空格代替这是 Makefile 的语法强制要求没有为什么。2.3 生活化理解为什么需要两者同时存在老师用了一个非常形象的例子月底你给爸爸打电话“爸我是你儿子。”只表明了依赖关系你爸会很奇怪“这小子是不是疯了”正确的做法是“爸我是你儿子今天中午12点前给我农行卡打1000块钱。”同时表明了依赖关系和依赖方法结论任何事情的完成都必须同时具备合理的依赖关系和可行的依赖方法。Makefile 也不例外。三、Makefile 的工作原理推导栈与递归思想3.1 完整的程序翻译过程为了理解 Makefile 的推导过程我们先回顾 C 程序的完整翻译步骤test.c → 预处理 → test.i → 编译 → test.s → 汇编 → test.o → 链接 → test.exe3.2 模拟完整翻译过程的 Makefile我们可以写出一个展示完整翻译过程的 Makefiletest.exe: test.o gcc -o test.exe test.o test.o: test.s gcc -c test.s -o test.o test.s: test.i gcc -S test.i -o test.s test.i: test.c gcc -E test.c -o test.i3.3 make 命令的解析过程当我们执行make命令时make 会从 Makefile 的第一个目标开始解析这里是test.exe检查目标文件是否存在以及所有依赖文件是否都是最新的如果某个依赖文件不存在或者比目标文件更新就会去查找该依赖文件的生成规则这个过程会一直持续下去直到找到一个已经存在的源文件这里是test.c然后从最底层开始依次执行依赖方法生成上一层的文件直到最终生成目标文件核心比喻这个过程就像一个栈结构先进后出或者函数递归。test.c就是递归的出口。四、项目清理与伪目标 .PHONY4.1 为什么需要清理项目一个完整的项目不仅要能构建还要能清理。清理就是删除所有生成的中间文件.i、.s、.o和最终的可执行程序。4.2 初步的清理目标我们可以在 Makefile 中添加一个clean目标clean: rm -f test.exe test.i test.s test.o执行make clean命令就会自动删除这些文件。4.3 问题出现了如果当前目录下恰好有一个名为clean的文件那么执行make clean时make 会认为clean文件已经是最新的不会执行任何命令。4.4 解决方案伪目标 .PHONY.PHONY是 Makefile 中的一个特殊关键字用来声明伪目标。语法.PHONY: clean clean: rm -f test.exe test.i test.s test.o核心结论被.PHONY修饰的目标总是会被执行无论当前目录下是否存在同名文件也不会进行时间对比。五、Makefile 高效编译的秘密文件时间对比5.1 为什么第二次 make 不会重新编译当我们第一次执行make生成test.exe后再次执行make会看到提示make: test.exe is up to date.这是因为 make 会比较源文件和目标文件的修改时间如果源文件的修改时间比目标文件晚 → 源文件被修改过需要重新编译如果目标文件的修改时间比源文件晚 → 源文件没有被修改不需要重新编译核心价值这种机制可以大大提高大型项目的编译效率。当你只修改了一个源文件时make 只会重新编译这一个文件然后重新链接而不是编译整个项目。5.2 Linux 文件的三个时间属性使用stat 文件名命令可以查看文件的详细时间信息时间属性英文全称含义AccessAccess Time最近一次访问文件内容的时间ModifyModify Time最近一次修改文件内容的时间ChangeChange Time最近一次修改文件属性的时间如权限、大小等关键区别修改文件内容 → Modify 时间和 Change 时间都会更新因为内容改变会导致文件大小等属性改变只修改文件属性如chmod命令→ 只有 Change 时间会更新5.3 为什么 Access 时间不会每次访问都更新老师提出了一个深刻的问题为什么我们多次cat同一个文件Access 时间却不一定更新原因系统中读操作的频率远远高于写操作如果每次访问文件都更新 Access 时间就意味着每次读操作都要伴随一次写磁盘操作更新文件属性磁盘 IO 是计算机系统中最慢的操作之一频繁的写磁盘会严重降低系统性能解决方案现代 Linux 系统会对 Access 时间的更新进行优化通常是每隔一段时间或累计一定次数的访问后才更新一次。六、Makefile 语法进阶6.1 禁止命令回显符号默认情况下make 会把它执行的每一条命令都打印到终端上。如果我们不想看到这些命令可以在命令前加上符号。示例test.exe: test.c echo 开始编译代码... gcc -o $ $^ echo 编译完成 .PHONY: clean clean: echo 清理工程... rm -f test.exe echo 清理完毕执行make时只会看到我们自定义的提示信息不会看到实际执行的 gcc 和 rm 命令。6.2 自定义变量当 Makefile 变得复杂时使用变量可以让代码更简洁、更易维护。定义变量BIN test.exe SRC test.c使用变量$(BIN): $(SRC) gcc -o $(BIN) $(SRC) .PHONY: clean clean: rm -f $(BIN)注意等号两侧可以有空格定义变量时等号左右两侧允许加空格并且强烈推荐加上以提高代码的可读性如BIN test.exe。千万警惕“尾随空格”致命陷阱Makefile 中的所有变量本质上都是字符串。它会自动忽略等号前后的空格但会把变量值后面的所有空格当成值的一部分例如如果写成 BIN test.exe 末尾不小心敲了几个空格那么 BIN 的实际值就是 test.exe 。这会导致后续编译器找不到对应的文件而报错。区分 Shell 脚本如果你在写 Shell 脚本.sh等号两边才是绝对不能加空格的必须写成 BINtest.exe不要和 Makefile 搞混。6.3 内置自动变量Makefile 提供了一些非常有用的内置自动变量它们会根据当前的规则自动展开自动变量含义$表示规则中的目标文件$表示规则中的第一个依赖文件$^表示规则中的所有依赖文件以空格分隔使用示例test.exe: test.c gcc -o $ $ # $ 展开为 test.exe$ 展开为 test.c七、处理多文件项目7.1 问题100个源文件怎么办如果我们的项目有100个源文件main.c、src1.c、src2.c…src100.c难道我们要在 Makefile 中把它们一个个列出来吗7.2 解决方案一使用 shell 命令获取源文件列表SRC $(shell ls *.c)$(shell 命令)会执行括号中的 shell 命令并将命令的输出结果作为变量的值。这里ls *.c会列出当前目录下所有的 .c 文件。7.3 解决方案二使用 wildcard 函数推荐Makefile 内置的wildcard函数专门用来获取符合特定模式的文件名SRC $(wildcard *.c)7.4 将 .c 后缀替换为 .o 后缀我们需要将所有的 .c 源文件编译成对应的 .o 目标文件。可以使用 Makefile 的变量替换功能OBJ $(SRC:.c.o)这行代码的意思是将SRC变量中所有以.c结尾的字符串替换为以.o结尾。7.5 模式规则%.o: %.c现在我们需要一个通用的规则告诉 make 如何将任意一个 .c 文件编译成对应的 .o 文件%.o: %.c gcc -c $ echo 编译 $ 完成这里的%是一个通配符它会匹配任意字符串。例如当 make 需要生成main.o时它会自动匹配这条规则将%替换为main然后执行gcc -c main.c。7.6 多文件项目的完整 MakefileBIN bite.exe SRC $(wildcard *.c) OBJ $(SRC:.c.o) $(BIN): $(OBJ) echo 链接所有目标文件... gcc -o $ $^ echo 生成可执行文件 $ 完成 %.o: %.c echo 编译 $ ... gcc -c $ .PHONY: clean clean: echo 清理工程... rm -f $(OBJ) $(BIN) echo 清理完毕7.6.1 符号详解1. 变量操作符号 (,$())对应行1, 2, 3, 5, 7 等(赋值号)最基本的变量赋值。如第 1 行BIN bite.exe将右边的字符串赋值给左边的变量。$()(取值/展开符)用来获取变量的值或者调用 Makefile 的内置函数。例如$(BIN)就是把BIN的值bite.exe提取出来。在 Makefile 中只要想使用变量就必须用$()包裹它单字符变量除外但建议全包。2. 内置函数与文本处理对应行2, 3wildcard(通配符函数)* 语法$(wildcard 匹配模式)解释第 2 行$(wildcard *.c)的意思是去当前目录下找所有以.c结尾的文件并把它们的名字用空格拼成一长串字符串赋值给SRC比如main.c utils.c。:.c.o(模式替换语法)语法$(变量名:原后缀新后缀)解释第 3 行$(SRC:.c.o)是一个非常巧妙的文本替换。它会把SRC变量里所有的.c结尾的字符串全部替换成.o。如果SRC是main.c utils.c那么OBJ就会自动变成main.o utils.o。3. 规则定义与模式匹配 (:,%)对应行5, 10:(规则分隔符)语法目标 : 依赖解释告诉 make左边的文件是怎么来的依赖于右边的文件。%(模式通配符)解释第 10 行%.o: %.c称为“模式规则” (Pattern Rule)。这里的%就像一个占位符Stem。含义它告诉 Make 一个通用的道理——“任何一个.o文件都依赖于和它同名的.c文件”。当 Make 需要生成main.o时它会自动套用这条规则把%替换成main去寻找main.c。这比你手动一行行写main.o: main.c要聪明得多。4. 自动变量 ($,$,$^)对应行7, 8, 11, 12$代表冒号左边的目标文件。在第 7 行里它就是bite.exe在第 12 行里它就是当时匹配到的那个.o文件。$^代表冒号右边的所有依赖文件。在第 7 行里它代表所有的.o文件。所以gcc -o $ $^实际上是在把所有.o文件链接成一个bite.exe。$代表冒号右边的第一个依赖文件。在第 12 行的编译命令gcc -c $中它代表当前正在被编译的那个.c源文件。5. 命令控制符号 ()对应行6, 7, 8, 11 等所有缩进的执行命令(静默执行符)* 解释默认情况下Make 在执行命令前会先把这行命令原原本本地打印到屏幕上。如果在命令前面加上Make 就只会执行命令而不会在屏幕上回显命令本身。作用让终端输出更干净。比如你只想看到echo打印出来的中文提示语而不想看到系统把echo 链接所有目标文件...这句代码本身也打印一遍。6. 特殊伪目标 (.PHONY)对应行14.PHONY(声明伪目标)解释第 14 行.PHONY: clean告诉 Makeclean不是一个真正的文件名它只是一个“动作的代号”Pseudo-target。为什么要加假设你的目录下刚好新建了一个叫clean的文件如果你没加.PHONY当你敲make clean时Make 会发现“咦clean文件已经存在了且没有依赖项需要更新”它就会罢工不再执行下面的删除命令。加上.PHONY后无论有没有同名文件Make 都会强制执行clean标签下的命令。总结这个 Makefile 的工作流收集当前所有的.c文件第 2 行。推导出需要生成的.o文件列表第 3 行。发现终极目标bite.exe需要所有的.o文件第 5 行。于是自动利用模式规则第 10 行把一个个.c编译成.o。所有.o都准备好后把它们链接成最终的bite.exe第 7 行。7.6.2 增量编译疑问为什么不能像链接那样把所有.c一次性全编译了这个想法在底层是可以实现的。gcc确实支持你一口气传入所有的源文件比如执行gcc -c main.c utils.c它也会乖乖吐出main.o和utils.o。但是如果在 Makefile 里这么写就完全毁了 Makefile 的“灵魂”Makefile 存在的最大意义叫作增量编译Incremental Build。如果按照这个设想写成“全包”的形式# 假设我们这么写反面教材 $(OBJ): $(SRC) echo 一次性编译所有文件... gcc -c $^ # 把所有 .c 一起喂给 gcc致命后果假设你的项目有 1000 个.c文件。今天你只修改了其中1 个文件比如utils.c的一行代码。当你敲下make时由于所有文件被绑在了一起Make 会把这 1000 个文件全部重新编译一遍本来 0.1 秒能搞定的事你要等 5 分钟。现在的写法分离式%.o: %.cMake 为每一个.o建立了独立的依赖关系。当你只修改了utils.c时Make 会检查时间戳main.c比main.o旧 - 不需要重新编译。utils.c比utils.o新因为你刚改了它 -只触发utils.o: utils.c这一条规则只重新编译utils.c。最后再把现成的main.o和刚生成的utils.o重新链接成.exe。总结链接打包必须大家一起上$^但编译加工必须拆开单干$就是为了“谁改了就只编译谁”极大提高大型项目的编译速度。八、通用 Makefile 最佳实践8.1 进一步变量化为了让 Makefile 更加通用我们可以把编译器、命令等也都变量化BIN bite.exe SRC $(wildcard *.c) OBJ $(SRC:.c.o) # 编译器 CC gcc # 回显命令 ECHO echo # 删除命令 RM rm -rf $(BIN): $(OBJ) $(ECHO) linking $^ to $ ... done $(CC) -o $ $^ %.o: %.c $(ECHO) compiling $ to $ ... done $(CC) -c $ .PHONY: clean clean: $(ECHO) cleaning project ... $(RM) $(OBJ) $(BIN) $(ECHO) clean done!8.2 这个通用 Makefile 的优势高度通用将这个 Makefile 复制到任何 C 语言项目目录下基本都能直接使用易于修改如果需要切换到 C 编译器只需将CC gcc改为CC g清晰易读所有的配置都集中在文件开头一目了然高效编译只重新编译被修改过的源文件8.3 调试技巧添加 test 目标在编写 Makefile 的过程中我们经常需要查看变量的值是否正确。可以添加一个test伪目标来帮助调试.PHONY: test test: echo SRC $(SRC) echo OBJ $(OBJ)执行make test命令就可以看到SRC和OBJ变量的实际值。