effective C++读书笔记(八)

8. 定制newdelete(Templatesand Generic Programming)

条款49:了解new-handler的行为(Understandthe behavior of the new-handler

operatornew无法满足某一内存分配需求时,它会调用一个客户指定的错误处理函数,及new-handlernew-handler可能会为operatornew找到足够的内存或者其他怎样以处理内存不足的情况,如果new-handler为空,operatornew抛出异常。 
通过set_new_handler来设置内存不足时调用的操作,它是声明于<new>的一个标准程序库函数: 

Cpp代码  

1. namespace std{  

2.   typedef void (*new_handler) ();  

3.   new_handler set_new_handler( new_handler p ) throw(); //承诺不抛出异常  

4. };  

5. //这样使用set_new_handler  

6. void outOfMem(){  

7.   std::err<<"Unable to satisfy request for memory\n";  

8.   std::abort();  

9. }  

10.  

11.int main(){  

12.  std::set_new_handler( outOfMem );  

13.  int* pBigDataArray = new int[10000000L];  

14.  ...  

15.}  


operatornew无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存,或者最后抛出异常。因此,一个设计良好的new-handler函数必须做以下事情: 
1)让更多内存可被使用。实现此策略的做法是,程序一开始执行就分配一大块内存,而后当new-handler第一次被调用,将它们释还给程序。 
2)安装另一个new-handler。如果目前这个new-handler无法取得更多可用内存,或许它知道另外哪个new-handler有此能力,因为可以通过set_new_handler安装一个新的new-handler 
3)卸载new-handler,也就是将null指针传给set_new_handler。一旦没有安装任何new-handleroperatornew会在内存分配不成功时抛出异常。 
4)抛出bad_alloc(或派生自bad_alloc)的异常。这样的异常不会被operatornew捕捉,因此会被传播到内存索求处。 
5)不返回,通常调用abortexit 
假如你希望以不同方式处理内存分配失败情况,具体视class而定,也就是提供class之专属new-handler,你可以自己实现出这种行为,只需要令每一个class提供自己的set_new_handleroperatornew即可,其中set_new_handler使客户得以指定class专属的new-handler,而operatornew则确保在分配class对象内存的过程中以class专属之new-handler替换globalnew-handler 

Cpp代码  

1. class Widget{  

2.   public:  

3.     static std::new_handler set_new_handler( std::new_handler p )throw();  

4.     static void* operator new( std::size_t size ) throw( std::bad_alloc );  

5.   private:  

6.     static std::new_handler currentHandler;  

7. };  

8.   

9. std::new_handler Widget::currentHandler = 0;  

10.std::new_handler Widget::set_new_handler( std::new_handler p )throw(){  

11.  std::new_handler oldHandler = currentHandler;  

12.  currentHandler = p;  

13.  return oldHandler;  

14.};  


Widget专属的operatornew做的事情就是在调用globaloperator new之前将globalnew handler安装成Widget专属的new-handler,并在成功分配内存或抛出异常前将原先的globalnew-handler安装回去。为了达到这一点,可以使用资源管理类: 

Cpp代码  

1. class NewHandlerHolder{  

2.   public:  

3.     explicit NewHandlerHolder( std::new_handler nh ):handler(nh){}  

4.     ~NewHandlerHolder(){  

5.       std::set_new_handler(handler); }  

6.   private:  

7.     std::new_handler handler;  

8.     //阻止copying  

9.     NewHandlerHolder( const NewHandlerHolder& );  

10.    NewHandlerHolder& operator=( const NewHandlerHolder& );  

11.};  

12.//于是Widget::operator new的实现就相当简单了:  

13.void* Widget::operator new( std::size_t size ) throw ( std::bad_alloc ){  

14.  NewHandlerHolder h(std::set_new_handler(currentHandler));  

15.  return ::operator new(size);  

16.}  


事实上,所有希望实现专属set_new_handler的类在这方面的实现基本没有差异,因此我们其实可以把它总结成一个基类: 

Cpp代码  

1. template<typename T>  

2. class NewHandlerSupport {  

3.   public:  

4.     static std::new_handler set_new_handler( std::new_handler p )throw();  

5.     static void* operator new( std::size_t size ) throw( std::bad_alloc );  

6.     ...  

7.   private:  

8.     static std::new_handler currentHandler;  

9. };  

