多态与虚函数
多态与虚函数
面向对象程序设计语言有封装、继承和多态3种机制,这3种机制能够有效提高程序的可读性、可扩充性和可重用性。
“多态" (polymorphism)是指同一名字的事物可以完成不同的功能。
多态可分为:
-
编译时的多态
主要是指函数的重载(包括运算符的重载),对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数;
-
运行时的多态。
和继承、虚函数等概念有关,是本章要讲述的内容。本书后面提及的多态都是指运行时的多态。
15. 1 多态的基本概念
总结:
多态满足条件
- 有继承关系
- 子类重写父类中的虚函数
多态使用条件
- 父类指针或引用指向子类对象,调用的重写方法
通过基类指针或引用调用成员函数的语句,只有当该成员函数是虚函数时才会是多态。
"多态"的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的虚函数,运行时才确定
只要在基类中某个函数被声明为虚函数,那么,在派生类中,同名、同参数表的成员函数即使前面不写virtual关键字,也自动成为虚函数
15.2 多态的作用
在面向对象的程序设计中使用多态
-
增强程序的可扩充性
即程序需要修改或增加功能的时候,需要改动和增加的代码较少。
-
精简代码
15.3 多态的实现原理
对比发现,有了虚函数以后,对象的体积比没有虚函数时多了4个字节。实际上,任何有虚函数的类及其派生类的对象,都包含这多出来的4个字节,这4个字节就是实现多态的关键,它位于对象存储空间的最前端,里面存放的是虚函数表的地址。
每一个有虚函数的类(或有虚函数的类的派生类)都有一个虚函数表,该类的任何对象中都放着该虚函数表的指针(可以认为这是由编译器自动添加到构造函数中的指令完成的)。虚函数表是编译器生成的,程序运行时被载入内存。一个类的虚函数表中,列出了该类的全部虚函数地址。
例如,上面程序中类A 对象的存储空间以及虚函数表,如图15. 2 所示(假定类A 还有其他虚函数)。
类B 对象的存储空间以及虚函数表,如图15. 3 所示(假定类B 还有其他虚函数)。
多态的函数调用语句被编译成:
- 根据基类指针所指向的(或基类引用所引用的)对象中存放的虚函数表的地址,在虚函数表中查找虚函数地址,并调用虚函数的一系列指令。
假设pa的类型是A*'那么"pa->func()"这条语旬的执行过程如下。
(1)取出pa指针所指位置的前4个字节,即对象所属的类的虚函数表的地址。如果pa指向的是A的对象,那么这个地址就是类A的虚函数表的地址;如果pa指向的是B的对象,那么这个地址就是类B的虚函数表的地址。
(2)根据虚函数表的地址找到虚函数表,在其中查找要调用的虚函数的地址。不妨认为虚函数表是以函数名作为索引来查找的,虽然还会有更高效的查找方法。如果pa指向的是A的对象,自然就会在A的虚函数表中查出A::func的地址;如果pa指向的是B的对象,就会在B的虚函数表中查出B::func的地址。B没有自己的func2函数,因此在B的虚函数表中照抄了A::func2的地址,这样,即便pa指向B对象,"pa->func2();"这条语句在执行过程中也能在B的虚函数表中找到A::func2的地址。
(3)根据找到的虚函数的地址,调用虚函数。
由以上可以看出,只要是通过基类指针或基类引用调用虚函数的语句,一定就是多态,一定就会执行上面的查表过程,哪怕这个虚函数仅在基类中有,在派生类中没有。
多态机制能够提高开发效率,但是也增加了程序运行时的开销。各个对象中包含的4个字节的虚函数表的地址,是空间上的额外开销;而查虚函数表的过程,就是时间上的额外开销了。在计算机发展的早期,计算机非常昂贵稀有,而且运行速度慢,计算机的运算时间和内存都是宝贵的,因此人们不惜多花人力编写运行速度更快、更节省内存的程序;而在今天,计算机的运算时间和内存往往没有入的时间宝贵,运算速度也很快,因此,在用户可以接受的前提下,降低程序运行的效率以提升人员的开发效率,就是值得的了。“多态”的应用就是典型例子。
15.4 关于多态的注意事项
1 . 在成员函数中调用虚函数
2. 在构造函数和析构函数中调用虚函数
234:
在执行一个派生类的构造函数之前,总是先执行基类的构造函数
派生类对象消亡时,先执行派生类的析构函数,再执行基类的析构函数。
236:
当派生类对象生成时,会从最顶层的基类开始逐层往下执行所有基类的构造函数,最后再执行自身的构造函数;派生类对象消亡时,会先执行自身的析构函数,然后从底向上依次执行各个基类的析构函数。
260:
析构函数中调用虚函数不能是多态,原因也是类似的,因为执行到基类的析构函数时,派生类的析构函数已经执行,派生类对象中的成员变量的值可能已经不正确了。
3. 注意区分多态和非多态的情况
15.5 虚析构函数
如果一个基类指针指向new出来派生类对象,而释放该对象的时候是通过 delete该基类指针来完成,就如几何形体程序的第93行那样,就可能导致程序不正确
为了在这种情况下实现多态,C++规定, 需要将基类的析构函数声明为虚函数,即虚析构函数。将上面的程序中的CShape类改写,在析构函数前加"virtual"关键字,将其声明为虚函数:
只要基类的析构函数是虚函数,那么派生类的析构函数不论是否用"virtual"关键字声明,都自动成为虚析构函数。
一般来说, 一个类如果定义了虚函数,则最好将析构函数也定义成虚函数。
析构函数可以是虚函数,但是构造函数不能是虚函数。
在执行一个派生类的构造函数之前,总是先执行基类的构造函数。
15.6 纯虚函数和抽象类
纯虚函数就是没有函数体的虚函数。包含纯虚函数的类就称为抽象类。下面的类A就是一个抽象类:

Prnint就是纯虚函数。纯虚函数的写法就是在函数声明后面加"= 0"不 写函数体。所以纯虚函数实际上是不存在的,引入纯虚函数是为了便于实现多态。
之所以把包含纯虚函数的类称为" 抽象类“,是因为这样的类不能生成独立的对象。例如定义了上面的classA后, 下面的语句都是会编译出错的:

既然抽象类不能用来生成独立对象,那么抽象类有什么用呢?抽象类可以作为基类, 用来派生新类。可以定义抽象类的指针或引用,并让它们指向或引用抽象类的派生类的对象,这就为多态的实现创造了条件。独立的抽象类的对象不存在,但是被包含在派生类对象中的抽象类的对象,是可以存在的。
如果一个类从抽象类派生而来,那么当且仅当它对基类中的所有纯虚函数都进行覆盖并都写出了函数体(空的函数体" { } "也可以),它才能成为非抽象类。
15.7 小结
-
通过基类的指针,调用基类和派生类中都有的同名虚函数时
-
基类指针指向的是基类对象,执行的就是基类的虚函数
-
基类指针如果指向派生类对象,执行的就是派生类的虚函数,
这就称为多态。多态也适用于通过基类引用调用基类和派生类中都有的同名虚函数的情况。
-
-
多态的作用是提高程序可扩充性,简化编程。多态是通过虚函数表来实现的。
-
在普通成员函数中调用虚函数是多态的,但在构造函数和析构函数中调用虚函数不是多态的。
-
有虚函数的类,其析构函数也应该实现为虚函数。
-
包含纯虚函数的类称为抽象类。不能用抽象类定义对象。抽象类的派生类,仅当实现了所有的纯虚函数,才会变成非抽象类。

浙公网安备 33010602011771号