Effective C++条款49:定制new和delete——了解new-handler的行为

一、set_new_handler()函数

  • set_new_handler()函数语法介绍,参阅:https://blog.csdn.net/qq_41453285/article/details/103553037
  • 当opertor new分配内存时:
    • 旧C++标准中,让operator new返回null
    • 现代C++标准中,operator new抛出异常(bad_alloc),并且该异常不能被operator new捕获,因此会被传播到内存索求处
  • 我们可以在使用operator new之前,使用set_new_handler()绑定一个错误处理函数,这个错误处理函数将在opertor new分配内存时内存不足被调用执行

set_new_handler()函数的定义

  • 该函数定义于<new>头文件中
  • 源码如下,实际上是一个typedef

  • 该函数接受一个new_handler参数(这个参数通常指向一个函数),并返回一个new_handler数据
  • throw()说明符:表示该函数不会抛出异常。在C++11中已经被改为了noexcept

使用演示案例

  • 下面是我们定义的一个函数,在operator new无法分配足够内存时被调用:
void outOfMem()

{

    std::cerr << "Unable to satisfy request for memory\n";

    std::abort();

}


int main()

{

    //绑定函数

    std::set_new_handler(outOfMem);


    //如果此处内存分配失败,将会调用outOfMem()

    int *pBigDataArray = new int[100000000L];


    return 0;

}

二、new_handler函数的设计原则

  • 当在内存分配不足时,就会调用set_new_handler()参数所指定的new_handler函数
  • 如果内存不足时,new_handler函数被执行,但是我们同时又在new_handler函数中进行了动态内存分配,那么后果不可预期
  • 因此,一个设计良好的new_handler函数有以下几项原则:
    • ①让更多内存可被使用:这种做法时,程序在一开始执行就分配一大块内存,而后当new_handler函数第一次被执行时,将它们释放给系统
    • ②安装另一个new_handler
      • 如果目前这个new_handler无法取得更多可用内存,或许它知道另外哪个new_handler有此能力
      • 目前这个new_handler就可以安装另外的new_handler来替换自己(只要调用set_new_handler)
      • 下次当operator new调用new_handler,调用的将是最新安装的那个(这个旋律的变奏之一是让new_handler修改自己的行为,于是当它下次被调用,就会做某些不同的事。为达到此目的,做法之一是令new_handler修改“会影响new_handler行为”的static数据、namespace数据或global数据)
    • ③卸除new_handler:也就是将null指针传给set_new_handler。一旦没有安装任何new_handler函数,operator new会在内存分配不成功时抛出异常
    • ④抛出bad_alloc(或派生自bad_alloc)的异常:这样的异常不会被operator new捕获,因此会被传播到内存索求处
    • ⑤不返回:通常调用abort()或exit

三、为类设置new-handler行为

  • 有时我们希望为每个类单独设置new-handler行为
  • 例如不同的类分配操作失败就调用相对应的new-handler函数。例如下面是伪代码:
class X {

public:

    static void outOfMemory();

};


class Y {

public:

    static void outOfMemory();

};


int main()

{

    X* p1 = new X; //如果new失败了,我们希望调用X::outOfMemory()

    Y* p2 = new Y; //如果new失败了,我们希望调用Y::outOfMemory()

    return 0;

}

实现class的new-handler行为

  • 默认情况下,C++不支持class专属的new-handlers行为,但是我们可以自己实现
  • 实现方法为:
    • 为class设置operator new()内存分配函数,和set_new_handler()函数
    • 然后添加一个new_handler成员函数,代表本class在new失败时的调用函数
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;

};
  • 成员函数与成员变量的初始化:
    • currentHandler代表当前class的内存分配错误处理函数,初始化时将其设置为0
    • set_new_handler()用来设置当前的new_handler函数,原理与标准库set_new_handler()函数类似
std::new_handler Widget::currentHandler = 0;


std::new_handler Widget::set_new_handler(std::new_handler p)throw()

