• 第八章、定制new和delete


    条款49:了解new-handler的行为

    当operator new 抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误函数,一个所谓的new-handler。
    代码示例如下:

    namespace std {
        typedef void (*new_handler) ();
        new_handler set_new_handler(new_handler p) throw();
    }
    
    • 1
    • 2
    • 3
    • 4

    set_new_handler 的参数是个指针,指向operator new无法分配足够内存时该被调用的函数。其返回值也是个指针,指向set_new_handler 被调用前正在执行(但马上就要被替换)的那个new-handler函数。
    你可以这样使用set_new_handler:

    //以下是当operator new 无法分配足够内存时,该被调用的函数
    void outOfMem(){
        std::cerr<<"unable to satisfy request for memory \n";
        std::abort();
    }
    int main()
    {
        std::set_new_handler(outOfMem);
        int * pBigDataArray = new int[100000000L]l
        //...        
        
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    一个设计良好的new-handler函数必须做以下事情:
    1、让更多内存可被使用。
    2、安装另一个new-handler。
    3、卸除new-handler。
    4、抛出bad_alloc(或派生自bad_alloc)的异常。
    5、不返回,通常调用abort或exit 。

    有时候你或许希望以不同的方式处理内存分配失败情况,你希望视被分配物属于哪个class而定:

    class X
    {
    public:
        static void outOfMemory();
        //...
    };
    class Y
    {
    public:
        static void outOfMemory();
        //...
    };
    
    
        X * p1 =new X;//如果分配不成功,调用X::outOfMemory
        Y * p2 =new Y;//如果分配不成功,调用Y::outOfMemory
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    C++ 并不支持class 专属之new-handlers ,但其实也不需要。

    现在,假设你打算处理Widget class 的内存分配失败情况。首先你必须登录“ 当operator new 无法为一个Widget 对象分配足够内存时” 调用的函数,所以你需要声明一个类型为new_handler 的static 成员,用以指向class Widget 的new-handler 。看起来像这样:

    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;    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    static成员必须在class定义式之外被定义(除非它们是const 而且是整数型,见条款2),所以需要这么写:

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

    Widget内的set_new_handler 函数会将它获得的指针存储起来,然后返回先前(在此调用之前)存储的指针,这也正是标准版 set_new_handler 的行为:

    std::new_handler Widget ::set_new_handler(std::new_handler p) throw()
    {
        std::new_handler oldHandler = currentHandler;
        currentHandler = p;
        return  oldHandler;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    最后,Widget的operator new 做以下事情:
    1、调用标准set_new_handler ,告知Widget 的错误处理函数。
    2、调用global operator new ,执行实际之内存分配。
    3、如果global operator new 能够分配足够一个Widget 对象所用的内存,Widget 的operator new 会返回一个指针,指向分配所得。

    下面代码阐述一遍。我将从资源处理类开始,那里面只有基础性RAII操作,在构造过程中获得一笔资源,并在析构过程中释还:

    
    class NewHandlerHolder
    {
    public:
        explicit NewHandlerHolder(std::new_handler nh):handler(nh){}//取得目前的new-handler
        ~NewHandlerHolder()
        {
            std::set_new_handler(handler);//释放它
        }
            
    private:
        std::new_handler handler;//记录下来
        NewHandlerHolder(const NewHandlerHolder&);//阻止copying
        NewHandlerHolder& operator=(const NewHandlerHolder&);
            
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这就使得Widget 的operator new的实现相当简单:

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Widget的客户应该类似这样适用其new-handler:

        void outOfMem();//函数声明,此函数在Widget对象分配失败时被调用
        Widget::set_new_handler(outOfMem);//设定outOfMem为Widget的new-handler函数
        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函数)
        
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    实现这一方案的代码并不因class的不同而不同,因此在它处加以复用是个合理的构想。

    下面的代码的类可以被任何有所需要的class使用:

    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);
            //... 其它的operator new版本——见条款52
    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)
    {
        NewHandlerSupport h(std::set_new_handler(currentHandler));
        return ::operator new(size);
    }
    //以下将每一个currentHandler 初始化为null
    template<typename T> 
    std::new_handler NewHandlerSupport<T>::currentHandler = 0 ;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    有了这个class template ,为Widget 添加set_new_handler 支持能力就轻而易举了:只要令Widget 继承自NewHandlerSupport< Widget > 就好,像下面这样。看起来似乎很奇妙:

    class Widget:public NewHandlerSupport<Widget>
    {
        //...和先前一样,但不必声明
        //set_new_handler 或operator new
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这就是Widget 为了提供“class 专属之set_new_handler”所需要做的全部动作。

    另一个形式的operator new ,负责供应传统的 “分配失败便返回null行为 ”。这个形式被称为“ nothrow ”形式——某种程度上是因为他们在new 的使用场合用了 nothrow对象(定义于头文件< new >):

    class Widge 
    {
        //...和先前一样,但不必声明
        //set_new_handler 或operator new
    };
    
     
        Widget *pw1 =new Widget;//如果分配失败,抛出bad_alloc
        if(pw1 == 0)
        {
            //...  //这个测试一定失败
        }
        Widget *pw2 =new (std::nothrow)Widget;//如果分配Widget失败,返回0
        if(pw2 == 0)
        {
            //...  //这个测试可能成功
        }
        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    Nothrow new 对异常的强制保证性并不高。
    使用nothrow new 只能保证operator new 不抛掷异常,不保证像“ new (std::nothrow) Widget ”这样的表达式绝不导致异常. 因此你其实没有运用nothrow new的需要。

    请记住:
    1、set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
    2、Nothrow new 是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常。

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

    想要替换编译器提供的operator new 或operator delete ,一般有以下三个理由:
    1、用来检测运用上的错误。
    2、为了强化效能。
    3、为了收集使用上的统计数据。

    下面的代码是个快递发展得出的初阶段global operator new ,促进并协助检测“overruns ”(写入点在分配区块尾端之后)或“ underruns”(写入点在分配区块起点之前。)

    static const int signature  = 0xDEADBEEF;
    typedef unsigned char Byte;
    //这段代码还有若干小错误,详细如下
    void * operator new(std::size_t size) throw(std::bad_alloc)
    {
        using namespace std;
        size_t realSize = size +2*sizeof (int);//增加大小,使能够塞入两个signature
        void *pMem = malloc(realSize);// 调用malloc 取得内存
        if(!pMem) throw bad_alloc();
        
        //将 signature写入内存的最前段落和最后段落
        *(static_cast<int*>(pMem)) =signature;
        *(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof (int))) = signature;
        
        //返回指针,指向恰位于第一个 signature 之后的内存位置
        return static_cast<Byte*>(pMem) +sizeof (int);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    这个operator new的缺点主要在于它疏忽了身为这个特殊函数所应该具备的“坚持C++规则”的态度。

    齐位的意义:C++要求所有operator new 返回的指针都有适当的对齐(取决于数据类型)。malloc 就是在这样的要求下工作,所以令operator new 返回一个得自malloc 的指针是安全的。然而上述operator new 中我并未返回一个得自malloc 的指针,而是返回一个得自malloc 且偏移一个int 大小的指针。如果客户端调用 operator new 企图获取足够给一个double 所用的内存(或者如果我们写个operator new[] ,元素类型是double ),而我们在一部“int 为4bytes 且double 必须8bytes齐位”的机器上跑,我们可能获得一个未有适当齐位的指针。那可能会造成程序崩溃或执行速度变慢。

    了解何时可在“全局性”或“class专属”基础上合理替换缺省的new 和delete。有如下用途:
    1、用来检测运用上的错误。
    2、为了收集动态分配内存之使用统计信息。
    3、为了增加分配和归还的速度。
    4、为了降低缺省内存管理器带来的空间额外开销。
    5、为了弥补缺省分配器中的最佳齐位。
    6、为了将相关对象成簇集中。
    7、为了获得非传统的行为。

    请记住:
    1、有许多理由需要写个自定的new 和 delete ,包括改善效能、对heap运用错误进行调试、收集heap 使用信息。

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

    奇怪的C++规定,即使客户要求0 bytes , operator new 也得返回一个合法指针。这种看似诡异的行为其实是为了简化语言其它部分。下面是个non-member operator new 伪码(pseudocode):

    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();
            }
            
        }
        
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

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

    下面的代码,base class 的operator new 被调用用以分配derived class对象:

    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
    //如果Base class 专属的operator new 并非被设计用来对付上述情况(实际上往往如此),
    //处理此情势的最佳做法是将“内存申请量错误”的调用行为改采标准operator new,像这样
    void *Base::operator new(std::size_t size) throw(std::bad_alloc)
    {
        if(size ! = sizeof (Base))//如果大小错误
            return ::operator new(size);//令标准的operator new 起而处理
        //。。。       //否则在这里处理。
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    C++裁定所有非附属(独立式)对象必须有非零大小(见条款39).因此sizeof(Base) 无论如何不能为零,所以如果size 是0,这份申请会被转交到 ::operator new 手上,后者有责任以某种合理方式对待这份申请。
    【这里所谓非附属/独立式对象,指的是不以“某对象之base class成分”存在的对象】

    如果你决定写个operator new [ ] ,唯一要做的事情的一件事就是分配一块未加工内存,因为你无法对array 之内迄今尚未存在的元素对象做任何事情。
    动态分配的arrays 可能包含额外空间用来存放元素个数。
    operator new 的要求是保证“删除null 指针永远安全”,下面是non-member operator delete 的伪码:

    void operator delete (void *rawMemory) throw()
    {
        if(rawMemory == 0)//如果被删除的是个Null指针
            return;// 那就什么都不做
        //现在归还rawMemory所指的内存
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    多加一个动作检测删除数量。万一你的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(std::bad_alloc);
    };
    
    void Base::operator delete(void *rawMemory,std::size_t size) throw(std::bad_alloc);
    {
        if(rawMemory == 0)//如果被删除的是个Null指针
            return;// 那就什么都不做
        if(size != sizeof(Base))//如果大小错误,令标准版operator delete 处理此一申请
        {
            ::operator delete(rawMemory);
            return;
        }
        //现在,归还rawMemory所指的内存
           return;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    请记住:
    1、operator new 应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler .。它应该有能力处理0 bytes申请。class 专属版本则还应该处理“比正确大小更大的(错误申请)”。
    2、operator delete 应该在收到null 指针时不做任何事情,class专属版本还应该处理“比正确大小更大的(错误申请)”。

    条款52:写了placement new也要写placement delete

    当你写一个new 表达式像这样:

    Widget* pw = new Widget;
    
    • 1

    有两个函数被调用:一个是用以分配内存的operator new , 一个是Widget 的 default 构造函数。

    如果目前面对的是拥有正常签名式的new和delete ,这并不是问题,因为正常的operator new:

    void * operator new(std::size_t) throw(std::bad_alloc);
    
    • 1

    对于正常的operator delete:

    void operator delete(void *rawMemory) throw();//global 作用域中的正常签名式
    void operator delete(void *rawMemory,std::size_t size) throw();//class 作用域中典型的签名式
    
    • 1
    • 2

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

    class Widget
    {
    
    public:
        static void * operator new(std::size_t size,std::ostream & logStream) throw(std::bad_alloc);
        //非正常形式的new
        
        static void operator delete(void *rawMemory,std::size_t size) throw();//正常的class 专属delete
        //...
        
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    下面是一个placement版本“ 接受一个指针指向对象该被构造之处”,具体代码如下:

    void * operator new(std::size_t,void *pMemory) throw();//placement new
    
    • 1

    这个版本的new属于C++ 标准库。

    下面代码是,在动态创建一个Widget时将相关的分配信息志记(logs)于cerr:

        Widget* pw = new (std::cerr)Widget;//调用operator new并传递cerr为其ostream 实参,这个动作会在Widget
        //构造函数抛出异常时泄漏内存
    
    • 1
    • 2

    如果内存分配成功,而Wdget构造函数抛出异常,运行期系统有责任取消operator new的分配并恢复旧观。

    由于上面的operator new接受类型为ostream& 的额外实参,所以
    下面是一个对应的operator delete:

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

    如果一个带额外参数的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();
        //...
        
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这样改变之后,如果以下语句引发Widget构造函数抛出异常:

    Widget* pw = new (std::cerr)Widget;//一如既往,但这次不再泄漏
    
    • 1

    对应的placement delete会被自动调用,让Widget有机会确保不泄漏内存任何内存。
    然而如果没有抛出异常,而客户代码中有个对应的delete ,会发生什么事情。

    delete pw;//调用正常的operator delete
    
    • 1

    假设你有一个base class ,其中声明唯一一个placement operator new,客户端会发现他们无法使用正常形式的new :

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    class Derived:public 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 Derived;//没问题,调用Derived 的operator new
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    对于撰写内存分配函数,你需要记住的是,缺省情况下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();//nothrow new 见条款49
    
    • 1
    • 2
    • 3

    如果你在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()
        {
            return ::operator delete(pMemory);
        }
        
           
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

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

    class Widget:public StandardNewDeleteForms //继承标准形式
    {
    
    public:
        using StandardNewDeleteForms::operator new;//让这些形式可见
        using StandardNewDeleteForms::operator delete;//让这些形式可见
        
        static void * operator new(std::size_t size, //添加一个自定的placement new
                                   std::ostream & logStream) throw(std::bad_alloc);
    
    
    
        static void operator delete(void *pMemory,//添加一个对应的placement delete
                                    std::ostream & logStream) throw();
        //...
    
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    请记住:
    1、当你写一个placement operator new ,请确定也写出了对应的placement operator delete 。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏。
    2、当你声明placement new和placement delete ,请确定不要无意识(非故意)地遮掩了它们的正常版本。

  • 相关阅读:
    数据结构-链表(OJ题)(4)
    数据结构与算法是什么?
    深入学习JVM底层(五):类加载机制
    【MicroPython ESP32】ssd1306 0.96“OLED+气象数据中文显示
    微信小程序文件类型
    java计算机毕业设计ssm二手商品交易平台
    如何避免JavaScript中的内存泄漏?
    打车出行小程序APP定制开发代驾拼车专车
    3.新建ZigBee工程
    Python列表条件筛选
  • 原文地址:https://blog.csdn.net/qq_30457077/article/details/128123178