我们写完的源码是通过gcc编译、链接的但是如果每一次代码的改变都要重新编译,工程文件量少的话还好,当文件有几十几百甚至上千的话在Terminal输入一条条指令肯行是不现实的。这样就要引入一种工具:make,这个工具不光可以自动编译代码,还可以只对更改的代码进行编译和链接。
我们先做一个计算器的小工程,里面包含下面几个文件
代码很简单
#ifndef _CALCU_H #define _CALCU_H int calcu(int a, int b); #endifcalcu.h
#include "calcu.h" int calcu(int a, int b) { return (a+b); }calcu.c
#ifndef _INPUT_H #define _INPUT_H void input_int(int *a, int *b); #endifinput.h
#include <stdio.h> #include "input.h" void input_int(int *a,int *b) { printf("input two num:"); scanf("%d %d",a,b); printf("\r\n"); }input.c
#include <stdio.h> #include "input.h" #include "calcu.h" int main(int argc,char *argv[]) { int a,b,num; input_int(&a,&b); num = calcu(a,b); printf("%d + %d = %d\r\n",a,b,num); }main.c
我们想要编译文件的话,要执行gcc编译文件
gcc main.c calcu.c input.c -o main
因为工程一共有3个.c文件,编译的时候每个都要敲进去,那么对于大的工程来说,成百上千个工程,不可能一个个打进去的,并且基本上编译不可能一次就过,可定有错的地方,修改以后还要重新编译。所有文件包括那些编译过的还要重新编译,几个小时就又过去了。
所以这里引入Makefile,可以直接在Makefile里定义编译的过程然后通过make直接编译,并且编译的时候使用-c选项只编译不链接生成-o文件。用这个方法,在第一次编译文件后,后面修改了哪个文件,Makefile会比较哈个文件被修改过,只对修改过的文件进行编译。最后在对所有的.o文件进行链接。整个过程是在BASH里下面两条命令完成
gcc -c calcu.c input.c main.c gcc calcu.o input.o main.o -o main
-c为不链接生成obj文件,我们没有用-o选项指定文件名,新生成的obj文件名和原文件一样。
第二条指令为把所有.o文件链接,生成可执行文件,-o指定文件名main
Makefile的引入
上面的过程只能解决改变代码重新编译文件袋过程,还是要我们每次输入命令,并且如果修改的文件多了我们也会忘记哪个需要重新编译,哪个已经编译过了。这里使用Makefile,可以解决一下几种问题
- 如果工程没有被编译过,工程中所有的.c/.s文件都要被编译并链接为可执行程序
- 如果工程中个别源码被修改,只编译对应文件
- 如果工程文件的头文件被修改,所有引用这个头文件的文件重新编译并链接。
我们在工程目录下创建文件,文件名一定要是Makefile,一定注意首字母M为大写,里面内容如下
main:main.o calcu.o input.o gcc -o main main.o calcu.o input.o main.o:main.c gcc -c main.c calcu.o:calcu.c gcc -c calcu.c input.o:input.c gcc -c input.c clean: rm *.o rm main
要注意点是行首空格出来的地方要用Tab键,不要用空格!这时Makefile语法的基本要求。写完代码就可以直接用make命令编译代码了,如下图所示
在上面的过程中,make编译完成各个文件后,会直接对.o文件进行链接生成执行文件main。当我们对某个代码文件进行修改,比如讲main.c随便修改下,最后打印输入的地方加几个字符
int main(int argc,char *argv[]) { int a,b,num; input_int(&a,&b); num = calcu(a,b); printf("计算完毕: %d + %d = %d\r\n",a,b,num); //将该行修改一下
重新make一下对工程进行编译:
可以发现make只是将修改过的main.c进行重新编译然后再链接。
Makefile的基本语法
Makefile的基本语法很简单,是由一些咧规则组成:
对我们刚才写的Makefile来说,比如前两行
main:main.o calcu.o input.o gcc -o main main.o calcu.o input.o
意思就是我们的目标是main,他的构成依赖了三个.o文件,如果要更新目标main,就要先更新里面的依赖文件。如果依赖文件有更新,则目标必须更新,更新过程就是执行一遍下面的命令。如果我们是第一次编译,那么第一条规则中的main还不存在,就会直接执行,make会发现这个目标main是依赖与三个.o文件,就会接着往下去找这三个文件的规则并执行,待三个依赖文件都生成后会直接进行链接。如果重新被执行,他会通过时间戳进行比较,如果有新的文件更改并且没有被编译就会重新对这个文件进行编译,否则不再重新编译
最后的clean是用来删除目录下所有.o文件和main的功能,可以用来作为工程的清理。
要注意几点:
- 目标不一定必须是文件,也可以是某个步骤(暂时还用不到!)
- 命令可以是Shell里的任意指定,但是一定要以Tab键开头
- Makefile里的顺序可以随便放,但是唯独终极的目标(最后链接的文件)是有顺序要求的
在Makefile中使用变量
上面的Makefile文件帮我们解决了每次编译都要在Terminal里输入命令的问题,但是这次的项目一共就3个依赖文件,编写Makefile还是简单的,但实际项目中会有非常多的依赖文件,在头两行中需要输入两遍,重复输入就会浪费时间也容易出错,还好Makefile执行的命令都是Shell指令,是支持变量使用的,我们可以通过简单的变量定义来实现上面的Makefile功能,先放代码
#Makefile变量版 objs = main.o calcu.o input.o main:$(objs) gcc -o main $(objs) main.o:main.c gcc -c main.c calcu.o:calcu.c gcc -c calcu.c input.o:input.c gcc -c input.c clean: rm *.o rm main
简单的分析一下:我们先定义了一个变量objs并给他赋值。然后再用$调用这个值(和BASH里调用变量是一样的),就可以了。但是这里的赋值语句是有些讲究的。
Makefile里的赋值语句
我们上面通过等号=进行了赋值操作,然而在make里还有另外两种赋值方法:":="和"?="
=的使用
使用等号赋值时,不但可以使用已经定义好的值,也可以使用后面定义的值,我们用下面的命令来试一下(make是可以执行BASH命令的):
A = 1 B = $(A) A = 2 print: @echo B: $(B)
我们先定义A的值为1,再把A的值付给变量B,再把A的值进行修改后打印B的值,猜猜是多少?
发现赋值符"="的神奇之处了,它可以借助另外一个变量,将变量的真实值推到后文中定义,也就是其真实值取决于引用对象的最后一次赋值。
赋值符":="
把上面代码终端赋值符号修改为:=
A = 1 B := $(A) A=2 print: @echo B: $(B)
然后重新执行一下
可以发现赋值符":=“是不会使用后面定义的变量的,只能使用调用前的赋值,这就是两者的区别。
赋值符"?="
赋值符"?="该符号是如果B前面没有被赋值,就执行赋值语句,如果已经被赋值了,就保持原有值。
A = 1 B := $(A) A ?= 2 print: @echo A: $(A)
这次是打印A的值
追加赋值"+="
这个就没什么可说的了,对已有的变量追加新的值,要注意点是Makefile里的变量都是字符串,追加是像列表添加新的元素一样
A = main.o input.o A += calcu.o print: @echo A: $(A)
打印值如下
通配符使用
目标及依赖的简化
我们上面建立了一个Makefile文件来编译工程
1 main:main.o calcu.o input.o 2 gcc -o main main.o calcu.o input.o 3 4 main.o:main.c 5 gcc -c main.c 6 7 calcu.o:calcu.c 8 gcc -c calcu.c 9 10 input.o:input.c 11 gcc -c input.c 12 13 clean: 14 rm *.o 15 rm main
上面的文件中每个.c文件都要编译一个对应的.o文件,也就是要写一个对应的规则。如果C文件有很多的话就不能这么做了,这里可以利用Makefile模式规则里的通配符来编译所有.c文件对应的.o文件。
Makefile的模式规则中,在定义目标中要包含"%"字符来表示匹配到文件名,其表示任意长度的字符串,比方"%.c"就对应文件管理中"*.c"的文件,”a.%.c"就表示以a.开头,以.c结尾的文件。当目标值出现%的时候,目标中的%和依赖的%表示的是同样的字符串,那么目标和依赖就可以改成这样
%.o:%.c #command
这样一行命令就把所有的.o文件的目标和依赖的部分就替代了。
自动化变量
上面的规则里讲过,目标和依赖都是一系列的文件那么怎么从依赖文件中获取依赖文件名放到下面的命令行里呢?这就要用到自动化变量了。
自动化变量 |
描述 |
$@ |
规则中的目标集合,在模式规则中,如果有多个目标的话,“$@”表示匹配模式中定义的目标集合。 |
$% |
当目标是函数库的时候表示规则中的目标成员名,如果目标不是函数库文件,那么其值为空。 |
$< |
依赖文件集合中的第一个文件,如果依赖文件是以模式(即“%”)定义的,那么“$<”就是符合模式的一系列的文件集合。 |
$? | 所有比目标新的依赖目标集合,以空格分开。 |
$^ |
所有依赖文件的集合,使用空格分开,如果在依赖文件中有多个重复的文件,“$^”会去除重复的依赖文件,值保留一份。 |
$+ | 和“$^”类似,但是当依赖文件存在重复的话不会去除重复的依赖文件。 |
$* |
这个变量表示目标模式中"%"及其之前的部分,如果目标是 test/a.test.c,目标模式为 a.%.c,那么“$*”就是 test/a.test。 |
上面的表格中最常用的就是$@,$<和$^三种。在上面的Makefile中,我们用%.c来表示依赖文件,那么就可以用$<来表示依赖文件中的第一个文件,那么整体的Makefile就可以简化成下面这样
objs = main.o calcu.o input.o main : $(objs) gcc -o main $(objs) %.o : %.c gcc -c $< clean: rm *.o rm main
看看是不是简化的多了!
Makefile的伪目标
前面说过,Makefile的目标可以不光是文件名,还有一种情况是伪目标,这个目标不是真正的目标名,而是执行make命令时通过这个伪目标来执行其所定义规则的命令,就像前面定义的make clean和make print一样,都是伪目标。
定义伪目标的时候,要注意目标名称不能同文件名一样,假如文件中有个文件名是clean,我们定义的clean就不起作用了
但是如果不方便对文件名进行修改时,可以在定义前对其进行声明为伪目标,关键字为.PHONY:
.PHONY:clean clean: rm *.o rm main
先讲这么多,Makefile还支持if判断、函数调用等功能,我们以后用到的时候再说!