程序的 编译 和 链接

要先总结 ​​make ​​​和 ​​makefile​​,就需要先了解下面这个过程:

make 和 makefile 的关系_可执行文件


  1. 预编译:也叫预处理,进行一些文本替换工作,比如将 ​​#define​​ 定义的内容,在代码中进行替换;
  2. 编译:将预处理得到的代码,进行词法分析、语法分析、中间代码……;如果是在Windows下,中间代码就是 ​​.obj​​​ 文件;在Linux系统下,中间代码就是 ​​.o​​ 文件;
  3. 汇编:将编译得到的汇编代码,通过汇编程序得到 0 和 1 机器语言;
  4. 链接:链接各种静态链接库和动态链接库得到可执行文件。

 

make 和 makefile 能干啥?

一个工程,那么多源文件,一堆的 ​​cpp ​​​和 ​​h ​​文件,怎么编译啊?编译一个大型工程,如果Rebuild可能就需要好几个小时,甚至十几个小时,那我们就可能要问了。


  1. 如何像VS那样,一键就能编译整个项目?
  2. 如何修改了哪个文件,就编译修改的那个文件,而不是重新编译整个工程?

好吧,​​make ​​​和 ​​makefile ​​​就能搞定这些。​​makefile ​​​定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 ​​makefile ​​​就像一个Shell脚本一样,其中也可以执行操作系统的命令。​​makefile ​​​带来的好处就是——“自动化编译”,一旦写好,只需要一个 ​​make ​​命令,整个工程完全自动编译,极大的提高了软件开发的效率。

​make ​​​是一个命令工具,是一个解释 ​​makefile ​​​中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的​​make​​​,Visual C++的​​nmake​​​,Linux 下 GNU 的 ​​make​​​。可见,​​makefile ​​​都成为了一种在工程方面的编译方法。make 命令执行时,需要一个 ​​makefile​​​ 文件,以告诉 ​​make ​​命令需要怎么样的去编译和链接程序。

现在,应该明白了吧。​​make ​​​是一个命令,用来解析 ​​makefile ​​​文件;​​makefile ​​​是一个文件,用来告诉 ​​make ​​命令,如何编译整个工程,生成可执行文件。再打个比方:

导演 == make

剧本 == makefile

演员 == MAKE调用的外部命令,如编译器、链接器等

电影 == 生成的程序

 

解决问题举例

怎么就出现了make这个东西了呢?还记得你入门C语言时,写下的Hello World程序么?

#include <stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}


当你在终端中输入 ​​gcc HelloWorld.c ​​​命令时,就会生成一个 ​​a.out ​​​文件(如果不用 -o 参数指定输出文件名的话,默认为 a.out),然后就可以神奇的使用 ​​./a.out ​​​执行该文件,打印出了 ​​Hello World​​​。这是一件让初学者兴奋的事情。问题来了,现在就仅仅是一个 ​​HelloWorld.c ​​文件,如果有多个代码文件,而多个代码文件之间又存在引用关系,这个时候,该如何去编译生成一个可执行文件呢?比如现在有一下源文件:

add.h

add.c

sub.h

sub.c

mul.h

mul.c

divi.h

divi.c

main.c

这些代码文件的定义分别如下:

add.h 文件

#ifndef _ADD_H_
#define _ADD_H_
int add(int a, int b);
#endif


add.c 文件

#include "add.h"
int add(int a, int b)
{
return a + b;
}


sub.h 文件

#ifndef _SUB_H_
#define _SUB_H_
int sub(int a, int b);
#endif


sub.c 文件

