1. 多态基本概念
举个简单栗子,构造一个动物类,设定动物都会叫;再构造一个猫类和一个狗类,猫类会喵喵,狗类会汪汪,就是两个子类中也有父类中实现的方法(有重载那味了)。
多态分为两类:
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数(virtual)实现运行时多态
两者区别:
- 静态多态的函数地址早绑定,编译阶段确定函数地址
- 动态多态的函数地址晚绑定,运行阶段确定函数地址
动态多态满足的条件:
- 有继承关系
- 子类重写父类中的虚函数
动态多态使用:父类的指针或者引用指向子类对象(例如子类中编写函数void Speak(Animal &animal)
)
使用多态的好处:
- 组织结构清晰
- 可读性强
- 分别前期和后期扩展以及维护性高
2. 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数
通俗点讲,写纯虚函数的目的就是想提醒子类中必需要实现这样一个函数,你不实现就给报错,就根本不能算是它的一个子类
纯虚函数语法:
virtual 返回值类型 函数名 (参数列表) = 0;
virtual void func() = 0;
当类中有了纯虚函数,这个类也称为 抽象类(抽象,懂吧,跟Java一个道理)
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
3. 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区(比如new了一个指针),那么父类指针在释放时会无法调用到子类的析构代码
解决方式:将父类中的析构函数改为 虚析构 或者 纯虚析构
就这么理解:
当子类中用函数new了一个东西出来:Son *son = new m_A
,而在main函数里有个父类指针new了一个子类出来:Base *base = new Son
,当程序运行完后,父类的析构函数运行了,但子类的析构函数没有运行,导致son指针没有释放。这时候就需要将父类的析构函数设置为虚析构。
3.1 虚析构写法:
virtual ~类名(){}
virtual ~Base(){
}
3.2 纯虚析构写法:
virtual ~类名() = 0;
类名::~类名(){……}
virtual ~Base() = 0;
Base :: ~Base(){
}
代码实现
#include <iostream>
using namespace std;
class Animal
{
public:
Animal()
{
cout << "Animal构造函数调用" << endl;
}
~Animal()
{
cout << "Animal析构函数调用" << endl;
}
virtual void speak() = 0; //纯虚函数
};
class Cat :public Animal
{
public:
Cat(string name)
{
cout << "Cat构造函数调用" << endl;
m_Name = new string(name);
}
void speak()
{
cout << *m_Name << "猫在说话" << endl;
}
~Cat()
{
if (m_Name) {
delete m_Name;
m_Name = NULL;
cout << "Cat析构函数调用完成" << endl;
}
}
string* m_Name;
};
int main()
{
Animal* animal = new Cat("Tom");
animal->speak();
//父类指针在析构时候,没有调用子类中的析构函数
delete animal;
return 0;
}
输出结果为:
仔细点看,可以发现Cat析构函数并没有被调用,可是我代码中明明就有一行 "Cat析构函数调用完成"啊?
原因在于,父类指针在析构时候,没有调用子类中的析构函数,导致子类如果有堆区属性,就会出现内存泄漏
解决方法很简单,将父类析构函数改为虚析构:
virtual ~Animal()
{
cout << "Animal析构函数调用" << endl;
}
修改后的输出结果:
注意:
- 纯虚析构属于抽象类,无法实例化对象,子类中必需要重写析构函数
- 虚析构或纯虚析构都是用来解决通过父类指针释放子类对象的问题