一、为什么要重载 operator new / operator delete

在 C++ 中,newdelete 是运行时库提供的全局操作符,用于从堆上分配与释放内存。
通常情况下,程序员不需要关心它们的底层实现,但在以下情况中,我们可能需要重载它们

  • 跟踪内存分配:用于调试、分析性能或检测内存泄漏。

  • 实现自定义内存池(Memory Pool):加快对象分配、减少碎片。

  • 定位异常内存行为:例如频繁分配、内存增长过快等问题。

  • 控制特定类型的分配策略:比如对象从固定缓冲区分配。

通过重载 operator newoperator delete,我们就可以拦截所有内存分配事件,从而实现自定义统计与调试。


二、operator new / operator delete 的常见重载形式

基本版本(最常用)

void* operator new(std::size_t size);
void operator delete(void* ptr) noexcept;
  • operator new:分配 size 字节的内存。

  • operator delete:释放内存。

示例:

#include <iostream>
#include <cstdlib>

void* operator new(std::size_t size) {
    std::cout << "Allocating " << size << " bytes\n";
    void* memory = malloc(size);
    if (!memory)
        throw std::bad_alloc();  // 标准要求:分配失败必须抛出
    return memory;
}

void operator delete(void* ptr) noexcept {
    std::cout << "Freeing memory\n";
    free(ptr);
}

 C++17 新增的 “带大小” delete(sized delete)

void operator delete(void* ptr, std::size_t size) noexcept;

编译器在知道对象大小时(例如 delete obj; 时能确定类型),会优先调用该版本。

示例:

void operator delete(void* memory, std::size_t size) noexcept {
    std::cout << "Deleting " << size << " bytes\n";
    free(memory);
}

建议同时提供两个版本:

void operator delete(void* ptr) noexcept;
void operator delete(void* ptr, std::size_t size) noexcept;

不抛异常版本(nothrow

void* operator new(std::size_t size, const std::nothrow_t&) noexcept;

当分配失败时,返回 nullptr,而不是抛出异常:

void* operator new(std::size_t size, const std::nothrow_t&) noexcept {
    return malloc(size);
}

使用方式:

int* p = new(std::nothrow) int;
if (!p)
    std::cerr << "allocation failed\n";

三、重载时需要注意的几个坑点

 1. 不要在 operator new 里直接用 std::cout

因为 std::cout 内部可能会触发内存分配,
而我们现在正拦截 operator new → 导致递归调用、死循环、崩溃

错误示例:

void* operator new(std::size_t size) {
    std::cout << "Allocating " << size << " bytes\n"; // 可能递归
    return malloc(size);
}

正确做法:

  • printf()fwrite()write(2) 等底层输出;

  • 或者使用 重入保护(设置标志防止重复调用)。


2. 线程安全问题

如果使用全局变量记录统计值:

struct Metrics {
    size_t totalAlloc = 0;
    size_t totalFree = 0;
} gMetrics;

多个线程同时修改时会产生竞态条件。
 推荐改成原子变量:

#include <atomic>
std::atomic<size_t> totalAlloc = 0;

3. 对齐版本(alignment)

C++17 引入对齐分配:

void* operator new(std::size_t size, std::align_val_t align);
void operator delete(void* ptr, std::align_val_t align) noexcept;

当类有 alignas(64) 等修饰时,会调用这些版本。
一般做简单统计可忽略它们,但要知道它们存在,否则有些分配可能被“漏掉”。


四、调试实践:跟踪堆分配来源

我们先写一个最简单的例子:

#include <iostream>

void* operator new(size_t size) {
    std::cout << "Allocating " << size << " bytes\n";
    return malloc(size);
}

struct Object { int x, y, z; };

int main() {
    Object* obj = new Object;
    std::string s = "Miku";
}

编译运行(MSVC 环境):

Allocating 12 bytes

此时只打印出 Object 的分配。
因为 "Miku" 属于小字符串(SSO, Small String Optimization),不会真正堆分配(但在 Debug 模式下仍可能分配内部缓冲区)。


调用堆栈分析

return malloc(size); 处设置断点,可以查看是谁触发了 new

比如堆栈中:

> operator new(size_t size)
  main()

就说明是 Object* obj = new Object; 触发了分配。

如果是 std::string s = "Miku";,堆栈会多出:

> operator new(size_t size)
  std::_Default_allocate_traits::_Allocate()
  main()

转到 _Allocate 定义(来自 <xmemory>):

struct _Default_allocate_traits {
    static void* _Allocate(size_t _Bytes) {
        return ::operator new(_Bytes);
    }
};

也就是说,std::string 内部是通过 ::operator new 来进行堆分配的。


使用智能指针的情况

#include <iostream>
#include <memory>

void operator delete(void* memory) {
    free(memory);
}

struct Object { int x, y, z; };

int main() {
    {
        std::unique_ptr<Object> obj = std::make_unique<Object>();
    }
}

free(memory) 设置断点,查看调用栈可发现:

operator delete(void*)
operator delete(void*, size_t)
std::default_delete<Object>::operator()
std::unique_ptr<Object>::~unique_ptr()

这说明:

  1. unique_ptr 析构时调用 default_delete

  2. default_delete 内部执行 delete _Ptr;

  3. 编译器选择合适的 operator delete(可能是带 size_t 参数的版本);

  4. 最终调用我们重载的全局 operator delete

完整调用链被你“拦截”到,就能精准跟踪智能指针的释放过程。


五、实现一个完整的内存统计系统

#include <iostream>
#include <atomic>
#include <cstdint>

struct AllocationMetrics {
    std::atomic<uint32_t> TotalAllocated{0};
    std::atomic<uint32_t> TotalFreed{0};

    uint32_t CurrentUsage() const {
        return TotalAllocated - TotalFreed;
    }
};

static AllocationMetrics s_AllocationMetrics;

void* operator new(size_t size) {
    s_AllocationMetrics.TotalAllocated += size;
    return malloc(size);
}

void operator delete(void* memory, size_t size) noexcept {
    s_AllocationMetrics.TotalFreed += size;
    free(memory);
}

void operator delete(void* memory) noexcept {
    free(memory);
}

static void PrintMemoryUsage() {
    std::cout << "Memory Usage: " << s_AllocationMetrics.CurrentUsage() << " bytes\n";
}

struct Object { int x, y, z; };

int main() {
    Object* obj = new Object;
    PrintMemoryUsage();

    delete obj;
    PrintMemoryUsage();
}

输出示例:

Memory Usage: 12 bytes
Memory Usage: 0 bytes

六、总结对比表

函数签名 作用 是否建议
void* operator new(size_t) 分配内存 必须
void operator delete(void*) noexcept 释放内存 必须
void operator delete(void*, size_t) noexcept C++17 带大小 delete 推荐
void* operator new(size_t, const std::nothrow_t&) noexcept 不抛异常分配 可选
void* operator new(size_t, align_val_t) 对齐分配 可选(C++17)

结语

通过重载 operator new / operator delete
我们可以深入了解 C++ 内存分配机制
追踪对象分配与释放的来源,
甚至扩展成自定义的 内存池分配追踪系统

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