《Effective C++》学习笔记(条款49:了解new-handler的行为)

最近开始看《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 做以下事情:

  1. 调用标准 set_new_handler,告知 Widget 的错误处理函数。这会将 Widget 的 new-handler 安装为 global new-handler。
  2. 调用 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)。
  3. 如果 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_handleroperator 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 是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是有可能抛出异常

条款50:了解new和delete的合理替换时机

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值