前言:本教程不涉及基础,稍微了解一下C++virtual多态的知识就可以了,不了解的话可以先去看一下菜鸟教程,也可以看我往期的文章《virtual》、《虚函数表》
多态分为静态多态和动态多态
静态多态:也成为编译时的多态;在编译时期就已经确定要执行了的函数地址了;主要有函数重载和函数模板。
动态多态:即动态绑定,在运行时才去确定对象类型和正确选择需要调用的函数,一般用于解决基类指针或引用派生类对象调用类中重写的方法(函数)时出现的问题。
我们今天讲的就是动态多态时,也就是基类指针指向派生类对象时,如何析构派生类;
先看代码:
#include <iostream>
using namespace std;
class Person
{
public:
Person() { cout << "基类的构造函数" << endl; }
virtual void show() = 0 ;
~Person() { cout << "基类的析构函数" << endl; }
};
class Son :public Person
{
public:
Son() { cout << "派生类Son的构造函数" << endl; }
void show() { cout << "派生类Son的show函数" << endl; }
~Son() { cout << "派生类Son的析构函数" << endl; }
};
class Teacher:public Person
{
public:
Teacher() { cout << "派生类Teacher的构造函数" << endl; }
void show() { cout << "派生类Teacher的show函数" << endl; }
~Teacher() { cout << "派生类Teacher的析构函数" << endl; }
};
int main()
{
}
我们创造了一个基类Person里面有构造、析构和一个show函数,子类Son和Teacher也一样
但是父类Father的show是virtual = 0、也就是说父类是个抽象类,不能实例化(创建)对象:
但是它可以创建指针,通过指针传入的对象来调用对应的show函数:
(代码中忘记释放指针,是一种不好的习惯,大家不要学我,抱歉抱歉)
这也就实现了动态多态;
上面代码我想告诉大家的是:基类指针指向子类对象是一种很常见的用法
知道了这一点之后,我们接着往下看:
当派生类对象构造时会先调用基类的构造函数、然后调用派生类的构造
析构的时候会先调用派生类的析构函数然后调用基类的析构函数;
这个流程是很完美的;
我们下面手动调用几次派生类的析构函数看看有什么反应:
我们发现每次调用派生类的析构函数析构之后都会自动调用一次基类的析构函数
这是C++规定的;
我们回到刚才那句话“基类指针指向派生类对象”:
但是他少了一个步骤:析构派生类对象;
如果派生类中存放动态申请的指针,如果不释放是很危险的;那么要怎么释放呢?
手动调用派生类的析构吗?
我们试试:
这个指针他没有办法调用派生类的成员啊;派生类的析构是属于他自己的,父类的指针可以说是“碰不到”这个析构函数;
举个栗子:假设基类A有成员a和b,派生类B新增了成员c,那么基类的指针范围指向的就是8个字节的地址,但是成员c的地址是8-12字节;基类指针碰不到这个变量;可能有人会想到指针偏移,但是指针偏移寻找变量是可以的,如果是用来寻找函数呢?函数的内存地址不在类中啊,偏移有点太难了吧;
但是我们学过多态,我们知道,如果将函数前面加上virtual关键字,那么他的地址就会被写入虚表,基类指针可以通过指向子类对象来调用子类虚表中的重写函数;
所以我们只需要一个virtual关键字就可以了;
再次运行:
任务完成了;
最后补充一点细节:
基类中的纯虚函数也是可以实现的,像这样:
也可以不写:
但是,如果析构函数是纯虚函数,则必须实现:
如果不实现:
你编译的时候可能没问题,但是运行的话,会出错;因为派生类的对象析构了之后会调用基类的析构,但是你基类的析构函数没写实现他怎么调用?肯定会出错;
所以如果基类的析构函数声明为纯虚函数也就是virtual 函数声明 = 0则必须要写实现!!!
可能会有人有问题:基类的析构函数为什么要设置为纯虚函数?既然虚函数就能满足多态,而且还必须要实现,那么为什么要设置为纯虚函数呢?
首先,只有存在纯虚函数的类才称为抽象类;抽象类是很常见的
但是有时候我们不知道该把哪一个函数设置为纯虚函数,这个时候把析构函数设置为纯虚函数是一个很完美的选择;
另外还有一点建议:对于基类来说,即使它不需要析构函数,也应该提供一个"虚析构函数",对于是否提供"纯虚析构函数"要看需求了;
文章至此结束,感谢观看!