Effective C++(七)

       八、定制new和delete

       (49)、了解new-handler的行为

         当operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理程序,一个所谓的new-handler。为了指定这样“用以处理内存不足”的函数,客户必须调用set_new_handler,那是声明于<new>的个标准程序库函数:

        namespace std {

              typedef void (*new handler)();

             new_hander set_new_handler(new_handler p) throw();

       }

       你可以这样使用set_new_handler:

        void outOfMem()

        {

              std::cerr << "Unable to satisfy request for memory\n";

              std::abort();

       }

       int main()

       {

             std::set_new_handler(outOfMem);

             int* pBigDataArray = new int[1000000000000L];

             ...

      }

      当operator new无法为1000000000000整数分配足够空间时,outOfMem会被调用。

       当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。引起反复调用的代码显示于条款51,这里的高级描述已足够获得一个结论,那就是一个设计良好的new-handler函数必须做以下事情:

       让更多内存可被使用。这便造成operator new内的下一次内存分配动作可能成功。实现此策略的做法是,程序一开始执行就分配一大块内存,而后当new-handler第一次被调用,将它们释还给程序使用。

      安装另一个new-handler。如果目前这个new-handler无法取得更多可用内存,或许它知道另外哪个new-handler有此能力。

     卸除new-handler,也就是将null指针传给set_new_handler。一旦没有安装任何new-handler,operator new会在内存分配不成功时抛出异常。

    抛出bad_alloc(或派生自bad_alloc)的异常。

     不返回,通常调用abort或exit。

     在类中可用定义class专属的new-handler替换global new-handler。

     下面以Widget为例说明:

      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;

      }

      static std::new_handler  Widget::currentHandler = 0;  //在class实现文件内初始化为null

      std::new_handler Widget::set_new_handler(std::new_handler p) throw()

      {

           std::new_handler oldHandler = currentHandler;

           currentHandler = p;

           return oldHandler;

      }

      最后,Widget的operator new做以下事情:

      1.调用标准set_new_handler,告知Widget的错误处理函数。这会将Widget的new-handler安装为global new-handler。

      2.调用global operator new,执行实际之内存分配。如果分配失败,global operator new会调用Widget的new-handler,因为那个函数才刚被安装为global new-handler。如果global operator new最终无法分配足够内存,会抛出一个bad_alloc异常。在此情况下Widget的operator new必须恢复原本的global new-handler,然后再传播该异常。为确保原来的new-handler总是能够被重新安装回去,Widget将global new-handler视为资源并遵守条款13的忠告,运用资源管理对象(resource-managing objects)防止资源泄露。

     3.如果global operator new能够分配足够一个Widget对象所用的内存,Widget的operator new会返回一个指针,指向分配所得。Widget析构函数会管理global new-handler,它会自动将Widget的operator new被调用前的那个global new-handler恢复回来。

     如下:

     class NewHandlerHolder {

       public:

             explicit NewHandlerHolder(std::new_handler nh)     //取得目前的new-handler

                : handler(nh) {}

             ~NewHandlerHolder()

             { std::set_new_handler(handler); }                         //释放它

        private:

             std::new_handler handler;                                   //记录下来

             NewHandlerHolder(const NewHandlerHolder&); //阻止copying

             NewHandlerHolder& operator=(const NewHandlerHolder&);             

     };

      void* Widget::operator new(std::size_t size) throw(std::bad_alloc)

       {

             NewHandlerHolder h(std::set_new_handler(currentHandler));   //安装Widget的new-handler

             return ::operator new(size);   //分配内存或抛出异常

      }                                                    //恢复global new-handler

      Widget的客户应该类似这样使用其new-handling:

          void outOfMem();   //函数声明。此函数在Widget对象分配失败时调用

         Widget::set_new_handler(outOfMem);    //设定outOfMem为Widget的new-handling函数

         Widget* pw1 = new Widget;  //如果内存分配失败,调用outOfMem

         std::string* ps = new std::string;   //如果内存分配失败,调用global new-handling函数(如果有的话)

         Widget::set_new_handler(0);       //设定Widget专属的new-handling函数为null

         Widget* pw2 = new Widget;        //如果内存分配失败,立刻抛出异常(class Widget并没有专属的new-handling                                                              //函数)

        

          可以改写为"mixin"的base calss供继承,可以使用template。

       template<typename T>             //“mixin”风格的base class,用以支持class专属的set_new_handler

      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);

      }

      template<typename T>

      std::new_handler NewHandlerSupport<T>::currentHandler = 0;

      可以向下面这样使用:

       class Widget: public NewHandlerSupport<Widget> {

           ...                      //和先前一样,但不必声明set_new_handler或operator new

       };

       直至1993年,C++都还要求operator new必须在无法分配足够内存时返回null。新一代的operator new则应抛出bad_alloc异常。为了不想抛弃“侦测null”的族群,提供了另一形式的operator new,使用“nothrow”形式。

        class Widget { ... };

        Widget *pw1 = new Widegt;                            //如果分配失败,抛出bad_alloc

        if(pw1 == 0) ...                                                 //这个测试一定失败

        Widget *pw2 = new (std::nothrow)Widegt;      //如果分配Widget失败,返回0

        if(pw2 == 0) ...                                                 //这个测试可能成功

        请记住:

        set_new_handler允许客户指定一个函数,在内存无法获得满足时被调用。

        Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常。



       (50)、了解new和delete的合理替换时机

        替换编译器提供的operator new和operator delete有下面三个最常见的理由:

        用来检测运用上的错误。

        为了强化效果。

        为了收集使用上的统计数据。

        以齐位为了展开。

        本条款的主题是,了解何时可在“全局性”或“class专属的”基础上合理替换缺省的new和delete。挖掘更多细节之前,让我先对答案做一些摘要:

        为了检测运用错误。

        为了收集动态分配内存之使用统计信息。

        为了增加分配和归还的速度。

        为了降低缺省内存管理区带来的空间额外开销。

        为了弥补缺省分配器中的非最佳齐位(suboptimal alignment)。

        为了将相关对象成簇集中。

        为了获得非传统的行为。

        请记住:

         有许多理由编写自定的new和delete,包括改善性能、对heap运用错误进行调试、收集heap使用信息。       



        (51)、编写new和delete时需固守常规

          operator new的返回值十分单纯。如果它有能力供应客户申请的内存,就返回一个指针指向那块内存。如果没有那个能力,就遵循条款49描述的规则,并抛出一个bad_alloc异常。

         其实并不单纯,operator new实际上不只异常尝试分配内存,并在每次失败后调用new-handling函数。这里假设new-handling函数也许能够做某些动作将内存释放出来。只有当指向new-handling函数的指针是null,operator new才会抛出异常。

        奇怪的是C++规定,即使客户要求0bytes,operator new也得返回一个合法指针。下面是个non-member operator new伪码(pesudocide):

         void* operator new(std::size_t size) throw(std::bad_alloc)

        {                                                                                    //你的operator new可能接受额外参数

               using namespace std;

               if(size == 0)       //处理0-byte申请,将它视为1-byte申请

              {

                    size = 1;

             }

             while(true)

             {

                   尝试分配size bytes;

                   if(分配成功)

                       return (一个指针,指向分配得来的内存);

                    

                   //分配失败;找出目前的new-handling函数

                  new_handler globalHandler = set_new_handler(0);

                  set_new_handler(globalHandler);

               

                  if(globalHandler)  (*globalHandler)();

                  else throw std::bad_alloc();

             }

        }

       这里的伎俩是把0byte申请量视为1byte申请量。

       你也许带着怀疑的眼光斜睨这份伪码(pseudocode),因为其中将new-handling函数指针设为null而后又立刻恢复原样。那是因为我们很不幸地没有任何办法可以直接取得new-handling函数指针,所有必须调用set_new_handler找出它来。拙劣,但有效——至少对单线程程序而言。若在多线程环境中你或许需要某种机锁(lock)以便安全处置new-handling函数背后的(global)数据结构。

       条款49谈到operator new内含一个无穷循环,而上述伪码表面这个循环;“while(truw)”就是那个无穷循环。退出此循环的唯一办法是:内存被成功分配或new-handling函数做了一件描述于条款49的事情:让更多内存可用、安装另一个new-handler、卸除new-handler、抛出bad_alloc异常(或派生物),或是承认失败而直接return。现在,对应new-handler为什么必须做出其中某些事你应该很清楚了。如果不那么做,operator new内的while循环永远不会接受。

         class Base {

            public:

                 static void* operator new(std::size_t size)  throw(std::bad_alloc);

                 ...

        };

        class Derived:public Base   //假设Derived未声明operator new

        { ... };

        Derived* p = new Derived;  //这里调用的是Base::operator new

        对付上面的最好办法如下:

        void* Base::operator new(std::size_t size) throw(std::bad_alloc)

        {

              if(size != sizeof(Base))                         //如果大小错误,令标准的operator new起而处理。

                    return ::operator new(size);

              ...                                                         //否则在这里处理。

        }

       看似这段代码没有检验size等于0,不是没有检验而是将检验与sizeof(Base)融合一起了。

        如果你决定写个operator new[],记住,唯一需要做的一件事情是分配一块未加工内存。

        

        operator delete情况更简单,你需要记住的唯一事情就是C++保证“删除null指针拥有安全”,所以你必须兑现这项保证。下面是non-member operator delete的伪码(pseudocode):

        void operator delete(void * rawMemory)  throw()

        {

              if(rawMemory == 0) return;  //如果将被删除的是个null指针,那就什么都不做

              现在,归还rwaMemory所指的内存;

       }

       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;

           }

           现在,归还rawMemory所指的内存;

           return;

      }

      有趣的是,如果即将被删除的对象派生自某个base class而后者欠缺virtual析构函数,那么C++传给operator delete的size_t数值可能步正确。这是“让你的base class拥有virtual析构函数”的一个够好的理由;条款7还提到一个更好的理由。

      请记住:

       operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0byte申请。Class专属版本则还应该处理“比正确大小更大的(错误)申请”。

     operator delete应该在收到null指针时不做任何事。Class专属版本则还应该处理“比正确大小更大的(错误)申请”。

      

         (52)、写了placement new也要写placement delete

           如果一个带额外参数的operator new没有“带相同额外参数”的对应版operator delete,那么当new的内存分配动作需要取消并恢复旧观时就没有任何operator delete会被调用。因此,为了消弭稍早代码中的内存泄露,Widget有必要声明一个placement delete,对应于那个有志记功能(logging)的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* pMenory,std::ostream& logStream) throw();

                 ...

         };

         palcement delete只有在“伴随placement new调用而触发的构造函数”出现异常时才会被调用。对着一个指针施行delete绝不会导致调用placement delete。

        这意味如果要对所有与placement new相关的内存泄露宣战,我们必须同时提供一个正常的operator delete(用于构造期间无异常抛出)和一个placement版本(用于构造期间有异常抛出)。


       附带一提,由于成员函数的名称会掩盖其外围作用域中的相同名称,你必须小心避免让class专属的news掩盖客户期望的其他news(包括正常版本)。

        class Base {

        public:

           ...

           static void* operator new(std::size_t size,std::ostream& logStream)     throw(std::bad_alloc);  //这个new会掩盖

           ...                                                                                                                                          //正常的global形式

       };

       Base* pb = new Base;  //错误!因为正常形式的operator new被掩盖

       Base* pb = new (std::cerr)Base;   //正确,调用Base的placement new

      

      class Derived : public Base {                //继承自先前的Base

       public:

           ...

           static void* operator new(std::size_t size)  throw(std::bad_alloc)  //重新声明正常形式的new

           ...

      };

       Derived* pd = new (std::clog)Derived;  //错误!因为Base的placement new被掩盖了。

       Derived* pd = new Dericed;    //没问题,调用Derived的operator new

       

        如果你在class内声明任何operator news,它会掩盖下述这些标准形式。

        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();  //nothrow new

       下面是建立标准的没有故意掩盖的new和delete:

        class StandardNewDelelteForms {

        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(pMenory); }

             

            // placement new/delete

            static void* operator new(std::size_t,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); }

       };

        凡是想以自定形式扩充标准形式的客户,可利用继承机制及using声明式取得标准形式:

        class Widget:public StandardNewDeleteFroms {          //继承标准形式

        public:

              using StandardNewDeleteFroms::operator new;            //让这些形式可见

              using StandardNewDeleteFroms::operator delete;

              static void* operator new(std::size_t size,std::ostream& logStream) throw(std::bad_alloc)  //添加一个自定的

                                                                                                                                                            //placement new

              static void operator delete(void* pMemory,std::ostream& logStream)  throw();  //添加一个对应的plaement                                                                                                                                               //delete

              ...             

        }     

        请记住:

        当你写一个placement operator new,请确定也写出了对应的placement operator delete。如果没有这样做,你的程序可能会发生隐微而时段时续的内存泄露。

       当你声明placement new和placement delete,请确定不要无意识(非故意)地掩盖了它们的正常版本。     

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值