03C++ 定位 new 运算符(Placement new)

C++ 定位 new 运算符(Placement new)

1. 为什么需要定位 new?

普通的 new 在**堆(heap)**上分配内存,由操作系统管理。但在某些场景下,我们需要精确控制内存的位置:

  • 嵌入式系统:内存地址是固定的硬件映射
  • 共享内存:多个进程共享同一块物理内存
  • 内存池:预先分配一大块内存,然后从中分配对象,避免碎片
  • 实时系统:不能承受堆分配的不确定性延迟

定位 new(Placement new)就是干这个的------在指定的内存地址上构造对象

2. 基本用法

头文件

定位 new 需要包含 <new> 头文件:

cpp 复制代码
#include <new>

语法

cpp 复制代码
new (地址) 类型;
new (地址) 类型[大小];

完整示例

cpp 复制代码
#include <iostream>
#include <new>  // 定位 new 需要这个头文件

char buffer[512];  // 静态缓冲区,作为定位 new 的目标

int main() {
    // 在 buffer 上创建一个 double 数组
    double* pd = new (buffer) double[5];
    
    for (int i = 0; i < 5; i++)
        pd[i] = 1000 + 20.0 * i;
    
    for (int i = 0; i < 5; i++) {
        std::cout << pd[i] << " (地址: " << &pd[i] << ")\n";
    }
    
    return 0;
}

3. 普通 new vs 定位 new

对比项 普通 new 定位 new
头文件 不需要额外头文件 需要 #include <new>
内存来源 堆(heap) 你指定的任意地址
分配失败 抛出 std::bad_alloc 取决于你给的地址
释放方式 delete / delete[] 不能用 delete!
内存管理 自动跟踪已用/空闲 不跟踪,需要手动管理
速度 相对慢(系统调用) 极快(只调用构造函数)

4. 关键注意事项

4.1 不能使用 delete 释放

cpp 复制代码
char buffer[512];
double* pd = new (buffer) double[5];

// delete[] pd;  // ❌ 错误!buffer 是栈/静态数组,不是堆内存

定位 new 分配的内存不能用 delete 释放,因为:

  • 定位 new 不分配内存,它只是在你给出的地址上调用构造函数
  • 如果 buffer 是栈数组或静态数组,调用 delete 会导致程序崩溃(试图释放非堆内存)
  • 你得手动管理 buffer 的生命周期

4.2 不跟踪已用内存

cpp 复制代码
double* pd1 = new (buffer) double[5]{1,2,3,4,5};
double* pd2 = new (buffer) double[5]{9,9,9,9,9};
// 此时 pd1[0] 也是 9,因为 pd2 覆盖了同一位置

定位 new不管理内存偏移------如果你不手动指定偏移量,第二次调用会覆盖第一次的数据。

4.3 使用偏移量避免覆盖

cpp 复制代码
// 在 buffer 的不同位置分配
double* pd1 = new (buffer) double[5];
double* pd2 = new (buffer + 5 * sizeof(double)) double[5];
// pd2 从 buffer + 40 字节处开始,不会覆盖 pd1

5. 实际应用场景

场景一:嵌入式寄存器映射

cpp 复制代码
#define GPIO_BASE 0x40021000
volatile uint32_t* gpio_reg = new (reinterpret_cast<void*>(GPIO_BASE)) uint32_t;

场景二:内存池

cpp 复制代码
class MemoryPool {
    char pool[1024];
    int offset = 0;
public:
    template<typename T>
    T* alloc() {
        if (offset + sizeof(T) > 1024) return nullptr;
        T* ptr = new (pool + offset) T;
        offset += sizeof(T);
        return ptr;
    }
};

场景三:共享内存

cpp 复制代码
#include <sys/mman.h>
void* shared_mem = mmap(nullptr, 4096, PROT_READ|PROT_WRITE,
                        MAP_SHARED|MAP_ANONYMOUS, -1, 0);
int* counter = new (shared_mem) int(0);

6. 定位 new 与对象生命周期

对于自定义类型,定位 new 会调用构造函数,而你需要手动调用析构函数:

cpp 复制代码
struct MyClass {
    MyClass()  { std::cout << "构造\n"; }
    ~MyClass() { std::cout << "析构\n"; }
};

char buffer[sizeof(MyClass)];
MyClass* obj = new (buffer) MyClass();  // 调用构造函数

obj->~MyClass();  // 必须手动调用析构函数!

7. 总结

定位 new 是 C++ 提供的一个精确控制内存的机制,核心要点:

  1. 语法new (地址) 类型
  2. 头文件 :必须 #include <new>
  3. 不能 delete:定位 new 的内存来源不是堆,delete 会导致崩溃
  4. 手动管理偏移:定位 new 不会自动追踪已用内存
  5. 手动调用析构:对于类对象,需要显式调用析构函数
  6. 适用场景:嵌入式系统、内存池、共享内存、实时系统

