虚拟析构函数和内存释放

Virtual destructor and memory deallocation

本文关键字:释放 内存 析构函数 虚拟      更新时间:2023-10-16

我不太确定我是否理解虚拟析构函数和在堆右侧分配空间的概念。让我们看看下面的例子:

class Base
{
public:
    int a;
};
class Derived : public Base
{
public:
    int b;
};

我想如果我做这样的

Base *o = new Derived;

堆上分配了8个字节(或系统上需要的任何两个整数),看起来如下:…|a|b|。。。

现在,如果我这样做:

delete o;

"delete"如何知道,为了从堆中删除所有内容,实际上是哪种类型的o?我想它必须假设它是Base类型,因此只从堆中删除a(因为它不能确定b是否属于对象o):…|b|。。。

b将保留在堆上并且不可访问。

执行以下操作:

Base *o = new Derived;
delete o;

真的会引发内存泄漏,我需要一个虚拟析构函数吗?或者delete知道o实际上是派生类的,而不是基类的吗?如果是的话,这是怎么回事?

谢谢大家。:)

您对实现做了很多假设,这可能或者可以不保持。在delete表达式中,动态类型必须是与静态类型相同,除非静态类型具有虚拟析构函数。否则,它就是未定义的行为。时期那是你所要知道的一切;我在实现中使用过否则它就会崩溃,至少在某些情况下是这样;我用过这样做会破坏自由空间领域的实现,因此代码稍后会崩溃,变成一个完全无关的部分的代码。(记录在案,VC++和g++都属于第二种情况,分别为至少在使用已发布代码的常用选项进行编译时是这样。)

首先,您在示例中声明的类具有琐碎的内部结构。从纯粹的实用角度来看,为了正确地销毁此类的对象,运行时代码不需要知道被删除对象的实际类型。它只需要知道要释放的内存块的正确大小。这实际上是C样式库函数(如mallocfree)已经实现的。正如您可能知道的,free隐式地"知道"要释放多少内存。你上面的例子没有涉及到除此之外的任何内容。换句话说,您上面的例子不够详细,无法真正说明任何特定于C++的东西。

然而,从形式上讲,您的示例的行为是未定义的,因为C++语言在形式上需要虚拟析构函数来进行多态删除,而不管类的内部结构有多琐碎。因此,您的"delete如何知道…"问题根本不适用。您的代码已损坏。它不起作用。

其次,当您开始要求对类进行非平凡的销毁时,实际的、具体的C++效果开始出现:通过为析构函数定义显式主体,或者通过向类添加非平凡的成员子对象。例如,如果将std::vector成员添加到派生类,则派生类的析构函数将负责该子对象的(隐式)销毁。为了实现这一点,您必须声明您的析构函数virtual。通过与调用任何其他虚拟函数相同的机制来调用适当的虚拟析构函数。这基本上就是你的问题的答案:运行时代码并不关心对象的实际类型,因为普通的虚拟调度机制将确保调用正确的析构函数(就像它与任何其他虚拟函数一起工作一样)。

第三,当您为类定义专用的operator delete函数时,虚拟销毁的另一个显著效果就会出现。语言规范要求选择正确的operator delete函数,就好像它是从要删除的类的析构函数内部查找的一样。许多实现实际上实现了这一要求:它们实际上从类析构函数内部隐式调用operator delete。为了使该机制正常工作,析构函数必须是虚拟的。

第四,你问题的一部分似乎表明,你认为如果不能定义虚拟析构函数,就会导致"内存泄漏"。这是一个流行的,但完全不正确和完全无用的城市传说,由低质量的来源延续。在没有虚拟析构函数的类上执行多态删除会导致未定义的行为,并导致完全不可预测的破坏性后果,而不是一些"内存泄漏"。在这种情况下,"内存泄漏"不是问题所在。

被删除对象的大小没有问题-这是已知的。虚拟析构函数解决的问题可以演示如下:

class Base
{
public:
    Base() { x = new char[1]; }
    /*virtual*/ ~Base() { delete [] x; }
private:
    char* x;
};
class Derived : public Base
{
public:
    Derived() { y = new char[1]; }
    ~Derived() { delete [] y;}
private:
    char* y;
};

然后拥有:

Derived* d = new Derived();
Base* b = new Derived();
delete d;   // OK
delete b;   // will only call Base::~Base, and not Derived::~Derived

第二次删除将无法正确完成对象。如果virtual关键字被取消注释,那么第二个delete语句将按预期运行,它将与Base::~Base一起调用Derived::~Derived

正如评论中所指出的,严格地说,第二次删除会产生未定义的行为,但这里使用它只是为了强调虚拟析构函数。