如何在单个内存块中分配两个不同类型的对象?
如何在单个内存块中分配两个不同类型的对象?
嘿,你这个需求其实挺常见的——就像std::make_shared把控制块和托管对象塞在同一块内存里那样,用一次分配搞定两个不同类型的对象完全可行。核心思路就是:先申请一块足够大的连续内存,然后用**放置new(placement new)**在内存的不同位置分别构造两个对象,同时必须严格处理内存对齐的问题,不然很容易踩坑。
我给你拆解成具体的步骤,再附上实用的代码示例:
核心原理
普通的new会同时完成“分配内存+构造对象”两个操作,但放置new只负责在你指定的已分配内存地址上构造对象,不会额外分配内存。这就给了我们自由:先一次性申请能装下两个对象的内存块,再手动指定位置构造不同类型的对象。
但有个绝对不能忽略的点:不同类型的内存对齐要求可能不一样(比如int可能是4字节对齐,std::complex<double>是8字节对齐)。如果直接把第二个对象硬塞在第一个对象后面,很可能会导致未对齐访问,这在很多架构上是直接触发硬件错误的,就算x86能兼容,也是标准里的未定义行为,绝对不能这么干。
手动实现的基础版本
比如我们要在同一块内存里放一个int和一个std::complex<double>,代码可以这么写:
#include <iostream> #include <complex> #include <cstdlib> #include <memory> // 用于std::align int main() { // 1. 提前计算两个对象的大小和对齐要求 const size_t obj1_size = sizeof(int); const size_t obj1_align = alignof(int); const size_t obj2_size = sizeof(std::complex<double>); const size_t obj2_align = alignof(std::complex<double>); // 2. 分配一块足够大的内存:留足两个对象的大小,再加上最大对齐值的冗余空间,避免对齐调整后空间不够 const size_t total_size = obj1_size + obj2_size + std::max(obj1_align, obj2_align); void* raw_mem = malloc(total_size); if (!raw_mem) { std::cerr << "内存分配失败啦!" << std::endl; return 1; } // 3. 调整指针,为第一个对象找到符合对齐要求的起始位置 void* obj1_ptr = raw_mem; size_t remaining_space = total_size; std::align(obj1_align, obj1_size, obj1_ptr, remaining_space); // 4. 为第二个对象计算符合对齐的起始位置 size_t used_by_obj1 = static_cast<char*>(obj1_ptr) - static_cast<char*>(raw_mem) + obj1_size; void* obj2_ptr = static_cast<char*>(raw_mem) + used_by_obj1; remaining_space = total_size - used_by_obj1; std::align(obj2_align, obj2_size, obj2_ptr, remaining_space); // 5. 用放置new构造两个不同类型的对象 int* my_int = new(obj1_ptr) int(42); // 构造int对象,值为42 std::complex<double>* my_complex = new(obj2_ptr) std::complex<double>(3.14, 2.71); // 构造复数对象 // 现在可以正常使用这两个对象了 std::cout << "第一个对象(int)的值:" << *my_int << std::endl; std::cout << "第二个对象(复数)的值:" << *my_complex << std::endl; // 6. 必须手动调用析构函数!放置new不会自动帮你析构 my_complex->~complex<double>(); my_int->~int(); // 7. 最后释放一开始申请的整块原始内存 free(raw_mem); return 0; }
更安全的RAII封装版本
上面的基础版本需要手动管理析构和内存释放,很容易忘,我们可以用RAII(资源获取即初始化)的方式封装一下,让它更安全,用起来也更省心:
#include <iostream> #include <complex> #include <cstdlib> #include <memory> #include <utility> // 用于std::forward // 封装两个对象和原始内存的结构体,自动处理析构和释放 template <typename T1, typename T2> struct DualObject { void* raw_memory; T1* obj1; T2* obj2; // 自动析构:先析构对象,再释放内存 ~DualObject() { if (obj2) obj2->~T2(); if (obj1) obj1->~T1(); if (raw_memory) free(raw_memory); } // 禁止拷贝,允许移动(避免double free) DualObject(const DualObject&) = delete; DualObject& operator=(const DualObject&) = delete; DualObject(DualObject&& other) noexcept : raw_memory(other.raw_memory), obj1(other.obj1), obj2(other.obj2) { other.raw_memory = nullptr; other.obj1 = nullptr; other.obj2 = nullptr; } DualObject& operator=(DualObject&& other) noexcept { if (this != &other) { // 先清理当前资源 if (obj2) obj2->~T2(); if (obj1) obj1->~T1(); if (raw_memory) free(raw_memory); // 接管对方的资源 raw_memory = other.raw_memory; obj1 = other.obj1; obj2 = other.obj2; // 清空对方的指针,避免重复释放 other.raw_memory = nullptr; other.obj1 = nullptr; other.obj2 = nullptr; } return *this; } }; // 构造两个对象的工厂函数 template <typename T1, typename T2, typename... Args1, typename... Args2> DualObject<T1, T2> make_dual_object(Args1&&... args1, Args2&&... args2) { const size_t size1 = sizeof(T1); const size_t align1 = alignof(T1); const size_t size2 = sizeof(T2); const size_t align2 = alignof(T2); // 分配足够大的内存,预留对齐冗余 const size_t total_size = size1 + size2 + std::max(align1, align2); void* raw_mem = malloc(total_size); if (!raw_mem) { throw std::bad_alloc(); } // 调整第一个对象的指针到符合对齐的位置 void* ptr1 = raw_mem; size_t remaining = total_size; std::align(align1, size1, ptr1, remaining); // 调整第二个对象的指针到符合对齐的位置 size_t used = static_cast<char*>(ptr1) - static_cast<char*>(raw_mem) + size1; void* ptr2 = static_cast<char*>(raw_mem) + used; remaining = total_size - used; std::align(align2, size2, ptr2, remaining); // 用完美转发构造对象,支持任意构造参数 T1* t1 = new(ptr1) T1(std::forward<Args1>(args1)...); T2* t2 = new(ptr2) T2(std::forward<Args2>(args2)...); return {raw_mem, t1, t2}; } // 使用示例 int main() { try { // 直接创建两个对象,自动管理资源 auto dual = make_dual_object<int, std::complex<double>>(42, std::complex<double>(3.14, 2.71)); std::cout << "int对象的值:" << *dual.obj1 << std::endl; std::cout << "复数对象的值:" << *dual.obj2 << std::endl; // 离开作用域时,DualObject的析构函数会自动处理所有清理工作 } catch (const std::bad_alloc& e) { std::cerr << "内存分配失败:" << e.what() << std::endl; return 1; } return 0; }
几个关键注意事项
- 绝对不能用
delete去释放obj1或obj2!普通delete会先析构对象再释放内存,但这里的对象是在我们手动分配的原始内存上构造的,delete会错误地尝试释放这块内存,导致重复释放或者内存泄漏。 - 对齐问题是硬要求!一定要用
std::align来调整指针,或者用std::aligned_alloc来分配已经对齐的内存,不要自己瞎算偏移。 - 如果用
operator new代替malloc分配内存,释放的时候要对应用operator delete,不要混用。
内容来源于stack exchange
相关产品推荐
相关产品推荐

