如果类有析构函数,堆数组分配 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 个额外字节的主要内容,如果未能解决你的问题,请参考以下文章