条款49:了解new_handler的行为
operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个用户指定的错误处理函数,一个所谓的new_handler。为了指定这个函数,客户必须调用set_new_handler:
namespace std{
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
}
该函数的参数是operator new无法满足内存分配时所需要调用的函数的指针,返回值是set_new_handler被调用前正在执行(但马上将被替换)的那个new_handler函数。
当operator new无法满足内存申请时,会不断调用new_handler函数,直到找到足够内存。因此设计一个良好的new_handler函数,必须做一下事情:
- 让更多内存可被使用:实现方法是,程序一开始就分配一大块内存,当new_handler第一次被调用时,将申请的内存分配给程序使用
- 安装另一个new_handler:如果当前new_handler无法取得足够内存,或许它知道另外哪个new_handler有此能力,就使用set_new_handler将当前new_handler替换掉。当下次operator new调用new_handler时,调用的就是新设置的那个new_handler。实现方法是,令new_handler修改会影响new_handler行为的static数据、namespace数据或global数据
- 卸除new_handler:将null指针传递给set_new_handler,一旦没有安装任何new_handler,operator new会在分配失败时抛出异常
- 抛出bad_alloc(或者派生自bad_alloc)的异常:这样的异常不会被new捕捉,因此会被传播到内存索求处
- 不返回:通常调用abort或exit
如果希望对不同的class内存分配失败以不同的方式处理,可令每一个class提供自己的set_new_handler以指定专属的new_handler,至于operator new则确保在分配class对象时,以class专属的new_handler替换global 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;
};
std::new_handler Widget::currentHandler=0;
Widget内的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将做以下事情:
- 调用标准set_new_handler,告知Widget的错误处理函数。这会将new_handler安装为标准的global new_handler
- 调用global operator new,执行实际的内存分配。如果分配失败,global operator new会调用Widget的new_handler。如果最终无法完成内存分配,会抛出一个bad_alloc异常。此情况下,Widget的operator new必须恢复原本的global new_handler,然后传播该异常
- 如果global operator new能够分配足够一个Widget对象所用的内存,Widget的operator new会返回一个指针,指向分配所得。Widget析构函数会管理global new_handler,它会自动将Widget's operator new被调用前的那个global new_handler恢复回来
Widget's operator new的实现:
void* Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandler)); //资源管理类
return ::operator new(size);
}
在此基础上,我们设计template 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>::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 template,为Widget添加set_new_handler支持能力就轻而易举了:只要令Widget继承自NewHandlerSupport<Widget>就好:
class Widget:public NewHandlerSupport<Widget>{
...
};
负责供应传统的”分配失败便返回null”行为的形式成为“nothrow”形式:
class Widget{...};
Widget *pw1=new Widget; //分配失败抛出bad_alloc
if(pw1==0)... //测试一定失败
Widget *pw2=new(std::nothrow)Widget; //分配失败返回0
if(pw2==0)... //测试可能成功
但nothrow仅仅能保证operator new不抛出异常,但是后续使用Widget构造函数时可能new一些内存,这些new操作符可能会抛出异常。由此可见,使用nothrow只能保证operator new不抛出异常,不保证像“new (std::nothrow) Widget”这样的表达式绝不导致异常。
请记住:
set_new_handler允许客户指定一个函数,在分配内存无法获得满足时被调用
Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后续的构造函数调用还是肯能抛出异常
条款51:编写new和delete时需固守常规
让我们从operator new开始,如果能提供申请的内存就返回一个指针指向分配的内存,如果没有能力,就抛出一个bad_alloc异常。operator new不止一次尝试内存分配,并在每次内存分配失败后调用new_handling函数。只有当指向new_handling函数的指针时null,operator new才会抛出异常。
C++规定即使用户要求0bytes,operator new也得返回一个指针:
void* operator new(std::size_t size)throw(bad_alloc)
{
using namespace std;
if(size==0) size=1;
while(true)
{
尝试分配 size bytes;
if(分配成功)
return (一个指针指向分配内存);
//分配失败;找出目前new_handling函数
new_handler globalHandler=set_new_handler(0);
set_new_handler(globalHadler);
if(globalHandler)(*globalHandler)();
else throw std::bad_alloc();
}
}
注意代码中将new_handling函数指针设为null而后又恢复原样,那是由于没有办法直观的取出new_handling函数指针,所以必须调用set_new_handler找出它。
条款49谈到operator new内含一个无限循环,上述伪代码明白表明出这个循环while(true),退出循环的唯一办法是:内存被成功分配或new_handling函数做了一件描述于条款49的事情:让更多内存可用、安装另一个new_handler、卸除new_handler、抛出bad_alloc异常,或是承认失败而直接return。现在可以了解,new_handler为什么必须做其中某些事,不然while循环永不结束。
注意到为base class而设计的operator new有可能被derived class误用:
class Base{
public:
static void* operator new(std::size_t size)throw(bad_alloc);
...
};
class Derived:public Base
{...};
Derived *p=new Derived; //这里调用的是base::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处理
...
}
上述代码中,将size等于0和size与sizeof(Base)的检测融合在一起了,因为它裁定所有非附属(独立式)对象必须有非零大小,因此sizeof(Base)无论如何不会为0,所以如果size=0,这就会被转交到::operator new手上。
当你打算撰写operator new[]时,你不知道每个对象多大,因为base class的operator new []有可能经由继承被调用,将内存分配给“元素为derived class对象”。以上就是编写operator new时,需要注意的问题。
对于operator delete的编写较为简单:
class Base{
public:
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); //大小错误,调用global operator delete
return;
}
//归还rawMemory所指内存
return;
}
请记住:
operator new内应含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就应该调用new_handler。它也应该有能力处理0 bytes申请。class的专属版本还应该处理“比正确大小更大的(错误)申请”
operator delete应该在收到null指针时,不作任何事情。class的专属版本还应该处理“比正确大小更大的(错误)申请”
条款52:写了placement new也要写placement delete
Widget *pw=new Widget;
上述代码中有两个函数被调用:一是内存分配的operator new,一个是Widget的默认构造函数。假设第一个函数调用成功,但第二个函数却抛出异常。那么第一部所分配的内存必须返还并且恢复初始状态,否则会造成内存泄漏。在这个时候用户没有能力返还内存,因为构造失败,pw尚未被赋值,客户手上就没有指针指向该被归还的内存,这一责任就落到了C++运行期系统身上。
如果使用正常形式的operator new,运行期系统可以找到对应的正常形式的operator delete以取消operator new的影响。但是当使用非正常形式的operator new,也就是带附加参数的operator new,究竟哪一个delete伴随这个new的问题就浮现了。
void* operator new(std::size_t size)throw(std::bad_alloc); //正常形式的new
void operator delete(void *rawMemory)throw(); //global作用域中的正常形式delete
void operator delete(void *rawMemory,std::size_t size)throw(); //class专属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 *pMemory,std::size_t size)throw(); //正常形式的class专属delete
...
};
如果operator new接受的参数除了一定会有的那个size_t之外还有其他,这边是所谓的placement new。上述operator new便是一个placement new,并且是一个特殊的placement new。它接受一个指针指向对象该被构造之处。一般谈到placement new都是指的这一特殊placement new。
Widget *pw=new (std::cerr)Widget; //调用operator new,并将cerr作为ostream实参;这会导致在构造函数抛出异常时泄露内存
当上式构造函数抛出异常时,运行期系统寻找参数个数和类型都与operator new相同的某个operator delete来恢复operator new所造成的影响。上述式应寻找接受ostream&额外实参的delete:
void operator delete(void*,std::ostream)throw();
类似于new的placement版本,delete如果接受额外实参便成为placement delete。现在Widget没有声明placement版本的operator delete,所以运行期系统不知道如何恢复,于是什么也不做。
因此写了placement new也要写一个placement delete:
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自动被调用,不会泄露内存。然而,如果没有抛出异常,编写以下代码时会调用正常版本的delete:
delete pw;
可见,placement delete只有在伴随placement new调用而触发的构造函数出现异常时才会被调用。对一个指针试行delete绝不会导致调用placement delete。
这意味着我们必须同时提供一个正常的operator delete(用于构造期间无任何异常抛出)和一个placement版本(用于构造期间有异常抛出)。placement版本的额外参数必须与operator new一样。
但是这会导致成员函数名称遮盖外围作用域中相同名称(条款33),必须避免class专属new遮盖其他new:
class Base{
public:
static void* operator new(std::size_t size,std::ostream &logStream)throw(std::bad_alloc); //会遮掩global new
...
};
Base *pb=new Base; //错误,正常形式的new被遮盖
Base *pb=new(std::cerr)Base; //正确,调用Base的placement new
同样,derived class的operator new会遮盖global和base class中的operator new:
class Derived:public Base{
public:
static void* operator new(std::size_t size)throw(std::bad_alloc);
...
};
Derived *pd=new Derived; //正确,调用derived的operator new
Derived *pd=new (std::cerr)Derived; //错误,Base的placement new被遮盖了
条款33更详细的描述了名字遮盖问题,C++在global作用域提供以下形式的operator new:
void* operator new(std::size_t)throw(std::bad_alloc);
void* operator new(std::size_t,void*)throw();
void* operator new(std::size_t,const std::nothrow_t&)throw();
在自定义的class中声明任何的operator new都会遮盖上述标准形式,解决办法是:将标准形式定义在一个base class中,令含自定义operator new的class继承该base class,并使用using声明使标准形式可见:
class StandardNewDeleteForms{
public:
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);}
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);}
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);}
};
class Widget:public StandardNewDeleteForms{
public:
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete;
static void* operator new(std::size_t size,std::ostream &logStream)throw(std::bad_alloc);
static void operator delete(void *pMemory,std::ostream &logStream)throw();
...
};
请记住:
当你写一个placement operator new,请你确保也写出了对应的placement operator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏
当你声明placement new和placement delete,请不要无意识(非故意)地遮盖了它们的正常版本
/* 至此effective C++就完结了,后面三条是偏向于构架,自身层次不够就不撰写了,又吸收了大牛的著作,还是很开心 */