10.template<typename T>  

11.std::new_handler  

12.NewHandlerSupport<T>::set_new_handler( std::new_handler p )throw(){  

13.  std::new_handler oldHandler = currentHandler;  

14.  currentHandler = p;  

15.  return oldHandler;  

16.}  

17.  

18.template<typename T>  

19.void*   

20.NewHandlerSupport<T>::operator*( std::size_t size ) throw( std::bad_alloc ){  

21.  NewHandlerHolder h(std::set_new_handler(currentHandler));  

22.  return ::operator new(size);  

23.}  


这里的templateT完全没有被使用,但是,它确保了每一个derivedclass获得一个实体互异的currentHandler 

最后,可以使用 

Cpp代码  

1. Widget* pw2 = new (std::nothrow) Widget;  


来获得返回0而不是抛出异常的operator new,但是它只保证operator new不抛出异常,并不保证后续的构造函数不抛出异常,要知道,operator new分配完内存后,还会使用构造函数初始化这片内存。而nothrow只能保证在分配内存时不抛出异常。 

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

为什么会想要替换编译器提供的operator newoperator delete呢?下面是三个常见的理由: 
1)用来检测运用上的错误。如果将“new所得内存delete”却不幸失败,会导致内存泄漏。如果在“new所得内存身上多次delete则会导致不确定行为。如果让operator new持有一串动态分配所得的地址,而operator delete将地址从中移动,便可以很容易地检测出上述错误用法。另外可以在operator new时超额分配内存,以额外空间放置特定的byte patternoperator delete便得以检查上述签名是否原封不动,若否的话说明在分配区的某个生命时间发生了overrununderrun。可以通过operator delete将出错的指针载入日志。 
2)为了强化效能。编译器所带的operator newoperator delete主要用于一般目的,它处理的内存请求有时很大,有时很小,它必须处理大数量短命对象的持续分配和归还。它们必须考虑碎片问题。定制版的operator newoperator delete通常在性能上用过缺省版本,它们运行得比较快,需要的内存比较少。 
3)为了收集使用上的统计数据。在定制特定newdelete之前,我们应该收集软件如何使用其动态内存:分配区块的大小分布如何?寿命分布如何?它们倾向于使用FIFO还是LIFO还是随机次序来分配和归还?它们的运用形态是否随时间改变?等等。 
4)为了弥补缺省分配器中的非最佳齐位。 
5)为了将相关对象成簇集中。 
6)为了获得非传统的行为。 

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

operator new必须遵守的规矩:返回正确的值,内存不足时必得调用new-handling函数,必须有对付零内存的准备,还需避免不慎掩盖正常形式的new。C++规定,即使客户要求0 bytes,operator new也得返回一个合法指针。下面是个non-member operator new的伪码: 

Cpp代码  

1. void* operator new( std::size_t, size ) throw (std::bad_alloc ){  

2.   using namespace std;  

3.   if( size == 0 )   //处理0 byte申请  

4.     size = 1;  

5.   whiletrue ){  

6.     //尝试分配size bytes  

7.     void* pMem = malloc(size);  

8.     if( pMem )      //成功分配  

9.       return pMem;  

10.    //分配失败  

11.    //获取当前的new_handler  

12.    new_handler globalHandler = set_new_handler(0);  

13.    set_new_handler( globalHandler );  

14.  

15.    if( globalHandler ) (*globalHandler)();  

16.    else throw std::bad_alloc();  

17.  }  

18.}  


对于class专属的operator new,还必须注意到它很可能被其派生类继承。而写出定制型内存管理器的一个最常见理由是为针对某特定class的对象分配行为提供最优化,却不是为了该class的任何派生类。也就是说,针对class X而设计的operator new,其行为很典型地只为大小刚好为sizeof(X)对象而设计。然而一旦被继承下去,有可能基类的operator new被调用用以分配派生类对象: 

Cpp代码  

1. class Base{  

2.   public:  

3.     static void* operator new(std::size_t size) throw( std::bad_alloc );  

4.     ...  

5. };  

