C++调试断言仅在使用VPTR时失败
C++ Debug Assertion Fails Only With VPTR
我想知道为什么在一种情况下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
,因此将始终调用B
的dtor
。
编辑:对于第一种情况,以下是标准的说明:
§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_part
是B
的第一个成员,因此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);
在这种情况下,vp2
和vp1
是相同的。系统正在分配和解除分配相同的内存地址,因此程序运行时不会出错。
当类B
具有虚拟成员函数(在这种情况下为析构函数)时。编译器添加了一个虚拟表指针,所以类B最终看起来像这样:
struct B {
B_vtable* __vptr;
A __a_part;
};
这里的问题是__a_part
不再是第一个成员,convertToAPointer
操作现在将更改指针的地址,因此vp2
和vp1
不再指向同一地址。由于释放的内存位置与分配的内存位置不同,因此会出现错误。
- 如果没有malloc,链表实现将失败
- 模板参数替换失败,并且未完成隐式转换
- 具有默认模板参数的多态类的模板推导失败
- 视图中的参数推导失败:take_while
- 链接到自行创建的dll失败
- 带有特殊路径部分的"std::filesystem::weakly_canonical"失败
- GetShortPathName在网络驱动器上使用中文文件夹时失败
- gcc和c++17的过载解析失败
- 为什么使用 P/Invoke 调用 dll 时,某些计算机中的 LoadLibrary 失败?
- 在WSL:configure_file上对config_file的每次调用都失败:配置文件时出现问题
- 使用 GCC 卸载的 OpenMP 卸载失败,并出现"Ptx assembly aborted due to errors"
- 使用cmake从源代码构建MySQL连接器/C++失败(与以前的声明冲突)
- 链接阶段在Ubuntu上失败,但在MacOS上失败
- 从父数组测试用例构造二叉树失败
- LibGit2 SSH身份验证失败
- 如何让LLDB在成功时退出,在失败时等待
- VS2017,C++包含目录与附加包含目录,子文件夹包含失败-但为什么
- 生成MRPT库时cmake配置失败
- 为什么除非添加括号,否则构造函数上的模板替换会失败?
- C++调试断言仅在使用VPTR时失败