最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!
当 operator new 无法满足某一内存分配时,就会抛出异常。以前它会返回NULL指针,某些旧式编译器目前也还这么做。
在operator new抛出异常以前,会先调用一个客户指定的错误处理函数:new-handler。(这其实并非全部事实,operator new 真正做的事更复杂,见条款51)。为了指定这个“用以处理内存不足”的函数,客户必须调用 set_new_handler,那是声明于 <new>
的标准程序库函数:
namespace std{
typedef void (*new_handler)();//*new_handler是个typedef,定义出一个指针指向函数,该函数没有参数也不返回任何东西
new_handler set_new_handler(new_handler p) throw();
}
set_new_handler
是“获得一个 new-handler
并返回一个 new-handler
” 的函数,后面的 throw()
是一份异常明细,表示该函数不抛出任何异常。
set_new_handler
的参数是个指针,指向 operator new 无法分配足够内存时该被调用的函数,其返回值也是个指针,指向 set_new_handler
被调用前正在执行的那个 new-handler 函数。可以这样使用set_new_handler
:
void outOfMem() //operator new 无法分配足够内存时该被调用的函数
{
std::cerr<<"Unable to satisfy request for memoryn";
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有此能力。如果真这样,可用使用
set_new_handler
来替换有能力的那个。 - 卸除 new-handler。即将null指针传给
set_new_handler
,一旦没有安装任何 new-handler,operator new 在内存分配不成功时便抛出异常。 - 抛出 bad_alloc(或派生自bad_alloc)的异常。这样的异常不会被 operator new 捕捉,因此不会被传播到内存索求处。
- 不返回。通常 abort 或 exit 。
有时候,我们希望处理内存分配失败的情况和类相关。例如:
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++并不支持类专属的 new-handler,但是我们自己可以实现这种行为。令每一个类提供自己的 set_new_handler
和 operator new即可。其中 set_new_handler
使客户得以指定类专属的 new-handler,operator new 则确保在分配 类对象 内存的过程中 以类专属的 new-handler替换 global new-handler。
假设打算处理 Widget
类 内存分配失败的情况。首先要有一个"当 operator new 无法为Widget
分配足够内存时"调用的函数,所以你需要声明一个类型为 new_handler 的 static 成员,用以指向 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;
};
std::new_handler Widget::currentHandler = 0; //static成员需要在类外定义
std::new_handler Widget::set_new_handler(std::new_handler p) throw() {
std::new_handler oldHandler = currentHandler; //存储之前的new-handler
currentHandler = p; //设置新的new-handler
reutrn oldHandler; //返回之前的new-handler
}
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 异常。这时Widget
的operator new 必须恢复原本的 global new-handler,之后再传播该异常。为确保原本的 new-handler 总是能够被重新安装回去,使用资源管理对象防止资源泄漏(见条款13)。 - 如果 global operator new 分配内存成功,
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) : handlere(nh){}
~NewHandlerHolder() {
std::set_new_handler(handler); //恢复存储的new+handler(很可能是global new-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()安装Widget的new-handler,并返回global new-handler 存储在资源管理类中
//分配内存或抛出异常时,在资源管理类的析构函数中恢复global new-handler
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
Widget
的用户应该类似这样使用其 new-handling
void outOfMem(); //函数声明,此函数在 Widget 对象分配失败时被调用
Widget::set_new_handler(outOfMem); //设定 outOfMem 为 Widget 的 new-handling 函数,set_new_handler是静态函数
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; //若内存分配失败则立刻抛出异常,因为 Widget 没有专属 new-handling函数
实现这个方案的代码并不因 class 的不同而不同,因此在其它地方也复用这个代码是个合理的构想。一个简单的方式是建立起一个“mixin” 风格的 基类,这种 基类 用来允许 派生类 继承单一特定能力——在本例中是“设定 类 专属的 new-handler 能力”。然后将这个 基类 转换为 模板,如此一来每个 派生类 将获得实体互异的 class data 复件。
这个 基类 让其 派生类 继承它获取 set_new_handler
和 operator new
函数,而 模板 部分确保每一个 派生类 获得一个实体互异的currentHandler 成员变量。实现代码和前一个版本的近似,唯一真正意义上不同的是,它现在可被任何有所需要的 类 使用:
template<typename T>
class NewHandlerSupport{ // "mixin" 风格的基类,用以支持类专属的set_new_handler
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 版本,见条款52
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;//静态变量初始化
有了这个 类模板,为 Widget
添加 set_new_handler
支持能力就容易了:只要令 Widget
继承自 NewHandlerSupport<Widget>
就好,像下面这样:
class Widget:public NewHandlerSupport<Widget>{
//和先前一样,但不必声明 set_new_handler 和 operator new 函数
};
在 NewHandlerSupport
模板中,从未使用到 类型T,这是为什么呢?
实际上 T 的确不需被使用。我们只希望继承 NewHandlerSupport
的每一个 类 拥有自己的 NewHandlerSupport
复件(其 static 成员变量 currentHandler ),类型参数 T 是用来区分不同的 派生类,模板机制会自动为每一个 T 生成一份 currentHandler 成员变量。
虽然通过继承 NewHandlerSupport
,使得“为任何类添加一个它们专属的new-handler”成为一件很容易的事,但 “mixin” 风格的继承肯定导致 多重继承 的争议,要注意条款40所提到的内容。
C++中新一代的 operator new 分配失败抛出异常 bad_alloc,但是旧标准是返回 null 指针,为了兼容以前使用旧标准的C++程序,C++委员会提供了另一种符合旧标准形式的 operator new , 这个形式被称为 “nothrow” 形式:
class Widget{ ... };
Widget* pw1 = new Widget; //分配失败,抛出bad_alloc
if(pw1 == null) { ... } //判断是否分配成功。但是这个测试一定失败
Widget* pw2 = new (std::nothrow)Widget; //分配失败,返回null
if(pw2 == null) { ... } //这个测试可能成功
nothrow new 对 异常的强制保证性(见条款29)并不高。表达式 new (std::nothrow)Widget
会发生两件事:
第一,分配内存给 Widget
对象,如果失败返回 null ;第二,如果成功,调用 Widget
的构造函数,在这个构造函数中可能又 new 一些内存,但没人可以强迫它再次使用 nothrow new。因此,虽然 new (std::nothrow)Widget
调用的 operator new 函数并不抛出异常,但 Widget
的构造函数可能会抛出异常 。
结论是:使用 nothrow new 只能保证 operator new 不抛出异常,不能保证像new (std::nothrow)Widget
这样的表达式不抛出异常。所以,并没有使用 nothrow 的需要。
无论使用正常(会抛出异常)的 new,或是不抛出异常的 nothrow new ,重要的是需要了解 new-handler 的行为,因为两种形式都使用到 new-handler 。
Note:
- set_new_handle 允许用户指定一个函数,在内存分配无法获得满足时被调用
- nothrow new 是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是有可能抛出异常