16.4 作为模板的Stash和Stack

贯穿本书反复讨论的Stash和Stack容器类面临的“所有权”问题,源于我们还不能确切地知道这些容器包含的是什么类型。最近出现的是Object的Stack容器,这在第15章最后的OStackTest.cpp中已经看到了。

如果客户程序员不显式地移去所有指向存放在容器中对象的指针,则容器应当能正确地删除这些指针。这就是说,容器“拥有”不被移走的对象,负责清除它们。问题是这个清除要求关于对象类型的知识,而创造一个一般性的容器类不要求关于对象类型的知识。然而,利用模板,我们可以编写不知道对象类型的代码,并且对于我们希望包含的每种类型,我们可以更容易地实例化这个容器的新版本。个别的已实例化的容器不知道它们保存的对象的类型,但能调用正确的析构函数(假定在典型情况下包含多态性,这时已提供了虚析构函数)。

对于Stack,结果很简单,因为所有成员函数都能合理地内联。

16.4 作为模板的Stash和Stack - 图1

16.4 作为模板的Stash和Stack - 图2

如果将它与第15章最后的例子OStack.h相比较,我们可以看到Stack实际上相同,除了Object已经用T替换以外。测试程序也近似相同,除了消除了从string和Object多重继承的必要性(甚至对于Object本身的需要)以外。现在,没有MyString类宣布它的销毁,所以增加了一个小的新类来显示Stack容器清除它的对象。

16.4 作为模板的Stash和Stack - 图3

X的析构函数是虚的,这里不是因为需要如此,而是因为xx稍后能用来存放从X派生的对象。

注意,对于string和对于X创造不同种类的Stack是多么容易。由于模板的存在,我们可以得到两方面的好处—即Stack类容易使用和正确清除。

16.4.1 模板化的指针Stash

重新组织PStash代码成为模板并不简单,因为有一些成员函数不应当内联。但是,作为一个模板,这些函数定义仍然存放在头文件中(编译器和连接器处理多定义问题)。代码看上去非常类似于通常的PStash,除了增量的大小(由inflate()使用)已经被模板化为具有默认值的无类参数以外,所以这个增量的大小能在实例化时修改(注意,这意味着增量大小是固定的,尽管有人会争辩增量大小应当在对象的整个生命期中都是可以改变的)。

16.4 作为模板的Stash和Stack - 图4

16.4 作为模板的Stash和Stack - 图5

在这里使用的默认增量大小是很小的,以便保证能发生对inflate()的调用。我们采用的这种方法可以确保工作正确。

为了测试模板化的PStash的控制权,下面的类将报告自身的创建和销毁,并保证被创建的对象都能被销毁。AutoCounter只允许它的类型的对象在栈上创建。

16.4 作为模板的Stash和Stack - 图6

16.4 作为模板的Stash和Stack - 图7

AutoCounter类做两件事。第一,它继续对AutoCounter的每个实例编号:这个编号的值保存在id中,并且使用static数据成员count来生成这个编号。

第二,更复杂,嵌套类CleanupCheck的一个静态实例(称为verifier)跟踪被创建和销毁的所有的AutoCounter对象,如果程序员没有完全清除它们,它就向程序员报告(也就是假定这里有一个内存泄漏)。这个行为是使用标准C++类库中的set类完成的,这是良好设计的模板如何能方便使用的极好例子(在本书的第2卷,我们可以学习C++标准类库中的所有容器)。

set类是按照它所包含的类型建立模板的;在这里,它被实例化为包含AutoCounter指针的实例。一个set只允许每个不同对象的一个实例被添加;在add()中,这由set:insert()函数完成。如果我们正在试图添加先前已经添加过的内容,insert()就用它的返回值通知我们。然而,因为对象地址被添加,所以我们可以依靠C++保证所有对象有惟一的地址。

在remove()中,使用set:erase()从set中移出AutoCounter指针。返回值告诉我们这个元素的多少个实例被移出。在这种情况下,我们只希望返回0或1。如果返回值是0,表示这个对象已经从set中删除,并且这是第二次试图删除它,这是一个程序设计错误,可以通过require()报告这个错误。

CleanupCheck的析构函数最后检查set的长度是否确实是0。如果是0,表示它的所有对象都已经被完全清除。如果不是0,说明有内存泄漏,可以通过require()报告这个错误。

AutoCounter的构造函数和析构函数用verifier对象注册和注销它们自己。注意,构造函数、拷贝构造函数以及赋值运算符都是private的,所以创建对象的惟一方法是用static create()成员函数,这是factory的一个简单例子,它保证所有的对象都在堆上创建,所以verifier对于赋值和拷贝构造不会混淆。

因为所有的成员函数都是内联的,所以使用实现文件的惟一原因是为了包含静态数据成员的定义。

16.4 作为模板的Stash和Stack - 图8

利用手边的AutoCounter,我们现在可以测试PStash的功能。下面的例子不仅表明PStash析构函数清除了它现在所拥有的对象,而且还表明AutoCounter类如何检测到还没有被清除的对象。

16.4 作为模板的Stash和Stack - 图9

16.4 作为模板的Stash和Stack - 图10

当从PStash中移出AutoCounter元素5和元素6时,它们就变成了调用者的责任,但是因为调用者没有清除它们,所以就引起了内存泄漏,它们随后在运行时被AutoCounter检测到。

当我们运行这个程序时,会看到错误信息不像希望的那样详细。如果在系统中使用AutoCounter中所描述的方案去发现内存泄漏,也许希望打印出关于未被清除对象的更详细的信息。本书的第2卷表明了做这件事情的更好方法。