定制new和delete
条款49:了解new-handler的行为
当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函数:
void outOfMen()
{
std::cerr<<"Unable to satisfy request for memory\n";
std::abort();
}
int main()
{
std::set_new_handler(outOfMem);
int* pBigDataArray = new int[100000000L];
...
}
上述代码表示,一旦未能够为此数组分配足够的空间,那么outOfMem就会被调用,于是程序发出一个信息后就会夭折(abort)。同时,当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够的内存。而一个设计良好的new-handler函数必须做以下事情:
- 让更多内存可被使用。实行的做法是:程序一开始执行就分配一大块内存,而后当new-handler第一次被调用,将它们释还给程序使用;
- 安装另一个new-handler。如果当前的new-handler无法取得更多可用内存,或许另外的new-handler可以做到。实行的做法是:令new-handler修改“会影响new-handler行为”的static数据、namespace数据或global数据;
- 卸除new-handler,也就是将null指针传给set_new_handler。一旦没有安装任何new-handler,operator new会在内存分配不成功时抛出异常;
- 抛出bad_alloc的异常。这样的异常不会被operator new捕捉,因此会被传播到内存索求处;
- 不返回。通常调用abort或exit;
有时候你或许希望以不同的方式处理内存分配失败情况,你希望视被分配物属于哪个class而定:
class X{
public:
static void outOfMemory();
...
};
class Y{
public:
static void outOfMemory();
...
};
X* p1 = new X;
Y* p2 = new Y;
C++并不支持class专属之new-handlers,但其实也不需要。你可以自己实现出这种行为。只需令每一个class提供自己的set_new_handler和operator new即可。
现在,假设你打算处理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;
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;
}
条款50:了解new和delete的合理替换时机
替换编译器提供的operator new或operator delete的理由有三个:
- 用来检测运用上的错误。如果将“new所得内存” delete掉却不幸失败,会导致内存泄露。如果在“new所得内存”身上多次delete则会导致不确定行为。此外各种各样的编程错误可能导致数据“overruns”(写入点在分配区块尾端之后)或“underruns”(写入点在分配区块起点之前);
- 为了强化效能。编译器所带的operator new和operator delete主要用于一般目的,它们不但可被长时间执行的程序接受,也可被执行时间少于一秒的程序接受。所以它们必须考虑破碎问题,这最终会导致程序无法满足大区块内存要求,即使彼时有总量足够但分散为许多小区块的自由内存;
- 为了收集使用上的统计数据。在使用定制型news和定制型deletes之前,理当先收集你的软件如何使用其动态内存;
以下一个例子是快速发展得出的初阶段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();
*(static_cast<int*> (pMem)) = signature;
*(reinterpret_cast<int*> (static_cast<Byte*>(pMem) + realSize - sizeof(int)))
= signature;
//返回指针,指向恰位于第一个signature之后的内存位置
return static_cast<Byte*> (pMem) + sizeof(int);
}
以上代码的缺点主要在于它疏忽了身为特殊函数所应该具备的“坚持C++规矩”的态度。举个例子,条款51说所有operator news都应该内含一个循环,反复调用某个new-handling函数,这里却没有。