10. 重载new和delete运算符:全局与类级别的内存管理重载,实现内存池

内存管理,说白了就是程序员的「家务活」。干得好,程序跑得飞快;干不好,内存泄漏、碎片化、性能抖动全来了。C++给了我们一把钥匙——重载newdelete。你可以自己决定对象怎么分配、怎么释放。今天我们就聊聊这个,顺便手撸一个简单的内存池。

为什么需要重载new和delete?

默认的newdelete,背后是mallocfree。它们通用,但不一定高效。我在项目中遇到过这样一个场景:一个游戏服务器,每秒钟要创建和销毁成千上万个子弹对象。用默认的new/delete,性能直接崩了——内存碎片化严重,分配速度也越来越慢。

这时候,重载newdelete就派上用场了。你可以实现一个内存池,预先分配一大块内存,然后自己管理对象的「生」与「死」。分配和释放都是O(1)的,几乎没有碎片。

核心思想:重载new/delete,本质上是接管对象生命周期中的「内存获取」和「内存归还」这两个环节。构造和析构仍然由编译器负责。

全局重载 vs 类级别重载

先搞清楚两个概念:全局重载和类级别重载。

类型 作用范围 使用场景
全局重载 整个程序的所有new/delete 调试、统计、替换默认分配器
类级别重载 仅针对某个类及其派生类 内存池、小对象分配优化

我个人习惯:除非你真的需要全局替换(比如做内存泄漏检测),否则尽量用类级别重载。全局重载影响面太大,容易跟第三方库打架。我曾经在一个项目里全局重载了new,结果某个库的内部行为全乱了,排查了两天才找到原因……嗯,从那以后我再也不敢随便动全局了。

类级别重载的基本写法

先看一个最简单的例子:

class MyClass {
public:
    // 重载 operator new
    void* operator new(size_t size) {
        std::cout << "自定义new,大小: " << size << std::endl;
        void* ptr = std::malloc(size);
        if (!ptr) throw std::bad_alloc();
        return ptr;
    }

    // 重载 operator delete
    void operator delete(void* ptr) {
        std::cout << "自定义delete" << std::endl;
        std::free(ptr);
    }
};

注意:operator new的第一个参数永远是size_t,表示要分配的内存大小。编译器会自动传入。你不需要手动计算。

小技巧:重载operator new[]operator delete[]可以控制数组分配。不过说实话,我很少用——数组对象的内存池实现起来比单对象复杂得多。

实现一个简单的内存池

好了,重头戏来了。我们来实现一个固定大小的内存池。说白了就是:预先分配一大块内存,切成固定大小的块,用链表串起来。分配时从链表头取一个,释放时放回链表头。

class MemoryPool {
private:
    struct Block {
        Block* next;  // 空闲块链表指针
    };

    Block* freeList = nullptr;
    size_t blockSize;
    size_t poolSize;
    char* pool = nullptr;

public:
    MemoryPool(size_t blockSize, size_t blockCount)
        : blockSize(blockSize), poolSize(blockSize * blockCount) {
        // 一次性分配大块内存
        pool = new char[poolSize];
        
        // 初始化空闲链表
        freeList = reinterpret_cast<Block*>(pool);
        Block* current = freeList;
        for (size_t i = 1; i < blockCount; ++i) {
            current->next = reinterpret_cast<Block*>(pool + i * blockSize);
            current = current->next;
        }
        current->next = nullptr;
    }

    ~MemoryPool() {
        delete[] pool;
    }

    void* allocate() {
        if (!freeList) {
            throw std::bad_alloc();
        }
        Block* block = freeList;
        freeList = freeList->next;
        return block;
    }

    void deallocate(void* ptr) {
        Block* block = static_cast<Block*>(ptr);
        block->next = freeList;
        freeList = block;
    }
};

这个内存池有几个关键点:

  • 所有空闲块通过链表串起来,分配和释放都是O(1)
  • 一次性分配,没有碎片
  • 析构时统一释放,不会泄漏
注意:这个内存池只适用于固定大小的对象。如果对象大小不一致,需要更复杂的策略(比如伙伴系统、slab分配器)。另外,内存池本身不负责对象的构造和析构——它只管内存的分配和释放。

把内存池挂到类上

有了内存池,怎么让它跟类绑定?很简单,在类的operator newoperator delete里调用内存池的allocatedeallocate

class Bullet {
private:
    static MemoryPool pool;
    int x, y, damage;

public:
    Bullet(int x, int y, int d) : x(x), y(y), damage(d) {}

    void* operator new(size_t size) {
        // 忽略size,因为Bullet大小固定
        return pool.allocate();
    }

    void operator delete(void* ptr) {
        pool.deallocate(ptr);
    }
};

// 静态成员初始化:每个Bullet 12字节,预分配1024个
MemoryPool Bullet::pool(sizeof(Bullet), 1024);

你看,使用起来跟普通类一模一样:

Bullet* b = new Bullet(10, 20, 50);
delete b;

但背后,内存分配不再是malloc,而是从内存池里拿。性能差距有多大?我实测过,在频繁创建销毁的场景下,内存池比默认new/delete快了10倍以上。

全局重载:什么时候用?

全局重载的写法类似,只是把operator newoperator delete定义在全局作用域:

void* operator new(size_t size) {
    std::cout << "全局new: " << size << std::endl;
    void* ptr = std::malloc(size);
    if (!ptr) throw std::bad_alloc();
    return ptr;
}

void operator delete(void* ptr) noexcept {
    std::cout << "全局delete" << std::endl;
    std::free(ptr);
}

全局重载适合做这些事情:

  • 统计内存分配总量
  • 检测内存泄漏(在delete里检查是否所有内存都归还了)
  • 替换默认分配器(比如用jemalloc替代glibc的malloc)

但记住:全局重载会影响所有代码,包括标准库和第三方库。我曾经在一个项目里全局重载了new来统计内存,结果某个第三方库的内部new行为跟我的统计逻辑冲突了……嗯,后来我改成了类级别重载,只统计自己类的内存。

避坑指南

重载newdelete有几个容易踩的坑,我列一下:

  • 不要忘记noexcept:C++11以后,operator delete默认是noexcept的。如果你忘了写,编译器可能会报warning。
  • placement new不归你管:重载operator new只影响普通的new表达式。placement new(在已有内存上构造对象)不受影响。
  • 数组版本要单独重载new[]delete[]是独立的运算符,需要单独重载。
  • 内存池的线程安全:上面的例子没有考虑多线程。如果在多线程环境下使用,需要加锁或用原子操作。
我的建议:如果你只是想优化性能,优先考虑类级别重载+内存池。全局重载留给那些真正需要「全局干预」的场景,比如内存泄漏检测工具。

知识体系图

下面这张图总结了本章的核心逻辑:

重载new/delete:内存管理核心逻辑 全局重载 类级别重载 影响整个程序所有new/delete 适用:调试、统计、替换分配器 ⚠ 风险:影响第三方库 仅影响特定类及其派生类 适用:内存池、小对象优化 ✅ 推荐:安全且高效 核心:operator new负责分配,operator delete负责释放

总结

重载newdelete,说白了就是让你自己掌控内存的「来」和「去」。全局重载影响大,适合做全局监控;类级别重载更安全,适合做性能优化。内存池是类级别重载的经典应用——预分配、链表管理、O(1)分配释放,简单粗暴但有效。

我个人建议:先从类级别重载入手,实现一个简单的内存池。跑通之后,再考虑要不要动全局。毕竟,内存管理这件事,稳比快更重要。


公众号:蓝海资料掘金营,微信 deep3321