背景:
一个大型工程中的源文件不计其数,它们按照不同的功能分别放到相对应的文件中去,当我们想去编译某些特定的文件的时候,那我们可以利用来书写 makefile 来制定编译规则。比如:什么文件需要先编译,什么文件需要后编译,什么文件需要重新编译等等一些功能我们都可以通过来书写 makefile 文档来完成相应的功能,这样我们的工作效率就大大提高了,而且相对而言比较省力,不用一行一行的去敲命令,可以更好的帮助我们完成工作。
理解:
make 和 makefile
两个需要搭配使用,make 是命令工具,是一个解释 makefile 中指令的工具,而 makefile 它是一个集合了多个命令的文本文档。
makefile 的书写:
首先利用 vi 打开 makefile 。(文档名必须为 makefile/Makefile )
(注: 如果当前目录下没有makefile 文档,那么就相当于新建一个 makefile 文档)如下图:
注:我这里是配置过的 VI 编辑器,正常情况下,可能没有这么好看,实用,但是不影响我们书写的东西。我们需要进入 vi 编辑器的插入模式(i)进行书写。
makefile 的书写规则:
目标对象:依赖对象
【tab】被执行的命令
(注:必须为 tab 键,不能用空格进行替换,语法不支持.)
例子:假如我们想要编译以下的代码。
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("hello makefile!\n");
return 0;
}
上图是我们需要被编译的代码,下面是书写的 makefile 文档。
我们来仔细说一下这个例子,首先,我们知道 IDE 在 “编译” 我们的代码的时候会经过以下几个步骤:1.预处理。 2.编译。 3.汇编。4.链接。每经过一个步骤都会生成一个目标文件。我们之所以将它写的这么详细是因为我想说明一下 make 的执行过程。
make的原理:
1.当执行 make 命令时,它会在当前目录下寻找 makefile / Makefile文件。
2. 如果找到,它会找文件中的第一个目标文件,在上面的例子中,他会找到“ main ”这个文件,并把这个文件作为最终的目标文件,所做的一切都是朝它去。
3. 如果 main 文件不存在,或是 main 所依赖的后面 main.o 文件的文件修改时间要比 main 这个文件新(可以用 touch 测试),那么,他就会执行后面所定义的命令来生成 main 这个文件。这样做的目的是:所生成的文件是由最新的源文件生成的。
4. 如果 main 所依赖的 main.o 文件不存在,那么make会在当前文件中找目标为 main.o 文件的依赖性,如果找到,则再根据那一个规则生成 main.o 文件,然后依此类推的向下找。(这有点像一个堆栈的过程,递归进行查找目标对象所以依赖的项,并生成。)
5. 这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。
6. 在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make 根本不理。
7. make 只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不存在,那它还是不工作。
具体过程如下:
make 执行之后会生成很多的中间文件,我们需要删除清理掉这些中间文件,我们应该怎么做呐?一个一个的删除吗? 我们的回答当然是 “ 不 ”,现在我们只是写了一个简单的程序,如果我们所写的程序比较大,每次生成编译一次,就要删除一次,中间所产生的文件可能有成千上万个,那么我们这样做的效率也就太慢了,费时费力,为了解决这个问题,我们有一个办法就是声明一个 “伪对象” 。
.PHONY::声明伪对象
伪对象:不管目标对象是否存在,不管目标对象是否最新,每次都要重新执行生成规则
这样,每次不管怎么样都会执行 rm 指令。
最后再介绍一下 makefile 编写过程常用的技巧:
makefile 的变量的定义与使用:(这个有点像 C/C++ 中的宏定义与使用)
- 变量的定义与使用:
第 1,2,3 行分别定义了变量gc,he.c,he,并且使用了直接赋值的方式进行了赋值,第5,6行利用了 “ $( 变量名) ” 这样的形式使用了变量。
makefile 变量的四种赋值方式:
- 直接赋值 ====> " := "
- 递归赋值 ====> " = "
- 条件赋值 ====> " ?= "
- 追加赋值 ====> " += "
下面利用例子来说一下它们有什么区别:
- 直接赋值:
输出为:
x => poo
y => xx - 递归赋值:
输出为:
x => aa
y => aabb
由结果可知,我们直接赋值和递归赋值是不一样的。区别是:
直接赋值:将当前所给的值直接展开赋值给变量,类似于 C/C++中的赋值。
递归赋值:当某一个变量在的定义的时候。引用了其他变量的值,那么它就会去递归的展开寻找这个值,直到找到这个变量的最新定义才停止,找到之后,然后,再赋值给变量。 - 追加赋值:
输出为:
x => poo p
y => bbb
追加赋值:由例子可知,为已经赋值的变量增加新值,如果,变量原先没有定义那么它相当于直接赋值。 - 条件赋值:
输出为:
x => haha
y => hello
由此可知,如果变量没有被初始化,那么它的值就是条件赋值所提供的值,如果变量事先被初始化了,那么它的值就是初始化的值。(这一点有点像我们 C/C++ 的条件编译)
预定义变量
$@ : 目标对象
$^ :所有的依赖对象
$< : 依赖对象的第一个
wildcard(通配符): 获取指定目录下的文件名
patsubst : 字符串替换
使用例子: