Effective C++ 08 定制 new 和 delete

8. 定制 new 和 delete

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

operator new 无法满足某一内存分配需求时,它会抛出异常。当 operator new 抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,一个所谓的 new-handler。为了指定这个“用以处理内存不足”的函数,客户必须调用 set_new_handler,那是声明于 <new> 的标准程序库函数:

namespace std {
    typedef void (*new_handler)();
    new_handler set_new_handler(new_handler p) throw();
}

new_handler 是个 typedef,定义出一个指针指向函数,该函数没有参数也不返回任何东西。set_new_handler 则是“获得一个 new_handler 并返回一个 new_handler ”的函数。set_new_handler 声明式尾端的 “throw()” 是一份异常明细,表示该函数不抛出任何异常 —— 虽然事实更有趣些,详见条款 29。

set_new_handler 的参数是个指针,指向 operator new 无法分配足够内存时该被调用的函数。其返回值也是个指针,指向 set_new_handler 被调用前正在执行(但马上就要被替换)的那个 new-handler 函数。你可以这样使用 set_new_handler:

// 以下是当 operator new 无法分配足够内存时,该被调用的函数
void outOfMem() {
    std::cerr << "Unable to statisfy request for memeory\n";
    std::abort();
}
 
int main() {
    std::set_new_handler(outOfMem);
    int* pBigDataArray = new int[100000000L];
    ...
}

就本例而言,如果 operator new 无法为 100000000 个整数分配足够空间,outOfMem 会被调用,于是程序在发出一个信息之后夭折(abort)。

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

  • 让更多内存可被使用。这便造成 operator new 内的下一次内存分配动作可能成功。实现此策略的一个做法是,程序一开始执行就分配一大块内存,而后当 new-handler 第一次被调用,将它们释还给程序使用。
  • 安装另一个 new-handler。如果目前这个 new-handler 无法取得更多可用内存,或许它知道另外哪个 new-handler 有此能力。
  • 卸除 new-handler。也就是将 null 指针传给 set_new_handler。一旦没有安装任何 new-handler,operator new 会在内存分配不成功时抛出异常。
  • 抛出 bad_alloc(或派生自 bad_alloc)的异常。这样的异常不会被 operator new 捕捉,因此会被传播到内存索求处。
  • 不返回,通常调用 abort 或 exit。

假设你打算处理某个类内存分配失败的情况,其看起来像这样:

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;
};

Widget 内的 set_new_handler 函数会将它获得的指针存储起来,然后返回先前存储的指针,这也正是标准版 set_new_handler 的作为:

std::new_handler Widget::set_new_handler(std::new_handler p) throw() {
    std::new_handler oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
}

最后,Widget 的 operator new 做以下事情:

  1. 调用标准 set_new_handler,告知 Widget 的错误处理函数。这会将 Widget的 new-handler 安装为 global new-handler。
  2. 调用 global operator new,执行实际之内存分配。如果分配失败,global operator new 会调用 Widget 的 new-handler,因为那个函数才刚被安装为global new-handler。如果 global operator new 最终无法分配足够内存,会抛出一个 bad_alloc 异常。
  3. 如果 global operator new 能够分配足够一个 Widget 对象所用的内存,Widget 的 operator new 会返回一个指针,指向分配所得。Widget 析构函数会管理 global new-handler,它会自动将 Widget’s operator new 被调用前的那个 global new-handler 恢复回来。

下面以 C++ 代码再阐述一次。将从资源处理类开始,那里面只有基础性 RAII 操作,在构造过程中获得一笔资源,并在析构过程中释还(见条款13):

class NewHandlerHolder {
public:
    explicit NewHandlerHolder(std::new_handler nh)  // 取得目前的 new-handler
    : handler(nh) {}
    ~NewHandlerHolder()  // 释放它
    { std::set_new_handler(handler); }
private:
    std::new_handler handler;  // 记录下来
    NewHandlerHolder(const NewHandlerHolder&);  // 阻止 copying(见条款 14)
    NewHandlerHolder& operator=(const NewHandlerHolder&);
};