#include "sub.h"
int sub(int a, int b
{
return a - b;
}


mul.h 文件

#ifndef _MUL_H_
#define _MUL_H_
int mul(int a, int b);
#endif


mul.c 文件

#include "mul.h"
int mul(int a, int b)
{
return a * b;
}


divi.h 文件

#ifndef _DIVI_H_
#define _DIVI_H_
int divi(int a, int b);
#endif


divi.c 文件

#include "divi.h"
int divi(int a, int b)
{
if (b == 0)
{
return 0;
}
return a / b;
}


main.c 文件

#include <stdio.h>
#include "add.h"
#include "sub.h"
#include "mul.h"
#include "divi.h"

int main()
{
int a = 10;
int b = 2;

printf("%d + %d = %d\n", a, b, add(a, b));
printf("%d - %d = %d\n", a, b, sub(a, b));
printf("%d * %d = %d\n", a, b, mul(a, b));
printf("%d / %d = %d\n", a, b, divi(a, b));
return 0;
}


你也看到了,在 ​​main.c​​ 中要引用这些文件,那现在如何编译,生成一个可执行文件呢?

 

最笨的解决方法

最笨的解决方法就是依次编译所有文件,生成对应的 ​​.o​​ 目标文件。参考如下:

$ gcc -c sub.c -o sub.o
$ gcc -c add.c -o add.o
$ gcc -c sub.c -o sub.o
$ gcc -c mul.c -o mul.o
$ gcc -c divi.c -o divi.o
$ gcc -c main.c -o main.o


然后再使用如下命令对所生成的单个目标文件进行链接,生成可执行文件。

$ gcc -o main add.o sub.o mul.o divi.o main.o


然后就可以得到一个可执行程序 ​​main​​​,可以直接使用 ​​./main​​ 进行运行。还不错,虽然过程艰辛,至少也可以得到可执行程序。那么有没有比这更简单的方法呢?如果一个项目,几千个文件,这么写下去,还不得累死人啊。办法是有的,我接着总结。

 

使用makefile文件

使用上面那种最笨的办法,效率是非常低得,当添加新的文件,或者修改现有文件时,维护起来也是非常难得。基于此,现在就来说说使用 ​​makefile​​ 文件来搞定这一切。

关于什么是 ​​makefile​​​,在文章的开头我就已经总结了,至于它和 ​​make​​​ 的关系,在文章的开头也说的非常清楚了,现在就来看看如何使用 ​​makefile​​​ 来完成上面同样的任务,生成一个 ​​main​​ 的可执行文件。

#target:dependency-file

main:main.o add.o sub.o mul.o divi.o

    gcc -o main main.o add.o sub.o mul.o divi.o

main.o:main.c add.h sub.h mul.h divi.h

    gcc -c main.c -o main.o

add.o:add.c add.h

    gcc -c add.c -o add.o

sub.o:sub.c sub.h

    gcc -c sub.c -o sub.o

mul.o:mul.c mul.h

    gcc -c mul.c -o mul.o

divi.o:divi.c divi.h

    gcc -c divi.c -o divi.o

clean:

    rm -f *.o

上面就是 ​​makefile​​​ 文件的内容,对于 ​​makefile​​ 的内容的编写规则,这里先不说。

现在你可以在 ​​makefile​​​ 的同目录下执行 ​​make​​​ 命令,然后就可以看到生成了一堆 ​​.o​​​ 目标文件,还有那个可执行的 ​​main​​​ 文件;接着运行 make clean,那些 ​​.o​​ 文件就全部被删除了。为什么是这样?好了,你先照着做一遍吧。

 

makefile文件编写规则

上面只是给出了一个简单的 ​​makefile​​​ 文件,你肯定好奇这个 ​​makefile​​ 的书写规则是什么样子的?

makefile 的规则大体上就是以下格式:

​target​​​ 是一个目标文件,可以是 ​​Object File​​​(.o文件),也可以使最终的执行文件,而 ​​dependency-file​​​ 是生成对应 ​​target​​ 所需要依赖的文件或者其它的 target,command 就是最终由 make 执行的命令。

上面说了一段话,简短而言就是:生成一个 target,需要依赖的文件,而使用命令来将依赖文件生成对应的 target 的规则,是在 command 中定义的。如果 ​​dependency-file​​​ 中有一个或者多个文件比 ​​target​​​ 文件要新的话,​​command​​​ 所定义的命令就会被执行,这就是 ​​makefile​​​ 的规则,也就是 ​​makefile​​ 最核心的内容。

​makefile​​​ 文件中可以定义变量,可以使用函数,还有各种判断,内容繁多,这里就不一一总结了,更详细的介绍,可以看看大牛陈皓的系列博客《​​跟我一起写makefile​​》。