【C++】定制new和delete

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(竞速状态)出现机会.

原文链接: https://www.cnblogs.com/dwdxdy/archive/2012/07/28/2613350.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/57068

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月9日 上午7:52
下一篇 2023年2月9日 上午7:52

相关推荐