3.11 make:管理分段编译

当使用分段编译(separate compilation)(把代码拆分为许多翻译单元)时,需要某种方法去自动编译每个文件并且告诉连接器把所有分散的代码段,连同适当的库和启动代码,构造成一个可执行的文件。许多编译器允许用一个简单的命令行语句完成。例如,对于GNU C++编译器,可能会用:

3.11 make:管理分段编译 - 图1

使用这种方法的问题是编译器事先要编译每个文件而不管文件是否需要重建。在具有多个文件的工程中,如果仅仅改变了一个文件,就可能不得不重新编译所有文件。

解决问题的方法是用一个称为make的程序。该程序是在UNIX上开发的,但某些形式到处都可使用。make工具按照一个名为makefile的文本文件中的指令去管理一个工程中的所有单个文件。当编辑了工程中的某些文件并使用make时,make程序会按照makefile中的说明去比较源代码文件与相应目标文件的日期,如果源代码文件的日期比它的目标文件的日期新,make会调用编译器对源代码进行编译。make仅仅编译已经改变了的源代码,以及其他受修改文件影响的源代码文件。使用make程序,每次修改程序时,不必重新编译工程中的所有文件,也不必核对所有生成的东西。makefile文件包含了组合工程的所有命令。学会使用make命令会节省大量时间,也会减少挫折。在Linux/Unix机器上安装新软件时使用make是一种典型的方式(虽然那些makefile比本书上出现的要复杂得多,而且作为安装过程的一部分,对于特定的机器,通常会自动地生成makefile文件)。

因为make实际上对所有C++编译器有某种可用的形式(即使没有,也可以在任何编译器上使用免费的make),因此它将作为贯穿于本书的工具。然而,编译器提供商也创建了自己的工程构造工具。这些工具询问工程中包括哪些文件,然后它们确定所有的关系。这些工具使用与makefile相似的文件,通常称为工程文件(project file),程序环境会维护该文件因此不必为它而担心。配置和使用工程文件随开发环境的改变而有所不同,因此必须找到怎样使用它们的相关文档(虽然工程文件工具由不同的厂商提供,但是使用都很简单)。

即使还使用特定厂商的构建工程工具,本书中所用的makefile仍然有效。

3.11.1 make的行为

当输入make(或你的“make”程序的其他名字)时,make程序在当前目录中寻找名为makefile的文件,该文件作为工程文件已经被建立。这个文件列出了源代码文件间的依赖关系。make程序观察文件的日期。如果一个依赖文件的日期比它所依赖的文件旧,make程序执行依赖关系之后列出的规则。

在makefile中的所有注释都从“#”开始一直延续到本行的末尾。

作为一个简单的例子,一个名为“hello”的程序的makefile文件可能包含:

3.11 make:管理分段编译 - 图2

这就是说hello.exe(目标文件)依赖于hello.cpp。当hello.cpp比hello.exe文件日期新时,make执行“规则”mycompiler hello.cpp。可能会有多重依赖和多重规则。许多make程序要求所有规则以tab开头。这与空格通常被忽略的空格不一样,空格可以用于格式化以便于阅读。

规则不仅局限于调用编译器。在make中还可以调用想要调用的任何程序。通过创建相互依赖的规则集的分组,可以修改源代码文件,输入make,确信所有受影响的文件会重新正确地重建。

3.11.1.1 宏

makefile可以包含某些宏(注意,这些宏完全不同于C/C++的预处理宏)。用宏进行字符串替换是很方便的。本书中的makefile使用一个宏去调用C++编译器。例如,

3.11 make:管理分段编译 - 图3

等号‘=’用来把CPP定义为一个宏,符号‘$’和圆括号扩展宏。在这里,扩展意味着宏调用$(CPP)将被字符串mycompiler取代。对于上面的宏,如果想改变到名为cpp的不同编译器,只需把宏改变为:

3.11 make:管理分段编译 - 图4

也可以在宏中加入编译器标志,或使用分开的宏加入编译器标志。

3.11.1.2 后缀规则

说明make怎样为工程中的每个单独的cpp文件调用编译器是很乏味的,特别当知道了每次相同的处理过程之后。因为make的设计注重节约时间,所以只要依赖于文件名字后缀,它就有一种简化操作的方式。这些简化称为后缀规则。一条后缀规则是一种教make怎样从一种类型文件(如.cpp)转化为另一种类型(如.obj或.exe)的方法。一旦有了make从一种文件转化为另外一种文件的规则,其他要做的只是告诉make哪些文件依赖于其他文件。当make发现一个文件比它依赖的文件旧,它就会使用规则创建一个新文件。

后缀规则告诉make可以根据文件的扩展名去考虑怎样构建程序而不需用显式规则去构建一切。在这种情况下它指出:“调用下面的命令从扩展名为cpp的文件去构造扩展名为exe的文件”。上述例子看起来如以下所示:

3.11 make:管理分段编译 - 图5

.SUFFIXES指令告诉make必须注意后面的扩展名,因为它们对于这个特定的makefile有特殊的意义。其后看到后缀规则.cpp.exe,说明“这里是怎样把任何扩展名为cpp的文件转化为一个扩展名为exe的文件的”(当cpp文件比exe文件新的时候)。和前面一样使用了宏$(CPP),但是发现了某种新东西:$<。因为以‘$’开头,所以这是一个宏,但它是make内部的特殊的宏。符号$<只能用于后缀规则,意思是“无论怎样都要触发的规则”(有时称为依赖),在本例中表示“需要被编译的cpp文件。”

一旦建立了后缀规则,就能简单地说明,例如说明“make Union.exe”,后缀规则会展开,即使在整个makefile文件中从未提及“Union”。

3.11.1.3 默认目标

在宏和后缀规则之后,make在文件中查找第一个“目标”,并构建它,除非指定了不同的目标文件。因此对于makefile文件:

3.11 make:管理分段编译 - 图6

如果简单地输入‘make’,那么会生成target1.exe文件(使用默认的后缀规则),因为它是make遇到的第一个目标。为了生成target2.exe我们不得不显式说明‘make target2.exe’。这样做就比较冗长,因此通常会创建一个依赖于所有其余目标文件的默认“哑元”目标,例如:

3.11 make:管理分段编译 - 图7

在这里,‘all’并不存在,没有名为‘all’的文件,因此每次键入make,它会把‘all’作为第一个目标(这是默认的目标),然后发现‘all’不存在,所以它检查所有的依赖关系。因此它查看target1.exe并(使用后缀规则)判断:(1)target1.exe文件是否存在,(2)target1.cpp文件是否比target1.exe文件新。如果(1)(2)都成立,就使用后缀规则(除非为某个特定的文件提供了一个显式规则)。然后在默认的目标列表上查找下一个目标文件。因此通过建立一个默认的目标文件列表(按习惯通常称为‘all’,但可以随便起名),只需简单地键入make就能够生成在工程中的所有可执行文件。此外,可以定义其他的非默认目标文件列表用于其他目的,例如,当键入‘make debug’时会重新构建所有带有调试信息的文件。