C++ placement new在内存池管理中的定位构造
在编写对性能要求极高的服务器程序或游戏引擎时,频繁的内存申请与释放会导致内存碎片化并增加 CPU 开销。为了解决这个问题,我们通常使用“内存池”技术预先申请一大块内存,后续直接从这块内存中分配。C++ 提供的 placement new 语法,正是实现这一过程的核心工具,它允许我们在已有的内存地址上直接构造对象,而无需重新分配。
本文将手把手教你如何利用 placement new 在内存池中实现对象的定位构造与销毁。
1. 理解 Placement New 的核心机制
普通的 new 操作符做了两件事:向操作系统申请内存,然后在该内存上调用对象的构造函数。而 placement new 将这两件事解耦,它跳过申请内存的步骤,直接在指定的内存地址上调用构造函数。
理解以下区别是进行内存池开发的前提:
| 特性 | 普通 new | Placement new |
|---|---|---|
| 内存分配 | 自动从堆分配 | 使用已分配的内存地址 |
| 构造函数 | 调用 | 调用 |
| 使用场景 | 通用对象创建 | 内存池、嵌入式系统、共享内存 |
| 语法格式 | new ClassName |
new (address) ClassName |
2. 准备工作:引入必要的头文件
使用 placement new 不需要安装任何第三方库,但必须包含特定的 C++ 标准头文件。
打开你的 C++ 源代码文件。
添加以下代码到文件顶部:
#include <iostream>
#include <new> // 必须包含,用于 placement new
#include <cstdlib> // 用于 malloc 和 free
3. 设计内存池的基本结构
为了演示,我们将设计一个最简化的内存块结构。这个结构包含一个数据区,用于存放实际对象。
定义一个简单的类作为我们要管理的对象:
class MyObject {
public:
int id;
double value;
MyObject(int i, double v) : id(i), value(v) {
std::cout << "对象构造 ID: " << id << std::endl;
}
void show() {
std::cout << "对象 ID: " << id << ", 值: " << value << std::endl;
}
~MyObject() {
std::cout << "对象析构 ID: " << id << std::endl;
}
};
4. 执行定位构造:使用 Placement New
这是最关键的一步。假设你已经通过 malloc 或者其他方式从大内存块中切分出了一块闲置内存,现在要把这块内存变成一个 MyObject 对象。
声明一个指向 void 的指针,模拟从内存池获取的原始内存地址:
void* rawMemory = std::malloc(sizeof(MyObject));
std::cout << "获取原始内存地址: " << rawMemory << std::endl;
调用 placement new 语法在 rawMemory 上构造对象:
// 语法:new (指针地址) 类名(构造参数)
MyObject* obj = new (rawMemory) MyObject(101, 3.14);
注意:这里 obj 指针的值通常等于 rawMemory,但类型不同。rawMemory 只是一堆字节,而 obj 是一个合法的对象指针。
调用对象的方法验证构造是否成功:
obj->show();
此时,控制台将输出构造信息,证明对象已经在指定的内存地址上成功创建。
5. 流程图解:内存状态转换
为了更直观地理解内存中发生的变化,请参考下方的状态流转过程。内存从“原始数据”状态,经过 placement new 处理,变成了“活跃对象”,最后经过手动析构又变回了“原始数据”。
6. 执行销毁:显式调用析构函数
使用 placement new 创建的对象有一个极其重要的规则:严禁使用 delete 关键字释放内存。
因为内存本身不是由 new 分配的(可能是 malloc 来的,或者属于内存池的一小部分),delete 会尝试把内存归还给系统堆,导致程序崩溃。我们只需要销毁对象(即运行析构函数代码),保持内存原样不动即可。
执行以下代码来销毁对象但保留内存:
// 语法:指针->~类名()
obj->~MyObject();
此时,obj 指向的内存中,MyObject 的部分已经被销毁,但这块内存地址依然存在,可以被后续再次利用。
最后,当你彻底不再需要这块原始内存时(例如要关闭整个内存池),才使用对应的方式释放原始内存:
std::free(rawMemory);
7. 封装一个简易内存池模板
为了实际工程应用,我们将上述步骤封装成一个简单的工具类。你可以直接复制以下代码并在本地编译运行。
编写完整的代码示例:
#include <iostream>
#include <new>
#include <cstdlib>
class MyObject {
public:
int id;
MyObject(int i) : id(i) { std::cout << "构造: " << id << std::endl; }
void work() { std::cout << "工作: " << id << std::endl; }
~MyObject() { std::cout << "析构: " << id << std::endl; }
};
// 简易内存池节点
template <typename T>
class ObjectPool {
private:
void* memoryChunk;
size_t poolSize;
public:
ObjectPool(size_t count) {
// 1. 预先分配一大块内存
poolSize = count;
memoryChunk = std::malloc(sizeof(T) * count);
std::cout << "内存池初始化完成,分配大小: " << sizeof(T) * count << " 字节" << std::endl;
}
~ObjectPool() {
// 4. 最终释放整个内存池
std::free(memoryChunk);
std::cout << "内存池释放" << std::endl;
}
// 在指定索引的位置构造对象
T* createAtIndex(size_t index, int id) {
if (index >= poolSize) return nullptr;
// 计算目标地址
void* targetAddr = static_cast<char*>(memoryChunk) + index * sizeof(T);
// 2. 使用 placement new 定位构造
return new (targetAddr) T(id);
}
// 销毁指定索引的对象
void destroyAtIndex(size_t index, T* objPtr) {
if (index >= poolSize || objPtr == nullptr) return;
// 3. 显式调用析构函数
objPtr->~T();
std::cout << "位置 " << index << " 的对象已销毁,内存归还池中" << std::endl;
}
};
int main() {
// 创建一个能容纳 3 个对象的内存池
ObjectPool<MyObject> pool(3);
// 在位置 0 创建对象
MyObject* obj1 = pool.createAtIndex(0, 1);
if (obj1) obj1->work();
// 在位置 1 创建对象
MyObject* obj2 = pool.createAtIndex(1, 2);
if (obj2) obj2->work();
std::cout << "--- 开始销毁对象 ---" << std::endl;
// 销毁对象,但不释放内存池
pool.destroyAtIndex(0, obj1);
pool.destroyAtIndex(1, obj2);
// 这里可以再次利用 createAtIndex 在相同的内存位置创建新对象
return 0;
// 程序结束时,pool 析构函数调用 std::free 释放整块内存
}
编译并运行上述代码,观察控制台输出的“构造”与“析构”顺序。
8. 关键操作总结
在处理内存池与定位构造时,请严格遵循以下操作顺序,以免造成内存泄漏或程序崩溃:
- 申请原始内存(
malloc、::operator new或从预分配池中获取指针)。 - 使用
new (address) Class(...)在内存上构造对象。 - 使用对象指针进行业务逻辑操作。
- 调用
ptr->~Class()显式销毁对象(仅清理数据,不释放内存)。 - 归还指针给内存池或保持不动。
- 在系统生命周期结束时,调用
free或delete[]释放整块原始内存。

暂无评论,快来抢沙发吧!