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++ 提供的一个精确控制内存的机制,核心要点:
- 语法 :
new (地址) 类型 - 头文件 :必须
#include <new> - 不能 delete:定位 new 的内存来源不是堆,delete 会导致崩溃
- 手动管理偏移:定位 new 不会自动追踪已用内存
- 手动调用析构:对于类对象,需要显式调用析构函数
- 适用场景:嵌入式系统、内存池、共享内存、实时系统
日常开发中很少用到定位 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;
}
会编译通过吗?运行时会怎样?为什么?