⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 10章 虚函数与多态性.txt

📁 C++大学教程txt版中文版 C++大学教程txt版中文版
💻 TXT
📖 第 1 页 / 共 5 页
字号:
219 // using dynamic binding.
220 void virtualViaReference( const Shape &baseClassRef )
221 {
222   baseClassRef.printShapeName();
223   baseClassRef.print();
224   cout << "\nArea = "<< baseClassRef.area()
225        << "\nVolume  "<< baseClassRef.volume() << "\n\n";
226 }

输出结果:
Point: [ 7, 11 ]
Circle: [ 22, 8 ]; Radius  3.50
Cylinder: [ 10, 10 ] ; Radius = 3.30; Height = 10.00

Virtual function calls made off base-class pointers
Point: [7, 11]
Area = 0.00
Volume = 0.00

Circle: [ 22, 8]; Radius = 3.50
Area = 38.48
Volume = 0.00

Cylider: [ 10,10 ]; Radius = 3.30; Height = 10.00
Area = 275.77
Volume = 342.12

Virtual function calls made off base-class pointers
Point: [ 7, 11]
Area = 0.00
Volume = 0.00

Circle: [ 22, 8] ; Radius = 3.50
Area = 38.48
Volume = 0.00

Cylinder:[10, 10]; Radius = 3.30; Height = 10.00
Area = 275.77
Volume = 342.12

                            图10.2定义抽象基类Shape

    基类Shape由三个public虚函数组成,不包含任何数据。函数print和printShapeName是纯虚函数,因此它们要在每个派生类中重新定义。函数area和volume都返回0.0,当派生类需要对面积(area)和(或)体积(volume)有不同的计算方法时,这些函数就需要在派生类中重新定义。注意Shape是个抽象类,包含一些“不纯”的虚函数(area和volume)。抽象类可以包含非虚函数和通过派生类继承的数据。
    类Point是通过public继承从类Shape派生来的。因为Point没有面积和体积(均为0.0),所以类中没有重新定义基类成员函数area和volume,而是从类Shape中继承这两个函数。函数printShapeName和print是虚函数(在基类被定义为纯虚函数)的实现,如果不在类Point中重新定义这些函数,那么Point仍然为抽象类则不能实例化Point对象。其他成员函数包括:将新的x和y坐标值赋绐Point对象(即点)的一个”set”函数和返回Point对象的x和y坐标值的“get”函数。
    类Circle是通过public继承从类Point派生来的。因为它没有体积,所以类中没有重新定义基类成员函数volume,而是从类Shape中继承。Circle是有面积的,因此要重新定义函数area。函数printShapeName和print是虚函数(在基类中被定义为纯虚函数)的实现。如果此处不重新定义该函数,则会继承类Point中该函数的版本。其他成员函数包括为Circle对象设置新的radius(半径值)的“set”函数和返回Circle对象的radius的“get”函数。
    类Cylinder是通过public继承从类Circle派生来的。因为Cylinder对象的面积和体积同Circle的不同,所以需要在类中重新定义函数area和volume。函数printShapeName和print是虚函数(在基类中被定义为纯虚函数)的实现。如果此处不重新定义该函数,则会继承类Circle中该函数的版本。
  类中还包括一个设置Cylinder对象height(高度)的“set”函数和一个读取Cylinder对象(圆柱体)的height的”get”函数。
    驱动程序一开始就分别实例化了类Point的对象point、类Circle的对象circle和类Cylinder的对象cylinder。程序随后调用了每个对象的printShapeName和print函数,并输出每一个对象的信息以验证对象初始化的正确性。每次调用printShapeName和print(第164行到第173行)都使用静态关联,编译器在编译时知道调用printShapeName和print的每种对象类型。
    接着把指针数组arrayOfShapes的每个元素声明为Shape*类型,诙数组用来指向每个派生类对象。首先把对象point的地址赋给了arrayOfShapes[O](第179行)、把对象circle的地址赋给了arrayOfShapes[1](第182行)、把对象cylinder的地址赋给了arrayOfShapes[2](第185行)。
 然后用for结构(第193行)遍历arrayOfShapes数组,并对每个数组元素调用函数virtualViaPointer
  (第194行):
    virtualViaPointer(arrayOfShapes[ i ]);
  函数virtualViaPointer用baseClassPtr(类型为constShape*)参数接收arrayOfShapes数组中存放的地址。每次执行virtualViaPointer时,调用下列4个虚函数:
    baseClassPtr->printShapeName()
    baseClassPtr->print()
    baseClassPtr->area()
    baseClassPtr->Volume()
  这些调用方法对执行时baseClassPtr所指的对象调用一个虚函数,对象类型无法在编译时确定。输出中显示了对每个类调用的相应函数。首先,辅出字符串“Point:”和相应的point对象,面积和体积的计算结果都是0.00。然后,输出字符串“Circle:”和circle对象的圆心及半径,程序计算出了对象circle的面积,返回体积值为0.00。最后,输出字符串“Cylinder:”以及相应的cylinder对象的底面圆心、半径和高,程序计算出了对象cylinder的面积和体积。所有调用函数printShapeName、print、area以及volume的虚函数都是在运行时用动态关联解决的。
    最后用for结构(第202行遍历arrayOfShapes数组,并对每个数组元素调用函数virtualViaReference
  (第203行):
    virtualViaReference(*arrayofShapes[ j ]);
  函数virtualViaReference用baseClassRef(类型为constShape&)参数接收对arrayOfShapes数组中存放的地址的引用(通过复引用)。每次执行virtualViaReference时,调用下列4个虚函数:
    baseClassRef.printShapeName()
    baseClassRef.print()
    baseClassRef.area()
    baseClassRef.volume()
  这些调用方法对执行时baseClassRef所指的对象调用上述函数。输出中使用基类引用与使用基类指针时产生的结果是相同的。

10.10  多态、虚函数和动态关联
    C++中的多态比较容易编程。虽然还可以像C语言等非面向对象语言中一样进行多态编程,但这种做法既复杂,又危险,需要进行指针操作。本节介绍C++如何在内部实现多态、虚函数和动态关联,以便了解这些功能是如何实现的,更重要的是帮助读者了解多态的开销(除了内存占用和处理器时间)。这样就可以更清楚地确定何时使用多态,何时不用多态。第20章“标准模板库(STL)”中将会介绍STL组件不用多态和虚函数,从而避免运行开销,达到符合STL特定要求的最优性能。
    首先,我们要介绍C++编译器在编译时建立怎样的数据结构来支持运行时的多态。然后我们介绍执行程序如何利用这些数据结构执行虚函数和实现与多态相关的动态关联。
    C++编译有一个或几个虚函数的类时,对该类建立虚函数表(virtualfunctiontable,vtableL vtable让执行程序选择每次执行类的虚函数时正确的实现方法。图10.3演示了Shape、Point、Circle和Cylinder类的虚函数表。
Shape类的vtable中,第一个指针指向该类area函数的实现方法,即返回面积0.0的函数。第二个指针指向该类volume函数的实现方法,即返回体积0.0的函数。printShapeName和print函数都是纯虚函数,没有实现方法,因此函数指针都设置为0。类中的vtable有一个或几个0指针时,称为抽象类。类中的vtable没有0指针时,称为具体类(如Point、Circle和Cylinder)。
    Point类继承Shape类的area和volume函数,因此编译器只是把Point类vtable表中的这两个指针设为Shape类中area和volume指针的副本。Point类将函数printShapeName重定义为打印”Point:”,使函数指针指向Point类的printShapeName函数。Point类还重定义print,使相应函数指针指向Point类打印[x,y]的函数。
    Circle类vtable表中Circle的area函数指针指向Circle的area函数(返回πr2)。volume函数指针只是从Point类复制,是原先从Shape向Point复制的指针。printShapeName函数指针指向Circle版本打印”Circle:”的函数。print函数指针指向Circle类的打印[x,y]r的函数。
    Cylinder类vtable表中Cylinder的area函数指针指向Cylinder的area函数,该函数计算Cylinder的表面积2πr2+2πrh。Cylinder的volume函数指针指向volume函数,返回πr2h。Cylinder的priintShapeName函数指针指向打印"Cylinder"的函数。Cyelinder的print函数指针指向Cylinder类打印[x,y]rh的函数。
    多态是通过复杂的数据结构实现的,涉及三层指针。前面只介绍了其中一层,即vtable中的函数指针。这些指针在调用虚函数时指向实际执行的函数。
    下面要考虑第二层指针。实例化带虚函数的类对象时,编译器在对象前面连接该类的vtable指
针(注意:这个指针通常放在对象前面,但也不一定非要这样实现)。
    第三层指针是接受虚函数调用的对象句柄(这个句柄也可以是个引用)。
    下面看看典型的虚函数调用如何执行。考虑函数virtualViaPointer中的下列调用:
    baseClassPtr->printShapeName()
假设baseClassPtr包含arrayOfShapes[1]的指针,即对象circle的地址。则编译器编译这条语句时,它确定调用实际上是对基类指针进行,并且printShapeName是个虚函数。
    然后编译器确定printShapeName是每个vtable表中的第三个项目。要找到这个项目,编译器发现需要跳过前两个项目。为此,编译器编译8个字节的偏移量或位移量(在目前流行的32位机器中,每个指针为4个字节),将其编译到机器语言目标码中,用于执行虚函数调用。
    然后编译器产生完成下列工作的代码(说明:下列编号对应图1O.3中圆圈内的数字):
    1.从arrayOfShapes中选择第i个项目(这里是对象circle的地址)并将其传递给virtualViaPointer,从而将baseClassPtr设置为指向circle。
    2.复引用指针,取得circle对象,它以指向Circlevtable的指针开始。
    3. 复引用circle的vtable指针,取得Circlevtable。
    4.跳过8个字节位移,选择printShapeName函数指针。
    5.复引用printShapeName函数指针,构成要执行的实际函数名,并用函数调用运算符()执行相应的printShapeName函数和打印字符串”Circle:”。
    图10.3的数据结构看起来有点复杂,但这些调节大部分由编译器负责,程序员不必担心,C++中的多态编程并不复杂。
    每个虚函数调用中发生的指针复引用操作和内存访问需要增加一些执行时间。vtable和vtable指针要占用一些内存。
    现在,已经有了关于虚函数调用如何工作的足够细节,可以确定其是否适合具体的应用程序。





    性能提示10.1
    多态性(它是用虚函数和动态关联实现的)是高效的,程序更使用这种功能对系统性能的影响极小。

    性能提示10.2
    虚函数和动态关联使得多态性编程和switch逻辑编程形成了对照。C++优化编译器通常能生成至少和手写的基于switch逻辑的代码具有同样效率的代码。对大多数应用程序而言,多态的开销是可以接受的。但有时则不能接受多态的开销,例如性能要求很高的实时应用程序。


小  结
    ●虚函数和多态性使得设计和实现易于扩展的系统成为可能。在程序开发过程中,不论类是否已经建立,程序员都可以利用虚函数和多态性编写处理这些类对象的程序。
    ●虚函数和多态性的程序设计无需使用switch逻辑。程序员可以用虚函数机制自动完成等价的逻辑,因而避免与swilch逻辑有关的各种各样的错误。如果要让客户代码确定对象类型和表达,则是低质的类设计。
    ●派生类在需要的时候可以自己提供基类的虚函数实现,否则就使用基类的实现。
    ●如果通过用名字和圆点成员选择运算符引用一个特定的对象来调用虚函数,则引用是在编译时确定的(称为静态关联).被调用的虚函数是为该特定对象的类定义的函数或继承该类的函数。
    ●在许多情况下,定义不实例化为任何对象的类很有用处,这种类称为“抽象类”。因为抽象类要作为基类被其他类继承,所以通常也把它称为“抽象基类”。抽象基类不能用来建立实例化的对象。
    ●可以建立实例化对象的类称为具体类。
    ●将带有虚函数的类中的一个或者多个虚函数声明为纯虚函数,则该类就成为抽象类。纯虚函数是在声明时“初始化值”为。的函数。
    ●如果某个类是从一个带有纯虚函数的类派生出来,并且没有在该派生类中提供该纯虚函数的定义,则该虚函数在派生类中仍然是纯虚函数,因而该派生类也是一个抽象类(不能有任何对象)。
    ●C++支持多态性。所谓多态性是指:通过继承而相关的不同的类,他们的对象能够对同一个函数调用做出不同的响应。
    ●多态性是通过虚函数实现的。
    ●当通过基类指针或引用请求使用虚函数时,c++会在与对象关联的派生类中正确的选择重定义的函数。
    ●使用虚函数和多态性能够使成员函数的调用根据接收到该调用的对象的类型产生不同的动作。
    ●尽管不能实例化抽象基类的对象,但是可以声明抽象基类的指针。当实例化了具体类的对象后,可以用这种指针使派生类对象具有多态操作能力。
    ●使用动态关联(也叫滞后关联)可以向系统中添加新类。对于要被编译的虚函数调用,编译时可以不必知道对象的类型。在运行时,虚函数调用和被调用对象的成员函数相匹配。
    ●动态关联可以使独立软件供应商(ISV)在不透露其秘密的情况下发行软件。发行的软件可以只包括头文件和对象文件,不必透露源代码。软件开发者可以利用继承机制从ISV提供的类中派生出新类。和ISV提供的类一起运行的软件也能够和派生类一起运行,并且能够使用 (通过动态关联)这些派生类中重定义的虚函数。
    ●动态关联要求在运行时把对虚函数的调用转换为对应类的虚函数版本。虚函数表(称为vtable)实现为包含函数指针的数组,每一个包含虚函数的类都有一个vtable。对于类中的每一个虚函数,viable都有一个包含一个函数指针的项目,该指针指向类的对象所要使用的虚函数版本。特定类所要使用的虚函数可能是该类中重新定义的函数,也可能是从较高层的基类直接或间接继承来的函数。
    ●当基类提供了一个成员函数并将它声明为virtual时,泥生类可以但不是必须重定义该虚函数,因此派生类可以使用基类的虚函数版本,这会在vtable中指明。
    ●带有虚函数的类的每一个对象都包含一个指向该类viable的指针。系统在运行时会获取并复引用正确的函数指针来完成函数调用,查找vtable和复引用指针只需要极少的运行时间的开销,一般少于最优的客户代码。
    ●如果基类中包含虚函数,把其析构函数声明为虚析构函数。这样做将会使所有派生类的析构函数自动成为虚析构函数(即使它们与基类析构函数的函数名不同)。这时,如果delete运算符用于指向派生类对象的基类指针,而程序中又显式地用该运算符删除每一个对象,那么系统会调用相应类的析构函数。
    ●任何类在vtable中有一个或几个0指针时就成为抽象类。而没有0指针时就成为具体类(如Point、Circle和Cylinder)。


术  语
  abstract base class   抽象基类                     offset into vtable    vtable偏移量
  abstract class   抽象类                     override a pure virtual function   重定义纯虚函数
  base-class virtual function   基类虚函数           override a viltual function  重定义虚函数
  class hieratchy    类层次                          pointer to a base class    基类指针
  concrete class    具体类                           pointer to a derivedclass  派生类指针
  convert derived-class pointer to base-class spointer   pointer to an abstract class抽象类指针
    将派生类指针变为基类指针                         polymorphism    多态
  derived class   派生类                             programming“in the general”  常规编程
  derived-class constructor   派生类构造函数         programming "in the specific"  特定编程
  direct base class   直接基类                    pure virtual function(=0)  纯虚函数(=O)
  displacement into vtable   vtable位移              reference to a base class   基类引用
  dynamic binding    动态关联                        reference to a derived class  派生类引用
  early binding    提前关联                          reference to an abstract class 抽象类引用
  eliminating switch statements   消除switch语句    software reusability    软件复用性
  explicit pointer conversion   显式指针转换        static binding    静态关联
  extensibility    可扩展性                          switch logic     switch逻辑
  implementation inheritance    实现继承             virtual destructor    虚析构函数
  independent software vebdor(ISV)   独立软件供应商  viltnalfunction    虚函数
  indirect base class  间接基类                    virtual function table   虚函数表
  inheritance    继承                                vtable
  interface inheritance    接口继承                  vtable pointer   vtable指针
  late binding    滞后关联


自测练习
10.1填空
    a)使用继承和多态性有助于消除——逻辑。
    b)在类定义中,将——置于虚函数的函数原型的末尾可以声明该函数为纯虚函数。
    c)如果一个类包含一个或多个纯虚函数,则该类为——。
    d)在编译时就解决的函数调用称为——关联。
    e)在运行时才解决的函数调用称为——关联。

