美文网首页C++Effective C++
【Effective C++(8)】定制new和delete

【Effective C++(8)】定制new和delete

作者: downdemo | 来源:发表于2018-01-09 14:21 被阅读36次

    operator new
    operator delete
    set_new_handler

    49 了解new-handler的行为

    • 如果operator new分配内存失败,在抛出异常之前,它会先调用一个new-handler函数用以处理内存不足的错误,这个错误处理函数需要通过调用头文件<new>中的set_new_handler函数指定
    • set_new_handler做三件事
      • 尝试使更多内存可用
      • 终止程序(如调用std::terminate)
      • 抛出std::bad_alloc或std::bad_alloc的派生类异常
    namespace std {
        typedef void(*new_handler)();
        new_handler set_new_handler(new_handler new_p) noexcept;
    }
    
    • new_handler是个无参数无返回值函数的typedef,它定义出一个函数指针,用作set_new_handler函数的实参和返回类型
    • set_new_handler的参数是个函数指针,指向operator new无法分配足够内存时该调用的new-handler函数,如果是空指针,默认的分配函数直接抛std::bad_alloc异常
    • set_new_handler的返回值也是个指针,指向set_new_handler被调用前正在执行的那个new-handler函数
    • 可以这样使用set_new_handler函数
    #include <iostream>
    #include <new>
    
    void handler()
    {
        std::cerr << "Unable to satisfy request for memory\n";
        std::abort();
    }
    
    int main()
    {
        std::set_new_handler(handler);
        while (true) new char[1024*1024*1024]; // 循环分配1GB内存
        // 分配失败时调用handler()函数
    };
    
    • 如果operator new无法分配足够空间,new-handler函数会被调用,于是程序发出错误信息后abort。当operator new无法满足内存申请时,他会不断调用new-handler函数,直到找到足够内存。 一个设计良好的new-handler函数必须做到以下事情
      • 让更多的内存可以被使用。一个做法是程序一开始执行就分配一大块内存,而后当new-handler第一次被调用,将它们释放给程序使用
      • 安装另一个new-handler。如果目前这个new-handler无法取得更多内存,或许它知道另外哪个有此能力,这样目前这个就可以调用set_new_handler安装另外那个替换自己
      • 卸载new-handler。也就是将空指针传给set_new_handler,一旦没安装任何new-handler,operator new会在内存分配不成功时抛出异常
      • 抛出bad_alloc(或派生自bad_alloc)的异常。这样的异常不会被operator new捕捉,因此会被传播到内存索求处
      • 不返回。通常调用std::abort()或者std::exit(1)
    #include <iostream>
    #include <new>
     
    void handler()
    {
        std::cout << "Memory allocation failed, terminating\n";
        std::set_new_handler(nullptr); // 分配失败则抛std::bad_alloc异常
    }
     
    int main()
    {
        std::set_new_handler(handler);
        try {
            while (true) {
                new int[100000000ul];
            }
        } catch (const std::bad_alloc& e) {
            std::cout << e.what() << '\n';
        }
    }
    
    • 要对类对象处理内存分配失败情况,需要声明一个类型为new_handler的static成员,用以指向类的new-handler,当operator new无法为对象分配足够内存时调用
    class A {
    public:
        static std::new_handler set_new_handler(std::new_handler p) noexcept;
        static void* opreator new(std::size_t size) throw(std::bad_alloc);
    private:
        static std::new_handler currentHandler;
    };
    // static成员必须在class定义式之外被定义
    std::new_handler A::currentHandler = 0;
    
    • set_new_handler函数会将类当前的指针存储起来,再将当前指针设置为new-handler,然后返回存储的旧指针,这也是标准版set_new_handler的行为
    std::new_handler A::set_new_handler(std::new_handler p) noexcept
    {
        std::new_handler oldHandler = currentHandler;
        currentHandler = p;
        return oldHandler;
    }
    
    • 最后,operator new做以下事情
      • 调用标准set_new_handler,告知类的错误处理函数,即将new-handler安装为global new-handler
      • 调用global operator new,执行实际分配内存。如果分配失败,global operator new会调用类的new-handler(上一步刚被安装为global new-handler)
      • 如果global new最终无法分配足够内存,会抛出一个std::bad_alloc异常,在此情况下类的operator new必须恢复为原本的global new-handler然后再传播该异常
      • 如果global operator new能够分配足够一个类对象所用的内存,类的operator new会返回一个指针,指向分配所得。析构函数会自动恢复operator new被调用前的global new-handler
    • 下面通过代码详细解释。使用基础RAII操作,构造时存储原有的new-handler,析构时将通过调用set_new_handler安装原有的new-handler
    class A {
    public:
        static std::new_handler set_new_handler(std::new_handler p) noexcept;
        static void* opreator new(std::size_t size) throw(std::bad_alloc);
    private:
        static std::new_handler currentHandler;
    };
    std::new_handler A::currentHandler = 0;
    std::new_handler A::set_new_handler(std::new_handler p) noexcept
    {
        std::new_handler oldHandler = currentHandler;
        currentHandler = p;
        return oldHandler;
    }
    
    class NewHandlerHolder {
    public:
        explicit NewHandlerHolder(std::new_handler nh) // 取得目前new-handler
        : handler(nh) {}
    
        ~NewHandlerHolder() // 释放它
        { std::set_new_handler(handelr); }
    
        NewHandlerHolder(const NewHandlerHolder&) = 0; // 阻止copying
        NewHandlerHolder& operator=(const NewHandlerHolder&) = 0;
    private:
        std::new_handler handlder; // 记录下来
    };
    
    // 这样operator new的实现就很简单
    void* A::operator new(std::size_t size) throw(std::bad_alloc)
    {
        NewHandlerHolder h(std::set_new_handler(currentHandler)); // A安装new-handler
        // 返回值为A之前拥有的currentHandler,即旧的指针被传给NewHandlerHolder存储
        return ::operator new(size); // 分配内存或者抛出异常
    }; // NewHandlerHolder执行析构函数,安装存储的旧指针,从而恢复global new-handler
    
    // A的用户应该类似这样使用其new-handling
    void outOfMem(); // 函数声明,此函数在对象分配失败时被调用
    A::set_new_handler(outOfMem); // 设定outOfMem为类的new-handling函数
    A* p1 = new A; // 如果内存分配失败,调用outOfMem
    std::string* ps = new std::string; // 如果内存分配失败,调用global new-handling函数
    A::set_new_handler(0); // 设定类专属的new-handling函数为空
    A* p2 = new A; // 如果内存分配失败,立刻抛出异常
    
    • 上述实现并不因class的不同而不同,因此创建适用于所有类的类模板new-handler是个好想法。创建一个用于设置new-handler的类模板,然后在子类中只需简单继承即可
    template <typename T>
    class NewHandlerSupport {
    public:
        static std::new_handler set_new_handler(std::new_handler p) noexcept;
        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) noexcept
    {
        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(currentHandlder));
        return ::operator new(size);
    }
    
    template <typename T>
    std::new_handler NewHandlerSupport<T>::currentHandler = 0;
    
    // 有了这个class template,为A添加set_new_handler就很容易了
    class A::public NewHandlderSupport<A> {
        ...
        // 和先前一样,但不必声明set_new_handler或者operator new
    };
    
    • 上述做法就是CRTP,看起来模板参数T从未被使用,实际上也不需要使用。这种做法只是希望每个派生类拥有实例互异的NewHandlerSupport复件,更确切地说是其static成员currentHandler,T只是用来区分不同的派生类,模板机制会为每个T生成一份currentHandler

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

    • 替换标准库提供的operator new或operator delete通常基于以下三个理由
      • 用来检测运行上的错误。将"new所得内存"delete失败会导致内存泄露,多次对同一块"new所得内存"delete会导致未定义行为。如果operator new持有一串动态分配所得地址,而operator delete将地址从中移走,很容易检测出上述错误。各式各样的编程错误会导致数据"overruns"(写入点在分配区块末端之后)或"underruns"(写入点在分配区块起点之前)。如果自定义一个operator new便可超额分配内存,以额外空间放置特定的byte patterns(即签名,signatures)。operator delete便可以上述签名是否原封不动,若否就表示在分配区的某个生命时间点发生overrun或underrun,这时operator delete可以志记(log)那个事实以及进行非法操作的指针
      • 为了强化效能。标准库所提供的operator new和operator delete主要用于一般目的,它们不但可被长时间执行的程序(如网页服务器,web server)接受,也可被执行时间小于一秒的程序接受。它们必须处理一系列需求,包括大块内存、小块内存、大小混合内存。它们必须接纳各种分配形态,范围从程序存活期间的少量区块动态分配,到大数量短命对象的持续分配和归还。它们必须考虑破碎问题,这最终会导致程序无法满足大区块内存要求。由于对内存管理器的要求多种多样,因此标准库所提供的operator new和operator delete采取中庸之道。如果对自己程序的动态内存运行形态有深刻的了解,就会发现定制版的operator new和operator delete性能胜过缺省版本,它们比较快,需要的内存比较少
      • 为了收集使用上的统计数据。自行定义operator new和operator delete可以帮助收集软件内存区块大小分布、寿命分布、内存归还次序、最大动态分配量等信息
    • 以下是一个初阶段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);
        void* pMem = malloc(realSize);
        if (!pMem) throw bad_alloc();
        // 将signature写入内存的最前段落和最后段落
        *(static_cast<int*>(pMem)) = signatrue;
        *(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof(int))) = signature;
        // 返回指针,指向恰位于第一个signature之后的内存位置
        return static_cast<Byte*>(pMem) + sizeof(int);
    }
    
    • 这个operator new的主要缺点在于它忽略了其应该具备的坚持C++规则的态度,比如条款51提到所有operator new都应该内含一个循环,反复调用某个new-handling函数,这里却没有。此外,还有一个更加微妙的主题:内存对齐(alignment)
    • 许多计算机体系结构要求特定类型必须放在特定的内存地址上,例如指针的地址必须是4的倍数,double的地址必须是8的倍数。如果不满足这个条件可能会导致运行期硬件异常。有些体系结构如Intel x86架构的double可被对其于任何边界,但如果它是8-byte齐位,其访问速度将会快许多
    • 在定制operator new的过程中,内存对齐意义重大,因为C++要求所有operator new返回的指针都有适当的对齐。malloc就是在这样的要求下工作,所以令malloc返回一个得自malloc的指针是安全的。然而上述operator new并未提供这样的保证,而是返回一个得自malloc且偏移一个int大小的指针,并不安全。如果客户端调用operator new来获取给一个double所用的内存,同时在一部int为4bytes且doubles必须为8-byte齐位的机器上运行,可能会获得一个未有适当齐位的指针,这可能会导致程序崩溃或执行速度变慢。因此写一个能优良运作的内存管理器可能并不容易,许多平台已有可以替代编译器自带之内存管理器的商业产品,开放源码领域的内存管理器(如Boost库的Pool)也都可用,因而可能并不需要自己定制operator new和operator delete
    • 总结下来,定制operator new和operator delete的作用
      • 为了检测运行错误
      • 为了收集动态分配内存之使用统计信息
      • 为了增加分配和归还的速度
      • 为了降低缺省内存管理器所带来的额外开销
      • 为了弥补缺省分配器中的非最佳齐位
      • 为了将相关对象成簇集中
      • 为了获得非传统行为

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

    • 上条已经说明为什么要写自己的operator new和operator delete,本条款解释在编写时遵循什么守则
    • 从operator new开始。operator new必须返回正确的值,内存不足时必须调用new-handling函数,要有对付零内存需求的准备,避免不慎掩盖正常形式的new——虽然这比较偏近class接口的要求而非实现要求。正常形式的new描述于条款52
    • operator new如果申请内存成功,就返回指向那块内存的指针,否则抛出bad_alloc异常。operator new实际上不只一次尝试内存分配,并在每次失败后都调用new-handling函数。这里假设new-handling函数能做某些动作将某些内存释放出来,只有指向new-hangling函数的指针为空operator new才会抛出异常。C++规定,即使客户要求0 byte,operator new也要返回一个合法指针,这种看似诡异的行为其实是为了简化语言其他部分,下面是个non-member operator new的伪码
    void* operator new(std::size_t size) throw(std::bad_alloc)
    {
        using namespace std;
        if (size == 0) { // 处理0-byte申请
            size = 1; // 将其视为1-byte申请
        }
        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();
        }
    }
    
    • 如果在多线程环境下,还需要某种锁机制,以便处理new-handling函数背后的global数据结构。上面包含一个无穷循环,退出此循环的唯一办法是:内存分配成功或new-handling做了一件描述于条款49中的事:让更多内存可用、安装另一个new-handler、卸载new-handler、抛出bad_alloc异常(或其派生类),或承认失败直接return。上面的operator new成员函数可能会被derived classes继承。函数尝试分配size bytes,这很合理,因为size是函数接收的实参。条款50提到,定制内存分配器往往是为了针对特定的class对象分配行为提供最优化,却不是为了该class的派生类
    class Base {
    public:
        static void* operator new(std::size_t size) throw(std::bad_alloc);
        ...
    };
    class Derived : public Base
    { ... }; // 假设重新定义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);
        ...
    }
    
    • 如果希望控制class专属版的arrays内存分配,那么需要实现operator new[]。编写operator new[]时,唯一要做的一件事就是分配一块未加工内存,因为无法对array之内迄今尚未存在的元素对象做任何事,无法知道这个array含有多少个元素对象。可能不知道每个对象多大,因为base class的operator new[]有可能经由继承被调用,将内存分配给derived class对象的array使用。因此不能在Base::operator new[]中假设每个元素对象大小是sizeof(Base),这也意味着不能假设array元素个数是bytes申请数/sizeof(Base)。此外,传递给operator new[]的size_t参数,其值有可能比“将被填以对象”的内存数量更多,因为条款16说过,动态分配的array可能包含额外空间用来存放元素个数
    • operator delete情况就简单很多,唯一需要记住的就是C++保证删除空指针永远安全,你必须兑现这项保证。下面是non-member operator delete的伪码
    void operator delete(void* rawMemory) noexcept
    {
        if(rawMemory == 0) return; // 如果被删除的是个空指针就什么都不做
        归还rawMemory所指内存;
    }
    
    • 这个函数的member版本也很简单,只需多加一个检查删除数量
    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) noexcept
        ...
    };
    
    void Base::operator delete(void rawMemory, std::size_t size) noexcept
    {
        if (rawMemory == 0) return;
        if (size != sizeof(Base)) {
            ::operator delete(rawMemory);
            return;
        }
        归还rawMemory所指内存;
        return;
    }
    
    • operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0bytes申请。class专属版本则还应该处理“比正确大小更大的(错误)申请”
    • operator delete应该在收到null指针时不做任何事。class专属版本则还应该处理“比正确大小更大的(错误)申请”

    52 写了placement new也要写placement delete

    • placement new和placement delete是C++经常用到但是却不常见的两个操作符。当我们使用new创建一个对象时
    A* p = new A;
    
    • 有两个函数被调用,第一个函数就是用以分配内存的operator new,第二个是类的默认构造函数。如果第一个函数调用成功,但是第二个函数抛出异常,这时需要释放第一步分配的内存,否则就造成了内存泄露。此时用户无法归还内存,因为构造函数抛出异常,p尚未被赋值,用户手中没有指向要被归还的内存的指针
    • 释放内存的任务落到了C++运行期系统身上,运行期系统会调用第一个函数operator new所对应的operator delete版本,operator delete可能有多个版本,因此它必须知道该调用的版本
    void* operator new(std::size_t) throw(std::bad_alloc);
    void operator delete(void* rawMemory) noexcept; // global作用域中的正常签名式
    void operator delete(void* rawMemory, std::size_t size) noexcept; // class作用域中典型的签名式
    
    • 正常的operator new对应于正常的operator delete,如果使用正常的operator new和operator delete,运行期系统可以找到释放new开辟内存的delete函数。但是如果使用非正常形式的operator new就会有问题了。假设有一个类专属的operator new,它接受一个用来记录相关分配信息的ostream,同时又写了一个正常形式的类专属的operator delete
    class A {
    public:
        ...
        static void* operator new(std::size_t size, std::ostream& logStream) // 非正常形式的new
            throw(std::bad_alloc);
        static void operator delete(void* pMemory, std::size_t size) noexcept; // 正常的class专属delete
        ...
    };
    
    • 这个设计有问题,在讨论问题前先说明若干术语。operator new除了必需的size_t,还接受其他参数,这就是所谓的placement new。因此,上述的operator new是个placement版本
    • 特别有用的一个placement new版本会接受一个指向对象构造之处的指针,形式如下
    void* operator new(std::size_t, void* pMemory) noexcept; // placement new
    
    • 这个版本的new已被纳入标准库<new>中,它的用途之一是负责在vector的未使用空间上创建对象,它同时也是最早的placement new版本,谈到placement new时通常指的就是这一特定版本
    • 现在再来看一下类的声明式,这个类会引起微妙的内存泄露。例如动态创建一个类对象时将分配信息记录于cerr
    A* p = new (std::cerr) A; // 调用operator new,并传递cerr作为ostream实参
    // 这个动作会在构造函数抛出异常时泄露内存
    
    • 如果内存分配成功,而构造函数抛出异常,运行期系统负责释放operator new分配的内存,但运行期系统不知道真正被调用的operator new行为。于是运行期系统寻找参数个数和类型都与operator new相同的operator delete,这里的operator new接受ostream&类型的额外实参,对应的operator delete就应该是
    void operator delete(void*, std::ostream&) noexcept;
    
    • 类似于new的placement版本,接收额外参数的operator delete称为placement delete。由于类没有声明placement delete,运行期系统不知道如何取消并恢复对placement new的调用,于是什么都不做,因此类必须声明一个对应的placement delete
    class A {
    public:
        ...
        static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
        static void operator delete(void* pMemory) noexcept;
        static void operator delete(void* pMemory, std::ostream& logStream) noexcept;
        ...
    };
    
    • 这样,如果下面的代码导致构造函数抛出异常,就会调用对应的placement delete
    A* p = new (std::cerr) A; // 一如既往,但这次不再泄漏
    
    • 但如果没有抛出异常(通常如此),即使有对应的placement delete,调用的也是正常形式的operator delete,因为只有伴随placement new调用的构造函数抛出异常,对应的placement delete才会被调用
    • 成员函数的名称会覆盖其外围作用域中的相同名称,必须避免类专属的new覆盖外围作用域的new。比如一个类中声明了唯一的placement operator new,客户将无法使用正常形式的new
    class Base {
    public:
        ...
        static void* operator new(std::size_t size, std::ostream& logStream)
            throw(std::bad_alloc); // 这个new会掩盖正常形式的new
        ...
    };
    Base* pb1 = new Base; // 错误,因为正常形式的operator new被掩盖
    Base* pb2 = new (std::cerr) Base; // 正确,调用Base的placement new
    
    • 同理,派生类中的operator new会覆盖全局版本和继承版本
    class Derived: public Base {
    public:
        ...
        static void* operator new(std::size_t size) throw(std::bad_alloc); // 重新声明正常形式的new
    };
    Derived* pd1 = new (std::clog) Derived; // 错误,因为Base的placement new被掩盖了
    Derived* pd2 = new Derived; // 正确,调用Derived的operator new
    
    • 以下标准形式operator new在全局作用域默认提供
    void* operator new(std::size_t) throw(std::bad_alloc); // normal new
    void* operator new(std::size_t, void*) noexcept; // placement new
    void* operator new(std::size_t, const std::nothrow_t&) noexcept; // nothrow new
    
    • 类内声明任何形式的operator new都会覆盖这些标准形式,对于每个可用的operator new必须提供对应的operator delete。一个简单做法是建立一个包含所有正常形式的new和delete的类
    class StadardNewDeleteForms {
    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) noexcept
        { ::operator delete(pMemory); }
        // placement new/delete
        static void* operator new(std::size_t size, void* ptr) noexcept
        { return ::operator new(size, ptr); }
        static void operator delete(void* pMemory, void* ptr) noexcept
        { return ::operator delete(pMemory, ptr); }
        // nothrow new/delete
        static void* operator new(std::size_t size, const std::nothrow_t& nt) noexcept
        { return ::operator new(size,nt); }
        static void operator delete(void* pMemory,const std::nothrow_t&) noexcept
        { ::operator delete(pMemory); }
    };
    
    • 使用继承机制和using声明就能以自定义方式扩充标准形式
    class A : public StandardNewDeleteForms {
    public:
        using StandardNewDeleteForms::operator new;
        using StandardNewDeleteForms::operator delete;
    
        // 添加自定义的placement new
        static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
        // 添加自定义的placement delete
        static void operator detele(void* pMemory, std::ostream& logStream) noexcept;
        ...
    };
    

    相关文章

      网友评论

        本文标题:【Effective C++(8)】定制new和delete

        本文链接:https://www.haomeiwen.com/subject/omnwnxtx.html