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种使用方式中,有两种是不好的:
- 父类析构函数非虚函数,子类析构函数是虚函数,使用父类作为静态类型的析构(崩溃);
- 父类析构函数非虚函数,子类析构函数非虚函数,使用父类作为静态类型的析构(跳过了子类的析构函数)。
其余情况下,只要父类的析构函数是虚函数,就不需要关心指针的静态类型;统一指针的静态类型和动态类型(显式让运行时调用子类的析构函数)也可以避免意外。