文章目录
- 一、自动化构建工具
- 1. 什么是 make 和 Makefile?
- 2. 如何自动化构建可执行程序?
- 3. Makefile 的核心思想
- 4. 如何清理可执行文件?
- 5. make 的工作原理
- 5.1 make 的执行顺序
- 5.2 为什么 make 要检查文件是否更新?
- 5.2.1 避免重复编译,节省时间
- 5.2.2 时间戳检查机制
- 5.3 `.PHONY`伪目标
- 补充知识:make 依赖关系推导
- 补充知识:分阶段编译
- 6. Makefile 扩展语法
- 6.1 定义变量
- 6.2 多个源文件
- 6.3 多个.o文件
- 6.4 测试是否可行
- 6.5 `$^` 和 `$@`
- 6.6 `%`通配符与`<`
- 6.7 测试一下
- 6.8 清理文件
- 6.9 自定义输出信息
- 6.10 Makefile 扩展语法汇总
一、自动化构建工具
平时用 gcc / g++ 编译代码时,每次都要手动输入编译命令。尤其是当项目包含多个源文件时,重复输入命令不仅繁琐,还容易出错。有没有办法自动化完成编译过程,一键生成可执行文件呢?
答案是:make 与 makefile
1. 什么是 make 和 Makefile?
make 是一个自动化构建工具,而 Makefile 是配合 make 使用的配置文件,它们的组合可以帮你解决手动重复输入编译命令的问题。
简单说:make是一条指令,makefile是一个文件。
2. 如何自动化构建可执行程序?
简单认识一下 make 和 makefile 如何自动化构建可执行程序。
第一步:在源代码同级目录下,创建一个文件名为:makefile / Makefile 的文件。(首字母大小写均可)
第二步:在文件内部写上一些语句用来构建可执行程序
目标可执行程序文件名:依赖的文件名 //依赖关系
gcc xxx -o xxx //依赖方法
//第二行要紧跟着以tab键开头
冒号右面又叫依赖文件列表,依赖文件可能有多个。
第三步:执行make命令,即可自动形成可执行程序
3. Makefile 的核心思想
根据依赖关系和依赖方法形成目标文件。
例如:小明想和小明爸爸要生活费,并且小明只能用学校公用电话。
场景一:
小明给爸爸打电话说:爸,我是你儿子。(表明依赖关系)
如果此时小明不再说话,此时小明爸爸会一脸蒙圈,回复:干啥?
小明继续说:给我打钱!(表明依赖方法)
此时小明爸爸明白了小明的意思:转账2000元。
场景二:
小明给小李爸爸打电话说:我是小明,给我打钱!
此时小李的爸爸依然会一脸蒙圈,回复:???打错了吧?(依赖关系不匹配)
场景三:
小明想追求心仪的女同学小美,直接跟小美表白。(直接表明依赖方法)
小美说:你是谁呀,我都不认识你,算了吧。(没有先产生依赖关系)
场景四:
小明想追求心仪的女同学小花,这次有经验了,先给小花讲解数学题,又经常约小花一起共进晚餐(先产生依赖关系)
在某次一起共进晚餐时,和小花表白。(再表明依赖方法)
小花说:我也喜欢你!
所以,成事需要依赖方法和依赖关系同时存在、匹配,且往往需先建立依赖关系,再执行方法。
Makefile 核心逻辑:
依赖关系 = “我和对方的关系” = “目标和依赖文件的关系”
依赖方法 = “怎么让对方行动” = “怎么用命令处理文件”
顺序原则 = “先混熟,再提需求” = “先处理依赖项,再生成目标”
4. 如何清理可执行文件?
先打开Makefile,添加新配置,如图:
.PHONY:clean
clean:
rm -f ok
当想要删除可执行文件时,执行make clean
就会自动进行rm
5. make 的工作原理
5.1 make 的执行顺序
make 会自顶向下扫描makefile文件,默认形成第一个目标文件
如果想指定形成目标文件,语法:make 目标文件
我们将 clean 和 ok 调换一下顺序
可以发现,再次执行make
指令,就默认执行rm
了,只有执行make ok
才执行gcc
指令
5.2 为什么 make 要检查文件是否更新?
make 工具通过检查文件更新状态,仅重新编译必要的文件,从而大幅提高编译效率。
5.2.1 避免重复编译,节省时间
假设一个项目包含 100 个源文件,其中只有 1 个文件被修改:
不检查更新时:每次执行 make 都需重新编译全部 100 个文件,耗时可能长达数十分钟。
检查更新时:make 仅编译被修改的文件及其依赖项,其他文件直接复用之前的编译结果(如 .o 目标文件),节省大量时间。
5.2.2 时间戳检查机制
make 会比较 依赖文件(如 .c、.h)和 目标文件(如 .o) 的修改时间戳。
若依赖文件的时间戳比目标文件新(即依赖文件被修改过),则认为目标文件 “过时”,需要重新编译。
若所有依赖文件未更新,则跳过编译目标文件。
简单说:通过对比源文件(依赖文件)和可执行程序(目标文件)的修改时间(Modify time)
总结:make 检查文件更新的核心目的是实现增量编译—— 仅更新过时的文件,在保证编译结果正确性的前提下最大化提升效率。这一机制通过 Makefile 定义的依赖关系和文件时间戳对比实现,是现代软件开发中构建工具的基础设计思想。
5.3 .PHONY
伪目标
在 Makefile 中,伪目标(Phony Target) 的依赖方法总是会被执行。
伪目标是一种特殊的目标,它不代表一个实际的文件,而是作为执行一组命令的标识。通过.PHONY
声明:
为什么伪目标的依赖方法总是执行?
-
make 的默认行为
对于普通目标,make 会:
检查目标文件是否存在。
比较目标文件与依赖文件的修改时间。
仅当目标不存在或依赖更新时,才执行命令。 -
伪目标打破时间戳检查
伪目标没有对应的文件,因此 make 无法检查其时间戳。当你执行 make clean 时:
make 发现 clean 是伪目标,直接执行对应的命令,不检查任何条件。
即使当前目录存在名为 clean 的文件,.PHONY 声明会让 make 忽略它。
简单说:凡是被.PHONY
修饰的伪目标,忽略对比修改时间,总是能被执行。
由于clean
被.PHONY
修饰:
在ok
目标文件前加上.PHONY
,可以发现,和上面的clean一样,可以一直被执行。
补充知识:make 依赖关系推导
make 会进行依赖关系的推导,直到依赖文件是存在的
类似将依赖方法不断入栈,推导完毕出栈执行方法。
补充知识:分阶段编译
一般在编译时,我们通常不会直接把文件从 .c 直接到可执行文件,而是先从 .c 到 .o 再到可执行。
为什么要分阶段编译?
- 效率更高:只修改一个源文件时,只需重新编译对应的 .o 文件,无需重新编译整个项目。
- 模块化开发:不同模块可以独立编译,最后链接在一起,便于团队协作。
- 复用性:可以将常用功能编译为静态库或动态库,供多个项目复用。
6. Makefile 扩展语法
讲解这部分知识,是为了能让我们的Makefile文件变的更加通用,假设未来有变更,直接修改变量即可。
6.1 定义变量
下面讲解一下如何将Makefile文件写的更加通用
首先定义一批变量
BIN=ok
SRC=test.c
OBJ=test.o
CC=gcc
RM=rm -f
将所有可以被变量替换的指令替换,用这种形式:$(变量)
可以发现可以正常使用make
6.2 多个源文件
当我们想把非常多的源文件编译形成一个可执行文件时,该怎么办呢?
第一种方法:SRC=$(shell ls *.c)
,在Makefile中可以通过这种方法使用shell指令
第二种方法:SRC=$(wildcard *.c)
,Makefile中的内部语法
6.3 多个.o文件
把所有的源文件的.c后缀替换为.o,形成OBJ
OBJ=$(SRC:.c=.o)
此时,OBJ就等于所有的同名.o文件了
6.4 测试是否可行
测试多个源文件是否正确
make回显问题:
make默认会对指令进行回显,如果不想回显,在不想回显的指令前加上@符号
测试多个目标文件是否正确
加了@符号之后就不会再多回显一次了。
正常显示,一段.c文件,一段.o文件。并且确实都是源文件的同名.o文件。
6.5 $^
和 $@
此时 SRC 和 OBJ 不再是单一文件,他们表示的都是多个文件
依然可以是用之前的方法:
$(BIN):$(OBJ)
gcc $(OBJ) -o $(BIN)
更优方法:
$(BIN):$(OBJ)
gcc $^ -o $@
如图,$^
表示所有的OBJ文件,$@
表示BIN文件
6.6 %
通配符与<
%.o:%.c
$(CC) -c $<
6.7 测试一下
6.8 清理文件
6.9 自定义输出信息
BIN=ok
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)
CC=gcc
RM=rm -f
$(BIN):$(OBJ)
@$(CC) $^ -o $@
@echo "链接 $^ 成 $@"
%.o:%.c
@$(CC) -c $<
@echo "编译 $< 成 $@"
.PHONY:clean
clean:
$(RM) $(OBJ) $(BIN)
.PHONY:test
test:
@echo $(SRC)
@echo $(OBJ)
这样就直观很多了:
6.10 Makefile 扩展语法汇总
BIN=proc.exe # 定义变量
CC=gcc
#SRC=$(shell ls *.c) # 采⽤shell命令⾏⽅式,获取当前所有.c⽂件名
SRC=$(wildcard *.c) # 或者使⽤ wildcard 函数,获取当前所有.c⽂件名
OBJ=$(SRC:.c=.o) # 将SRC的所有同名.c 替换 成为.o 形成⽬标⽂件列表
LFLAGS=-o # 链接选项
FLAGS=-c # 编译选项
RM=rm -f # 引⼊命令
$(BIN):$(OBJ)
@$(CC) $(LFLAGS) $@ $^ # $@:代表⽬标⽂件名。 $^: 代表依赖⽂件列表
@echo "linking ... $^ to $@"
%.o:%.c # %.c 展开当前⽬录下所有的.c。 %.o: 同时展开同名.o
@$(CC) $(FLAGS) $< # %<: 对展开的依赖.c⽂件,⼀个⼀个的交给gcc。
@echo "compling ... $< to $@" # @:不回显命令
.PHONY:clean
clean:
$(RM) $(OBJ) $(BIN) # $(RM): 替换,⽤变量内容替换它
.PHONY:test # 测试$符号的作用
test:
@echo $(SRC)
@echo $(OBJ)