6.2 虚函数方法与多态机制
6.2.1 虚函数的应用特性要实现运行时的多态,进行动态联编,就必须使用虚函数。虚函数的说明很简单,只要在成员函数的说明前加上一个关键字virtual即可。
[例6.3b] 虚函数使用的例子EX6_3b.CPP。
从上例分析可知,拥有虚函数的类对象中,必然包含着相应的类型信息,否则动态绑定不可能实现。
[例6.3c] 动态绑定中隐藏的信息挖掘EX6_3c.CPP。
下面简单介绍一下虚函为九的实现原理。
当一个类拥有虚函数时,编译系统将为该类创建一个数组VTABLE。VTABLE的元素是虚函数的地址,且同一虚函数的地址在基类和派生类的VTABLE中相对首位置的偏移是一样的。同时编译系统还加入了相应的调用虚函数的代码。这些都是由系统自动完成的。在初始化该类对象时,将加入一个指向VTABLE的指针(一般称为VPTR)。一般来说,VPTR位于该类对象的存储单元的最开始部位。
当VPTR被正确地初始化后,便指向了该对象的VTABLE,从而在对象及其特定的虚函数定义间建立了联系。从虚函数调用的意义上来说,VPTR表明了类型信息,因为它使得调用与类型相符合。
例6.3b的VPTR和VTABLE的关系如图6.1。所示。
图6.1 VPTR和VTABLE的关系。
[例6.3d] 一个更复杂的例子EX6_3d.CPP。
在该例中加入了更多的类,包括instrument类的派生类和其派生类的派生类,但程序运行仍能得到正确结果.该程序的虚函数调用如图6.2所示。(P176)
从该例可以看到,不管类层次进行了多少扩充,对于已有类对象的操作都有不必做出改动。我们知道,系统的设计者在初期集中于整个系统的框架构建,而在后期进行具体问题的分析,并逐渐扩充该框架。运行时多态保证了分析、设计、实现和扩充各个阶段的统一,使得系统的设计者在各个阶段可以集中于眼前的工作,而不必为了以后不可预见的变化会出代价。
虚函数的使用应注意以下几点:
(1)虚函数只能是类成员函数,它在基类体内部说明,目的是提供一种接口界面。不能是友元函数,也不能是静态成员函数,但可在另一个类中被声明为友元函数。
(2)一旦一个函数定义为虚函数,那么无论它传下多少层,都将保持为虚函数,而不必每次都有加关键字virtual。
(3)基类的虚函数可以在一个或多个派生类中被重新定义,但其原型与基类必须完全相同(即返回类型、函数名、参数个数、类型及顺序一样),否则系统将认为派生类中的函数是重载的,而非虚函数。如果仅有返回类型不同,那么编译将出错。
[例6.3e] 派生类中的原型 与基类不一致情况EX6_3e.CPP。
这里进一步讨论一下虚函数与一般重载函数的区别。
①重载函数在类型和参数数量上一定不相同,而重定义的虚函数则要求参数的类型和个数、函数返回类型相同。
②虚函数必须是类的成员函数,重载的函数则不一定是这样。
③构造函数可以重载,但不能是虚函数,而析构函数可以是虚函数。
(4)要虚函数发挥作用,必须用基类的指针(或引用)指向派生类的对象,并用指针(或引用)调用虚函数。即只有地址才能体现运行多态性。因为指向类的指针大小都一样。这时指针提供的信息是不完全的,在编译阶段不知道应该调用虚函数的哪个版本。如果用对象调用虚函数,由于类型已经确定,因此很可能采用预绑定。
[例6.3f] 对象中调用虚函数的例子EX6_3f.CPP。
在这里,了解一下指针的转换规则:
①指向基类的指针,可以指向它的公有派生的对象,但不能指向私有派生的对象。
②只能利用它直接访问派生类从基类继承来的成员,不能直接访问公有派生类中特定的成员。
③不能将指向派生类对象的指针指向其基类的一个对象。
当虚函数在操作中引用的基类数据成员无法被派生类直接引用时,便会出错。为了使用虚函数达到最好的动态联编效果,一般应以该虚函数第一次出现的类的引用体或指针作为参数,避免不确定因素。
(5) 由于包含虚函数的基类指针可以指向其不同的派生类,并可执行不同版本的虚函数,担任了实现程序运行的多态性方法,因而将包含虚函数的类称为多态类。
6.2.2 虚函数与构造函数、析构函数
在此讨论类的两种特殊成员函数——构造函数和析构函数在虚函数机制中的使用。构造函数为对象分配存储空间,使一个对象初始化,而析构函数在该对象生命期完结时做相应的扫尾工作并释放由构造函数分配的内存。那么它们是否可以声明为虚函数呢?
构造函数不能是虚函数。原因有二胩:
(1)从概念上讲,虚函数机制只有在应用于地址时才有效。构造函数的功能是为了一个对象在内存中分配空间,此时该对象的类型已经确定,不需要也不可能应用动态绑定。
(2)从实现上来看,每个对象的VPTR需要构造函数来初始化,在构造函数没有调用之前,VPTR没有形成,根本不可能实现动态绑定。
如果构造函数内部有虚函数时又会出现什么情况?此时虚函数机制不起作用了,调用虚函数如同调用一般的成员函数一样。
[例6.3g] 构造函数内部有虚函数的情况EX6_3g.CPP。
析构函数可以是虚函数,而且应该声明为虚函数。与一般成员函数相似,析构函数被调用时对象的构造已经完成,VPTR和VTABLE也已被正确初始化,因此虚析构函数在实现上是可能性的。从设计角度看,析构函数的任务是释放内存,因此它必须确切被释放的对象的类型,否则可能破坏有用的数据,产生不可预知的后果。
析构函数内部有虚函数,会如何工作呢?析构函数只有“局部”的版本被调用。因为派生类版本的信息已经不可靠了。如果进行调用,就相当于对一些不可靠的数据进行操作,这是非常危险的。因此,在析构函数中,虚函数机制也是不起作用的。
6.2.3 多继承中的虚函数用法在多继承中,由于派生类是继承多个基类,则要对问题作具体分析。
(1) 若派生类中函数是来自基类的虚函数和非虚函数,则依照派生路径,分别确定它们是具有虚特性,还是具有重载性。而路径体现在指针是从哪个基类指向派生类对象的。
[例6.3h] 多继承中的虚函数用法EX6_3h.CPP。
(2)若一个派生类的多个基类之上,存在一个公共基类,并且公共基类中定义了虚函数,则多级派生后依然可重定义虚函数,即虚函数的传递继承性,这也是用于在编程中表达类的统一接口协议的方便工具。
利用虚基类类(见第五章)方法解决多重继承关系下可能发生的问题,也是多态性应用设计的有效处理手段。
6.2.4 虚函数的多态性应用实例分析本节将演示虚函数的多态性的应用。动物、猫科动物、老虎之间的关系。
[例6.4] 虚函数的多态性应用实例EX6_4.CPP。