如何分配内存对齐的C++对象数组?operator new[]异常解析
operator new[]搭配delete[]时的内存对齐坑点解析
先看你给出的测试代码和运行结果:
#include <stdlib.h> #include <stdio.h> class V4 { public: float v[ 4 ]; V4() {} void *operator new( size_t sz ) { return aligned_alloc( 16, sz ); } void *operator new[]( size_t sz ) { printf( "sz: %zu\n", sz ); return aligned_alloc( 16, sz ); } void operator delete( void *p, size_t sz ) { free( p ); } //void operator delete[]( void *p, size_t sz ) { free( p ); } }; class W4 { public: float w[ 4 ]; W4() {} void *operator new( size_t sz ) { return aligned_alloc( 16, sz ); } void operator new[]( size_t sz ) { printf( "sz: %zu\n", sz ); return aligned_alloc( 16, sz ); } void operator delete( void *p, size_t sz ) { free( p ); } void operator delete[]( void *p, size_t sz ) { free( p ); } }; int main( int argc, char **argv ) { printf( "sizeof( V4 ): %zu\n", sizeof( V4 )); V4 *p = new V4[ 1 ]; printf( "p: %p\n", p ); printf( "sizeof( W4 ): %zu\n", sizeof( W4 )); W4 *q = new W4[ 1 ]; printf( "q: %p\n", q ); exit(0); }
运行输出:
$ g++ -Wall main.cpp && ./a.out sizeof( V4 ): 16 sz: 16 p: 0x55be98a10030 sizeof( W4 ): 16 sz: 24 q: 0x55be98a10058
你疑惑的点很关键:为啥定义了operator delete[]后,分配大小就从16变成24了?而且这还破坏了内存对齐。咱们一步步拆解:
为啥定义delete[]会额外多8字节?
这是GCC编译器的特定行为:当你为类定义了**带size_t参数的operator delete[]**时,编译器会默认认为你需要在释放时知道这块内存的准确大小,或者需要知道数组里的对象总数。为了实现这个,它会在分配的内存块最前面额外添加8字节(64位系统下的标准大小),用来存储数组的元素个数。
而如果没定义这个带参数的operator delete[],编译器会使用默认的释放逻辑,它不需要这个额外的头部信息(或者用其他隐式方式处理),所以分配的大小就是1 * sizeof(W4) = 16,正好符合你的对齐要求。
正确的自定义对齐数组分配流程
要搞定这个问题,你得把编译器的额外头部空间也纳入对齐考虑,正确的步骤应该是这样:
- 计算总内存需求:编译器需要的头部大小(64位下8字节) + 数组对象的总大小 + 必要的对齐补全字节(确保第一个对象的起始地址满足你的对齐要求,比如16字节)
- 分配足够大的对齐内存块:用
aligned_alloc分配一块满足最大对齐要求的内存,这块内存要能装下头部、对象数组和对齐补全的部分 - 调整指针位置:从原始分配的地址偏移头部大小+对齐补全数,让返回的指针指向第一个对象的起始位置;同时要把原始分配的地址记下来,方便delete[]时释放
- 释放时反向计算:根据返回的对象指针,反向算出原始分配的内存地址,再调用
free释放
对你现有尝试的改进建议
你之前写的代码里用固定的8字节偏移确实不够优雅,而且换个编译器或者32位系统就会出错。咱们可以改进一下,把头部空间明确处理,同时确保对齐:
#include <stdlib.h> #include <stdio.h> #include <stdint.h> class W16 { public: float w[16]; W16() {} void* operator new(size_t sz) { return aligned_alloc(16, sz); } void* operator new[](size_t sz) { // 64位系统下,编译器需要的头部大小固定为8字节,用来存数组元素数 const size_t header_size = 8; // 总内存 = 头部 + 对象数组大小 size_t total_size = header_size + sz; // 确保总内存是16字节的倍数,满足对齐要求 if (total_size % 16 != 0) { total_size += 16 - (total_size % 16); } // 分配对齐的内存块 void* raw_ptr = aligned_alloc(16, total_size); if (!raw_ptr) return nullptr; // 把原始指针存在头部区域,方便delete时找回 *reinterpret_cast<uintptr_t*>(raw_ptr) = reinterpret_cast<uintptr_t>(raw_ptr); // 计算对象数组的起始地址:头部之后正好是16字节对齐(8+8=16) void* obj_ptr = reinterpret_cast<uint8_t*>(raw_ptr) + header_size; printf("sizeof(W16): %zx, sz: %zx, total_size: %zx\nraw_ptr: %p, obj_ptr: %p\n", sizeof(W16), sz, total_size, raw_ptr, obj_ptr); return obj_ptr; } void operator delete(void* p, size_t sz) { free(p); } void operator delete[](void* p, size_t sz) { // 反向找到头部地址:对象指针减去头部大小 void* raw_ptr = reinterpret_cast<uint8_t*>(p) - header_size; // 取出存在头部的原始指针(其实这里直接用raw_ptr也可以,因为是连续分配的) uintptr_t original_ptr = *reinterpret_cast<uintptr_t*>(raw_ptr); printf("obj_ptr: %p, raw_ptr: %p, original_ptr: %p\n", p, raw_ptr, reinterpret_cast<void*>(original_ptr)); free(reinterpret_cast<void*>(original_ptr)); } }; int main(int argc, char** argv) { printf("sizeof(W16): %zx\n", sizeof(W16)); W16* q = new W16[16]; printf("&q[0]: %p\n", &q[0]); delete[] q; return 0; }
这个版本的好处:
- 明确处理了编译器的头部需求,不会因为环境变化出错
- 保证了对象数组的起始地址严格满足16字节对齐
- delete[]时可以准确找回原始分配的内存,释放逻辑更可靠
内容的提问来源于stack exchange,提问作者PatB
相关产品推荐
相关产品推荐

