文章目录
- [1. 背景知识](#1. 背景知识)
- 2.基本使用
-
- 2.1make/makefile的初识
- [2.2 make命令和Makefile的依赖关系和依赖方法](#2.2 make命令和Makefile的依赖关系和依赖方法)
- [2.3 项目清理(伪目标依赖)](#2.3 项目清理(伪目标依赖))
- [2.4 Makefile编辑语法细节](#2.4 Makefile编辑语法细节)
- [2.5 \< 和 ^ 的区别(加餐)](#2.5 < 和 ^ 的区别(加餐))
- 求三连!!
1. 背景知识
- 会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力
- 一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作
- -makefile带来的好处就是------"自动化编译",一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
- make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。
- make是一条命令,makefile是一个文件 ,两个搭配使用,完成项目自动化构建。

假如一个项目一共有1w个源文件,难道一个一个gcc编译吗?这显然是不可能的,这里就需要我们项目的自动化构建
2.基本使用
2.1make/makefile的初识
- 创建一个在当前目录下的
Makefile或者makefile首字母大小写都行,然后打开Makefile

- 在内部写下以下内容,保存退出

- 然后 输入
make发现自动执行了gcc命令

这就是最简单的Makefile
2.2 make命令和Makefile的依赖关系和依赖方法
什么是依赖关系、依赖方法?
比如月底,你没钱了,你和你爸是父子关系,所以你找他打电话要钱,这层关系叫依赖关系 ,而打电话要钱的行为就叫依赖方法 。只有依赖关系 和依赖方法同时存在且合理,你才能达到你要钱的目的。
依赖关系
make命令依赖Makefile:make命令是一个解释执行工具,它需要读取Makefile文件中的规则来确定如何编译、链接项目中的文件。如果没有Makefile,make命令无法执行自动化构建操作。Makefile定义依赖规则:Makefile中通过一系列规则来定义文件之间的依赖关系(如目标文件依赖于源文件、头文件等),以及对应的构建命令。make命令根据这些依赖规则来判断哪些文件需要重新编译、链接,从而实现增量构建,提高开发效率。
依赖方法(语法要求)
格式:目标文件: 依赖文件列表
test.exe: test.c
gcc -o test.exe test.c #单行缩进使用Table键
这里声明test.exe依赖于test.c,当test.c或有更新时,make 会执行后面的编译命令重新生成test.exe。
同时依赖关系依赖方法可以多重定义书写


这里make会形成推导栈,通过依赖方式的入栈,直到找到突破口(这里的突破口就是test.c文件的存在),从而进行栈返回,类似于函数递归的方式一步步生成可执行文件。
以下为流程图:

2.3 项目清理(伪目标依赖)
工程是需要被清理的 这里就提到一个新的概念伪目标依赖.
格式:.:PHONY :核心伪目标

这样项目就被清理了

细节解析:
- 依赖关系必须存在,但是依赖列表可以为空。

- 依赖方法(
Makefile文件内的内容)可以是任何shell命令(可以gcc也可以rm)。 - clean目标,只是利用make的自动推导能力,让他执行了
rm命令,在构建工程视角,看起来就是清理项目。清理项目,本质就是删除不需要的临时文件。 make命令,后面可以跟"目标名",后面跟谁,就解析谁的依赖关系和依赖方法,同时make默认只会推导一条完整的推导链路!当make后面没有跟目标名字的时候默认推导第一个依赖关系对应的推导链。
默认推导:

当修改顺序:

此时:

PHONY解析(伪目标)
.PHONY是用来修饰目标文件是一个伪目标,本质就是总是被执行
什么叫总是被执行 ?
当我每次make 生成可执行程序时候,发现只有第一次可以成功。

只有当源文件发生改变以后我们才能再次执行。
但是make clean确能一直执行
当我们为我们的命令加上.PHONY

此时就能总是执行 了

为什么我们可用执行程序往往不.PHONY修饰,gcc为什么无法二次编译老代码???
源代码如果在没有修改的情况下可以一直编译,这不是一个极度浪费资源的行为吗?假如一共有一万个源文件,哪怕你只改了一个源文件,也要全部重新编译。所以make默认只会重新编译你修改过的文件不会重复编译!
那么gcc如何做到的?
我们可以通过stat命令查看文件ACM时间信息

Access:访问内容时间。
Modify:文件内容修改时间。
Change:文件属性修改时间。
Birth:文件创建时间。

判断文件是否要被重新编译,靠的就是.exe文件和源文件的Mod时间的对比!当源文件比可执行文件旧,就无法进行重新编译,当你修改源文件内容,Mod时间就会更新,这样你就可以重新编译了。
那么我们touch直接修改文件时间是不是又可以重新编译了?
答案是:是的。

当然文件时间还有更多细节部分。
文件时间细节(加餐)
当我们修改文件内容的时候 为什么文件属性时间也发生了改变?

修改文件内容,会影响文件大小,同时Mod时间也会改变,但是文件内容大小和Mod时间本身也是文件属性,所以也会修改Change时间。
assess时间修改细节
我们多次使用cat命令访问文件


我们发现两次access时间都没有改变,这个是为什么呢?
文件内容发生更改是会从内存刷新到磁盘上的,文件的属性也是数据,每次更改也会刷新到磁盘上面。
我们修改文件和查看文件属性内容的频率是大大不一样的,明显查看文件比重更高。
而查看文件就会->更新时间->修改文件属性->刷新磁盘!
而每次查看文件就要刷新磁盘,就会导致增加访问磁盘的次数,而这种外设本身就效率低下,最终就会导致OS整体效率低下。
所以,就规定访问文件内容,特定次数后,才会刷新一次!
2.4 Makefile编辑语法细节
@(禁止命令回显)

我们发现此时命令被回显出来

我们可以利用@符号禁止命令回显
效果:

变量替换
我们同样可以这么写

类似于宏一样定义变量,这样可以方便修改,$表示为内容提取符号。
依赖方法同样可以替换成符号:
这里@和^都是定义好的内置变量 $@表示对应依赖关系中的目标文件 $^表示对应依赖关系中的依赖列表
多文件替换
假如一共有199个源文件我们不可能写一百九十九个依赖关系和依赖方法吧。
我们先创建199个文件 这里的{1..199}表示1~199

把test.c改名为main.c作为主函数文件

我们把先前写得备份起来 #就是shell语句中的注释

做法一:采用 shell命令行方式,获取当前所 有.c文件名
格式:$(shell 命令)

测试:

做法二:使用wildcard(通配符),获取当前所有.c文件名
这里wildcard XXX表示获取当前所有XXX文件名字

测试:

然后将 SRC的所有同名 .c 替换成为.o 形成目标文件列表 语法如下:

测试:

结果:

接下来 书写他们的依赖关系和方式
前置知识:
当我们gcc -c xxx.c 会自动生成对应的.o文件

这里的%.c\%.o表示文件里所有的XXX.c和XXX.o文件 %是一种通配符。
同时$< 指代规则中 "第一个依赖文件" 即 .c 文件 然后生成对应的 .o 文件 相当于将其展开成200行gcc -c XXX代码:

展示:

有了创建 肯定还要有清理

展示:

替换进阶内容(命令包装)
如果我们不光想编译C语言 而是别的语言呢? 所以 我们这里的Makefile还可以再次优化

至此 意味着我们可以把该Makefile放入任何目录 我们只需要改变"别名"就可以编译清除任何项目程序了。
2.5 \< 和 ^ 的区别(加餐)
在 Makefile 中,$^ 和 $< 都是 自动变量 (预定义变量),核心区别在于 指代的依赖文件范围不同,适用场景完全不同,以下是清晰对比和实战示例:
一、核心区别总结表
| 自动变量 | 核心含义 | 关键特点 | 适用场景 |
|---|---|---|---|
$< |
指代 第一个依赖文件 | 只取依赖列表的「第一个」,忽略后续所有依赖 | 1. 隐式规则(如 .c→.o 编译); 2. 核心依赖明确的场景(如链接时只需要第一个 .o 触发) |
$^ |
指代 所有依赖文件(自动去重) | 包含依赖列表中所有文件,重复依赖会合并 | 1. 链接多个 .o 文件生成可执行程序; 2. 需用到所有依赖的场景(如打包、多文件编译) |
二、示例(基于 test.exe 多文件场景)
假设项目有 2 个源文件(test.c、utils.c)和 1 个头文件(common.h),最终生成 test.exe,通过示例看两者区别:
项目文件结构
test.c # 主程序源码
utils.c # 工具函数源码
common.h # 公共头文件(两个 .c 文件都依赖)
Makefile # 编译规则
Makefile 规则示例
# 定义变量:所有源文件、目标文件、可执行文件
SRC = test.c utils.c
OBJ = $(SRC:.c=.o) # 推导目标文件:test.o、utils.o
BIN = test.exe
# 规则1:生成可执行文件(依赖所有 .o 文件 + 头文件)
$(BIN): $(OBJ) common.h
gcc $^ -o $@ # 这里必须用 $^,需链接所有 .o 文件
# 展开后:gcc test.o utils.o common.h -o test.exe(头文件不影响编译,仅用于判断是否重新编译)
# 规则2:生成目标文件(每个 .o 依赖对应的 .c 文件 + 头文件)
%.o: %.c common.h
gcc -c $< -o $@ # 这里用 $<,仅需编译第一个依赖(.c 文件)
# 对 test.o 展开:gcc -c test.c -o test.o
# 对 utils.o 展开:gcc -c utils.c -o utils.o