今天我们只讨论一个话题,为什么析构函数最好是虚函数?
总结: 当类之间存在继承关系时,最后做析构时有可能析构不完全,父子类中同时使用虚析构函数就可以解决这样的问题
。
在大家在VS(vs2017中未见)创建类的时候会看到有“虚析构函数”
的选项,当其打上勾之后,生成的类的析构函数时一个虚函数,本篇将会介绍析构函数使用virtual
关键字的原因。
1. 普通写法下对象的构造与析构
对以下代码,使用普通的写法创建CTest2
的对象CTest2 t;
,其析构是否为虚析构函数得到的结果是一样的
。
#include <iostream>
class CParent {
public:
CParent() {
printf("CParent::CParent()\r\n");
}
~CParent() {
printf("CParent::~CParent()\r\n");
}
};
class CTest2 :public CParent
{
public:
CTest2() {
printf("CTest2::CTest2()\r\n");
}
~CTest2() {
printf("CTest2::~CTest2()\r\n");
}
};
int main(int argc, char* argv[])
{
CTest2 t;
return 0;
}
运行结果:可以正常实现构造与析构
将子类父类的析构函数均加virtual
关键字变为虚析构函数,得到的结果与上面一致
2. 儿子对象指针赋值给父亲时的析构
- 当使用
CParent* p = new CTest2;
将儿子对象指针赋值给父亲,父子类的析构函数均不为虚,使用delete p;
进行析构
#include <iostream>
class CParent {
public:
CParent() {
printf("CParent::CParent()\r\n");
}
~CParent() {
printf("CParent::~CParent()\r\n");
}
};
class CTest2 :public CParent
{
public:
CTest2() {
printf("CTest2::CTest2()\r\n");
}
~CTest2() {
printf("CTest2::~CTest2()\r\n");
}
};
int main(int argc, char* argv[])
{
//创建了儿子对象,并将其指针赋值给父亲
CParent* p = new CTest2;//儿子的内容包含父亲所有内容,指针访问安全
delete p;
return 0;
}
运行结果:少了一次子类CTest2
的析构
出现上面结果的原因是delete
是CParent
类型(this->delete
),delete p;
等价于(先调用析构p指向的对象的析构函数,再释放资源):p->~CParent(); free(p)
,显然无法调用子类的析构。
此处还可以按照“当使用从基类派生的虚方法时,派生类也将拥有一个 vptr,但是编译器将让它指向派生类的 vtable,即编译器将使用派生类的虚方法实现的地址填充 vtable。”理解,此处由于调用的析构函数非虚函数,那么也就不存在查子类的虚表的过程,就会按照CParent
类型进行析构。
- 通过给析构函数加
virtual
关键字就可以调用到子类的析构
通过给父子类的析构函数都加virtual关键字,可以使得两个类都有虚表,其第0项为类对应的析构函数。
参考C++57个入门知识点_33_深入理解虚函数的原理-重点(间接调用:先查虚表地址,再查虚表中的虚函数指针;编译器先取对象的前4个字节地址,再取对应地址下函数指针;查看内存、反汇编的方法;成员函数指针),C++57个入门知识点_37 虚函数的直接调用与间接调用(函数的调用分为直接调用和间接调用,间接调用是虚函数所具有的的性质;间接调用:运行期通过查找对象的虚表下标来调用函数的方法)
#include <iostream>
class CParent {
public:
CParent() {
printf("CParent::CParent()\r\n");
}
virtual ~CParent() {
printf("CParent::~CParent()\r\n");
}
};
class CTest2 :public CParent
{
public:
CTest2() {
printf("CTest2::CTest2()\r\n");
}
virtual ~CTest2() {
printf("CTest2::~CTest2()\r\n");
}
};
int main(int argc, char* argv[])
{
//创建了儿子对象,并将其指针赋值给父亲
CParent* p = new CTest2;//儿子的内容包含父亲所有内容,指针访问安全
delete p;
return 0;
}
运行结果:父类子类对象的析构函数均被调用
上述结果也可以从p->~CParent(); free(p)
进行理解,p
为指针,~CParent()
为虚函数,因此为间接调用,就会查子类虚表,子类虚表第0项–子类的析构,即p
是CTest2
类型,就会调用 ~CTest2()
。子类析构调用之后再调用父类析构,再调用父类的虚表。
一般情况是否有虚并不重要,一旦有父子的继承关系,调用
delete的时候,调用对应的析构函数,就会通过查询虚表进行调用,避免了某一次析构少调用的情况
。
3.学习视频地址: C++57个入门知识点_46 虚析构函数的作用