分类: C/C++
2008-11-28 14:54:32
事实上,C++程序使用头文件包含的不仅仅是类定义。回想一下,名字在使用前必须先声明或定义。到目前为止,我们编写的程序是把代码放到一个文件里来处理这个要求。只要每个实体位于使用它的代码之前,这个策略就有效。但是,很少有程序简单到可以放置在一个文件中。由多个文件组成的程序需要一种方法连接名字的使用和声明,在C++中这是通过头文件实现的。
为了允许把程序分成独立的逻辑块,C++支持所谓的分离编译(separate compilation)。这使得可由多个文件组建程序。为了支持分离编译,我们把Sales_item的定义放在一个头文件里面。我们后面在7.7节中定义的Sales_item成员函数将放在单独的源文件中。像main这样使用Sales_item对象的函数放在其他的源文件中,任何使用Sales_item的源文件都必须包含Sales_item.h头文件。
2.9.1 设计自己的头文件
头文件为相关声明提供了集中场所。头文件一般包含类的定义、extern变量的声明和函数的声明。函数的声明将在7.4节介绍。使用或定义这些实体的文件要包含适当的头文件。
头文件的正确使用能够带来两个好处:保证所有文件使用给定实体的同一声明;当声明需要修改时,只有头文件需要更新。
设计头文件还须注意以下几点:头文件中的声明在逻辑上应该是统一的。编译头文件需要一定的时间。如果头文件太大,程序员可能不愿意承受包含该头文件所带来的编译时代价。
为了减少处理头文件的编译时间,有些C++的实现支持预编译头文件。欲进一步了解详细情况,请参考你的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++中,有些地方需要放置常量表达式(2.7节)。例如,枚举成员的初始化式必须是常量表达式。在以后的章节中将会看到其他需要常量表达式的例子。
一般来说,常量表达式是编译器在编译时就能够计算出结果的表达式。当const整型变量通过常量表达式自我初始化时,这个const整型变量就可能是常量表达式。而const变量要成为常量表达式,初始化式必须为编译器可见。为了能够让多个文件使用相同的常量值,const变量和它的初始化式必须是每个文件都可见的。而要使初始化式可见,一般都把这样的const变量定义在头文件中。那样的话,无论该const变量何时使用,编译器都能够看见其初始化式。
但是,C++中的任何变量都只能定义一次(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 确定你的编译器提供了哪些提高警告级别的选项。使用这些选项重新编译以前选择的程序,察看是否会报告新的问题。
2.9.2 预处理器的简单介绍
既然已经知道了什么应该放在头文件中,那么我们下一个问题就是真正地编写头文件。我们知道要使用头文件,必须在源文件中#include该头文件。为了编写头文件,我们需要进一步理解#include指示是怎样工作的。#include设施是C++预处理器(preprocessor)的一部分。预处理器处理程序的源代码,在编译器之前运行。C++继承了C的非常精细的预处理器。现在的C++程序以高度受限的方式使用预处理器。
#include指示只接受一个参数:头文件名。预处理器用指定的头文件的内容替代每个#include。我们自己的头文件存储在文件中。系统的头文件可能用特定于编译器的更高效的格式保存。无论头文件以何种格式保存,一般都含有支持分离编译所需的类定义及变量和函数的声明。
1. 头文件经常需要其他头文件
头文件经常#include其他头文件。头文件定义的实体经常使用其他头文件的设施。例如,定义Sales_item类的头文件必须包含string库。Sales_item类含有一个string类型的数据成员,因此必须可以访问string头文件。
包含其他头文件是如此司空见惯,甚至一个头文件被多次包含进同一源文件也不稀奇。例如,使用Sales_item头文件的程序也可能使用string库。该程序不会(也不应该)知道Sales_item头文件使用了string库。在这种情况下,string头文件被包含了两次:一次是通过程序本身直接包含,另一次是通过包含Sales_item头文件而间接包含。
因此,设计头文件使其可以多次包含在同一源文件中,这一点很重要。我们必须保证多次包含同一头文件不会引起该头文件定义的类和对象被多次定义。使得头文件安全的通用做法,是使用预处理器定义头文件哨兵(header guard)。头文件哨兵用于避免在已经见到头文件的情况下重新处理该头文件的内容。
2. 避免多重包含
在编写头文件之前,我们需要引入一些额外的预处理器设施。预处理器允许我们自定义变量。
预处理器变量的名字必须在程序中唯一。任何与预处理器变量相匹配的名字的使用都关联到该预处理器变量。
为了避免名字冲突,预处理器变量经常用全大写字母表示。
预处理器变量有两种状态:已定义或未定义。定义预处理器变量和检测其状态用到不同的预处理器指示。#define指示接受一个名字并定义该名字为预处理器变量。#ifndef指示检测指定的预处理器变量是否未定义。如果预处理器变量未定义,那么跟在其后的所有指令都被处理,直到出现#endif。
可以使用这些设施来预防多次包含同一头文件:
#ifndef SALESITEM_H
#define SALESITEM_H
// Definition of Sales_item class and related functions goes here
#endif
条件指令
#ifndef SALESITEM_H
测试SALESITEM_H预处理器变量是否未定义。如果SALESITEM_H未定义,那么#ifndef测试成功,跟在#ifndef后面的所有行都被执行,直到发现#endif。相反,如果SALESITEM_H已定义,那么#ifndef指示测试为假,该指示和#endif指示间的代码都被忽略。
为了保证头文件在给定的源文件中只处理过一次,我们首先检测#ifndef。第一次处理头文件时,测试会成功,因为SALESITEM_H还未定义。下一条语句定义了SALESITEM_H。那样的话,如果我们编译的文件恰好又一次包含了该头文件。#ifndef指示会发现SALESITEM_H已经定义,并且忽略该头文件的剩余部分。
头文件应该含有哨兵,即使这些头文件不会被其他头文件包含。编写头文件哨兵并不困难,而且如果头文件被包含多次,它可以避免难以理解的编译错误。
当没有两个头文件定义和使用同名的预处理器常量时,这个策略相当有效。我们可以为定义在头文件里的实体(如类)命名预处理器变量来避免预处理器变量重名的问题。一个程序只能含有一个名为Sales_item的类。通过使用类名来组成头文件和预处理器变量的名字,可以使得很可能只有一个文件将会使用该预处理器变量。
3. 使用自定义的头文件
#include指示接受以下两种形式:
#include
#include "my_file.h"
如果头文件名括在尖括号(< >)里,那么认为该头文件是标准头文件。编译器将会在预定义的位置集查找该头文件,这些预定义的位置可以通过设置查找路径环境变量或者通过命令行选项来修改。使用的查找方法因编译器的不同而差别迥异。建议你咨询同事或者查阅编译器用户指南来获得更多的信息。如果头文件名括在一对引号里,那么认为它是非系统头文件,非系统头文件的查找通常开始于源文件所在的路径。
小结
类型是C++程序设计的基础。
每种类型都定义了其存储空间要求和可以在该类型的所有对象上执行的操作。C++提供了一组基本内置类型,如int、char等。这些类型与它们在机器硬件上的表示方式紧密相关。
类型可以为const或非const;const对象必须要初始化,且其值不能被修改。另外,我们还可以定义复合类型,如引用。引用为对象提供了另一个名字。复合类型是用其他类型定义的类型。
C++语言支持通过定义类来自定义类型。标准库使用类设施来提供一组高级的抽象概念,如IO和string类型。
C++是一种静态类型语言:变量和函数在使用前必须先声明。变量可以声明多次但是只能定义一次。定义变量时就进行初始化几乎总是个好主意。
access labels(访问标签) 类的成员可以定义为private,它可以保护成员不被使用该类型的代码访问。成员还可以定义为public,它使得成员在整个程序中都是可访问的。
address(地址) 数字,通过该数字可在存储器上找到一个字节。
arithmetic types(算术类型) 算术类型代表数值:整数和浮点数。浮点型值有三种类型:long double、double和float,分别表示扩展精度值、双精度值和单精度值。一般总是使用double型。特别地,float只能保证六位有效数字,这对于大多数的计算来说都不够。整型包括bool、char、wchar_t、short、int和long。整型可以是带符号或无符号的。一般在算术计算中总是避免使用short和char。unsigned可用于计数。bool类型只有true和false两个值。wchar_t类型用于扩展字符集的字符;char类型用于适合8个位的字符,比如Latin-1或者ASCII。
array(数组) 存储一组可通过下标访问的未命名对象的数据结构。本章介绍了存储字符串字面值的字符数组。第4章将会更加详细地介绍数组。
byte(字节) 最小的可寻址存储单元。大多数的机器上一个字节有8个位(bit)。
class(类) C++中定义数据类型的机制。类可以用class或struct关键字定义。类可以有数据和函数成员。成员可以是public或private。一般来说,定义该类型的操作的函数成员设为public;用于实现该类的数据成员和函数设为private。默认情况下,用class关键字定义的类其成员为private,而用struct关键字定义的类其成员为public。
class member(类成员) 类的一部分,可以是数据或操作。
compound type(复合类型) 用其他类型定义的类型,如引用。第4章将介绍另外两种复合类型:指针和数组。
const reference(const引用) 可以绑定到const对象、非const对象或右值的引用。const引用不能改变与其相关联的对象。
constant expression(常量表达式) 值可以在编译时计算出来的整型表达式。
constructor(构造函数) 用来初始化新建对象的特殊成员函数。构造函数的任务是保证对象的数据成员拥有可靠且合理的初始值。
copy-initialization(拷贝初始化) 用“=”表明变量初始化为初始化式的副本的初始化形式。
data member(数据成员) 组成对象的数据元素。数据成员一般应设为私有的。
declaration(声明) 断言程序中其他地方定义的变量、函数或类型的存在性。有些声明也是定义。只有定义才为变量分配存储空间。可以通过在类型前添加关键字extern来声明变量。名字直到定义或声明后才能使用。
default constructor(默认构造函数) 在没有为类类型对象的初始化式提供显式值时使用的构造函数。例如,string类的默认构造函数将新建的string对象初始化为空string,而其他构造函数都是用在创建string对象时指定的字符去初始化string对象。
definition(定义) 为指定类型的变量分配存储空间和可选择地初始化该变量。名字直到定义或声明后才能使用。
direct-initialization(直接初始化) 把用逗号分隔的初始化式列表放在圆括号内的初始化形式。
enumeration(枚举) 把命名整型常量集分组的类型。
enumerator(枚举成员) 枚举类型的有名字的成员。每个枚举成员都初始化为整型值且值为const。枚举成员可用在需要整型常量表达式的地方,比如数组定义的维度。
escape sequence(转义字符) 一种表示字符的可选择机制。通常用于表示不可打印字符如换行符或制表符。转义字符是反斜线后面跟着一个字符、一个3位八进制数或一个十六进制的数。C++语言定义的转义字符列在2.2节。转义字符还可用作字符字面值(括在单引号里)或用作字符串字面值的一部分(括在双引号里)。
global scope(全局作用域) 位于任何其他作用域外的作用域。
header(头文件) 使得类的定义和其他声明在多个源文件中可见的一种机制。用户定义的头文件以文件方式保存。系统头文件可能以文件方式保存,也可能以系统特有的其他格式保存。
header guard(头文件哨兵) 为防止头文件被同一源文件多次包含而定义的预处理器变量。
identifier(标识符) 名字。每个标识符都是字母、数字和下划线的非空序列,且序列不能以数字开头。标识符是大小写敏感的:大写字母和小写字母含义不同。标识符不能使用C++中的关键字,不能包含相邻的下划线,也不能以下划线后跟一个大写字母开始。
implementation(实现) 定义数据和操作的类成员(通常为private),这些数据和操作并非为使用该类型的代码所用。例如,istream和ostream类管理的IO缓冲区是它们的实现的一部分,但并不允许这些类的使用者直接访问。
initialized(已初始化的) 含有初始值的变量。当定义变量时,可指定初始值。变量通常要初始化。
integral types(整型) 见arithmetic types。
interface(接口) 由某种类型支持的操作。设计良好的类分离了接口和实现,在类的public部分定义接口,private部分定义实现。数据成员一般是实现的一部分。当函数成员是期望该类型的使用者使用的操作时,函数成员就是接口的一部分(因此为public);当函数成员执行类所需要的、非一般性使用的操作,函数成员就是实现的一部分。
link(链接) 一个编译步骤,此时多个目标文件放置在一起以形成可执行程序。链接步骤解决了文件间的依赖,如将一个文件中的函数调用链接到另一个文件中的函数定义。
literal constant(字面值常量) 诸如数、字符或字符串的值,该值不能修改。字面值字符用单引号括住,而字面值字符串则用双引号括住。
local scope(局部作用域) 用于描述函数作用域和函数内嵌套的作用域的术语。
lvalue(左值) 可以出现在赋值操作左边的值。非const左值可以读也可以写。
magic number(魔数) 程序中意义重要但又不明显的字面值数字。它的出现好像变魔术一般。
nonconst reference(非const引用) 只能绑定到与该引用同类型的非const左值的引用。非const引用可以修改与其相关联的对象的值。
nonprintable character(非打印字符) 不可见字符。如控制符、回退删除符、换行符等。
object(对象) 具有类型的一段内存区域。一个变量就是一个有名字的对象。
preprocessor(预处理器) 预处理器是作为C++程序编译的一部分运行的程序。预处理器继承于C语言,C++的特征大量减少了它的使用,但仍保存了一个很重要的用法:#include设施,用来把头文件并入程序。
private member(私有成员) 使用该类的代码不可访问的成员。
public member(公有成员) 可被程序的任何部分使用的类成员。
reference(引用) 对象的别名。定义如下:
type &id = object ;
定义id为object的另一名字。任何对id的操作都会转变为对object的操作。
run time(运行时) 指程序正执行的那段时间。
rvalue(右值) 可用于赋值操作的右边但不能用于左边的值。右值只能读而不能写。
scope(作用域) 程序的一部分,在其中名字有意义。C++含有下列几种作用域:
全局——名字定义在任何其他作用域外。
类——名字由类定义。
命名空间——名字在命名空间中定义。
局部——名字在函数内定义。
块——名字定义在语句块中,也就是说,定义在一对花括号里。
语句——名字在语句(如if、while和for语句)的条件内定义。
作用域可嵌套。例如,在全局作用域中声明的名字在函数作用域和语句作用域中都可以访问。
separate compilation(分离编译) 把程序分成多个分离的源文件的能力。
signed(带符号型) 保存负数、正数或零的整型。
staticlly typed(静态类型的) 描述进行编译时类型检查的语言(如C++)的术语。C++在编译时验证表达式使用的类型可以执行该表达式需要的操作。
struct 用来定义类的关键字。除非有特殊的声明,默认情况下struct的成员都为公有的。
type-checking(类型检查) 编译器验证给定类型的对象的使用方式是否与该类型的定义一致,描述这一过程的术语。
type specifier(类型标识符) 定义或声明中命名其后变量的类型的部分。
typedef 为某种类型引入同义词。格式:
typedef type synonym ;
定义synonym为名为type的类型的另一名字。
undefined behavior(未定义行为) 语言没有规定其意义的用法。编译器可以自由地做它想做的事。有意或无意地依赖未定义行为将产生大量难于跟踪的运行时错误和可移值性问题。
uninitialized(未初始化的) 没有指定初始值的变量。未初始化变量不是0也不是“空”,相反,它会保存碰巧遗留在分配给它的内存里的任何位。未初始化变量会产生很多错误。
unsigned(无符号型) 保存大于等于零的值的整型。
variable initialization(变量初始化) 描述当没有给出显式初始化式时初始化变量或数组元素的规则的术语。对类类型来说,通过运行类的默认构造函数来初始化对象。如果没有默认构造函数,那么将会出现编译时错误:必须要给对象指定显式的初始化式。对于内置类型来说,初始化取决于作用域。定义在全局作用域的对象初始化为0,而定义在局部作用域的对象则未初始化,拥有未定义值。
void type(空类型) 用于特殊目的的没有操作也没有值的类型。不可能定义一个void类型的变量。最经常用作不返回结果的函数的返回类型。
word(字) 机器上的自然的整型计算单元。通常一个字足以容纳一个地址。一般在32位的机器上,机器字长为4个字节。