EffectiveC条款10如写了operatornew就要写operatordelete
为什么有必要写operator new 和 operator delete?
答案通常是:为了效率。缺省的 operator new和 operator delete 具有非常好的通用性,它的这种灵活性也使得在某些特定的场合下,可以进一步改善它的性能。尤其在那些需要动态分配大量的但很小的对象的应用程序里,情况更是如此。
例如:类 Airplane 只包含一个指针,指向的是飞机对象的实际描述(条款 34 进行说明): class AirplaneRep { ... }; // 表示一个飞机对象 // class Airplane { public: ... private: AirplaneRep *rep; // 指向实际描述 };
一个 Airplane 对象并不大,它只包含一个指针(正如条款 14 和 M24 所说明的,如果
Airplane 类声明了虚函数,会隐式包含第二个指针)。但当调用operator new 来分配一个
Airplane 对象时,得到的内存可能要比存储这个指针(或一对指针)所需要的多。原因在于operator new 和 operator delete 之间需要互相传递信息 。
因为缺省版本的 operator new 是一种通用型的内存分配器,它必须可以分配任意大小的内存块。同样,operator delete 也必须可以释放任意大小的内存块。
operator delete 想弄清它要释放的内存有多大,就必须知道当初 operator new分配的内存有多大。 有一种常用的方法可以让 operator new 来告诉 operator delete 当初分配的内存大小是多少,就是在它所返回的内存里预先附带一些额外信息,用来指明被分配的内存块的大小。 // 得到的是pa——> 内存块大小数据 + Airplane 对象的内存 Airplane *pa = new Airplane;//而不是pa——> Airplane 对象的内存
对于像 Airplane 这样很小的对象来说,这些额外的数据信息会使得动态分配对象时所需要的的内存的大小翻倍(特别是类里没有虚拟函数的时候)。
如果软件运行在内存宝贵的环境中,就承受不起这种奢侈的内存分配方案了。为 Airplane 类专门写一个 operator new,就可以利用 Airplane大小相等的特点,不必在每个分配的内存块上加上附带信息了。
实现自定义operator new思路:先让缺省operator new 分配一些大块的原始内存,每块的大小都足以容纳很多个 Airplane对象。Airplane 对象的内存块就取自这些大的内存块。当前没被使用的内存块被组织成链表——称为自由链表——以备未来 Airplane 使用。听起来好象每个对象都要承担一个 next 域的开销(用于支持链表),但不会:rep 域的空间也被用来存储 next 指针(因为只是作为 Airplane 对象来使用的内存块才需要 rep 指针;同样,只有没作为 Airplane 对象使用的内存块才需要 next 指针),这可以用 union 来实现。 具体实现:修改 Airplane 的定义,从而支持自定义的内存管理。class Airplane { // 修改后的类 — 支持自定义的内存管理 public: // static void * operator new(size_t size); ... private: union { AirplaneRep *rep; // 用于被使用的对象 Airplane *next; // 用于没被使用的(在自由链表中)对象 }; // 指定一个大的内存块中放多少个Airplane 对象,在后面初始化 static const int BLOCK_SIZE;//大内存块大小 static Airplane *headOfFreeList;//跟踪自由链表的表头 //表头指针声明为静态成员很重要,因为整个类只有一个自由链表,而不是每个 Airplane 对象都有。 };
下面该写 operator new 函数了: void * Airplane::operator new(size_t size) { // 把"错误"大小的请求转给::operator new()处理; // 详见条款 8 if (size != sizeof(Airplane)) return ::operator new(size); Airplane *p = // p 指向自由链表的表头 headOfFreeList; // // p 若合法,则将表头移动到它的下一个元素 // if (p) headOfFreeList = p->next; else { // 自由链表为空,则分配一个大的内存块, // 可以容纳 BLOCK_SIZE 个 Airplane 对象 Airplane *newBlock = static_cast(::operator new(BLOCK_SIZE * sizeof(Airplane))); // 将每个小内存块链接起来形成一个新的自由链表 // 跳过第 0 个元素,因为它要被返回给 operator new 的调用者 for (int i = 1; i < BLOCK_SIZE-1; ++i) newBlock[i].next = &newBlock[i+1]; // 用空指针结束链表 newBlock[BLOCK_SIZE-1].next = 0; // p 设为表的头部,headOfFreeList 指向的内存块紧跟其后 p = newBlock; headOfFreeList = &newBlock[1]; } return p; }
如果你读了条款 8,就会知道在 operator new 不能满足内存分配请求时,需要有 new-handler 函数和异常有关的例行性动作之类的,只是::operator new 里面已经有这样的处理方案,
Airplane类中不需要了。
有了 operator new,下面要做的就是给出 Airplane 的静态数据成员的定义: Airplane *Airplane::headOfFreeList;//静态成员的初始值都被缺省设为 0 const int Airplane::BLOCK_SIZE = 512;
这个版本的 operator new 将会工作得非常好 。它为 Airplane 对象分配的内存要比缺省 operator new 更少,而且运行得更快,可能会快 2 次方的等级。这没什么奇怪的,通用型的缺省 operator new 必须应付各种大小的内存请求,还要处理内部外部的碎片;而你的 operator new 只用操作链表中的一对指针。抛弃灵活性往往可以很容易地换来速度。
下面我们将讨论 operator delete。还记得 operator delete 吗?本条款就是关于 operator delete 的讨论。但直到现在为止,Airplane 类只声明了 operator new,还没声明 operator delete。想想如果写了下面的代码会发生什么: Airplane *pa = new Airplane; // 调用Airplane::operator new ... delete pa; // 调用 ::operator delete
读这段代码时,如果你竖起耳朵,会听到飞机撞毁燃烧的声音,还有程序员的哭泣。问题出在 operator new(在 Airplane 里定义的那个)返回了一个不带头信息的内存的指针,而 operator delete(缺省的那个)却假设传给它的内存包含头信息。这就是悲剧产生的原因。
这个例子说明了一个普遍原则:operator new和 operator delete 必须同时写(另一个理由,参见 article on counting objects 一文的 the sidebar on placement 章节)
因而,继续设计 Airplane 类如下: class Airplane { public: ... // 和前面的一样,只不过增加了一个,operator delete 的声明 static void operator delete(void *deadObject, size_t size); }; // 传给 operator delete 的是一个内存块, 如果其大小正确,就加到自由内存块链表的最前面 void Airplane::operator delete(void *deadObject, size_t size) { if (deadObject == 0) return; // 见条款 8 if (size != sizeof(Airplane)) { // 见条款 8 ::operator delete(deadObject); return; } Airplane *carcass = static_cast(deadObject); carcass->next = headOfFreeList; headOfFreeList = carcass; }
因为前面在 operator new 里将"错误"大小的请求转给了全局 operator new(见条款 8),那么这里同样要将"错误"大小的对象交给全局 operator delete来处理。如果不这样,就会重现你前面费尽心思想避免的那种问题——new 和delete 句法上的不匹配。
如果删除的对象是从没有虚析构函数的类继承而来的,那传给 operator delete 的 size_t值有可能不正确。这就是必须保证基类必须要有虚析构函数的原因。
所有一切都很好,但从你皱起的眉头我可以知道你一定在担心内存泄露。有着大量开发经验的你不会没注意到,operator new 调用::operator new 得到了大块内存,但 operator delete 却没有释放它们。内存泄露!内存泄露!
但这里没有内存泄露!
引起内存泄露的原因在于内存分配后指向内存的指针丢失了。 如果没有垃圾处理或其他语言之外的机制,这些内存就不会被收回。但上面的设计没有内存泄露,因为它决不会出现内存指针丢失的情况。每个大内存块首先被分成Airplane 大小的小块,然后这些小块被放在自由链表上。当客户调用Airplane::operator new 时,小块被自由链表移除,客户得到指向小块的指针。当客户调用 operator delete 时,小块被放回到自由链表上。采用这种设计,所有的内存块要不被 Airplane 对象使用(这种情况下,是由客户来负责避免内存泄露),要不就在自由链表上(这种情况下内存块有指针)。所以说这里没有内存泄露。
然而确实,::operator new 返回的内存块是没有被完全释放。但这个内存块叫内存池:内存泄漏会无限地增长,即使客户循规蹈矩;而内存池的大小决不会超过客户请求内存的最大值。
修改 Airplane 的内存管理程序使得::operator new 返回的内存块在不被使用时自动释放并不难,但这里不会这么做,这有两个原因: 第一个原因和自定义内存管理的初衷有关。
自定义内存管理,最基本的一条是你确认缺省的 operator new和 operator delete 使用了太多的内存或(并且)运行很慢。和采用内存池策略相比,跟踪和释放那些大内存块所写的每一个额外的字节和每一条额外的语句都会导致软件运行更慢,用的内存更多。在设计性能要求很高的库或程序时,如果你预计内存池的大小会在一个合理的范围之内,那采用内存池的方法再好不过了。 第二个原因和处理一些不合理的程序行为有关。
假设 Airplane 的内存管理程序被修改了,Airplane 的 operator delete 可以释放任何没有对象存在的大块的内存。那看下面的程序:int main() { Airplane *pa = new Airplane; // 第一次分配: 得到大块内存,生成自由链表,等 delete pa; // 内存块空; 释放它 pa = new Airplane; // 再次得到大块内存, 生成自由链表,等 delete pa; //内存块再次空, 释放 ... // 你有了想法... return 0; }
这个糟糕的小程序会比用缺省的 operator new和 operator delete 写的程序运行得还慢,占用还要多的内存,更不要和用内存池写的程序比了。
当然有办法处理这种不合理的情况,但考虑的特殊情况越多,就越有可能要重新实现内存管理函数,而最后你又会得到什么呢?内存池不能解决所有的内存管理问题,在很多情况下是很适合的。
实际开发中,你会经常要给许多不同的类实现基于内存池的功能。你会想,"一定有什么办法把这种固定大小内存的分配器封装起来,从而可以方便地使用"。是的,有办法。虽然我在这个条款已经唠叨这么长时间了,但还是要简单介绍一下,具体实现留给读者做练习。
下面简单给出了一个 Pool 类的最小接口(见条款 18),Pool 类的每个对象是某类对象(其大小在 Pool 的构造函数里指定)的内存分配器。 class Pool { public: Pool(size_t n); // 为大小为 n 的对象创建 一个分配器 void * alloc(size_t n) ; // 为一个对象分配足够内存 ,遵循条款 8 的 operator new 常规 // 将 p 所指的内存返回到内存池; 遵循条款 8 的 operator delete常规 void free( void *p, size_t n); ~Pool(); // 释放内存池中全部内存 };
这个类支持 Pool 对象的创建,执行分配和释放操作,以及被摧毁。Pool对象被摧毁时,会释放它分配的所有内存。这就是说,现在有办法避免 Airplane的函数里所表现的内存泄漏似的行为了。然而这也意味着,如果 Pool 的析构函数调用太快(使用内存池的对象没有全部被摧毁),一些对象就会发现它正在使用的内存猛然间没了。这造成的结果通常是不可预测的。有了这个 Pool 类,即使 Java 程序员也可以不费吹灰之力地在 Airplane 类里增加自己的内存管理功能: class Airplane { public: ... // 普通 Airplane 功能 static void * operator new(size_t size); static void operator delete(void *p, size_t size); private: AirplaneRep *rep; // 指向实际描述的指针 static Pool memPool; // Airplanes 的内存池 }; inline void * Airplane::operator new(size_t size) { return memPool.alloc(size); } inline void Airplane::operator delete(void *p, size_t size) { memPool.free(p, size); } // 为 Airplane 对象创建一个内存池, // 在类的实现文件里实现 Pool Airplane::memPool(sizeof(Airplane));
这个设计比前面的要清楚、干净得多,因为 Airplane 类不再和非 Airplane的代码混在一起。union,自由链表头指针,定义原始内存块大小的常量都不见了,它们都隐藏在它们应该呆的地方——Pool 类里。让写 Pool 的程序员去操心内存管理的细节吧,你只是让 Airplane 类正常工作。
现在应该明白了,自定义的内存管理程序可以很好地改善程序的性能,而且它们可以封装在象 Pool 这样的类里。但请不要忘记主要的一点,operator new和 operator delete 需要同时工作,那么你就写了 operator new,就也一定要写operator delete。
LeetCode每日一题141。环形链表题目给你一个链表的头节点head,判断链表中是否有环。如果链表中有某个节点,可以通过连续跟踪next指针再次到达,则链表中存在环。为了表示给定链表中的环,评测系统内部使用整数pos
T03防疲劳驾驶摄像头从目前零跑后台统计的用户使用数据来看,用户尤其青睐ACC和LKA功能,ACC功能的使用总次数达到了257812次,LKA功能的使用总次数达到了331592次,这些都说明用户在日常使
2022年再看经典iPhone4s时光飞逝,从4s发布到今天已经过去足足十年之久,在这期间,智能手机也已经有了很大的发展。回想当年,90后的我还是一名在校大学生,学校里的广告牌上看到过iPhone3GS的广告,那个
石榴快评全链条治网暴,决不让互联网变痰盂高娃俗话说唾沫星子淹死人。网络时代,唾沫星子更是能轻松聚集成山呼海啸,威力和杀伤力之大,令人瞠目结舌。为有效防范和解决网络暴力问题,切实保障广大网民合法权益,中央网信办近日部署开展
离大谱!iPhone14全系涨价,买不起了2021年9月,苹果给了我们一个大惊喜,iPhone13系列全系配置升级,但原价却没有改变,四个型号仍为699美元799美元999美元1099美元起。甚至说,受到汇率变化影响,同存
中国联通助力法律产业全链条数字化转型新闻页台海网4月22日,在天府中央法务区建设推进活动上,四川省检察院检察长冯键中国联通集团副总经理买彦州共同启动智慧司法科技创新中心,同步发布天府中央法务云天府中央法务链。据了解,
华为Nova10Pro曝光新骁龙7骁龙778G混用,携鸿蒙3。0回归今年竞争激烈的中端机型,大多都已经发布,每一款手机都卖点十足,用料非常扎实。而现在华为Nova10系列也即将发布,或将携鸿蒙3。0系统回归!据爆料,处理器方面将搭载两款骁龙处理器,
Meta市值狂泻两千亿扎克伯格财富缩水近三百亿跌出富豪榜前十周四,拥有Facebook的科技巨头Meta公司在公布了不及预期的财报后,股价大跌26,市值狂泻超过2370亿美元,创下美股历史上最大的单日跌幅。Meta的单日跌幅超过苹果公司20
中国联通对美国联邦通信委员会撤销中国联通美洲公司214牌照做出回应来源新华网新华社北京2月3日电(记者高亢)中国联通3日发出声明回应中国联通(美洲)运营有限公司于美国当地时间2月2日收到美国联邦通信委员会(FCC)撤销214牌照的命令一事。中国联
外媒点赞北京冬奥会科技方便高效又安全中国日报网2月3日电综合外媒报道,为了给北京2022年冬奥会的运动员媒体记者工作人员等参与者创造更加安全和便捷的环境,许多新科技被应用到本届冬奥会当中。英国路透社3日报道称,科技是
智能交通影响人类未来1040年的重大变革来源四川日报川观新闻百度创始人董事长兼CEO李彦宏的新书智能交通影响人类未来1040年的重大变革发布,这是他写的第三本关于人工智能的书。第一本是2017年编写的智能革命迎接人工智能