第6章 初始化与清除

第4章采用一个典型C语言库中所有分散的构件,并把它们封装进一个结构(一个抽象数据类型,从现在起,称其为一个类),从而在库的应用方面作出了重大改进。

这样不仅为访问库构件提供了统一的入口,也用类名隐藏了类内部的函数名。在第5章中,我们介绍了访问控制(实现隐藏)。这就为类的设计者提供了一种设立边界的途径,通过边界的设立来决定哪些是允许客户程序员处理的,哪些是禁止客户程序员处理的。这意味着,对某种数据类型进行操作的内部机制处于类的设计者控制之下,可以由他们斟酌决定,这样也可以让客户程序员清楚哪些成员是他们能够使用并应该加以注意的。

封装和访问控制在改进库的易用性方面取得了重大进展。它们提供的“新的数据类型”的概念在某些方面比来自C语言的现有的内置数据类型要好。现在,C++编译器可以为这种新的数据类型提供类型检查保证,从而在使用这种数据类型时就确保了一定程度的安全性。

当然,说到安全性,C++的编译器能比C编译器提供更多的功能。在本章及以后的章节中,我们将看到C++的另外一些特征。它们可以让程序中的错误充分暴露,有时甚至在编译这个程序之前,帮助查出错误,但通常是编译器的警告和出错信息。基于这个原因,我们不久就会习惯于这样一种情景:一个C++程序在第一次编译时就能正确运行。

安全性问题包括初始化和清除两个方面。在C语言中,如果程序员忘记了初始化或清除一个变量,就会出现一大段程序错误。这在一个C库中尤其如此,特别是当客户程序员不知如何初始化一个struct,或甚至不知道他们必须要初始化一个struct时。(库中通常不包含初始化函数,所以客户程序员不得不自己手工初始化struct。)清除是一个特殊问题,因为C程序员一旦用过一个变量后就会把它忘记,所以对于一个库的struct来说必要的清除工作往往会被遗忘。

在C++中,初始化和清除的概念是简化库的使用的关键所在,并可以减少那些在客户程序员忘记去完成这些操作时会引起的细微错误。本章就来讨论C++的这些特征,它们有助于保证正常的初始化和清除。

6.1 用构造函数确保初始化

在Stash和Stack类中都曾调用initialize()函数,这个函数名暗示无论用什么方法使用这些对象都应当在对象使用之前调用这一函数。不幸的是,这要求客户程序员必须正确地初始化。而客户程序员在专注于用那令人惊奇的库来解决问题的时候,往往忽视了初始化的细节。在C++中,初始化实在太重要了,不应该留给客户程序员来完成。类的设计者可以通过提供一个叫做构造函数(constructor)的特殊函数来保证每个对象都被初始化。如果一个类有构造函数,编译器在创建对象时就自动调用这一函数,这一切在客户程序员使用他们的对象之前就已经完成了。是否调用构造函数不需要客户程序员来考虑,它是由编译器在对象定义时完成的。

接下来的问题是这个函数叫什么名字。这必须考虑两点,首先这个名字不能与类的其他成员函数冲突,其次,因为该函数是由编译器调用的,所以编译器必须总能知道调用哪个函数。Stroustrup的方法似乎是最简单也最符合逻辑的:构造函数的名字与类的名字一样。这样的函数在初始化时会自动被调用。

下面是一个带构造函数的类的简单例子:

第6章 初始化与清除 - 图1

现在当一个对象被定义时:

第6章 初始化与清除 - 图2

这时就好像a是一个int一样:为这个对象分配内存。但是当程序执行到a的序列点(sequence point)执行的点时,构造函数自动被调用,因为编译器已悄悄地在a的定义点处插入了一个X:X()的调用。就像其他成员函数被调用一样。传递到构造函数的第一个(秘密)参数是this指针,也就是调用这一函数的对象的地址,不过,对构造函数来说,this指针指向一个没有被初始化的内存块,构造函数的作用就是正确的初始化该内存块。

像其他函数一样,也可以通过向构造函数传递参数,指定对象该如何创建或设定对象初始值,等等。构造函数的参数保证对象的所有部分都被初始化成合适的值。举例来说,如果类Tree有一个带整型参数的构造函数,用以指定树的高度,那么就必须这样来创建一个树对象:

第6章 初始化与清除 - 图3

如果Tree(int)是惟一的构造函数,编译器将不会用任何其他方法来创建一个对象(在下一章将看到多个构造函数以及调用它们的不同方法)。

关于构造函数就全部介绍完了。构造函数有着特殊的名字,在每个对象创建时,编译器自动调用的函数。尽管构造函数简单,但是它解决了类的很多问题,并使得代码更容易读写。例如在前面的代码段中,对有些initialize()函数并没有看到显式的调用,这些函数从概念上说是与定义分开的。在C++中,定义和初始化是集为一体的,不能只取其中之一。

构造函数和析构函数是两个非常特殊的函数:它们没有返回值。这与返回值为void的函数显然不同。后者虽然也不返回任何值,但还可以让它做点别的事情,而构造函数和析构函数则不允许。在程序中创建和消除一个对象的行为非常特殊,就像出生和死亡,而且总是由编译器来调用这些函数以确保它们被执行。如果它们有返回值,要么编译器必须知道如何处理返回值,要么就只能由客户程序员自己来显式地调用构造函数与析构函数,这样一来,安全性就被破坏了。