zoukankan      html  css  js  c++  java
  • Effective C++读书笔记~8 定制new和delete

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

    Understand the behavior of the new-handler.

    operator new:分配例程;
    operator delete:归还例程。
    new-handler:operator new无法满足客户的内存需求时所调用的函数。

    由于heap是一个可写的全局资源,多线程环境下可能会出现多线程争用情况。因此,多线程环境下,需要适当的同步控制(synchronization),加锁等手段防止并发访问(concurrent access)内存。

    operator new,operator delete适合分配单一对象。Array所有的内存需要用operator[] new分配,由operator[] delete归还。

    STL容器使用的heap内存是由容器所拥有的分配器对象(allocator object)管理,不是被new和delete直接管理。

    new-handler错误处理函数

    当operator new无法满足某一内存分配需求时,会抛出异常。以前返回null指针,现在,在抛出异常前,会先调用一个客户指定的错误处理函数,即所谓new-handler。

    客户调用set_new_handler指定这个错误处理函数,其原型:

    // Effective C++描述
    namespace std {
        typedef void (*new_handler)();
        new_handler set_new_handler(new_handler p) throw();
    }
    
    // MSVC 2017中的声明
    // handler for operator new failures
    typedef void (__CLRCALL_PURE_OR_CDECL * new_handler) ();
                  // FUNCTION AND OBJECT DECLARATIONS
    _CRTIMP2 new_handler __cdecl set_new_handler(_In_opt_ new_handler) noexcept;
    

    new_handler是一个由typedef定义的函数指针类型,没有返回值也不返回任何东西。
    set_new_handler 是获得一个new_handler类型参数p并返回一个new_handler函数。尾端throw()是一份异常说明,表明该函数不抛出任何异常。

    可以这样使用set_new_handler:

    // 当operator new无法分配足够内存时,该函数被调用
    void outOfMem()
    {
        std::cerr << "Unable to satisfy request for memory" << endl;
        std::abort();
    }
    
    // 客户端测试代码
    int main()
    {
           set_new_handler(outOfMem); //设置 new无法满足客户内存分配申请需求时,调用的错误处理函数
           int **p = new int*[1000];
           for (int i = 0; i < 1000; i++) {
                  p[i] = new int[100000000L];
           }
           cout << "return from main" << endl;
           return 0;
    }
    

    在24GB内存机器上,报错:"Unable to satisfy request for memory"。

    new-handler函数的规范

    当operator new无法满足内存申请时,会不断调用new-handler函数,直到找到足够内存。一个设计良好的new-handler函数必须做以下事情:

    • 让更多内存可被使用
      以便operator new的下一次内存分配动作可能成功。一个实现策略:程序一开始执行分配一大块内存:而后当new-handler第一次被调用,就释放以归还给程序用。

    • 安装另一个new-handler
      如果当前new-handler没有取的更多可用内存能力,但知道哪个new-handler有这个能力,可以安装另一个new-handler替换自己(调用set_new_handler即可)。

    • 卸除new-handler
      传递null指针给set_new_handler,operator new内存分配不成功时,不会抛出任何异常。

    • 抛出bad__alloc(或派生自bad_alloc)的异常
      该异常不会被operator new捕捉,因此会被传播到内存申请处。

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

    new-handler的使用

    有时,希望根据不同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
    

    C++不支持class专属new-handler,只支持global new-handler。如果需要,就需要自行实现。方法是:令每个class提供自己的set_new_handler和operator new的static函数即可。
    例如, class Widget使用operator new分配内存失败时,利用辅助类NewHandlerHolder的析构函数帮助恢复原来的new-handler。

    // RAII对象管理new_handler, 对象创建时保存原来的global new_handler到handler, 析构时还原global new_handler
    class NewHandlerHolder {
    public:
           explicit NewHandlerHolder(std::new_handler nh) : handler(nh) {}
           ~NewHandlerHolder() { std::set_new_handler(handler); }
    private:
           std::new_handler handler;
           NewHandlerHolder(const NewHandlerHolder&); // 阻止copying constructor
           NewHandlerHolder& operator=(const NewHandlerHolder&); // 阻止copying  assignment
    };
    
    // 假设要处理Widget class内存分配失败情况
    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; // currentHandler用来保存当前要传入的错误处理函数, 是在对象生成之前就有的, 所以是static
    };
    std::new_handler Widget::currentHandler = nullptr;
    
    std::new_handler Widget::set_new_handler(std::new_handler p) throw()
    {
           std::new_handler oldHandler = currentHandler;
           currentHandler = p;
           return oldHandler;
    }
    void* Widget::operator new(std::size_t size) throw(std::bad_alloc)
    {
           // 安装Widget的new-handler, 分配内存或抛出异常就恢复global new-handler
           NewHandlerHolder h(std::set_new_handler(currentHandler)); // 创建局部变量, 退出local作用域时自动析构, i.e. 用户自定义new-handler只有operator new申请分配内存期间有效
           return ::operator new(size);
    }
    
    // 客户端测试代码
    void outOfMem()
    {
           cerr << "out of memory" << endl;
           std::abort();
    }
    int main()
    {
           Widget::set_new_handler(outOfMem);
           // 会导致out of memory
           for (size_t i = 0; i < LLONG_MAX; i++) {
                  Widget *pwl = new Widget;
           }
           std::string* ps = new std::string;
           Widget::set_new_handler(0);
           Widget *pw2 = new Widget;
           cout << "return from main" << endl;
           return 0;
    }
    

    上面代码巧妙之处,就是利用RAII方式,恢复Widget用operator new申请内存发生时的错误处理函数。
    operator new中的临时对象 NewHandlerHolder h会在调用全局operator new之后,自动恢复global new-handler(不论成功与否)。

    奇特的循环模板模式 CRTP

    上面代码只适用于具体的class,然而每个要这样处理operator new异常的class都会这样写。于是,我们改写成class template形式:

    template<typename T>
    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);
    
           ~NewHandlerSupport() { std::set_new_handler(olderHandler); }
    private:
           static std::new_handler currentHandler;
           static std::new_handler olderHandler;
    };
    
    template<typename T>
    std::new_handler NewHandlerSupport<T>::currentHandler;
    
    template<typename T>
    std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()
    {
           olderHandler = currentHandler;
           currentHandler = p;
           return olderHandler;
    }
    
    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);
    }
    
    class Widget : public NewHandlerSupport<Widget> {
           // 已经拥有了NewHandlerSupport<T> 那部分成员
           // ... 和先前一样, 但不必声明
    };
    

    为什么使用template?

    我们并没有使用NewHandlerSupport template中的参数T,只是希望继承自NewHandlerSupport的每个class,都拥有不同的NewHandlerSupport复件,明确说,是其static成员currentHandler,参数T只是用来区分不同derived class。Template机制会为每个T生成一份currenHandler。

    循环模板模式 CRTP - Do It For Me

    Widget继承自一个模板化的templated base class,而后者又以Widget作为类型参数。这种技术被称为 奇特的循环模板模式(curiously recurring template pattern;CRTP)。有了NewHandlerSupport这样的template,为任何class添加一个专属new-handler成为易事。模板化的NewHandlerSupport,更像是为了Widget而存在的专属template,因此另一种理解这种模式为Do It For Me。

    1993年以前,旧的operator new在无法分配足够内存时,返回null。新operator new,则应该抛出bad_alloc异常。由于要兼容新规范以前的程序,C++提供另一种形式operator new:负责供应传统的“分配失败便返回null”,称为“nothrow”形式 -- 因为在new的使用场合用了nothrow对象(头文件):

    class Widget { ... };
    Widget* pw1 = new Widget; // 如果分配失败,抛出bad_alloc
    if (pw1 == 0) ... // 该测试一定失败,因为pw1不会为null
    Widget* pw2 = new (std::nothrow) Widget; // 如果分配失败,则返回0
    if (pw2 == 0) ... // 该测试可能成功
    

    nothrow new对异常强制保证性并不高。表达式“new(std::nothrow) Widget”发生两件事:1)nothrow 版的operator new被调用,用来分配足够内存给Widget对象。2)如果分配失败,返回null;如果分配成功,接下来调用Widget构造函数。nothrow new只能保证operator new不抛出异常,无法保证构造函数的调用不抛出异常

    小结

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

    [======]

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

    Understand when it makes sense to replace new and delete.

    为什么会需要替换编译器提供的operator new或operator delete?
    常见三个理由:

    • 用来检测运用上的错误。
      如果new所得内存,delete失败(或者没有delete),会导致内存泄漏。
      如果new所得内存,多次调用delete,会导致不确定行为。
      如果程序很可能导致数据“overruns”(写入点在分配区块尾端之后)或“underruns”(写入点在分配区块之前)。
      重写operator new可超额分配内存,提供额外空间用于签名,重写operator delete变可以检查是否有越界操作。如果有,operator delete可以log发生问题的指针。

    • 为了强化效能
      编译器提供的operator new和operator delete主要用于一般目的,但对于特定问题,定制版本修改内存的分配和回收策略,可能更有效。

    • 为了收集使用上的统计数据
      在定制new和delete前,如何得知动态内存的使用情况?比如,分配区块大小分布,FIFO or LIFO or 随机分配和归还?自定义operator new和operator delete能轻松收集到这些信息。

    定制operator new示例

    例,定制简单operator new,协助检测“overruns”和“underruns”。

    static const int signature = 0xDEADBEEF; // 签名
    typedef unsigned char Byte;
    
    // 定制operator new
    // 这段代码还有若干小错误
    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)) = signature;
           *(reinterpret_cast<int*>(static_cast<Byte*>(pMem)
                  + realSize - sizeof(int))) = signature;
           // 返回指针,指向第一个signature之后的内存位置
           return static_cast<Byte*>(pMem) + sizeof(int);
    }
    // 定制配套operator delete
    void operator delete(void* p) throw()
    {
           void *start = (static_cast<Byte*>(p) - sizeof(int));
           free(start);
    }
    
    // 客户端测试
    int main()
    {
           int* p = new int;
           *p = 1;
           cout << *p << endl;
           delete p;
           return 0;
    }
    

    该定制版operator new缺点:
    1)没有遵循条款51,未内含一个无穷循环并在其中尝试分配内存,调用new-handler。也没有处理0byte申请。
    2)没有考虑对齐问题。
    3)还有可移植性,线程安全等问题。

    对齐问题

    这里,我们主要探讨对齐(alignment):有些计算机体系结构要求特定类型必须放在特定内存地址上。如指针地址必须是4倍数(four-byte aligned)或double地址必须是8倍数(eight-byte aligned)。如果没有遵循这个约束条件,可能导致运行期硬件异常。而有些并没有这么严格要求,对于如double,只要是byte对齐即可,但如果是8byte对齐,则访问速度会快很多。
    malloc返回的指针是安全的,但我们在程序里面对其偏移了一个int大小的位置,而int是固定4byte,如果我们申请8byte的double,就可能导致对齐问题。

    何时替换new/delete

    何时在“全局性的”或者“class专属的”基础上,合理替换缺省的new和delete?

    • 为了检测运用错误;
    • 为了收集动态分配内存之使用统计信息;
    • 为了增加分配和归还的速度
      针对特定类型定制new和delete的速度,往往快于编译器提供的缺省new和delete
    • 为了降低缺省内存管理器带来的空间额外开销
      泛用型内存管理器往往比定制性慢,还使用更多内存,因为它们常常在每个分配区块上招引某些额外开销。针对小型对象开发的分配器(如Boost的Pool程序库)本质上消除了这样的额外开销。
    • 为了弥补缺省分配器中的非最佳对齐
      将不保证对齐的new替换为对齐的版本,可能导致程序效率大幅提升。
    • 为了将相关对象成簇集中
      如果指定某个数据结构往往一起使用,而你有希望处理这些数据时,将“内存页错误”(page fault)的频率降至最低,那么为此数据结构创建另一个heap就有意义,这样它们就可以被成簇集中在尽可能少的内存页(page)上。见条款52。
    • 为了获得非传统的行为
      有时希望operator new和delete做编译器提供的缺省版本没做的事情,如将C API封装成C++ API,将归还内存覆盖为0。

    小结

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

    [======]

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

    Adhere to conversion when writing new and delete.

    条款50解释何时需要编写自己的operator new和operator delete。但如果定制自己的new和delete,应当遵守什么规则呢?

    自定义new需要遵循的规则

    1)内存不足时,必调用new-handler函数,必须有对付零内存需求的准备,需避免不慎掩盖正常形式的new(接口要求)。

    2)operator new的返回值十分单纯。如果有能力提供客户申请的内存,就返回一个指针指向那块内存;如果没有能力,就遵循条款49,抛出bad_alloc异常。不过,operator new实际上不止一次尝试分配内存,并在每次失败后调用new-handling函数。这里假设new-handling函数也许能做某些动作,将某些内存释放。只有当指向new-handling函数的指针是null,operator new才会抛出异常。

    3)处理零内存申请:即使客户要求0byte,operator new也得返回一个合法指针。这个看似诡异的行为,是为了简化语言的其他部分。

    例,适用于单线程

    void *operator new(std::size_t size) throw(std::bad_alloc)
    {
           using namespace std;
           if (size == 0) { // 处理0byte申请
                  size = 1; // 将其视为1byte申请
           }
           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();
           }
    }
    

    当上述operator new作为一个class的专属operator new时,存在一个问题:class作为Base可能会被继承,而针对class Base设计的operator new可能只刚好只为sizeof(Base)大小对象而设计,对于继承自Base的Derived,其对象大小很可能大于Base对象大小,这样就会导致“内存申请量错误”的问题。

    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
    

    客户端调用operator new时,传入的是Derived对象大小sizeof(Derived),而Base::operator new中考虑申请对象大小是sizeof(Base),该参数由编译器自动生成并传入,Base对象大小通常大于Derived对象大小,这样就产生了问题。

    解决办法:

    void* Base::operator new(std::size_t size) throw(std::bad_alloc)
    {
         if (size != sizeof(Base)) //如果大小错误, 就令标准的operator new处理
              return ::operator new(size);
         ... // 否则,在这里处理
    }
    

    撰写operator new时,不能保证要申请元素大小一定是当前class对象大小。传递给operator newp[]的大小,也不一定是每个元素大小 * 元素个数,因为可能还包含其他信息,如元素个数。

    自定义delete需要遵循的规则

    撰写operator delete时,需要记住:C++保证“删除null指针永远安全”。
    1)non-member operator delete伪码(pseudocode)

    void operator delete(void *rawMemory) throw()
    {
         if (rawMemory == 0) return; // 如果被删除的是null指针,那就什么都不做
         现在,归还rawMemory所指内存;
    }
    

    2)member 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();
           ...
    };
    
    void Base::operator delete(void* rawMemory, std::size_t size) throw()
    {
           if (rawMemory == 0) return; // 如果被删除的是null指针,那就什么都不做
           if (size != sizeof(Base)) {
                  ::operator delete(rawMemory);
                  return;
           }
           现在,归还rawMemory所指内存;
           return;
    }
    

    需要注意的是:如果即将被删除的对象派生自某个base class,而后者欠缺virtual析构函数,那么C++传给operator delete的size_t数值可能不正确。

    小结

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

    [======]

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

    Write placement delete if you write placement new.

    什么是placement new,placement delete?

    默认情况下,我们使用operator new给对象分配存储空间并调用其构造函数

    void* operator new(std::size_t) throw(std::bad_alloc); // 缺省new
    
    // 客户端 对应调用方式
    Widget* pw = new Widget;
    // delete 对应正常的operator delete
    void operator delete(void* )
    

    客户端调用了2个函数:1)operator new分配内存,2)Widget的default构造函数。
    假设第一个函数(operator new分配内存)调用成功,第二个函数(default构造函数)调用失败。那么第一步申请分配的内存就必须释放并恢复到旧的状态,否则会造成内存泄漏(memory leak)。但此时,客户还没用能力归还内存,因为Widget构造函数抛出了异常,也就是说,pw指针并没有被赋值,客户也就没有指向这块内存的指针。因此,归还内存就成了运行期系统的责任。

    典型的正常形式operator new和delete:

    void* operator new(std::size_t) throw(std::bad_alloc); // global和class作用域正常签名式
    
    void operator delete(void* rawMem) throw(); // global作用域的正常签名式
    void operator delete(void* rawMem, std::size_t size) throw(); // class作用域的典型签名式
    

    placement new与placement delete

    如果operator new接受的参数除了默认size_t外,还有其他参数,那么就称该operator new为placement new。
    其中,有个特别的placement new版本,接受一个指针指向对象被构造之处,也就是说,pMem指向一块已经分配得到的内存,调用该placement new可以在指定内存(参数pMem指向的)上创建对象。大多数情况下,人们所指的placement new就是特定版本的operator new(唯一额外参数是void*),少数情况指包含任意额外参数。

    与placement new相对地,也存在placement delete。

    #include <new>
    
    void* operator new(std::size_t, void* pMem) throw(std::bad_alloc); // placement new
    void operator delete(void* pMem, std::size_t size) throw(); // placement delete
    
    // Widget class 声明式
    class Widget {
    public:
        static void* operator new(std::size_t, std::ostream& logStream) throw(std::bad_alloc); // placement new
        static void operator delete(void* pMem, std::size_t size) throw();  // 正常class的专属delete
        static void operator delete(void *pMem) throw();
        static void operator delete(void* pMem, std::ostream& logStream) throw(); // 与placement new配套的placement delete
    };
    

    如果Widget构造函数抛出异常,调用哪个版本operator delete?

    Widget构造函数抛出了异常,运行期系统有责任取消operator new的分配并恢复到旧状态,不过,运行期系统无法知道真正被调用的那个operator new如何运作(如在构造函数中又做了哪些事情),因此它无法取消分配、恢复旧状态。取而代之的是,运行期系统寻找“参数个数和类型都与operator new相同的某个operator delete”。如果找到,调用之;如果没有,就不会有任何operator delete被调用。

    因此,Widget class的operator new抛出异常时,对应版本placement delete会被自动调用,让Widget有机会确保不泄漏任何内存。

    // 客户端
    Widget* pw = new (std::cerr) Widget; // 调用Widget::operator new(sizeof(Widget), cerr);
    // 出现异常时,运行期系统选择调用operator new配套的operator delete(2者额外参数相同)
    void Widget::operator delete(void*, std::ostream&) throw();
    
    // 正常的释放内存操作
    delete pw;
    // delete pw调用Widget::operator delete(void*, size_t)
    

    注意:对一个指针施行delete绝不会调用placement delete。

    placement与名称遮掩问题

    1)class专属placement new会遮掩正常的global new

    class B
    {
    public:
           ...
           // 该placement new会遮掩正常形式的global new
           static void* operator new(std::size_t size, std::ostream& logStream)  throw(std::bad_alloc);
    }
    
    B* pb = new B; // 错误:正常形式operator new会被遮掩
    B* pb = new (std::cerr) B; // OK:调用B::operator new(size_t, ostream&)
    

    2)derived class的专属operator new会这样global new和继承而来的operator new

    class C : public B {
    public:
           ...
           // 该placement new会遮掩正常形式的global new和从B继承的placement new
           static void* operator new(std::size_t size) throw(std::bad_alloc);
    };
    
    C* pc = new(std::clog) C; // 错误:B的placement new被遮掩
    C* pc = new C; // OK:调用C::operator new(size_t)
    

    如何解决placement名称遮掩问题?

    除非确定就是想遮掩global new和Base class的placement版本,否则,可以使用using,或者在当前class明确定义专属placement new和placement delete。还有一个简便办法,就是建立一个base class,内含所有normal new和delete:

    /* 标准形式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* pMem) throw()
           {
                  ::operator delete(pMem);
           }
           // placement new/delete
           static void* operator new(std::size_t size, void* ptr) throw()
           {
                  return ::operator new(size, ptr);
           }
           static void operator delete(void* pMem, void* ptr) throw()
           {
                  return ::operator delete(pMem, 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* pMem, const std::nothrow_t) throw()
           {
                  ::operator delete(pMem);
           }
    };
    class MyWidget : public StandardNewDeleteForms { // 继承标准形式
    public:
           // 避免基类的new/delete名称被遮掩
           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 delete(void* pMem, std::ostream& logStream) throw();
           // ...
    };
    

    小结

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

    [======]

  • 相关阅读:
    网站开发
    mysql字符编码问题
    [ztg@localhost lineage-17.1---dipper]$ brunch dipper --- error
    [ztg@localhost lineage-17.1---dipper]$ brunch dipper --- error
    [ztg@localhost lineage-17.1---dipper]$ brunch dipper --- error
    [ztg@localhost lineage-17.1---dipper]$ brunch dipper
    RFC 8684---TCP Extensions for Multipath Operation with Multiple Addresses
    Apple uses Multipath TCP
    [tip:,x86/urgent] x86: Fix early boot crash on gcc-10, third try
    Fedora version history --- kernel version
  • 原文地址:https://www.cnblogs.com/fortunely/p/15682123.html
Copyright © 2011-2022 走看看