【c++面向对象编程】第29篇:定位new(placement new):在指定内存上构造对象

目录

一、一个现实需求

[二、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::alignaligned_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 如何实现独占所有权?下篇开始智能指针系列。

相关推荐
淞綰6 小时前
c语言的练习-字符串的练习-寻找最长连续字符以及出现次数
c语言·数据结构·学习·算法·c语言的练习
计算机安禾6 小时前
【c++面向对象编程】第27篇:空类的大小为什么是1?——C++对象标识的秘密
开发语言·c++·算法
河阿里6 小时前
Python容器:特性、区别和使用场景
开发语言·python
我不是8神6 小时前
面试题:Gorutine泄露的条件有哪些?
java·开发语言
奇树谦6 小时前
QListView和QListWidget区别详细说明
开发语言
郭龙_Jack6 小时前
Java并发包(JUC)深度解析:从LockSupport到云原生演进
开发语言·云原生·java并发编程
Highcharts.js6 小时前
AI向量知识谱系图表创建示例代码|Highcharts网络图表(networkgraph)搭建案例
开发语言·前端·javascript·网络·信息可视化·编辑器·highcharts
rGzywSmDg6 小时前
如何在Dev-C++中选择TDM-GCC编译器
linux·jvm·c++
周杰伦fans6 小时前
C# AutoCAD 二次开发极简入门:从环境搭建到高效实战
开发语言·c#