2.9 编写自己的头文件
我们已经从1.5节了解到,一般类定义都会放入头文件(header file)。在本节中我们将看到怎样为Sales_item类定义头文件。
事实上,C++(www.cppentry.com)程序使用头文件包含的不仅仅是类定义。回想一下,名字在使用前必须先声明或定义。到目前为止,我们编写的程序是把代码放到一个文件里来处理这个要求。只要每个实体位于使用它的代码之前,这个策略就有效。但是,很少有程序简单到可以放置在一个文件中。由多个文件组成的程序需要一种方法连接名字的使用和声明,在C++(www.cppentry.com)中这是通过头文件实现的。
为了允许把程序分成独立的逻辑块,C++(www.cppentry.com)支持所谓的分离编译(separate compilation)。这使得可由多个文件组建程序。为了支持分离编译,我们把Sales_item的定义放在一个头文件里面。我们后面在7.7节中定义的Sales_item成员函数将放在单独的源文件中。像main这样使用Sales_item对象的函数放在其他的源文件中,任何使用Sales_item的源文件都必须包含Sales_item.h头文件。
2.9.1 设计自己的头文件
头文件为相关声明提供了集中场所。头文件一般包含类的定义、extern变量的声明和函数的声明。函数的声明将在7.4节介绍。使用或定义这些实体的文件要包含适当的头文件。
头文件的正确使用能够带来两个好处:保证所有文件使用给定实体的同一声明;当声明需要修改时,只有头文件需要更新。
设计头文件还须注意以下几点:头文件中的声明在逻辑上应该是统一的。编译头文件需要一定的时间。如果头文件太大,程序员可能不愿意承受包含该头文件所带来的编译时代价。
为了减少处理头文件的编译时间,有些C++(www.cppentry.com)的实现支持预编译头文件。欲进一步了解详细情况,请参考你的c++实现的手册。
编译和链接多个源文件
要产生可执行文件,我们不但要告诉编译器到哪里去查找main函数,而且还要告诉编译器到哪里去查找Sales_item类所定义的成员函数的定义。假设我们有两个文件:main.cc含有main函数的定义,Sales_item.cc含有Sales_item的成员函数。我们可以按以下方式编译这两个文件:
$ CC -c main.cc Sales_item.cc # by default generates a.exe # some compilers generate a.out # puts the executable in main.exe $ CC -c main.cc Sales_item.cc -o main |
其中$是我们的系统提示符,#开始命令行注释。现在我们可以运行可执行文件,它将运行我们的main程序。
如果我们只是修改了一个.cc源文件,较有效的方法是只重新编译修改过的文件。大多数编译器都提供了分离编译每一个文件的方法。通常这个过程产生.o文件,.o扩展名暗示该文件含有目标代码。
编译器允许我们把目标文件链接在一起以形成可执行文件。我们所使用的系统可以通过命令名CC调用编译。因此可以按以下方式编译程序:
$ CC -c main.cc # generates main.o $ CC -c Sales_item.cc # generates Sales_item.o $ CC main.o Sales_item.o # by default generates a.exe; # some compilers generate a.out # puts the executable in main.exe $ CC main.o Sales_item.o -o main |
你将需要检查你的编译器的用户手册,了解如何编译和执行由多个源文件组成的程序。
许多编译器提供了增强其错误检测能力的可选功能。检视你的编译器的用户指南,看有哪些额外的检测方法。
1. 头文件用于声明而不是用于定义
当设计头文件时,记住定义和声明的区别是很重要的。定义只可以出现一次,而声明则可以出现多次(2.3.5节)。下列语句是一些定义,所以不应该放在头文件里:
extern int ival = 10; // initializer, so it's a definition double fica_rate; // no extern, so it's a definition |
虽然ival声明为extern,但是它有初始化式,代表这条语句是一个定义。类似地,fica_rate的声明虽然没有初始化式,但也是一个定义,因为没有关键字extern。同一个程序中有两个以上文件含有上述任一个定义都会导致多重定义链接错误。
因为头文件包含在多个源文件中,所以不应该含有变量或函数的定义。
对于头文件不应该含有定义这一规则,有三个例外。头文件可以定义类、值在编译时就已知道的const对象和inline函数(7.6节介绍inline函数)。这些实体可在多个源文件中定义,只要每个源文件中的定义是相同的。
在头文件中定义这些实体,是因为编译器需要它们的定义(不只是声明)来产生代码。例如:为了产生能定义或使用类的对象的代码,编译器需要知道组成该类型的数据成员。同样还需要知道能够在这些对象上执行的操作。类定义提供所需要的信息。在头文件中定义const对象则需要更多的解释。
2. 一些const对象定义在头文件中
回想一下,const变量(2.4节)默认为定义该变量的文件的局部变量。正如我们现在所看到的,这种默认的原因在于允许const变量定义在头文件中。
在C++(www.cppentry.com)中,有些地方需要放置常量表达式(2.7节)。例如,枚举成员的初始化式必须是常量表达式。在以后的章节中将会看到其他需要常量表达式的例子。
一般来说,常量表达式是编译器在编译时就能够计算出结果的表达式。当const整型变量通过常量表达式自我初始化时,这个const整型变量就可能是常量表达式。而const变量要成为常量表达式,初始化式必须为编译器可见。为了能够让多个文件使用相同的常量值,const变量和它的初始化式必须是每个文件都可见的。而要使初始化式可见,一般都把这样的const变量定义在头文件中。那样的话,无论该const变量何时使用,编译器都能够看见其初始化式。
但是,C++(www.cppentry.com)中的任何变量都只能定义一次(2.3.5小节)。定义会分配存储空间,而所有对该变量的使用都关联到同一存储空间。因为const对象默认为定义它的文件的局部变量,所以把它们的定义放在头文件中是合法的。
这种行为有一个很重要的含义:当我们在头文件中定义了const变量后,每个包含该头文件的源文件都有了自己的const变量,其名称和值都一样。
当该const变量是用常量表达式初始化时,可以保证所有的变量都有相同的值。但是在实践中,大部分的编译器在编译时都会用相应的常量表达式替换这些const变量的任何使用。所以,在实践中不会有任何存储空间用于存储用常量表达式初始化的const变量。
如果const变量不是用常量表达式初始化,那么它就不应该在头文件中定义。相反,和其他的变量一样,该const变量应该在一个源文件中定义并初始化。应在头文件中为它添加extern声明,以使其能被多个文件共享。
习题
习题2.31 判别下列语句哪些是声明,哪些是定义,请解释原因。
(a) extern int ix = 1024 ; (b) int iy ; (c) extern int iz ; (d) extern const int &ri ; |
习题2.32 下列声明和定义哪些应该放在头文件中?哪些应该放在源文件中?并解释原因。
(a) int var ; (b) const double pi = 3.1416; (c) extern int total = 255 ; (d) const double sq2 = squt (2.0)
|
习题2.33 确定你的编译器提供了哪些提高警告级别的选项。使用这些选项重新编译以前选择的程序,察看是否会报告新的问题。
【责任编辑:
董书 TEL:(010)68476606】