Effective C++读书笔记——item50(什么时候替换new和delete)
在 C++ 中,有时候需要替换编译器提供的 operator new
和 operator delete
版本。下面详细介绍替换的原因、可能遇到的问题以及合适的替换时机,并给出相应的代码示例。
1. 替换 new
和 delete
的主要原因
- 监测使用错误:通过记录已分配地址和在分配块前后设置标志字节,可以检测内存泄漏、多次删除和数据上溢 / 下溢等错误。
- 提升性能:编译器提供的默认版本是为多种用途设计的,采用中间路线策略。如果对程序的动态内存应用模式有充分理解,自定义版本可能运行更快且需要更少的内存。
- 收集使用方法的统计数据:自定义版本可以方便地收集被分配区块大小、生存期、分配和释放顺序等信息。
2. 自定义 operator new
示例及问题
以下是一个简单的自定义 operator new
示例,用于检测数据上溢和下溢:
#include <iostream>
#include <new>
static const int signature = 0xDEADBEEF;
typedef unsigned char Byte;
// 此代码存在缺陷
void* operator new(std::size_t size) throw(std::bad_alloc) {
using namespace std;
size_t realSize = size + 2 * sizeof(int); // 增加请求大小以容纳标志字节
void *pMem = std::malloc(realSize); // 调用 malloc 获取内存
if (!pMem) throw std::bad_alloc();
// 在内存的开头和结尾写入标志字节
*(static_cast<int*>(pMem)) = signature;
*(reinterpret_cast<int*>(static_cast<Byte*>(pMem) + realSize - sizeof(int))) = signature;
// 返回指向第一个标志字节之后的内存指针
return static_cast<Byte*>(pMem) + sizeof(int);
}
该代码存在一些问题,例如没有遵循 operator new
的 C++ 惯例(如未包含调用 new-handling function
的循环),更微妙的问题是可能存在排列对齐问题。
3. 排列对齐问题
很多计算机架构要求特定类型的数据放置在具有特定性质的地址中,不遵守约束可能导致运行时硬件异常或性能下降。C++ 要求 operator new
返回适合任何数据类型排列的指针,上述代码返回的指针偏移了一个 int
大小,可能导致对齐不恰当。
4. 替换 new
和 delete
的合适时机
- 监测使用错误:记录已分配地址和检查标志字节,检测内存泄漏、多次删除和数据上溢 / 下溢等错误。
- 收集使用统计数据:收集被分配区块大小、生存期、分配和释放顺序等信息。
- 提升分配和回收速度:通用目的的分配器通常比自定义版本慢,特别是针对特定类型对象专门设计的自定义版本。单线程程序可以通过编写非线程安全分配器获得速度提升,但需确定这些函数是真正的瓶颈。
// 简单的单线程固定大小分配器示例
#include <vector>
template <typename T>
class FixedSizeAllocator {
private:
std::vector<T*> freeList;
public:
void* allocate() {
if (freeList.empty()) {
return ::operator new(sizeof(T));
}
void* p = freeList.back();
freeList.pop_back();
return p;
}
void deallocate(void* p) {
freeList.push_back(static_cast<T*>(p));
}
};
- 减少缺省内存管理的空间成本:通用目的的内存管理器通常比自定义版本使用更多的内存,针对小对象调谐的分配器可以消除这种成本。
- 调整缺省分配器不适当的排列对齐:某些编译器提供的
operator new
可能不能保证特定类型(如double
)的动态分配按照要求的字节对齐,替换为保证对齐的版本可以提升性能。 - 聚集相关的对象:使用
new
和delete
的定位版本(placement versions),为特定数据结构创建独立的堆,降低页错误频率。 - 获得不同寻常的行为:例如在共享内存中分配和回收区块,或用零覆盖被回收的内存以提高数据安全性。
总结要点
- 替换原因多样:有很多正当理由编写
new
和delete
的自定义版本,包括改进性能、调试堆用法错误以及收集堆用法信息。 - 注意排列对齐:自定义
operator new
时要注意排列对齐问题,确保返回的指针适合任何数据类型的排列。 - 考虑多种因素:在决定替换
new
和delete
时,要综合考虑性能、内存使用、错误检测等多种因素,并进行实际测试以确定是否真的有必要替换。