📄 10章 虚函数与多态性.txt
字号:
C++大学教程(第10章 虚函数与多态性)
教学目标
●了解多态性的概念
●了解怎样声明和使用实现多态性的虚函数
●了解抽象类和具体类的区别
●学会怎样声明建立抽象类的纯虚函数
●认识多态性是如何扩展和维护系统
●了解C++如何实现虚函数和动态关联
10.1 简介
虚函数(virtual function)和多态性(Plymorphism)使得设计和实现易于扩展的系统成为可能。程序可以对层次中所有现有类的对象(基类对象)进行一般性处理。程序开发期间不存在的类可以用一般化程序稍作修改或不经修改即加进去,只要这些类属于一般处理的继承层次。程序中惟一要修改的部分是需要直接了解加进层次中的特定类的部分。
10.2 类型域和switch语句
处理多种不同类型对象的手段之一是使用switch语句。switch语句能够根据每一种对象的类型选择对该对象合适的操作。例如,在形状层次中,每个形状指定自己的类型数据成员,switch结构可以根据特定对象的类型确定调用哪个print函数。
但是,使用switch逻辑存在许多问题。例如,程序员可能会忘记应有的类型测试;在一条switch语句中可能会忘记测试所有可能的情况;在修改基于switch语句的系统时可能会忘记在现有的switch语句中插入新类;为了处理新的类型,每次修改switch语句都要修改系统中的每一条switch语句,这很费时并且容易出错。
正如以后会看到的,利用了虚函数和多态性的程序设计无需使用switch逻辑。程序员可以用虚函数机制自动完成等价的逻辑,因而避免与switch逻辑有关的各种各样的错误。
软件工程视点10.1
使用虚函数和多态性可简化源代码的长度。为支持更简单的顺序代码,虚函数和多态性包含的分支逻辑更少。这种简化有助于程序的测试、调试和维护。
10.3 虚函数
假定一组形状类(如Circle、Trriangle、Rectangle和Square等等)都是从基类Shape派生出来的。在面向对象的程序设计中,我们可能要使每一个这样的类都能够绘制其自身形状。尽管每个类都有它自己draw函数,但是绘制每种形状的draw函数却是大不相同的。当需要绘制形状时,不管它是什么形状,把它作为基类Shape的对象处理是再好不过的。然后,我们只需要简单地调用基类Shape的函数draw,并让程序动态地确定(即在执行时确定)使用哪个派生类的draw函数。
为了使这种行为可行,我们把基类中的函数draw声明为虚函数,然后在每个派生类中重新定义draw使之能够绘制合适的形状。虚函数的声明方法是在基类的函数原型前加上关键字virtual。例如,基类Shape中可能出现:
virtual void draw() const;
上述原型声明函数draw是不取参数也不返回数值的常量函数,而且是个虚函数。
软件工程视点10.2
一旦一个函数被声明为虚函数,即使重新定义类时没有声明虚函数,那么它从该点之后的继承层次结构中都是虚函数。
编程技巧10.1
虽然函数在类层次结构的高层中声明为虚函数会使它在低层隐式地成为虚函数,但有些程序员为了提高程序的清晰性更喜欢在每一层中显式地声明这些虚函数。
软件工程视点10.3
没有定义虚函数的派生类简单地继承其直接基类的虚函数。
如果在基类中将函数draw声明为virtual,然后用基类指针或引用指明派生类对象并使用该指针调用draw函数(如shapePtr->draw()),则程序会动态地(即在运行时)选择该派生类的draw函数,这称为动态关联(见10.6和10.9节的实例研究)。
如果用名字和圆点成员选择运算符引用一个特定的对象来调用虚函数(如squareObject.draw()),则被调用虚函数是在编译时确定的(称为静态关联),调用的虚函数也就是为该特定对象的类定义(或继承该特定对象类)的函数。
10.4 抽象基类和具体类
当我们把类看作一种数据类型时,我们通常认定该类型的对象是要被实例化的。但是,在许多情况下,定义不实例化为任何对象的类是很有用处的,这种类称为“抽象类”(abstract class)。因为抽象类要作为基类被其他类继承,所以通常也把它称为“抽象基类”(abstract base class)。抽象基类不能用来建立实例化的对象。
抽象类的惟一用途是为其他类提供合适的基类,其他类可从它这里继承和(或)实现接口。能够建立实例化对象的类称为具体类(concrete class )。
例如,我们可以建立抽象基类TwoDimensionalObject,然后从它派生出具体类Square、Circle和Triangle等等,也可以建立抽象基类ThreeDimensionalObject,然后从它派生出具体类Cube、Sphere和Cylinder等等。这些抽象基类表述的含义因为太广泛而定义不出实在的对象。如果要建立实例对象,则需要含义更加明确的类,,这就是所谓的“具体类”。具体类具有足以能够建立实例化对象的明确含义。
如果将带有虚函数的类中的一个或者多个虚函数声明为纯虚函数,则该类就成为抽象类。纯虚函数是在声明时”初始化值”为0的函数,例如:
virtual float earnings() const = O; // pure virtual
软件工程视点10.4
如果某个类是从一个带有纯虚函数的类派生出来的,并且没有在该派生类中提供该纯虚函数的定义,则该虚函数在派生类中仍然是纯虚函数,因而该派生类也是一个抽象类。
常见编程错误10.1
试图实例化一个抽象类对象(即包合一个或者多个纯虚函数的类)是一种语法错误。
一个类层次结构中可以不包含任何抽象类,但是正如以后会看到的,很多良好的面向对象的系统,其类层次结构的顶部是一个抽象基类。在有些情况中,类层次结构顶部有好几层都是抽象类。
形状类的层次结构就是一种典型的范例。我们可以在该层次结构的顶部建立抽象基类shape,在往下的一层中还可以再建立两个抽象基类,即二维形状类TwoDimensionalShape和三维形状类ThreeDimensionalShape,再往下我们就可以开始定义二维形状的具体类如圆形类和正方形类以及三维形状的具体类如球类和立方体类等等。
10.5 多态性
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。
使用虚函数和多态性能够使成员函数的调用根据接收到该调用的对象的类型产生不同的动作(但会需要少量执行时的开销)多态性赋予了程序员极大的灵活性。下面几节要举例说明多态性和虚函数的功能。
软件工程视点10. 5
利用虚函数和多态性,程序员可以处理普遍性而让执行环境处理特殊性。即使在不知道一些对象的类型的情况下,程序员也可以命令各种各样的对象表现出适合这些对象妁行为。
软件工程视点10.6
多态性提高了可扩展性:处理多态性行为的软件可以用与接收消息的对象类型无关的方式编写。因此,不必修改基本系统应可以把能够响应现有消息的新类型的对象添加到系统中。除了实例化新对象的客户代码需要重新编译外,程序无需重新编译。
软件工程视点10.7
抽象类为类层次结构中的各个成员定义接口。抽象类中包含了要在派生类中定义的纯虚函数,该层次结构中的所有函数都可以通过多态性使用同样的接口。
尽管不能实例化抽象基类的对象,但是可以声明引用抽象基类的指针。当实例化了具体类的对象后,可以用这种指针使派生类对象具有多态操作能力。
下面考虑一个应用多态性和虚函数的例子。一个屏幕管理程序需要显示各种各样的对象,甚至包括在屏幕管理程序编写后又添加到系统中的新类型的对象。系统可能需要显示各种各样的形状,例如正方形、圆形、三角形、矩形等等(每一个类都是基类Shape的派生类)。屏幕管理程序使用基类指针(指向Shape)来管理要显示的对象。为了能够绘制所有的对象(不管该对象在继承层次结构中的哪一层),管理程序都是使用指向该对象的基类指针并向该对象简单地发送一条draw消息。函数draw在基类Shape中被声明为纯虚函数,并且在每一个派生类中被重新定义,每个对象都知道如
何绘制自身。屏幕管理程序不必关心这些细节内容,它只要简单地告诉每个对象进行绘制即可。
多态性特别适合于实现分层的软件系统。例如,在操作系统中各种类型的物理设备彼此之间的操作是不同的,然而从设备读取数据和把数据写入设备的命令在某种程度是统一的。发送给设备驱动程序对象的“写”消息(write函数调用)需要在该设备驱动程序的上下文中具体地解释,并且还要解释设备驱动程序是如何操作该特定类型设备的。但是,write调用本身和对任何其他对象的write调用实际上没有什么区别,都只是把内存中一定数目的字节放在设备中。面向对象的操作系统可能会用抽象基类为所有设备驱动程序提供合适的接口,然后通过继承抽象基类生成执行所有类似操作
的派生类。设备驱动程序所提供的功能(即public接口)在抽象基类中则是以纯虚函数形式出现的,派生类中提供了这些虚函数的实现.已实现的函数能够响应特定类型的设备驱动程序。
利用多态编程,程序可以从类层次的不同层中遍历对象的指针数组。这种数组中的指针都是派生类对象的基类指针。例如,TwoDimensionalshape类的对象数组可以包含指向派生类Square、Circle、Triangle、Rectangle和Line等对象的TwoDimensionalShape *指针。使用多态编程时,发出一个绘制数组中每个对象的消息即可在屏幕上画出正确的图形。
10.6 实例研究:利用多态性的工资单系统
下面的范例程序用虚函数和多态性根据雇员的类型完成工资单的计算(见图10.1)。所用的基类是雇员类Employee,其派生类包括:老板类Boss,不管工作多长时间他总是有固定的周薪;销售员类CommissionWorker,他的收入是一小部分基本工资加上销售额的一定的百分比;计件工类PieceworkWorker,他的收入取决他生产的工件数量;小时工类HourlyWorker,他的收入以小时计算,再加上加班费。
函数earnings的调用当然要普遍适用于所有的雇员。每人收入的计算方法取决于它属于哪一类雇员。因为这些类都是由基类Employee派生出来的,所以函数earnings在基类Employee中被声明为virtual,并在每个派生类中都正确地实现earnings。为计算任何雇员的收入,程序简单地使用了一个指向该雇员对象的基类指针并调用函数earnings。在一个实际的工资单系统中,各种雇员对象可能保存在一个数组(链表)中,数组每个指针都是Employee *类型,然后程序遍历链表中的每一个节点,并在每一个节点处用Employee *指针调用对象的earnings函数。
下面看一看类Employee。该类的public成员函数包括:构造函数,该构造函数有两个参数,第一个参数是雇员的姓,第二个参数是雇员的名;析构函数,用来释放动态分配的内存;两个“get”函数,分别返回雇员的姓和名;纯虚函数earnings和虚函数print。为什么要把earnings函数声明为纯虚函数呢?因为在类Employee中提供这个函数的实现是没有意义的,将它声明为纯虚函数表示要在派生类中而不是在基类中提供具体的实现。对于具有广泛含义的雇员,我们不能计算出他的收入,而必须首先知道该雇员的类型。程序员不会试图在基类Employee中调用该纯虚函数,所有的派生类根据相应的实现为这些类重定义earnings。
类Boss是通过public继承从类Employee派生出来的,它的public成员函数包括:构造函数,构造函数有三个参数,即雇员的姓和名以及周薪,为了初始化派生类对象中基类部分的成员firstName和lastName,雇员的姓和名传递给了类Employee的构造函数;“set”函数,用来把新值赋绐private数据成员weeklySalary;虚函数earnings,用来定义如何计算Boss的工资;虚函数print,它输出雇员类型,然后调用Employee:print()输出员工姓名。
类CommissionWorker是通过public继承从类Employee派生出的,它的public成员函数包括:构造函数,构造函数有五个参数,即姓、名、基本工资、回扣及产品销售量,井将姓和名传递给了类Employee的构造函数;"set"函数,用于将新值赋给private数据成员salary、commission和quantity;
虚函数earnings,用来定义如何计算CommissionWorker的工资;虚函数print,输出雇员类型,然后调用Employs:print()输出员工姓名。
类PieceWorker是通过public继承从类Employee派生出来的,public成员函数包括:构造函数,构造函数有四个参数,即计件工的姓、名、每件产品的工资以及生产的产品数量,并将姓和名传递给了类Employee的构造函数;"set"函数,用来将新值赋给private数据成员wagePerPiece和quantity;
虚函数earnings,用来定义如何计算PieceWorker的工资;虚函数print,它输出雇员类型,然后调用 Employee:print()输出员工姓名。
类HourlyWorker是通过public继承从类Employee派生出来的,public成员函数包括: 构造函数,构造函数有四个参数,即姓、名、每小时工资及工作的时间数,并将姓、名传递给了类Employee的构造函数;“set”函数,将新值赋给private数据成员wage和hours;虚函数earnings,用来定义如何计算HourlyWorker的工资;虚函数print,输出雇员类型,然后调用Employee:print()输出员工姓名。
1 // Fig. 10.1: employ2.h
2 // Abstract base class Employee
3 #ifndef EMPLOY2_H
4 #define EMPLOY2_H
5
6 #include<iostream.h>
7
8 class Employee {
9 public:
10 Employee( const char *, const char * );
11 ~Employee(); // destructor reclaims memory
12 const char *getFirstName() const;
13 const char *getLastName() const;
14
15 // Pure virtual function makes Employee abstract base class
16 virtual double earnings() const = 0; // pure virtual
17 virtual void print() const; // virtual
18 private:
19 char *firstName;
20 char *lastName;
21 };
22
23 #endif
24 // Fig. 10.1: employ2.cpp
25 // Member function definitions for
26 // abstract base class Employee.
27 // Note: No definitions given for pure virtual functions.
28 #include<string.h>
29 #include <assert.h>
30 #include "employ2.h"
31
32 // Constructor dynamically allocates space for the
33 // first and last name and uses strcpy to copy
34 // the first and last names into the object.
35 Employee::Employee( const char *first, const char *last )
36 {
37 firstName = new char strlen( first ) + 1 ];
38 assert( firstName != 0 ); // test that new worked
39 strcpy( firstName, first );
40
41 lastName = new char strlen( last ) + 1 ] ;
42 assert( lastName != 0 ); // test that new worked
43 strcpy( lastName, last );
44 }
45
46 // Destructor deallocates dynamically allocated memory
47 Employee::~Employee()
48 {
49 delete [] firstName;
50 delete [] lastName;
51 }
52
53 // Return a pointer to the first name
54 // Const return type prevents caller from modifying private
56 // deletes dynamic storage to prevent undefined pointer.
57 const char *Employee::getFirstName() const
58 {
59 return firstName; // caller must delete memory
60 }
61
62 // Return a pointer to the last name
63 // Const return type prevents caller from modifying private
64 // data. Caller should copy returned string before destructor
65 // deletes dynamic storage to prevent undefined pointer
66 const char *Employee::getLastName() const
67 {
68 return lastName; // caller must delete memory
69 }
7O
71 // Print the name of the Employee
72 void Employee::print() const
73 { cout << firstName << ' ' << lastName; }
74 // Fig. 10.1: boss1.h
76 #ifndef BOSS1_H
78 #include "employ2.h"
79
80 class Boss : public Employee {
81 public:
82 Boss( const char *, const char *, double = 0.0 );
83 void setWeeklySalary( double );
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -