一、set_new_handler()函数
set_new_handler()函数的定义
- 该函数定义于<new>头文件中
- 源码如下,实际上是一个typedef
- 该函数接受一个new_handler参数(这个参数通常指向一个函数),并返回一个new_handler数据
- throw()说明符:表示该函数不会抛出异常。在C++11中已经被改为了noexcept
使用演示案例
- 下面是我们定义的一个函数,在operator new无法分配足够内存时被调用:
void outOfMem()
{
std::cerr << "Unable to satisfy request for memory\n";
std::abort();
}
int main()
{
//绑定函数
std::set_new_handler(outOfMem);
//如果此处内存分配失败,将会调用outOfMem()
int *pBigDataArray = new int[100000000L];
return 0;
}
二、new_handler函数的设计原则
- 当在内存分配不足时,就会调用set_new_handler()参数所指定的new_handler函数
- 如果内存不足时,new_handler函数被执行,但是我们同时又在new_handler函数中进行了动态内存分配,那么后果不可预期
- 因此,一个设计良好的new_handler函数有以下几项原则:
- ①让更多内存可被使用:这种做法时,程序在一开始执行就分配一大块内存,而后当new_handler函数第一次被执行时,将它们释放给系统
- ②安装另一个new_handler:
- 如果目前这个new_handler无法取得更多可用内存,或许它知道另外哪个new_handler有此能力
- 目前这个new_handler就可以安装另外的new_handler来替换自己(只要调用set_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)的异常:这样的异常不会被operator new捕获,因此会被传播到内存索求处
- ⑤不返回:通常调用abort()或exit
三、为类设置new-handler行为
- 有时我们希望为每个类单独设置new-handler行为
- 例如不同的类分配操作失败就调用相对应的new-handler函数。例如下面是伪代码:
class X {
public:
static void outOfMemory();
};
class Y {
public:
static void outOfMemory();
};
int main()
{
X* p1 = new X; //如果new失败了,我们希望调用X::outOfMemory()
Y* p2 = new Y; //如果new失败了,我们希望调用Y::outOfMemory()
return 0;
}
实现class的new-handler行为
- 默认情况下,C++不支持class专属的new-handlers行为,但是我们可以自己实现
- 实现方法为:
- 为class设置operator new()内存分配函数,和set_new_handler()函数
- 然后添加一个new_handler成员函数,代表本class在new失败时的调用函数
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;
};
- 成员函数与成员变量的初始化:
- currentHandler代表当前class的内存分配错误处理函数,初始化时将其设置为0
- set_new_handler()用来设置当前的new_handler函数,原理与标准库set_new_handler()函数类似
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::operator new()我们需要再定义一个class,使用RAII封装,其在构造过程中获得一笔资源,在析构过程中释放。代码如下:
class NewHandlerHolder
{
public:
explicit NewHandlerHolder(std::new_handler nh)
:handler(nh) {}
~NewHandlerHolder() {
std::set_new_handler(handler);
}
private:
std::new_handler handler;
//阻止拷贝操作
NewHandlerHolder(const NewHandlerHolder&);
NewHandlerHolder& operator=(const NewHandlerHolder&);
};
- Widget::operator new()的实现如下:
void* Widget::operator new(std::size_t size)throw(std::bad_alloc)
{
//我们调用std::set_new_handler(),使其返回全局的new-handler
//然后使用全局的new-handler初始化h
NewHandlerHolder h(std::set_new_handler(currentHandler));
//分配实际内存操作
return ::operator new(size);
}
//不论分配是否成功,函数结束之后,h自动析构
//其析构函数中调用std::set_new_handler(),将之前的全局new-handler设置回来
void outOfMem();
int main()
{
//设定outOfMem()为Widget的new-handler函数
Widget::set_new_handler(outOfMem);
//如果内存分配失败,调用outOfMem()
Widget* pw1 = new Widget;
//Widget::operator new()执行结束后还会将全局new-handler设置回来
//如果内存分配失败,调用全局new-handler函数
std::string* ps = new std::string;
//设定Widget的new-handler函数为null
Widget::set_new_handler(0);
//如果内存分配失败,立刻抛出异常(因为Widget的new-handler函数置空了)
Widget* pw2 = new Widget;
return 0;
}
四、封装一个new-handler行为的base class
- 在“三”中,我们最终设计出的Widget拥有自己的new-handler函数。借助Widget的实现原始,我们想设计一个类,让其只拥有new-handler的处理行为,然后让其他类继承于这个类,那么每个类就可以都拥有自己的new-handler行为了
- 为此:
- 我们将Widget中的功能重新封装为一个单独的类,在其中实现new-handler行为
- 为了使每种class拥有自己的new-handler行为,我们将该类设置为模板,然后让各自的class继承于这个模板
- 当class在定义时,就会实例化模板,因此不同的类将会拥有各自的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);
private:
static std::new_handler currentHandler;
};
//仍未非模板
class NewHandlerHolder
{
//内容同上
};
- 有了这个模板,我们可以将自己的类继承于这个类,那么我们的类也将拥有自己的new-handler行为。例如:
//继承NewHandlerSupport
class Widget :public NewHandlerSupport<Widget>{};
class Widget2 :public NewHandlerSupport<Widget2>{};
- 当我们不同的类继承于NewHandlerSupport之后,定义对象时,会分别实例化处不同的NewHandlerSupport模板,互相之间不为干扰
- 总结:
- 上面的NewHandlerSupport模板,我们通常称之为“mixin”风格的base class
- 继承于NewHandlerSupport,可能会导致多重继承带来的争议(可以参阅条款40)
五、nothrow说明
- 当opertor new分配内存时:
- 旧C++标准中,让operator new返回null
- 现代C++标准中,operator new抛出异常(bad_alloc),并且该异常不能被operator new捕获,因此会被传播到内存索求处
- nothrow说明符:为了与之前的C++标准中new操作符分配失败时返回null保持兼容,C++提供了这个关键字,使用了这个关键字能让operator new()在内存分配失败时返回null,而不抛出bad_alloc异常
- 例如:
class Widget {};
int main()
{
//当new出错时,抛出bad_alloc异常
Widget* pw1 = new Widget;
//当new出错时,返回null
Widget* pw2 = new (std::nothrow) Widget;
return 0;
}
- 注意事项:
- nothrow只与operator new有关,让其在申请内存失败时返回null
- 但是与其后面类型的构造函数无关。例如上面Widget的构造函数是否会抛出异常与nothrow无关
六、总结
- set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用
- Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能会抛出异常