条款49:了解new-handler的行为
当operator new 抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误函数,一个所谓的new-handler。
代码示例如下:
namespace std {
typedef void (*new_handler) ();
new_handler set_new_handler(new_handler p) throw();
}
set_new_handler 的参数是个指针,指向operator new无法分配足够内存时该被调用的函数。其返回值也是个指针,指向set_new_handler 被调用前正在执行(但马上就要被替换)的那个new-handler函数。
你可以这样使用set_new_handler:
//以下是当operator new 无法分配足够内存时,该被调用的函数
void outOfMem(){
std::cerr<<"unable to satisfy request for memory \n";
std::abort();
}
int main()
{
std::set_new_handler(outOfMem);
int * pBigDataArray = new int[100000000L]l
//...
}
一个设计良好的new-handler函数必须做以下事情:
1、让更多内存可被使用。
2、安装另一个new-handler。
3、卸除new-handler。
4、抛出bad_alloc(或派生自bad_alloc)的异常。
5、不返回,通常调用abort或exit 。
有时候你或许希望以不同的方式处理内存分配失败情况,你希望视被分配物属于哪个class而定:
class X
{
public:
static void outOfMemory();
//...
};
class Y
{
public:
static void outOfMemory();
//...
};
X * p1 =new X;//如果分配不成功,调用X::outOfMemory
Y * p2 =new Y;//如果分配不成功,调用Y::outOfMemory
C++ 并不支持class 专属之new-handlers ,但其实也不需要。
现在,假设你打算处理Widget class 的内存分配失败情况。首先你必须登录“ 当operator new 无法为一个Widget 对象分配足够内存时” 调用的函数,所以你需要声明一个类型为new_handler 的static 成员,用以指向class Widget 的new-handler 。看起来像这样:
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成员必须在class定义式之外被定义(除非它们是const 而且是整数型,见条款2),所以需要这么写:
std::new_handler Widget::currentHandler= 0;//在class 实现文件内初始化为null
Widget内的set_new_handler 函数会将它获得的指针存储起来,然后返回先前(在此调用之前)存储的指针,这也正是标准版 set_new_handler 的行为:
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 的错误处理函数。
2、调用global operator new ,执行实际之内存分配。
3、如果global operator new 能够分配足够一个Widget 对象所用的内存,Widget 的operator new 会返回一个指针,指向分配所得。
下面代码阐述一遍。我将从资源处理类开始,那里面只有基础性RAII操作,在构造过程中获得一笔资源,并在析构过程中释还:
class NewHandlerHolder
{
public:
explicit NewHandlerHolder(std::new_handler nh):handler(nh){}//取得目前的new-handler
~NewHandlerHolder()
{
std::set_new_handler(handler);//释放它
}
private:
std::new_handler handler;//记录下来
NewHandlerHolder(const NewHandlerHolder&);//阻止copying
NewHandlerHolder& operator=(const NewHandlerHolder&);
};
这就使得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
}
Widget的客户应该类似这样适用其new-handler:
void outOfMem();//函数声明,此函数在Widget对象分配失败时被调用
Widget::set_new_handler(outOfMem);//设定outOfMem为Widget的new-handler函数
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函数)
实现这一方案的代码并不因class的不同而不同,因此在它处加以复用是个合理的构想。
下面的代码的类可以被任何有所需要的class使用:
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);
//... 其它的operator new版本——见条款52
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)
{
NewHandlerSupport h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
//以下将每一个currentHandler 初始化为null
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0 ;
有了这个class template ,为Widget 添加set_new_handler 支持能力就轻而易举了:只要令Widget 继承自NewHandlerSupport< Widget > 就好,像下面这样。看起来似乎很奇妙:
class Widget:public NewHandlerSupport<Widget>
{
//...和先前一样,但不必声明
//set_new_handler 或operator new
};
这就是Widget 为了提供“class 专属之set_new_handler”所需要做的全部动作。
另一个形式的operator new ,负责供应传统的 “分配失败便返回null行为 ”。这个形式被称为“ nothrow ”形式——某种程度上是因为他们在new 的使用场合用了 nothrow对象(定义于头文件< new >):
class Widge
{
//...和先前一样,但不必声明
//set_new_handler 或operator new
};
Widget *pw1 =new Widget;//如果分配失败,抛出bad_alloc
if(pw1 == 0)
{
//... //这个测试一定失败
}
Widget *pw2 =new (std::nothrow)Widget;//如果分配Widget失败,返回0
if(pw2 == 0)
{
//... //这个测试可能成功
}
Nothrow new 对异常的强制保证性并不高。
使用nothrow new 只能保证operator new 不抛掷异常,不保证像“ new (std::nothrow) Widget ”这样的表达式绝不导致异常. 因此你其实没有运用nothrow new的需要。
请记住:
1、set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
2、Nothrow new 是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常。
条款50:了解new 和delete 的合理替换时机
想要替换编译器提供的operator new 或operator delete ,一般有以下三个理由:
1、用来检测运用上的错误。
2、为了强化效能。
3、为了收集使用上的统计数据。
下面的代码是个快递发展得出的初阶段global operator new ,促进并协助检测“overruns ”(写入点在分配区块尾端之后)或“ underruns”(写入点在分配区块起点之前。)
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);//增加大小,使能够塞入两个signature
void *pMem = malloc(realSize);// 调用malloc 取得内存
if(!pMem) throw bad_alloc();
//将 signature写入内存的最前段落和最后段落
*(static_cast<int*>(pMem)) =signature;
*(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof (int))) = signature;
//返回指针,指向恰位于第一个 signature 之后的内存位置
return static_cast<Byte*>(pMem) +sizeof (int);
}
这个operator new的缺点主要在于它疏忽了身为这个特殊函数所应该具备的“坚持C++规则”的态度。
齐位的意义:C++要求所有operator new 返回的指针都有适当的对齐(取决于数据类型)。malloc 就是在这样的要求下工作,所以令operator new 返回一个得自malloc 的指针是安全的。然而上述operator new 中我并未返回一个得自malloc 的指针,而是返回一个得自malloc 且偏移一个int 大小的指针。如果客户端调用 operator new 企图获取足够给一个double 所用的内存(或者如果我们写个operator new[] ,元素类型是double ),而我们在一部“int 为4bytes 且double 必须8bytes齐位”的机器上跑,我们可能获得一个未有适当齐位的指针。那可能会造成程序崩溃或执行速度变慢。
了解何时可在“全局性”或“class专属”基础上合理替换缺省的new 和delete。有如下用途:
1、用来检测运用上的错误。
2、为了收集动态分配内存之使用统计信息。
3、为了增加分配和归还的速度。
4、为了降低缺省内存管理器带来的空间额外开销。
5、为了弥补缺省分配器中的最佳齐位。
6、为了将相关对象成簇集中。
7、为了获得非传统的行为。
请记住:
1、有许多理由需要写个自定的new 和 delete ,包括改善效能、对heap运用错误进行调试、收集heap 使用信息。
条款51:编写new 和delete 时需要固守常规
奇怪的C++规定,即使客户要求0 bytes , operator new 也得返回一个合法指针。这种看似诡异的行为其实是为了简化语言其它部分。下面是个non-member operator new 伪码(pseudocode):
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();
}
}
}
这里的技俩是把0 bytes申请量视为1 byte 申请量。
下面的代码,base class 的operator new 被调用用以分配derived class对象:
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
//如果Base class 专属的operator new 并非被设计用来对付上述情况(实际上往往如此),
//处理此情势的最佳做法是将“内存申请量错误”的调用行为改采标准operator new,像这样
void *Base::operator new(std::size_t size) throw(std::bad_alloc)
{
if(size ! = sizeof (Base))//如果大小错误
return ::operator new(size);//令标准的operator new 起而处理
//。。。 //否则在这里处理。
}
C++裁定所有非附属(独立式)对象必须有非零大小(见条款39).因此sizeof(Base) 无论如何不能为零,所以如果size 是0,这份申请会被转交到 ::operator new 手上,后者有责任以某种合理方式对待这份申请。
【这里所谓非附属/独立式对象,指的是不以“某对象之base class成分”存在的对象】
如果你决定写个operator new [ ] ,唯一要做的事情的一件事就是分配一块未加工内存,因为你无法对array 之内迄今尚未存在的元素对象做任何事情。
动态分配的arrays 可能包含额外空间用来存放元素个数。
operator new 的要求是保证“删除null 指针永远安全”,下面是non-member operator delete 的伪码:
void operator delete (void *rawMemory) throw()
{
if(rawMemory == 0)//如果被删除的是个Null指针
return;// 那就什么都不做
//现在归还rawMemory所指的内存
}
多加一个动作检测删除数量。万一你的class 专属的operator new 将大小有误的分配行为转交 :: operator new执行,你必须将大小有误的删除行为转交 :: operator delete :
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(std::bad_alloc);
};
void Base::operator delete(void *rawMemory,std::size_t size) throw(std::bad_alloc);
{
if(rawMemory == 0)//如果被删除的是个Null指针
return;// 那就什么都不做
if(size != sizeof(Base))//如果大小错误,令标准版operator delete 处理此一申请
{
::operator delete(rawMemory);
return;
}
//现在,归还rawMemory所指的内存
return;
}
请记住:
1、operator new 应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler .。它应该有能力处理0 bytes申请。class 专属版本则还应该处理“比正确大小更大的(错误申请)”。
2、operator delete 应该在收到null 指针时不做任何事情,class专属版本还应该处理“比正确大小更大的(错误申请)”。
条款52:写了placement new也要写placement delete
当你写一个new 表达式像这样:
Widget* pw = new Widget;
有两个函数被调用:一个是用以分配内存的operator new , 一个是Widget 的 default 构造函数。
如果目前面对的是拥有正常签名式的new和delete ,这并不是问题,因为正常的operator new:
void * operator new(std::size_t) throw(std::bad_alloc);
对于正常的operator delete:
void operator delete(void *rawMemory) throw();//global 作用域中的正常签名式
void operator delete(void *rawMemory,std::size_t size) throw();//class 作用域中典型的签名式
假设你写了一个class专属的operator new ,要求接受一个ostream , 用来志记(logged)相关分配信息,同时又写了一个正常形式的class 专属operator delete :
class Widget
{
public:
static void * operator new(std::size_t size,std::ostream & logStream) throw(std::bad_alloc);
//非正常形式的new
static void operator delete(void *rawMemory,std::size_t size) throw();//正常的class 专属delete
//...
};
下面是一个placement版本“ 接受一个指针指向对象该被构造之处”,具体代码如下:
void * operator new(std::size_t,void *pMemory) throw();//placement new
这个版本的new属于C++ 标准库。
下面代码是,在动态创建一个Widget时将相关的分配信息志记(logs)于cerr:
Widget* pw = new (std::cerr)Widget;//调用operator new并传递cerr为其ostream 实参,这个动作会在Widget
//构造函数抛出异常时泄漏内存
如果内存分配成功,而Wdget构造函数抛出异常,运行期系统有责任取消operator new的分配并恢复旧观。
由于上面的operator new接受类型为ostream& 的额外实参,所以
下面是一个对应的operator delete:
void operator delete(void * ,std::ostream&) throw();
如果一个带额外参数的operator new 没有“带相同额外参数” 的对应版 operator delete ,那么当new 的内存分配动作需要取消并恢复旧观时就没有任何operator delete 会被调用。为了消除稍早代码中的内存泄漏,Widget 有必要声明一个 placement delete ,对应于那个有志记功能的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 *pMemory,std::ostream & logStream) throw();
//...
};
这样改变之后,如果以下语句引发Widget构造函数抛出异常:
Widget* pw = new (std::cerr)Widget;//一如既往,但这次不再泄漏
对应的placement delete会被自动调用,让Widget有机会确保不泄漏内存任何内存。
然而如果没有抛出异常,而客户代码中有个对应的delete ,会发生什么事情。
delete pw;//调用正常的operator delete
假设你有一个base class ,其中声明唯一一个placement operator new,客户端会发现他们无法使用正常形式的new :
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
同样道理,derived classes中的operator new会掩盖global 版本和继承而得的operator 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* pd = new Derived;//没问题,调用Derived 的operator new
对于撰写内存分配函数,你需要记住的是,缺省情况下C++在global 作用域内提供以下形式的operator new:
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 见条款49
如果你在class 内声明任何operator new , 它会遮掩上述这些标准形式。
建立一个base class ,内含所有正常形式的new 和delete :
class StandardNewDeleteForms
{
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(pMemory);
}
//placement new/delete
static void * operator new(std::size_t size,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()
{
return ::operator delete(pMemory);
}
}
凡是想以自定形式扩充标准形式的客户,可利用继承机制及using 声明式取得标准形式:
class Widget:public StandardNewDeleteForms //继承标准形式
{
public:
using StandardNewDeleteForms::operator new;//让这些形式可见
using StandardNewDeleteForms::operator delete;//让这些形式可见
static void * operator new(std::size_t size, //添加一个自定的placement new
std::ostream & logStream) throw(std::bad_alloc);
static void operator delete(void *pMemory,//添加一个对应的placement delete
std::ostream & logStream) throw();
//...
};
请记住:
1、当你写一个placement operator new ,请确定也写出了对应的placement operator delete 。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏。
2、当你声明placement new和placement delete ,请确定不要无意识(非故意)地遮掩了它们的正常版本。