博客园地址
以前,知道了虚函数表的低效性之后,一直尽量避免使用之。所以,在最近的工程中,所有的析构函数都不是虚函数。
今天趁着还书的机会到图书馆,还书之后在 TP 分类下闲逛,偶然读到一本游戏编程书,里面说建议将存在派生的类的析构函数都设置为 virtual
。例如 ParentClass
和 ChildClass
(派生自 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种使用方式中,有两种是不好的:
父类析构函数非虚函数,子类析构函数是虚函数,使用父类作为静态类型的析构(崩溃);
父类析构函数非虚函数,子类析构函数非虚函数,使用父类作为静态类型的析构(跳过了子类的析构函数)。
其余情况下,只要父类的析构函数是虚函数,就不需要关心指针的静态类型;统一指针的静态类型和动态类型(显式让运行时调用子类的析构函数)也可以避免意外。