第一章见 Effective C++ 学习笔记 第一章:让自己习惯 C++ 第二章见 Effective C++ 学习笔记 第二章:构造、析构、赋值运算 第三章见 Effective C++ 学习笔记 第三章:资源管理 第四章见 Effective C++ 学习笔记 第四章:设计与声明 第五章见 Effective C++ 学习笔记 第五章:实现 第六章见 Effective C++ 学习笔记 第六章:继承与面向对象设计 第七章见 Effective C++ 学习笔记 第七章:模板与泛型编程 第八章见 Effective C++ 学习笔记 第八章:定制 new 和 delete 第九章见 Effective C++ 学习笔记 第九章:杂项讨论
本章中,若不特别说明,则 new 和 delete 的描述也适用于 new[] 和 delete[]。
Understand the behavior of the new-handler.
现代 C++ 中的 new 在无法分配有效空间时,会抛出一个 no_alloc 异常,有些时候,我们不希望抛出异常,而是能让分配失败时进入到一个处理函数中,这个机制 C++ 是支持的,它叫做 new_handler。 提示:new_handler 中的 new 是指 operator new 的意思,表示保存 new 操作处理函数的意思,而不是指新的处理函数。下文可能出现 old_new_handler,则表示旧的 new 操作处理函数。
声明在 <new> 文件中:
namespace std { typedef void (*new_handler) (); new_handler set_new_handler(new_handler p) throw(); }new_handler 是一个 typedef,它实际是一个函数指针,默认是 null,操作它的接口 set_new_handler 接受一个 new_handler,返回一个 new_handler,接受的参数是我们用户提供的处理函数的指针,返回的是之前的处理函数的指针,它用来将我们用户提供的处理函数替换到默认位置,并把替换前默认的处理函数返回。
备注:题外话,C++ 11 中已经弃用了异常声明符 throw(),取而代之的是 noexcept。后者可以阻止异常的抛出,当然还包括 noexcept(false) 和 noexcept(true),noexcept(true) 与 noexcept 一致,noexcept(false) 表示可能会抛出异常。
如果 p 的分配失败,则会调用到 OutOfMem 函数,打印错误信息并 abort。
可以自定义的处理函数为我们提供了更多的灵活性,避免了无脑抛异常,比如我们可以在处理函数中打印信息,想办法分配内存,或直接终止程序。
这块比较简单。在类内除了重载 operator new 之外,还需要重载 set_new_handler 函数,同时还要提供一个 static 的 new_handler 成员,这个数据成员用来保存旧的(替换前的)new_handler,从而可以在类之外能够让其他代码调用到大环境下的 new_handler (而不是类内做了私自修改的那个),也就是说,程序在某一时刻只会维护一个有效的 new_handler。 类内的 set_new_handler 需要替换新的 new_handler 并维护旧的 new_handler,在 new 失败时需要恢复旧的 new_handler,而类内的 new 便完成调用 std::operator new 和维护旧的 new_handler 的动作。 一种更优雅的方式是使用 RAII,建立一个 new_handler 的资源类来管理。
class NewHandlerHolder { public: explicit NewHandlerHolder(std::new_handler nh) : old_new_handler(nh) {} ~NewHandlerHolder() { std::set_new_handler(old_new_handler); } private: std::new_handler old_new_handler; // 保存替换之前的旧的 handler NewHandlerHolder(const NewHandlerHolder&); // 禁止 copy 操作 NewHandlerHolder& operator=(const NewHandlerHolder&); } // 以下是使用,假设一个 W 类需要重载 operator new,以下是实现 class W { 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; // 可以定义 class 自有的 Handler }; std::new_handler W::currentHandler = 0; std::new_handler W::set_new_handler(std::new_handler p) throw() { std::new_handler old_new_handler = currentHandler; currentHandler = p; return currentHandler; } void* W::operator new(std::size_t size) throw(std::bad_alloc) { new_handler old_new_handler = std::set_new_handler(currentHandler)); NewHandlerHolder h(old_new_handler); return ::operator new(size); // 调用公共的 new 操作,留意 RAII 特性会在退出函数时, // 也就是 new 成功之后,调用 h 的析构函数,从而还原旧的 handler } // 以下为使用 void OutOfMem(); // 自定义的处理函数 W::set_new_handler(OutOfMem); // 设定 class W 默认的 handler W* pw = new W; // 如果 new 失败,会调用 OutOfMem std::string* ps = new std::string; // 如果 new 失败,会抛出异常,也就是说不影响其他 class W::set_new_handler(0); // 设定 class W 默认的 handler 为 null W* pw2 = new W; // 这次如果 new 失败,会抛出异常如果我们有多个 class,都想设计这套机制,如何能复用已有代码呢?本话题引出称为 mixin 的编程风格。 定义一个模板基类,模板参数是不同的 class,基类中的实现就是话题 2 的这套设计,代码几乎不变,模板参数在基类中也不会使用。 然后每个 class 都继承这个基类:
template<typename T> class BaseNewHandlerHolder { ... }; // 实现代码和话题 2 中的 NewHandlerHolder 一致 class W : public BaseNewHandlerHolder<W> { ... }; // 特殊写法这里我们注意到有个特殊写法,派生类继承的基类中的模板参数,是派生类类型,这是允许的,被称为 “怪异的循环模板模式”(curiously recurring template pattern, CRTP)。
这样,我们就能够利用模板类的特点,针对每个不同的类 (如 W)来设计只属于它的 new handler 机制。
最早的 C++ 设计时,new 不会抛出异常,而是在分配失败时返回 null,为了兼容这种老的设计,<new> 中还提供了一种不抛出异常的方式。
class W { ... }; W* p1 = new W; if (p1 == 0) { ... } // 没意义,new 不会返回错误的 null W* p2 = new (std::nothrow) W; if (p2 == 0) { ... } // 当 new 失败时,能够检查到 p2 为 0 // 不过有个前提,就是 W 的构造函数不会再抛出异常Understand when it makes sense to replace new and delete.
当有以下几点需求时,才需要主动去写一个自定义的 new 和 delete 。
为了检测使用错误。比如 new 之后 delete 失败,或者多次 delete 同一块内存,还有分配额外的内存空间来保存默认数据之外的一些内容(如区块签名)。为了提高性能。默认的 new 和 delete 的设计是为了满足所有需求的一种中庸实现,所以它的运行时间和运行空间可能会比一个自定义的版本更差。为了收集使用统计数据。统计调用次数,内存状态等信息。手动完成数据对齐。默认版本的 new 可能不考虑数据对齐的问题,这可能会导致性能问题或运行错误。实现内存集簇。避免不必要的页错误,将分配的内存集中在尽可能少的内存页中。额外行为。添加一些以上条目没涉及到的功能,比如像操作共享内存,为释放的内存写入 0 值。Adhere to convention when writing new and delete.
为了实现和标准 new/delete 一致的行为,我们自定义的 new/delete 也应该完成标准规范的动作。
Write placement delete if you write placement new.
先把 placement new 是什么的问题放到一边。 如果我们写的一个:
W *pw = new W;这个表达式会调用两个 W 的成员函数,分别是 W 的 new 和 W 的默认构造函数。如果 W 的 new 分配成功,但 W 的默认构造函数却失败,那么因为我们无法取得 pw 指向的对象,也就无法去将 new 的内存释放,从而可能导致内存泄漏。 不过还好,如果 new 是一个标准的 new (只接受一个 size_t 参数作为内存大小),运行时系统会自动调用对应的那个 delete 来将内存释放。 但是,如果 new 是一个自定义的版本,那运行期系统也会找与其对应的一个 delete 来调用,如果找不到,就不会调用。
如果有一个自定义版本的 new,这个 new 除了 size_t 参数以外还有其他参数,就被叫做 placement new,而与其对应的相同参数的一个 delete,就叫与其对应的 placement delete。 比如:
class W { public: static void* operator new(std::size_t size) throw(std::bad_alloc); // 标准 new static void* operator delete(void *pMemory) throw(); // 标准 delete static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc); // placement new static void* operator delete(void *pMemory, std::ostream& logStream) throw(); // 与之对应的 placement delete }; W *pw = new (std::cerr) W; // 调用 placement new delete pw; // 需要注意,手动调用的 delete,其实是标准的 delete,placement delete 只会在失败时自动调用应当在编写了一个 placement new 之后,也同步编写一个对应的 placement delete,来避免出现 new 时意外构造失败,导致内存无法自动释放的问题。
在条款 33 中提到,内部作用域的同名函数会遮掩外部作用域的函数(注意是同名,无所谓参数列表),对于 new 和 delete 也是如此。 所以:
class Base { public: static void operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc); // 这个 new 遮掩了默认外部标准 new // 假设这个类中没有实现默认的标准 new }; Base *pb = new Base; // 会失败 Base *pb = new (std::cerr) Base; // 成功,调用类内的 placement new同理的是继承结构,派生类内的 new 会遮掩基类中的 new。 所以,我们应该默认在类内定义好默认的标准 new 和 delete,以防止出现问题。
C++ 标准提供 3 中 new:
void* operator new(std::size_t) throw(bad_alloc); // 标准 new void* operator new(std::size_t, void*) throw(); // 默认的 placement new void* operator new(std::size_t, const std::nothrow_t&) throw(); // 不抛出异常的 new我们可以将这 3 种 new 实现在基类中,然后在派生类中使用 using 来暴露基类内的 new,当然也包括对应的 delete。在派生类中也可以定义自定义的 new 来遮掩这 3 种默认 new。