首页 > 代码库 > virtual 修饰符与继承对析构函数的影响(C++)

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

以前,知道了虚函数表的低效性之后,一直尽量避免使用之。所以,在最近的工程中,所有的析构函数都不是虚函数。
今天趁着还书的机会到图书馆,还书之后在 TP 分类下闲逛,偶然读到一本游戏编程书,里面说建议将存在派生的类的析构函数都设置为 virtual。例如 ParentClass 和 ChildClass(派生自 ParentClass),如果 ParentClass 的 ~ParentClass() 不是 virtual 的话,以下代码会产生潜在的问题:

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

有什么问题呢?~ChildClass() 此时不会被调用。
于是想起来,赶快回来改代码!
我觉得其实析构函数也遵循 virtual 修饰的规则嘛。之前的例子,delete 的时候其实调用的是 ~ParentClass(),因为该函数不是虚函数;而如果是 virtual ~ParentClass() 的话,~ParentClass() 实际上是在虚函数表里的,因此会调用覆盖(override)之的 ~ChildClass()。
实际情况是否是这样的呢?我写了一个小小的示例,展示析构函数修饰符的影响。其中,后缀“v”表示析构函数是虚函数。

  1 #include <stdio.h>
  2 
  3 class P
  4 {
  5 public:
  6     P() {}
  7     ~P()
  8     {
  9         printf("P destruction\n");
 10     }
 11 };
 12 
 13 class Pv
 14 {
 15 public:
 16     Pv() {}
 17     virtual ~Pv()
 18     {
 19         printf("Pv destruction\n");
 20     }
 21 };
 22 
 23 class CP
 24     : public P
 25 {
 26 public:
 27     CP() {}
 28     ~CP()
 29     {
 30         printf("CP destruction\n");
 31     }
 32 };
 33 
 34 class CPv
 35     : public Pv
 36 {
 37 public:
 38     CPv() {}
 39     ~CPv()
 40     {
 41         printf("CPv destruction\n");
 42     }
 43 };
 44 
 45 class CvP
 46     : public P
 47 {
 48 public:
 49     CvP() {}
 50     virtual ~CvP()
 51     {
 52         printf("CvP destruction\n");
 53     }
 54 };
 55 
 56 class CvPv
 57     : public Pv
 58 {
 59 public:
 60     CvPv() {}
 61     virtual ~CvPv()
 62     {
 63         printf("CvPv destruction\n");
 64     }
 65 };
 66 
 67 int main(int argc, char *argv[])
 68 {
 69     P *p = new P();
 70     Pv *pv = new Pv();
 71     P *pc = new CP();
 72     //P *pcv = new CvP(); // 析构时崩溃
 73     Pv *pvc = new CPv();
 74     Pv *pvcv = new CvPv();
 75     CP *cp = new CP();
 76     CPv *cpv = new CPv();
 77     CvP *cvp = new CvP();
 78     CvPv *cvpv = new CvPv();
 79 
 80     printf("-----------------------------\n");
 81     delete p;
 82     printf("-----------------------------\n");
 83     delete pv;
 84     printf("-----------------------------\n");
 85     delete pc;
 86     printf("-----------------------------\n");
 87     //delete pcv; // 父类析构调用没问题,然后崩溃
 88     printf("-----------------------------\n");
 89     delete pvc;
 90     printf("-----------------------------\n");
 91     delete pvcv;
 92     printf("-----------------------------\n");
 93     delete cp;
 94     printf("-----------------------------\n");
 95     delete cpv;
 96     printf("-----------------------------\n");
 97     delete cvp;
 98     printf("-----------------------------\n");
 99     delete cvpv;
100     printf("-----------------------------\n");
101 
102     return 0;
103 }

其中删除静态类型为 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. 父类析构哈数非虚函数,子类析构函数非虚函数,使用父类作为静态类型的析构(跳过了子类的析构函数)。

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