目录
[二、placement new 的基本用法](#二、placement new 的基本用法)
[三、placement new 的核心要点](#三、placement new 的核心要点)
[1. 不分配内存](#1. 不分配内存)
[2. 必须手动调用析构函数](#2. 必须手动调用析构函数)
[3. 避免重复构造](#3. 避免重复构造)
[五、placement new 与对齐](#五、placement new 与对齐)
[六、placement new 数组](#六、placement new 数组)
[1. 忘记包含 头文件](#1. 忘记包含 头文件)
[2. 忘记手动调用析构函数](#2. 忘记手动调用析构函数)
[3. 重复构造(不析构就再次 placement new)](#3. 重复构造(不析构就再次 placement new))
[4. 对 nullptr 或无效地址使用 placement new](#4. 对 nullptr 或无效地址使用 placement new)
[八、placement new 的应用场景总结](#八、placement new 的应用场景总结)
一、一个现实需求
假设你实现了一个内存池:预先分配一大块内存,然后重复使用它来创建和销毁对象。
cpp
// 预分配 1MB 内存池
char* pool = new char[1024 * 1024];
// 想要在这个 pool 的某个位置构造一个 MyClass 对象
// 不能用普通的 new,因为普通 new 会再次分配内存
解决方案:placement new------告诉编译器:"这块内存我已经准备好了,你只需要在上面构造对象。"
二、placement new 的基本用法
语法
cpp
#include <new> // 必须包含这个头文件
// 在指定地址上构造对象
T* ptr = new (address) T(constructor_args...);
最简单的例子
cpp
#include <iostream>
#include <new> // placement new 需要这个头文件
using namespace std;
class Point {
public:
int x, y;
Point(int a, int b) : x(a), y(b) {
cout << "Point 构造: (" << x << "," << y << ")" << endl;
}
~Point() {
cout << "Point 析构: (" << x << "," << y << ")" << endl;
}
};
int main() {
// 1. 分配原始内存(不构造对象)
void* mem = operator new(sizeof(Point));
cout << "分配的内存地址: " << mem << endl;
// 2. 在已分配的内存上构造对象
Point* p = new (mem) Point(10, 20);
// 3. 使用对象
cout << "x = " << p->x << ", y = " << p->y << endl;
// 4. 手动调用析构函数
p->~Point();
// 5. 释放原始内存
operator delete(mem);
return 0;
}
输出:
text
分配的内存地址: 0x12345678
Point 构造: (10,20)
x = 10, y = 20
Point 析构: (10,20)
三、placement new 的核心要点
1. 不分配内存
普通的 new = 分配内存 + 构造对象
placement new = 只构造对象(内存已存在)
cpp
// 普通 new
Point* p1 = new Point(1, 2); // 分配内存 + 构造
// placement new
void* mem = malloc(sizeof(Point));
Point* p2 = new (mem) Point(1, 2); // 只构造,不分配
2. 必须手动调用析构函数
因为 placement new 没有对应的 "placement delete",所以必须显式调用析构函数:
cpp
p->~Point(); // 手动析构
// 然后根据需要释放内存
free(mem);
3. 避免重复构造
在已经构造了对象的内存上再次构造,会导致未定义行为:
cpp
Point* p = new (mem) Point(1, 2); // 第一次构造
new (mem) Point(3, 4); // ❌ 在已构造对象上再构造,未定义行为
四、内存池技术的实现
placement new 最常见的应用是内存池------提前分配一大块内存,然后反复使用。
cpp
#include <iostream>
#include <new>
#include <cassert>
using namespace std;
// 简单的内存池(固定大小块)
template<typename T>
class ObjectPool {
private:
char* pool; // 内存池起始地址
size_t capacity; // 最多能容纳多少个对象
bool* used; // 标记每个槽位是否被使用
size_t blockSize; // 每个对象占用的字节数(含对齐)
public:
ObjectPool(size_t maxObjects) : capacity(maxObjects) {
// 计算每个对象需要的字节数(考虑对齐)
blockSize = sizeof(T);
// 简单对齐到 alignof(T)
if (blockSize < alignof(T)) blockSize = alignof(T);
// 分配内存池
pool = static_cast<char*>(::operator new(blockSize * capacity));
used = new bool[capacity](); // 全部初始化为 false
cout << "创建对象池,容量: " << capacity << ", 块大小: " << blockSize << endl;
}
~ObjectPool() {
// 清理还在使用的对象
for (size_t i = 0; i < capacity; i++) {
if (used[i]) {
T* obj = reinterpret_cast<T*>(pool + i * blockSize);
obj->~T(); // 手动析构
}
}
::operator delete(pool);
delete[] used;
cout << "销毁对象池" << endl;
}
// 从池中分配一个对象
T* allocate(const T& value) {
for (size_t i = 0; i < capacity; i++) {
if (!used[i]) {
T* obj = new (pool + i * blockSize) T(value); // placement new
used[i] = true;
cout << "分配对象,索引: " << i << ", 地址: " << obj << endl;
return obj;
}
}
return nullptr; // 池已满
}
// 释放对象回池中
void deallocate(T* obj) {
if (obj == nullptr) return;
// 计算对象在池中的索引
size_t offset = reinterpret_cast<char*>(obj) - pool;
size_t index = offset / blockSize;
if (index < capacity && used[index]) {
obj->~T(); // 手动调用析构
used[index] = false;
cout << "释放对象,索引: " << index << endl;
}
}
size_t getFreeCount() const {
size_t free = 0;
for (size_t i = 0; i < capacity; i++) {
if (!used[i]) free++;
}
return free;
}
};
// 测试用的类
class Student {
private:
int id;
string name;
public:
Student(int i, const string& n) : id(i), name(n) {
cout << " 构造 Student(" << id << ", " << name << ")" << endl;
}
~Student() {
cout << " 析构 Student(" << id << ", " << name << ")" << endl;
}
void print() const {
cout << " Student: id=" << id << ", name=" << name << endl;
}
};
int main() {
// 创建一个能容纳 3 个 Student 的对象池
ObjectPool<Student> pool(3);
cout << "\n=== 分配对象 ===" << endl;
Student* s1 = pool.allocate(Student(1, "张三"));
Student* s2 = pool.allocate(Student(2, "李四"));
Student* s3 = pool.allocate(Student(3, "王五"));
cout << "\n=== 尝试分配第4个(应该失败)===" << endl;
Student* s4 = pool.allocate(Student(4, "赵六"));
cout << "s4 = " << s4 << " (nullptr 表示失败)" << endl;
cout << "\n=== 使用对象 ===" << endl;
s1->print();
s2->print();
s3->print();
cout << "\n=== 释放一个对象 ===" << endl;
pool.deallocate(s2);
cout << "\n=== 重新分配(复用释放的位置)===" << endl;
Student* s5 = pool.allocate(Student(5, "孙七"));
s5->print();
cout << "\n=== 程序结束,对象池自动清理 ===" << endl;
return 0;
}
输出:
text
创建对象池,容量: 3, 块大小: 32
=== 分配对象 ===
构造 Student(1, 张三)
分配对象,索引: 0, 地址: 0x...
构造 Student(2, 李四)
分配对象,索引: 1, 地址: 0x...
构造 Student(3, 王五)
分配对象,索引: 2, 地址: 0x...
=== 尝试分配第4个(应该失败)===
s4 = 0 (nullptr 表示失败)
=== 使用对象 ===
Student: id=1, name=张三
Student: id=2, name=李四
Student: id=3, name=王五
=== 释放一个对象 ===
析构 Student(2, 李四)
释放对象,索引: 1
=== 重新分配(复用释放的位置)===
构造 Student(5, 孙七)
分配对象,索引: 1, 地址: 0x...
Student: id=5, name=孙七
=== 程序结束,对象池自动清理 ===
析构 Student(5, 孙七)
析构 Student(3, 王五)
析构 Student(1, 张三)
销毁对象池
五、placement new 与对齐
placement new 不检查地址是否对齐。如果地址不满足类型的内存对齐要求,访问对象可能崩溃或性能下降。
cpp
// 错误示范:地址可能不对齐
char buf[100];
int* p = new (buf) int(42); // buf 可能不是 4 字节对齐
// 正确做法:使用 alignas
alignas(int) char buf[100];
int* p = new (buf) int(42);
C++17 提供了 std::align 和 aligned_alloc 来手动控制对齐。
六、placement new 数组
也可以 placement new 数组,但非常麻烦(需要手动计算内存大小、手动调用每个元素的析构函数),通常不推荐。
cpp
// 不推荐:placement new 数组
char* mem = new char[10 * sizeof(Point)];
Point* arr = new (mem) Point[10] { {1,1}, {2,2}, ... }; // 语法复杂
// 推荐:逐元素 placement new
for (int i = 0; i < 10; i++) {
new (mem + i * sizeof(Point)) Point(i, i);
}
// 析构时也要逐个调用
for (int i = 0; i < 10; i++) {
arr[i].~Point();
}
七、常见错误
1. 忘记包含 <new> 头文件
cpp
// ❌ 缺少 #include <new>
T* p = new (mem) T(); // 编译错误
2. 忘记手动调用析构函数
cpp
void* mem = malloc(sizeof(Point));
Point* p = new (mem) Point(1, 2);
// 没有调用 p->~Point() → 如果 Point 管理资源,会泄漏
free(mem);
3. 重复构造(不析构就再次 placement new)
cpp
new (mem) Point(1, 2);
new (mem) Point(3, 4); // ❌ 未定义行为
4. 对 nullptr 或无效地址使用 placement new
cpp
void* mem = nullptr;
Point* p = new (mem) Point(1, 2); // ❌ 未定义行为
八、placement new 的应用场景总结
| 场景 | 说明 |
|---|---|
| 内存池 | 预分配大块内存,反复使用,减少 malloc 开销 |
| 共享内存 | 在共享内存上构造对象,供多进程访问 |
| 嵌入式系统 | 在固定地址(如硬件寄存器)上构造对象 |
| 容器优化 | std::vector 的原地构造(emplace_back 底层) |
| 避免异常 | 预先分配内存,保证构造不会因内存不足失败 |
九、这一篇的收获
你现在应该理解:
-
placement new:在已分配的内存上构造对象,不分配新内存
-
需要
<new>头文件 :语法new (address) T(args...) -
必须手动析构 :
p->~T(),placement new 没有对应的 delete -
避免重复构造:同一地址不能构造两次而不析构
-
内存池技术:预分配 + placement new + 显式析构,实现高效复用
-
注意内存对齐:地址必须满足类型的对齐要求
💡 小作业:实现一个简单的
RingBuffer模板类,内部使用一个固定大小的数组(如char[1024]),用 placement new 来存储元素。提供push()和pop()操作,注意正确调用构造和析构。
下一篇预告 :第30篇《RAII与智能指针(一):auto_ptr的缺陷与unique_ptr》------告别手动 new/delete,进入现代 C++ 的资源管理世界。RAII 是什么?为什么 auto_ptr 被废弃?unique_ptr 如何实现独占所有权?下篇开始智能指针系列。