Effective C++读书笔记15

来源:互联网 发布:未来教育软件下载 编辑:程序博客网 时间:2024/06/02 23:05

8 定制new和delete

条款49:了解new-handler的行为

当operator new无法满足某一内存分配需求时,它会抛出异常。以前她会返回一个null指针,某些旧式编译器目前也还那么做。

当new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数:new-handler,为了指定这个用以处理内存不足的函数,客户必须调用set_new_handler,在<new>中:

namespace std {typedef void (*new_handler)();new_handler set_new_handler(new_handler p) throw ();}

set_new_handler则是获得一个new_handler并返回一个new_handler函数,末尾的throw()表示不抛出任何异常。

set_new_handler的参数是个指针,指向new无法分配足够内存时该被调用的函数。返回的指针指向调用set_new_handler被调用前正在执行的那个new-handler函数。可以这样使用set_new_handler函数:

void outOfMem{std::cerr << "Unable to satisfy request for memory\n";std::abort();}int main(){std::set_new_handler(outOfMem);int* pBigDataArray = new int[1000000000L];}

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

1.让更多内存被调用。

2.安装另一个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的异常。

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

有时你或许希望以不同的方式处理内存分配失败的情况,你希望视被分配物属于哪个class而定。C++并不支持class专属的new-handler,你可以自己实现出这种行为。只需令每一个class提供自己的set_new_handler和operator new即可。set_new_handler使客户得以指定class专属的new-handler,至于operator new 则确保在分配class对象内存的过程中以class专属之new-handler替换global new-handler。

来看处理Widget类的内存分配失败的情况:

class Widget{public:static std::new_hanler set_new_handler(std::new_handler p) throw();static void* operator new(std::size_t size) throw(std::bad_alloc);private:sttic std::new_handler currentHandler;};
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做以下事情:

1.调用标准set_new_handler,告知Widget的错误处理函数。

2.调用global operator new,执行实际之内存分配。

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

代码如下:

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&);};void* Widget::operator new(std::size_t size) throw (std::bad_alloc){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函数Widget* pwl = new Widget;              //如果内存分配失败,调用outOfMemstd::string* ps = new std::string;     //如果内存分配失败,调用global new-handling函数Widget::set_new_handler(0);            //设定Widget专属的new-handling函数为nullWidget* pw2 = new Widget;              //如果内存分配失败,立刻抛出异常。Widget并没有专属的new-handling函数

实现这一方案的代码并不因class的不同而不同,因此在他处加以复用是个合理的构想。一个简单的做法是建立起一个minxin丰哥的基类,这种基类用来允许派生类继承单一特定能力(在本例中是设定class专属的new-handler能力)。然后将这个base class转换为template,如此一来每个派生类将获得实体互异的class data复件。听起来有点复杂,但代码非常近似前个版本:
template<typename T>std::new_handlerNewHandlerSupport<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初始化为nulltemplate<typename T>std::new_handler NewHandlerSupport<T>::currentHandler = 0;

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

class Widget: public NewHandlerSupport<Widget>{

        //...        //和先前一样,但不必声明set_new_handler或者operator new

}

以下没看懂,不抄了。。。

请记住:

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

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


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

怎么会有人想要替换编译器提供的new或者delete呢?下面是三个最常见的理由:

1.用来检测运行商的错误:

1)delete失败导致内存泄露

2)在new所得内存上多次delete,结果不确定

3)写入点在分配区块尾端之后;写入点在分配区块起点之前

2.为了强化效能:

编译器所带的new和delete主要用于一般目的,他们不但能被长时间执行的程序接受,也可以被执行时间少于一秒的程序接受。他们必须处理一系列需求,包括大块内存、小块内存、大小混合内存,他们必须接纳各种分配形态,范围从程序存活期间的少量区块动态分配,到大数量短命对象的持续分配和归还,他们必须考虑碎片问题,这最终会导致程序无法满足大块内存需求。因此编译器所带的new和delete采用中庸之道也就不足惊讶了。

3.为了收集使用上的统计数据:

首先收集你的软件如何使用其动态内存:

1)分配区块的大小分布如何?

2)寿命分布如何?

3)它们倾向于以FIFO次序或LIFO次序或随机次序来分配和归还?

4)它们的运用形态是否随时间改变,也就是说你的软件在不同的执行阶段有不同的分配和归还形态吗?

5)任何时刻使用的最大动态分配量是多少?


对齐。。。

4.为了增加分配和归还的速度。泛型分配器往往比定制型分配器慢,特别是当定制型分配器专门针对某特定类型之对象而设计时。

5.为了降低缺省内存管理带来的空间额外开销。泛用型内存分配器往往不只比定制型慢,他们往往还使用更多内存,那是因为他们常常在每个分配区块身上招引某些额外开销。针对小型对象而开发的分配器(例如Boost的Pool)本质上消除了这样的额外开销。

6.为了弥补缺省分配器的非最佳齐位:在x86体系结构上double的访问速度最快——如果他们都是8-bytes齐位。但是编译器自带的operator new并不保证对动态分配而得的double采取8-bytes齐位。这种情况下,将缺省的new替换为一个8-byte齐位保证版,可导致程序效率大幅提升。

7.为了将相关对象成簇集中。如果你知道特定之某个数据结构往往被一起使用,而你又希望在处理这些数据时将内存页错误的频率降至最低,那么为此数据结构创建另一个heap就有意义,这么一来他们就可以被成簇集中在尽可能少的内存页上。new和delete的placement版本有可能完成这样的行为。

8为了获得非传统的行为。


请记住:

有许多理由需要写个自定的new和delete,包括改善性能,对heap运用错误进行调试,收集heap使用信息。

原创粉丝点击