使用 GCC 的命令行进行程序编译在单个文件下是比较方便的,当工程中的文件逐渐增多,甚至变得十分庞大的时候,使用 GCC 命令编译就会变得力不从心。这种情况下我们需要借助项目构造工具 make 帮助我们完成这个艰巨的任务。 make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Visual C++的nmake,QtCreator的qmake等。
make工具在构造项目的时候需要加载一个叫做makefile的文件,makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。
makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
makefile文件有两种命名方式 makefile 和 Makefile,构建项目的时候在哪个目录下执行构建命令 make这个目录下的 makefile 文件就会别加载,因此在一个项目中可以有多个 makefile 文件,分别位于不同的项目目录中。
1. 规则
规则的基本格式为:
# 每条规则的语法格式:
target1,target2...: depend1, depend2, ...
command
......
......
每条规则由三个部分组成分别是目标(target), 依赖(depend)和命令(command)。
-
命令(command): 当前这条规则的动作,一般情况下这个动作就是一个 shell 命令
- 例如:通过某个命令编译文件、生成库文件、进入目录等。
- 动作可以是多个,每个命令前必须有一个Tab缩进并且独占占一行。
-
依赖(depend): 规则所必需的依赖条件,在规则的命令中可以使用这些依赖。
- 例如:生成可执行文件的目标文件(*.o)可以作为依赖使用
- 如果规则的命令中不需要任何依赖,那么规则的依赖可以为空
- 当前规则中的依赖可以是其他规则中的某个目标,这样就形成了规则之间的嵌套
- 依赖可以根据要执行的命令的实际需求, 指定很多个
-
目标(target): 规则中的目标,这个目标和规则中的命令是对应的
- 通过执行规则中的命令,可以生成一个和目标同名的文件
- 规则中可以有多个命令, 因此可以通过这多条命令来生成多个目标, 所有目标也可以有很多个
- 通过执行规则中的命令,可以只执行一个动作,不生成任何文件,这样的目标被称为伪目标
例子:
# 举例: 有源文件 a.c b.c c.c head.h, 需要生成可执行程序 app
################# 例1 #################
app:a.c b.c c.c
gcc a.c b.c c.c -o app
################# 例2 #################
# 有多个目标, 多个依赖, 多个命令
app,app1:a.c b.c c.c d.c
gcc a.c b.c -o app
gcc c.c d.c -o app1
################# 例3 #################
# 规则之间的嵌套
app:a.o b.o c.o
gcc a.o b.o c.o -o app
# a.o 是第一条规则中的依赖
a.o:a.c
gcc -c a.c
# b.o 是第一条规则中的依赖
b.o:b.c
gcc -c b.c
# c.o 是第一条规则中的依赖
c.o:c.c
gcc -c c.c
2. 工作原理
2.1 规则的执行
**在调用 make 命令编译程序的时候,make 会首先找到 Makefile 文件中的第 1 个规则,分析并执行相关的动作。**但是需要注意的是,好多时候要执行的动作(命令)中使用的依赖是不存在的,如果使用的依赖不存在,这个动作也就不会被执行。
对应的解决方案是先将需要的依赖生成出来,我们就可以在makefile中添加新的规则,将不存在的依赖作为这个新的规则中的目标,当这条新的规则对应的命令执行完毕,对应的目标就被生成了,同时另一条规则中需要的依赖也就存在了。
这样,makefile中的某一条规则在需要的时候,就会被其他的规则调用,直到makefile中的第一条规则中的所有的依赖全部被生成,第一条规则中的命令就可以基于这些依赖生成对应的目标,make 的任务也就完成了。
# makefile
# 规则之间的嵌套
# 规则1
app:a.o b.o c.o
gcc a.o b.o c.o -o app
# 规则2
a.o:a.c
gcc -c a.c
# 规则3
b.o:b.c
gcc -c b.c
# 规则4
c.o:c.c
gcc -c c.c
在这个例子中,如果执行 make 命令就会根据这个 makefile 中的4条规则编译这三个源文件。在解析第一条规则的时候发现里边的三个依赖都是不存在的,因此规则对应的命令也就不能被执行。
**当依赖不存在的时候,make就是查找其他的规则,看哪一条规则是用来生成需要的这个依赖的,找到之后就会执行这条规则中的命令。**因此规则2, 规则3, 规则4里的命令会相继被执行,当规则1中依赖全部被生成之后对应的命令也就被执行了,因此规则1的目标被生成,make工作结束。
- 知识点拓展:
如果想要执行 makefile 中非第一条规则对应的命令, 那么就不能直接 make, 需要将那条规则的目标也写到 make的后边, 比如只需要执行规则3中的命令, 就需要: make b.o。
2.2 文件的时间戳
make 命令执行的时候会根据文件的时间戳判定是否执行makefile文件中相关规则中的命令。
-
目标时间戳 > 所有依赖的时间戳, 如果执行 make 命令的时候检测到规则中的目标和依赖满足这个条件, 那么规则中的命令就不会被执行。(目标生成时间点在依赖修改时间点后面)
-
当依赖文件被更新了, 文件时间戳也会随之被更新, 这时候 目标时间戳 < 某些依赖的时间戳, 在这种情况下目标文件会通过规则中的命令被重新生成。
-
如果规则中的目标对应的文件根本就不存在, 那么规则中的命令肯定会被执行。
# makefile
# 规则之间的嵌套
# 规则1
app:a.o b.o c.o
gcc a.o b.o c.o -o app
# 规则2
a.o:a.c
gcc -c a.c
# 规则3
b.o:b.c
gcc -c b.c
# 规则4
c.o:c.c
gcc -c c.c
根据上文的描述, 先执行 make 命令,基于这个 makefile 编译这几个源文件生成对应的目标文件。然后**再修改例子中的 a.c, 再次通过make编译这几个源文件,那么这个时候先执行规则2更新目标文件a.o, 然后再执行规则1更新目标文件app,其余的规则是不会被执行的**
2.3 自动推导
假设本地项目目录中有以下几个源文件:
$ tree
.
├── add.c
├── div.c
├── head.h
├── main.c
├── makefile
├── mult.c
└── sub.c
目录中 makefile 文件内容如下
# 这是一个完整的 makefile 文件
calc:add.o div.o main.o mult.o sub.o
gcc add.o div.o main.o mult.o sub.o -o calc
通过make构建项目:
$ make
cc -c -o add.o add.c
cc -c -o div.o div.c
cc -c -o main.o main.c
cc -c -o mult.o mult.c
cc -c -o sub.o sub.c
gcc add.o div.o main.o mult.o sub.o -o calc
我们可以发现上边的 makefile 文件中只有一条规则, 依赖中所有的 .o文件在本地项目目录中是不存在的, 并且也没有其他的规则用来生成这些依赖文件, 这时候 make 会使用内部默认的构造规则先将这些依赖文件生成出来, 然后在执行规则中的命令, 最后生成目标文件 calc。