{

    std::new_handler oldHandler = currentHandler;

    currentHandler = p;

    return oldHandler;

}
  • 针对于Widget::operator new()我们需要再定义一个class,使用RAII封装,其在构造过程中获得一笔资源,在析构过程中释放。代码如下:
class NewHandlerHolder

{

public:

    explicit NewHandlerHolder(std::new_handler nh)

    :handler(nh) {}

    ~NewHandlerHolder() {

        std::set_new_handler(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(),使其返回全局的new-handler

    //然后使用全局的new-handler初始化h
    
    NewHandlerHolder h(std::set_new_handler(currentHandler));


    //分配实际内存操作

    return ::operator new(size);


}

//不论分配是否成功,函数结束之后,h自动析构

//其析构函数中调用std::set_new_handler(),将之前的全局new-handler设置回来
  • 客户端应该使用下面的格式使用Widget类:
void outOfMem();


int main()

{

    //设定outOfMem()为Widget的new-handler函数

    Widget::set_new_handler(outOfMem);


    //如果内存分配失败,调用outOfMem()

    Widget* pw1 = new Widget;


    //Widget::operator new()执行结束后还会将全局new-handler设置回来


    //如果内存分配失败,调用全局new-handler函数

    std::string* ps = new std::string;


    //设定Widget的new-handler函数为null

    Widget::set_new_handler(0);


    //如果内存分配失败,立刻抛出异常(因为Widget的new-handler函数置空了)

    Widget* pw2 = new Widget;


    return 0;

}

四、封装一个new-handler行为的base class

  • 在“三”中,我们最终设计出的Widget拥有自己的new-handler函数。借助Widget的实现原始,我们想设计一个类,让其只拥有new-handler的处理行为,然后让其他类继承于这个类,那么每个类就可以都拥有自己的new-handler行为了
  • 为此:
    • 我们将Widget中的功能重新封装为一个单独的类,在其中实现new-handler行为
    • 为了使每种class拥有自己的new-handler行为,我们将该类设置为模板,然后让各自的class继承于这个模板
    • 当class在定义时,就会实例化模板,因此不同的类将会拥有各自的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);

private:

    static std::new_handler currentHandler;

};


//仍未非模板

class NewHandlerHolder

{

    //内容同上

};
  • 有了这个模板,我们可以将自己的类继承于这个类,那么我们的类也将拥有自己的new-handler行为。例如:
//继承NewHandlerSupport

class Widget :public NewHandlerSupport<Widget>{};


class Widget2 :public NewHandlerSupport<Widget2>{};
  • 当我们不同的类继承于NewHandlerSupport之后,定义对象时,会分别实例化处不同的NewHandlerSupport模板,互相之间不为干扰
  • 总结:
    • 上面的NewHandlerSupport模板,我们通常称之为“mixin”风格的base class
    • 继承于NewHandlerSupport,可能会导致多重继承带来的争议(可以参阅条款40)

五、nothrow说明

  • 当opertor new分配内存时:
    • 旧C++标准中,让operator new返回null
    • 现代C++标准中,operator new抛出异常(bad_alloc),并且该异常不能被operator new捕获,因此会被传播到内存索求处
  • nothrow说明符:为了与之前的C++标准中new操作符分配失败时返回null保持兼容,C++提供了这个关键字,使用了这个关键字能让operator new()在内存分配失败时返回null,而不抛出bad_alloc异常
  • 例如:
class Widget {};


int main()

{

    //当new出错时,抛出bad_alloc异常

    Widget* pw1 = new Widget;


    //当new出错时,返回null

    Widget* pw2 = new (std::nothrow) Widget;
    
    return 0;

}
  • 注意事项:
    • nothrow只与operator new有关,让其在申请内存失败时返回null
    • 但是与其后面类型的构造函数无关。例如上面Widget的构造函数是否会抛出异常与nothrow无关

六、总结

  • set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用
  • Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能会抛出异常

猜你喜欢

转载自blog.csdn.net/www_dong/article/details/113848862