4

我想知道为什么我delete在这里的一种情况下得到了例外,而在另一种情况下却没有。

无异常情况

#include <iostream>
using namespace std;

class A
{
public:
    ~A() { cout << "A dtor" << endl; }
};

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


A* f() { return new B; }

int _tmain(int argc, _TCHAR* argv[])
{
    cout << sizeof(B) << " " << sizeof(A) << endl;
    A* bptr= f();
    delete bptr;
}

这里的输出是4 1 .. A dtor,因为 A 有 1 个字节用于标识,B 有 4 个字节,因为int x

例外情况

#include <iostream>
using namespace std;

class A
{
public:
    ~A() { cout << "A dtor" << endl; }
};

class B : public A
{
public:
    virtual ~B() { cout << "B dtor" << endl; }
};


A* f() { return new B; }

int _tmain(int argc, _TCHAR* argv[])
{
    cout << sizeof(B) << " " << sizeof(A) << endl;
    A* bptr= f();
    delete bptr;
}

这里的输出是4 1 .. A dtor,因为 A 有 1 个字节用于标识,B 有 4 个字节,因为vptr它的虚拟析构函数需要它。 但随后在调用 ( )中的调试断言失败delete_BLOCK_TYPE_IS_VALID

环境

我正在使用 Visual Studio 2010 SP1Rel 运行 Windows 7。

4

2 回答 2

3

看到这个帖子

快速总结:

  • 您正在告诉机器删除 A 的一个实例
  • 由于这是我们通过指针/引用调用的类,也许我们应该使用虚拟表(VT)?
  • A 中没有虚拟成员,因此没有使用 VT
  • 我们将 A 的标准析构函数称为...</li>
  • 砰! 我们正在尝试删除类 A,但碰巧指针已将我们引导到 B 的对象,该对象包含 A 不知道的 VT。sizeof(A) 是 1(因为 AFAIK,大小等于 0 是不合法的)并且 sizeof(B) 是 4(由于 VT 的存在)。我们希望删除 1 个字节,但是有一个 4 个字节的块。由于 DEBUG 堆监控,错误被捕获。

解决方案当然是声明基类的 ( ' s A)dtor因为virtualso总是会被调用。Bdtor

编辑:对于第一种情况,这是标准所说的:

§5.3 在第一种选择(删除对象)中,如果待删除对象的静态类型与其动态类型不同,则静态类型应为待删除对象的动态类型和静态类型的基类应具有虚拟析构函数或行为未定义。在第二种选择(删除数组)中,如果要删除的对象的动态类型与其静态类型不同,则行为未定义。

因此,这两种情况都将我们引向未定义行为的领域,这当然不同于另一种实现。但有理由认为,对于大多数实现来说,第一种情况比第二种情况更容易处理或至少更容易考虑,后者只是一种深奥的反模式。

于 2013-02-16T12:52:52.410 回答
1

正如其他人指出的那样,您正在删除一个静态类型与其动态类型不同的对象,并且由于静态类型没有虚拟析构函数,您会得到未定义的行为。这包括您所看到的有时工作有时不工作的行为。但是,我认为您有兴趣更深入地了解您的特定编译器正在发生的事情。

A根本没有成员,所以它的数据布局最终看起来像这样:

struct A {
};

由于 classB派生自 class A,因此 classA嵌入在 B 中。当 classB没有虚函数时,布局最终看起来像这样:

struct B {
  A __a_part;
  int x;
};

编译器只需获取 的地址就可以将 a 转换B*为 an ,就好像编译器有这样的函数:A*__a_part

A* convertToAPointer(B* bp) { return &bp->__a_part; }

由于__a_part是 的第一个成员BB*A*指向同一个地址。

像这样的代码:

A* bptr = new B;
delete bptr;

正在有效地做这样的事情:

// Allocate a new B
void* vp1 = allocateMemory(sizeof(B));
B* bp = static_cast<B*>(vp1);
bp->B(); // assume for a second that this was a legal way to construct

// Convert the B* to an A*
A* bptr = &bp->__a_part;

// Deallocate the A*
void* vp2 = ap;
deallocateMemory(vp2);

在这种情况下,vp2vp1都是一样的。系统正在分配和释放相同的内存地址,因此程序运行没有错误。

当类B具有虚拟成员函数(在这种情况下为析构函数)时。编译器添加了一个虚拟表指针,所以 B 类最终看起来像这样:

struct B {
  B_vtable* __vptr;
  A __a_part;
};

这里的问题是,__a_part不再是第一个成员,并且convertToAPointer操作现在将更改指针的地址,因此vp2不再vp1指向相同的地址。由于释放的内存位置与分配的内存位置不同,因此您会收到错误消息。

于 2013-02-17T16:21:22.510 回答