49:了解new-handler的行为

当operator new无法满足某一内存分配需求时,它会抛出异常。以前它会返回一个null指针。

当operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,一个所谓的new-handler(其实operator new真正做的事情要稍微复杂些)。为了指定这个“用以处理内存不足”的函数,客户必须调用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()"是一份异常明细,表示该函数不抛出任何异常。

set_new_handler的参数是个指针,指向operator new无法分配足够内存时该被调用的函数。其返回值也是个函数,指向set_new_handler被调用前正在执行(但马上就要被替换)的那个new-handler函数。

可以这样使用set_new_handler:

//以下是当operator new无法分配足够内存时,该被调用的函数
void outOfMem()
{
    cout << "Unable to satisfy request for memory\n";
    std::abort();
}
int main()
{
    int* pBigDataarray = new int[100000000L];
    return 0;
}

就本例而言,若operator new无法为100000000个整数分配足够空间,outOfMem会被调用,于是程序在发出一个信息之后夭折。

当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。

一个设计良好的new-handler函数必须做以下事情:

1.让更多内存可被使用。

这便造成operator new内的下一次内存分配动作可能成功。

实现此策略的一个做法是,程序一开始执行就分配一大块内存,而后当new-handler第一次被调用,将它们释还给程序使用。

2.安装另一个new-handler。

若目前这个new-handler无法取得更多可用内存,或许它知道另外哪个new-handler由此能力。若真如此,目前这个new-handler就可以安装另外那个new-handler以替换自己。

做法之一是令new-handler修改“会影响new-handler行为”的static数据、namespace数据或global数据。

3.卸除new-handler,也就是将null指针传给set_new_handler。

一旦没有安装任何new-handler,operator new会在内存分配不成功时抛出异常。

4.抛出bad_alloc(或派生自bad_alloc)的异常。

这样的异常不会被operator new捕捉,因此会被传播到内存索求处。

5.不返回。通常调用abort或exit。

这些选择让你在实现new-handler函数时拥有很大弹性。

有时候,你或许希望以不同的方式处理内存分配失败情况,你希望被分配物属于哪个class而定:

class X {
public:
    static void outOfMemory();
    //...
};
class Y {
public:
    static void outOfMemory();
    //...
};
//若分配不成功,调用X::outOfMemory
X* p1 = new X;
//若分配不成功,调用Y::outOfMemory
Y* p2 = new Y;

C++并不支持class专属的new-handler,但也并不需要。

你可以自己实现出这种行为。只需令每一个class提供自己的set_new_handle和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而且是整数型),所以需要这么写:

//在class实现文件内初始化为null
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;
}

最后,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总是能够被重新安装回去,Widget运用资源管理对象防止资源泄漏。

3.若global operator new能够分配足够一个Widget对象所用的内存,Widget的operator new会返回一个指针,指向分配所得。Widget析构函数会管理global new-handler,它会自动将Widget的operator new被调用前的那个global new-handler恢复回来。

下面以C++代码再阐述一次:

class NewHandlerHolder {
public:
    //取得目前的new-handler
    explicit NewHandlerHolder(std::new_handler nh): handler(nh){}
    ~NewHandlerHolder() { std::set_new_handler(handler); }//释放它
private:
    std::new_handler handler;//记录下来
    NewHandlerHolder(const NewHandlerHolder&);//阻止copying
    NewHandlerHolder& operator=(const NewHandlerHolder&);
};

这就使得Widget的operator new的实现相当简单:

void* Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
    //安装Widget的new-handler
    NewHandlerHolder h(std::set_new_handler(currentHandler));
    //分配内存或抛出异常,恢复global new-handler
    return ::operator new(size);
}

Widget的客户应该类似这样使用其new-handlering:

//函数声明,此函数在Widget对象分配失败时调用
void outOfMem();
//设定outOfMem为Widget的new-handling函数
Widget::set_new_handler(outOfMem);
//若内存分配失败,调用outOfMem
Widget* pw1 = new Widget;
//若内存分配失败,调用global new-handing函数(若有的话)
std::string* ps = new std::string;
//设定Widget专属的new-handling函数为null
Widget::set_new_handler(0);
//若内存分配失败,立刻抛出异常
Widget* pw2 = new Widget;//class Widget并没有专属的new-handling函数

实现这一方案的代码并不因class的不同而不同,因此在它处加以复用是个合理的构想。一个简单的做法是建立起一个"mixin"风格的base class,这种base class用来允许derived class继承单一特定能力,在本例中是“设定class专属的new-handler”的能力。然后将这个base class转换为template,如此一来每个derived class将获得实体互异的class data复件。

这个设计的base class部分让derived class继承它们所需的set_new_handler和operator new,而template部分则确保每一个derived class获得一个实体互异的currentHandler成员变量。

与前个版本相比,唯一真正意义上的不同是,它现在可被任何有所需要的class使用:

//"mixin"风格的base class,用以支持class专属的set_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);
    //...//其他的operator new版本
private:
    static std::new_handler currentHandler;
};
template<typename T>
std::new_handler 
NewHandlerSupport<T>::set_new_handler(std::new_handler) 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 ::new_handler(size);
}
//以下将每一个currentHandler初始化为null
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;

有了这个class template,为Widget添加set_new_handler支持能力就轻而易举了:只要令Widget继承自NewHandlerSupport<Widget>就好。

像下面这样:

class Widget : public NewHandlerSupport<Widget> {
    //...//和先前一样,但不必声明set_new_handler或operator new
};

这就是Widget为了提供“class专属的set_new_handler:所需做的全部动作。

总结

1.set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。

2.Nothrow new是个颇为局限的工具,因为它只适用于内存分配,后继的构造函数调用还是可能抛出异常。 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值