makefile是一个工程管理器,可以在makefile文件中给工程,定义一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作。简而言之就是告诉系统如何去编译这个工程。
其中我们使用makefile还有一个很重要的原因,就是make工程管理器根据时间戳,来自动发现更新过的文件从而减少编译的工作量。
就这点文字说明可能体会不到这个make工程管理器减少编译的工作量,我举个例子说明一下,假如编译器编译一个源文件需要0.1s,假如你的工程有10个源文件,那个工程编译耗时1s,然后后面我们写的大型工程代码往往有成千上万个源文件,那么每次编译都需要100~1000s中,这里花费的时间就特别的多,后期代码出现bug,简单的修改一次,就得编译一次,不用make工程管理器的话,你的时间就都花在了编译代码上了。
make工程管理器是怎么通过时间戳来减少编译的工作量?我们应该了解一下程序编译的过程:预处理,编译,汇编,链接
预处理:添加头文件,展开宏定义,处理条件编译的命令,删除注释
编译:把预处理后的文件转换成特定的汇编代码(中间包含程序的语法查错)。
汇编:将上一步得到的汇编代码进一步生成机器执行代码,生成目标文件。
链接:将上一步得到的目标文件及各种调用到的库文件建立链接,最终生成可执行文件。
工程中的所有源文件都得单独进行预处理、编译、汇编过程,最后在一起进行链接过程。
make工程管理器,根据源文件和目标文件的时间戳来判断源文件是否修改过,若修改过,则重新生成对应的目标文件,然后将生成的目标文件重新链接得到新的可执行文件。这里没有修改的源文件则make工程管理器不会对其进行操作。
我们改了那个文件,工程管理器就只重新编译那个源文件,而不用编译工程中的所有源文件,大大的减小了编译的工作量。
不同产商的make各不相同,也有不同的语法,但其本质都是在“文件依赖性”上做文章,这里,我仅对GNU的make进行讲述。
默认情况下,我们在终端使用make命令,系统会自动在当前目录下按顺序寻找文件名为“ GUNmakefile ”,“ Makefile ”,“makefile”的文件,并且执行文件中的命令,也就是说我们只要将makefile文件设置为上面三个文件名中的一个即可。
makefile的格式如下:
target1 : dependent_file
(Tab)command 1
(Tab)command 2
.
.
.
目标1 : 依赖文件
(Tab)命令1
(Tab)命令2
(Tab)命令3
目标2 : 依赖文件
(Tab)命令4
(Tab)命令5
目标3 : 依赖文件
(Tab)命令6
这个格式类似于一个函数,先写目标和依赖文件,依赖文件可为空,然后换行写命令,切记命令前一定要用一次Tab键,否则会出错。命令根据实际情况编写。
把最上面的目标1称为是终级目标。默认情况下,make命令执行结果的就是完成终级目标对应的命令。在执行这些命令前,先去查看相关的依赖文件是否存在。如果依赖文件不存在,先去执行其它目标,产生当前依赖文件。
定义变量 :
OBJ=1.o 2.o 3.o
使用变量:${OBJ}
直接展开:
x := var1
y := $(x) bar
x := var2
等价于:
y = var1 bar
x = var2
条件赋值操作
前面没有定义该变量,现在定义使用现在赋的值。如果前面已经定义了该变量,则使用之前的值,不改变
例:
IMMEDIATE ?= DEFERRED
自定义变量:
$@ | 目标名 |
$< | 第一个依赖目标. 如果依赖目标是多个, 逐个表示依赖目标 |
$^ | 所有依赖目标的集合, 会去除重复的依赖目标 |
$? | 比目标新的依赖目标的集合 |
$% | 当目标是函数库文件时, 表示其中的目标文件名 |
$* | 这个是GNU make特有的, 其它的make不一定支持 |
$+ | 所有依赖目标的集合, 不会去除重复的依赖目标 |
常用的为红色字体的前三个。
下面根据代码讲解一下makefile编程上的语法
头文件head.h
#ifndef _HEAD_H_
#define _HEAD_H_
#include
#include
#include
void Test1(void);
void Test2(void);
void Test3(void);
#endif
主函数源文件main.c
#include "head.h"
int main(int argc, const char *argv[])
{
Test1();
Test2();
Test3();
printf("Test is end \n");
}
工程的的驱动源文件test1.c
#include "head.h"
void Test1(void)
{
printf("Test1ing \n");
}
工程的的驱动源文件test2.c
#include "head.h"
void Test2(void)
{
printf("Test2ing \n");
}
工程的的驱动源文件test2.c
#include "head.h"
void Test3(void)
{
printf("Test3ing \n");
}
makefile文件
OBJ=main.o test1.o test2.o test3.o
GCC=gcc -c
output : ${OBJ}
gcc ${OBJ} -o output
main.o : main.c head.h
${GCC} main.c -o main.o
test1.o : test1.c head.h
${GCC} test1.c -o test1.o
test2.o : test2.c head.h
${GCC} test2.c -o test2.o
test3.o : test3.c head.h
${GCC} test3.c -o test3.o
.PHONY : clean
clean:
rm *.o
这一种写法是最基本的写法,每个目标文件都由依赖文件编译所得。在这里有时候我们的头文件并不是在同一个位置,这里我们就需要我们自己利用gcc相关命令对头文件路径进行指定。变量的使用就是为了减少书写的工作量以及代码查看更直观。
OBJ=main.o test1.o test2.o test3.o
GCC=gcc -c
output : ${OBJ}
gcc $^ -o $@
main.o : main.c head.h
${GCC} $< -o $@
test1.o : test1.c head.h
${GCC} $< -o $@
test2.o : test2.c head.h
${GCC} $< -o $@
test3.o : test3.c head.h
${GCC} $< -o $@
.PHONY : clean
clean:
rm *.o
这一种写法是利用自动变量来减少书写工作量。
还有一种是利用makefile的自动推到规则,
OBJ=main.o test1.o test2.o test3.o
GCC=gcc -c
output : ${OBJ}
gcc $^ -o $@
main.o : main.c head.h
test1.o : test1.c head.h
test2.o : test2.c head.h
test3.o : test3.c head.h
.PHONY : clean
clean:
rm *.o
//超级简写 把依赖关系也省略掉
OBJ=main.o test1.o test2.o test3.o
GCC=gcc -c
output : ${OBJ}
gcc $^ -o $@
.PHONY : clean
clean:
rm *.o
只要make看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,如果make找到一个main.o,那么mian.c,就会是main.o的依赖文件。并且 cc -c main.c 也会被推导出来,于是,我们的makefile再也不用写得这么复杂。我们的是新的makefile又出炉了。
在makefile编程中依赖文件中注意要将头文件写上,不写的话,头文件修改,make工程管理器并不会自动检测头文件,在后面链接的时候会出现错误。
仓促成文,不当之处,尚祈方家和读者批评指正。联系邮箱[email protected]