条款8. 写operator new和operator delete时要遵循常规
实际做起来也就是:要有正确的返回值;可用内存不够时要调用出错处理函数(见条款7);处理好0字节内存请求的情况。此外,还要避免不小心隐藏了标准形式的new,不
过这是条款9的话题
过这是条款9的话题
这样,非类成员形式的operator new的伪代码看起来会象下面这样:
void * operator new(size_t size) // operator new还可能有其它参数
{
if (size == 0) { // 处理0字节请求时,
size = 1; // 把它当作1个字节请求来处理
}
while (1) {
分配size字节内存;
if (分配成功)
return (指向内存的指针);
// 分配不成功,找出当前出错处理函数
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if (globalHandler) (*globalHandler)();
else throw std::bad_alloc();
}
}
条款7提到operator new内部包含一个无限循环,上面的代码清楚地说明了这一点——while (1)将导致无限循环。跳出循环的唯一办法是内存分配成功或出错处理函数完成了
很多人没有认识到的一点是operator new经常会被子类继承。这会导致某些复杂性。基类中的operator new可能会被调用去为一个子类对象分配内存
C++标准很怪异,其中之一就是规定所以独立的(freestanding)类的大小都是非零值。
如果想控制基于类的数组的内存分配,必须实现operator new的数组形式——operator new[]
基类的operator new[]会通过继承的方式被用来为子类对象的数组分配内存,而子类对象往往比基类要大。所以,不能想当然认为Base::operator new[]里的每个对象的大小都是sizeof(Base),也就是说,数组里对象的数量不一定就是(请求字节数)/sizeof(Base)。
重写operator new(和operator new[])时所有要遵循的常规就这些。对于operator delete(以及它的伙伴operator delete[]),情况更简单。所要记住的只是,
C++保证删除空指针永远是安全的,所以你要充分地应用这一保证。下面是非类成员形式的operator delete的伪代码:
void operator delete(void *rawMemory)
{
if (rawMemory == 0) return; file://如果指针为空,返回
//
释放rawMemory指向的内存;
return;
}
对于类的情况:
class Base { // 和前面一样,只是这里声明了
public: // operator delete
static void * operator new(size_t size);
static void operator delete(void *rawMemory, size_t size);
...
};
void Base::operator delete(void *rawMemory, size_t size)
{
if (rawMemory == 0) return; // 检查空指针
if (size != sizeof(Base)) { // 如果size"错误",
::operator delete(rawMemory); // 让标准operator来处理请求
return;
}
释放指向rawMemory的内存;
return;
}
条款9. 避免隐藏标准形式的new
内部范围声明的名称会隐藏掉外部范围的相同的名称
class X {
public:
// operator new的参数指定一个
// new-hander(new的出错处理)函数
static void * operator new(size_t size, new_handler p);
};
void specialErrorHandler(); // 定义在别的地方
X *px1 = new (specialErrorHandler) X; // 调用X::operator new
X *px2 = new X; // 错误!
在类里定义了一个称为“operator new”的函数后,会不经意地阻止了对标准new的访问
解决办法:
一个办法是:
class X {
public:
static void * operator new(size_t size, new_handler p);
static void * operator new(size_t size)
{ return ::operator new(size); }
};
X *px1 =
new (specialErrorHandler) X; // 调用 X::operator
// new(size_t, new_handler)
X* px2 = new X; // 调用 X::operator
// new(size_t)
另一种方法:
class X {
public:
static
void * operator new(size_t size, // p缺省值为0
new_handler p = 0); //
};
X *px1 = new (specialErrorHandler) X; // 正确
X* px2 = new X; // 也正确
条款10. 如果写了operator new就要同时写operator delete
让我们回过头去看看这样一个基本问题:为什么有必要写自己的operator new和operator delete?
答案通常是:为了效率
因为缺省版本的operator new是一种通用型的内存分配器,它必须可以分配任意大小的内存块。同样,operator delete也要可以释放任意大小的内存块。
operator delete想弄清它要释放的内存有多大,就必须知道当初operator new分配的内存有多大。
operator delete想弄清它要释放的内存有多大,就必须知道当初operator new分配的内存有多大。
Airplane *pa = new Airplane;
你不会得到一块看起来象这样的内存块:
pa——> Airplane 对象的内存
而是得到象这样的内存块:
pa——> 内存块大小数据 + Airplane 对象的内存
你不会得到一块看起来象这样的内存块:
pa——> Airplane 对象的内存
而是得到象这样的内存块:
pa——> 内存块大小数据 + Airplane 对象的内存
解决方案:自定义operator new + 内存池
class Airplane { // 修改后的类 — 支持自定义的内存管理
public: //
static void * operator new(size_t size);
...
private:
union {
AirplaneRep *rep; // 用于被使用的对象
Airplane *next; // 用于没被使用的(在自由链表中)对象
};
// 类的常量,指定一个大的内存块中放多少个
// Airplane 对象,在后面初始化
static const int BLOCK_SIZE;
static Airplane *headOfFreeList;
};
void * Airplane::operator new(size_t size)
{
// 把“错误”大小的请求转给::operator new()处理;
// 详见条款8
if (size != sizeof(Airplane))
return ::operator new(size);
Airplane *p = // p指向自由链表的表头
headOfFreeList; //
// p 若合法,则将表头移动到它的下一个元素
//
if (p)
headOfFreeList = p->next;
else {
// 自由链表为空,则分配一个大的内存块,
// 可以容纳BLOCK_SIZE个Airplane对象
Airplane *newBlock =
static_cast<Airplane*>(::operator new(BLOCK_SIZE *
sizeof(Airplane)));
// 将每个小内存块链接起来形成一个新的自由链表
// 跳过第0个元素,因为它要被返回给operator new的调用者
for (int i = 1; i < BLOCK_SIZE-1; ++i)
newBlock[i].next = &newBlock[i+1];
// 用空指针结束链表
newBlock[BLOCK_SIZE-1].next = 0;
// p 设为表的头部,headOfFreeList指向的
// 内存块紧跟其后
p = newBlock;
headOfFreeList = &newBlock[1];
}
return p;
}
这个版本的operator new将会工作得非常好。因为通用型的缺省operator new必须应付各种大小的内存请求,还要处理内部外部的碎片
operator new和operator delete必须同时写,这样才不会出现不同的假设
void Airplane::operator delete(void *deadObject,
size_t size)
{
if (deadObject == 0) return; // 见条款 8
if (size != sizeof(Airplane)) { // 见条款 8
::operator delete(deadObject);
return;
}
Airplane *carcass =
static_cast<Airplane*>(deadObject);
carcass->next = headOfFreeList;
headOfFreeList = carcass;
}
这里没有内存泄露!
引起内存泄露的原因在于内存分配后指向内存的指针丢失了。
每个大内存块首先被分成Airplane 大小的小块,然后这些小块被放在自由链表上。当客户调用Airplane::operator new时,小块被自由链表移除,客户得到指向小块的指针。当客户调用operator delete时,小块被放回到自由链表上。
然而确实,::operator new 返回的内存块是从来没有被Airplane::operatordelete 释放,这个内存块有个名字,叫内存池。
修改Airplane的内存管理程序使得::operator new 返回的内存块在不被使用时自动释放并不难,但这里不会这么做
内存池不能解决所有的内存管理问题,在很多情况下是很适合的
一定有什么办法把这种固定大小内存的分配器封装起来,从而可以方便地使用
一个可能的实现:
class Pool { public: Pool(size_t n); // 为大小为n的对象创建 // 一个分配器 void * alloc(size_t n) ; // 为一个对象分配足够内存 // 遵循条款8的operator new常规 void free( void *p, size_t n); // 将p所指的内存返回到内存池; // 遵循条款8 的operator delete常规 ~Pool(); // 释放内存池中全部内存 };
class Airplane { public: ... // 普通Airplane功能 static void * operator new(size_t size); static void operator delete(void *p, size_t size); private: AirplaneRep *rep; // 指向实际描述的指针 static Pool memPool; // Airplanes 的内存池 }; inline void * Airplane::operator new(size_t size) { return memPool.alloc(size); } inline void Airplane::operator delete(void *p,size_t size) { memPool.free(p, size); } // 为Airplane对象创建一个内存池, // 在类的实现文件里实现 Pool Airplane::memPool(sizeof(Airplane));