自测练习答案
    10.1  a)switch。b)=O。c)抽象基类。d)静态。e)动态。

练  习
    10.2  什么是虚函数?举一个适合使用虚函数的例子。
    10.3  构造函数不能是虚函数。怎样使构造函数具有虚函数的效果?
    10.4  多态如何让程序“一般化”而不是“特殊化”。说明“一般化”编程的主要好处。
    10.5  说明用switch逻辑编程的问题。请解释为什么多态可以代替switch逻辑。
    10.6  区分静态关联与动态关联。请解释动态关联中虚函数和vtable的用法。
    10.7  区分继承接口与继承实现的方法,继承接口的继承层次设计与继承实现的继承层次设计有什么不同?
    10.8  区分虚函数与纯虚函数。
    10.9  (判断对错)抽象基类中所有虚函数都要声明为纯虚函数。
    10.10 对本章介绍的Shape层次提出一层或几层抽象基类(第一层是Shape,第二层包括类    TwoDimensionalShape和ThreeDimensionalShape)。
    10.11 多态如何促进可扩展性?
    10.12 要求开发一个详细描述图形输出的飞行模拟程序。说明多态对这类问题为什么特别有用。
    10.13 开发一个基本图形包。用Shape类继承层次,只限于二维形状,如正方形、长方形、三角形和圆。并与用户交互,让用户指定每个形状的位置、尺寸、形状和填充字符。用户可以指定多个同一形状的项目。生成每个形状时,将每个新Shape对象的Shape*指针放在数组中。每个类有自己的draw成员函数。编写一个多态屏幕管理程序,遍历数组(可 用迭代器)。向数组中的每个对象发一个draw消息,形成屏幕图形。每次用户指定新形状时,重新输出屏幕图形。
    10. 14 修改图10.1的工资系统,增加private数据成员birthData(Date对象)和departmentCode(int类型)到Employee中。假设工资系统每月处理一次。这样,程序计算每个员工的工资时(多态),遇到过生日的员工多发100美元奖金。
    10.15 练习9.14开发了形状类Shape的层次结构,并在该结构中定义了若干类。修改该层次结构,使Shape成为一个包含接口(供层次结构中的类使用)的抽象基类。从类Shape派生出二维形状类TwoDimensionalShape和三维形状类ThreeDimensionalShape,它们也都是抽象类,然后用虚函数print输出每个类的类型和维数。为了计算类层次结构中每个具体类的对象,这两个类中还要包括虚函数are和volume。最后再编写一个驱动程序测试类Shape的层次结构。

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -