C++调试断言仅在使用VPTR时失败

C++ Debug Assertion Fails Only With VPTR

本文关键字:VPTR 失败 调试 断言 C++      更新时间:2023-10-16

我想知道为什么在一种情况下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。

请参阅

快速总结:

  • 您正在告诉计算机删除A的实例
  • 由于这是一个我们通过指针/引用调用的类,也许我们应该使用虚拟表(VT)
  • A中没有虚拟成员,因此没有使用VT
  • 我们称A的标准析构函数为
  • 砰!我们正试图删除类A,但碰巧指针把我们引向了B的对象,它包含了A不知道的VT。sizeof(A)为1(作为AFAIK,大小等于0是不合法的),并且(B)的大小为4(由于VT的存在)。我们希望删除1个字节,但是存在一个4字节的块。由于DEBUG堆监视,错误被抓住了

当然,解决方案是将基类的(A的)dtor声明为virtual,因此将始终调用Bdtor

编辑:对于第一种情况,以下是标准的说明:

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

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

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

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

struct A {
};

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

struct B {
  A __a_part;
  int x;
};

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

A*convertToAPointer(B*bp){return&bp->__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不再指向同一地址。由于释放的内存位置与分配的内存位置不同,因此会出现错误。