

目录
一.make和Makefile的基本概念
make工具的作用和背景
在软件开发中,特别是使用 C/C++ 这类需要编译的语言时,一个项目通常由多个源文件组成,手动逐个编译这些文件不仅繁琐、效率低下,而且在只修改了少数几个文件后重新编译整个项目也非常耗时,make工具就是为了解决这个问题而诞生的。它是一个自动化构建工具,能够根据Makefile文件中定义的规则,判断项目中哪些部分需要重新编译,并执行相应的命令来完成编译和链接工作
Makefile的结构和作用
Makefile是一个文本文件,它告诉make工具如何编译和链接程序。它包含了一系列的规则 (Rules),每个规则说明了如何从一个或多个依赖文件来构建目标文件
为什么需要make/Makefile
-
自动化:只需一个make命令,即可完成复杂的编译流程
-
效率:基于时间戳的依赖检查,只编译必要的文件,节省大量时间
-
可维护性:将编译指令、链接选项、依赖关系集中管理,项目结构清晰
-
可移植性:一个好的Makefile可以在不同的 Unix-like 系统上工作
二.Makefile的基本结构与语法
依赖关系与依赖方法
- 依赖关系
test:test.c
“:”左边的为目标文件,冒号右边的是依赖文件列表,即可以有多个文件,要得到可执行文件test,它的生成依赖于源文件test.c,这就是文件之间的依赖关系,特别注意:依赖关系必须存在,但依赖文件列表可以为空
- 依赖方法
test:test.c
gcc test -o test.c # 此行为依赖方法
依赖方法则是依赖文件列表要执行怎样的操作才能得到目标文件,这里的test.c要通过gcc进行编译才能得到可执行文件test,即相当于告诉编译器我们要进行的操作,要注意的是在写依赖方法前要加一个Tab,并且依赖方法可以是任意的shell命令
一个简单的Makefile实例



使用方法:
- make:编译生成可执行文件
- make clean:清理中间文件和可执行文件
三.make的工作原理
make是如何工作的?

文件的有关时间
- 在二次输入make命令后,系统会提示当前构建的目标文件已是最新的状态而无法执行

- 而源文件发生更改后,才会重新编译

- 这本质上是因为gcc无法二次编译老代码,但在源文件发生更改后,旧代码变为新代码,从而能被重新编译,这归结于源文件和可执行文件谁的修改时间谁更新的问题,同时也表明文件的新旧取决于文件的有关时间
如何查看文件的有关时间
stat test # 查看文件的状态与时间

- Acess:文件最近一次被访问的时间,查看文件内容、修改文件内容,都属于访问文件
- Modify:最近一次新建文件或修改文件内容的时间
- Change:最近一次修改文件属性的时间
由于文件 = 文件内容 + 文件属性,若修改文件内容,首先需要访问文件,Mod和Ace时间都会更新,同时文件的大小也会因为内容而改变,而文件的大小与Mod时间都属于文件的属性,所以Change时间也会更新,因此修改文件内容会导致三个时间都进行更新,进一步验证了这三个时间是相互关联的

stat也属于访问文件,但Acess为什么没有更新?这是因为对文件的各种操作,都会导致Access时间改变,从而增加访问磁盘的次数,导致OS整体效率降低,所以在现在的Linux中,对Access的更新策略进行了修改,维护了一个计数器,会根据Modify和Access的更新达到一定次数的时候,才会更新Access,以此来提高系统的运行效率
手动更新文件时间
touch test.c # 更新文件的所有时间

判断文件新旧
- 判断文件的新旧是根据文件的Mod时间判定的
最后通过时间轴再来梳理一下gcc/make二次编译的问题:

推导栈的形成与推导过程
下面是一个进行完整编译过程的Makefile:
test:test.o
gcc test.o -o test
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.i
.PHONY:clean
clean:
rm -rf test test.i test.s test.o

从上图可以看出,输入make指令以后,它实际执行makefile文件中指令的顺序,和我们写在makefile文件中的指令顺序是反的
这是因为,要得到test必须要依赖test.o,但是当前目录下没有test.o这个文件,但是makefile文件中有得到test.o的方法,但是,要得到test.o又必须依赖test.s,但当前目录下依旧没有test.s,以此类推,要先得到test.i文件,才能得到test.s文件,然后才能得到test.o文件,最后才能得到test,所以实际指令的执行顺序,于我们在makefile文件中写入的,是反过来的
实际上保存这些依赖关系的是一种栈式的结构,即为推导栈:

这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第⼀个目标文件,上面的Makefile只是为了演示其自动化推导的能力,实际运用中可以省略中间过程的依赖关系和依赖方法
test:test.c
gcc test.c -o test
.PHONY:clean
clean:
rm -rf test
make clean
clean: # 依赖列表为空
rm -rf test
make命令的后面可以跟目标名,后面跟哪个目标就解析谁的依赖关系和依赖方法,所以make clean只是创建了一个没有依赖列表的clean目标,利用了make的自动化推导能力,让其选择性执行rm命令,在构建工程的角度,看起来就是清理项目,本质就是删除不需要的临时文件
make后面如果不跟目标名,默认只会按顺序推导第一个依赖关系对应的推导链

