条款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,请确定不要无意识(非故意)地遮掩了它们的正常版本
--------------------------------------------------------------------------------------------
placement new和placement delete是C++常用到但却不常见的两个操作符。当我们使用new创建一个对象时:
Widget* pw=new Widget;
有两函数被调用:
第一个函数就是operator new用以分配内存;
第二个是Widget的default构造函数。
若第一个函数调用成功,但第二个函数调用失败,这时需要释放第一步开辟的内存,否则就造成了内存泄露。这个时候,客户无能力去归还内存,因为如果Widget构造函数抛出异常,那么pw尚未被赋值,客户手中的指针还没有指向开辟的内存。这时释放内存的任务落到了C++运行期系统身上。运行期系统会调用第一个函数operator new所对应的operator delete版本,前提当然是它必须知道哪一个operator delete该被调用(因为operator delete可能有多个版本)。
若目前面对的是正常的new和delete,这不是问题。正常的operator new和对应的operator delete:
void* operator new( std::size_t ) throw( std::bad_alloc ); //正常的operator new
void operator delete( void* rawMemory ) throw(); //global作用域中的正常签名式
void operator delete( void* rawMemory, std::size_t size ) throw(); //class作用域中典型的签名式
若使用正常的operator new和operator delete,运行期系统可以找到如何释放new开辟内存的delete函数。但是若使用非正常形式的operator new,究竟使用哪个delete就会有问题了。
举例说明:
假设写一个class专属operator new,要接收一个ostream,用来志记相关分配信息,同时又写了一个正常形式的class专属operator delete:
class Widget{ public: …… static void* operator new(std::size_t size, std::ostream& logStream) //非正常形式的new throw(std::bad_alloc); //这其实是个placement版本!!! static void operator delete(void* pMemory, std::size_t size) //正常的class专属delete throw(); …… };
此设计有问题!!!但在讨论问题前,先说明一个术语:
operator new接受的参数除了必有的size_t之外还有其他,这便是placement new。∴上述的operator new是个placement版本。
众多placement new版本中,有一个特别有用的版本是“接受一个指针指向对象该被构造之处”,这个operator new形式如下:
void* operator new( std::size_t, void* pMemory ) throw(); //placement new
此版new已被纳入C++标准库,在#include <new>中。当谈到placement new时,大多时候特指这一特定版本,即还有额外实参void*。但有一点比较普遍,一般性术语“placement new”意味着带有额外参数的new,对应有另一个术语placement delete。
现回到刚Widget class的声明式,这个class会引起内存泄露!!!
例如动态创建一个Widget时将相关的分配信息志记于cerr:
Widget* pw=new (std:cerr) Widget; //调用operator new并传递cerr为ostream实
//参;此动作会在Widget构造函数抛出异常时泄漏内存!!!
若内存分配成功,但Widget构造函数抛出异常,运行期系统要释放operator new开辟的内存。但是运行期系统不知道真正被调用的operator new如何运作。运行期系统试图寻找参数个数和类型都与operator new相同的operator delete。这里对应的operator delete应为:
void operator delete( void*, std::ostream& ) throw();
但上面Widget并没有placement版本的operator delete,所以运行期系统不知道如何释放operator new开辟的内存!!!
∴Widget class应定义如下:
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* pMemory, std::ostream& logStream) throw(); //也应声明一个placement版delete!! };
Widget* pw=new (std:cerr) Widget; //一如既往,但这次不再泄漏
这样若Widget constructor抛异常,就会调用对应版的placement delete,确保无泄露内存。
而若没有异常,像正常的这样调用(这也是通常的情况):
delete pw; //调用正常的operator new!
记住:placement delete只有在“伴随placement new调用而触发的构造函数”抛出异常时才会被调用!!!对着一个指针(如上述pw)施行delete绝不会导致调用placement delete!!!
--------------------------------------------------------
需注意的是,因为成员函数的名称会掩盖其外围作用域中相同名称的函数,∴要小心避免class专属的new掩盖客户所希望调用的new。
如,你有一个base class,其中声明唯一一个placement operator new
class Base{ public: …… static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc); //此new会掩盖正常的global形式的new …… };
Base* pb=new Base; //错误!因为正常形式的operator new被掩盖
Base* pb1=new (std::cerr) Base; //正确:调用Base的placement new
同样,derived class中的operator new会掩盖继承而来的operator new和global版的new:
class Derived: public 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* pd1 = new Derived; //正确
缺省情况下,C++在global作用域内提供以下形式的operator new:
void* operator( std::size_t ) throw(std::bad_alloc); //normal new
void* operator( std::size_t, void* ) throw(); //即上述那个常用的placement new版
void* operator( std::size_t, const std::nothrow_t& ) throw(); //nothrow new
在class内声明任何形式的operator new都会掩盖上面这些标准形式。对于每一个可用的operator new,要确保提供了对应形式的operator delete。
一个简单的做法是建立一个base class,内含所有正常形式的new和delete:
class StadardNewDeleteForms { public: //normal版 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(pMemory); } //placement版 static void* operator new( std::size_t size, void* ptr ) throw( std::bad_alloc ) { return ::operator new( size, ptr ); } static void operator delete( void* pMemory, void* ptr ) throw() { ::operator delete( pMemory, ptr ); } //nothrow版 static void* operator new( std::size_t size, const std::nothrow_t& nt) throw(std::bad_alloc) { return ::operator new( size,nt ); } static void operator delete( void* pMemory, const std::nothrow_t& ) throw() { ::operator delete( pMemory ); } };
这时若想以自定义方式扩充标准形式,可以使用继承机制和using声明:
class Widget : public StandardNewDeleteForms{ public: //让这些形式可见 using StandardNewDeleteForms::operator new; using StandardNewDeleteForms::operator delete; //添加自己定义的placement版本的new和delete static void* operator new(std::size_t size, std::ostream& logStream) throw(std:;bad_alloc); static void operator detele( std::size_t size, std::ostream& logStream ) throw(); };