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

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

分享到 评论