0%

8 定制new和delete

以下operator new和operator delete的规则同样适用于operator new[]和operator
delete[]

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

  1. 当operator new无法满足某一内存分配需求时,就会抛出异常。在它抛出异常前,会先调用一个用户指定的错误处理函数,一个所谓的new-handler,为了指定这个函数,用户需要调用set_new_handler,那是声明于的一个标准程序库函数:
    1
    2
    3
    4
    5
    6
    7
    namespace std {
    typedef void (*new_handler)();
    new_handler set_new_handler(new_handler p) throw();
    }

    // 使用时
    std::set_new_handler(outOfMem);
    一个设计良好的new-handler函数必须做以下事情:
    • 让更多内存可被使用。让operator new的下一次内存分配动作可能成功,就让程序一开始执行分配一大块内存,当new-handler第一次被调用,将它们释放给程序使用。
    • 安装另一个new-handler。如果目前这个new-handler无法取得个更多可用内存,或许其他new-handler有。那么这个new-handler则set_new_handler来替换自己。下次当operator new调用new-handler时,调用的将是最新安装的那个。
    • 卸除new-handler。一旦没有安装任何new-handler,operator new会在内存分配不成功时抛出异常,这时候应该将null指针给set_new_handler。
    • 抛出bad_alloc(或派生自bad_alloc)的异常。这样异常不会被operator new捕捉,会被传播到内存索求处。
    • 不返回。调用abort或exit。
  2. 使用class内自定义的operator new的set_new_handler可以替换global new-handler。
    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
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    class NewHandlerHolder
    {
    public:
    // 取得当前的的new-handler
    explicit NewHandlerHolder(std::new_handler nh) : handler(nh) {}
    // 释放它
    ~NewHandlerHolder() {
    std::set_new_handler(handler);
    }
    private:
    std::new_handler handler;
    // 禁用copying函数
    NewHandlerHolder(const NewHandlerHolder&);
    NewHandlerHolder& oeprator=(const NewHandlerHolder&);
    }

    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 = 0;
    };
    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
    NewHandlerHolder h(std::set_new_handler(currentHandler));
    // 恢复global new-handler
    return ::operator new(size);
    }

    // 函数声明,将在Widget对象分配失败时被调用
    void outOfMem();
    // 设定outOfMem为Widget的new-hander函数
    Widget::set_new_handler(outOfMem);
    // 分配失败调用outOfMem
    Widget *pw1 = new Widget;
    // 分配失败调用global new-handler函数
    std::string *ps = new std::string;
    // 设定Widget专属的new-handing函数为null
    Widget::set_new_handler(0);
    // 如果分配失败,立刻抛出异常
    Widget *pw2 = new Widget;
  3. 现在设计一个可以被任何有需要的class使用的template:
    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
    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);
    ...
    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)
    {
    NewHandlerHolder h(std::set_new_handler(currentHandler));
    return ::operator new(size);
    }
    template<typename T>
    std::new_handler NewHandlerSupport<T>::currenHandler = 0;

    // 对于显式声明nothrow,只能保证operator new不抛出异常,构造函数可能又new一些内存,这就无法强迫了
    // Widget *pw = new (std::nothrow)Widget;
    怪异的循环模板模式curiously recurring template pattern;CRTP)。

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

  1. 为什么要替换编译期提供的operator new或operator delete呢?由三个理由:
    • 用来检测运用上的错误。如果new出的内存delete失败,会导致内存泄露memory leaks)。new上多次delete会导致不确定行为。如果我们自行定义operator news,超额分配内存,在额外内存上放置byte patterns(签名,signatures)。这样delete时就可以发现分配区中有哪些点发生了overrun(写入点在分配区尾端之后)或underruns(写入点在分配区起点之前)。这时候就可以log那个事实了。
    • 为了强化效能。对于一些需求,包括大块内存、小块内存、大小混合型内存。它们接纳各种分配形态,从程序存活期间的少量区块动态分配,到大量短命对象的持续分配和归还。必须考虑破碎问题fragmentation)。这会导致程序有总量足够但分散为很多小区块的自由内存,却无法分配大区块内存。
    • 为了收集使用上的统计数据。制定news和delete之前,先收集软件如何使用动态内存。分配区块的大小分配?寿命分布?FIFO次序还是LIFO次序或随机分配?不同执行阶段有不同分配/归还形态?任何时刻使用最大动态分配量是多少?
  2. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    static const int signature = 0xDEADBEEF;
    typedef unsigned char Byte;
    // 还有若干错误
    void* operator new(std::size_t size) throw(std::bad_alloc)
    {
    using namespace std;
    // 增加大小,使之能够塞入两个signatures
    size_t realSize = size + 2 * sizeof(int);
    void* pMem = malloc(realSize);
    if (!pMem) throw bad_alloc();
    // 将signature写入内存的前段落和后段落
    *(static_cast<int*>(pMem)) = signature;
    *(reinterrpret_cast<int*>(static_cast<Byte*>(pMem) + realSize - sizeof(int))) = signature;
    // 返回指针,指向第一个signature之后的内存位置
    return static_cast<Byte*>(pMem) + sizeof(int);
    }
    计算机体系结构computer architectures)要求特定的类型必须放在特定内存地址上,例如指针的地址必须是4倍数for-byte aligned)或doubles地址是8倍数eight-byte aligned)。齐位alignment)意义重大,C++要求operator news返回的指针都有适当的内存对齐。malloc就是在这样做的,所以operator new返回一个malloc指针是安全的。然而上面代码中返回一个malloc且偏移一个int大小的指针,没人保证它的安全!
  3. 摘要:
    • 为了检测运用错误。
    • 为了收集动态分配内存之使用统计信息
    • 为了增加分配和归还速度
    • 为了降低缺省内存管理器带来的空间额外开销。
    • 为了弥补缺省分配器中的非最佳齐位(suboptimal alignment)。
    • 为了将相关对象成簇集中。
    • 为了获得非传统的行为。

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

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

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

  1. 1
    2
    3
    4
    5
    6
    // 正常的operator new
    void* operator new(std::size_t) throw(std::bad_alloc);
    // global作用域中的正常签名式
    void operator delete(void *rawMemory) throw();
    // class作用域中典型的签名式
    void operator delete(void *rawMemory, std::size_t size) throw();
    如果operator new接受的参数除了一定会有个size_t之外还有其他,这就是所谓的placement new。众多placement new版本中特别有用的一个是”接受一个指针指向对象该被构造之处“:
    1
    void* operator new(std::size_t, void *pMemory) throw();  // placement new
    这个版本在C++ STL中,只要#include <new>就可以用它。
    举个例子,现在写一个class专属的operator new,要求接受一个ostream,用来log相关分配信息,同时写一个正常形式的class专属operator delete:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Widget
    {
    public:
    ...
    // 非正常形式的new
    static void* operator new(std::size size, std::ostream &logStream) throw(std::bad_alloc);
    // 正常class专属delete
    static void operator delete(void *pMemory, std::size_t size) throw();
    ...
    }
    // 这里有微妙的内存泄漏
    // 调用operator new并传递cerr为其ostream实参,这个动作会在构造函数抛出异常时泄露内存
    Widget *pw = new (std::cerr) Widget;
    对于类似的new placement版本,operator delete如果接受额外参数,称为placement deletes。带有额外参数的operator new应该调用带相同额外参数的对应版本operator delete。
  2. 为了消弭代码中的内存泄露,Widget有必要声明一个placement delete,对应于有志记功能(logging)的placement new:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    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();
    ...
    }
    对于placement delete自动调用期间构造函数可能会抛出异常,代码会自动调用placement delete;如果没有抛出异常,则只会调用普通operator delete。
    顺带一提,成员函数的名称会掩盖外围作用域中的相同名称,假设有一个base class,其中声明唯一的placement operator new,用户会无法使用正常的new:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Base
    {
    public:
    ...
    // 会掩盖正常版本
    static void* operator new(std::size_t size, std::ostream &logStream) throw(std::bad_alloc);
    ...
    };
    class Derived : public Base
    {
    public:
    ...
    static void* operator new(std::size_t size) throw(std::bad_alloc);
    ...
    };
    Base *pb = new Base; // error
    Base *pb = new (std::cerr) Base; // ok
    Derived *pb = new (std::clog) Derived;// error
    Derived *pb = new Derived; // ok
    需要记住的时,C++在global作用域中提供了以下operator new:
    1
    2
    3
    4
    5
    6
    // normal new
    void* operator new(std::size_t) throw(std::bad_alloc);
    // placement new
    void* operator new(std::size_t, void *) throw();
    // nothrow new
    void* operator new(std::size_t, const std::nothrow_t &) throw(); // 49
    除非你要阻止class用户使用这些形式,否则确保它们在你生成的定制型operator new之外还可用。对于每个operator new也有对应的operator delete。如果希望这些函数有平常的行为,令你class专属版本调用global版本:
    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
    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 ::opeator 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); }
    };

    // 供想自定义扩充标准形式的用户,利用继承机制或using声明式(33)取得标准形式
    class Widget : public StandardNewDeleteForms
    {
    public:
    using StandardNewDeleteForms::operator new;
    using StandardNewDeleteForms::operator delete;
    static void* operator new(std::size_t size, std::ostream &logStream) throw(std::bad_alloc);
    static void oeprator delete(void *pMemory, std::ostream &logStream) throw();
    ...
    };