virtual 修饰符与继承对析构函数的影响(C++)

博客园地址

以前,知道了虚函数表的低效性之后,一直尽量避免使用之。所以,在最近的工程中,所有的析构函数都不是虚函数。

今天趁着还书的机会到图书馆,还书之后在 TP 分类下闲逛,偶然读到一本游戏编程书,里面说建议将存在派生的类的析构函数都设置为 virtual。例如 ParentClassChildClass(派生自 ParentClass),如果 ParentClass~ParentClass() 不是 virtual 的话,以下代码会产生潜在的问题:

ParentClass *pClass = new ChildClass();
delete pClass;

有什么问题呢?~ChildClass() 此时不会被调用。

于是想起来,赶快回来改代码!

我觉得其实析构函数也遵循 virtual 修饰的规则嘛。之前的例子,delete 的时候其实调用的是 ~ParentClass(),因为该函数不是虚函数;而如果是 virtual ~ParentClass() 的话,~ParentClass() 实际上是在虚函数表里的,因此会调用覆盖(override)之的 ~ChildClass()

实际情况是否是这样的呢?我写了一个小小的示例,展示析构函数修饰符的影响。其中,后缀“v”表示析构函数是虚函数。

#include <stdio.h>

class P
{
public:
P() {}
~P()
{
printf("P destruction\n");
}
};

class Pv
{
public:
Pv() {}
virtual ~Pv()
{
printf("Pv destruction\n");
}
};

class CP
: public P
{
public:
CP() {}
~CP()
{
printf("CP destruction\n");
}
};

class CPv
: public Pv
{
public:
CPv() {}
~CPv()
{
printf("CPv destruction\n");
}
};

class CvP
: public P
{
public:
CvP() {}
virtual ~CvP()
{
printf("CvP destruction\n");
}
};

class CvPv
: public Pv
{
public:
CvPv() {}
virtual ~CvPv()
{
printf("CvPv destruction\n");
}
};

int main(int argc, char *argv[])
{
P *p = new P();
Pv *pv = new Pv();
P *pc = new CP();
//P *pcv = new CvP(); // 析构时崩溃
Pv *pvc = new CPv();
Pv *pvcv = new CvPv();
CP *cp = new CP();
CPv *cpv = new CPv();
CvP *cvp = new CvP();
CvPv *cvpv = new CvPv();

printf("-----------------------------\n");
delete p;
printf("-----------------------------\n");
delete pv;
printf("-----------------------------\n");
delete pc;
printf("-----------------------------\n");
//delete pcv; // 父类析构调用没问题,然后崩溃
printf("-----------------------------\n");
delete pvc;
printf("-----------------------------\n");
delete pvcv;
printf("-----------------------------\n");
delete cp;
printf("-----------------------------\n");
delete cpv;
printf("-----------------------------\n");
delete cvp;
printf("-----------------------------\n");
delete cvpv;
printf("-----------------------------\n");

return 0;
}

其中删除静态类型为 P * 动态类型为 CvP *pcv 时会崩溃。

其余结果如下:

-----------------------------
P destruction
-----------------------------
Pv destruction
-----------------------------
P destruction
-----------------------------
-----------------------------
CPv destruction
Pv destruction
-----------------------------
CvPv destruction
Pv destruction
-----------------------------
CP destruction
P destruction
-----------------------------
CPv destruction
Pv destruction
-----------------------------
CvP destruction
P destruction
-----------------------------
CvPv destruction
Pv destruction
-----------------------------

可见,我的想法不是完全正确的。

总结一下,在10种使用方式中,有两种是不好的:

  1. 父类析构函数非虚函数,子类析构函数是虚函数,使用父类作为静态类型的析构(崩溃);
  2. 父类析构函数非虚函数,子类析构函数非虚函数,使用父类作为静态类型的析构(跳过了子类的析构函数)。

其余情况下,只要父类的析构函数是虚函数,就不需要关心指针的静态类型;统一指针的静态类型和动态类型(显式让运行时调用子类的析构函数)也可以避免意外。

分享到 评论