6. class Derieved : public Base{  

7.   ...  

8. };  

9.   

10.Derieved* p = new Derieved; //这里调用的是Base::operator new  

11.//因此,假如operator new是被专门设计用以返回Base对象大小的内存  

12.//有必要为此情况做出一点处理:  

13.void* Base::operator new( std::size_t size ) throw ( std::bad_alloc ){  

14.  if( size != sizeof(Base) )  

15.    //注意,sizeof(Base)一定不为0,所以size0的情况将交给::operator new  

16.    return ::operator new(size);  

17.  ...  

18.}  


如果你打算控制class专属之arrays内存分配行为,那么便需要实现operator new的array兄弟版:operator new[],这时你唯一需要做的一件事就是分配一块未加工内存。 
至于operator delete,唯一需要记住的是:C++保证“删除null指针永远安全”。另外,如果你的class专属operator new将大小有误的分配行为转交::operator new执行,那么也必须将大小有误的删除行为转交::operator delete: 

Cpp代码  

1. class Base{  

2.   public:  

3.     static void* operator new( std::size_t size ) throw( std::bad_alloc );  

4.     static void operator deletevoid* rawMemory, std::size_t size ) throw();  

5.     ...  

6. };  

7.   

8. void Base::operator deletevoid* rawMemory, std::size_t size ) throw(){  

9.   if( rawMemory == 0 )  

10.    return;  

11.  if( size != sizeof(Base){  

12.      ::operator delete(rawMemory);  

13.      return;  

14.  }  

15.  return;  

16.}  

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

所谓的placement new,是相对于正常的operator new而言的,或者从广义上说,应该是“被重载的operator new”(我个人是这么感觉的)。 
正常的operator new及其相应的operator delete版本如下: 

Cpp代码  

1. void* operator new( std::size_t ) throw ( std::bad_alloc );  

2. void operator delete(  void* rawMemory ) throw ();  

3. //class作用域中的delete,包含一个大小  

4. void operator deletevoid* rawMemory, std::size_t size )throw();  


new表达式: 

Cpp代码  

1. Widget* pw = new Widget;  


实际调用了两个函数,一个是用以分配内存的operator new,一个是Widget的default构造函数。 
假设其中第一个函数调用成功,即内存已经被分配,第二个函数却抛出异常。此时,pw尚未被赋值,被内存已经被分配,但是我们没有得到指向该内存的指针,因此我们没有能力释放被分配的内存。因此,释放内存的责任就落在了C++运行期系统上。运行期系统将会找到与刚才调用的operator new相配对的operator delete来执行这一任务。 
假设我们为Widget写了一个专属的operator new,要求接受一个ostream用以记录分配信息: 

Cpp代码  

1. class Widget{  

2.   public:  

3.     static void* operator new( std::size_t size, std::ostream& log )  

4.       throw( std::bad_alloc );  

5.     ...  

6. };  


“如果operator new接受的参数除了一定会有的那个size_t之外还有其他,这便是个所谓的placement new”。 
最早的placement new版本是一个“接受一个指针指向对象被构造之处”的operator new,其长相如下: 

Cpp代码  

1. void* operator new( std::size_tvoid* pMemory ) throw();  


它的用途之一是负责在vector的未使用空间上创建对象。 
如果在调用 

Cpp代码  

1. Widget* pw = new (std::err) Widget;  


时Widget构造函数抛出异常,C++运行期系统将自动调用与operatornew相对应的版本来释放内存。所谓的“相对应的版本”,是指“参数个数和类型都与operator new相同”的某个operator delete,因此,当你定义了一个placementnew时,你必须定义一个对应的placement delete,否则系统找不到对应的delete,无法执行内存回收工作,那么内存就泄漏了。 
另外,operatornew函数的名字匹配规则同一般的成员函数一样,如果你只在基类里声明了一个placement new,关于基类的new调用将无法使用正常版本;然后如果你又在继承自该基类的派生类里重新声明正常形式的new,基类的placement new也将不能默认作用在该派生类上。总之就是,operator new的名字匹配规则跟一般的成员函数是一样的。 
placement delete只有在“伴随placement new调用而触发的构造函数”出现异常时才会被调用。 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

necrazy

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值