📄 8660.htm
字号:
<p>这个版本的operator new将会工作得非常好。它为airplane对象分配的内存要比缺省operator new更少,而且运行得更快,可能会快2次方的等级。这没什么奇怪的,通用型的缺省operator new必须应付各种大小的内存请求,还要处理内部外部的碎片;而你的operator new只用操作链表中的一对指针。抛弃灵活性往往可以很容易地换来速度。</p>
<p>下面我们将讨论operator delete。还记得operator delete吗?本条款就是关于operator delete的讨论。但直到现在为止,airplane类只声明了operator new,还没声明operator delete。想想如果写了下面的代码会发生什么:</p>
<p>airplane *pa = new airplane; // 调用<br> // airplane::operator new<br>...</p>
<p>delete pa; // 调用 ::operator delete</p>
<p>读这段代码时,如果你竖起耳朵,会听到飞机撞毁燃烧的声音,还有程序员的哭泣。问题出在operator new(在airplane里定义的那个)返回了一个不带头信息的内存的指针,而operator delete(缺省的那个)却假设传给它的内存包含头信息。这就是悲剧产生的原因。</p>
<p>这个例子说明了一个普遍原则:operator new和operator delete必须同时写,这样才不会出现不同的假设。如果写了一个自己的内存分配程序,就要同时写一个释放程序。(关于为什么要遵循这条规定的另一个理由,参见article on counting objects一文的the sidebar on placement章节)</p>
<p>因而,继续设计airplane类如下:</p>
<p>class airplane { // 和前面的一样,只不过增加了一个<br>public: // operator delete的声明<br> ...</p>
<p> static void operator delete(void *deadobject,<br> size_t size);</p>
<p>};</p>
<p>// 传给operator delete的是一个内存块, 如果<br>// 其大小正确,就加到自由内存块链表的最前面<br>// <br>void airplane::operator delete(void *deadobject,<br> size_t size)<br>{<br> if (deadobject == 0) return; // 见条款 8</p>
<p> if (size != sizeof(airplane)) { // 见条款 8<br> ::operator delete(deadobject);<br> return;<br> }</p>
<p> airplane *carcass =<br> static_cast<airplane*>(deadobject);</p>
<p> carcass->next = headoffreelist;<br> headoffreelist = carcass;<br>}</p>
<p>因为前面在operator new里将“错误”大小的请求转给了全局operator new(见条款8),那么这里同样要将“错误”大小的对象交给全局operator delete来处理。如果不这样,就会重现你前面费尽心思想避免的那种问题——new和delete句法上的不匹配。</p>
<p>有趣的是,如果要删除的对象是从一个没有虚析构函数的类继承而来的,那传给operator delete的size_t值有可能不正确。这就是必须保证基类必须要有虚析构函数的原因,此外条款14还列出了第二个、理由更充足的原因。这里只要简单地记住,基类如果遗漏了虚拟构函数,operator delete就有可能工作不正确。</p>
<p>所有一切都很好,但从你皱起的眉头我可以知道你一定在担心内存泄露。有着大量开发经验的你不会没注意到,airplane的operator new调用::operator new 得到了大块内存,但airplane的operator delete却没有释放它们。内存泄露!内存泄露!我分明听见了警钟在你脑海里回响。</p>
<p>但请仔细听我回答,这里没有内存泄露!</p>
<p>引起内存泄露的原因在于内存分配后指向内存的指针丢失了。如果没有垃圾处理或其他语言之外的机制,这些内存就不会被收回。但上面的设计没有内存泄露,因为它决不会出现内存指针丢失的情况。每个大内存块首先被分成airplane大小的小块,然后这些小块被放在自由链表上。当客户调用airplane::operator new时,小块被自由链表移除,客户得到指向小块的指针。当客户调用operator delete时,小块被放回到自由链表上。采用这种设计,所有的内存块要不被airplane对象使用(这种情况下,是由客户来负责避免内存泄露),要不就在自由链表上(这种情况下内存块有指针)。所以说这里没有内存泄露。</p>
<p>然而确实,::operator new返回的内存块是从来没有被airplane::operator delete释放,这个内存块有个名字,叫内存池。但内存泄漏和内存池有一个重要的不同之处。内存泄漏会无限地增长,即使客户循规蹈矩;而内存池的大小决不会超过客户请求内存的最大值。</p>
<p>修改airplane的内存管理程序使得::operator new返回的内存块在不被使用时自动释放并不难,但这里不会这么做,这有两个原因:</p>
<p>第一个原因和你自定义内存管理的初衷有关。你有很多理由去自定义内存管理,最基本的一条是你确认缺省的operator new和operator delete使用了太多的内存或(并且)运行很慢。和采用内存池策略相比,跟踪和释放那些大内存块所写的每一个额外的字节和每一条额外的语句都会导致软件运行更慢,用的内存更多。在设计性能要求很高的库或程序时,如果你预计内存池的大小会在一个合理的范围之内,那采用内存池的方法再好不过了。</p>
<p>第二个原因和处理一些不合理的程序行为有关。假设airplane的内存管理程序被修改了,airplane的operator delete可以释放任何没有对象存在的大块的内存。那看下面的程序:</p>
<p>int main()<br>{<br> airplane *pa = new airplane; // 第一次分配: 得到大块内存,<br> // 生成自由链表,等</p>
<p> delete pa; // 内存块空;<br> // 释放它</p>
<p> pa = new airplane; // 再次得到大块内存,<br> // 生成自由链表,等</p>
<p> delete pa; // 内存块再次空,<br> // 释放</p>
<p> ... // 你有了想法...</p>
<p> return 0;<br>}</p>
<p>这个糟糕的小程序会比用缺省的operator new和operator delete写的程序运行得还慢,占用还要多的内存,更不要和用内存池写的程序比了。</p>
<p>当然有办法处理这种不合理的情况,但考虑的特殊情况越多,就越有可能要重新实现内存管理函数,而最后你又会得到什么呢?内存池不能解决所有的内存管理问题,在很多情况下是很适合的。</p>
<p>实际开发中,你会经常要给许多不同的类实现基于内存池的功能。你会想,“一定有什么办法把这种固定大小内存的分配器封装起来,从而可以方便地使用”。是的,有办法。虽然我在这个条款已经唠叨这么长时间了,但还是要简单介绍一下,具体实现留给读者做练习。</p>
<p>下面简单给出了一个pool类的最小接口(见条款18),pool类的每个对象是某类对象(其大小在pool的构造函数里指定)的内存分配器。</p>
<p>class pool {<br>public:<br> pool(size_t n); // 为大小为n的对象创建<br> // 一个分配器</p>
<p><br> void * alloc(size_t n) ; // 为一个对象分配足够内存<br> // 遵循条款8的operator new常规</p>
<p> void free( void *p, size_t n); // 将p所指的内存返回到内存池;<br> // 遵循条款8的operator delete常规</p>
<p> ~pool(); // 释放内存池中全部内存</p>
<p>};</p>
<p>这个类支持pool对象的创建,执行分配和释放操作,以及被摧毁。pool对象被摧毁时,会释放它分配的所有内存。这就是说,现在有办法避免airplane的函数里所表现的内存泄漏似的行为了。然而这也意味着,如果pool的析构函数调用太快(使用内存池的对象没有全部被摧毁),一些对象就会发现它正在使用的内存猛然间没了。这造成的结果通常是不可预测的。</p>
<p>有了这个pool类,即使java程序员也可以不费吹灰之力地在airplane类里增加自己的内存管理功能:</p>
<p>class airplane {<br>public:</p>
<p> ... // 普通airplane功能</p>
<p> static void * operator new(size_t size);<br> static void operator delete(void *p, size_t size);</p>
<p>private:<br> airplanerep *rep; // 指向实际描述的指针<br> static pool mempool; // airplanes的内存池</p>
<p>};</p>
<p>inline void * airplane::operator new(size_t size)<br>{ return mempool.alloc(size); }</p>
<p>inline void airplane::operator delete(void *p,<br> size_t size)<br>{ mempool.free(p, size); }</p>
<p>// 为airplane对象创建一个内存池,<br>// 在类的实现文件里实现<br>pool airplane::mempool(sizeof(airplane));</p>
<p>这个设计比前面的要清楚、干净得多,因为airplane类不再和非airplane的代码混在一起。union,自由链表头指针,定义原始内存块大小的常量都不见了,它们都隐藏在它们应该呆的地方——pool类里。让写pool的程序员去操心内存管理的细节吧,你的工作只是让airplane类正常工作。</p>
<p>现在应该明白了,自定义的内存管理程序可以很好地改善程序的性能,而且它们可以封装在象pool这样的类里。但请不要忘记主要的一点,operator new和operator delete需要同时工作,那么你写了operator new,就也一定要写operator delete。<br>
</p>
</DIV></div></div>
</center></BODY></HTML>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -