new和delete是C++中进行内存分配和释放的基本接口,程序员对内存的管理就是依靠这两个接口完成.
既然C++的编译器已经提供了new和delete接口,那我们为什么重新定制new和delete.
这就引出第一个问题,什么情况下需要重新定制new和delete?(肯定是编译器提供的new和delete满足需求的时候)
1.用来检测运用上的错误
编程的过程中,可能出现导致数据"overruns"(写入点在分配区块尾端之后)或"underruns"(写入点在分配区块起点之前).如果我们自行定义一个operator news,便可超额分配内存,以额外空间(位于客户所得区块之前或后)放置特定的byte patterns.operator deteles便可以检查上述签名是否没有变化,若否就表示在分配区某个生命时间点发生了overrun和underrun,此时operator delete便可以记录这个事实.
2.为了强化效能
各种各样的应用对内存管理器的要求,也各不相同,编译器为了满足各种各样的分配形态,其提供的operator news和operator deletes采取中庸之道,它们的工作对每个人都是适度地好,但不对特定的任何需要有最佳表现.如果对程序的动态内存运用型态有深刻的了解,定制operator new和operaotr delete,其性能可比缺省的好.
3.为了收集使用时的统计数据
在这制自己的operator new和operator delete之前,我们必须了解程序是如何使用动态内存的.分配区块的大小分布如何?存命分布如何?它们倾向于以FIFO次序或LIFO次序或随机次序来分配和归还等,为了收集这些信息,我们自定义operator new和operator delete.
4.为了增加分配和归还的速度
泛用型分配器往往比定制型分配器慢,特别是当定制型分配器专门针对某特定类型对象而设计时.当然,在获得"operator new"和"operator delete"有加快程序速度的价值这个结论之前,我们必须分析程序,确认程序瓶颈的确发生在那些内存函数身上.
5.为了降低缺省内存管理器带来的空间额外开销
泛用型分配器往往使用更多的内存,尤其是分配小型对象时,会造成资源浪费,因为它们常常在每一个分配区块身上招引某些额外开销.针对小型对象而开发的分配器可以消除这样的额外开销.
6.为了弥补缺省分配中的非最佳齐位
在x86体系结构上doubles的访问最是快速,如果它们都是8-byte齐位.但编译器自带的operator news并不保证对动态分配而得的doubles采取8-bytes齐位.在这种情况下,将缺省的operator new替换为一个8-byte齐位保证版,可导致程序效率大幅提升.
7.为了将相关对象成簇集中
如果我们知道特定的某个数据结构往往被一起使用,而我们又希望在处理这些数据据时将"内存页错误"的频率降至最低,那么我们可以为些数据结构创建另一个heap,让它们集中在尽可能少的内存页上.
8.为了获得非传统的行为
有时候我们需要operator news和deletes做编译器附带版没有做的某些事情.例如分配和归还共享内存内的区块,但唯一能够管理该内存的只有C API函数,因而写一个定制的new和delete,在C API的基础上添加一层C++的外套.
现在我们知道了定制new和delete的时机,我们就要开始动手定制new和delete.
在动手定制new和delete之前,我们需要了解编译器提供new和delete的机制,相当于在打造自己的轮子之前,先看看现在的轮子,参考现有的实现.
具体的实现,这里不作详细介绍,只是介绍一下里面new-handler处理机制.
编译器提供的new,在内存不足的情况下,自己的一套处理机制.当operator new抛出异常以反映一个未满足的内存需求之前,它会调用一个客户指定的错误处理函数,new-handler.
当我们自己定制new和delete时,也得提供相应的处理机制.我们可以利用set_new_handler来指定自己的错误处理函数.具体使用方法如下:
1 namespace std { 2 typedef void (*new_handler)(); 3 new_handler set_new_handler(new_handler p) throw(); 4 } 5 6 void outOfMem() 7 { 8 std::cerr<<"Unable to satisfy request for memory\n"; 9 std::abort(); 10 } 11 int main() 12 { 13 std::set_new_handler(outOfMem); 14 int* pBigDataArray = new int[1000000000L]; 15 ... 16 }
关键是我们该如何实现错误处理函数,让其更满足我们的需求.一般一个设计良好的new-handler函数必须做以下事情:
1.让更多内存可被使用.
2.安装另一个new-handler.
3.卸除new-handler.
4.抛出bad_alloc(或派生自bad_alloc)的异常.
5.不返回.通常调用abort或exit.
虽然我们可以通过set_new_handler指定错误处理函数,以不同的方式处理内存分配失败的情况.
但每次调用set_new_handler都是设置全局的错误处理函数,当我们需针对不同类的对象设定不同的错误处理函数时,需要调用set_new_handler多次.
此外,为了保证一般情况下,默认的错误处理函数被调用,我们还必须在设置不同的错误处理函数之前,保存默认的错误处理函数,以便重新设定回原始状态.
如何才能保证以不同的方式处理内存分配失败的情况,其处理内存方式依据对象的类型而定,即class专属的new-handlers.
下面介绍一下,如何实现class专属的new-handler.
实现思路:
令每个class提供自己的set_new_handler和operator new,其中set_new_handler使用户可以指定class专属的new-handler,operator new则确保在分配class对象内存的过程中以class专属的new-handler替换global new-handler.
operator new具体做以下事情:
1.调用标准set_new_handler,将class的专属new-handler安装为global new-handler.
2.调用 global operator new,执行实际内存分配.如果分配失败,global operator new 会调用class的专属new-handler.
如果global operator new最终无法分配足够内存,会抛出一个bad_alloc异常.在此情况下,需要确保原本的new-handler能够被恢复,因而参用对象管理new-handler.
new-handler的恢复过程时,是由class的析构函数完成的,当对象脱离作用域时,调用析构函数,恢复原本的new-handler.
1 class NewHandlerHolder { 2 public: 3 explicit NewHandlerHolder(std::new_handler nh) 4 :handler(nh){} 5 ~NewHandlerHolder() 6 { 7 std::set_new_handler(handler); 8 } 9 private: 10 std::new_handler handler; 11 NewHandlerHolder(const NewHandlerHolder&); 12 NewHandlerHolder& operator=(const NewHandlerHolder&); 13 }; 14 template<typename T> 15 class NewHandlerSupport { 16 public: 17 static std::new_handler set_new_handler(std::new_handler p) throw(); 18 static void* operator new(std::size_t size) throw(std::bad_alloc); 19 ... 20 private: 21 static std::new_handler currentHandler; 22 }; 23 template<typename T> 24 std::new_handler 25 NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw() 26 { 27 std::new_handler oldHandler = currentHandler; 28 currentHandler = p; 29 return oldHandler; 30 } 31 32 template<typename T> 33 void* NewHandlerSupport<T>::operator new(std::size_t size) 34 { 35 NewHandlerHolder h(std::set_new_handler(currentHandler)); 36 return ::operator new(size); 37 } 38 39 template<typename T> 40 std::new_handler NewHandlerSupport<T>::currentHandler = 0; 41 42 class Widget: public NewHandlerSupport<Widget> { 43 ... 44 };
了解了编译器自身的new和delete实现机制,我们需要开始定制new和delete.
定制过程中,为了保证定制的new和delete的一致性,需要保证其遵守一些规则.
1.operator new必须得返回正确的值,内存不足时必须得调用new-handling函数.
2.operator new必须得对付零内存需求的准备.
编译器提供的operator new中,即使客户要求0bytes,operator new也得返回一个合法指针.定制的new也得合理的处理0byte需求.
3.operator new必须得处理派生类中调用基类operator new的情况.
处理方法是将内存申请量错误的调用行为改采标准operator new.
1 void* Base::operator new(std::size_t size) throw(std::bad_alloc) 2 { 3 if(size != sizeof(Base)) 4 return ::operator new(size); 5 ... 6 }
4.operator delete需要保证"删除null指针永远安全".
1 void Base::operator delete(void* rawMemory,std::size_t) throw() 2 { 3 if(rawMemory == 0) return; 4 if(size != sizeof(Base)) 5 { 6 ::operator delete(rawMemory); 7 return; 8 } 9 ... 10 return; 11 }
此外,定制new和delete应注意线程安全问题.
由于heap是一个可被改动的全局性资源,因此多线程系统充斥着访问这一类资源的race conditions(竞速状态)出现机会.