Reload Original PagePrint PageEmail Page

云风的 BLOG: 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


::...

免责声明:
当前网页内容, 由 大妈 ZoomQuiet 使用工具: ScrapBook :: Firefox Extension 人工从互联网中收集并分享;
内容版权归原作者所有;
本人对内容的有效性/合法性不承担任何强制性责任.
若有不妥, 欢迎评注提醒:

或是邮件反馈可也:
askdama[AT]googlegroups.com


点击注册~> 获得 100$ 体验券: DigitalOcean Referral Badge

订阅 substack 体验古早写作:


关注公众号, 持续获得相关各种嗯哼:
zoomquiet


自怼圈/年度番新

DU22.4
关于 ~ DebugUself with DAMA ;-)
粤ICP备18025058号-1
公安备案号: 44049002000656 ...::