C ++调试断言仅对VPTR失败

时间:2013-02-16 12:37:38

标签: c++ exception memory-management

我想知道为什么我在这里的一个案例中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。

2 个答案:

答案 0 :(得分:3)

请参阅this post

快速摘要:

  • 您告诉机器删除A
  • 的实例
  • 由于这是我们通过指针/引用调用的类,我们应该使用虚拟表(VT)吗?
  • A中没有虚拟成员,因此没有使用VT
  • 我们称之为A ...
  • 的标准析构函数
  • 砰!我们试图删除A类,但它发生了指针 导致我们成为B的对象,其中包含一个A不知道的VT。 sizeof(A)是1(作为AFAIK,大小等于0是不合法的)和 sizeof(B)为4(由于存在VT)。我们希望删除1个字节,但是 有一个4字节的块。由于DEBUG堆监控,错误 被抓了。

当然,解决方案是将基类(Adtor声明为virtual,因此始终会调用B的{​​{1}}

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

  

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

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

答案 1 :(得分:1)

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

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

struct A {
};

由于类B派生自类A,类A将嵌入到B中。当类B没有虚函数时,布局最终如下所示:

struct B {
  A __a_part;
  int x;
};

编译器可以通过取B*的地址将A*转换为__a_part,就像编译器有这样的函数一样:

A * convertToAPointer(B * bp){return&amp; bp-&gt; __ a_part; }

由于__a_partB的第一个成员,B*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操作现在将更改指针的地址,因此vp2vp1否更长的点指向同一地址。由于释放的内存位置与分配的内存位置不同,因此会出现错误。