对于一个简单的多文件程序:
main.c
#include <stdio.h>
void fun();
int main()
{
fun();
}
str.c
#include <stdio.h>
void fun()
{
printf("hello worldn");
}
如上,程序被分别放置在多个文件的情况,此时我们想要编译出这这个程序的代码为:
gcc main.c str.c -o main
执行之后gcc编译器会分别编译main.c和str.c两个源文件,并生成其相应的对象文件后,链接成新的可执行文件main(也就是“-o”参数所指定的名称)
可以想象在面对多文件编程的时候直接使用gcc命令编译,未来将会出现的情况:
1. 编译命令冗长
如果一个程序的源文件有数十个,那么编译命令便会在gcc之后跟上数十个文件的文件名,还包括一些调试选项参数或者某些函数要引用某些特定特定的库,可能使得直接使用gcc编译变成一件繁琐的过程
2. 编译时间变长
对于上述的例子程序而言,没次通过 gcc main.c str.c -o main 命令来编译链接的时候,我们的gcc编译器都会重新生成main.c和str.c的对象文件。这也意味着,即使我只是修改了str.c中的一行代码,包括main.c在内这两个文件都会被重新编译。在编程初期这不是什么大问题,可是当程序变大,文件变多之后这个问题就变得很严重了。因为如果一个程序的源文件有100个的话,仅仅修改了其中一个文件的一行代码,通过上述的方式,也会将所有100个文件重新编译。如此会造成大量的时间浪费。
我们也可以使用shell脚本来解决输入问题,不过即使这样也需要重新编译所有文件,程序较大源文件较多的时候同样会造成效率低下。
解决方案:
make 程序。通过读入当前目录下的 makefile 来配置自动编译。通过使用make可以很好的解决上面的两个问题。
main : main.o str.o # 完成 main 依赖 main.o str.o gcc main.o str.o -o main # 完成 main 的具体命令 main.o : main.c # 完成 main.o 依赖 main.c gcc main.c -c # 完成 main.o 的具体命令 str.o : str.c # 完成 str.o 依赖 str.c gcc str.c -c # 完成 str.o 的具体命令
这是一个非常简单的makefil1e,注意每一句的具体命令都要向后缩进一行(使用Tab键缩进,不用会报错遗漏分隔符),在编写好之后,在当前目录下执行make命令可以看到终端上显示的信息:
gcc main.c -c gcc str.c -c gcc main.o str.o -o main
可以想象make程序的解释器是从第一行开始解析makefile文件的内容,然后看到 main 便将其作为最后生成的目标,然后是“:”冒号。
冒号这里意思可以很简单的理解为依赖,即实现冒号左边的目标要依赖冒号右边的各项。在第二行则是生成最后目标可执行文件 main 的gcc代码。
因为生成main所依赖的main.o还不存在所以,程序先找到生成 main.o 的部分去执行,然后生成 main.o 之后发现 str.o 还没生成,于是程序又会去找 生成 str.o 的部分去执行。最后依赖项都已经充足才回链接生成 main 。
这也是为什么执行 make 看到的命令会是
gcc main.c -c # 编译main.c 生成 main.o gcc str.c -c # 编译str.c 生成 str.o gcc main.o str.o -o main # 链接main.o str.o 生成 main
makefile 的选项
事实上makefile除了直接通过make来执行,还可以跟上参数来调用
main : main.o str.o gcc main.o str.o -o main main.o : main.c gcc main.c -c str.o : str.c gcc str.c -c clear : rm *.o -rf
在编写完上述makefile之后,执行 make 命令与我们开始写的没有什么区别,但是我们可以通过给 make 命令一个参数来调用新加的clear 项。
make clear
执行后会发现它弹出一条命令,内容就是我们写在 makefile 中的 rm *.o -rf
通过这样的方式可以很简单的实现“.o”文件的清理。事实上这个就是简单的 makefile 中各项调用的方式。
值得一提的是,这里博主把 makefile 中的每个生成目标都叫做“选项”,这时博主自定义的称呼(如果有人知道更准确的称呼,还望评论联系博主)。
例如刚刚的 make clear 命令就调用了 clear 这个选项,而 clear 这个选项没有依赖项,也就可以视为已经满足执行条件便直接执行其后跟随的代码。按照这个理论我们也可以单独调用如 str.o 这个选项:
make str.o
执行之后会发现终端上显示:
gcc str.c -c # 编译str.c 生成 str.o
是的,这样确实单独只调用了一个选项。
有些思维比较灵活的,可能已经理解我的意思了,紧紧只是把冒号右边的东西当成一个选项来看的话,这个makefile还可以改写成:
main : item1 item2 gcc main.o str.o -o main item1 : main.c gcc main.c -c item2 : str.c gcc str.c -c clear : rm *.o -rf
至此,我们可以对 makefile 有了更进一步的理解,我们知道了 makefile 中分可以分成两种东西:
1.选项以及它的依赖项
2.这个选项对应的shell命令
在 makefile 中一个选项的依赖项可以是另一个选项(这会导致另一个选项先执行),也可以是一个外部的文件名。然后,在使用 make 命令的时候可以通过 make 选项名 来单独调用某一个选项的代码来执行(当然,如果依赖项中有另一个选项的话,也会先执行另一个选项),除此之外默认就是执行第一个选项。
好的,如果你已经掌握了这些内容,那么恭喜你对于 makefile 执行的基本思路你已经知道的差不多了。
makefile 的变量
现在我们要跟深入的了解一些 makefile 的变量。依旧是通过一个问题来引出,且慢慢来看博主的叙述:
gcc作为一款成熟的编译器,其中自然包括的许多的编译选项,例如 -E 的只预处理不编译,又例如程序优化、调试等等,真的算起来gcc的编译选项实际上有上百种。虽然说中间的一些选项,可能我们一辈子都不会用到,但是不能否认这些编译选项的重要性。例如用于gdb调试的 -g 选项。
好吧,不说闲话,我们回到正题。在出现了编译选项的时候,我们刚才的 makefile 又要对此作出相应的更改了。
main : main.o str.o gcc main.o str.o -o main main.o : main.c gcc -g main.c -c str.o : str.c gcc -g str.c -c
那么新的问题出现了,我这里是两个文件编译,于是我加了两个 -g 可是如果我有几十个文件的话,那么为了调试,我是不是要加几十个 -g ?如果我加了几十个 -g 写了一阵子之后,我们调试结束了是不是又要将这几十个 -g 删掉?
感觉有点麻烦吧。这也是引出 makefile 变量的问题,我可以通过设置一个变量解决:
CC = gcc -g
main : main.o str.o
${CC} main.o str.o -o main
main.o : main.c
${CC} main.c -c
str.o : str.c
${CC} str.c -c
变量名不用定义类型直接写出来,然后等号之后紧跟着就是变量的值。调用变量时通过 ${变量名} 这样来调用即可。如此碰到很多选项要编译也不用一直改编译选项的麻烦了。更多一点还可以这样:
CC = gcc -g
OBJ = main.o str.o
main : ${OBJ}
${CC} ${OBJ} -o main
main.o : main.c
${CC} main.c -c
str.o : str.c
${CC} str.c -c
当然上述方案是可行的,只不过这样写还不够简单哦,到了这里,我们终于可以引出 makefile 中一直让不少新手瞠目结舌的地方了:
CC = gcc -g
OBJ = main.o str.o
main : ${OBJ}
${CC} $^ -o $@
main.o : main.c
${CC} $^ -c
str.o : str.c
${CC} $^ -c
是不是突然觉得有点眼花?比如其中的 $^ 和 $@ 事实上这是一种自动化变量,make程序可以自动通过具体情况来生成这个变量:
$^ 依赖的所有文件 ( 观察一下 ^ 的形状是不是有点像包括所有文件感觉 )
$@ 依赖的目标文件 ( 联想一下 @ 的单词 at, 是不是一条微博写完了就该去 @ 目标了 ^_^ )
这个这样再反过来看下开始的 makefile 是不是感觉有点理解了?
如果感觉没什么问题的话,那么基本的 makefile 对你来说已经不是什么大问题了,还有兴趣了解更多的话,可以带着如下的问题去搜索一下 makefile 的更多功能:
CC = gcc -g
OBJ = main.o str.o
main : ${OBJ}
${CC} $^ -o $@
.c.o:
${CC} -c $< -o $@




