C++学习笔记(3)——多态与虚函数
零、为什么引入多态
这部分是我写完第一部分后再加进来的,因为在我写这篇笔记的时候,其实还是不太懂为什么要用多态这个玩意儿的,虽说多态在面向对象程序设计里十分重要,而且不可或缺。既然不知道为什么要用多态,那就自己来体验一下没有多态的感觉吧!
(本部分暂略)
一、多态的概念&原理
“多态”指的是同一名字的事物可以完成不同的功能。多态可以分为两种:
- 编译时的多态:即函数、运算符的重载、对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数
- 运行时的多态:与继承、虚函数有关
虚函数,就是在声明前加了virtual关键字的成员函数。静态成员函数不能是虚函数。包含虚函数的类称为“多态”。下面是一个程序例子:
1 |
|
程序的输出结果是:
1 | A |
注意,编译器是不会分析程序的运行过程的,比如pa -> Print()这样的语句在编译时不能确定调用的是哪个类的成员函数,只有在运行时才能确定。因此多态的函数调用语句称为动态联编,而普通的函数调用语句称为静态联编。
假如基类A的Print函数前不加virtual,那么输出结果将会是:
1 | A |
这是因为,我们上一篇笔记说过,编译器看到的是哪个类的指针,那么就会认为要通过它访问哪个类的成员。编译器是不会分析基类指针指向的到底是基类对象还是派生类对象。那么这是如何实现多态的呢?
多态的实现原理是,在有虚函数的类的存储空间内,都多了4个字节(在32位系统下是4个字节,如果是64位系统就是8个字节了),它们位于对象存储空间的最前面,存放着的就是虚函数表的地址。我们以上面程序的类A为例,画个图表明它的存储空间以及虚函数表。注意,类B、类C的存储空间也是类似的。
如上图,下面我们来分析一下对语句于pa -> Print()的运行细节:
- 当pa指向对象a时,pa指向的是类A的虚函数表的地址(因为它在类A的存储空间的最前面),然后程序就会在类A的虚函数表中查出
A::Print()的地址。 - 当pa指向对象b时,pa指向的是类B的虚函数表的地址(因为它在类B的存储空间的最前面),然后程序就会在类B的虚函数表中查出
B::Print()的地址。
再来分析pa -> test()的情况:
如上图,当pa指向对象b,对于语句pa -> test(),pa指向的是类B的虚函数表的地址(因为它在类B的存储空间的最前面)。类B没有test函数,因此,在类B的虚函数表里,保存的是A::test()的地址。这样,即使pa指向对象b,对于语句pa -> test(),程序也能找到A::test()的地址。
要注意,上面的例子都是在普通函数内调用虚函数的,这些语句都是多态。但是在构造函数和析构函数内部调用虚函数,就不是多态了。这是因为,根据上面的原理可知,多态是编译时不能确定调用哪个函数,但是在构造和析构函数中调用,编译时是能确定调用哪个函数的,也就不能实现多态了。
二、虚析构函数
通过基类指针删除派生类对象时,通常情况下只调用基类的析构函数。但是,我们希望的是,删除一个派生类对象时,应该先调用派生类的析构函数,然后调用基类的析构函数。
这个解决办法就是在基类的析构函数前加上virtual,那么就称为虚析构函数了,派生类的析构函数可以不加virtual声明。一般来讲,我们在类定义了虚函数,析构函数也应该定义为虚函数。例子:
1 |
|
输出结果是BA。
需要注意的是,不允许以虚函数作为构造函数。否则的话会编译出错:[Error] constructors cannot be declared virtual [-fpermissive]。
三、纯虚函数和抽象类
纯虚函数就是没有函数体的函数,而包含纯虚函数的类就叫抽象类。如下面的例子,类A就是一个抽象类:
1 | class A{ |
注意,在抽象类的成员函数内部可以调用纯虚函数,但是在构造函数或析构函数内部不能调用纯虚函数。下面是一个例子:
1 |
|
程序输出结果:test!
抽象类其实很有用。例如下面的程序片段:
1 | class Shape{ //基类 |
在这个程序中,不会有一个独立的Shape对象,因为没有一个图形的形状是什么都不是的。在我们这个程序里,要么是圆,要么是矩形,而绝不会是什么一个其他东西。因此,我们把Shape类写成抽象类,防止我们写程序的时候不小心将Shape类对象创建了,这是十分符合逻辑的。



