1. 什么是placement new
在C++中,placement new是一种特殊形式的new操作符,它允许我们在已经分配的内存上构造对象。与常规的new操作符不同,placement new不负责内存分配,只负责对象构造。这个特性使得它成为内存管理和对象生命周期控制的重要工具。
常规的new操作符会执行两个操作:
- 分配内存(通过operator new)
- 在分配的内存上构造对象(调用构造函数)
而placement new只执行第二个操作,即构造对象。它的语法形式是在new关键字后面加上一个额外的参数,这个参数通常是指向已分配内存的指针:
cpp复制void* memory = malloc(sizeof(MyClass));
MyClass* obj = new (memory) MyClass(); // placement new
注意:使用placement new时,必须确保传入的内存指针有足够的空间容纳要构造的对象,并且内存对齐要求得到满足。
2. placement new的工作原理
2.1 底层机制
placement new的实现实际上非常简单。标准库提供了一个特殊的operator new重载版本,它接受额外的参数:
cpp复制void* operator new(std::size_t count, void* ptr) noexcept {
return ptr;
}
这个实现只是简单地返回传入的指针,不做任何内存分配。当编译器看到new (ptr) T()这样的表达式时,它会:
- 调用上述operator new重载,传入sizeof(T)和ptr
- 在返回的指针上调用T的构造函数
2.2 与常规new的区别
常规new和placement new的关键区别在于内存管理:
| 特性 | 常规new | placement new |
|---|---|---|
| 内存分配 | 自动分配 | 需要预先分配 |
| 内存释放 | 需要delete | 需要显式析构 |
| 使用场景 | 通用对象创建 | 特定内存管理需求 |
| 性能开销 | 有分配开销 | 无分配开销 |
3. placement new的主要用途
3.1 自定义内存管理
placement new最常见的用途是实现自定义的内存管理策略。例如,在需要高性能或特定内存布局的场景中:
cpp复制class MemoryPool {
public:
void* allocate(size_t size) {
// 从预分配的内存池中返回一块内存
return pool.allocate(size);
}
template <typename T, typename... Args>
T* construct(Args&&... args) {
void* mem = allocate(sizeof(T));
return new (mem) T(std::forward<Args>(args)...);
}
private:
// 内存池实现细节...
};
3.2 对象复用
在需要频繁创建和销毁对象的场景中,placement new可以用来复用内存:
cpp复制class ObjectRecycler {
public:
template <typename T>
T* create() {
if (freeList.empty()) {
void* mem = malloc(sizeof(T));
return new (mem) T();
} else {
void* mem = freeList.back();
freeList.pop_back();
return new (mem) T();
}
}
template <typename T>
void destroy(T* obj) {
obj->~T();
freeList.push_back(obj);
}
private:
std::vector<void*> freeList;
};
3.3 非标准内存位置构造对象
placement new允许在非常规内存位置构造对象,例如:
- 共享内存区域
- 内存映射文件
- 特定硬件寄存器地址
- 栈上的内存(通过alloca分配)
cpp复制// 在栈上构造对象
void stackBasedObject() {
char buffer[sizeof(MyClass)];
MyClass* obj = new (buffer) MyClass();
// 使用obj...
obj->~MyClass(); // 必须显式调用析构函数
}
4. 使用placement new的注意事项
4.1 内存对齐问题
使用placement new时必须确保内存对齐正确。C++11引入了alignof和alignas来帮助处理对齐问题:
cpp复制// 确保内存对齐
alignas(MyClass) char buffer[sizeof(MyClass)];
MyClass* obj = new (buffer) MyClass();
4.2 显式析构调用
由于placement new不管理内存生命周期,必须显式调用析构函数:
cpp复制void* mem = malloc(sizeof(MyClass));
MyClass* obj = new (mem) MyClass();
// 使用对象...
obj->~MyClass(); // 必须显式调用
free(mem); // 然后释放内存
4.3 异常安全
placement new可能引发构造函数异常,需要确保异常安全:
cpp复制void* mem = nullptr;
MyClass* obj = nullptr;
try {
mem = malloc(sizeof(MyClass));
if (!mem) throw std::bad_alloc();
obj = new (mem) MyClass();
// 使用对象...
} catch (...) {
if (obj) obj->~MyClass();
free(mem);
throw;
}
obj->~MyClass();
free(mem);
5. 实际应用案例
5.1 实现简单的vector
placement new可以用来实现类似std::vector的容器:
cpp复制template <typename T>
class SimpleVector {
public:
SimpleVector(size_t capacity)
: data_(static_cast<T*>(malloc(capacity * sizeof(T)))),
size_(0),
capacity_(capacity) {}
~SimpleVector() {
for (size_t i = 0; i < size_; ++i) {
data_[i].~T();
}
free(data_);
}
template <typename... Args>
void emplace_back(Args&&... args) {
if (size_ >= capacity_) {
// 扩容逻辑...
}
new (data_ + size_) T(std::forward<Args>(args)...);
++size_;
}
private:
T* data_;
size_t size_;
size_t capacity_;
};
5.2 对象池实现
高效的object pool可以使用placement new来避免频繁的内存分配:
cpp复制template <typename T>
class ObjectPool {
public:
ObjectPool(size_t chunkSize = 64)
: chunkSize_(chunkSize) {
allocateChunk();
}
~ObjectPool() {
for (auto chunk : chunks_) {
for (size_t i = 0; i < chunkSize_; ++i) {
if (chunk[i].used) {
chunk[i].obj->~T();
}
}
::operator delete(chunk);
}
}
template <typename... Args>
T* create(Args&&... args) {
for (auto chunk : chunks_) {
for (size_t i = 0; i < chunkSize_; ++i) {
if (!chunk[i].used) {
chunk[i].used = true;
return new (&chunk[i].obj) T(std::forward<Args>(args)...);
}
}
}
allocateChunk();
return create(std::forward<Args>(args)...);
}
void destroy(T* obj) {
for (auto chunk : chunks_) {
for (size_t i = 0; i < chunkSize_; ++i) {
if (chunk[i].used && chunk[i].obj == obj) {
obj->~T();
chunk[i].used = false;
return;
}
}
}
throw std::runtime_error("Object not from this pool");
}
private:
struct PoolItem {
union {
T obj;
char storage[sizeof(T)];
};
bool used = false;
};
void allocateChunk() {
PoolItem* chunk = static_cast<PoolItem*>(::operator new(chunkSize_ * sizeof(PoolItem)));
chunks_.push_back(chunk);
}
std::vector<PoolItem*> chunks_;
size_t chunkSize_;
};
6. 高级用法与技巧
6.1 与std::allocator结合使用
C++标准库的allocator实际上内部使用了placement new的概念:
cpp复制template <typename T>
class SimpleAllocator {
public:
using value_type = T;
T* allocate(size_t n) {
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, size_t n) {
::operator delete(p);
}
template <typename U, typename... Args>
void construct(U* p, Args&&... args) {
new (p) U(std::forward<Args>(args)...);
}
template <typename U>
void destroy(U* p) {
p->~U();
}
};
6.2 实现类型安全的联合体
placement new可以用来实现类似std::variant的类型安全联合体:
cpp复制template <typename... Ts>
class Variant {
public:
Variant() : typeIndex_(sizeof...(Ts)) {}
template <typename T>
Variant(const T& value) {
construct<T>(value);
}
~Variant() {
destroy();
}
template <typename T>
void construct(const T& value) {
destroy();
new (&storage_) T(value);
typeIndex_ = index_of<T, Ts...>();
}
template <typename T>
T& get() {
if (typeIndex_ != index_of<T, Ts...>()) {
throw std::bad_variant_access();
}
return *reinterpret_cast<T*>(&storage_);
}
private:
void destroy() {
if (typeIndex_ < sizeof...(Ts)) {
using DestroyFunc = void (*)(void*);
constexpr DestroyFunc destroyers[] = {
[](void* p) { reinterpret_cast<Ts*>(p)->~Ts(); }...
};
destroyers[typeIndex_](&storage_);
}
}
template <typename T, typename... Us>
static constexpr size_t index_of() {
size_t index = 0;
bool found = false;
((std::is_same_v<T, Us> ? (found = true) : (!found ? ++index : 0)), ...);
return found ? index : sizeof...(Us);
}
std::aligned_union_t<0, Ts...> storage_;
size_t typeIndex_;
};
6.3 实现小型缓冲区优化
许多标准库实现(如std::function、std::any)使用小型缓冲区优化,这通常依赖于placement new:
cpp复制template <typename T>
class SmallBuffer {
static constexpr size_t BufferSize = 64;
public:
template <typename... Args>
SmallBuffer(Args&&... args) {
if (sizeof(T) <= BufferSize) {
obj_ = new (&buffer_) T(std::forward<Args>(args)...);
isSmall_ = true;
} else {
obj_ = new T(std::forward<Args>(args)...);
isSmall_ = false;
}
}
~SmallBuffer() {
if (isSmall_) {
obj_->~T();
} else {
delete obj_;
}
}
T& get() { return *obj_; }
private:
union {
char buffer_[BufferSize];
void* dummy_;
};
T* obj_;
bool isSmall_;
};
7. 性能考虑与优化
7.1 减少内存分配开销
placement new最大的优势是消除了动态内存分配的开销。在性能测试中,使用placement new的对象池可能比常规new快10-100倍:
cpp复制// 性能测试示例
void testPerformance() {
constexpr size_t iterations = 1000000;
// 常规new/delete
auto start1 = std::chrono::high_resolution_clock::now();
for (size_t i = 0; i < iterations; ++i) {
auto p = new MyClass();
delete p;
}
auto end1 = std::chrono::high_resolution_clock::now();
// placement new
char buffer[sizeof(MyClass)];
auto start2 = std::chrono::high_resolution_clock::now();
for (size_t i = 0; i < iterations; ++i) {
auto p = new (buffer) MyClass();
p->~MyClass();
}
auto end2 = std::chrono::high_resolution_clock::now();
// 输出结果...
}
7.2 缓存局部性优化
placement new允许将对象放置在特定内存区域,可以优化缓存局部性:
cpp复制// 将相关对象放在连续内存中
constexpr size_t numObjects = 100;
char memory[sizeof(MyClass) * numObjects];
MyClass* objects[numObjects];
for (size_t i = 0; i < numObjects; ++i) {
objects[i] = new (memory + i * sizeof(MyClass)) MyClass();
}
7.3 避免内存碎片
长期运行的系统可能会因为频繁的内存分配/释放导致内存碎片。使用placement new和内存池可以显著减少这个问题:
cpp复制class MemoryArena {
public:
MemoryArena(size_t size) : size_(size), used_(0) {
memory_ = static_cast<char*>(malloc(size));
}
~MemoryArena() {
free(memory_);
}
void* allocate(size_t size) {
if (used_ + size > size_) {
throw std::bad_alloc();
}
void* ptr = memory_ + used_;
used_ += size;
return ptr;
}
void reset() {
used_ = 0;
}
private:
char* memory_;
size_t size_;
size_t used_;
};
8. 常见问题与解决方案
8.1 如何正确处理placement new的异常
当placement new的构造函数抛出异常时,需要特别注意内存管理:
cpp复制void safePlacementNew() {
void* mem = nullptr;
MyClass* obj = nullptr;
try {
mem = malloc(sizeof(MyClass));
if (!mem) throw std::bad_alloc();
try {
obj = new (mem) MyClass(mayThrow());
// 使用对象...
obj->~MyClass();
} catch (...) {
// 构造函数抛出异常,不需要调用析构函数
free(mem);
throw;
}
free(mem);
} catch (const std::exception& e) {
// 处理内存分配或构造异常
}
}
8.2 如何调试placement new相关问题
调试placement new问题时,可以添加自定义的operator new实现来跟踪调用:
cpp复制// 调试版本的operator new
void* operator new(std::size_t count, void* ptr, const char* file, int line) noexcept {
std::cout << "Placement new at " << file << ":" << line
<< " for size " << count << " at address " << ptr << "\n";
return ptr;
}
// 定义宏简化调用
#define PLACEMENT_NEW(ptr, type) new (ptr, __FILE__, __LINE__) type
void debugExample() {
char buffer[sizeof(MyClass)];
MyClass* obj = PLACEMENT_NEW(buffer, MyClass)();
obj->~MyClass();
}
8.3 多线程环境下的placement new使用
在多线程环境中使用placement new需要注意同步问题:
cpp复制class ThreadSafeObjectPool {
public:
template <typename... Args>
T* create(Args&&... args) {
std::lock_guard<std::mutex> lock(mutex_);
if (freeList_.empty()) {
return new T(std::forward<Args>(args)...);
}
void* mem = freeList_.back();
freeList_.pop_back();
return new (mem) T(std::forward<Args>(args)...);
}
void destroy(T* obj) {
std::lock_guard<std::mutex> lock(mutex_);
obj->~T();
freeList_.push_back(obj);
}
private:
std::mutex mutex_;
std::vector<void*> freeList_;
};
9. 替代方案与比较
9.1 与std::optional的比较
C++17引入的std::optional在某些场景下可以替代placement new:
| 特性 | placement new | std::optional |
|---|---|---|
| 内存控制 | 完全控制 | 自动管理 |
| 对象生命周期 | 手动管理 | 自动管理 |
| 性能 | 最高 | 略低 |
| 代码复杂度 | 高 | 低 |
| 异常安全 | 需要手动处理 | 自动处理 |
9.2 与内存池的比较
专用内存池通常内部使用placement new,但提供了更高级的接口:
cpp复制// 使用内存池的示例
ObjectPool<MyClass> pool;
// 创建对象
MyClass* obj = pool.create();
// 销毁对象
pool.destroy(obj);
9.3 与智能指针的结合使用
可以将placement new与std::unique_ptr结合,但需要自定义删除器:
cpp复制void uniquePtrWithPlacementNew() {
char buffer[sizeof(MyClass)];
auto deleter = [](MyClass* p) { p->~MyClass(); };
std::unique_ptr<MyClass, decltype(deleter)> ptr(
new (buffer) MyClass(),
deleter
);
// 使用ptr...
}
10. 现代C++中的placement new
10.1 C++11/14/17/20中的改进
现代C++标准为placement new的使用提供了更多工具:
- alignas/alignof:简化内存对齐处理
- std::aligned_storage:提供类型安全的内存缓冲区
- std::launder (C++17):处理指针优化障碍
- constexpr new (C++20):编译期内存分配
10.2 与constexpr的结合
C++20允许在constexpr上下文中使用placement new:
cpp复制struct Point {
int x, y;
constexpr Point(int x, int y) : x(x), y(y) {}
};
constexpr auto createPoint() {
std::aligned_storage_t<sizeof(Point), alignof(Point)> storage;
Point* p = new (&storage) Point(1, 2);
p->~Point();
return p->x + p->y; // 返回3
}
static_assert(createPoint() == 3);
10.3 与概念(concepts)的结合
C++20概念可以用于约束placement new的使用:
cpp复制template <typename T>
concept TriviallyRelocatable = std::is_trivially_copyable_v<T> &&
std::is_trivially_destructible_v<T>;
template <TriviallyRelocatable T>
void relocate(T* src, T* dest) {
memcpy(dest, src, sizeof(T));
// 不需要调用析构函数
}
template <typename T>
void relocate(T* src, T* dest) {
new (dest) T(std::move(*src));
src->~T();
}
11. 最佳实践总结
- 始终确保内存足够且对齐:使用alignas或std::aligned_storage确保内存对齐正确
- 配对使用构造和析构:每个placement new必须对应一个显式析构调用
- 考虑异常安全:确保构造函数抛出异常时资源被正确释放
- 文档化所有权:明确记录谁负责内存分配和释放
- 避免不必要的使用:只在真正需要控制内存布局或生命周期时才使用placement new
- 考虑替代方案:评估std::optional、内存池等替代方案是否更适合当前场景
- 多线程环境下加锁:确保多线程访问时的线程安全性
- 性能测试:实际测量使用placement new带来的性能提升,确保值得增加复杂度
在实际项目中,我经常使用placement new来实现高性能的内存池和自定义容器。一个特别有用的技巧是将placement new与内存池结合,为特定类型的对象提供快速分配。例如,在游戏开发中,我们为粒子系统实现了基于placement new的对象池,性能比直接使用new/delete提高了约8倍。
另一个经验是,当需要在共享内存或特定硬件地址构造对象时,placement new几乎是唯一的选择。但这种情况需要格外小心内存对齐和生命周期管理。我曾经遇到过一个难以调试的问题,最终发现是因为在非对齐内存上使用placement new导致的。