为什么有必要写operatornew和operatordelete? 答案通常是:为了效率。缺省的operatornew和operatordelete具有非常好的通用性,它的这种灵活性也使得在某些特定的场合下,可以进一步改善它的性能。尤其在那些需要动态分配大量的但很小的对象的应用程序里,情况更是如此。 例如:类Airplane只包含一个指针,指向的是飞机对象的实际描述(条款34进行说明):classAirplaneRep{。。。};表示一个飞机对象classAirplane{public:。。。private:AirplaneR指向实际描述}; 一个Airplane对象并不大,它只包含一个指针(正如条款14和M24所说明的,如果 Airplane类声明了虚函数,会隐式包含第二个指针)。但当调用operatornew来分配一个 Airplane对象时,得到的内存可能要比存储这个指针(或一对指针)所需要的多。原因在于operatornew和operatordelete之间需要互相传递信息。 因为缺省版本的operatornew是一种通用型的内存分配器,它必须可以分配任意大小的内存块。同样,operatordelete也必须可以释放任意大小的内存块。 operatordelete想弄清它要释放的内存有多大,就必须知道当初operatornew分配的内存有多大。有一种常用的方法可以让operatornew来告诉operatordelete当初分配的内存大小是多少,就是在它所返回的内存里预先附带一些额外信息,用来指明被分配的内存块的大小。得到的是pa内存块大小数据Airplane对象的内存AirplanepanewA而不是paAirplane对象的内存 对于像Airplane这样很小的对象来说,这些额外的数据信息会使得动态分配对象时所需要的的内存的大小翻倍(特别是类里没有虚拟函数的时候)。 如果软件运行在内存宝贵的环境中,就承受不起这种奢侈的内存分配方案了。为Airplane类专门写一个operatornew,就可以利用Airplane大小相等的特点,不必在每个分配的内存块上加上附带信息了。 实现自定义operatornew思路:先让缺省operatornew分配一些大块的原始内存,每块的大小都足以容纳很多个Airplane对象。Airplane对象的内存块就取自这些大的内存块。当前没被使用的内存块被组织成链表称为自由链表以备未来Airplane使用。听起来好象每个对象都要承担一个next域的开销(用于支持链表),但不会:rep域的空间也被用来存储next指针(因为只是作为Airplane对象来使用的内存块才需要rep指针;同样,只有没作为Airplane对象使用的内存块才需要next指针),这可以用union来实现。具体实现:修改Airplane的定义,从而支持自定义的内存管理。classAirplane{修改后的类支持自定义的内存管理public:staticvoidoperatornew(sizetsize);。。。private:union{AirplaneR用于被使用的对象A用于没被使用的(在自由链表中)对象};指定一个大的内存块中放多少个Airplane对象,在后面初始化staticconstintBLOCKSIZE;大内存块大小staticAirplaneheadOfFreeL跟踪自由链表的表头表头指针声明为静态成员很重要,因为整个类只有一个自由链表,而不是每个Airplane对象都有。}; 下面该写operatornew函数了:voidAirplane::operatornew(sizetsize){把错误大小的请求转给::operatornew()处理;详见条款8if(size!sizeof(Airplane))return::operatornew(size);Airplanepp指向自由链表的表头headOfFreeLp若合法,则将表头移动到它的下一个元素if(p)headOfFreeLelse{自由链表为空,则分配一个大的内存块,可以容纳BLOCKSIZE个Airplane对象AirplanenewBlockstaticcast(::operatornew(BLOCKSIZEsizeof(Airplane)));将每个小内存块链接起来形成一个新的自由链表跳过第0个元素,因为它要被返回给operatornew的调用者for(inti1;iBLOCKSIZE1;i)newBlock〔i〕。nextnewBlock〔i1〕;用空指针结束链表newBlock〔BLOCKSIZE1〕。next0;p设为表的头部,headOfFreeList指向的内存块紧跟其后pnewBheadOfFreeListnewBlock〔1〕;}} 如果你读了条款8,就会知道在operatornew不能满足内存分配请求时,需要有newhandler函数和异常有关的例行性动作之类的,只是::operatornew里面已经有这样的处理方案, Airplane类中不需要了。 有了operatornew,下面要做的就是给出Airplane的静态数据成员的定义:AirplaneAirplane::headOfFreeL静态成员的初始值都被缺省设为0constintAirplane::BLOCKSIZE512; 这个版本的operatornew将会工作得非常好。它为Airplane对象分配的内存要比缺省operatornew更少,而且运行得更快,可能会快2次方的等级。这没什么奇怪的,通用型的缺省operatornew必须应付各种大小的内存请求,还要处理内部外部的碎片;而你的operatornew只用操作链表中的一对指针。抛弃灵活性往往可以很容易地换来速度。 下面我们将讨论operatordelete。还记得operatordelete吗?本条款就是关于operatordelete的讨论。但直到现在为止,Airplane类只声明了operatornew,还没声明operatordelete。想想如果写了下面的代码会发生什么:AirplanepanewA调用Airplane::operatornew。。。调用::operatordelete 读这段代码时,如果你竖起耳朵,会听到飞机撞毁燃烧的声音,还有程序员的哭泣。问题出在operatornew(在Airplane里定义的那个)返回了一个不带头信息的内存的指针,而operatordelete(缺省的那个)却假设传给它的内存包含头信息。这就是悲剧产生的原因。 这个例子说明了一个普遍原则:operatornew和operatordelete必须同时写(另一个理由,参见articleoncountingobjects一文的thesidebaronplacement章节) 因而,继续设计Airplane类如下:classAirplane{public:。。。和前面的一样,只不过增加了一个,operatordelete的声明staticvoidoperatordelete(voiddeadObject,sizetsize);};传给operatordelete的是一个内存块,如果其大小正确,就加到自由内存块链表的最前面voidAirplane::operatordelete(voiddeadObject,sizetsize){if(deadObject0)见条款8if(size!sizeof(Airplane)){见条款8::operatordelete(deadObject);}Airplanecarcassstaticcast(deadObject);carcassnextheadOfFreeLheadOfFreeL} 因为前面在operatornew里将错误大小的请求转给了全局operatornew(见条款8),那么这里同样要将错误大小的对象交给全局operatordelete来处理。如果不这样,就会重现你前面费尽心思想避免的那种问题new和delete句法上的不匹配。 如果删除的对象是从没有虚析构函数的类继承而来的,那传给operatordelete的sizet值有可能不正确。这就是必须保证基类必须要有虚析构函数的原因。 所有一切都很好,但从你皱起的眉头我可以知道你一定在担心内存泄露。有着大量开发经验的你不会没注意到,operatornew调用::operatornew得到了大块内存,但operatordelete却没有释放它们。内存泄露!内存泄露! 但这里没有内存泄露! 引起内存泄露的原因在于内存分配后指向内存的指针丢失了。如果没有垃圾处理或其他语言之外的机制,这些内存就不会被收回。但上面的设计没有内存泄露,因为它决不会出现内存指针丢失的情况。每个大内存块首先被分成Airplane大小的小块,然后这些小块被放在自由链表上。当客户调用Airplane::operatornew时,小块被自由链表移除,客户得到指向小块的指针。当客户调用operatordelete时,小块被放回到自由链表上。采用这种设计,所有的内存块要不被Airplane对象使用(这种情况下,是由客户来负责避免内存泄露),要不就在自由链表上(这种情况下内存块有指针)。所以说这里没有内存泄露。 然而确实,::operatornew返回的内存块是没有被完全释放。但这个内存块叫内存池:内存泄漏会无限地增长,即使客户循规蹈矩;而内存池的大小决不会超过客户请求内存的最大值。 修改Airplane的内存管理程序使得::operatornew返回的内存块在不被使用时自动释放并不难,但这里不会这么做,这有两个原因:第一个原因和自定义内存管理的初衷有关。 自定义内存管理,最基本的一条是你确认缺省的operatornew和operatordelete使用了太多的内存或(并且)运行很慢。和采用内存池策略相比,跟踪和释放那些大内存块所写的每一个额外的字节和每一条额外的语句都会导致软件运行更慢,用的内存更多。在设计性能要求很高的库或程序时,如果你预计内存池的大小会在一个合理的范围之内,那采用内存池的方法再好不过了。第二个原因和处理一些不合理的程序行为有关。 假设Airplane的内存管理程序被修改了,Airplane的operatordelete可以释放任何没有对象存在的大块的内存。那看下面的程序:intmain(){AirplanepanewA第一次分配:得到大块内存,生成自由链表,等内存块空;释放它panewA再次得到大块内存,生成自由链表,等内存块再次空,释放。。。你有了想法。。。return0;} 这个糟糕的小程序会比用缺省的operatornew和operatordelete写的程序运行得还慢,占用还要多的内存,更不要和用内存池写的程序比了。 当然有办法处理这种不合理的情况,但考虑的特殊情况越多,就越有可能要重新实现内存管理函数,而最后你又会得到什么呢?内存池不能解决所有的内存管理问题,在很多情况下是很适合的。 实际开发中,你会经常要给许多不同的类实现基于内存池的功能。你会想,一定有什么办法把这种固定大小内存的分配器封装起来,从而可以方便地使用。是的,有办法。虽然我在这个条款已经唠叨这么长时间了,但还是要简单介绍一下,具体实现留给读者做练习。 下面简单给出了一个Pool类的最小接口(见条款18),Pool类的每个对象是某类对象(其大小在Pool的构造函数里指定)的内存分配器。classPool{public:Pool(sizetn);为大小为n的对象创建一个分配器voidalloc(sizetn);为一个对象分配足够内存,遵循条款8的operatornew常规将p所指的内存返回到内存池;遵循条款8的operatordelete常规voidfree(voidp,sizetn);Pool();释放内存池中全部内存}; 这个类支持Pool对象的创建,执行分配和释放操作,以及被摧毁。Pool对象被摧毁时,会释放它分配的所有内存。这就是说,现在有办法避免Airplane的函数里所表现的内存泄漏似的行为了。然而这也意味着,如果Pool的析构函数调用太快(使用内存池的对象没有全部被摧毁),一些对象就会发现它正在使用的内存猛然间没了。这造成的结果通常是不可预测的。有了这个Pool类,即使Java程序员也可以不费吹灰之力地在Airplane类里增加自己的内存管理功能:classAirplane{public:。。。普通Airplane功能staticvoidoperatornew(sizetsize);staticvoidoperatordelete(voidp,sizetsize);private:AirplaneR指向实际描述的指针staticPoolmemPAirplanes的内存池};inlinevoidAirplane::operatornew(sizetsize){returnmemPool。alloc(size);}inlinevoidAirplane::operatordelete(voidp,sizetsize){memPool。free(p,size);}为Airplane对象创建一个内存池,在类的实现文件里实现PoolAirplane::memPool(sizeof(Airplane)); 这个设计比前面的要清楚、干净得多,因为Airplane类不再和非Airplane的代码混在一起。union,自由链表头指针,定义原始内存块大小的常量都不见了,它们都隐藏在它们应该呆的地方Pool类里。让写Pool的程序员去操心内存管理的细节吧,你只是让Airplane类正常工作。 现在应该明白了,自定义的内存管理程序可以很好地改善程序的性能,而且它们可以封装在象Pool这样的类里。但请不要忘记主要的一点,operatornew和operatordelete需要同时工作,那么你就写了operatornew,就也一定要写operatordelete。