• 以下程序导致段错误。从打印结果中我发现崩溃前没有调用任何 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

  • 5
    此行 X *xp = new Y[5]; xp 不是指向数组的指针,而是指向新创建数组的第一个元素的基类的指针。因此delete[] xp无法通过指针删除数组xp


    – 


  • 4
    动态类型擦除和数组分配\删除不兼容,它是 ub。使用指针,X您可以销毁 的一个实例Y或 的数组X。可能存在可以工作的实现,但不一定。


    – 


  • @Swift-FridayPie 不知道为什么这是 UB,但在 msvc 版本中,所有这些都正常工作


    – 

  • 您可以使用 aX*来引用 a Y。但您不能使用 aX*来引用数组Y。某物的数组是数组;某物的数组不是某物。


    – 

  • 为什么当我将 m_b 的类型更改为 double(或任何其他 8 字节长类型)时程序不会崩溃?为什么当我将 X 的 Dtor 设为非虚拟时程序也不会崩溃? — 说实话,你会花很多时间试图找出未定义的行为吗?未定义的行为就是 — 未定义。如果另一个版本的编译器、不同的编译器设置或其他因素导致不同的行为怎么办?


    – 



最佳答案
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 Ybase 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 输出正确,你如何查看


    –