条款49 了解new-handler的行为
记住:
★set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用
★Nothrow new是一个颇为局限的工具,∵其只适用于内存分配;后继的构造函数调用还是可能抛出异常
-----------------------------------------------------------------------------------
new操作符私底下通过调用operator new来实现内存分配。当operator new抛出异常以反映一个未获满足的内存需求之前,它会调用一个客户指定的错误处理函数,一个所谓的new-handler。而客户是通过set_new_handler将自己的new-handler传递给它的,其声明在<new>标准库函数中:
namespace std{ typedef void (*new_handler)(); //函数指针声明!!! new_handler set_new_handler( new_handler p ) throw(); }
当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。
一个设计良好的new-handler函数必须做以下事情:
■ 让更多内存可被使用.
■ 安装另一个new-handler.
■ 卸除new-handler
■ 抛出bad_alloc(或派生自bad_alloc)的异常.
■ 不返回
这些选择让你在实现new-handler函数时拥有很大的弹性。而有时候你会希望依据不同的类,调用附属与该类的new-handler,看起来也许是这个样子:
struct TestA{ static void outOfMemory(); ... }; struct TestB{ static void outOfMemory(); ... }; TestA* a = new TestA; //if memory runs out,call TestA::outOfMemory TestB* b = new TestB; //if memory runs out,call TestB::outOfMemory
但C++不支持class专属之new-handlers。但可自己实现:
仅需令每一个class提供自己的set_new_handler和operator new即可:
■ 其中set_new_handler使客户得以指定class专属的new-handler(就像标准的set_new_handler允许客户指定global new-handler);
■ 而operator new则确保在分配class对象内存的过程中以class专属之new-handler替换global new-handler。
举例如下: 打算处理Widget class的内存分配失败情况:
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; //注意此为函数指针! };
std::new_handler Widget::currentHandler = 0; //静态成员初始化为null
Widget专属的set_new_handler函数会将它获得的指针存储起来,然后返回调用之前的指针,这也跟标准版set_new_hander的行为是一样的:
std::new_handler Widget::set_new_handler( std::new_handler p ) throw () { std::new_handler oldHandler = currentHandler; currentHandler = p; return oldHandler; }
为了实现Widget内的operator new,先构造一个资源处理类来操作new-handler,在构造过程中获得资源,在析构过程中释放:
class NewHandlerHolder { //这也是一种资源处理类 public: explicit NewHandlerHolder( std::new_handler nh ) :handler( nh ) {} //取得目前的new-handler ~NewHandlerHolder(){ //释放它 std::set_new_handler( handler ); } private: NewHandlerHolder(const NewHandlerHolder&); //阻止copying NewHandlerHolder& operator=(const NewHandlerHolder&); //阻止copying std::new_handler handler; };
这样Widget内的operator new的实现就变得特别简单:
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,这是由~NewHandlerHolder引起
客户如下使用:
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; //若内存分配失败,立刻抛异常(∵Widget并没有专属的new-handling函数)
更进一步,我们甚至可以用template base class将new-handler操作独立出来,让Widget继承自base class。这样就可以被任何有需要的class使用:
template<typename T> 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>::currentHandler = 0; //currentHandler初始化为null 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 ) ); //安装Widget的new-handler return ::operator new( size ); //分配内存或抛出异常 } //恢复global new-handler,这是由~NewHandlerHolder引起
有了此template,为刚才的Widget添加set_new_handler支持就轻而易举:
class Widget : public NewHandlerSupport<Widget> {
//和先前一样但此时不必声明set_new_handler或operator new
}
--------------------------------------
关于Nothrow new:
很久以前(卧槽,这么描述!!!)C++都还要求operator new在无法分配足够内存时返回null;而新一代的operator new则应该抛出bad_alloc异常,但很多程序都是在编译器开始支持新修规范前写出来的!C++标准委员会为了不想抛弃那些“侦测Null”的族群,于是提供另一形式的operator new,即nothrow形式:
class Widget { ... };
Widget *pw1 = new Widget; //这是现代C++做的:若分配失败,则抛出bad_alloc
if( pw1 == 0 ) ... //∴这个测试一定会失败!!!
Widget *pw2 = new (std::nothrow) Widget; //这是nothrow版,若分配失败则返回0
if( pw2 == 0 ) ... //这个测试可能成功
注:然而nothrow new对异常的强制保证性并不高!!!对表达式 new (std::nothrow) Widget来说发生两件事:
第一,nothrow版的operator new被调用,用以分配足够内存给Widget对象。若失败则返回null指针。若分配成功则
第二,接下来Widget的constructor会被调用,而在那一点上所有的筹码便都耗尽,∵Widget的constructor可以做其想做的任何事。其有可能又new一些内存,而没人可以强迫它再次使用nothrow new。∴虽“new (std::nothrow) Widget”调用的operator new并不抛掷异常,但Widget构造函数却可能会。若它真那么做,该异常会一如往常的传播。
结论:
使用nothrow new仅保证operaor new不抛掷异常,但不保证像new (std::nothrow) Widget这样的表达式不导致异常,∴其实你没有运用nothrow new的需要!
条款50 了解new和delete的合理替换时机
记住:
★有许多理由需要写个自定的new和delete,包括改善性能、对heap运用错误进行调试、收集heap使用信息。
-------------------------------------------------------------------
怎么会有人想要替换编译器提供的operator new或operator delete呢?可列出如下常见理由:
■ 用来检测运用上的错误
■ 为了强化效能
■ 为了收集使用上的统计数据
■ 为了增加分配和归还的速度
■ 为减低缺省内存管理器带来的空间额外开销
■ 为了弥补缺省分配器中的非最佳齐位
■ 为了将相关对象成簇集中
■ 为了获得非传统行为
条款51 编写new和delete时需固守常规
记住:
★operator new应该内含一个无穷循环,并在其中尝试分配内存,若它无法满足内存需求,就该调用new_handler。它也应该有能力处理0bytes申请。Class专属版本则还应该处理“比正确大小更大的(错误)申请”。
★operator delete应该在收到null指针时不做任何事。Class专属版本则还应该处理“比正确大小更大的(错误)申请”
---------------------------------------------------------------------------------------------------
a、自己实现一致性的operator new必须得返回正确的值,内存不足时调用new-handling函数,必须有对付零内存需求的准备,还需要避免不掩盖正常形式的new。
b、operator new的返回值十分单纯。如果它有能力供应客户申请的内存,就返回一个指针指向那块内存。如果没那个能力,则视情况抛出一个bad_alloc异常。
c、然而其实也不是很单纯,因为operator new实际上不只一次尝试分配内存,并在每次失败后调用new-handling函数。这里假设new-handling函数也许能够做某些动作将某些内存释放出来。只有当指向new-handling函数的指针是null,operator new才会抛出异常。
d、另要注意C++明文规定,即使客户要求0 bytes,operator new也得返回一个合法指针。
下面是个non-memeber operator new伪代码:
void* operator new( std::size_t size ) throw( std::bad_alloc ) //你自己的operator new可能接受额外的参数 { using namespace std; if(size == 0) //处理0bytes申请,将它视为1-bytes申请 { 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(); } }
这里是把0bytes申请视为1bytes申请量,看起来不爽,但合法、可行且客户调用的机会并不多。
你可能带着怀疑的目光看待这份代码,其中将new-handling函数指针设为null而后又立刻恢复原样。那是因为我们很不幸地没有任何办法可以直接取得new-handling函数指针,所以必须调用set_new_handler找出它来。拙劣,但有效——至少对单线程而言。多线程中可能需要加锁来安全处理new-handling函数背后的(global)数据结构。
operator new内含一个无限循环,退出此循环的唯一办法是:内存被成功分配或new-handling函数做了一件以下事情:让更多的内存可用、安装另一个new-handler、卸除new-handler、抛出bad_alloc异常(或其派生物),或承认失败而直接return。如果不那么做,operator new内的while循环永远不会结束。
----------------------------------
注意:当operator new成员函数(即成员函数版的operator new)被派生类继承时,会导致有趣的复杂度。上述operator伪码中,函数尝试分配size bytes(除非是0)。那非常合理,因为size是函数接受的实参。写出定制型内存管理器的一个常见理由是为针对特定类的对象分配行为提供最优化,却不是为了该类的任何派生类。也就是说,针对class X而设计的operator new,其行为很典型地只为大小刚好为szieof(X)的对象设计。然而一旦被继承下去,有可能基类的operator new被调用用以分配派生类对象:
class Base { public: static void* operator new(size_t size) throw(bad_alloc); ... }; class Drived : public Base //假设Drived未声明operator new {...}; Drived* p = new Drived; //这里调用的是Base::operator new
若Base 类专属的operator new并非被设计用来对付上述情况(实际往往也如此),处理此情势的最佳做法是将“内存申请量错误”的调用行为改采标准operator new像这样:
void* Base::operator new(size_t size) throw(bad_alloc) { if(size != sizeof(Base)) //大小错误 return ::operator new(size); //令标准的operator new起而处理 ... //否则在这里处理 }
你是不是认为上面的代码“忘了检验size等于0这种病态但是可能出现的情况!”是的,没检验,但是没有“但是”。测试依然存在,只不过它和上述的“size与sizeof(Base)的检测”融合在一起了。是的,C++在某种秘境中运行,而其中一个秘境就是它裁定所有非附属(独立式)对象必须有非零大小。因此sizeof(Base)无论如何不能为零,所以如果size是0,这份申请会被转交到::operator new手上,后者有责任以某种合理方式对待这份申请。
如果你打算控制类专属之“array内存分配行为”,那么你需要实现operator new的array兄弟版:operator new[]。这个函数通常被称为“array new”。如果你决定写个operator new[],记住,唯一需要做的一件事就是分配一块未加工内存(raw memory),因为你无法对array之内迄今尚未存在的元素对象做任何事情。实际上你甚至无法计算这个array将含多少个元素对象。首先你不知道每个对象的大小,毕竟基类的operator new[]有可能经由继承被调用,将内存分配给“元素为派生类对象”的array使用,显然,派生类对象通常比基类对象大。
因此,你不能在Base::operator new[]内假设array的每个元素对象的大小是sizeof(Base),这也意味着你不能假设array的元素个数是(bytes申请数/sizeof(Base))。此外,传递给operator new[]的size_t参数,其值有可能比“将被填以对象”的内存数量更多,因为动态分配的array可能包含额外空间用来存放元素个数。
-----------------------------------------
operator delete情况更简单,你需要记住的唯一事情就是C++保证“删除null指针永远安全”,所以必须兑现这项保证。下面是non-member operator delete的伪码:
void operator delete(void* rawMemory)throw { if(rawMemory == 0) return; //如果将被删除的是个null指针,那就什么都不做 现在,归还rawMemory所指的内存 }
此函数的member版也简单,只需要多加一个动作检查删除数量,万一你的类专属的operator new将大小有误的分配行为转交::operator new执行,你必须将大小有误的删除行为转交::operator delete执行:
class Base { public: static void* operator new( size_t size ) throw(bad_alloc); static void operator delete( void* rawMemory, size_t size ) throw(); ... }; void Base::operator delete( void* rawMemory, size_t size ) throw() { if(rawMemory == 0)return; //检查null指针 if(size != sizeof(Base)) //若大小错误,令标准版operator delete处理此一申请 { ::operator delete(rawMemory); return; } 现在,归还rawMemory所指向的内存; return; }
有趣的是,如果即将被删除的对象派生自某个基类而后者欠缺vitual析构函数,那么C++传给operator delete的size_t 数值可能不正确。这是“让你的基类拥有virtual析构函数”的一个够好的理由。你需要保持警觉,如果你的基类遗漏virtual析构函数,operator delete可能就无法正确运作。
条款52 写了placement new也要写placement delete
记住:
★当你写一个placement operator new,请确定也写出了对应的placement operator delete。若没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏
★当你声明placement new和placement delete,请确定不要无意识(非故意)地遮掩了它们的正常版本
--------------------------------------------------------------------------------------------
原文:http://www.cnblogs.com/hansonwang99/p/5025480.html