1.2.4 子类化(1)
子类化(在统一建模语言(UML)中也称作泛化或继承)是一种表示类是另一个类的专门方式。基本的规则是,专属类继承了更多一般类的功能。也就是说,其他一般类(也称作基类或父类)的所有功能也是专属类(也称作派生类或子类)的功能,但是后者允许特化和扩展一般类。
子类化最主要的优势是能够重用设计和代码。如果我为一个专门的上下文设计一个类,并且想以后在不同的上下文中重用,那么可以仅仅设计和编码专有的功能和扩展功能,而不需要触碰基类。此外,在使用时,可以编写算法操作类型的一个实例,而不需要考虑它是谁的子类,并且当需要的时候,能修改行为,通过子类保证语义的正确性。
通过"特化",我的意思是专属类,能够重新定义基类提供的操作。因为子类是基类的特殊形式,在基类的很多实例中,合理的操作在派生类的实例中也是合理的。但是,该操作的实现一定会稍有不同。例如,如果我写来自传感器队列数据的代码,将会有insert(int value)、int remove()、int getSize()等操作;如果我创建一个特殊种类的队列,它能够存储超出最大存储设备很多的数据,如闪存驱动器,这些操作仍然很合理,但是它们的实现就非常困难。注意,实例化是专门改变操作的实现,而不是重新定义数据。
实例化能够很容易通过前面内容中提到的switch-case语句定义完成,但是,如果使用函数指针代替则更通用。那么设计者能够很容易地编写专有功能,并且创建新的构造函数指向新的功能去替代旧的。也就是说,子类能够通过在不同的函数中插入指针重载操作,来提供专有的行为。缺点是,函数指针有一点棘手,而且在C程序中,指针是程序员诱生缺陷的主要原因。
通过"扩展",我的意思是子类将有一些新功能,如新属性或新操作。在数据队列的实例中,这意味着我能加入新的属性,如用于存储缓存数据到闪存驱动器的数据文件的名字,以及一些新的操作如flush()和load(),以便将内存缓冲区的数据写入闪存或从闪存驱动器上将数据读入内存缓冲区。图1-2中的UML类图展示了Queue类和CachedQueue类。为了简化问题,我们假设从不滥用队列(这样不必考虑超出或下溢的情况),并且想存储最大100整数到队列中去。
如果你想增加一些难度,自由提升例子水平而加入了处理下溢和溢出的处理,同时使用C预处理宏来改变队列中数据的类型和元素的个数。不要担心,我会一直等到你做完。CachedQueue类在概念上简单明了。如果在插入缓冲区中有空间,那么就像普通队列一样插入数据。但是,如果你填满了内部缓冲区,那么insert()操作将会调用flush()写入磁盘之外的插入缓冲区,然后重置内部缓冲区。对于删除数据,稍有一些复杂。如果内部缓冲区为空,则remove()调用load()函数从硬盘上读取最旧的数据。理解UML类图
图1-2所示类图包含了一些经典UML类图的特性。方框表示类,线表示类之间的关系。类框(可选)分割为三个段,最上面的段包含类的名字,中间的段列出属性(数据成员)和它们的类型,最下面的段表示类的操作。
图1-2展示了两种不同的关系。带有封闭箭头的线是泛化关系,线指向多个基类;另一条线称作组合。组合关系隐含很强的所属关系,而且在关系的另一方面还有创建和销毁类的实例的职责。开放箭头代表导航(这个例子中拥有者有一个指针指向一个部分,而不是以其他方式),接近箭头的名字是CachedQueue类中这个指针的名字,箭头周围的数字是阶元--指发挥作用需要的实例数(在这个例子中,仅需一个实例)。
本书将会通过类图展示模式和例子,尽管重点是使用代码表示。附录A有UML的简要介绍。如果你想要获取更多的细节,请参看任意关于UML的书籍,如我的书《Real-Time UML 3rd Edition:Advances in the UML for Real-Time Systems》(Addison-Wesley,2004)。
图1-2 Queue和Cached Queue
C语言至少有一种方法可以实现子类化。这里使用的方法,为了处理特殊化,我们将使用一些前面内容中描述过的成员函数指针。为了扩展,我们将简单地把基类作为一个结构体嵌入到子类中。
例如,让我们看看图1-2中队列例子的代码。代码清单1-5是Queue类的头文件,它的结构就像你期待的那样。
代码清单1-5 Queue类头部
代码清单1-6显示Queue类操作的(简单)实现,代码清单1-7提供一个简单测试程序,显示在队列中插入和删除元素。
代码清单1-6 Queue类实现
代码清单1-7 队列测试程序