• C++ 内存分配操作符new和delete详解


    重载new和delete


    首先借用C++ Primer 5e的一个例子:

    string *sp = new string("a value");
    string *arr = new string[10];
    这其实进行了以下三步操作:
    1. new表达式调用一个名为operator new(或者operator new[])的标准函数,分配一块足够大的,原始的,未命名的内存空间来存储特定的类型或者对象的数组。
    2. 编译器运行相应的构造函数以构造这些对象,并且传入初值。
    3. 对象构造完毕后返回指向该对象的指针。
    当我们进行下列的语句时:
    delete sp;
    delete[]arr;
     
    这段代码也执行了以下两个步骤:
    1. sp所指的对象或者arr所指的数组中的元素执行析构函数
    2. 然后第二部调用operator delete或者(operator delete[])的标准库来释放掉内存空间。
    应用程序可以在全局作用域定义operator new和operator delete函数,也可以把他们定义为成员函数。operator new和operator delete的查找满足C++作用域的查找方式。
     
    现在C++17版本的operator new可以有以下形式
    replaceable allocation functions
    void* operator new  ( std::size_t count );
    void* operator new[]( std::size_t count );
    void* operator new  ( std::size_t count, std::align_val_t al);(since C++17)
    void* operator new[]( std::size_t count, std::align_val_t al);(since C++17)
    
    replaceable non-throwing allocation functions(nothrow版本表示承诺不抛出异常,分配内存失败直接返回null,但是不保证构造函数不抛出异常,没什么使用必要)
    void* operator new  ( std::size_t count, const std::nothrow_t& tag);
    void* operator new[]( std::size_t count, const std::nothrow_t& tag);
    void* operator new  ( std::size_t count, std::align_val_t al, const std::nothrow_t&);(since C++17)
    void* operator new[]( std::size_t count, std::align_val_t al, const std::nothrow_t&);(since C++17)
    
    non-allocating placement allocation functions(注意这两个版本不能重定义, 也就是常见的placement newvoid* operator new  ( std::size_t count, void* ptr );
    void* operator new[]( std::size_t count, void* ptr );
    
    user-defined placement allocation functions
    void* operator new  ( std::size_t count, user-defined-args... );
    void* operator new[]( std::size_t count, user-defined-args... );
    void* operator new  ( std::size_t count, std::align_val_t al, user-defined-args... );    (since C++17)
    void* operator new[]( std::size_t count, std::align_val_t al, user-defined-args... );(since C++17)
    
    class-specific allocation functions
    void* T::operator new  ( std::size_t count );
    void* T::operator new[]( std::size_t count );
    void* T::operator new  ( std::size_t count, std::align_val_t al );(since C++17)
    void* T::operator new[]( std::size_t count, std::align_val_t al );(since C++17)
    
    class-specific placement allocation functions
    void* T::operator new  ( std::size_t count, user-defined-args... );
    void* T::operator new[]( std::size_t count, user-defined-args... );
    void* T::operator new  ( std::size_t count, std::align_val_t al, user-defined-args... );(since C++17)
    void* T::operator new[]( std::size_t count, std::align_val_t al, user-defined-args... );(since C++17)
     
    现在C++17版本的operator delete可以存在以下版本:
    replaceable usual deallocation functions
    void operator delete  ( void* ptr );
    void operator delete[]( void* ptr );
    void operator delete  ( void* ptr, std::align_val_t al );(since C++17)
    void operator delete[]( void* ptr, std::align_val_t al );(since C++17)
    void operator delete  ( void* ptr, std::size_t sz );(since C++14)
    void operator delete[]( void* ptr, std::size_t sz );(since C++14)
    void operator delete  ( void* ptr, std::size_t sz, std::align_val_t al );(since C++17)
    void operator delete[]( void* ptr, std::size_t sz, std::align_val_t al );(since C++17)
    replaceable placement deallocation functions(同new,只能保证operator delete不抛出异常,但是不能保证析构不抛出异常)
    void operator delete ( void* ptr, const std::nothrow_t& tag ); void operator delete[]( void* ptr, const std::nothrow_t& tag ); void operator delete ( void* ptr, std::align_val_t al, const std::nothrow_t& tag );(since C++17) void operator delete[]( void* ptr, std::align_val_t al, const std::nothrow_t& tag );(since C++17)
    non
    -allocating placement deallocation functions(也就是传统意义上的placement deletevoid operator delete ( void* ptr, void* place ); void operator delete[]( void* ptr, void* place );
    user
    -defined placement deallocation functions void operator delete ( void* ptr, args... ); void operator delete[]( void* ptr, args... );
    class-specific usual deallocation functions void T::operator delete ( void* ptr ); void T::operator delete[]( void* ptr ); void T::operator delete ( void* ptr, std::align_val_t al );(since C++17) void T::operator delete[]( void* ptr, std::align_val_t al );(since C++17) void T::operator delete ( void* ptr, std::size_t sz ); void T::operator delete[]( void* ptr, std::size_t sz ); void T::operator delete ( void* ptr, std::size_t sz, std::align_val_t al );(since C++17) void T::operator delete[]( void* ptr, std::size_t sz, std::align_val_t al );(since C++17)
    class-specific placement deallocation functions void T::operator delete ( void* ptr, args... ); void T::operator delete[]( void* ptr, args... );
     
    当我们重载operator new和operator delete的时候,一定不能改变其分配内存/回收内存的本质。
     
    placement new与placement delete

    上面有四个版本的operator new和operator delete
    non-allocating placement allocation functions(注意这两个版本不能重定义, 也就是常见的placement newvoid* operator new  ( std::size_t count, void* ptr );
    void* operator new[]( std::size_t count, void* ptr );
    
    non-allocating placement deallocation functions(也就是传统意义上的placement deletevoid operator delete  ( void* ptr, void* place );
    void operator delete[]( void* ptr, void* place );
     
    就是传统的placement new和placement delete,不允许重新定义,特点就是额外参数多了一个指针,不分配内存,专门对对象进行构造。使用形式可以像下面那样:
    new (place_address) type
    new (place_address) type (initializers)
    new (place_address) type [size]
    new (place_address) type [size]{ braced initializer list }
     
    使用placement我们可以更方便控制内存分配(比如要做内存池),比如SGI STL中的construct就运用了这种方法:(实际上在C++11以后consturct就规定可以不是默认构造了,可以有其他构造方法。)
    template <class _T1>
    inline void _Construct(_T1* __p) {
      new ((void*) __p) _T1();
    }
    template <class _Tp>
    inline void _Destroy(_Tp* __pointer) {
      __pointer->~_Tp();
    }
     
    当我们使用了我们自定义的placement new(注意一般来说,我们说的placement new就是上面那个不分配内存的那个版本,其他对operator new进行重载的版本,也可以称为placement new(有额外参数),placement delete同理)时,要注意一定要同时定义相同形式的placement delete,否则会发生潜在的内存泄漏。
     
    比如现在我们在某个类中定义了我们自己的placement new和placement delete
    class Widget
    {
    public:
        Widget(int i) :m_haha(i) { throw std::exception(); }
        static void *operator new(std::size_t size, std::ostream& logStream);
        static void operator delete(void *pMemory)
        { 
            ::operator delete(pMemory);
        }
        static void operator delete(void *pMemory, std::ostream& logStream);
    private:
        int m_haha;
    };
    void *Widget::operator new(std::size_t size, std::ostream& logStream)
    {
        cout << "Hello World" << std::endl;
        while (true)
        {
            auto p = malloc(size);
            if (p)
                return p;
            
            auto h = get_new_handler();
            if (!h) 
                h();
            else
                throw std::bad_alloc();
        }
    }
    void Widget::operator delete(void *pMemory, std::ostream& logStream)
    {
        cout << "Hello World delete" << std::endl;
        ::delete pMemory;
    }
    int main()
    {
        try
        {
            Widget *k = new (std::cout) Widget(1);
            delete k;
        }
        catch (const std::exception&)
        {
        }
        return 0;
    }
     
    这里可能会让人产生一个疑问,为什么我们不是调用placement delete来删除对象呢?这里需要注意的是,placement delete只会在对应形式(也就是除了开头的第一个,其他参数一模一样)placement new构造对象发生异常以后才会被调用(也就是有placement delete function,但是没有 placement delete expression,我们不能手动调用placement delete)。
     
    上述代码的运行结果:
     
    如果我们要定义一个类型公有继承某个重载了operator new 和operator delete的类型,而我们又想在新的类型里面添加新的operator new 和operator delete,则可以这么写:
    class DerivedWidget : public Widget
    {
    public:
        using Widget::operator delete;
        using Widget::operator new;
        static void *operator new(std::size_t size, const std::nothrow_t nt);
        static void operator delete(void *pMemory, const std::nothrow_t nt);
    };
    new_handler

    当operator new无法申请到所需内存时,我们可以调用所谓的new_handler,在标准库中有以下内容:
    typedef void (__CLRCALL_PURE_OR_CDECL * new_handler) ();
     #endif /* !defined(_INC_NEW) || !defined(_MSC_EXTENSIONS) */
            // FUNCTION AND OBJECT DECLARATIONS
    _CRTIMP2 new_handler __cdecl set_new_handler(_In_opt_ new_handler)
        _THROW0();    // establish alternate new handler
    _CRTIMP2 new_handler __cdecl get_new_handler()
        _THROW0();    // get current new handler
    _STD_END
    其中get_new_handler()是C++ 11新增的,在没有这个方法之前,我们想要获得全局的hanlder只能用set一个0的handler(这样会获得当前的hanlder),然后再把获得的handler再set回去的别扭手段。
     
    由于operator new的实现要求是:当存在new_handler,就会不断调用,直到找到够用的内存为止。
     
    一个设计良好的new_handler必须做以下事情:
    1. 让更多内存可被使用
    2. 安装另一个new-handler(如果当前的new-handler无法获取更多的内存,可以用另一个代替)
    3. 卸载new-handler
    4. 抛出bad_alloc(或者派生自此异常的其他异常),让异常传播到其他地方
    5. 不返回,调用abort或者exit
     
    effective C++ 3e有一个非常好的实践例子,以CRTR模式封装new_handler的技术(curiously recurring template pattern; CPTR)
    template<typename T>
    class NewHandlerSupport
    {
    public:
        NewHandlerSupport() = default;
        explicit NewHandlerSupport(std::new_handler h):_handler(h) { }
        ~NewHandlerSupport() { std::set_new_handler(_handler); }
        static void *operator new(std::size_t size) { ::operator new(size); }
        static void *operator new(std::size_t size, std::ostream& logStream);
        static void operator delete(void *pMemory) { ::operator delete(pMemory); }
        static void operator delete(void *pMemory, std::ostream& logStream);
        static std::new_handler set_new_handler(std::new_handler p)noexcept;
    private:
        std::new_handler _handler;//拿来临时替换的
        static std::new_handler _currnentHandler;
        NewHandlerSupport(NewHandlerSupport &&) = default;
        NewHandlerSupport(const NewHandlerSupport &) = delete;
        NewHandlerSupport& operator=(const NewHandlerSupport &) = delete;
    };
    template<typename T>
    std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p)noexcept
    {
        std::new_handler oldHandler = _currnentHandler;
        _currnentHandler = p;
        return oldHandler;
    }
    template<typename T>
    void NewHandlerSupport<T>::operator delete(void *pMemory, std::ostream& logStream)
    {
        cout << "Hello World delete" << std::endl;
        ::delete pMemory;
    }
    template<typename T>
    void * NewHandlerSupport<T>::operator new(std::size_t size, std::ostream& logStream)
    {
        NewHandlerSupport h(std::set_new_handler(_currnentHandler));
        cout << "Hello World" << std::endl;
        return ::operator new(size);
    }
    template<typename T>std::new_handler NewHandlerSupport<T>::_currnentHandler = 0;
    class Widget : public NewHandlerSupport<Widget>
    {
    public:
        Widget(int i) :m_haha(i) { }
    private:
        int m_haha;
    };
     
    这样就可以一个类型一个不同的new_handler了,而且不同类型的new_handler只会影响到自身的operator new。
     
    使用时可以这样:
    Widget::set_new_handler(error);
    Widget *k = new (std::cout) Widget(1);
    delete k;
    new与malloc在内存布局上的区别

    我们经常说C++的内存管理(new和delete)和C的(malloc和free)最大的不同是,C++分配的内存是在自由存储区,而C则是在堆区,至于堆区就是现代操作系统给进程分配的内存空间的一部分,看下面的图。


    实际上在VS编译器上,缺省的operator new 反汇编出来是下面的样子:
    void* __CRTDECL operator new(size_t const size)
    {
    00007FF74DC58630  mov         qword ptr [rsp+8],rcx  
    00007FF74DC58635  sub         rsp,38h  
        for (;;)
        {
            if (void* const block = malloc(size))
    00007FF74DC58639  mov         rcx,qword ptr [size]  
    00007FF74DC5863E  call        malloc (07FF74DC513FCh)  
    00007FF74DC58643  mov         qword ptr [rsp+20h],rax  
    00007FF74DC58648  cmp         qword ptr [rsp+20h],0  
    00007FF74DC5864E  je          operator new+27h (07FF74DC58657h)  
            {
                return block;
    00007FF74DC58650  mov         rax,qword ptr [rsp+20h]  
    00007FF74DC58655  jmp         operator new+4Bh (07FF74DC5867Bh)  
            }
            if (_callnewh(size) == 0)
    00007FF74DC58657  mov         rcx,qword ptr [size]  
    00007FF74DC5865C  call        _callnewh (07FF74DC516B8h)  
    00007FF74DC58661  test        eax,eax  
    00007FF74DC58663  jne         operator new+49h (07FF74DC58679h)  
            {
                if (size == SIZE_MAX)
    00007FF74DC58665  cmp         qword ptr [size],0FFFFFFFFFFFFFFFFh  
    00007FF74DC5866B  jne         operator new+44h (07FF74DC58674h)  
                {
                    __scrt_throw_std_bad_array_new_length();
    00007FF74DC5866D  call        __scrt_throw_std_bad_array_new_length (07FF74DC51604h)  
                }
                else
    00007FF74DC58672  jmp         operator new+49h (07FF74DC58679h)  
                {
                    __scrt_throw_std_bad_alloc();
    00007FF74DC58674  call        __scrt_throw_std_bad_alloc (07FF74DC5120Dh)  
                }
            }
            // The new handler was successful; try to allocate again...
        }
    00007FF74DC58679  jmp         operator new+9h (07FF74DC58639h)  
    }
     
     
    同样,全局delete反汇编出来是:
    void __CRTDECL operator delete(void* const block) noexcept
    {
    00007FF74DC586A0  mov         qword ptr [rsp+8],rcx  
    00007FF74DC586A5  sub         rsp,28h  
        #ifdef _DEBUG
        _free_dbg(block, _UNKNOWN_BLOCK);
    00007FF74DC586A9  mov         edx,0FFFFFFFFh  
    00007FF74DC586AE  mov         rcx,qword ptr [block]  
    00007FF74DC586B3  call        _free_dbg (07FF74DC5119Ah)  
        #else
        free(block);
        #endif
    }
     
    我们可以看到实际上在PJ版本的STL(也就是VS自带的那个),new缺省的实现方式本质上是通过malloc的,这个时候,C++的自由存储区的概念和C的堆的概念是没有区别的,但是如果我们通过重载operator new 的方式把内存分配在一些全局变量上,那么这些内存就不属于堆区了,而是在data segment。也就是说,C++的自由存储区可以包括堆区,也可以包括其他区域。
     
    同时我们也可以看到,当分配内存失败时,C++的处理方式也和C不一样,我们知道,在C当malloc失败的时候,会直接返回一个NULL,但是在C++不是的,如果在没有指定new_handler的情况下,会直接抛出bad_alloc异常,而不是返回NULL,如果指定了new_handler,那么会调用handler进行处理,再进行分配,直到分配成功为止。
     
    如果使用new进行内存分配,那么C++不仅会分配内存,而且还会进行placement new构造对象,而malloc不会的。同理,C++的delete也会对对象进行析构,而free不会。在申请数组时,在C++中,一定要进行delete[]进行内存的回收,在C++中用new分配了一个数组,会添加其他信息(比如长度),如果不使用delete[]形式删除数组,那么会导致内存不会被完全回收导致内存泄漏,而且对象也不会被正确析构。

    allocator类
    在C++11以后,我们可以使用std::allocator来进行像内存池的操作,可以像下面这样用:
    std::allocator<string> alloc;
    auto p = alloc.allocate(10);
    alloc.construct(p, "fuck");
    construct是构造一个对象,destory对对象进行析构,deallocate可以我们通过allocate分配的内存。这样我们就可以很方便地构造内存池了。
     
    allocator在C++17已经被弃用,转而应该使用std::allocator_traits。std::allocator_traits中有allocator内有的东西,但是所有方法都变成了静态的,这种设计的思路是:因为allocator的任务只是allocate和deallocate,而construct应该与allocator无关。这样变更以后allocator的功能更明显了。
     
     
     
     
    Reference:
     
     
     
  • 相关阅读:
    Java配置jdk图文教程
    线程池介绍与应用
    继承机制的探讨
    1.深入分析_NIO性能分析
    1.类的加载机制_继承类的加载(一个小的Demo)说明
    githup创建新java项目
    UE常用快捷键使用
    堡垒机上传文件
    16.linux常用查看命令
    15.vi/vim编辑器下常用光标移动
  • 原文地址:https://www.cnblogs.com/Philip-Tell-Truth/p/6567808.html
Copyright © 2020-2023  润新知