1. 什么是 Makefile
Makefile
文件描述了 Linux
系统下 C/C++
工程的编译规则,它用来自动化编译 C/C++
项目。一旦写编写好 Makefile
文件,只需要一个 make
命令,整个工程就开始自动编译,不再需要手动执行 GCC
命令。一个中大型 C/C++
工程的源文件有成百上千个,它们按照功能、模块、类型分别放在不同的目录中,Makefile
文件定义了一系列规则,指明了源文件的编译顺序、依赖关系、是否需要重新编译等。
Makefile
可以简单的认为是一个工程文件的编译规则,描述了整个工程的编译和链接等规则。其中包含了那些文件需要编译,那些文件不需要编译,那些文件需要先编译,那些文件需要后编译,那些文件需要重建等等。编译整个工程需要涉及到的,在 Makefile
中都可以进行描述。换句话说,Makefile
可以使得我们的项目工程的编译变得自动化,不需要每次都手动输入一堆源文件和参数。
2. 为什么要使用 Makefile
以 Linux
下的 C
语言开发为例来具体说明一下,多文件编译生成一个文件,编译的命令如下所示:
gcc -o outfile name1.c name2.c ...
outfile
要生成的可执行程序的名字,nameN.c
是源文件的名字。这是我们在 Linux
下使用 gcc
编译器编译 C
文件的例子。如果我们遇到的源文件的数量不是很多的话,可以选择这样的编译方式。如果源文件非常的多的话,就会遇到下面的这些问题。
2.1 编译的时候需要链接库的的问题
下面列举了一些需要我们手动链接的标准库:
name1.c
用到了数学计算库math
中的函数,我们得手动添加参数-lm
;name4.c
用到了小型数据库SQLite
中的函数,我们得手动添加参数-lsqlite3
;name5.c
使用到了线程,我们需要去手动添加参数-lpthread
;
因为有很多的文件,还要去链接很多的第三方库。所以在编译的时候命令会很长,并且在编译的时候我们可能会涉及到文件链接的顺序问题,所以手动编译会很麻烦。
如果我们学会使用 Makefile
就不一样了,它会彻底简化编译的操作。把要链接的库文件放在 Makefile
中,制定相应的规则和对应的链接顺序。这样只需要执行 make
命令,工程就会自动编译,省略掉手动编译中的参数选项和命令,非常的方便。
2.2 编译大的工程会花费很长的时间
Makefile
支持多线程并发操作,会极大的缩短我们的编译时间,并且当我们修改了源文件之后,编译整个工程的时候,make
命令只会编译我们修改过的文件,没有修改的文件不用重新编译,也极大的解决了我们耗费时间的问题。
并且文件中的 Makefile
只需要完成一次,一般我们只要不增加或者是删除工程中的文件,Makefile
基本上不用去修改,编译时只用一个 make
命令。为我们提供了极大的便利,很大程度上提高编译的效率。
3. Makefile 规则
它的规则主要是两个部分组成,分别是依赖的关系和执行的命令,其结构如下所示:
targets : prerequisites
command
或者
targets : prerequisites; command
command
相关说明如下:
targets
:规则的目标,是必须要有的,可以是Object File
(一般称它为中间文件),也可以是可执行文件,还可以是一个标签;prerequisites
:是我们的依赖文件,要生成targets
需要的文件或者是目标。可以是多个,用空格隔开,也可以是没有;command
:make
需要执行的命令(任意的shell
命令)。可以有多条命令,每一条命令占一行;
如果 command
太长, 可以用 \
作为换行符。
注意:我们的目标和依赖文件之间要使用冒号分隔开,命令的开始一定要使用
Tab
键,不能使用空格键。
简单的概括一下Makefile
中的内容,它主要包含有五个部分,分别是:
3.1 显式规则
显式规则说明了,如何生成一个或多的的目标文件。这是由 Makefile
的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。
3.2 隐晦规则
由于我们的 make
命名有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写 Makefile
,这是由 make
命令所支持的。
3.3 变量的定义
在 Makefile
中我们要定义一系列的变量,变量一般都是字符串,这个有点像 C 语言中的宏,当 Makefile
被执行时,其中的变量都会被扩展到相应的引用位置上。
3.4 文件指示
其包括了三个部分,一个是在一个 Makefile
中引用另一个 Makefile
,就像 C 语言中的 include
一样;另一个是指根据某些情况指定 Makefile
中的有效部分,就像 C 语言中的预编译 #if
一样;还有就是定义一个多行的命令。
3.5 注释
Makefile
中只有行注释,和 UNIX
的 Shell
脚本一样,其注释是用 #
字符,如果你要在你的 Makefile
中使用 #
字符,可以用反斜框进行转义,如: \#
。
3.6 规则中的通配符
*
表示任意一个或多个字符?
表示任意一个字符[...]
[abcd] 表示 a,b,c,d中任意一个字符, [^abcd]表示除 a,b,c,d 以外的字符, [0-9] 表示 0~9中任意一个数字~
表示用户的 home 目录
4. Makefile 示例
make
安装:
sudo apt-get install make
make -v # 查看是否安装成功
main.cpp
代码:
#include <iostream>
int main()
{
std::cout << "hello,world" << std::endl;
return 0;
}
通过下面的例子来具体使用一下 Makefile
的规则,Makefile
文件中添代码如下:
main: main.cpp
g++ main.cpp -o main
其中 main
是的目标文件,也是我们的最终生成的可执行文件。依赖文件就是 main.cpp
源文件,重建目标文件需要执行的操作是 g++ main.cpp -o main
。这就是 Makefile
的基本的语法规则的使用。
使用
Makefile
的方式:首先需要编写好Makefile
文件,然后在 shell 中执行make
命令,程序就会自动执行,得到最终的目标文件。
wohu@ubuntu:~/cpp/demo$ ls
main.cpp `Makefile`
wohu@ubuntu:~/cpp/demo$ make
g++ main.cpp -o main
wohu@ubuntu:~/cpp/demo$ ls
main main.cpp `Makefile`
wohu@ubuntu:~/cpp/demo$ ./main
hello,world
wohu@ubuntu:~/cpp/demo$
如果命令的开始使用的是空格键,那么会报错
Makefile:2: *** missing separator. Stop.
Makefile
:2 表示第二行错误,应该以 Tab
开始。
5. Makefile 流程
当我们在执行 make
条命令的时候,make
就会去当前文件下找要执行的编译规则,也就是 Makefile
文件。我们编写 Makefile
的时可以使用的文件的名称 GNUMakefile
、makefile
、Makefile
,make
执行时回去寻找 Makefile
文件,找文件的顺序也是这样的。
推荐使用 Makefile
(一般在工程中都这么写,大写的会比较的规范)。如果文件不存在,make
就会给我们报错,提示:
make: *** No targets specified and no `Makefile` found. Stop.
在 Makefile
中添加下面的代码:
main: main.o name.o greeting.o
g++ main.o name.o greeting.o -o main
main.o: main.cpp
g++ -c main.cpp -o main.o
name.o: name.cpp
g++ -c name.cpp -o name.o
greeting.o: greeting.cpp
g++ -c greeting.cpp -o greeting.o
在我们编译项目文件的时候,默认情况下,make
执行的是 Makefile
中的第一规则(Makefile
中出现的第一个依赖关系),此规则的第一目标称之为“最终目标”或者是“终极目标”。
在 shell
命令行执行的 make
命令,就可以得到可执行文件 main
和中间文件 main.o
、name.o
和 greeting.o
,main
就是我们要生成的最终文件。
通过 Makefile
我们可以发现,目标 main
在 Makefile
中是第一个目标,因此它就是 make
的终极目标,当修改过任何文件后,执行 make
将会重建终极目标 main
。
它的具体工作顺序是:当在 shell
提示符下输入 make
命令以后。 make
读取当前目录下的 Makefile
文件,并将 Makefile
文件中的第一个目标作为其执行的“终极目标”,开始处理第一个规则(终极目标所在的规则)。
在我们的例子中,第一个规则就是目标 main
所在的规则。规则描述了 main
的依赖关系,并定义了链接 .o
文件生成目标 main
的命令;make
在执行这个规则所定义的命令之前,首先处理目标 main
的所有的依赖文件(例子中的那些 .o
文件)的更新规则(以这些 .o
文件为目标的规则)。
对这些 .o
文件为目标的规则处理有下列三种情况:
- 目标
.o
文件不存在,使用其描述规则创建它; - 目标
.o
文件存在,目标.o
文件所依赖的 “.cpp” 源文件 “.h” 文件中的任何一个比目标.o
文件“更新”(在上一次 make 之后被修改),则根据规则重新编译生成它; - 目标
.o
文件存在,目标.o
文件比它的任何一个依赖文件(“.c” 源文件、“.h” 文件)“更新”(它的依赖文件在上一次 make 之后没有被修改),则什么也不做;
通过上面的更新规则我们可以了解到中间文件的作用,也就是编译时生成的 .o
文件。作用是检查某个源文件是不是进行过修改,最终目标文件是不是需要重建。
我们执行 make
命令时,只有修改过的源文件或者是不存在的目标文件会进行重建,而那些没有改变的文件不用重新编译,这样在很大程度上节省时间,提高编程效率。