四.常用的Makefile规则和变量
特殊规则:.PHONY
.PHONY:clean
- .PHONY用于修饰目标文件是一个伪目标。伪目标并不代表一个实际的文件名,它只是一个标签,代表一系列需要总是被执行的命令
- .PHONY能够让gcc或命令忽略Mod对比时间新旧,从而使命令具有总是被执行的特性
- 为什么需要它?如果你当前目录下恰好有一个文件叫做 clean,那么当你执行 make clean 时,make 会认为 clean 文件已经存在且没有依赖项需要更新,从而不会执行 rm 命令。用 .PHONY 声明后,make 就不会去检查文件是否存在,而是直接执行命令

所以我们通常会用.PHONY用来修饰clean,让make clean总是能够被执行,从而能有效地进行清理工程
自定义变量
- 定义变量(赋值)
BIN=test # 定义目标文件
SRC=test.c # 定义源文件列表
CC=gcc # 定义一个编译器
RM=rm -rf # 定义一个命令及其选项
- 使用变量(展开)
可以使用 $(value_name) 来获取变量的值:
BIN=test # 定义目标文件
SRC=test.c # 定义源文件列表
CC=gcc # 定义一个编译器
RM=rm -rf # 定义一个命令及其选项
$(BIN):$(SRC)
$(CC) $(SRC) -o $(BIN)
.PHONY:clean
clean:
$(RM) $(BIN)
在上面的依赖中,make 会将这些变量展开为:
test:test.c
gcc test.c -o test
.PHONY:clean
clean:
rm -rf test
- 为什么需要自定义变量
-
避免重复:比如,如果你在 Makefile 里写了十次gcc -o,有一天你想把编译器换成clang或者想增加一个-g调试选项,你就需要修改十个地方,很容易出错。使用变量后,你只需修改一个地方
-
集中配置:通常会把所有重要的变量定义在 Makefile 的顶部,形成一个清晰的“配置区域”。任何人一看就知道这个项目用什么编译、编译什么文件、输出是什么
-
易于覆盖:可以在调用make的命令行上轻松覆盖这些变量的值,而无需修改 Makefile 本身,这非常灵活
自动变量
这类变量在依赖方法的命令部分被求值,在每个依赖中都可以有不同的值,其优势体现于在目标或依赖文件名长,数量多的情景
-
$@:当前依赖中的目标文件名。
-
$<:当前依赖中的第一个依赖文件名,常与通配符%搭配使用
-
$^:当前依赖中的所有依赖文件列表,以空格分隔
-
$?:比目标文件更新的所有依赖文件列表
使用自动变量重写之前的简单示例:
test:test.c
$(CC) $^ -o $@
- 上面的依赖方法等价于 gcc test.c -o test 命令
五.多文件项目的Makefile编写
指定环境
现实中的项目通常包含多个 .c 文件和 .h 文件。我们需要先将每个 .c 文件编译成 .o 目标文件,最后再将所有 .o 文件链接成最终的可执行文件

- 模拟多文件环境

Makefile编写
# 配置列表
BIN=code.exe
#SRC=$(shell ls *.c) # 采⽤shell命令⾏⽅式,获取当前所有.c⽂件名
SRC=$(wildcard *.c) # 使用wildcard函数,获取当前目录所有.c文件
OBJ=$(SRC:.c=.o) # 将SRC中的所有同名.c替换为.o,形成目标文件列表
CC=gcc
RM=rm -rf
$(BIN):$(OBJ)
@$(CC) $^ -o $@ # @:不回显依赖方法
@echo "linking...$^ to $@" # 向显示器输出链接过程
# 告诉 make 如何从任意 .c 文件编译出同名的 .o 文件
%.o:%.c # %为通配符,展开当前目录下的.c文件,同时展开同名.o
@$(CC) -c $< -o $@
@echo "compling...$< to $@"
.PHONY:clean
clean:
@echo "清理工程..."
@$(RM) $(BIN) $(OBJ) # 删除.o/.exe文件
@echo "清理完成..."
.PHONY:test # 测试.c/.o文件完整性
test:
@echo $(SRC)
@echo $(OBJ)
- 运行效果


六.总结
本期的 Linux 开发工具指南介绍了 make 工具和 Makefile 文件。我们了解了 make 如何通过 Makefile 中定义的依赖关系和依赖方法来自动化管理项目的编译构建流程。其核心工作原理在于比较目标文件和依赖文件之间的修改时间,以此决定是否需要重新执行命令,从而避免重复编译,极大提升了开发效率。我们还学习了如何编写 Makefile,包括使用自定义变量和自动变量来简化脚本,以及如何为多文件项目构建高效的自动化编译规则。掌握 make/Makefile 是迈向高效开发的重要一步。下期,我们将介绍另一个至关重要的开发工具:Git,学习如何进行版本控制与团队协作

720

被折叠的 条评论
为什么被折叠?