这就使得 Widget’s operator new 的实现相当简单:

void* Widget::operator new(std::size_t size) throw(std::bad_alloc) {
    NewHandlerHolder h(std::set_new_handler(currentHandler));  // 安装 Widget 的 new-handler,分配内存或抛出异常
    return ::operator new(size);  // 恢复 global new-handler.
}

Widget 的客户应该类似这样使用其 new-handling:

void outOfMem();  // 函数声明。此函数在 Widget 对象分配失败时被调用
Widget::set_new_handler(outOfMem);  // 设定 outOfMem 为 Widget 的 new-handling 函数
Widget* pw1 = new Widget;  // 如果内存分配失败调用 outOfMem
std::string* ps = new std::string;  // 如果内存分配失败调用 global new-handling 函数
Widget::set_new_handler(0);  // 设定 Widget 专属的 new-handling 函数为 null
Widget* pw2 = new Widget;  // 如果内存分配失败,立即抛出异常(class Widget 没有专属的 new-handling 函数)
norhrow new

老版本 C++ 要求 operator new 必须在无法分配足够内存是返回 null(新一代要求抛出 bad_alloc 异常),但是很多 C++ 程序实在编译器开始支持新规范之前写出来的。C++ 标准委员会不想抛弃那些“侦测 null”的足球,于是提供另一种形式的 operator new,负责供应传统的“分配失败便返回 null”行为。这个形式被称为 “nothrow” 形式:

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

请记住:

  • set_new_handler 允许客户指定一个函数,在内存分配无法获得满足时被调用。
  • nothrow new 是一个颇为局限的工具,因为它只适用于内存分配;后续的构造函数调用还是可能抛出异常。

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

替换编译器提供的 operator new 或 operator delete 有下面是三个最常见的理由:

  1. 用来检测运用上的错误。如果 delete “new 所得内存” 时失败,会导致内存泄漏。如果在 “new 所得内存”身上多次 delete 则会导致不确定行为。如果我们自定义一个 operator new,便可超额分配内存,用多出的空间放置特定的 byte pattern(即签名)。operator delete 便得以检查上述签名是否原封不动。

  2. 为了强化效能。编译器所带的 operator new 和 operator delete 主要用于一般目的,它们不但可被长时间执行的程序(例如网页服务器,web servers)接受,也可被执行时间少于一秒的程序接受。它们必须处理一系列需求,包括大块内存、小块内存、大小混合型内存。它们必须接纳各种分配形态,范围从程序存活期间的少量区块动态分配,到大数量短命对象的持续分配和归还。它们必须考虑破碎问题,这最终会导致程序无法满足大区块内存要求,即使彼时有总量足够但分散为许多小区块的自由内存。

    现实存在这些对内存管理器的要求,因此编译器所带的 operator new 和 operator delete 采取中庸之道也就不令人惊讶了。对某些(虽然不是所有)应用程序而言,将旧有的(编译器自带的)new 和 delete 替换为定制版本,是获得重大效能提升的办法之一。

  3. 为了收集使用上的统计数据。收集你的软件如何使用其动态内存。例如,分配区块的大小分布如何?它们倾向于以 FIFO 次序或 LIFO 次序或随机次序来分配和归还?等等。自行定义 operator new 和 operator delete 使我们得以轻松收集到这些信息。

观念上,写一个定制型 operator new 十分简单。举个例子,下面是个快速发展得出的初阶段 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);  // 增加大小,使能够塞入两个 signatures
 
    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 new 的缺点主要在于它疏忽了身为这个特殊函数所应该具备的“坚持 C++ 规矩”的态度。举个例子,条款51。说所有 operator new 都应该内含一个循环,反复调用某个 new-handling 函数,这里却没有。

