IDE 不是程序员的唯一选择(三)
有了前面的介绍,相信好学的同学们对 make 已经有了一定的了解。
记住,编写 Makefile 也是构建整个软件的一部分,其重要性并不亚于编写 .c 或 .h 文件。当你用 IDE 的时候,是由 IDE 来生成相当于 Makefile 的文件。但是这个生成的过程并不是完全自动的,它是由你的鼠标点击、拖拽(把 .c 文件加入项目)、和填写一些表单、以及勾选编译选项完成的。
如果你的整个项目中其它源文件都是从键盘输入来编写的,那么 Makefile 也由手工编写就理所当然了。
如果你在用 C++ ,在用 C++ 中的 STL ,那么是不是应该搞清楚 STL 到底做了什么,怎么做到的,这些问题呢?这是用 C/C++ 语言编程的程序员的一个基本态度。即使不去研究透彻,至少也应该了解一些大致的原理吧。那么对于 Make 来说也是这样。我们应弄清楚 Make 到底如何在工作,我们在 Makefile 里写的每一行代码是什么含义。为什么可以帮我们完成那些工作。这个连载选择 Make 来展开,也正是因为 Make 的工作原理非常简单,方便我们学习。
越是简单的东西,越可以在其上做出各种奇妙的东西。Make 也是这样。但一开始就给出别人做好的完善的库,简单的用一下,会让我们迷失其真谛。相信我,最终,编写 Makefile 可以非常简单,善用工具、不必写任何多余的东西,甚至比在 IDE 里拖入几个源文件更简洁。但一开始,还是从繁琐开始,这些繁琐都是帮助你去理解,等你理解了自然能找到方法简化这些繁琐的工作。
云风绝非使用 Make 的高手,从某种意义上来说,也是一个入门者。写这个系列的时候,也需要去查阅文档确认是否写错。平时工作的时候,Makefile 文件通常也需要多次调试,才能正确的完成工作。也正是如此,才能体会到:怎样去理解和学习,容易跨过最初的门槛。
上一篇开篇,我们讲到了,如何用命令行分开编译 .c 文件,并把它们链接起来。这样做,可以使每次修改都可以让机器做最少的编译任务。对于小工程,这样做的意义不大。但是大工程,可能就能帮我们节约不少时间了。记住这一点,永远没有通用的最优方案。因为实施方案本身也是有成本的。我们只需要找到最直接最简单的方法就可以了。比如项目一开始,可以写一个最简单的 Makefile 文件,随着项目规模的扩大再逐步完善。
分开编译再链接这件事,人做起来都比较繁琐,把它教给机器去做,当然也会更繁琐。所以我没有在上一篇中详解。好学的同学应该会自己弄了,今天,我也来写写自己的方案。但在此之前,我们先梳理一下对 Make 的理解。
Make 是一个工作于非常简单的模式下的工具。它的内部有一张表,记录了目标文件之间的依赖关系。Makefile 就是用来描述这张依赖关系表的。对于依赖关系表的描述,用了一种非常简单的语法:
目标 : 依赖
这表示,"目标" 的构建依赖于 "依赖" 先构建出来。这里,"目标" 和 "依赖" 都是文件系统中的文件,而"依赖"本身也可以是一个"目标"。如果 "依赖" 的文件时间新于 "目标" 的文件时间,表示 "目标" 需要重新构建。如果 "目标" 文件不存在,也会触发这种构建过程。
一个目标可以有多个依赖,可以在 : 后以空格分开写上多个,比如:
目标 : 依赖1 依赖2 依赖3
这在我们前一篇中已经多次见过了。其实还有另一个规则,我们可以写:
目标 : 依赖1
目标 : 依赖2
这样分两行写,即,每次写 "目标 : 依赖" 都在依赖关系的依赖关系表中添加了一项。(关于同名的依赖添加的问题,以后我们在讨论)
举个例子:
a : b c
和
a : b a : c
其实是等价的。
每个目标的构建方法并不是由 Make 内置功能完成的,Make 只是简单调用写在 "目标" 定义的下一行的若干命令行脚本而已。而每一行命令行脚本必须以 Tab 键开头。注意,如果你把目标的依赖关系分成若干行实现,只可以有一个地方定义构建脚本。
举例:
all : a all : b a : echo $@ b : echo $@
这样一个 Makefile 用 Make 运行后,会显示:
echo a a echo b b
为什么呢?因为 Make 会找 Makefile 中定义的第一个目标,做为它这次的终极目标。在这里是 all 。all 依赖于 a 和 b 两个目标。由于你的工作目录下没有 a 和 b 两个文件,所以触发了 a 以及 b 的构建。而 a 的构建指令是 echo $@ ,$@ 指代了当前目标 "a" ,结果就执行了 echo a 。同样,b 文件的不存在,导致了 b 的构建,执行了 echo b 。
需要强调的是,echo 并不是 Make 的内置功能。echo 是 Windows 命令行指令(在 *nix 系统上,称为 shell 指令)。Make 只管忠实的执行那些相关的以 Tab 开始行内描述的命令行指令。目标的构建成功也不以目标文件是否正确生成为依据。而是以命令执行的结果是否为 0 。记得学 C 语言的时候,老师教你,main 函数得到正确结果时,应该 return 0 吧。这个 main 的 return 0 就是返回给系统用的。Make 通过检查这个返回值来觉得命令行指令是否被正确的执行。如果收到非 0 的返回值,整个 Make 的过程会被中断。当然,echo 这样的指令,一般都会返回 0 的。
注意,一个目标是否被正确构建,只取决于构建它的命令行指令是否正确的返回 0 ,而不取决于文件是否被创建出来或是被更新到最新的时间。在一次构建中,每个目标最多只会被构建一次。这是由于 Make 只对依赖关系表做一次简单的拓扑排序,然后就开始工作了。
同样,我们还可以让多个目标依赖同样的东西:
a b : c d
就等价于
a : c d b : c d
现在我们可以看到,在 Makefile 里写 : 定义,其实就是在填写一张依赖关系表。每次的一个 : 都向表里追加一些项目。Make 工作的时候,先读完整个文件,把完整的依赖关系表建立好,再根据命令行指定的目标开始工作,如果在命令行不指定目标,默认就是 Makefile 里写的第一个目标了。
这样我们就好理解,除了 Makefile 里的第一个目标定义之外,所有的依赖关系描述都是无所谓书写的先后次序的。Make 只管向依赖关系表里添加表项,不会删除。原则上也不保证按依赖表中的次序先后来构建,如果你需要一个目标一定先于另一个目标构建,就需要显式的描述出依赖关系。
让我们回到早先的例子:从 foo.c 和 bar.c 构建出 foobar.exe 。并且要求 foo.c 和 bar.c 里若只修改了一个文件,就只编译新修改的那一个。
foobar.exe : foo.obj bar.obj link /out:$@ $^ foo.obj : foo.c cl /c $< bar.obj : bar.c cl /c $<
先别急着编译,我们来看看如何调试 Makefile 文件。使用 gmake -n 看看,是不是显示了:
cl /c foo.c cl /c bar.c link /out:foobar.exe foo.obj bar.obj
如果显示的是,
gmake: `foobar.exe' is up to date.
那么就先 del foo.obj bar.obj foobar.exe 然后再看。
-n 参数可以让我们观察到底会执行些什么命令行指令,而不真的去执行它们。这对调试复杂的 Makefile 非常有用。
如果没有问题,就可以 gmake 构建出 foobar.exe 了。
如果你烦透了每次做一次实验都要手工清理一下 obj 和 exe 文件,那么可以把 del 指令也写在 Makefile 中。在上面的 Makefile 最后加上两行:
clean : -del foo.obj bar.obj foobar.exe
当然,你也可以偷懒写成 del *.obj *.exe
同学们会注意到,del 前有个减号。这个 - 是告诉 Make ,忽略掉后面这行指令的返回值,不要因为返回值非 0 而中断。什么时候 del 会返回非 0 值呢?自然是要删除的文件不存在啦。
现在你就可以通过 gmake clean 来清除目标文件了。或者用 gmake clean foobar.exe 来先清理再编译,这相当于我们在 IDE 中使用的 rebuild 指令。Make 的命令行可以跟多个目标,它会顺着构建这些目标。对于举一反三的同学,应该已经给 Makefile 加了一个 rebuild 目标了:
rebuild: clean foobar.exe
暂时先这么写着,以后我们会介绍更好的,更偷懒的写法。
最后给出一个云风当年初学 Make 时,犯过的一个错误。属于对 Make 理解不当造成的。
假设 foo.c 里 include 了一个 foo.h 文件,依赖关系应该怎么写?
在 Makefile 的最后,加上一行
foo.c : foo.h
这样行吗?
你可以试试,修改一下 foo.h ,然后 gmake 一下,看看有没有重新编译 foo.o 。为什么不能工作呢?留给同学们思考了。
Comments
Posted by: Anonymous | (30) December 6, 2016 09:22 PM
Posted by: peter | (29) October 8, 2008 11:10 AM
Posted by: wanggangzero | (28) September 26, 2008 05:08 PM
Posted by: Cloud | (27) September 25, 2008 10:12 PM
Posted by: Cloud | (26) September 25, 2008 09:24 PM
Posted by: 天堂的隔壁 | (25) September 25, 2008 11:53 AM
Posted by: 天堂的隔壁 | (24) September 25, 2008 11:44 AM
Posted by: 天堂的隔壁 | (23) September 25, 2008 11:43 AM
Posted by: david | (22) September 25, 2008 11:40 AM
Posted by: 天堂的隔壁 | (21) September 25, 2008 11:27 AM
Posted by: 天堂的隔壁 | (20) September 25, 2008 11:26 AM
Posted by: Chen | (19) September 25, 2008 04:06 AM
Posted by: grissiom | (18) September 25, 2008 01:15 AM
Posted by: macro | (17) September 25, 2008 01:06 AM
Posted by: Cloud | (16) September 25, 2008 01:00 AM
Posted by: macro | (15) September 25, 2008 12:53 AM
Posted by: Cloud | (14) September 25, 2008 12:47 AM
Posted by: Cloud | (13) September 25, 2008 12:36 AM
Posted by: Zhe | (12) September 25, 2008 12:06 AM
Posted by: Zhe | (11) September 25, 2008 12:04 AM
Posted by: macro | (10) September 25, 2008 12:03 AM
Posted by: Cloud | (9) September 24, 2008 11:45 PM
Posted by: chinainvent | (8) September 24, 2008 11:44 PM
Posted by: Zhe | (7) September 24, 2008 11:37 PM
Posted by: kai | (6) September 24, 2008 11:32 PM
Posted by: Cloud | (5) September 24, 2008 11:11 PM
Posted by: Zhe | (4) September 24, 2008 11:02 PM
Posted by: ix | (3) September 24, 2008 10:01 PM
Posted by: sjinny | (2) September 24, 2008 07:49 PM
Posted by: Jim | (1) September 24, 2008 07:35 PM