C/C++ · 2025年12月20日 0

C++ Placement New 与普通 New 的区别与应用指南

在 C++ 内存管理中,new 运算符有两种重要形式:普通 new 和 placement new。理解它们的区别对于编写高效、内存安全的代码至关重要。

📋 快速对比表

特性Placement New普通 New
内存来源调用者提供堆分配
语法new (地址) 类型(参数)new 类型(参数)
内存管理调用者管理new/delete 管理
性能无分配开销有分配开销
使用场景内存池、容器、性能优化常规动态分配

🚀 基本语法对比

普通 New(堆分配)

cpp

// 分配内存并在堆上构造对象
MyClass* obj = new MyClass(arg1, arg2);
// 使用后必须 delete
delete obj;

Placement New(指定位置构造)

cpp

#include <new>  // 需要包含此头文件

// 预先分配内存
char buffer[sizeof(MyClass)];

// 在指定内存上构造对象
MyClass* obj = new (buffer) MyClass(arg1, arg2);

// 必须显式调用析构函数
obj->~MyClass();
// 注意:不要 delete obj! buffer 会自动释放

🔍 核心区别详解

1. 内存管理责任

普通 Newnew 负责内存分配和对象构造,delete 负责析构和内存释放。

cpp

// 编译器生成类似这样的代码:
void* ptr = operator new(sizeof(MyClass));  // 1. 分配内存
MyClass::MyClass(ptr, args...);             // 2. 构造对象

Placement New:只负责对象构造,内存由调用者管理。

cpp

// 编译器生成类似这样的代码:
void* ptr = operator new(sizeof(MyClass), (void*)address);  // 不分配内存
MyClass::MyClass(ptr, args...);                            // 构造对象

2. 错误处理方式

cpp

// 普通 New - 可能抛出 std::bad_alloc
try {
    MyClass* obj = new MyClass();
} catch (const std::bad_alloc& e) {
    // 处理内存不足
}

// Placement New - 不分配内存,不会抛出 bad_alloc
// 但如果构造失败,可能抛出其他异常

💡 主要应用场景

应用1:自定义内存池

cpp

class MemoryPool {
private:
    char pool[1024 * 1024];  // 1MB 内存池
    size_t offset = 0;
    
public:
    template<typename T, typename... Args>
    T* allocate(Args&&... args) {
        if (offset + sizeof(T) > sizeof(pool)) {
            return nullptr;  // 池已满
        }
        
        void* ptr = pool + offset;
        T* obj = new (ptr) T(std::forward<Args>(args)...);
        offset += sizeof(T);
        return obj;
    }
};

// 使用
MemoryPool pool;
MyClass* obj = pool.allocate<MyClass>(arg1, arg2);
obj->~MyClass();  // 必须显式析构

应用2:容器类的实现(如 std::vector、UE 的 TArray)

cpp

template<typename T>
class SimpleVector {
private:
    T* data = nullptr;
    size_t size = 0;
    size_t capacity = 0;
    
public:
    void push_back(const T& value) {
        if (size >= capacity) {
            reserve(capacity == 0 ? 4 : capacity * 2);
        }
        
        // 使用 placement new 在已分配内存上构造
        new (data + size) T(value);
        ++size;
    }
    
    ~SimpleVector() {
        // 必须显式调用每个元素的析构函数
        for (size_t i = 0; i < size; ++i) {
            data[i].~T();
        }
        delete[] reinterpret_cast<char*>(data);
    }
};

应用3:性能敏感场景

cpp

// 避免频繁内存分配的开销
void processObjects() {
    // 一次性分配多个对象所需内存
    constexpr size_t count = 1000;
    char* buffer = new char[count * sizeof(MyClass)];
    
    for (size_t i = 0; i < count; ++i) {
        // 使用 placement new 构造
        MyClass* obj = new (buffer + i * sizeof(MyClass)) MyClass();
        // 使用对象...
        obj->~MyClass();  // 显式析构
    }
    
    delete[] buffer;  // 一次性释放所有内存
}

应用4:对齐内存构造

cpp

#include <cstddef>

struct alignas(64) CacheLineAligned {
    int data[16];
};

void demo() {
    // 确保内存对齐
    alignas(CacheLineAligned) char buffer[sizeof(CacheLineAligned)];
    
    // 在对齐的内存上构造对象
    CacheLineAligned* obj = new (buffer) CacheLineAligned();
    obj->~CacheLineAligned();
}

⚠️ 注意事项和最佳实践

1. 正确配对使用

cpp

// ✅ 正确做法
void* memory = malloc(sizeof(MyClass));
MyClass* obj = new (memory) MyClass();
obj->~MyClass();  // 必须调用!
free(memory);     // 释放原始内存

// ❌ 错误做法
void* memory = malloc(sizeof(MyClass));
MyClass* obj = new (memory) MyClass();
delete obj;       // 错误!会双重释放内存

2. 异常安全

cpp

template<typename T, typename... Args>
T* construct_at(void* location, Args&&... args) {
    try {
        return new (location) T(std::forward<Args>(args)...);
    } catch (...) {
        // 构造失败时,location 保持原样
        return nullptr;
    }
}

3. C++17 后的新特性

cpp

#include <memory>

// C++17 提供了更安全的方式
void* memory = std::aligned_alloc(alignof(MyClass), sizeof(MyClass));

// 使用 std::construct_at(内部使用 placement new)
MyClass* obj = std::construct_at(reinterpret_cast<MyClass*>(memory), args...);

// 使用 std::destroy_at(内部调用析构函数)
std::destroy_at(obj);
std::free(memory);

🎯 实际案例:Unreal Engine 的 TArray

在 UE 引擎中,TArray 使用 placement new 来实现类型安全的数组构造:

cpp

// UE 源码中的简化示例
template<typename ElementType>
class TArray {
    void Add(const ElementType& Item) {
        if (需要扩容) {
            reserve(newCapacity);
        }
        
        // 在数组末尾使用 placement new 构造新元素
        new (Data + Num) ElementType(Item);
        ++Num;
    }
    
    ~TArray() {
        // 必须显式析构所有元素
        for (int32 i = 0; i < Num; ++i) {
            Data[i].~ElementType();
        }
        FreeMemory(Data);
    }
};

📝 总结要点

  1. Placement new 不分配内存,只在已有内存上构造对象
  2. 必须显式调用析构函数,但不要使用 delete
  3. 内存生命周期由调用者管理,placement new 只负责构造
  4. 适用于性能敏感、自定义内存管理的场景
  5. C++17 提供了更安全的替代品std::construct_at 和 std::destroy_at

🔧 选择建议

  • 使用 普通 new 当:需要简单的动态分配,不想管理内存细节
  • 使用 placement new 当:
    • 实现自定义容器或内存池
    • 需要精确控制对象生命周期
    • 性能至关重要,需要避免分配开销
    • 需要在特定对齐的内存上构造对象

理解这两种 new 的区别,能帮助你编写更高效、更可控的 C++ 代码,特别是在游戏开发、嵌入式系统等对性能有严格要求的领域。