-
以下程序导致段错误。从打印结果中我发现崩溃前没有调用任何 Dtor。
-
在 gdb 中,我看到每个 Y 对象都包含一个指向其 vtable 的指针。因此,当尝试删除对象时,程序将在 Y 的 vtable 中搜索 Dtor。
-
但是,我们使用 X 指针指向 Y 对象 –
-
所以我的理论是,程序试图访问 Y 的 vtable 中与 X 的 Dtor 相对应的条目,因为(这是我试图获得的确认)它使用函数名称(’~X’ 因为指针的类型是 X)计算偏移量(从 vtable 的开头开始),这使得它尝试访问无效地址。
-
所以问题是 – 如何计算从 vtable 开始的偏移量?它真的是从函数的名称开始的吗?
-
请让我知道你们的想法,如果我说的完全是胡言乱语,请纠正我 XD
-
非常感谢 🙂
class X
{
public:
virtual ~X() {}
private:
double m_a;
};
class Y: public X
{
private:
int m_b;
};
int main()
{
X *xp = new Y[5];
delete[] xp;
return 0;
}
还有两件事我想弄清楚:
-
为什么当我将 m_b 的类型更改为 double(或任何其他 8 字节长类型)时程序不会崩溃?
-
为什么当我将 X 的 Dtor 设为非虚拟时程序也不会崩溃?
-
ps-我正在使用带有 g++ 编译器的 C++ 98。
5
最佳答案
3
让我们试着看看新对象是如何存储的,我认为是 32 位指针/整数,X、Y 是指向相应 vtable 的指针(您不能假设实现细节是正确的),a 代表 m_a,b 代表 m_b,每个字节一个字母。分配中仅显示 3 个元素。
Y000aaaaaaaabbbbY001aaaaaaaabbbbY002aaaaaaaabbbb
XP 如何看待世界
X000bbbbX001bbbbX002bbbb
当我们把它们放在一起时,就会出现问题
Y000aaaaaaaabbbbY001aaaaaaaabbbbY002aaaaaaaabbbb
X000bbbbX001bbbbX002bbbb
^^^^
因此,当调用第一个析构函数时,X000 是 Y 的虚拟函数,两者都不执行任何操作。当调用第二个析构函数时,X001 实际上是用作 X 的析构函数的虚拟地址的双精度数的最后 4 个字节,并且很可能为空,您会得到段错误。
当将 X 更改为非虚拟析构函数时,您不会调用 vtable,因此不会崩溃。
将 b 改为 8 字节
Y000aaaaaaaabbbbbbbbY001aaaaaaaabbbbbbbb
X000bbbbbbbbX001bbbbbbbbX002bbbbbbbb
^^^^
这应该会崩溃,如果没有,这里可能有一些填充或者指针是 8 个字节。
Y0000000aaaaaaaabbbbbbbbY0000001aaaaaaaabbbbbbbb
X0000000bbbbbbbbX0000001bbbbbbbbX0000002bbbbbbbb
^^^^^^^^
使用 m_b 作为虚拟表指针,它要么为空,要么为随机地址。
5
-
谢谢你的回答。但有些东西对我来说还是不太合理:
so when the destructor is called the first X000, is the virtual from Y, neither does anything
你在这里声称第一次调用 Dtor 应该没问题,但当我在其中打印时却看不到它,这意味着程序在那之前就崩溃了。
– -
@AlonKalif 这可能是因为一些优化,你使用-O0 吗?
– -
这是编译命令:
g++ -std=c++98 -pedantic-errors -Wall -Wextra -g
如果我没有指定它,我认为它是-O0,对吗?
– -
似乎默认为 -O0。但我认为我们无法再进一步,因为不同的编译器会给出不同的结果,因为这是未定义的行为。
– -
我想我已经找到答案了。当我们使用 new[] 然后使用 delete[] 时,对象需要按照创建它们的相反顺序被销毁。所以也许我没有看到 Dtor 的任何打印是因为第一个尝试调用的 Dtor 是在 xp[4] 中,而在内存中的那个位置有一个 double。你认为我的解释正确吗?
–
|
以下程序导致段错误。
程序导致段错误的原因是,创建新的 Y[5],allocating an array of 5 Y objects
将 Y 对象存储在类型为 X* 的指针中。然后调用 delete[]。由于 X 具有虚拟析构函数,因此运行时会尝试为数组中的每个 X 对象调用析构函数。但它无法正确处理 Y 对象的析构函数,从而导致段错误。
您可以尝试使用std::vector
来存储derived objects Y
在base class pointer X*
。
std::vector<std::unique_ptr<X>> objects;
for (int i = 0; i < 5; ++i) {
objects.push_back(std::make_unique<Y>());
}
|
我认为此代码必须正确运行。它至少如何正确生成并与 msvc 编译器一起工作。
void DbgPrint(const char* fmt, ...);
class X
{
char m_a[0x18];
public:
virtual ~X() {
DbgPrint("%hs<%p>\n", __FUNCTION__, this);
}
X() {
DbgPrint("%hs<%p>\n", __FUNCTION__, this);
}
void operator delete[](PVOID pv)
{
DbgPrint("%hs<%p>\n", __FUNCTION__, pv);
free(pv);
}
void* operator new[](size_t s)
{
PVOID pv = malloc(s);
DbgPrint("%hs<%p>\n", __FUNCTION__, pv);
return pv;
}
};
class Y : public X
{
char m_b[0x20];
public:
virtual ~Y() {
DbgPrint("%hs<%p>\n", __FUNCTION__, this);
}
Y() {
DbgPrint("%hs<%p>\n", __FUNCTION__, this);
}
};
void test ()
{
if (X *xp = new Y[2])
{
DbgPrint(";;;; xp = %p ;;;;\n", xp);
delete[] xp;
}
}
并输出:
X::operator new[]<00000214750FEC70>
X::X<00000214750FEC78>
Y::Y<00000214750FEC78>
X::X<00000214750FECB8>
Y::Y<00000214750FECB8>
;;;; xp = 00000214750FEC78 ;;;;
Y::~Y<00000214750FECB8>
X::~X<00000214750FECB8>
Y::~Y<00000214750FEC78>
X::~X<00000214750FEC78>
X::operator delete[]<00000214750FEC70>
(new[]/delete[]
我添加的操作符只是为了 dbgprint 内存分配值,即使没有它代码也能正常工作)
一般来说,此代码首先为 header + N 对象分配内存:
size_t cbHeader = (sizeof(size_t) + alignof(Y) - 1) & ~(alignof(Y) - 1);
void* pv = malloc(cbHeader + N * sizeof(Y));
在此内存的开头写入对象的数量
*(size_t*)pv = N;
然后调用
void `vector constructor iterator'(
void *pv,
size_t s,
size_t n,
void * (*fn)(void *));
`vector constructor iterator'((char*)pv + cbHeader, sizeof(Y), N, &Y::Y);
并返回xp = (char*)pv + cbHeader;
这delete[] xp;
是虚拟调用。因此通过指向虚拟表的指针来调用xp
结果被称为
virtual void * Y::`vector deleting destructor'(unsigned int);
函数在class Y
,尽管xp
声明为X*
这
Y::`vector deleting destructor'
内部调用
void `vector destructor iterator'(
void *pv,
size_t s,
size_t n,
void * (*fn)(void *));
`vector destructor iterator'(xp, sizeof(Y), *(size_t*)((char*)xp - cbHeader), &Y::Y)
最后释放内存块。
所以如果你的代码崩溃了,很可能是编译器实现错误
3
-
上尝试过,但只得到了 X 个析构函数。最新的 GCC,在 MSVC 中我得到了这两个函数(但使用的标志与 GCC 不同)。
– -
@Surt 关键点是如何
delete[] xp;
翻译。在您的示例中,这是静态(编译时)调用,而在我的示例中,这是通过 vtable 进行的虚拟xp
调用。这使得不同
–
-
@Surt 如果 msvc 输出正确,你如何查看
–
|
X *xp = new Y[5];
xp
不是指向数组的指针,而是指向新创建数组的第一个元素的基类的指针。因此delete[] xp
无法通过指针删除数组xp
。–
X
您可以销毁 的一个实例Y
或 的数组X
。可能存在可以工作的实现,但不一定。–
–
X*
来引用 aY
。但您不能使用 aX*
来引用的数组Y
。某物的数组是数组;某物的数组不是某物。–
–
|