如果使用基指针声明,为什么简单的析构函数不会删除派生对象

时间:2013-12-13 18:34:22

标签: c++ inheritance

int main()
{

    Base *p = new Derived;
    delete p;
    return 0;
}

我有些困惑,为什么在这里删除p不会删除派生对象?是这样派生对象包含基类型的子对象,而指针p实际上指向派生对象的基础部分(子对象)。因此,当delete p运行时,它将只能删除派生类对象的基类部分,并且它将具有导出部件销毁的未定义行为。这将导致内存泄漏。因此,为了避免内存泄漏,我们需要设法调用对象的基础和派生析构函数以避免内存泄漏!是这样吗?这是我们需要虚拟析构函数的原因吗?因此,当基础析构函数是虚拟的时,派生类析构函数将覆盖基类的析构函数(它是虚拟的)。请澄清??

4 个答案:

答案 0 :(得分:3)

您应该声明一个虚拟析构函数:

class Base {
   //etc...
   virtual ~Base();
};

class Derived {
   //etc...
   virtual ~Derived();
};

Base* p = new Derived();
delete p;

(当然上面遗漏了很多东西,包括构造函数)

答案 1 :(得分:3)

如果你在询问基类没有虚拟析构函数时的行为,那么你的混淆就是因为你已经对这个delete表达式的行为有一些预先设想的错误概念。

  

“它只能删除对象的基类部分”

     

“这会导致内存泄漏”

这些都没有任何意义。这里没有内存泄漏,并且没有任何确定性能够“删除它的基类部分”。

如果基类没有虚析构函数,则此类代码的行为只是 undefined 。未定义的行为可以表现出很多种不同的方式,包括但不限于不正确的析构函数调用,不正确的operator delete选择,堆损坏,以及是“内存泄漏”,包括直接和间接的。在这种情况下,有很多不同的东西可以搞砸。不只是一些“内存泄漏”,因为流行的误解让人们相信。 (关于“内存泄漏”的流行内容来自哪里。有人知道吗?)

所以,你确实需要虚拟析构函数。如果一个人决定全力以赴地详细分析它,那么你需要的完整原因列表可能会很长。但无论如何,这是一个实现细节。如果不将它与特定的实现联系起来,没有具体的解释。

至于“概念”解释......总是最明显的一个:当然,为了执行适当的破坏,必须调用正确的析构函数。即使我们只是考虑用户定义的销毁步骤(即用户在派生类析构函数中明确写出的内容),我们仍然需要析构函数多态来正确调用那个析构函数。

但是,还有许多其他内部原因。例如,在典型实现中,对原始内存释放的正确operator delete的选择也会依赖于析构函数的虚拟性(例如,参见here

答案 2 :(得分:1)

除非基类具有虚拟析构函数,否则通过指向其基类的指针删除对象是未定义的。即使使用虚拟析构函数,删除的顺序也可能不会立即浮现在脑海中。

有关详细信息,请参阅this destructor reference

答案 3 :(得分:1)

一方面有很多问题,不幸的是C ++语法具有误导性,所以让我们回顾一下我们的基础。


  

析构函数会发生什么?

当一个对象被破坏时,语言会调用它的析构函数;按顺序发生以下事件序列:

  • 执行析构函数的主体
  • 一次一个地破坏对象的属性,按照构造的相反顺序(从而声明)
  • 基础对象一次一个地被破坏,按照构造的相反顺序(并因此声明)

注意:virtual基数在属性之前被破坏,但是在执行主体之后。

重要的一点是,即使Derived方法隐藏了它的Base对应方(如果有的话),在构造函数和析构函数的情况下,基本对应方会自动为您调用你无法控制的定义明确的点。


  

virtual析构函数是什么?

Base具有virtual析构函数时,Derived类中隐式声明或用户声明的析构函数自然会覆盖它。与其他virtual方法类似,它意味着当析构函数被调用 unqualified (即,不在b.Base::foo()中)时,调用实际上被分派到 final -overider ,它是派生最多的对象的析构函数(对象的真实动态类型)。

然而,正如前面所述,这并不意味着Base析构函数本身永远不会运行,因为析构函数是特殊的;您可以将Derived析构函数视为(自动)实现为:

Derived::~Derived(): ~Base(), ~attr0(), ~attr1() { ... }

代码从右向左执行。


  

delete上的Base*表达式是什么?

好吧,很多人会认为Base* b = ...; delete b; desugared

// NOT QUITE
Base* b = ...;
b.~Base(); // possibly virtual destructor
operator delete(&b);

然而这实际上是不正确的。问题在于:

  • Derived对象地址可能与其Base子对象不同,但必须使用operator delete返回的确切指针值调用operator new
  • 必须在析构函数的定义点解析<{1}}(可以重载)

因此,编译器需要实现一些魔力;这取决于他们。例如,实现Itanium ABI(gcc,icc,clang,...)的编译器将向v表添加一个特殊条目,该条目包含一个魔术函数,该函数在调用最派生对象的析构函数并调用之前执行指针调整operator delete地址正确。它可以被视为:

operator delete

  

那么,如果 class Derived: public Base { public: virtual ~Derived() override {} // FOR ILLUSTRATION PURPOSE ONLY // DON'T DO THIS AT HOME: // - you are forbidden to use `__` in your identifiers // - you are forbidden to call `delete this;` or any similar statement // FOR ILLUSTRATION PURPOSE ONLY virtual void __automagic_delete() { this->Derived::~Derived(); operator delete(this); } }; 不是Base::~Base怎么办?

嗯,正式来说,这是未定义的行为

在实践中,经常出现两个问题:

  1. 由于未调用virtual,因此不会释放其属性或其他基类所拥有的任何资源。这可能导致内存泄漏,文件描述符泄漏,连接器泄漏,死锁,......
  2. 如果Derived::~Derived对象的地址与其Derived子对象的地址不同,则调用Base的地址不正确......严格执行后会立即导致{{1}并且在不太严格的情况下可能会导致内存损坏
  3. 但当然,由于这是未定义的行为,所以任何事情都可能发生,所以这只是冰山一角。