如果类有析构函数,堆数组分配 4 个额外字节

Posted

技术标签:

【中文标题】如果类有析构函数,堆数组分配 4 个额外字节【英文标题】:Heap array allocates 4 extra bytes if class has destructor 【发布时间】:2021-12-20 22:17:03 【问题描述】:

我是 C++ 新手,最近一直在玩内存分配。我发现当你声明一个带有析构函数的类时,像这样:

class B

public:
    ~B()  
;

然后像这样创建一个堆数组:

B* arr = new B[8];

分配器分配 12 个字节,但当我删除析构函数时,它只分配 8 个字节。这就是我衡量分配的方式:

size_t allocated = 0;

void* operator new(size_t size)

    allocated += size;
    return malloc(size);

void deallocate(B* array, size_t size)

    delete[] array;
    allocated -= size * sizeof(B);

当然,我必须手动调用deallocate,而自动调用new 运算符。

我在使用std::string* 时发现了这个问题,我意识到释放器在int* 上工作得很好,但在前者上却不行。

有谁知道为什么会发生这种情况,更重要的是:如何在运行时以编程方式检测这些?

提前致谢。

【问题讨论】:

“如何在运行时以编程方式检测这些?” - 你为什么要这样做? @Dai 我只是想看看 C++ 是如何工作的。为什么不应该这样使用malloc 我的评论不是关于特别使用malloc - 但如果你把它作为一种心理锻炼,那很好;我只是担心你可能会在生产代码中这样做。我删了。 "我想知道 malloc 出了什么问题" -- malloc 可以在 new 实现中使用,但它需要保持一致。你不能 delete[] 分配了malloc 的内存——它们是不同的函数,它是正式的未定义行为。见In what cases do I use malloc and/or new? 和Behaviour of malloc with delete in C++ 这是一个猜测,所以我不想发布答案,但是 - 添加一个非默认析构函数会使该类不易被破坏。释放一个可平凡破坏的对象数组只需要调用free(),但对于非平凡可破坏的对象,您还必须调用析构函数。额外的 8 个字节将作为编译器的簿记数据,它可能在那里存储元素数量,以便在释放内存之前可以运行正确数量的析构函数。 【参考方案1】:

您正在查看编译器如何处理new[]delete[] 的实现细节,因此对于分配的额外空间没有明确的答案,因为答案将特定于实现——尽管我可以在下面提供一个可能的原因。

由于这是实现定义的,您无法在运行时可靠地检测到这一点。更重要的是,不应该有任何真正的理由这样做。尤其是如果您是 C++ 新手,了解这个事实更有趣/深奥,但在运行时检测到这一点应该没有真正的好处。

还需要注意的是,这只发生在数组分配中,而不是对象分配。例如,以下将打印预期的数字:

struct A 
    ~A()
;
struct B 
;

auto operator new(std::size_t n) -> void* 
    std::cout << "Allocated: " << n << std::endl;
    return std::malloc(n);

auto operator delete(void* p, std::size_t n) -> void 
    std::free(p);


auto main() -> int 
    auto* a = new A;
    delete a;

    auto* b = new B;
    delete b;

输出:

Allocated: 1
Allocated: 1

Live Example

额外的存储空间只分配给具有非平凡析构函数的类型:

    auto* a = new A[10];
    delete[] a;

    auto* b = new B[10];
    delete[] b;

输出:

Allocated: 18
Allocated: 10

Live Example


发生这种情况的最可能原因为什么是单个size_t 的额外簿记被保留在包含非平凡析构函数的已分配数组的开头。这样做是为了在调用delete 时,语言可以知道有多少对象需要调用它们的析构函数。对于重要的析构函数,它能够依赖于其释放函数的底层 delete 机制。

这一假设也得到了 GNU ABI 的额外存储为sizeof(size_t) 字节这一事实的支持。为x86_64 构建产生18 分配A[10]8 字节为size_t)。 Building for x86 yields 14 用于相同的分配(4 字节用于size_t)。


编辑

我不建议在实践中这样做,但您实际上可以从数组中查看这些额外数据。从 new[] 分配的指针在返回给调用者 (which you can test by printing the address from the new[] operator) 之前得到调整。

如果您将该数据读入std::size_t,您可以看到该数据(至少对于 GNU ABI 而言)包含分配对象数量的准确计数。

同样,我不建议在实践中这样做,因为这会利用实现定义的行为。但只是为了好玩:

    auto* a = new A[10];
    const auto* bytes = reinterpret_cast<const std::byte*>(a);
    std::size_t count;
    std::memcpy(&count, bytes - sizeof(std::size_t), sizeof(std::size_t));

    std::cout << "Count " << count << std::endl;

    delete[] a;

输出:

Count 10

Live Example

【讨论】:

天哪,我的测试对于非平凡的析构函数额外的簿记是积极的。谢谢你放纵我。 @hisdudeness 很高兴您的回答!如果您有兴趣,这是利用实现,但您实际上可以查看额外的簿记。查看我的编辑! 哦,太好了!内存分配比我想象的更奇怪。

以上是关于如果类有析构函数,堆数组分配 4 个额外字节的主要内容,如果未能解决你的问题,请参考以下文章

kotlin-native 有析构函数吗?

一个完整的C++类应该包含什么?

Swift中 Class和Struct的区别

C# 的析构

C#-C# Dispose模式详细分析

用C++定义一个人员类