2

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

class Base
{
public:
    int a;
};

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

我想如果我做这样的事情

Base *o = new Derived;

在堆上分配了 8 个字节(或系统需要的任何两个整数),看起来像这样: ... | 一个 | 乙 | ...

现在,如果我这样做:

delete o;

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

然后 b 将保留在堆上并且无法访问。

执行以下操作:

Base *o = new Derived;
delete o;

真正引起内存泄漏,我需要一个虚拟析构函数吗?还是 delete 知道 o 实际上属于 Derived 类,而不是 Base 类?如果是这样,它是如何工作的?

多谢你们。:)

4

3 回答 3

3

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

于 2013-08-04T20:39:18.900 回答
2

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

但是,从形式上讲,您的示例的行为是未定义的,因为 C++ 语言正式要求使用虚拟析构函数进行多态删除,而不管类的内部结构多么微不足道。因此,您的“如何delete知道...”问题根本不适用。你的代码坏了。这没用。

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

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

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

于 2013-08-04T20:24:28.503 回答
1

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

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语句将按预期运行,并Derived::~DerivedBase::~Base.

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

于 2013-08-04T20:13:56.167 回答