什么是Makefile?
Makefile可以简单的理解成一个工程文件的编译规则。
Makefile文件描述了Linux系统下C/C++项目工程的编译规则,它的作用是用来自动化编译C/C++的项目。一旦我们编辑好Makefile文件,只需要一个make命令,整个项目工程就开始自动编译,而我们也不用手动去输入各种gcc/g++的指令。一个大型的C/C++项目中的源代码文件有成百上千个,它们按照功能、类型,模块存放在不同的目录中。Makefile文件定义了一系列的规则,指明了编译文件的先后顺序,依赖关系和是否需要重新编译等。
Makefile文件的好处
Makefile文件的好处就是带来了“自动化编译”,我们不需要再去手动的执行gcc命令去生成我们所需要的东西。一旦写好Makefile文件,只需要使用make命令,整个项目完全自动化编译,大大节省了软件项目开发的效率。
如何去制作一个Makefile文件
Makefile的命名
Makefile文件的文件名只能是Makefile或者是makefile,其他其余的名字make命令识别不了。
Makefile中的注释
Makefile文件中的注释是#号开头空格后书写注释文章的。
Makefile的规则
Makefile规则的构成
makefile的规则主要由三个部分组成,分别是所要达成的目的、依赖的关系和需要执行的命令,格式由下例所示:
targets : prerequisites
command
或者
targets : prerequisites; command
command
需要⚠️注意的是:
- targets:目标是必须要有的,可以是中间文件,也可以是可执行文件,还可以是一个标签。
- prerequisites:是我们的依赖关系,它是生成目标所需要的文件或者是目标。可以是多个,中间用空格隔开。
- command:是make需要执行的命令(任意的shell命令,也包括gcc指令)。可以有多条命令,一条命令占一行。如果命令太长可以用换行符 \ 去隔开。
最重要的一点便是: 命令的开始行要用tab键去隔开,而不是用空格键隔开。
编程演示:用Makefile文件去生成一个简单的小程序。
首先我写了6个文件,分别是 add.c sub.c mult.c div.c head.h main.c,这个小程序可以用我们自己定义的函数去进行加减乘除运算。
它们的代码片段分别如下:
add.c
#include <stdio.h>
#include "head.h"
int add(int a, int b)
{
return a+b;
}
sub.c
#include <stdio.h>
#include "head.h"
int subtract(int a, int b)
{
return a-b;
}
mult.c
#include <stdio.h>
#include "head.h"
int multiply(int a, int b)
{
return a*b;
}
div.c
#include <stdio.h>
#include "head.h"
double divide(int a, int b)
{
return (double)a/b;
}
head.h
#ifndef _HEAD_H
#define _HEAD_H
// 加法
int add(int a, int b);
// 减法
int subtract(int a, int b);
// 乘法
int multiply(int a, int b);
// 除法
double divide(int a, int b);
#endif
main.c
#include <stdio.h>
#include "head.h"
int main()
{
int a = 20;
int b = 12;
printf("a = %d, b = %d\n", a, b);
printf("a + b = %d\n", add(a, b));
printf("a - b = %d\n", subtract(a, b));
printf("a * b = %d\n", multiply(a, b));
printf("a / b = %f\n", divide(a, b));
return 0;
}
接下来我们将编写一个Makefile文件,文件名就是Makefile,文件内容如下
calcApp: add.c sub.c mult.c div.c main.c
gcc add.c sub.c mult.c div.c main.c -o calcApp
我们这个时候只需要在终端敲下make命令,就会自动编译生成calcApp文件。运行一下试试:
./calcApp
a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667
成功输出,演示结束。
Makefile的工作原理
原理一:
- 在执行命令之前,会先去检查规则中的依赖关系是否存在
- 如果存在,则执行命令
- 如果不存在,则向下检查其他的规则,看有没有其他规则的目标是生成这个规则的依赖的。如果找到了,则执行该规则中的命令。
之前我们编程演示的这个例子中,命令的依赖关系是存在的,所以就符合这个原理一中依赖关系存在的这一条。如果单单看这个例子,则不好去理解这个原理一,那么我们接下来去修改这个Makefile文件,让大家能更清楚的去理解一下这个原理一:
calcApp: add.o sub.o mult.o div.o main.o
gcc add.o sub.o mult.o div.o main.o -o calcApp
add.o: add.c
gcc -c add.c
sub.o: sub.c
gcc -c sub.c
mult.o: mult.c
gcc -c mult.c
div.o: div.c
gcc -c div.c
main.o: main.c
gcc -c main.c
修改过后的Makefile文件的功能和未修改的makefile的功能是一样的。都是生成calcApp这个可执行程序。
注意看这个Makefile文件。要执行第一条规则中的命令。我们是不是需要这些个 .o 依赖文件?
这种情况就是依赖关系不存在的情况,我们就需要先执行规则2、3、4、5,6得到这些依赖文件后再回过头执行规则1,得到我们这个makefile的目标。
这里就衍生出Makefile中特别重要的一条规则:
- Makefile中其他规则,都是为第一条规则去服务的。
原理二:
- 检测更新,在执行命令的过程中会去比较目标与所依赖文件的时间
- 如果所依赖文件的时间比目标的时间早,则代表这个目标已经是最新的了,不需要更新,也就不需要执行对应规则的命令。
- 如果所依赖文件的时间比目标的时间晚,则代表有依赖文件进行了更新,所以我们要去重新生成目标,所以要重新执行生成目标的命令。
刚刚我们只是重新修改了一下Makefile文件,还没有去执行,我们现在去执行一下:
nowcoder@nowcoder:~/Linux/lession07$ make
gcc -c add.c
gcc -c sub.c
gcc -c mult.c
gcc -c div.c
gcc -c main.c
gcc add.o sub.o mult.o div.o main.o -o calcApp
再去执行一次make命令:
nowcoder@nowcoder:~/Linux/lession07$ make
make: “calcApp”已是最新。
这一种情况,就是所依赖的文件比目标的文件早,make命令就会告诉你这个程序已是最新。
那我们去修改其中的一个文件试试?修改一下main.c文件,随便添加点什么,换行符都行。这个时候我们再去执行下make命令试试:
nowcoder@nowcoder:~/Linux/lession07$ make
gcc -c main.c
gcc add.o sub.o mult.o div.o main.o -o calcApp
nowcoder@nowcoder:~/Linux/lession07$
我们就会发现,makefile文件将我们的可执行程序更新了,这就对应原理二的第二种情况。
修改后的Makefile的好处
这个时候我们再去对比一下,修改前的Makefile文件和修改后的Makefile文件的区别到底在哪里呢?
区别其实就在于,在上述例子中我们更新了main.c这个文件,未修改前的Makefile就会将add.c sub.c 等所有文件都重新编译一遍,再链接成我们的这个可执行程序。修改后的Makefile文件只需要重新编译一下main.c这一个文件编译一下,然后和其他原来编译好的文件一起链接进去就行了。哪种写法更好,其实一看便知。当项目工程文件很大的时候,修改后的这种写法的好处一下就凸显出来了。在编译方面可以节约出很多的时间来。
回看我们刚刚写的Makefile文件其实会发现,其实写起来相当的麻烦,为了减少不必要的编译,一下子写了n多行代码。
有没有一种方法能使我们能够既减少不必要的编译又能减少我们所熟悉的代码量呢?当然有,
那就是我们接下来要讲的Makefile中的变量。
Makefile中的变量
Makefile中的变量分为预定义变量和自定义变量,预定义变量就是系统帮你定义好的,可以直接拿过来用,自定义变量顾名思义,就是自己定义的变量。那就先讲讲预定义变量吧,方便理解。
- 预定义变量:
- AR:归档维护程序的名称,默认值为ar
- CC:C 编译器的名称,默认值为cc
- CXX:C++ 编译器的名称,默认值为g++
- $@:目标的完整名称
- $^ :当前规则的所有依赖文件
- $< :当前规则的第一个依赖文件
其中 $@ $^ S< 这三个为自动变量,只能在规则的命令中使用。当我们指向make命令时,会自动将其替换成所需要的文件。
- 自定义变量
- 变量名=值
- 如何使用自变量: $(变量名)
我们用下面这个例子,来更好的去阐述下自定义变量的用法:
PS:记得先删除之前操作生成的.o文件和可执行程序,方便演示
# 创建自定义变量
target=calcapp
src=add.o sub.o mult.o div.o main.o
$(target):$(src)
cc $(src) -o $@
add.o: add.c
gcc -c add.c
sub.o: sub.c
gcc -c sub.c
mult.o: mult.c
gcc -c mult.c
div.o: div.c
gcc -c div.c
main.o: main.c
gcc -c main.c
修改好了,make后运行一下试试。
nowcoder@nowcoder:~/Linux/lession07$ make
gcc -c add.c
gcc -c sub.c
gcc -c mult.c
gcc -c div.c
gcc -c main.c
cc add.o sub.o mult.o div.o main.o -o calcapp
nowcoder@nowcoder:~/Linux/lession07$ ./calcapp
a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667
运行成功。仔细看,我们的Makefile文件是不是比刚刚要简洁一点了?有人问,可我还是觉得很麻烦啊,还是要写很多条规则,有没有办法将这些规则再简化一下,还能起到不重复生成以生成文件的效果。那么我想说当然可以,接下来我要讲的模式匹配,就可以解决这个问题。
Makefile中的通配符
讲到模式匹配,就得先讲一下Makefile中用到的通配符,因为在书写规则时经常用到。
- *号表示任意一个或多个字符
- ?号表示任意一个字符
- %号表示任意一个字符串
Makefile的模式匹配
由于我们的make命令有自动推导的能力,所以我们隐晦的书写一下规则时,make命令可以帮助我们自动替换或补全。下面看这个例子就知道了。
我们记得删除一下刚刚编译好的多余文件。再重新书写一下makefile:
# 创建自定义变量
target=calcapp
src=add.o sub.o mult.o div.o main.o
$(target):$(src)
cc $(src) -o $@
# 模式匹配 用%代替一个字符串
%.o: %.c
cc -c $<
修改完成,make后运行一下
nowcoder@nowcoder:~/Linux/lession07$ make
cc -c add.c
cc -c sub.c
cc -c mult.c
cc -c div.c
cc -c main.c
cc add.o sub.o mult.o div.o main.o -o calcapp
nowcoder@nowcoder:~/Linux/lession07$ ./calcapp
a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667
来我们再来看一下,是不是异常简洁,我们用了模式匹配了后,再不用写那么多规则了。make命令自动推导,用需要的字符串去替换了我们的通配符。瞬间我们的工作就少了一大半了。
这个时候还有人说,我们能不能再简单点再简单点!我不想写那么多啥啥啥 .o文件了,可不可以再让我懒一点!
我说可以,那我们再介绍一下Makefile的函数。
Makefile的函数
Makefile中的函数有很多,阿宋在这里就简单介绍两个我们这个小程序能用的到了两个。如果大家想要学习其他函数的话,请自行搜索,推荐去这个大佬的专栏下进一步加深对Makefile的理解与学习。
前话说完了,接下来开始这个小程序能用到的两个函数的学习。
获取匹配模式文件名函数: wildcard
函数的使用模式如下:
$(wildcard PATTERN)
这个函数的功能是获取指定目录下的制定文件列表。这个PATTERN在这里指代的就是某个目录或多个目录下对应的某种文件。不同目录直接我们可以去用空格隔开。
这个函数会返回给你匹配上的文件列表。文件与文件直接会帮你用空格隔开。
模式字符串替换函数:patsubst
函数的使用模式如下:
$(patsubst <pattern>,<replacement>,<text>)
这个函数的功能就是去text中的单词有没有符合pattern这个文件格式的,如果有,则将它替换成replacement这个格式的。
这个函数会返回给你符合replacement文件格式的文件列表,文件与文件之间用空格隔开。
现在我们介绍完了这两个函数了,我们用例子再去演示一下用法:
# 创建自定义变量
target=calcapp
src=$(wildcard ./*.c)
objs=$(patsubst %.c,%.o,$(src))
$(target):$(objs)
cc $(objs) -o $@
# 模式匹配 用%代替一个字符串
%.o: %.c
cc -c $<
我们来细细讲一下上面这个案例。
src这个变量用到了wildcard这个函数,我们在当前目录下去寻找所有的.c文件去作为这个src的值。
objs这个变量用到了patsubst这个函数,我们在src这个变量中去寻找有没有符合%.c的文件,如果有,将这些文件名全部改为%.o后作为objs的值。
至此我们的文件就修改完啦,make后运行一下看看:
nowcoder@nowcoder:~/Linux/lession07$ make
cc -c mult.c
cc -c main.c
cc -c add.c
cc -c div.c
cc -c sub.c
cc ./mult.o ./main.o ./add.o ./div.o ./sub.o -o calcapp
nowcoder@nowcoder:~/Linux/lession07$ ./calcapp
a = 20, b = 12
a + b = 32
a - b = 8
a * b = 240
a / b = 1.666667
同样的运行成功。有了这两个函数,我们就可以不用去写n多的.o文件啦。成功的将我们的需要写的代码量再减少了一番。
我们每次编译都会去生成很多的.o文件。有没有办法让我们去使用make命令然后再将它们清除呢?答案依旧是有,继续往下看。
Makefile的伪目标。
.PHONY: 这个目标的所有依赖被作为伪目标。伪目标是这样一个目标:当使用 make 命令行指定此目标时,这个目标所在的规则定义的命令、无论目标文件是否存在都会被无条件执行。
具体用法如下:
.PHONY: clean
clean:
rm *.o
我们给这个makefile文件多添加几行代码。退出文件后,执行命令 make clean,就可删除对应的.o文件
nowcoder@nowcoder:~/Linux/lession07$ make clean
rm *.o
nowcoder@nowcoder:~/Linux/lession07$ ls
add.c calcapp div.c head.h main.c Makefile Makefile1 Makefile2 Makefile3 Makefile4 mult.c redis-5.0.10 redis-5.0.10.tar.gz sub.c
nowcoder@nowcoder:~/Linux/lession07$
执行成功。