49:了解new-handler的行为

  • new申请内存失败会抛出bad alloc的异常,此前会调用一个错误处理函数,此函数由std::set_new_handler()指定
    • set::set_new_handler()
      • 接受一个错误处理函数,返回旧的错误处理函数
      • throw表示可能抛出的异常类型,参数为空表示不抛出任何异常
    1
    2
    
    typedef void (*new_handler)(); // 无形参,返回值为void的函数指针
    new_handler set_new_handler(new_handler f) throw();
    
    • 当new申请不到足够的内存时,会不断调用错误处理函数f,因此错误处理函数应该进行下面的处理之一:
      • 提供更多可用的内存
      • set_new_handler中传入一个新的错误处理函数
      • set_new_handler函数中传入一个空指针,因此内存分配失败时不进行处理,直接抛出异常
      • 抛出bad_alloc的异常
      • 不返回:调用std::abortstd::exit
        • abort会设置程序非正常退出
        • exit会设置程序正常退出,当存在未处理异常时,会调用terminate,内部回调set::set_terminate设置的回调函数,默认会调用abort
  • 类型相关错误处理
    • 为不同的类分配对象时,使用不同的错误处理函数
    • 重载set_new_handleroperator new,重载为static成员
    • 可以写成模板
      • 此处的模板参数T并没有真正被当成类型使用,而仅仅是用来区分不同的派生类,使得模板机制为每个派生类具现化出一份对应的currentHandler
      • 这个做法用到了所谓的 CRTP(curious recurring template pattern,奇异递归模板模式),也常被用于静态多态
     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
    
    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);
        ~NewHandlerSupport() {std::set_new_handler(currentHandler);}
    private:
        NewHandlerSupport(const NewHandlerSupport&); // 阻止拷贝构造
        NewHandlerSupport& operator=(const NewHandlerSupport&); // 阻止拷贝复制
        static std::new_handler currentHandler;
    };
    
    template <typename T>
    std::new_handler NewHandlerSupport<T>::currentHandler = nullptr;
    
    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) {
        NewHandlerSupport h(std::set_new_handler(currentHandler)); 
        // 返回的函数指针初始化了一个对象h,在退出函数时,执行h的析构过程,即将原来的handle恢复
        return ::operator new(size);
    }
    
    // 使用
    class Widget: public NewHandlerSupport<Widget>{ ... };
    
  • new分配失败后,可能不会抛出异常,而是返回null,这种称为nothrow new
    • 例子:new (std::nothrow) int[10];
    • nothrow new只能保证内存分配错误时不抛出异常,无法保证对象的构造函数不抛出异常

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

  • 为什么需要自定义operator new
    • 检测使用错误:检测多次delete,检测越界
    • 提高效率:手动维护更适合应用场景的存储策略
      • 比如针对特定类型,增加分配和归还的速度
      • 比如将相关对象集成到簇中(即尽量分配到一个内存页上)
    • 收集使用的统计信息
    • 其他原因:比如安全性(将申请到的内存初始化为0),字节对齐等

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

  • operator new需要无限循环地获取资源,如果没能获取则调用"new handler",不存在"new handler"时应该抛出异常;

  • operator new应该处理size == 0的情况;

  • operator delete应该兼容空指针;

  • operator new/delete作为成员函数应该处理size > sizeof(Base)的情况(因为继承的存在)。

  • 外部(非成员函数的)operator new

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    void* operator new(std::size_t size) throw(std::bad_alloc){
        if(size == 0) size = 1; // size=0时,返回合法的指针就说明成功分配了内存
        while(true){
            void *p = malloc(size);
            if(p) return p;
    
            // 申请失败,获得new handler,多线程需要加锁
            new_handler h = set_new_handler(0);
            set_new_handler(h);
            // auto h = get_new_handler(); // C++11方式
    
            if(h) (*h)(); // new-handler应该实现item49中描述的五种行为之一,否则,此处陷入死循环
            else throw bad_alloc();
        }
    }
    
  • 成员operator new

    • 如果operator new是针对基类的,也就是说operator new是针对大小为sizeof(Base)的内存进行优化的
    • 一般来说派生类不应该使用基类的operator new,因为派生类对象大小与基类对象大小一般不同
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    class Base{
        public:
            static void* operator new(std::size_t size);
    };
    
    void* Base::operator new(std::size_t size) {
        if(size != sizeof(Base)) // sizeof(Base)永远不会为0(至少为1),因为空对象至少会插入一个char
            return ::operator new(size); // 使用全局的operator new
        ...
    }
    
    class Derived: public Base { ... };
    
    • operator new[]operator new有相同的参数和返回值,只需要分配一块原始内存
  • delete

    • delete
    • 惯例:delete一个空指针是安全的
  • 外部operator delete

1
2
3
4
void operator delete(void* rawMemory) noexcept {
    if (rawMemory == 0) return;
    // 释放 rawMemory 所指的内存
}
  • 成员operator delete
    • 如果基类的析构函数不是虚函数,则size大小为静态类型的大小;
      • 比如Base* p = new Derived; delete p;中,很可能派生类大小大于基类大小,因此存在内存泄露
    • 否则size为动态类型的大小
    1
    2
    3
    4
    5
    6
    7
    8
    
    void Base::operator delete(void* rawMemory, std::size_t size) noexcept {
        if (rawMemory == 0) return;
        if (size != sizeof(Base)) {
            ::operator delete(rawMemory);    // 转交给标准的 operator delete 进行处理
            return;
        }
        // 释放 rawMemory 所指的内存
    }
    

52: 写了placement new也要写`palcement delete

  • placement new:广义上指拥有额外参数的operator new
  • 背景:
    • 在使用new创建对象时,往往进行了两个函数的调用:一个是operator new,进行内存分配;一个是对象的构造函数
    • 如果构造失败,此时对象没有被创建,对象无法被析构,且此时还没有拿到分配内存的地址
    • 因此需要运行时系统进行delete,运行时系统需要知道使用的是哪一种operator new,因此调用对应的operator delete
      • 如果没有对应的operator delete函数,则运行时系统什么都不做,导致内存泄露
  • 当定义了placement new时,同时也要定义对应的placement delete
    • 用户直接调用delete时,运行时系统不会将其解释为placement delete,因此还需要定义一个正常的delete
    1
    2
    3
    4
    5
    6
    
    class Widget{
    public:
        static void* operator new(std::size_t size, std::ostream& log) throw(std::bad_alloc);
        static void operator delete(void *mem, std::ostream& log);
        static void operator delete(void *mem) throw();
    };
    
    • 名称隐藏:类中的名称会隐藏类外的名称,子类的名称会隐藏父类的名称
      • 三种全局new
        1
        2
        3
        
        void* operator(std::size_t) throw(std::bad_alloc);           // normal new
        void* operator(std::size_t, void*) noexcept;                 // placement new
        void* operator(std::size_t, const std::nothrow_t&) noexcept; // nothrow new
        
  • 最佳实践:
    • 将全局版本new在一个基类中进行重载,内部调用全局new进行实现
    • 然后在自定义类Widget中,public继承,并使用using声明使得三种new和三种delete对Widget可见,因此同时Widget可以定义自己版本的placement new
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    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); }
    };
    
    class Widget: public StandardNewDeleteForms {           
    public:
       using StandardNewDeleteForms::operator new;         
       using StandardNewDeleteForms::operator delete;     
       static void* operator new(std::size_t size, std::ostream& log) throw(std::bad_alloc);   // 自定义 placement new
       static void operator delete(void *pMemory, std::ostream& logStream) throw();            // 对应的 placement delete
    };