Effective C++笔记⑦

定制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的理由有三个:

  1. 用来检测运用上的错误。如果将“new所得内存” delete掉却不幸失败,会导致内存泄露。如果在“new所得内存”身上多次delete则会导致不确定行为。此外各种各样的编程错误可能导致数据“overruns”(写入点在分配区块尾端之后)或“underruns”(写入点在分配区块起点之前);
  2. 为了强化效能。编译器所带的operator new和operator delete主要用于一般目的,它们不但可被长时间执行的程序接受,也可被执行时间少于一秒的程序接受。所以它们必须考虑破碎问题,这最终会导致程序无法满足大区块内存要求,即使彼时有总量足够但分散为许多小区块的自由内存;
  3. 为了收集使用上的统计数据。在使用定制型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函数,这里却没有。

条款51:编写new和delete时需固守常规

条款52:写了placement new也要写placement delete

发布了90 篇原创文章 · 获赞 6 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_37160123/article/details/103174464