最近看代码看到nothrow,突然想到effective c++上把new-handler与nothrow放在一起,并且new-handler的用法很“别致”,于是在这里做一下笔记。
当operator new分配空间不够时,除了抛出异常,还会调用new_handler函数,准确来说是先调用new_handler函数,再抛出异常,new_handler函数由std::set_new_handler函数定义,new_handler函数可以报错,也可以为new分配更大的空间。
std::set_new_handler函数用于为new指定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[1000000000L];
...
}
以上所示,如果无法分配空间则会调用outOfMem函数。
设计合理的new_handler函数应该满足以下几点:
1.让更多内存可被使用;
2.**如果自己无法分配更多的空间,则安装另一个new_handler函数以供空间的分配。**这点犹如STL中的适配器,优秀的内存空间分配工具总会想尽办法来分配更多的空间。
3.卸载new-handler。如果没有可用的new-handler,就将参数new-handler置为null(0),也是new_handler的初始值,使异常抛出。只有在new-handler为空的情况下,operator new才抛出异常。如果说用户没有设定new_handler函数,或者设定new_handler函数为0、null,则当内存分配不足时立即抛出异常。这样也可以理解为啥我们写代码什么都不做,内存空间不足时会直接报错了,挺好用的,所以当我们修改new_handler函数时也要遵循这一点,不可能程序内存不足还在跑而不报错,这是很恐怖的;
4.抛出bad_alloc异常,这块没看懂,大概是因为C++新规定内存分配不足必须要抛出异常,而不是像以前返回NULL就行;
5.不返回,通常调用abort或exit,很遗憾,这边也没看懂,大概是因为想只抛出异常不返回吧。
之前说的std::set_new_handler是为全局提供new_handler函数,如果想为每个类提供特定的new_handler则让类自己设计set_new_handler和operator new函数即可。但要注意的是,标准的set_new_handler函数除了设置新的new_handler函数,还会返回旧的new_handler函数指针,因为新设定的new_handler函数可能知识某个类需要的,但是该类修改的是全局new_handler函数,用完完后当然要恢复旧的new_handler函数(注意这个与new_handler函数本身提供的卸载功能不同,这个是使用完new_handler函数后恢复到旧的new_handler函数)。所以设计类自己的set_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;
std::new_handler Widget::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
以上就是以Widget类为例子重写set_new_handler函数,当然是书上的经典代码,如果我能写出来就好了,哈哈哈
void * Widget::operator new(std::size_t size) throw(std::bad_alloc){
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
以上是Widget类重写的operator new函数,就是我们常常调用的new,其中用到了一个NewHandlerHolder类,这个类设计的非常巧妙,可以保存旧的new_handler函数指针,为了Widget类能恢复原本的global new-handler,就是new-handler的初始值null,然后能顺利抛出异常。
class NewHandlerHolder{
public:
explicit NewHandlerHolder(std::new_handler nh) :handler(nh){};
~NewHandlerHolder() {std::new_handler (handler);}
private:
std::new_handler handler;
NewHandlerHolder(const NewHandlerHolder &);
NewHandlerHolder& operator=(const NewHandlerHolder);
};
结合重写的new函数可以看出,new中的set_new_handler函数返回旧的new_handler函数指针,该函数指针作为NewHandlerHolder类构造函数的初始值,并且被记录在handler变量中。当用NewHandlerHolder类创建对象,对象消亡时会自动调用析构函数,析构函数中会将new_handler函数再设定为旧的new_handler函数(变量handler记录的是旧的new_handler函数指针)。
void outOfMem();
Widget::set_new_handler(outOfMem);
Widget* pm1 = new Widget; //如果内存分配失败,将调用outOfMem
Widget::set_new_handler(0);
Widget* pm1 = new Widget; //如果内存分配失败, 将抛出异常
按照以上例子走一遍,首先Widget调用自己的set_new_handler函数,将Widget中的静态变量currentHandler设置为outOfMem,第二步Widget调用new函数时,调用std::set_new_handler设置new_handler为currentHandler,并且保留旧的std::new_handler,应该是null,如果outOfMem起作用则被调用,如果不起作用则恢复new_handler为null,抛出异常。
当然如果每一个类都要像这样声明set_new_handler和operator new用户会累死的,于是有一种很好的方法,把这种模式写成固定的类模板,用户只要提供new_handler即可,每个类要使用的时候集成该类模板具现化就行,代码如下,当然也是书上的。
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);
// ... 其他的operator new版本
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);
}
//将每一个currentHandler 初始化为null
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;
使用时继承就行,如下所示:
class Widget : public NewHandlerSupport<Widget>{
};
以上代码还没细看,等到需要用时再细细研读。
除了以上方式解决内存不够的问题,还有一种就是nothrow,nothrow可以实现分配失败便返回null的行为。
Widget *pw2 = new (std::nothrow) Widget;
这种方式是可行,但相比于new_handler有缺陷,因为new中分为几个步骤,一个是分配空间,接着是调用Widget的构造函数,nothrow只负责在分配空间时报错返回null,如果在Widget构造函数时又new一些内存,nothrow就没办法了,只能抛出异常,而不能做到“分配失败就返回null”。