1. 多态的基本用法
多态分为静态多态和动态多态,区别如下:
- 静态多态:函数重载、运算符重载就属于静态多态
- 动态多态:基于继承和虚函数在运行时实现多态
动态多态需要满足如下条件:
(i)有继承关系
(ii)子类要重写父类的虚函数
通过代码感受动态多态的用法,具体如下:
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak() {
cout << "speaking!" << endl;
}
};
class Cat : public Animal {
public:
void speak() {
cout << "cat is speaking!" << endl;
}
};
void speakingFunction(Animal& animal) {
animal.speak();
}
void test() {
Cat cat;
speakingFunction(cat);
}
int main() {
test();
return 0;
}
代码含义:定义一个Animal类,让Cat类继承它,用speakingFunction来让实例化的cat对象说话,如果在Animal类中的speak函数前不加virtual修饰,那么只会显示speaking而不是cat is speaking。原因在于speak函数是“早绑定”的,也就是在编译阶段就确定其调用地址了。
TIPS:
1、我们利用virtual关键字修饰的函数,就叫虚函数,继承该父类的子类必须重写父类的虚函数,以实现对应的功能。
2、在speakingFunction中,我们的传入参数是父类的引用,也就是说可以通过父类的指针或者引用,来执行子类对象,从而实现动态多态。
2. 多态的底层原理
在父类引入虚函数后,父类和子类的底层都是有一个虚函数表的,他们负责记录虚函数所在的入口地址,如图所示:
因此,此时调用Cat类的实例化对象cat的speak方法,实际上执行的是父类作用域下的speak方法。而当我们在子类中重写父类的虚函数后,这个新重写的虚函数入口地址就会覆盖原来的地址,变成:
3. 纯虚函数和抽象类
我们观察到,当子类重写了父类的虚函数时,父类虚函数中的内容其实并不重要(不会被调用了),于是我们可以用一种更简洁的写法来实现父类的虚函数,由此就有了纯虚函数,代码如下:
class Animal {
public:
virtual void speak() = 0;
};
此时,Animal类因为有了一个纯虚函数,它就变成了一个抽象类
抽象类的特点:
- 无法实例化对象,因为是抽象的
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
4. 虚析构函数和纯虚析构函数
为什么有这个玩意儿? 因为在多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码(执行了父类的析构,而不执行子类的析构,导致堆区释放不干净),这时我们就需要将父类中的析构函数改为虚析构函数或者纯虚析构函数。
用法如下:
virtual ~Animal(){
// 虚析构函数
}
virtual ~Animal() = 0; //纯虚析构函数
注意:对于父类中的虚析构函数或者纯虚析构函数,也要有具体实现,并且这个类也变成了抽象类:
Animal :: ~Animal(){
// do something here
}