浅显易懂 Makefile 入门 (01)— 什么是Makefile、为什么要用Makefile、Makefile规则、Makefile流程如何实现增量编译

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 需要的文件或者是目标。可以是多个,用空格隔开,也可以是没有;
  • commandmake 需要执行的命令(任意的 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 中只有行注释,和 UNIXShell 脚本一样,其注释是用 # 字符,如果你要在你的 Makefile 中使用 # 字符,可以用反斜框进行转义,如: \#

3.6 规则中的通配符

  • * 表示任意一个或多个字符
  • ? 表示任意一个字符
  • [...] [abcd] 表示 a,b,c,d中任意一个字符, [^abcd]表示除 a,b,c,d 以外的字符, [0-9] 表示 0~9中任意一个数字
  • ~ 表示用户的 home 目录

4. Makefile 示例

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 的时可以使用的文件的名称 GNUMakefilemakefileMakefilemake 执行时回去寻找 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.oname.ogreeting.omain 就是我们要生成的最终文件。

通过 Makefile 我们可以发现,目标 mainMakefile 中是第一个目标,因此它就是 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 命令时,只有修改过的源文件或者是不存在的目标文件会进行重建,而那些没有改变的文件不用重新编译,这样在很大程度上节省时间,提高编程效率。

猜你喜欢

转载自blog.csdn.net/wohu1104/article/details/110905996
今日推荐