日常开发中很少用到定位 new,但在需要精确控制内存的关键系统中,它是不可或缺的工具。


互动测验(选择题)

第 1 题:定位 new 的语法

定位 new 的正确语法是?

A. new (buffer) int;

B. new int(buffer);

C. buffer.new(int);

D. new int @buffer;

答案:A。定位 new 的格式是 new (地址) 类型;

第 2 题:为什么不能用 delete?

定位 new 分配的内存为什么不能用 delete 释放?

A. 因为定位 new 没有对应的 delete 运算符

B. 因为定位 new 只是在你给的地址上调用构造函数,内存本身不是从堆上来的。如果 buffer 是栈或静态数组,调用 delete 会崩溃

C. 可以用 delete,只是不推荐

D. 用了 delete 会导致内存泄漏,但不会崩溃

答案:B。定位 new 不分配内存,只是在你提供的地址上构造对象。delete 只能释放堆内存。

第 3 题:连续调用定位 new

cpp 复制代码
char buffer[512];
double* pd1 = new (buffer) double[5]{1,2,3,4,5};
double* pd2 = new (buffer) double[5]{9,9,9,9,9};

会发生什么?

A. 第二次分配会在 buffer 后面接着分配,不会覆盖

B. 第二次分配会覆盖第一次的数据,因为定位 new 不管理偏移

C. 编译错误

D. 第二次分配会自动释放第一次的数据

答案:B。定位 new 不追踪已用内存,连续用同一地址会覆盖。需要用偏移量 buffer + N * sizeof(T) 来避免。

第 4 题:定位 new 的用途

什么时候会用定位 new?

A. 日常开发中替代普通 new

B. 在嵌入式/实时系统中,需要精确控制内存分配位置(如共享内存、内存映射 IO、自定义内存池)

C. 从来没有实际用途

D. 只是为了面试

答案:B。


练习题:普通数组 + 定位 new

cpp 复制代码
char buffer[32];

用定位 new 在 buffer 上存放一个 int 数组(4 个元素),赋值为 10, 20, 30, 40,然后打印出来。

要求:

  • for 循环赋值
  • for 循环打印值和地址
  • 验证地址是否都在 buffer 范围内

习题 2:两个结构体对象

cpp 复制代码
struct Product {
    char name[20];
    double price;
    int quantity;
};

创建一个 char buffer[128],用定位 new 在上面存放两个 Product 对象:

  • 第一个从 buffer 起始位置
  • 第二个在第一个之后(用偏移量)

给两个对象赋值,打印它们的信息和地址,验证没有重叠。

习题 3:模拟内存池

实现一个简单的固定大小内存池:

cpp 复制代码
template<typename T>
class SimplePool {
    char pool[1024];
    int used;
public:
    SimplePool() : used(0) {}
    
    T* alloc() {
        if (used + sizeof(T) > 1024) return nullptr;
        T* ptr = new (pool + used) T;
        used += sizeof(T);
        return ptr;
    }
    
    void reset() { used = 0; }
    int available() const { return 1024 - used; }
};

写一个 main() 测试:

  • 创建 SimplePool<int>
  • 分配 5 个 int,赋值并打印
  • 打印剩余可用空间
  • 调用 reset() 重置,再次分配验证

习题 4:分析题

以下代码有什么问题?

cpp 复制代码
#include <new>

char buffer[64];

int main() {
    int* p = new (buffer) int(42);
    delete p;  // 有问题吗?
    return 0;
}

会编译通过吗?运行时会怎样?为什么?

相关推荐
汉克老师6 小时前
GESP2025年6月认证C++五级( 第一部分选择题(1-8))
c++·链表·线性筛·最大公约数·gesp5级·gesp五级·埃氏筛
Evand J6 小时前
【MATLAB代码介绍】基于RSSI的蓝牙定位程序,N个锚点、二维平面
开发语言·matlab·蓝牙·定位·rssi
初心未改HD6 小时前
Go语言Error处理与errors包深度解析
开发语言·golang
乐观勇敢坚强的老彭6 小时前
c++信奥循环嵌套讲解
开发语言·c++
十五年专注C++开发6 小时前
Qt实现带多选功能的组合复选框
开发语言·c++·qt·qcombobox
软泡芙6 小时前
【C# 】各种等待大全:从入门到精通
开发语言·c#·log4j
郭源潮16 小时前
从8k嘈杂到16k清晰,我是如何使用RNNoise+libresample构建音频降噪管道的?
c++·音视频·实时音视频
@小码农6 小时前
2026年信息素养大赛【星火征途】图形化编程复赛和决赛模拟题B
开发语言·数据结构·c++·算法
JMchen1236 小时前
NDK新趋势——Rust与Android深度集成实战
android·开发语言·rust·jni·内存安全·android ndk·移动端性能