许多计算机体系结构要求特定的类型必须放在特定的内存地址上。例如,它可能会要求指针的地址必须是 4 的倍数或 double 的地址必须是 8 的倍数。如果没有奉行这个约束条件,可能导致运行期硬件异常。有些体系结构比较慈悲,没有那么霹雳,而是宣称如果齐位条件获得满足,便提供较佳效率。例如 Intel x86 体系结构上的 double 可被对齐于任何 byte 边界,但如果它是 8-byte 齐位,其访问速度会快许多。

齐位意义重大,因为** C++ 要求所有 operator new 返回的指针都有适当的对齐**。malloc 就是在这样的要求下工作,所以令 operator new 返回一个得自 malloc 的指针是安全的。然而上述 operator new 中,并未返回一个得自 malloc 的指针,而是返回一个得自 malloc 且偏移一个 int 大小的指针。没人能够保证它的安全!

本条款的主题是,了解何时在“全局性的”或 “class 专属的”基础上合理替换缺省的 new 和 delete。挖掘更多细节之前,让我先对答案做一些摘要。

  • 为了检测运用错误(如前所述)。

  • 为了收集动态内存分配之使用统计信息(如前所述)。

  • 为了增加分配和归还的速度。泛用型分配器往往(虽然并不总是)比定制型分配器慢,特别是当定制型分配器专门针对某特定类型之对象而设计时。

  • 为了降低缺省内存管理器带来的空间额外开销。泛用型内存管理器往往(虽然并非总是)不只比定制型慢,它们往往还使用更多内存,那是因为它们常常在每一个分配区块身上招引某些额外开销。针对小型对象而开发的分配器本质上消除了这样的额外开销。

  • 为了弥补缺省分配器中的非最佳齐位。一如先前所说,在 x86 体系结构上 double 的访问最是快速——如果它们都是 8-byte 齐位。但是编译器自带的 operator new 并不保证对动态分配而得的 double 采取 8-byte 齐位。这种情况下,将缺省的 operator new 替换为一个 8-byte 齐位保证版,可导致程序效率大幅提升。

  • 为了将相关对象成簇集中。如果你知道特定之某个数据结构往往被一起使用,而你又希望在处理这些数据时将“内存页错误”的频率降至最低,那么为此数据结构创建另一个 heap 就有意义,这么一来它们就可以被成簇集中在尽可能少的内存页上。 new 和 delete 的 “placement 版本”(见条款52)有可能完成这样的集簇行为。

  • 为了获得非传统的行为。有时候你会希望 operator new 和 delete 做编译器附带版没做的某些事情。例如,你可能会希望分配和归还共享内存内的区块,但唯一能够管理该内存的只有 C API 函数,那么写下一个定制版 new 和 delete,你便得以为 C API 穿上一件 C++ 外套。你也可以写一个自定的 operator delete,在其中将所有归还的内存内容覆盖为 0,籍此增加应用程序的数据安全性。

请记住:

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

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

operator new

实现一致性 operator new 必须要返回真确的值,内存不足时必得调用 new-handling函数(见条款 49),必须有对付零内存需求的准备,还需避免不慎掩盖正常形式的 new 。

如果 operator new 有能力供应客户申请的内存,就返回一个指针指向那块内存。如果没有那个能力,就抛出一个 bad_alloc 异常。实际上, operator new 不止一次尝试分配内存,并在每次失败后调用 new-handling 函数,只有当 new-hangling 函数的指针是 null 时,operator new 才会抛出异常。

C++ 规定,即使客户要求 0 bytes,operator new 也得返回一个合法指针。这种行为是为了简化语言其他部分。下面是个 non-member operator new 伪代码:

void* operator new(std::size_t size) throw(std::bad_alloc) {  // 这个 operator new 可以接受额外参数
    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();
    }
}

