C++内存分配跟踪与 operator new / operator delete 重载详解
一、为什么要重载 operator new / operator delete
在 C++ 中,new 和 delete 是运行时库提供的全局操作符,用于从堆上分配与释放内存。
通常情况下,程序员不需要关心它们的底层实现,但在以下情况中,我们可能需要重载它们:
-
跟踪内存分配:用于调试、分析性能或检测内存泄漏。
-
实现自定义内存池(Memory Pool):加快对象分配、减少碎片。
-
定位异常内存行为:例如频繁分配、内存增长过快等问题。
-
控制特定类型的分配策略:比如对象从固定缓冲区分配。
通过重载 operator new 和 operator 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()
这说明:
-
unique_ptr析构时调用default_delete; -
default_delete内部执行delete _Ptr;; -
编译器选择合适的
operator delete(可能是带size_t参数的版本); -
最终调用我们重载的全局
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++ 内存分配机制,
追踪对象分配与释放的来源,
甚至扩展成自定义的 内存池 或 分配追踪系统。
更多推荐


所有评论(0)