虚析构函数

虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的。

首先看一段示例代码:

class A
{
public:
    ~A(){}
};

class AX : public A
{
public:
    ~AX(){}
};

A* pA = new AX{};
delete pA;

上面代码中 class AX 的析构函数不会被调用,如果在 AX 析构函数中需要进行一些资源释放工作(一般都是如此)则这些工作不会被执行。解决的方法就是将基类析构函数声明为虚函数,如下:

class A
{
public:
    virtual ~A(){}
};

class AX : public A
{
public:
    ~AX(){}
};

A* pA = new AX{};
delete pA;

看起来很完美对不对。

然而本文的目的并不是介绍如何使用虚析构函数,恰恰相反,本文想表达的观点是:当需要用虚析构解决问题时,通常意味着代码设计出问题了。

上面一段代码是所谓的“示例代码”,很少有示例代码的代码逻辑能直接在工程中使用,因为它们通常都太简单了,而实际工程通常都是复杂的,甚至有一些完全无法在工程中使用。这也是为什么很多知识学起来似乎很简单用起来却很难的原因。

接下来我们按照尽量贴近实际工程的方式修改上述代码:

// A.h
class A
{
public:
    virtual ~A(){}
};

// AX.h
#include "A.h"

class AX : public A
{
public:
    ~AX(){}
};

// method.cpp
#include "AX.h"

void Method()
{
    A* pA = new AX{};
    delete pA;
}

代码被分开到了三个文件中,Method() 中的代码实际使用的是 class A,并不需要了解 class AX 的定义,所以我们引入工厂模式来消除 method() 函数对 class AX 的依赖:

// A.h
class A
{
public:
    virtual ~A(){}
};

class AFactory
{
    A* CreateAX();
};

// AX.h
#include "A.h"

class AX : public A
{
public:
    ~AX(){}
};

// AFactory.cpp
#include "AX.h"

A* AFactory::CreateAX()
{
    return new AX{};
}

// method.cpp
#include "A.h"

void Method()
{
    AFactory factory;
    A* pA = factory.CreateAX();
    delete pA;
}

终于有点工业强度代码的样子了。看起来似乎没什么问题,对吗?那接着来。

有了工厂模式,可以玩很多花活儿了,比如如果 class AX 的对象会被频繁创建和销毁影响性能,所以我们引入内存池分配内存,或者建个对象池对使用完毕的对象销毁再利用等等。AFactory::CreateAX 可能被改成下面的样子:

A* AFactory CreateAX()
{
    AX* ptr = MemoryPoolAlloc(sizeof(AX));
    return new (ptr) AX{};
}

发现什么了吗?函数 Method() 中原本正确的代码现在变成错误代码了。我们常常强调代码的健壮性,什么叫健壮的代码不太好描述,什么叫脆弱的代码看这里就知道了:我们修改了某个位置的代码,然后在千里之外的其他地方引发了BUG。

更严重的是,不仅仅是修改内存分配方式这种明显的行为,其他诸如升级编译器版本甚至修改编译参数都可能产生同样的效果。在大规模的项目中,class A 和 Method() 的代码分属不同的开发人员、甚至不同团队、不同公司是很平常的事情,这种情况下如此脆弱的代码非常难以维护。

本质上是因为虚析构函数假定派生类对象必须使用 new 创建,使得调用代码和实现代码多出了一条隐藏的强耦合关系。更好的处理方式是如果一个类需要被继承,那么直接把其析构函数声明为 protected,禁止调用代码使用 delete 释放基类指针。可以在创建对象时使用智能指针,这样调用代码就不必关心对象如何创建和如何释放的问题。

虚析构的另一个问题是会改变虚函数表的结构,导致跨语言使用虚函数表时出现兼容性问题,情况比较少见,这里不再展开细说。

只有在设计上需要在基类中处理派生类释放时才必须使用虚析构(比如以继承实现的方式实现侵入式引用计数),其他情况都可以绕开并且比使用虚析构带来更好的代码健壮性。

所以,如果发现需要使用虚析构解决问题,意味着代码该重构了。

    原文作者:One-Direction
    原文地址: https://blog.csdn.net/BUCTOJ/article/details/105148112
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