为什么虚析构函数要写内存?

3

最近,在编写自定义分配器代码和放置new+delete时,我注意到了一件令人惊讶的事情:当调用虚析构函数时,它会向对象即将被释放的内存中写入内容。

为什么会这样呢?

(更新) 补充: 我更感兴趣的是实际的行为,而不是C++标准所规定的,我确信标准没有明确定义这种行为。

以下是一个简单的程序演示:

#include <new>
#include <cstring>
#include <iostream>

using std::cout;
using std::endl;

struct Interface {
    virtual ~Interface() = default;
};

struct Derived : public Interface {
};

alignas(Derived) unsigned char buffer[sizeof(Derived)];

int main() {

    memset(buffer, 0xff, sizeof(buffer));
    cout << "Initial first byte: 0x" << std::hex << (int)buffer[0] << endl;
    
    // Create an instance, using 'data' as storage
    Derived *pDer = ::new (buffer) Derived();
    cout << "After ctor, first byte: 0x" << std::hex << (int)buffer[0] << endl;
    
    pDer->~Derived();
    
    cout << "After destroy, first byte: 0x" << std::hex << (int)buffer[0] << endl;

    return 0;
}

在线链接:https://godbolt.org/z/jWv6qs3Wc

以下是输出结果:

Initial first byte: 0xff
After ctor, first byte: 0x68
After destroy, first byte: 0x88

如果我移除虚拟Interface,那么内存就不会改变,这是预期的行为。

这是一种调试功能吗?

它似乎是编译器特定的。Clang 不会这样做,但是 GCC 会。

使用-O2选项似乎可以解决此问题。但是,我仍然不确定它的目的。


5
在调试编译中,常见做法是用一种二进制模式覆盖已释放或未初始化的内存,这样可以 A)在使用时很可能导致崩溃;B)有助于识别其他问题,如缓冲区溢出。 - paddy
1
你的程序通过访问已经不存在的对象导致了未定义行为(new 导致字符数组中的字符的生命周期结束)。 - M.M
3
缓冲区可能没有正确对齐Derived - M.M
2
所有标准规定的是调用析构函数会结束受影响对象的生命周期。除了析构函数明确执行的操作外,标准既不要求也不阻止实现修改对象所占用的内存。很可能你看到的是编译器提供的辅助调试信息或者与你提供的缓冲区相对齐的对象对齐相关的内容。 - Peter
@M.M 哦,感谢你提醒我关于 alignas 的问题;我已经修复了它(行为相同)。我同意这是未定义的行为,因为它写得很模糊;我真的想要理解实际发生的事情的具体细节,而不仅仅是标准所说的。我会在问题中澄清这一点。我想知道:如果它是来自 mallocvoid*,那么它是否仍然是未定义的行为?毕竟,free 在之后访问该内存是被允许的。 - jwd
显示剩余3条评论
1个回答

10
为了销毁一个派生类Derived,概念上调用Derived::~Derived(在此情况下不执行任何操作),然后调整虚表使对象成为Interface,接着调用Interface::~Interface。您所观察到的是指向Interface虚表的指针(如此处所示,构造一个Interface会打印相同的第一个字节)。
如果启用优化,则由于Interface::~Interface没有任何操作,Derived::~Derived也可以被优化成无操作,因此您会看到打印相同的第一个字节。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接