教学目标
●了解多态性的概念
●了解怎样声明和使用实现多态性的虚函数
●了解抽象类和具体类的区别
●学会怎样声明建立抽象类的纯虚函数
●认识多态性是如何扩展和维护系统
●了解C++如何实现虚函数和动态关联
虚函数(virtual function)和多态性(Plymorphism)使得设计和实现易于扩展的系统成为可能。程序可以对层次中所有现有类的对象(基类对象)进行一般性处理。程序开发期间不存在的类可以用一般化程序稍作修改或不经修改即加进去,只要这些类属于一般处理的继承层次。程序中惟一要修改的部分是需要直接了解加进层次中的特定类的部分。
处理多种不同类型对象的手段之一是使用switch语句。switch语句能够根据每一种对象的类型选择对该对象合适的操作。例如,在形状层次中,每个形状指定自己的类型数据成员,switch结构可以根据特定对象的类型确定调用哪个print函数。
但是,使用switch逻辑存在许多问题。例如,程序员可能会忘记应有的类型测试;在一条switch语句中可能会忘记测试所有可能的情况;在修改基于switch语句的系统时可能会忘记在现有的switch语句中插入新类;为了处理新的类型,每次修改switch语句都要修改系统中的每一条switch语句,这很费时并且容易出错。
正如以后会看到的,利用了虚函数和多态性的程序设计无需使用switch逻辑。程序员可以用虚函数机制自动完成等价的逻辑,因而避免与switch逻辑有关的各种各样的错误。
使用虚函数和多态性可简化源代码的长度。为支持更简单的顺序代码,虚函数和多态性包含的分支逻辑更少。这种简化有助于程序的测试、
调试和维护。
假定一组形状类(如Circle、Trriangle、Rectangle和Square等等)都是从基类Shape派生出来的。在面向对象的程序设计中,我们可能要使每一个这样的类都能够绘制其自身形状。尽管每个类都有它自己draw函数,但是绘制每种形状的draw函数却是大不相同的。当需要绘制形状时,不管它是什么形状,把它作为基类Shape的对象处理是再好不过的。然后,我们只需要简单地调用基类Shape的函数draw,并让程序动态地确定(即在执行时确定)使用哪个派生类的draw函数。
为了使这种行为可行,我们把基类中的函数draw声明为虚函数,然后在每个派生类中重新定义draw使之能够绘制合适的形状。虚函数的声明方法是在基类的函数原型前加上关键字virtual。例如,基类Shape中可能出现:
virtual void draw() const;
上述原型声明函数draw是不取参数也不返回数值的常量函数,而且是个虚函数。
一旦一个函数被声明为虚函数,即使重新定义类时没有声明虚函数,那么它从该点之后的继承层次结构中都是虚函数。
虽然函数在类层次结构的高层中声明为虚函数会使它在低层隐式地成为虚函数,但有些程序员为了提高程序的清晰性更喜欢在每一层中显式地声明这些虚函数。
没有定义虚函数的派生类简单地继承其直接基类的虚函数。
如果在基类中将函数draw声明为virtual,然后用基类指针或引用指明派生类对象并使用该指针调用draw函数(如shapePtr->draw()),则程序会动态地(即在运行时)选择该派生类的draw函数,这称为动态关联(见10.6和10.9节的实例研究)。
如果用名字和圆点成员选择运算符引用一个特定的对象来调用虚函数(如squareObject.draw()),则被调用虚函数是在编译时确定的(称为静态关联),调用的虚函数也就是为该特定对象的类定义(或继承该特定对象类)的函数。
当我们把类看作一种数据类型时,我们通常认定该类型的对象是要被实例化的。但是,在许多情况下,定义不实例化为任何对象的类是很有用处的,这种类称为“抽象类”(abstract class)。因为抽象类要作为基类被其他类继承,所以通常也把它称为“抽象基类”(abstract base class)。抽象基类不能用来建立实例化的对象。
抽象类的惟一用途是为其他类提供合适的基类,其他类可从它这里继承和(或)实现接口。能够建立实例化对象的类称为具体类(concrete class )。
例如,我们可以建立抽象基类TwoDimensionalObject,然后从它派生出具体类Square、Circle和Triangle等等,也可以建立抽象基类ThreeDimensionalObject,然后从它派生出具体类Cube、Sphere和Cylinder等等。这些抽象基类表述的含义因为太广泛而定义不出实在的对象。如果要建立实例对象,则需要含义更加明确的类,,这就是所谓的“具体类”。具体类具有足以能够建立实例化对象的明确含义。
如果将带有虚函数的类中的一个或者多个虚函数声明为纯虚函数,则该类就成为抽象类。纯虚函数是在声明时”初始化值”为0的函数,例如:
virtual float earnings() const = O; // pure virtual
如果某个类是从一个带有纯虚函数的类派生出来的,并且没有在该派生类中提供该纯虚函数的定义,则该虚函数在派生类中仍然是纯虚函数,因而该派生类也是一个抽象类。
试图实例化一个抽象类对象(即包合一个或者多个纯虚函数的类)是一种语法错误。
一个类层次结构中可以不包含任何抽象类,但是正如以后会看到的,很多良好的面向对象的系统,其类层次结构的顶部是一个抽象基类。在有些情况中,类层次结构顶部有好几层都是抽象类。
形状类的层次结构就是一种典型的范例。我们可以在该层次结构的顶部建立抽象基类shape,在往下的一层中还可以再建立两个抽象基类,即二维形状类TwoDimensionalShape和三维形状类ThreeDimensionalShape,再往下我们就可以开始定义二维形状的具体类如圆形类和正方形类以及三维形状的具体类如球类和立方体类等等。
C++支持多态性。所谓多态性是指:通过继承相关的不同的类,他们的对象能够对同一个函数调用作出不同的响应。例如,如果类Rectangle是从类Quadrilateral派生出来的,那么类Rectangle的对象比类Quadrilateral的对象的更具体,对类Quadfilateral的对象的操作(如计算周长和面积)也能用在类Rextangle的对象上。
多态性是通过虚函数实现的。当通过基类指针(或引用)请求使用虚函数时,C++会在与对象关联的派生类中正确地选择重定义的函数。
有时候在基类中定义的非虚函数会在派生类中重新定义。如果用基类指针调用该成员函数,则选择基类版本的成员函数;如果用派生类指针调用该成员函数,则选择派生类版本的成员函数。这不是多态性行为。
下面的例子使用图9.5的基类Employee和派生类HourlYWorker:
Employee e, *ePtr = &e; HourlyWorker h, *hPtr = &h; ePtr->print(); // call base-class print function hPtr-> print(); // call derived-class print function ePtr = &h; // allowable implicit conversion ePtr->print(); // still calls base-class print
基类Employee和派生类HourlyWorker都定义了自己的print函数。由于这个函数没有声明为virtual,而且签名相同,因此通过Employee指针调用print函数时调用Employee::print() (不管Employee指针指向基类对象还是派生类HourlyWorker对象),而通过HourlyWorker指针调用print函数则调用Worker::print()。派生类也可以调用基类函数,但派生类对象通过派生类对象的指针调用基类print时,函数要显式调用如下:
hPtr-> Employee::print(); // call base—class print function
表示调用基类print。
使用虚函数和多态性能够使成员函数的调用根据接收到该调用的对象的类型产生不同的动作(但会需要少量执行时的开销)多态性赋予了程序员极大的灵活性。下面几节要举例说明多态性和虚函数的功能。
利用虚函数和多态性,程序员可以处理普遍性而让执行环境处理特殊性。即使在不知道一些对象的类型的情况下,程序员也可以命令各种各样的对象表现出适合这些对象妁行为。
多态性提高了可扩展性:处理多态性行为的软件可以用与接收消息的对象类型无关的方式编写。因此,不必修改基本系统应可以把能够响应现有消息的新类型的对象添加到系统中。除了实例化新对象的客户代码需要重新编译外,程序无需重新编译。
抽象类为类层次结构中的各个成员定义接口。抽象类中包含了要在派生类中定义的纯虚函数,该层次结构中的所有函数都可以通过多态性使用同样的接口。
尽管不能实例化抽象基类的对象,但是可以声明引用抽象基类的指针。当实例化了具体类的对象后,可以用这种指针使派生类对象具有多态操作能力。
下面考虑一个应用多态性和虚函数的例子。一个屏幕管理程序需要显示各种各样的对象,甚至包括在屏幕管理程序编写后又添加到系统中的新类型的对象。系统可能需要显示各种各样的形状,例如正方形、圆形、三角形、矩形等等(每一个类都是基类Shape的派生类)。屏幕管理程序使用基类指针(指向Shape)来管理要显示的对象。为了能够绘制所有的对象(不管该对象在继承层次结构中的哪一层),管理程序都是使用指向该对象的基类指针并向该对象简单地发送一条draw消息。函数draw在基类Shape中被声明为纯虚函数,并且在每一个派生类中被重新定义,每个对象都知道如
何绘制自身。屏幕管理程序不必关心这些细节内容,它只要简单地告诉每个对象进行绘制即可。
多态性特别适合于实现分层的软件系统。例如,在操作系统中各种类型的物理设备彼此之间的操作是不同的,然而从设备读取数据和把数据写入设备的命令在某种程度是统一的。发送给设备驱动程序对象的“写”消息(write函数调用)需要在该设备驱动程序的上下文中具体地解释,并且还要解释设备驱动程序是如何操作该特定类型设备的。但是,write调用本身和对任何其他对象的write调用实际上没有什么区别,都只是把内存中一定数目的字节放在设备中。面向对象的操作系统可能会用抽象基类为所有设备驱动程序提供合适的接口,然后通过继承抽象基类生成执行所有类似操作
的派生类。设备驱动程序所提供的功能(即public接口)在抽象基类中则是以纯虚函数形式出现的,派生类中提供了这些虚函数的实现.已实现的函数能够响应特定类型的设备驱动程序。
利用多态编程,程序可以从类层次的不同层中遍历对象的指针数组。这种数组中的指针都是派生类对象的基类指针。例如,TwoDimensionalshape类的对象数组可以包含指向派生类Square、Circle、Triangle、Rectangle和Line等对象的TwoDimensionalShape *指针。使用多态编程时,发出一个绘制数组中每个对象的消息即可在屏幕上画出正确的图形