Makefile概述
0.说明
此系列是阅读陈皓老师《跟我一起学Makefile》时所做的笔记,包括自己的理解和补充内容。
1.Makefile与make命令介绍
一个庞大的C/C++程序项目由多个.c/.cpp文件和多个.h/.hpp文件组成。在Windows中有VS以及QtCreator等IDE可以帮我们组织编译这些代码,但是在Linux下一般没有IDE,这个时候就需要用到Makefile来定义整个工程的编译规则,即Makefile用来说明每一个文件编译的顺序,编译方法等。
make是一个来解释Makefile中的语句命令的工具,你可以理解成它是Makefile的编译器,其实VS等IDE的内部也是用这个工具来执行Makefile的。Makefile带来的“自动编译”,只要Makelfile写好后,一条make命令就可以完成整个工程的编译。
2.程序的编译(compile)和链接(link)
不管是C还是C++,当我们对这样的程序进行编译时,都要经过编译和链接这两个步骤。其中编译将源文件编译成中间目标文件(在Windows下是.obj文件,Linux下是.o文件),编译器编译时会将所有的源文件以每个文件为单位依次编译(这也简化了编译器的设计),因此一般来说每一个源文件都应该对应一个中间目标文件。编译时编译器关注的是代码语法格式的正确以及函数与变量声明的正确,只要这些没问题,编译器就能编译出中间目标文件。
链接的时候实际上是把上一步编译生成的中间目标文件当作输入,然后链接生成一个可执行程序(例如在Linux系统的C语言项目,第一步有多少个.c源文件,就编译生成多少个.o文件,最终这些.o文件链接生成一个可执行文件。)。链接时主要链接函数和全局变量。
3.3种链接属性:外链接,内链接,无链接
外链接就是所需的函数和变量可以在外部文件中找到,外链接本质就是跨文件访问,例如由extern关键字修饰的全局变量和函数,就是外链接链接的内容。
内链接与外链接刚好相反,即所需的函数和变量在本文件内就可以找到。对于内链接的函数和变量,一般用static关键字来修饰,一旦这些函数和全局变量用static修饰,则外部文件,即项目中的其他源文件代码是无法访问到这些函数和变量的,只有该函数和变量所在的源文件代码才可访问。static修饰的静态全局变量和函数都是内链接的。
无链接就是这个符号本身不参与链接,一般的局部变量(auto,static类型)的都是无链接的。
4.常见编译错误原因
通常编译器报错LinkError是因为链接程序时,链接器没有能够在中间目标文件中找到函数的实现。这个时候就需要指定函数的中间目标文件。
5.Makefile编译工程文件规则
- 如果这个工程没有被编译过,那么所有的源文件都要被编译和链接,然后生成可执行文件。
- 如果工程中某几个源文件被修改过,那么只编译修改过的文件,然后重新链接生成新的可执行文件。
- 如果工程中的头文件被修改了,那么引用了这个头文件的源文件都需要被重新编译,然后链接生成新的可执行文件。
6.一个简单的Makefile示例
首先是Makefile的编写规则:
target:prerequisites...
command
...
...
其中target就是一个目标文件,它可以是中间目标文件,也可以是最终的可执行文件。
prerequisites是生成target所需要的文件(如果目标是.o文件,那么依赖文件就是其相应的.c和.h文件。如果目标是可执行文件,一般依赖文件就是其对应的若干个.o文件),也就是target的依赖文件,它们的生成规则由下面的command定义(一般一个command将一个源文件编译成一个中间目标文件)。与上面的编译工程文件规则相对应,只要有一个源文件被修改了,command所定义的命令就会被执行,这个源文件就会被重新编译。
下面来举个例子,这里假设我们的工程中有2个头文件和3个c文件:
add.h:
#include<stdio.h>
int add(int a,int b);
add.c:
#include"add.h"
int add(int a,int b)
{
return a+b;
}
multiply.h
#include<stdio.h>
int multiply(int a,int b);
multiply.c
#include"multiply.h"
int multiply(int a,int b)
{
return a*b;
}
main.c
#include"add.h"
#include"multiply.h"
int main(int argc,char**argv)
{
int a,b;
printf("现在测试加法,请输入两个整数:\n");
scanf("%d %d",&a,&b);
printf("%d + %d = %d\n",a,b,add(a,b));
printf("现在测试乘法,请输入两个整数:\n");
scanf("%d %d",&a,&b);
printf("%d x %d = %d\n",a,b,multiply(a,b));
return 0;
}
从代码中可以看到,add完成了两个整数的加法运算,multiply完成了两个整数的乘法运算,最终在main.c里调用了这两个函数。那么Makefile参照上述的规则不难写出:
main:main.o add.o multiply.o
gcc -o main main.o add.o multiply.o
main.o:main.c add.h multiply.h
gcc -c main.c
add.o:add.c add.h
gcc -c add.c
multiply.o:multiply.c multiply.h
gcc -c multiply.c
clean:
rm main
rm *.o
其中的main就是最终生成的可执行文件,冒号后的三个.o就是生成main所需要依赖的目标文件,而下一行的gcc -o main main.o add.o multiply.o则指定了生成main的规则,这里要注意Makefile的语法格式,即每一个指令行前面是一个tab键。
下面的main.o,add.o,multiply.o与main类似,只是生成中间目标文件和最终目标文件的gcc命令所带的参数不一样,关于gcc的使用这里不再赘述。
make前后的文件对比和make的编译过程如下:
merlin@Ubuntu1804:~/learn_makefile$ ls
add.c add.h main main.c Makefile multiply.c multiply.h
merlin@Ubuntu1804:~/learn_makefile$ make
gcc -c main.c
gcc -c add.c
gcc -c multiply.c
gcc -o main main.o add.o multiply.o
merlin@Ubuntu1804:~/learn_makefile$ ls
add.c add.o main.c Makefile multiply.h
add.h main main.o multiply.c multiply.o
最终测试执行结果如下:
merlin@Ubuntu1804:~/learn_makefile$ ./main
现在测试加法,请输入两个整数:
10 20
10 + 20 = 30
现在测试乘法,请输入两个整数:
10 20
10 x 20 = 200