operator new无法满足某一内存分配需求时,它会抛出异常。以前它会返回一个null指针,某些旧式编译器目前也还那么做。你还是可以取得旧行为,但本条款最后才会进行这项讨论。
当operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,一个所谓的new-handler。为了指定这个“用以处理内存不足”的函数,客户必须调用set_new_handler,那是声明于<new>的标准程序库函数:
namespace std {
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
}
如你所见,new_handler是个typedef,定义出一个指针指向函数,该函数没有参数也不返回任何东西。set_new_handler则是“获得一个new_handler并返回一个new_handler”的函数。set_new_handler声明式尾端的“throw()”是一份异常明细,表示该函数不抛出任何异常——虽然事实更有趣些,详见条款29。
set_new_handler的参数是个指针,指向operator new无法分配足够内存时该被调用的函数。其返回值也是个指针,指向set_new_handler被调用前正在执行(但马上就要被替换)的那个new-handler函数。
你可以这样使用set_new_handler:
// 以下是当operator new无法分配足够内存时,该被调用的函数
void outOfMem()
{
std::cerr<<"Unable to statisfy request for memeory\n";
std::abort();
}
int main()
{
std::set_new_handler(outOfMem);
int* pBigDataArray = new int[100000000L];
...
}
就本例而言,如果operator new无法为100000000个整数分配足够空间,outOfMem会被调用,于是程序在发出一个信息之后夭折(abort)。
当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。引起反复调用的代码显示于条款51,这里的高级描述已足够获得一个结论,那就是设计良好的new-handler函数必须做以下事情:
- 让更多内存可被使用。这便造成operator new内的下一次内存分配动作可能成功。实现此策略的一个做法是,程序一开始执行就分配一大块内存,而后当new-handler第一次被调用,将它们释还给程序使用。
- 安装另一个new-handler。如果目前这个new-handler无法取得更多可用内存,或许它知道另外哪个new-handler有此能力。果真如此,目前这个new-handler就可以安装另外那个new-handler以替换自己(只要调用set_new_handler)。
- 卸除new-handler,也就是将null指针传给set_new_handler。一旦没有安装任何new-handler,operator new会在内存分配不成功时抛出异常。
- 抛出bad_alloc(或派生自bad_alloc)的异常。这样的异常不会被operator new捕捉,因此会被传播到内存索求处。
- 不返回,通常调用abort或exit。
这些选择让你在实现new-handler函数时拥有很大弹性。
这时候你或许希望以不同的方式处理内存分配失败情况,你希望视被分配物属于哪个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,但其实也不需要。你可以自己实现出这种行为。只需令每个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的内存分配失败情况。首先你必须登录“当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做以下事情:
- 调用标准set_new_handler,告知Widget的错误处理函数。这会将Widget的new-handler安装为global new-handler。
- 调用global operator new,执行实际之内存分配。如果分配失败,global operator new会调用Widget的new-handler,因为那个函数才刚被安装为global new-handler。如果global operator new最终无法分配足够内存,会抛出一个bad_alloc异常。
- 如果global operator new能够分配足够一个Widget对象所用的内存,Widget的operator new会返回一个指针,指向分配所得。Widget析构函数会管理global new-handler,它会自动将Widget's operator new被调用前的那个global new-handler恢复回来。
下面以C++代码再阐述一次。我将从资源处理类开始,那里面只有基础性RAII操作,在构造过程中获得一笔资源,并在析构过程中释还(见条款13):
class NewHandlerHolder {
public:
explicit NewHandlerHolder(std::new_handler nh) // 取得目前的new-handler
: handler(nh) {}
~NewHandlerHolder() // 释放它
{ std::set_new_handler(handler); }
private:
std::new_handler handler; // 记录下来
NewHandlerHolder(const NewHandlerHolder&); // 阻止copying(见条款14)
NewHandlerHolder& operator=(const NewHandlerHolder&);
};
这就使得Widget's 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-handling:
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; // 如果内存分配失败,立即抛出异常(class Widget没有专属的new-handling函数)
请记住
- set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
- Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后续的构造函数调用还是可能抛出异常。