八、定制new和delete
(49)、了解new-handler的行为
当operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理程序,一个所谓的new-handler。为了指定这样“用以处理内存不足”的函数,客户必须调用set_new_handler,那是声明于<new>的个标准程序库函数:
namespace std {
typedef void (*new handler)();
new_hander set_new_handler(new_handler p) throw();
}
你可以这样使用set_new_handler:
void outOfMem()
{
std::cerr << "Unable to satisfy request for memory\n";
std::abort();
}
int main()
{
std::set_new_handler(outOfMem);
int* pBigDataArray = new int[1000000000000L];
...
}
当operator new无法为1000000000000整数分配足够空间时,outOfMem会被调用。
当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。引起反复调用的代码显示于条款51,这里的高级描述已足够获得一个结论,那就是一个设计良好的new-handler函数必须做以下事情:
让更多内存可被使用。这便造成operator new内的下一次内存分配动作可能成功。实现此策略的做法是,程序一开始执行就分配一大块内存,而后当new-handler第一次被调用,将它们释还给程序使用。
安装另一个new-handler。如果目前这个new-handler无法取得更多可用内存,或许它知道另外哪个new-handler有此能力。
卸除new-handler,也就是将null指针传给set_new_handler。一旦没有安装任何new-handler,operator new会在内存分配不成功时抛出异常。
抛出bad_alloc(或派生自bad_alloc)的异常。
不返回,通常调用abort或exit。
在类中可用定义class专属的new-handler替换global new-handler。
下面以Widget为例说明:
class Widget {
public:
static std::new_handler set_new_handler(std::new_handler p) throw();
static void* operator new(std::size_t size) throw(std::bad_alloc);
private:
static std::new_handler currentHandler;
}
static std::new_handler Widget::currentHandler = 0; //在class实现文件内初始化为null
std::new_handler Widget::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
最后,Widget的operator new做以下事情:
1.调用标准set_new_handler,告知Widget的错误处理函数。这会将Widget的new-handler安装为global new-handler。
2.调用global operator new,执行实际之内存分配。如果分配失败,global operator new会调用Widget的new-handler,因为那个函数才刚被安装为global new-handler。如果global operator new最终无法分配足够内存,会抛出一个bad_alloc异常。在此情况下Widget的operator new必须恢复原本的global new-handler,然后再传播该异常。为确保原来的new-handler总是能够被重新安装回去,Widget将global new-handler视为资源并遵守条款13的忠告,运用资源管理对象(resource-managing objects)防止资源泄露。
3.如果global operator new能够分配足够一个Widget对象所用的内存,Widget的operator new会返回一个指针,指向分配所得。Widget析构函数会管理global new-handler,它会自动将Widget的operator new被调用前的那个global new-handler恢复回来。
如下:
class NewHandlerHolder {
public:
explicit NewHandlerHolder(std::new_handler nh) //取得目前的new-handler
: handler(nh) {}
~NewHandlerHolder()
{ std::set_new_handler(handler); } //释放它
private:
std::new_handler handler; //记录下来
NewHandlerHolder(const NewHandlerHolder&); //阻止copying
NewHandlerHolder& operator=(const NewHandlerHolder&);
};
void* Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandler)); //安装Widget的new-handler
return ::operator new(size); //分配内存或抛出异常
} //恢复global new-handler
Widget的客户应该类似这样使用其new-handling:
void outOfMem(); //函数声明。此函数在Widget对象分配失败时调用
Widget::set_new_handler(outOfMem); //设定outOfMem为Widget的new-handling函数
Widget* pw1 = new Widget; //如果内存分配失败,调用outOfMem
std::string* ps = new std::string; //如果内存分配失败,调用global new-handling函数(如果有的话)
Widget::set_new_handler(0); //设定Widget专属的new-handling函数为null
Widget* pw2 = new Widget; //如果内存分配失败,立刻抛出异常(class Widget并没有专属的new-handling //函数)
可以改写为"mixin"的base calss供继承,可以使用template。
template<typename T> //“mixin”风格的base class,用以支持class专属的set_new_handler
class NewHandlerSupport {
public:
static std::new_handler set_new_handler(std::new_handler p) throw();
static void* operator new(std::size_t size) throw(std::bad_alloc);
...
private:
static std::new_handler currentHandler;
};
template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
template<typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size)
throw(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;
可以向下面这样使用:
class Widget: public NewHandlerSupport<Widget> {
... //和先前一样,但不必声明set_new_handler或operator new
};
直至1993年,C++都还要求operator new必须在无法分配足够内存时返回null。新一代的operator new则应抛出bad_alloc异常。为了不想抛弃“侦测null”的族群,提供了另一形式的operator new,使用“nothrow”形式。
class Widget { ... };
Widget *pw1 = new Widegt; //如果分配失败,抛出bad_alloc
if(pw1 == 0) ... //这个测试一定失败
Widget *pw2 = new (std::nothrow)Widegt; //如果分配Widget失败,返回0
if(pw2 == 0) ... //这个测试可能成功
请记住:
set_new_handler允许客户指定一个函数,在内存无法获得满足时被调用。
Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常。
(50)、了解new和delete的合理替换时机
替换编译器提供的operator new和operator delete有下面三个最常见的理由:
用来检测运用上的错误。
为了强化效果。
为了收集使用上的统计数据。
以齐位为了展开。
本条款的主题是,了解何时可在“全局性”或“class专属的”基础上合理替换缺省的new和delete。挖掘更多细节之前,让我先对答案做一些摘要:
为了检测运用错误。
为了收集动态分配内存之使用统计信息。
为了增加分配和归还的速度。
为了降低缺省内存管理区带来的空间额外开销。
为了弥补缺省分配器中的非最佳齐位(suboptimal alignment)。
为了将相关对象成簇集中。
为了获得非传统的行为。
请记住:
有许多理由编写自定的new和delete,包括改善性能、对heap运用错误进行调试、收集heap使用信息。
(51)、编写new和delete时需固守常规
operator new的返回值十分单纯。如果它有能力供应客户申请的内存,就返回一个指针指向那块内存。如果没有那个能力,就遵循条款49描述的规则,并抛出一个bad_alloc异常。
其实并不单纯,operator new实际上不只异常尝试分配内存,并在每次失败后调用new-handling函数。这里假设new-handling函数也许能够做某些动作将内存释放出来。只有当指向new-handling函数的指针是null,operator new才会抛出异常。
奇怪的是C++规定,即使客户要求0bytes,operator new也得返回一个合法指针。下面是个non-member operator new伪码(pesudocide):
void* operator new(std::size_t size) throw(std::bad_alloc)
{ //你的operator new可能接受额外参数
using namespace std;
if(size == 0) //处理0-byte申请,将它视为1-byte申请
{
size = 1;
}
while(true)
{
尝试分配size bytes;
if(分配成功)
return (一个指针,指向分配得来的内存);
//分配失败;找出目前的new-handling函数
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if(globalHandler) (*globalHandler)();
else throw std::bad_alloc();
}
}
这里的伎俩是把0byte申请量视为1byte申请量。
你也许带着怀疑的眼光斜睨这份伪码(pseudocode),因为其中将new-handling函数指针设为null而后又立刻恢复原样。那是因为我们很不幸地没有任何办法可以直接取得new-handling函数指针,所有必须调用set_new_handler找出它来。拙劣,但有效——至少对单线程程序而言。若在多线程环境中你或许需要某种机锁(lock)以便安全处置new-handling函数背后的(global)数据结构。
条款49谈到operator new内含一个无穷循环,而上述伪码表面这个循环;“while(truw)”就是那个无穷循环。退出此循环的唯一办法是:内存被成功分配或new-handling函数做了一件描述于条款49的事情:让更多内存可用、安装另一个new-handler、卸除new-handler、抛出bad_alloc异常(或派生物),或是承认失败而直接return。现在,对应new-handler为什么必须做出其中某些事你应该很清楚了。如果不那么做,operator new内的while循环永远不会接受。
class Base {
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
...
};
class Derived:public Base //假设Derived未声明operator new
{ ... };
Derived* p = new Derived; //这里调用的是Base::operator new
对付上面的最好办法如下:
void* Base::operator new(std::size_t size) throw(std::bad_alloc)
{
if(size != sizeof(Base)) //如果大小错误,令标准的operator new起而处理。
return ::operator new(size);
... //否则在这里处理。
}
看似这段代码没有检验size等于0,不是没有检验而是将检验与sizeof(Base)融合一起了。
如果你决定写个operator new[],记住,唯一需要做的一件事情是分配一块未加工内存。
operator delete情况更简单,你需要记住的唯一事情就是C++保证“删除null指针拥有安全”,所以你必须兑现这项保证。下面是non-member operator delete的伪码(pseudocode):
void operator delete(void * rawMemory) throw()
{
if(rawMemory == 0) return; //如果将被删除的是个null指针,那就什么都不做
现在,归还rwaMemory所指的内存;
}
class Base {
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
static void operator delete(void* rawMemory,std::size_t size) throw();
...
};
void Base::operator delete(void* rawMemory,std::size_t size) throw()
{
if(rawMemory == 0) return;
if(size != sizeof(Base))
{
::operator delete(rawMemory);
return;
}
现在,归还rawMemory所指的内存;
return;
}
有趣的是,如果即将被删除的对象派生自某个base class而后者欠缺virtual析构函数,那么C++传给operator delete的size_t数值可能步正确。这是“让你的base class拥有virtual析构函数”的一个够好的理由;条款7还提到一个更好的理由。
请记住:
operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0byte申请。Class专属版本则还应该处理“比正确大小更大的(错误)申请”。
operator delete应该在收到null指针时不做任何事。Class专属版本则还应该处理“比正确大小更大的(错误)申请”。
(52)、写了placement new也要写placement delete
如果一个带额外参数的operator new没有“带相同额外参数”的对应版operator delete,那么当new的内存分配动作需要取消并恢复旧观时就没有任何operator delete会被调用。因此,为了消弭稍早代码中的内存泄露,Widget有必要声明一个placement delete,对应于那个有志记功能(logging)的placement new:
class Widget {
public:
...
static void* operator new(std::size_t size,std::ostream& logStream) throw(std::bad_alloc);
static void operator delete(void* pMemory) throw();
static void operator delete(void* pMenory,std::ostream& logStream) throw();
...
};
palcement delete只有在“伴随placement new调用而触发的构造函数”出现异常时才会被调用。对着一个指针施行delete绝不会导致调用placement delete。
这意味如果要对所有与placement new相关的内存泄露宣战,我们必须同时提供一个正常的operator delete(用于构造期间无异常抛出)和一个placement版本(用于构造期间有异常抛出)。
附带一提,由于成员函数的名称会掩盖其外围作用域中的相同名称,你必须小心避免让class专属的news掩盖客户期望的其他news(包括正常版本)。
class Base {
public:
...
static void* operator new(std::size_t size,std::ostream& logStream) throw(std::bad_alloc); //这个new会掩盖
... //正常的global形式
};
Base* pb = new Base; //错误!因为正常形式的operator new被掩盖
Base* pb = new (std::cerr)Base; //正确,调用Base的placement new
class Derived : public Base { //继承自先前的Base
public:
...
static void* operator new(std::size_t size) throw(std::bad_alloc) //重新声明正常形式的new
...
};
Derived* pd = new (std::clog)Derived; //错误!因为Base的placement new被掩盖了。
Derived* pd = new Dericed; //没问题,调用Derived的operator new
如果你在class内声明任何operator news,它会掩盖下述这些标准形式。
void* operator new(std::size_t) throw(std::bad_alloc); //normal new
void* operator new(std::size_t,void*) throw(); //placement new
void* operator new(std::size_t,const std::nothrow_t&) throw(); //nothrow new
下面是建立标准的没有故意掩盖的new和delete:
class StandardNewDelelteForms {
public:
//normal new/delete
static void* operator new(std::size_t size) throw(std::bad_alloc)
{ return ::operator new(size); }
static void operator delete(void* pMemory) throw()
{ ::operator delete(pMenory); }
// placement new/delete
static void* operator new(std::size_t,void* ptr) throw()
{ return ::operator new(size,ptr); }
static void operator delete(void* pMemory,void* ptr) throw()
{ return ::operator delete(pMemory,ptr); }
//nothrow new/delete
static void* operator new(std::size_t size,const std::nothrow_t& nt) throw()
{ return ::operator new(size,nt); }
static void operator delete(void *pMemory,const std::nothrow_t&) throw()
{ ::operator delete(pMemory); }
};
凡是想以自定形式扩充标准形式的客户,可利用继承机制及using声明式取得标准形式:
class Widget:public StandardNewDeleteFroms { //继承标准形式
public:
using StandardNewDeleteFroms::operator new; //让这些形式可见
using StandardNewDeleteFroms::operator delete;
static void* operator new(std::size_t size,std::ostream& logStream) throw(std::bad_alloc) //添加一个自定的
//placement new
static void operator delete(void* pMemory,std::ostream& logStream) throw(); //添加一个对应的plaement //delete
...
}
请记住:
当你写一个placement operator new,请确定也写出了对应的placement operator delete。如果没有这样做,你的程序可能会发生隐微而时段时续的内存泄露。
当你声明placement new和placement delete,请确定不要无意识(非故意)地掩盖了它们的正常版本。