条款 49 谈到 operator new 内含一个无穷循环,而上述伪代码明白表明这个循环;“while(true)” 就是那个无穷循环。退出循环的唯一办法是:内存被成功分配或 new-handling 函数做了一件描述于条款 49 的事情:让更多内存可用、安装另一个 new-handler、卸除 new-handler、抛出 bad_alloc 异常(或派生物),或是承认失败而直接 return。如果不那么做,operator new 内的 while 循环永远不会结束。

事实上 operator new 成员函数可以被 derived classes 继承。然而就像条款 50 所言,写出定制型内存管理器的一个最常见理由是为针对某特定 class 的对象分配行为提供最优化,却不是为了该 class 的任何 derived classes。也就是说,针对 class X 而设计的 operator new,一旦被继承下去,有可能 base class 的 operator new 被调用用以分配 derived class 对象:

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

如果 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);  // 令标准的 operator new 起而处理。
    ...  // 否则在这里处理。
}

虽然这里没有检验 size 等于 0 的情况,实际上,它和上述的“ size 与 sizeof(Base) 的检测”融合一起了。C++ 裁定的所有非附属(独立式)对象必须有非零大小(见条款 39)。因此 sizeof(Base) 无论如何不能为零,如果 size 是 0,这份申请会被转交到 ::operator new 手上,后者有责任以某种合理方式对待这份申请。

array new

如果你打算控制 class 专属的 array 内存分配行为,那么你需要实现 operator new[]。这个函数通常被称为 array new,如果你决定写个 operator new[],记住,唯一需要做的一件事就是分配一块未加工内存,因为你无法对 array 之内迄今尚未存在的元素对象做任何事情。实际上你甚至无法计算这个 array 将含有可能经由继承被调用,将内存分配给“元素为 derived class 对象”的 array 使用,而你当然知道,derived class 对象通常比其 base class 对象大。

因此,你不能在 Base::operator new[] 内假设 array 的每个元素对象的大小是 sizeof(Base),这也就意味你不能假设 array 的元素对象个数是(byte 申请数)/ sizeof(Base),此外,传递给 operator new[] 的 size_t 参数,其值有可能比“将被填以对象”的内存数量更多,因为条款 16 说过,动态分配的 array 可能包含额外空间来存放元素个数。

这就是撰写 operator new 时你需要奉行的规矩。

operator delete

对于 operator delete,你只需记住 C++ 保证“删除 null 指针永远安全”,所以你必须兑现这项保证。下面是 non-member operator delete 的伪代码:

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

这个函数的 member 版本,只需要多加一个动作检查删除数量。万一你的 class 专属的 operator new 将大小有误的分配行为转交 ::operator new 指向,你也必须将大小有误的删除行为转交 ::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 (rawMemeory == 0) return;  // 检查 null 指针
    if (size != sizeof(Base)) {  // 如果大小错误,令标准版本 operator delete 处理此申请
        ::operator delete(rawMemory);
        return;
    }
    // 现在,归还 rawMemory 所指的内存;
    return;
}

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

请记住:

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

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

参考下面代码:

class Widget {
public:
    ...
    static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);  // 非正常形式的 new
    static void operator delete(void* pMemory std::size_t size) throw();  // 正常的 class 专属 delete
}

如果 operator new 接受的参数除了一定会有的 size_t 之外还有其他,这便是个所谓的 placement new。因此,上述的 operator new 是个 placement 版本。众多 placement new 版本中特别有用的一个是“接受一个指针指向对象该被构造之处”,那要的 operator new 长相如下:

void* operator new(std::size_t, void* pMemory) throw();

这个版本的 new 已被纳入C++ 标准程序库,你只要 #include<new> 就可以取用它。这个 new 的用途之一是负责在 vector 的未使用空间上创建对象。它同时也是最早的 placement new 版本。

placement new 有多重定义,但大多数时候是指此一特定版本,也就是“唯一额外实参是个 void*”,少数时候才是指接受任意额外实参的 operator new。

