3.3 继承关系
C++编程风格(修订版)
现在让我们来看看程序中的继承关系。Stack通过一个保护数据成员vec来为每个派生类提供索引服务,其中vec指向的是一个void类型指针数组。当在派生类中执行入栈和出栈的操作时,Stack将调节私有数据成员top以在数组中移动。在派生类中又分配了一个数组来存储堆栈中的值,并将数组的地址保存在数据成员data中。此外,在派生类中还对vec进行了初始化,这样对于每一个i(只要在数组边界内),vec[i]将指向data[i]。从Stack::push()和Stack::pop()返回void类型指针将分别告诉派生类将对data中的哪一个元素进行入栈和出栈的操作。
图3.2 从vec指向data数组的指针
图3.2中给出了相关的数据结构,注意其中指针的形式是一致的。在这个数据结构中几乎没有包含任何信息。在派生类的构造函数中将执行下面的语句:
对于在data所指向数组中的每一个元素,在vec中都有相应的指针。在构造函数执行完之后,这些指针的值将不再改变。基类将通过这些指针来告诉派生类到什么地方去访问数据。在这种处理方中式存在着一些问题:在类中存在着过多的指针,但只是包含了少量的信息。
如果我们将注意力转移到函数IntStack::pop()中的类型转换上,那么就可以从另一个角度来看这个问题。
在上面的类型转换中,我们将Stack::pop()返回的void类型指针转换为int类型指针,以访问由IntStack::data指向的数组中的元素。类型转换是不安全的,为了避免这种潜在的危险,在编写程序时最好不要使用类型转换。我们可以很容易地去掉上面的类型转换,但这样作将会暴露出vec中的问题。通常的情况是,Stack::pop()返回vec[i],其中vec[i]指向的是data[i],然后再由IntStack::pop()来返回data[i]。如果Stack::pop()直接返回的是i而不是vec[i],那么就可以不需要进行类型转换。在获得了i值之后,IntStack::pop()就可以立刻得到data[i],而无需进行类型转换。因此,在Stack类的成员函数push()和pop()中,应该返回的是派生类真正需要的整数下标值,而不是在使用前需要进行转换的void类型指针。在程序清单3.2中给出了一个更简单的Stack,我们注意到,在IntStack和CharStack的成员函数push()和pop()中将不再进行类型转换。同时还注意到,Stack的名字被改为StackIndex,因为这个名字可以更好地描述这个类的抽象。
程序清单3.2 简化后的堆栈抽象
StackIndex类的新接口能够更好地反映出它所提供的抽象。这个抽象就是移动索引,从而告诉派生类到什么地方去访问数据。堆栈的索引行为信息被限制在StackIndex类中,而堆栈中具体的元素信息则被限制在派生类中。派生类与基类唯一的交流是通过整数类型的索引来进行的。在不损失功能的情况下,堆栈的抽象得到了进一步的简化。它的功能就只是维护一个索引,而由其他的派生类来提供具体的堆栈存储。
找出简单的抽象。
图3.3 简化之后的数据结构
最初的Stack::vec在StackIndex中是多余的,因此我们在类的定义中去掉了它。每个派生类都能够从StackIndex::push()和StackIndex::pop()返回的索引中得到需要的信息,因此也就不再需要在基类中保存指向在派生类中声明的数据的指针。图3.3中给出了现在的数据结构。简化后的程序可以进一步提高内存使用效率。在去掉了Stack::vec之后,程序就大大减少了在实现堆栈时需要的内存空间。在一个典型的32位系统结构中,整数的大小是4个字节,因此指针也是4个字节,与最初的版本相比,现在一个IntStack对象只使用了一半的内存空间:每个元素需要的是4个字节,而并非最初的8个字节。CharStack对象使用的内存空间则减少到了1/5,从每个元素由需要5个字节减少为只需要1个字节。