2.2 分段编译工具

当创建大的项目时,分段编译尤其重要。在C/C++中,可以将一个大程序构造成为许多小程序块,而这些小程序块容易管理,可独立测试。程序分割的最基本的方法是创建命名子程序。在C和C++里,子程序称为函数(function),函数是一段代码段,可以将这些函数放在不同的文件中,并能分别编译。另一种解释,函数是程序的基本单位,因为不能把一个函数分开,让其不同的部分放在不同的文件中;整个函数必须完整地放在一个文件里(尽管文件可拥有不止一个函数)。

当调用函数时,通常要传给它一些参数(argument)。这些参数是我们希望函数在执行时使用的值。当函数执行完后,可得到一个返回值(return value),返回值是函数作为执行结果返回的一个值。但也可以编写不带参数没有返回值的函数。

程序可由多个文件构成,一个文件中的函数很可能要访问另一些文件中的函数和数据。编译一个文件时,C或C++编译器必须知道在另一些文件中的函数和数据,特别是它的名字和基本用法。编译器就是要确保函数和数据被正确地使用。“告知编译器”外部函数和数据的名称及它们的模样,这一过程就是声明(declaration)。一旦声明了一个函数或变量,编译器知道怎样检查对它们的引用,以确保引用正确。

2.2.1 声明与定义

声明(declaration)和定义(definition)这两个术语在整本书中都会准确地区分使用,因此必须弄清它们之间的区别。事实上,所有的C/C++程序都要求声明。编写第一个程序之前,需要了解声明的基本方法。

声明是向编译器介绍名字—标识符。它告诉编译器“这个函数或这个变量在某处可找到,它的模样像什么”。而定义是说:“在这里建立变量”或“在这里建立函数”。它为名字分配存储空间。无论定义的是函数还是变量,编译器都要为它们在定义点分配存储空间。对于变量,编译器确定变量的大小,然后在内存中开辟空间来保存变量的数据。对于函数,编译器会生成代码,这些代码最终也要占用一定的内存。

在C和C++中,可以在不同的地方声明相同的变量和函数,但只能有一个定义[有时这称为ODR(one-definition rule,单一定义规则)]。当连接器连接所有的目标模块时,如果发现一个函数或变量有多个定义,连接器将报告出错。

定义也可以是声明。如果定义int x;之前,编译器没有发现标识符x,编译器则把这一标识符看成是声明并立即为它分配存储空间。

2.2.1.1 函数声明的语法

C/C++的函数声明就是给函数取名、指定函数的参数类型和返回值。例如,下面是一个叫func1()的函数声明,它带了两个整数类型的参数(整数类型在C/C++中以关键字int表示)并返回一个整数:

2.2 分段编译工具 - 图1

第一个关键字是函数返回值类型:int。参数按其使用的顺序依次排在函数后面的括号内。分号说明声明结束,在这种情况下,它告诉编译器“就这些,这里没有函数定义。”

C/C++尽量使声明形式和使用形式一致。例如,假设a是另一个整数变量,上面的函数可以如下方式使用:

2.2 分段编译工具 - 图2

因为func1()返回的是一个整数,C/C++编译器要检查func1()的使用情况,以确保a能接受返回值,并且还要检查函数参数的类型匹配情况。

在函数声明时,可以给参数命名。编译器会忽略这些参数名,但对程序员来说它们可以帮助记忆。例如,我们有下面的形式声明func1(),它与前面的声明意义相同:

2.2 分段编译工具 - 图3

2.2.1.2 一点说明

对于带空参数表的函数,C和C++有很大的不同。在C语言中,声明

2.2 分段编译工具 - 图4

表示“一个可带任意参数(任意数目,任意类型)的函数”。这就妨碍了类型检查。而在C++语言中它就意味着“不带参数的函数”。

2.2.1.3 函数的定义

函数定义看起来像函数声明,但它还有函数体。函数体是一个用大括号括起来的语句集。大括号表示这段代码的开始和结束。为了定义函数体为空的(函数体不含代码)函数func1(),应当写为:

2.2 分段编译工具 - 图5

注意,在函数定义中,大括号代替了分号的作用,因为大括号括起了一条或一组语句,所以就不需要分号了。另外也要注意,如果要在函数体中使用参数的话,函数定义中的参数必须有名称(上面的函数没有用到定义的参数,因此在这里是可选的)。

2.2.1.4 变量声明的语法

对“变量声明”的解释向来很模糊且自相矛盾,而理解它准确的含义对于正确的理解定义和阅读程序十分重要。变量声明告知编译器变量的外表特征。这好像是对编译器说:“我知道你以前没有看到过这名字,但我保证它一定在某个地方,它是X类型的变量。”

