• Effective C++: 08定制new和delete


    49:了解new-handler的行为

             当operator new无法满足某一内存分配需求时,它会抛出异常(以前会返回一个null)。在抛出异常之前,它会调用一个客户指定的错误处理函数,也就是所谓的new-handler。

             客户通过调用set_new_handler来设置new-handler:

    namespace std {
      typedef void (*new_handler)();
      new_handler set_new_handler(new_handler p) throw();
    }

              set_new_handler返回之前设置的new_handler。

             当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。因此,一个设计良好的new-handler必须做以下事:

             a:让更多内存可被使用,以便使operator new下一次分配内存能够成功。实现方法之一就是程序一开始就分配一大块内存,而后当new-handler第一次被调用时,将它们还给程序使用;

             b:安装另一个new-handler:如果目前的new-handler无法获得更多内存,并且它直到另外哪个new-handler有此能力,则当前的new-handler可以安装那个new-handler以替换自己,下次当operator new调用new-handler时,就是调用最新的那个。

             c:卸载new-handler,一旦没有设置new-handler,则operator new就会在无法分配内存时抛异常;

             d:抛出bad_alloc异常;

             e:不返回,直接调用abort或exit。

             有时希望根据不同的class有不同的方式处理内存分配的情况,但是C++并不支持class专属之new-handler,但是C++支持class专属operator new,所以可以利用这点来实现。例子如下:

    class Widget {
    public:
      static std::new_handler set_new_handler(std::new_handler p) throw();
      static void * operator new(std::size_t size) throw(std::bad_alloc);
    private:
      static std::new_handler currentHandler;
    };
    
    std::new_handler Widget::currentHandler = 0;
    
    std::new_handler Widget::set_new_handler(std::new_handler p) throw()
    {
      std::new_handler oldHandler = currentHandler;
      currentHandler = p;
      return oldHandler;
    }

              使用Widget的用户首先调用Widget::set_new_handler设置其专属new-handler。然后,在Widget的专属operator new中,调用std::set_new_handler,设置全局new-handler为Widget的专属new-handler,然后调用全局operator new,执行内存分配。分配失败时,全局operator new会调用Widget的专属new-handler。

    如果全局operator new最终无法分配足够内存,会抛出一个异常,或者分配成功后,此时,必须恢复全局new-handler为原来的设置。为了实现这一点,可以使用资源管理对象的方法:

    class NewHandlerHolder {
    public:
      explicit NewHandlerHolder(std::new_handler nh):handler(nh) {}
      ~NewHandlerHolder()  { std::set_new_handler(handler); }
    private:
      std::new_handler handler;                         // remember it
    
      NewHandlerHolder(const NewHandlerHolder&);    // prevent copying
      NewHandlerHolder& operator=(const NewHandlerHolder&);
    };

              有了资源管理类之后,Widget::operator new的代码如下:

    void * Widget::operator new(std::size_t size) throw(std::bad_alloc)
    {
      //安装Widget专属new-handler,并使用NewHandlerHolder管理原有全局new-handler
      NewHandlerHolder h(std::set_new_handler(currentHandler));
    
      //不管分配成功还是抛出异常,NewHandlerHolder的析构函数中会恢复全局new-handler
      return ::operator new(size);                  
    }  

             使用Widget的客户代码如下:

    void outOfMem();
    Widget::set_new_handler(outOfMem); // 设置Widget的专属new-handler
    
    Widget *pw1 = new Widget; //如果分配失败,调用outOfMem
    
    std::string *ps = new std::string; //如果分配失败,调用全局new-handler
    
    Widget::set_new_handler(0); 
    Widget *pw2 = new Widget; //分配失败,直接抛出异常

      

             实现class专属new-handler不会因class不同而不同,所以可以建立起一个mixin风格的base class。但是因为涉及到static成员,为了使derived class获得不同的base class属性,这里可以使用template:

    template<typename T>
    class NewHandlerSupport{
    public:
      static std::new_handler set_new_handler(std::new_handler p) throw();
      static void * operator new(std::size_t size) throw(std::bad_alloc);
    private:
      static std::new_handler currentHandler;
    };
    
    template<typename T>
    std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()
    {
     std::new_handler oldHandler = currentHandler;
     currentHandler = p;
     return oldHandler;
    }
    
    template<typename T>
    void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc)
    {
      NewHandlerHolder h(std::set_new_handler(currentHandler));
      return ::operator new(size);
    }
    
    // this initializes each currentHandler to null
    template<typename T> std::new_handler NewHandlerSupport<T>::currentHandler = 0;

              有了这个模板之后,Widget可以直接继承它:

    class Widget: public NewHandlerSupport<Widget> {
      ...
    }; 

              这样看起来会有些奇怪,而且NewHandlerSupport模板从未使用类型参数T。这种技巧的作用,只是为了保证:继承自NewHandlerSupport的每一个class,都具有不同的NewHandlerSupport属性,也就是static成员变量currentHandler。这种技术还有自己的名字:“怪异的循环模板模式”(curiously recurring template pattern, CRTP)。

             直到1993年,C++还要求operator new在分配失败时返回null,新一代的operator new则应该抛出bad_alloc异常,但是许多C++程序是在编译器支持新规范之前写出来的,因此C++提供了另一形式的operator new,他会在分配失败时返回null:

    class Widget { ... };
    Widget *pw1 = new Widget; //分配失败抛出bad_alloc异常 
    if (pw1 == 0) ... // 这个测试一定失败
    
    Widget *pw2 = new (std::nothrow) Widget; // 分配失败返回NULL
    if (pw2 == 0) ... // 这个测试可能成功

              虽然nothrow版的operator new不抛异常,但是接下来Widget的构造函数调用时,内部可能又会new一些内存,而这次不一定会在使用nothrow new。所以,使用nothrow new只能保证operator new不抛异常,不保证像new (std::nothrow) Widget这样的表达式不抛异常,因此其实没有使用nothrow new的需要。

     50:了解new和delete的合理替换时机

             以下是替换编译器提供的operator new或operator delete的几个最常见的理由:

             a:检测运用错误:内存相关的错误包括内存泄漏、多次delete等,使operator new持有一串地址,而operator delete将地址从中移走,可以很容易检测出上述错误用法。另外,编程错误可能导致数据写入点在分配区块之后(overruns)或之前(underrun),可以自定义一个operator new,超额分配内存,在额外空间放置特定的签名,operator delete可以检查签名是否正确来判断是否发生了overrun或underrun。

             b:为了强化性能:编译器提供的operator new和operator delete主要用于一般目的,它们的工作对所有人都适度的好,但某些情况下,定制版的operator new和operator delete性能可以胜过缺省版本。要么是速度快,要么是更省内存。

             c:为了收集使用数据;

             d:为了弥补缺省分配器中的非最佳齐位,比如如果double是8-byte齐位的则访问速度最快,但编译器自带的operator new并不一定保证这一点,此时可以替换operator new为一个8-byte齐位的版本,使得程序效率大大提升;

             e:为了将相关对象成簇几种:如果某种数据结构往往一起使用,你希望在处理这些数据时将内存页错误的频率降至最低,则new和delete的placement版本有可能完成这样的任务;

             f:为了获得非传统的行为;

     51:编写new和delete时需固守常规

    编写operator new时需要注意:必得返回正确的值,内存不足时需要调用new-handling函数,必须有对付零内存需求的准备,还需避免不慎掩盖正常形式的new;operator new的返回值十分单纯。如果它有能力供应客户申请的内存,就返回一个指针指向那块内存。如果没有那个能力,则抛出一个bad alloc异常;operator new实际上不只一次尝试分配内存,并在每次失败后调用new-handling函数。这里假设new-handling函数也许能够做某些动作将某些内存释放出来。只有当指向new-handling函数的指针是null,operator new才会抛出异常。

    下面是一个非成员函数的operator new伪码:

    void * operator new(std::size_t size) throw(std::bad_alloc) 
    {                               //你的operator new可能有更多的参数
      using namespace std;          
      if (size == 0) {              //处理0内存需求,将其视为1Byte申请
        size = 1;                   
      }                            
    
      while (true) {
        attempt to allocate size bytes;
        if (the allocation was successful)
           return (a pointer to the memory);
    
        // 申请失败,找到当前的new-handler
        new_handler globalHandler = set_new_handler(0);
        set_new_handler(globalHandler);
    
        if (globalHandler) (*globalHandler)();
        else throw std::bad_alloc();
      }
    }

     因为没有办法可以直接取得当前的new-handler,所以先调用set_new_handler将其找出来。

    对于operator new的成员函数版本,因为该函数会被derived classes继承,所以需要考虑的更周全些。因为写出定制型内存管理器的一个最常见理由是为针对某特定class的对象分配行为提供最优化,却不是为了该class的任何derived classes。也就是说,针对class X而设计的operator new,其行为很典型地只为大小刚好为sizeof(X)的对象而设计。然而一旦被继承下去,有可能base class的operator new被调用用以分配derived class对象。处理这种情况的最佳做法是将内存申请量错误的调用行为改为标准operator new:

    void * Base::operator new(std::size_t size) throw(std::bad_alloc)
    {
      if (size != sizeof(Base)) 
         return ::operator new(size); 
      ... 
    }

     上面的代码并没有检测size等于0的情况,是因为C++保证所有独立式对象必须具有非0大小,所以sizeof(Base)不会等于0。   

    如果你打算控制class专属之“arrays内存分配行为”,那么你需要实现operator new [ ],唯一需要做的一件事就是分配一块未加工内存,因为你无法对array之内迄今尚未存在的元素对象做任何事情。实际上你甚至无法计算这个array将含多少个元素对象。首先你不知道每个对象多大,毕竟base class的operator new [ ]有可能经由继承被调用,将内存分配给“元素为derived class对象”的array使用。此外,传递给operator new[]的size t参数,其值有可能比“将被填以对象”的内存数量更多,因为条款16说过,动态分配的arrays可能包含额外空间用来存放元素个数。

    operator delete情况更简单,需要记住的唯一事情就是C++保证“删除null指针永远安全”,下面是伪码:

    void operator delete(void *rawMemory) throw()
    {
      if (rawMemory == 0) return;
      deallocate the memory pointed to by rawMemory;
    }

     这个函数的member版本也很简单,只需要多加一个动作检查删除数量。万一你的class专属的operator new将大小有误的分配行为转交::operator new执行,你也必须将大小有误的删除行为转交::operator delete执行:

    class Base {
    public:
      static void * operator new(std::size_t size) throw(std::bad_alloc);
      static void operator delete(void *rawMemory, std::size_t size) throw();
      ...
    };
    
    void Base::operator delete(void *rawMemory, std::size_t size) throw()
    {
      if (rawMemory == 0) return;
    
      if (size != sizeof(Base)) { 
         ::operator delete(rawMemory); 
         return; 
      }
    
      deallocate the memory pointed to by rawMemory;
      return;
    }

     有趣的是,如果即将被删除的对象派生自某个base class而后者欠缺virtual析构函数,那么C++传给operator delete的size_t可能不正确。

     52:写了placement new也要写placement delete

             当你写一个new表达式:  Widget* pw=new Widget; 这种情况下共有两个函数被调用,一个是用以分配内存的operator new,一个是Widget的default构造函数。

    假设operator new调用成功,构造函数却抛出异常。这种情况下,内存分配所得必须取消并恢复旧观,否则会造成内存泄漏。回收内存的责任是由C++运行期系统完成的。

    运行期系统会调用operator new的相应operator delete版本,前提是它必须知道哪一个(因为可能有许多个)operator delete该被调用。如果目前面对的是拥有正常签名式的new和delete,这并不是问题,因为正常的operator new对应于正常的operator delete:

    void* operator new(std::size_t) throw(std::bad_alloc);
    
    void operator delete(void *rawMemory) throw();  // 全局域
    void operator delete(void *rawMemory, std::size_t size) throw(); // 类专属

     因此,当只使用正常形式的new和delete,运行期系统毫无问题可以找出那个“知道如何取消new所作所为并恢复旧观”的delete。然而当你开始声明非正常形式的operator new,也就是带有附加参数的operator new,“究竟哪一个delete伴随这个new”的问题便浮现了。

    如果operatornew接受的参数除了size_t之外还有其他,这便是个所谓的placement new。众多placement new版本中特别有用的一个是“接受一个指针指向对象该被构造之处”,那样的operator new长相如下:

    void* operator new(std::size_t, void *pMemory) throw();  

     这个版本的new已被纳入C++标准程序库,只要#include <new>就可以取用它。这个new的用途之一是负责在vector的未使用空间上创建对象。它同时也是最早的placement new版本。实际上它正是这个函数的命名根据:一个特定位置上的new。以上说明意味术语placement new有多重定义。当人们谈到placement new,大多数时候他们谈的是此一特定版本,少数时候才是指接受任意额外实参之operator new。因此一般性术语“placement new”意味带任意额外参数的new,因为另一个术语“placement delete”直接派生自它,operator delete如果接受额外参数,便称为placement deletes。

    假设写一个class专属的operator new,要求接受一个ostream,用来记录相关分配信息,同时又写了一个正常形式的class专属operator delete:

    class Widget {
    public:
      ...
      static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
      static void operator delete(void *pMemory, std::size_t size) throw(); 
      ...
    };
    Widget *pw = new (std::cerr) Widget; 

     如果内存分配成功,而Widget构造函数抛出异常,运行期系统需要取消operator new的分配并恢复旧观。然而运行期系统无法知道真正被调用的那个operator new如何运作,因此运行期系统寻找“参数个数和类型都与operator new相同”的某个operator delete。所以对应的operator delete就应该是:

    void operator delete(void *, std::ostream&) throw();

     现在,既然Widget没有声明placement版本的operator delete,所以运行期系统不知道如何取消并恢复原先对placement new的调用。于是什么也不做,这就造成了内存泄漏。

    规则很简单:如果一个带额外参数的operator new没有“带相同额外参数”的对应版operator delete,那么当new的内存分配动作需要取消并恢复旧观时就没有任何operator delete会被调用。

    因此,Widget有必要声明一个placement delete,对应于那个有志记功能的placement new:

    class Widget {
    public:
      ...
      static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
      static void operator delete(void *pMemory) throw();
      static void operator delete(void *pMemory, std::ostream& logStream) throw();
      ...
    };

     这种情况下,如果Widget *pw = new(std::cerr) Widget中构造函数抛出异常,则对应的placement delete就会被调用。

    如果手动delete:delete pw; 这种情况下,调用的是正常形式的operator delete,而非其placement版本。placement delete只有在“伴随placement new调用而触发的构造函数”出现异常时才会调用,而delete一个指针永远不会调用placement delete。这就表示必须提供一个正常的operator delete,以及一个对应placement new的placement delete版本。

    另外,由于成员函数的名称会掩盖外围作用域中的相同名称,因此需要避免让class专属的new掩盖客户期望的其他new,比如:

    class Base {
    public:
      ...
      static void* operator new(std::size_t size, std::ostream& logStream)
                                            throw(std::bad_alloc);  
      ...
    };
    
    Base *pb = new Base; // 错误,正常的new已经被掩盖了
    Base *pb = new (std::cerr) Base; // 正确,调用Base的placement new

     同样的,derived classes中的operator new会掩盖global版本和继承而来的operator new:

    class Derived: public Base {
    public:
      ...
      static void* operator new(std::size_t size)  // 重新定义正常的operator new
          throw(std::bad_alloc); 
      ...
    };
    Derived *pd = new (std::clog) Derived; // 错误,Base的placement new被掩盖了
    Derived *pd = new Derived; // 正确,调用Derived的正常operator new

     缺省情况下,C++在global作用域内提供以下形式的operator new:

    void* operator new(std::size_t) throw(std::bad_alloc); // normal new
    void* operator new(std::size_t, void*) throw(); // placement new
    void* operator new(std::size_t, const std::nothrow_t&) throw(); // see Item 49

     如果在class内声明任何operator new,它都会遮掩这些标准形式。可以建立一个base class,内含所有正常形式的new和delete,在需要时,可以继承并覆盖:

    class StandardNewDeleteForms {
    public:
      // normal new/delete
      static void* operator new(std::size_t size) throw(std::bad_alloc)
      { return ::operator new(size); }
      static void operator delete(void *pMemory) throw()
      { ::operator delete(pMemory); }
    
      // placement new/delete
      static void* operator new(std::size_t size, void *ptr) throw()
      { return ::operator new(size, ptr); }
      static void operator delete(void *pMemory, void *ptr) throw()
      { return ::operator delete(pMemory, ptr); }
    
      // nothrow new/delete
      static void* operator new(std::size_t size, const std::nothrow_t& nt) throw()
      { return ::operator new(size, nt); }
      static void operator delete(void *pMemory, const std::nothrow_t&) throw()
      { ::operator delete(pMemory); }
    };

     当想自定义operator new和delete时,可以利用继承和using:

    class Widget: public StandardNewDeleteForms {           // inherit std forms
    public:
       using StandardNewDeleteForms::operator new;          // make those
       using StandardNewDeleteForms::operator delete;       // forms visible
    
       static void* operator new(std::size_t size, std::ostream& logStream) 
         throw(std::bad_alloc);
    
       static void operator delete(void *pMemory, std::ostream& logStream) 
         throw();
      ...
    };
  • 相关阅读:
    Cookie练习
    JS写九九乘法表
    对GridView实现分页
    对GridView的行加颜色并弹出Kindeditor
    对Dictionary的理解
    一、android 开发环境大搭建
    main方法的测试
    main 方法的书写(1)
    由InvocationTargetException引发的思考
    汇编学习笔记之处理器体系结构
  • 原文地址:https://www.cnblogs.com/gqtcgq/p/9393944.html
Copyright © 2020-2023  润新知