#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”,它可以正常工作
答案 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
和代码终止之间的间隔内。