考虑下面的代码,它在动态创建一个 Widget 时将相关的分配信息记录于 cerr:

Widget* pw = new (std::cerr) Widget;  // 调用 operator new 并传递 cerr 为其 ostream 实参;这个动作会在 Widget 构造函数中抛出异常时泄漏内存

如果内存分配成功,而 Widget 构造函数抛出异常,运行期系统有责任取消 operator new 的分配并恢复。然而运行期系统无法知道真正被调用的那个 operator new 如何运作,因此它无法取消分配并恢复,所以上述做法行不通。取而代之的是,运行期系统寻找“参数个数和类型都与 operator new 相同”的某个 operator delete。如果找到,那就是它的调用对象。既然这里的 operator new 接受类型为 ostream& 的额外实参,所以对应的 operator delete 就应该是:

void operator delete(void*, std::ostream&) throw();

operator delete 如果接受额外参数,便称为 placement delete。现在,既然 Widget 没有声明 placement delete,所以运行期系统不知道如何取消并恢复原先对 placement new 的调用。于是什么也不做。换句话说,如果 Widget 构造函数抛出异常,不会有任何 operator delete 被调用。

如果一个带额外参数的 operator new 没有带相同额外参数的对应版operator delete,那么当 new 的内存分配动作需要取消并恢复之前状态时,就没有任何 operator delete 被调用。

需要记住的是,placement delete 只有在伴随 placement new 调用而触发的构造函数出现异常时才会被调用。也就是下面这种情况发生时 placement delete 不会被调用:

delete pw;  // 调用正常的 operator delete

顺带一提,由于成员函数名称会掩盖其外围作用域中的相同名称(见条款33),你必须小心避免让 class 专属的 new 掩盖客户期望的其他 new (包括正常版本)。假设你有一个 base class,其中声明唯一一个 placement operator new,客户端会发现无法正常使用正常形式的 new:

class Base {
public:
    ...
    static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);  // 这个 new 会遮掩正常的 global 形式
    ...
};
 
Base* pb = new Base;  // 错误!因为正常形式的 operator new 被掩盖
Base* pb = new (std::cerr) Base;  // 正确,调用 Base 的 placement new

同理,derived class 中的 operator new 会掩盖 global 版本和继承而得的 operator new 版本。

对于撰写内存分配函数,你需要记住的是,缺省情况下 C++ 在 global 作用域内提供以下形式的 operator new:

void* operator new(std::size_t) throw(std::bad_alloc);  // normal new
void* operator new(std::size_t, void*) throw();  // placement new
void* operator new(std::size_t, const std::nothrow_t&) throw();  // nothrow new

如果你在 class 内声明任何 operator new,它会遮掩上述这些标准形式。除非你的意思就是要阻止 class 的客户使用这些形式,否则请确保它们在你所生产的任何定制型 operator new 之外还可用。对于每一个可用的 operator new 也请确定提供对应的 operator delete。如果你希望这些函数有着平常的行为,只要令你的 class 专属版本调用 global 版本即可。

完成以上所言的一个简单做法是,建立一个 base class,内含所有正常形式的 new 和 delete:

class StandardNewDeleteForms {
public:
    // normal new/delete
    static void* operator new(std::size_t size) throw(std::bad_alloc)
    { return ::operaotr 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); }
};

如果想自定义扩充标准形式的客户,可利用继承机制及 using 声明式(见条款33)取得标准形式:

class Widget : public StandardNewDeleteForms {
public:
	// using 声明 让这些形式可见
    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* pMemory, std::ostream& logStream) throw();  
    ...
};

请记住:

  • 当你写一个 placement operator new,请确定也写出了对应的 placement operator delete。如果没有这样做,你的程序可能会发生内存泄漏。
  • 当你声明 placement new 和 placement delete,请确定不要无意识(非故意)地掩盖了它们的正常版本。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值