• 条款十: 如果写了operator new就要同时写operator delete


    为什么有必要写自己的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当初分配的内存大小是多少,就是在它所返回的内存里预先附带一些额外信息,用来指明被分配的内存块的大小。也就是说,当你写了下面的语句,

    airplane *pa = new airplane;

    你不会得到一块看起来象这样的内存块:

        pa——> airplane对象的内存

    而是得到象这样的内存块:

        pa——> 内存块大小数据 + 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;//链表第一个结点
    
    };

    上面的代码增加了的几个声明:一个operator new函数,一个联合(使得rep和next域占用同样的空间),一个常量(指定大内存块的大小),一个静态指针(跟踪自由链表的表头)。表头指针声明为静态成员很重要,因为整个类只有一个自由链表,而不是每个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<airplane*>(::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;
    }

    有了operator new,下面要做的就是给出airplane的静态数据成员的定义:

    airplane *airplane::headoffreelist;                                       
    const int airplane::block_size = 512;    

    没必要显式地将headoffreelist设置为空指针,因为静态成员的初始值都被缺省设为0。block_size决定了要从::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必须同时写,这样才不会出现不同的假设。如果写了一个自己的内存分配程序,就要同时写一个释放程序。

    因而,继续设计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<airplane*>(deadobject);
    
      carcass->next = headoffreelist;
      headoffreelist = carcass;
    }

    引起内存泄露的原因在于内存分配后指向内存的指针丢失了。如果没有垃圾处理或其他语言之外的机制,这些内存就不会被收回。但上面的设计没有内存泄露,因为它决不会出现内存指针丢失的情况。每个大内存块首先被分成airplane大小的小块,然后这些小块被放在自由链表上。当客户调用airplane::operator new时,小块被自由链表移除,客户得到指向小块的指针。当客户调用operator delete时,小块被放回到自由链表上。采用这种设计,所有的内存块要不被airplane对象使用(这种情况下,是由客户来负责避免内存泄露),要不就在自由链表上(这种情况下内存块有指针)。所以说这里没有内存泄露。

    然而确实,::operator new返回的内存块是从来没有被airplane::operator delete释放,这个内存块有个名字,叫内存池。但内存泄漏和内存池有一个重要的不同之处。内存泄漏会无限地增长,即使客户循规蹈矩;而内存池的大小决不会超过客户请求内存的最大值。

    实际开发中,你会经常要给许多不同的类实现基于内存池的功能。下面简单给出了一个pool类的最小接口(见条款18),pool类的每个对象是某类对象(其大小在pool的构造函数里指定)的内存分配器。

    class pool {
    public:
      pool(size_t n);                      // 为大小为n的对象创建
                                           // 一个分配器
    
    
      void * alloc(size_t n)  ;            // 为一个对象分配足够内存
                                           // 遵循条款8的operator new常规
    
      void free(  void *p, size_t n);      // 将p所指的内存返回到内存池;
                                           // 遵循条款8的operator delete常规
    
      ~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));

    现在应该明白了,自定义的内存管理程序可以很好地改善程序的性能,而且它们可以封装在象pool这样的类里。但请不要忘记主要的一点,operator new和operator delete需要同时工作,那么你写了operator new,就也一定要写operator delete。

  • 相关阅读:
    知识收集
    代码片_笔记
    北理工软件学院2016程序设计方法与实践
    内存的初始化与清零问题
    LeetCode第七题
    KMP算法C代码
    在64位Linux上安装32位gmp大数库
    ASN1编码中的OID
    迷宫问题
    64位linux编译32位程序
  • 原文地址:https://www.cnblogs.com/ljygoodgoodstudydaydayup/p/3897158.html
Copyright © 2020-2023  润新知