c ++:下面一段代码崩溃了

时间:2010-02-10 07:36:02

标签: c++ destructor

#include <iostream>
using namespace std;

class B
{
public:
    B() { cout << "Base B()" << endl; }
    ~B() { cout << "Base ~B()" << endl; }
private:
    int x;
};

class D : public B
{
public:
    D() { cout << "Derived D()" << endl; }
    virtual ~D() { cout << "Derived ~D()" << endl; }
};

int
main ( void )
{
    B* b = new D;
    delete b;
}


---- output----------
Base B()
Derived D()
Base ~B()
*** glibc detected *** ./a.out: free(): invalid pointer: 0x0930500c ***
======= Backtrace: =========
/lib/tls/i686/cmov/libc.so.6[0xb7d41604]
/lib/tls/i686/cmov/libc.so.6(cfree+0x96)[0xb7d435b6]
/usr/lib/libstdc++.so.6(_ZdlPv+0x21)[0xb7f24231]
./a.out[0x8048948]
/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe5)[0xb7ce8775]
./a.out[0x80487c1]
Aborted

如果我从基类中删除私有成员“int x”,它可以正常工作

8 个答案:

答案 0 :(得分:12)

基类B的析构函数也必须是虚拟的。

答案 1 :(得分:6)

class B没有虚拟析构函数,您尝试delete通过指向class D的{​​{1}}派生class B的实例 - 这是未定义的行为。您必须使class B析构函数为虚拟以使代码正常工作。

答案 2 :(得分:2)

另一个答案可能是使用boost :: shared_ptr:其模板constructors将记住您的对象是D类型。

#include <boost/shared_ptr.hpp>

int
main ( void )
{
    boost::shared_ptr<B> b( new D );
}

即使没有虚拟析构函数,上面的代码修改也能正常工作。

顺便说一句,除非你想存储指向D的指针,否则将D的析构函数设为虚拟是没有用的。

答案 3 :(得分:2)

您正在做的是UB,但对于您使用行为的特定编译器,可以描述如下。要简化下面的ASCII图形,请修改示例,将y成员添加到D并修改main

#include <iostream>
using namespace std;

class B
{
public:
    B() { cout << "Base B()" << endl; }
    ~B() { cout << "Base ~B()" << endl; }
private:
    int x;
};

class D : public B
{
public:
    D() { cout << "Derived D()" << endl; }
    virtual ~D() { cout << "Derived ~D()" << endl; }
private:
    int y; // added.
};

int
main ( void )
{
    D* d = new D; // modified.
    B* b = d;
    delete b;
}

在您使用的编译器中,vtable(如果有)放在内存块的开头。 在编译器中,您使用的内存布局如下:

+--------+
| vtable | <--- d points here, at the start of the memory block.
+--------+
| x      | <--- b points here, in the middle of the memory block.
+--------+
| y      |
+--------+

稍后调用delete b时,程序将使用free指针尝试b内存块,该指针指向内存块的中间位置。

这将导致由于无效指针错误而导致崩溃。

答案 4 :(得分:1)

好吧,如果你不想要虚拟析构函数,那么必须删除对象,并指向它的实际类型:

int
main ( void )
{
    D* as_d = new D;
    B *as_b = as_d;
    // you can use the object via either as_b or as_d but
    // you must delete it via as_d
    delete as_d;
}

那就是说,如果你不小心,可以很容易通过错误的指针删除对象。

所以我知道你不想要它,但为了你自己的理智,只需将B析构函数虚拟化。

答案 5 :(得分:0)

当你使用基类指针销毁派生类对象时,如果基类ctor不是虚拟的,它将导致对象的部分破坏((只调用基类构造函数)。

因此您必须将基类desrtuctor设为虚拟。

答案 6 :(得分:0)

当一个类纯粹是非虚拟的时,它没有VPTR的条目。因此B恰好是4个字节。

这是记忆的illustration。 VPTR位于最小的内存位置。这样所有派生类都知道在哪里找到VPTR。因此,D是8个字节,前4个是VPTR,接下来是4个。

但是,不是D是-B?不,它的工作方式与多重继承相同。当你将一个D地址分配给一个B指针时,编译器知道这一点,而不是给你D的 REAL 地址,而是给你一个偏移的地址,使它像B一样工作。 case,它实际上偏移了4个字节。因此,当您尝试B-&gt; x时,您将获得正确的值。

当你将这个偏移地址免费传递回堆时,一切都变得疯狂,因为它需要的是原始地址。此行为未定义。它也发生在多重继承中。

答案 7 :(得分:0)

我认为作为整数成员导致内存崩溃的事实,这只是“运气”的问题。我的意思是,如果基类中的析构函数不是虚拟的,则不会调用D的“破坏”。

因此,在内存中,堆中的对象D看起来像:

  Object D
+-----------+
| B subobj. |
+-----------+
| D members |
+-----------+

如果B的析构函数不是虚拟的,并且您删除了B的指针基数,则D部分不会被破坏。在您的情况下,D部分的大小为0,B部分的大小为sizeof(int)(4个字节),这使得“猜测”情况稍微复杂一些,但也许您的编译器会因任何原因向内存中的对象添加其他信息。

因此,在删除b之后,但在应用程序结束之前,编译器在退出时添加的某些代码可能导致崩溃,因为您无法正确删除b (重复使用这部分内存或类似内容)。

由于您的代码非常短,您可以在汇编级别使用“gdb”检查代码的行为,在删除b和代码终止之间的间隔内。