2. new/delete 运算符深度剖析:operator new/delete 的重载、placement new、内存池原理
好,咱们今天来聊聊 C++ 里最基础、也最容易踩坑的东西——new 和 delete。
很多人觉得这俩不就是分配和释放内存吗?有什么好讲的?
嗯,我当年也是这么想的。直到有一次,我在一个高并发的服务器项目里,发现程序跑着跑着就卡死了,查了三天,最后定位到是 new 抛异常导致的内存泄漏。从那以后,我就把 operator new 和 placement new 翻来覆去看了好几遍。
今天咱们就把这层窗户纸捅破。
2.1 new 和 delete 到底干了什么?
先问个问题:new 和 malloc 有什么区别?
你可能会说:new 会调用构造函数,malloc 不会。
没错,但这只是表象。实际上,new 干了三件事:
- 调用
operator new分配原始内存 - 在分配的内存上调用构造函数
- 返回指向对象的指针
delete 则反过来:
- 调用析构函数
- 调用
operator delete释放内存
说白了,new 就是 malloc + 构造函数的组合。但这里有个关键点:operator new 和 operator delete 是可以被重载的。
核心理解: new 是语言关键字,operator new 是函数。前者负责调用后者,再加上构造逻辑。重载的是函数,不是关键字。
2.2 重载 operator new / operator delete
为什么要重载?
我举个例子。你在写一个游戏引擎,每帧要创建几千个小对象。默认的 new 每次都要向操作系统申请内存,这开销太大了。你想想看,系统调用一次就要几十微秒,几千次下来,帧率直接崩了。
这时候,重载 operator new 就派上用场了。
class MyClass {
public:
void* operator new(size_t size) {
std::cout << "自定义分配: " << size << " 字节" << std::endl;
return malloc(size);
}
void operator delete(void* ptr) {
std::cout << "自定义释放" << std::endl;
free(ptr);
}
};
注意,operator new 的第一个参数永远是 size_t,表示要分配的内存大小。这个参数由编译器自动传入,你不需要手动传。
我个人的习惯是,在重载时加上 std::nothrow 版本,这样分配失败时返回 nullptr 而不是抛异常:
void* operator new(size_t size, const std::nothrow_t&) noexcept {
return malloc(size);
}
小技巧: 如果你重载了全局的 operator new,记得也要重载对应的 operator delete,否则链接时会报错。我吃过这个亏。
2.3 placement new:在指定内存上构造对象
这个就更有意思了。
placement new 不分配内存,它只负责在已经分配好的内存上调用构造函数。语法长这样:
#include <new>
void* buffer = malloc(sizeof(MyClass));
MyClass* obj = new (buffer) MyClass(42); // placement new
为什么要用这个?
场景一:你有一个大块的内存池,想在上面反复创建和销毁对象,避免频繁的系统调用。
场景二:你需要在共享内存或 mmap 映射的区域上构造对象。
场景三:你写了一个自定义的内存分配器,想控制对象在特定地址上创建。
我曾经在一个嵌入式项目里,用 placement new 在固定地址上构造设备驱动对象。那会儿内存只有 64KB,每一字节都得精打细算。
注意,用 placement new 创建的对象,不能直接用 delete 释放。你得手动调用析构函数:
obj->~MyClass(); // 手动析构
free(buffer); // 释放原始内存
警告: 千万不要对 placement new 出来的对象调用 delete!那会调用 operator delete 去释放内存,但你的内存可能来自池子或共享内存,不是堆上分配的。后果就是——崩溃。
2.4 内存池原理:从零开始搭一个
好了,前面铺垫了这么多,终于到重头戏了。
内存池,说白了就是预先申请一大块内存,然后自己管理里面的小块分配。这样就不需要每次都跟操作系统打交道了。
最简单的内存池,就是一个固定大小的对象池。比如你只分配 MyClass 对象,每个对象大小固定,那就好办了。
我画了一张图,帮你理解内存池的核心逻辑:
你看,核心思路就是:
- 预先分配一大块连续内存
- 按固定大小切成小块
- 用链表把空闲块串起来
- 分配时从链表头取一块,释放时放回链表头
代码实现其实很简单:
class FixedPool {
struct Node {
Node* next;
};
Node* freeList = nullptr;
char* pool = nullptr;
size_t blockSize;
size_t poolSize;
public:
FixedPool(size_t blockSize, size_t blockCount)
: blockSize(blockSize), poolSize(blockSize * blockCount) {
pool = new char[poolSize];
// 初始化空闲链表
for (size_t i = 0; i < blockCount; ++i) {
Node* node = reinterpret_cast<Node*>(pool + i * blockSize);
node->next = freeList;
freeList = node;
}
}
void* allocate() {
if (!freeList) return nullptr; // 池子空了
Node* node = freeList;
freeList = node->next;
return node;
}
void deallocate(void* ptr) {
Node* node = static_cast<Node*>(ptr);
node->next = freeList;
freeList = node;
}
~FixedPool() { delete[] pool; }
};
然后配合 placement new 使用:
FixedPool pool(sizeof(MyClass), 1000);
void* mem = pool.allocate();
MyClass* obj = new (mem) MyClass();
// 使用 obj...
obj->~MyClass();
pool.deallocate(obj);
我的经验: 这种固定大小的内存池,在对象频繁创建销毁的场景下,性能提升非常明显。我在一个网络库中用过,分配速度比 malloc 快了将近 10 倍。但要注意,它只适合固定大小的对象,如果对象大小不一,就得用更复杂的伙伴系统或 slab 分配器了。
2.5 重载 operator new 与内存池结合
最后,咱们把内存池和 operator new 重载结合起来。这样,你的类就可以透明地使用内存池了:
class MyClass {
static FixedPool pool;
public:
void* operator new(size_t size) {
return pool.allocate();
}
void operator delete(void* ptr) {
pool.deallocate(ptr);
}
};
FixedPool MyClass::pool(sizeof(MyClass), 1000);
现在,你写 new MyClass() 时,分配的内存就来自池子了。用户完全感知不到底层的变化。
嗯,这里要注意一点:如果你的类有继承关系,子类的大小可能和父类不同。这时候 operator new 收到的 size 参数会变化,你得根据 size 决定从哪个池子分配。
我建议的做法是,用多个不同大小的池子,根据 size 参数选择对应的池子。或者干脆用 malloc 兜底,只对特定大小的对象走池子。
避坑指南: 我曾经在重载 operator new 时忘了处理 size == 0 的情况。标准规定,即使分配 0 字节,也要返回一个合法的非空指针。所以记得加个判断:if (size == 0) size = 1;
2.6 总结一下
今天咱们聊了:
new和delete的底层机制——分配内存 + 构造/析构- 如何重载
operator new和operator delete来自定义内存管理 placement new的用法和注意事项- 内存池的原理和实现,以及如何与
operator new重载结合
说白了,这些技术都是为了一个目的:更高效、更可控地管理内存。
你想想看,默认的 new 就像去超市买东西,每次都要排队结账。而内存池就像你提前囤了一冰箱的菜,随用随取,省去了排队的功夫。
在实际项目中,我一般不会一上来就用内存池。先分析性能瓶颈,确认是内存分配导致的,再动手优化。过早优化是万恶之源,这句话不是白说的。
好了,今天就到这儿。希望下次你在代码里看到 new 的时候,能多想一想它背后发生了什么。
公众号:蓝海资料掘金营,微信 deep3321