函数声明包括函数类型(即返回值类型)、函数名、参数列表和一个分号。这些信息使得编译器足以认出它是一个函数声明并可识别出这个函数的外部特征。由此推断,变量声明应该是类型标识后面跟一个标识符。例如:

2.2 分段编译工具 - 图6

可以声明变量a是一个整数,这符合上面的逻辑。但这就产生了一个矛盾:这段代码有足够的信息让编译器为整数a分配空间,而且编译器也确实给整数a分配了空间。要解决这个矛盾,对于C/C++需要一个关键字来说明“这只是一个声明,它的定义在别的地方”。这个关键字就是extern,它表示变量是在文件以外定义的,或在文件后面部分才定义。

在变量定义前加extern关键字表示声明一个变量但不定义它,例如:

2.2 分段编译工具 - 图7

extern也可用于函数声明。例如:

2.2 分段编译工具 - 图8

这种声明方式和先前的func1()声明方式一样。因为没有函数体,编译器必定把它作为声明而不是函数定义。extern关键字对函数来说是多余的、可选的。C语言的设计者并不要求函数声明使用extern,这可能有些令人遗憾;如果函数声明也要求使用extern,那么在形式上与变量声明更加一致,从而减少了混乱(但这就需要更多的输入,这也许能解释为什么不要求函数使用extern的原因)。

下面是一些声明的例子:

2.2 分段编译工具 - 图9

函数声明时参数标识符是可选的。函数定义时则要求要有标识符(这里指C语言,而C++不要求)。

2.2.1.5 包含头文件

大部分的库包含众多的函数和变量。为了减少工作量,确保一致性,当对这些函数和变量做外部声明时,C/C++使用“头文件”(header file)。头文件是一个含有某个库的外部声明函数和变量的文件。它通常是扩展名为“.h”的文件,如headerfile.h(可能还会看到一些较老的程序使用其他扩展名,如“.hxx”或“.hpp”,但现在已经很少了)。

头文件由创建库的程序员提供。为了声明在库中已有的函数和变量,用户只需包含头文件即可。包含头文件,要使用#include预处理器命令。它告诉预处理器打开指定的头文件并在#include语句所在的地方插入头文件。#include有两种方式来指定文件:尖括号(<>)或双引号。

以尖括号指定头文件,如下所示:

2.2 分段编译工具 - 图10

用尖括号来指定文件时,预处理器是以特定的方式来寻找文件,一般是环境中或编译器命令行指定的某种寻找路径。这种设置寻找路径的机制随机器、操作系统、C++实现的不同而不同,要视具体情况而定。

以双引号指定文件,如下所示:

2.2 分段编译工具 - 图11

用双引号时,预处理器以“由实现定义的方式”来寻找文件。它通常是从当前目录开始寻找,如果文件没有找到,那么include命令就按与尖括号同样的方式重新开始寻找。

包含iostream头文件,要用如下语句

2.2 分段编译工具 - 图12

预处理器会找到iostream头文件(通常在“include”子目录下)并把它插入include语句所在位置。

2.2.1.6 标准C++include语句格式

随着C++的不断演化,不同的编译器厂商选用了不同的文件扩展名。而且,不同的操作系统对文件名有不同的限制,特别是对文件名长度限制。结果引起了对源代码的可移植性的限制。为了消除这些差别,标准使用的格式允许文件名长度可以大于众所周知的8个字符,去除了扩展名。例如,代替老式的包含iostream.h的语句

2.2 分段编译工具 - 图13

现在可以写成:

2.2 分段编译工具 - 图14

如果需要截短文件名和加上扩展名,翻译器会按照一定的方式来实现包含语句,以适应特定的编译器和操作系统。当然,如果想使用这种没有扩展名的风格,但编译器厂商没有提供这种支持,也可以将厂商提供的头文件拷贝成没有扩展名的文件。

从C继承下来的带有传统“.h”扩展名的库仍然可用。然而,也可以用更现代的C++风格使用它们,即在文件名前加一个字母“c”。这样

2.2 分段编译工具 - 图15

就变为:

2.2 分段编译工具 - 图16

对所有的标准C头文件都一样。这就为读者提供了一个区分标志,说明所使用的是C还是C++库。

新的包含格式和老的效果是不一样的:使用.h的文件是老的、非模板化的版本,而没有.h的文件是新的模板化版本。如果在同一程序中混用这两种形式,会遇到某些问题。