话接上回,话说我们已经大致了解了 C 编译器的工作流程,知道了 IDE 在背后如何在驱动编译器生成代码。对于传统 IDE ,就是集成了编辑器、项目管理、编译器,和调试器等几个大件的一个庞然大物。其中 IDE 企图节省人力的最大的部分就是将源代码组织起来,自动生成其间的关系,调用编译器构建项目。
(此处删去几百字关于 IDE 优劣的讨论。因为我觉得这个话题会陷于无谓的争论,还是直入主题比较好。承接上篇的宗旨,本文只写给有兴趣学习相关知识却不知该如何入门的朋友。说服程序员放弃 IDE 不是本文的初衷。)
对于不太大的项目,比如学校里日常做作业。写一个 make.bat 文件管理你的 C 代码已经足够了。如果你用一些 windows style 的编辑器,比如流行的 editplus 之流,都可以配置相应的所谓 user tool,一键调用 .bat 构建出最终的程序。也可以设置捕获编译器的输出,方便的双击错误信息定位到源代码编译错误的地方。
如果想摆脱鼠标(纯键盘操作对于基于命令行的工作方式来说,非常的有效率。毕竟写程序最终也得靠键盘的。),可以考虑使用 vim 。不要惧怕学习新事物。任何被公认优秀的工具,都有学习的价值。vim 属于有一定学习门槛,但一旦掌握(一般程序员可能需要一周左右的时间熟悉),威力无穷的那种。我的另一个同事强烈推荐 emacs ,我没怎么用过。如果你打算写 10 年以上的程序,花上几天时间学习一个无数程序员公认好用的工具,这项投资我个人认为是非常值得的。
使用 bat 的方式来构建项目有一个问题,那就是当项目慢慢增大时,效率受到影响。因为每次 build 一次,都要重新编译链接所有的源代码。而 C/C++ 从设计之初就考虑到节省编译时间,可以每个源文件独立编译的,然后最后在把分别编译出来的目标文件链接在一起。(关于编译和链接的概念,不明白的同学请自己 google )下一步,我们可以每次只编译刚刚修改过的源文件,和那些没有修改过的源文件以前编译出来的目标链接在一起就够了。
早期的 C 编译器,但是将编译和链接过程分开由两个程序完成的(现在其实也是)。我们今天看到的类似微软的 cl 这种程序只是给独立的连接器加了个壳而已(gcc 也是这样)。下面我们来看怎样分开编译多个源文件,并链接它们。
如果只想编译一个 .c 文件,只需要在给 cl 加上参数 /c ( gcc 是加 -c )。那么编译上一篇中提到的例子 foo.c ,即用命令行指令:
cl /c foo.c
我们可以看到,当前目录下生成了目标文件 foo.obj
我们可以用同样的方法生成 bar.obj 。即 cl /c bar.c
那么如何把 foo.obj 和 bar.obj 链接起来?还是用 cl 即可。
cl /Fefoobar foo.obj bar.obj
cl 这个壳会正确的识别输入文件的类型,做出对应的链接这个行为。我们也可以直接调用微软的链接器:Link 。
link /out:foobar.exe foo.obj bar.obj
注意,这里输出文件必须写全 .exe 后缀。
罗罗嗦嗦写了一大堆,主要是想加深以前不太明白这些的同学们的印象。如果是玩 IDE 的老鸟,其实在 IDE 的各种设置中,差不多也都看过这些了。关于编译器的命令行参数不记得不要紧。一则可以用 /? 看帮助;二还可以去 VS 的 IDE 中对应的设置菜单里看看,通常一个 IDE 菜单选项的修改,都对应了最终命令行参数的差别。生成 exe 还是生成 dll ;使用控制台模式,还是标准 Win32 程序;预定义些什么宏,打开调试信息或是开启编译优化,无所不在。
记住这些参数的写法并不容易,也没有必要。因为相对于编写代码,敲打这些编译开关只占很少的工作。但要记住,如何调用编译器去构建工程,这个步骤本身其实也是项目构建的一部分。亲手写过,也就多了一分了解。
编程之道在于,让机器做机器的事,人专注于人的思考。如果真的靠手键入每条编译指令,无疑让程序员去做了机器之事。把编译指令写入批处理(脚本)文件,仅仅只是节省了每次编译的重复劳动,并没有从根本上解脱。
在从 C 的源代码构建最终的执行文件这个流程中,哪些是人的创造,哪些是机器应行之事?显而易见,程序员应该做的是:
-
教会机器,如何把一个 .c 文件编译成 .obj 文件。
-
教会机器,如何把若干个 .obj 文件链接成 .exe 文件。
-
告诉机器,你的项目由那些 .c 文件构成,最终你想生成一个叫什么名字的 .exe 文件。
其中前两步,对于大多数类似的项目来说是共同的,所以我们只用也只应该教机器一次。而第三步,提供一个文件列表和一个目标文件名给机器即可。
实现以上目标,显然我们需要额外的工具。IDE 的项目管理及构建模块是一个选择,但不是唯一的选择。IDE 对最为常见的构建需求做的相当不错。也就是说,IDE 完美的教会了机器前两个步骤,我们无须干预。但这件事情意义有多大呢?既然我们现在已经知道整件事情的流程手工怎么进行,完成它就有了无数的选择。教会机器做这前两件事情,只是一项一次性劳动。我们可以通过教授机器做这件事的过程,学会更多东西。而后举一反三去干更为复杂的工作。下面,云风将介绍一种叫做 MAKE 的工具。它是我们完成这个目的的第一个台阶。
为什么是 Make ?
我想说, make 只是诸多选择中的一个。它绝对不是最好的,但它是最容易理解的。make 以一种极其简单的规则运作,让程序员可以轻易看透它的实质。而简单的工具组合起来往往可以发挥出极大的能量。因为简单,所以 make 也很容易学习。你只需要掌握它很少的一部分功能,它就帮助你完成各种各样的工作。绝不仅仅是编译程序这么简单。你可以用它收发邮件、下载文件、制作安装包、运行单元测试,等等你能想的到的在你的机器上用命令行能够完成的所有事情。当然这些事情不用 make 也有别的方式去做,只是 make 用起来更方便。Make 会按你告诉它的事情的依赖关系,决定了做事情的前后次序,然后批量完成交给它的任务。如此而已。如果你用过 FreeBSD ,一定会爱上它的 ports 。需要什么软件,进入对应目录,make install 。下载、配置、编译、安装,一气呵成。这就是 make 的威力。
学会了 Make 后,你再接触别的项目构建工具,就不会有太多障碍。Make 做起来比较困难的事,也可以再高一个层次的工具来完成,比如 Automake 。我们要做的是,认识问题是什么,选择合适的工具,用程序员的方式解决它。
Make 有许多分支版本,细节使用起来各有差异。VS 里带了一个叫做 NMAKE 的小工具,是 Make 的一个旁支。我本来想从这个讲起,无奈用的不多,还是换成日常用的比较多的 GNU Make 吧。gmake 的内建指令也更为丰富,稍微熟悉一下,得心应手。更难得的是,在 Windows 下获得 gmake 非常方便,使用 Mingw 版的 gmake 即可,在 google 搜索 “mingw gnu make” 即可。其实不需要安装,它是一个绿色软件,一个独立的一百多K 的 gmake.exe 小程序就可以直接运行。
下面,云风假设你已经正确安装了 Mingw 版的 GNU Make (我的 Windows 系统上安装的版本是 3.81 )。在命令行下,无论在什么目录下都可以直接输入 gmake 运行。(默认安装的 Gnu make 的执行文件名可能不叫这个名字,但你可以自己改个顺手的名字)
让我们开始吧。
还是在一个你可以随意做实验的目录,顶好是你已经创建了 foo.c bar.c 的那个目录下,新建一个叫做 Makefile 的文本文件。编译它,写上:
all :
echo Hello World
这是你的第一个 Makefile ,输入请小心。第一行的 all: 应该顶格写,而第二行 echo 之前,必须有一个 Tab ,而不能用空格替代。即:第二行必须是 Tab 打头。
Tab 不是可以忽略的空白字符,并且是 Make 工作的关键。这个设定早就为许多人诟病,没有正确的输入 Tab ,也是许多 Make 初学者常见的错误。骂归骂,只能说这是一个历史原因造成的。好在一旦你习惯它,同样会觉得编写 Makefile 其实是非常顺手的。而且现在很多编辑器都可以让 Tab 明显的显示出来,而不会和空格混淆。
现在,运行 gmake ,你会看到它调用了命令行指令 echo Hello World ,回显了这行字符串。庆祝一下,你成功运行了自己编写的第一个 Makefile 。
Make 的一切设计都是为了简单、快捷。你把要做的事情写在一个文件中,运行 gmake 去跑这些任务。按通常的设计,gmake 应该跟一个任务文件的文件名,指定跑哪个文件。但是为了简洁,gmake 默认去找当前目录下的名为 Makefile 的文件了。至于你想把这些任务放在别的文件中,可以通过 gmake 的参数控制。有兴趣的同学可以按 Gnu 软件的习惯,通过 gmake --help 查询。
光能显示一行 Hello World 显然离题万里。接下来我们看看让 gmake 帮我们编译程序。不要删除前面的 Makefile 文件,再后面追加几行:
foobar.exe :
cl /Fefoobar foo.c bar.c
记住第二行开头的 Tab 不要敲漏了。
现在你的 Makefile 文件看起来应该是这样:
all :
echo Hello World
foobar.exe :
cl /Fefoobar foo.c bar.c
我们再运行一下 gmake foobar.exe 看看:gmake 调用了你写好的 cl 指令,编译出了 foobar.exe 这个文件。
OK,大家应该看出点什么。Makefile 比传统的 bat 批处理文件多了点功能。它可以把多个任务放在一个文件里,而不需要我们写多个文件。为了区分任务,我们在命令行指定要做什么。
那些顶行写的文本,如果由一个单词加一个冒号开始。这个单词就被称为一个目标。gmake 目标,就可以做对应的任务了。而 gmake 会去做什么呢?自然由目标定义的下面几行决定。所有以 Tab 开头的文本行,定义了完成这个目标应该执行的命令行指令。gmake 会运行目标定义之下一直到下一个目标定义之间的所有指令。(注:Make 并不会保证这些指令的执行次序,虽然原则上是按你书写次序执行。这可以让 Make 使用多个 CPU 加快运行成为可能)
当我们在命令行输出 gmake all ,它就 echo Hello World ;而输入 gmake foobar.exe 它就调用 cl 去生成 foobar.exe 。如果没有直接输入 gmake 而不跟任何目标参数。它会自动寻找文件里的第一个目标。(而 Makefile 编写的惯例,我们通常把第一个目标起名字为 all ,这仅仅是一个惯例而已)
多试几次看看。
第 2 次运行 gmake foobar.exe 你会发现,gmake 报告:
gmake: `foobar.exe' is up to date.
而拒绝再次编译。
没错,这就是 Make 为数不多的原则之一:一旦目标已经存在,就直接跳过任务。
所有的目标,都被 Make 认为是一个文件,它的规则就是,如果目标文件存在,就认为事情已经做完。除非……
让我们修改一下 Makefile 把 foobar.exe : 这行改成
foobar.exe : foo.c bar.c
然后修改一下 foo.c 存盘,再运行 gmake foobar.exe 试试?又重新编译了对吧。
这就是 Make 的第二条原则:目标 : 后面以空格写上它所依赖的其它目标。如果所依赖目标存在,且比目标本身的时间新,就重新构键一次目标。
由于我们修改了 foo.c 导致了 foo.c 这个文件(对 Make 来说是一个目标,只不过没有构建这个目标的方法而已,只能靠用户自己编辑生成)比 foobar.exe 更新。foobar.exe 依赖 foo.c ,所以触发了 foobar.exe 的构建方法。
同样,我们可以让 all 依赖与 foobar.exe 。这样 gmake all (或省略 all 不写,因为 all 是第一个目标)时,由于不存在 all 这个文件,而触发 foobar.exe 的构建流程。
接下来,我们回头来看看自己编写的这个 Makefile 文件。现在大约是这个样子:
all : foobar.exe
echo Hello World
foobar.exe : foo.c bar.c
cl /Fefoobar foo.c bar.c
第 2 个目标 foobar.exe 的编写非常的累赘。程序员的直觉告诉我们,信息的重复不是一个好味道。那么让我们来修改一下。
foobar.exe : foo.c bar.c
cl /Fe$@ $^
这样是不是味道好点了?$@ 和 $^ 都是 Make 的内设变量,也可以看成是一种类似 C 语言中宏的东西。$@ 在指令被运行时,被宏替换为目标,而 $^ 被宏替换为所有的依赖目标,即冒号后面的那一长串东西。同样常用的还是 $< ,可以替换所依赖的第一个目标。
今天已经写的足够多了。没接触过 Make 工具的同学应该能初窥门径了。没错,从现在已经介绍的知识来看,Make 并没有为我们节省太多体力,甚至还多敲了许多字符,今天快结束时,居然还要多记几个诸如 $@ 这样古怪的符号。而做到的事情离我们的目标还很远。
没关系,云风会带着你渐入佳境的,那么,且听下回分解。