条款 49:了解 new-handler 的行为
当operator new无法满足某一内存分配需求时,会不断调用一个客户指定的错误处理函数,即所谓的 new-handler,直到找到足够内存为止
new-handler 是一个 typedef,指向一个无参数值无返回值的函数。可以通过 set_new_handler 函数去指定客户想要的 new-handler。
set_new_handler 函数接受一个新的 new-handler 参数,返回被替换掉的 new-handler 函数
cpp
namespace std {
using new_handler = void(*)();
new_handler set_new_handler(new_handler) noexcept; // 返回值为原来持有的 new-handler
}
设计良好的 new-handler 函数:
- 让更多的内存可被使用: 可以让程序一开始执行就分配一大块内存,而后当 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行为的static数据,命名空间数据或者全局数据
- 卸除 new-handler: 将nullptr传给set_new_handler,使operator new在内存分配不成功时抛出异常
- 抛出 bad_alloc(或派生自 bad_alloc)的异常: 这样的异常不会被operator new捕捉,会被传播到内存分配处
- 不返回: 通常调用std::abort或std::exit
以不同的方式处理内存分配的情况,比如按不同的 class 进行处理。c++ 并不支持为每一个 class 提供专属版本的 new_handler,要用静态成员
cpp
public:
static std::new_handler set_new_handler(std::new_handler p) noexcept;
static void* operator new(std::size_t size);
private:
static std::new_handler currentHandler;
};
// 做和 std::set_new_handler 相同的事情
std::new_handler Widget::set_new_handler(std::new_handler p) noexcept {
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
void* Widget::operator new(std::size_t size) {
auto globalHandler = std::set_new_handler(currentHandler); // 切换至 Widget 的专属 new-handler
void* ptr = ::operator new(size); // 分配内存或抛出异常
std::set_new_handler(globalHandler); // 切换回全局的 new-handler
return globalHandler;
}
std::new_handler Widget::currentHandler = nullptr;
以对象管理资源的方法:
cpp
class NewHandlerHolder {
public:
explicit NewHandlerHolder(std::new_handler nh): handler(nh) {}
~NewHandlerHolder() {
std::set_new_handler(handler);
}
private:
std::new_handler handler;
};
Widget::operator new的实现可改为
cpp
void* Widget::operator new(std::size_t size) noexcept{
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
Widget的客户调用 :
cpp
void OutOfMem();
Widget::set_new_handler(OutOfMem);
auto pw1 = new Widget; // 若分配失败,则调用 OutOfMem
Widget::set_new_handler(nullptr);
auto pw2 = new Widget; // 若分配失败,则抛出异常
上述代码每个class要实现自己的set_new_handler和operator new。可以用template。建立起一个"mixin"风格的基类,让其派生类继承它们所需的set_new_handler和operator new,并且使用模板确保每一个派生类获得一个实体互异的currentHandler成员变量
cpp
template<typename T>
class NewHandlerSupport { // "mixin"风格的基类
public:
static std::new_handler set_new_handler(std::new_handler p) noexcept;
static void* operator new(std::size_t size);
... // 其它的 operator new 版本,见条款 52
private:
static std::new_handler currentHandler;
};
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) {
auto globalHandler = std::set_new_handler(currentHandler);
void* ptr = ::operator new(size);
std::set_new_handler(globalHandler);
return globalHandler;
}
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = nullptr;
class Widget : public NewHandlerSupport<Widget> {
public:
... // 不必再声明 set_new_handler 和 operator new
};
此处的模板参数T并没有真正被当成类型使用,而仅仅是用来区分不同的派生类,使得模板机制为每个派生类具现化出一份对应的currentHandler
即 CRTP(curious recurring template pattern,奇异递归模板模式),也被用于实现静态多态
cpp
template <class Derived>
struct Base {
void Interface() {
static_cast<Derived*>(this)->Implementation(); // 在基类中暴露接口
}
};
struct Derived : Base<Derived> {
void Implementation(); // 在派生类中提供实现
};
C++ 保留了传统的"分配失败便返回空指针"的operator new,称为 nothrow new,通过std::nothrow对象来使用
cpp
Widget* pw1 = new Widget; // 如果分配失败,抛出 bad_alloc
if (pw1 == nullptr) ... // 这个测试一定失败
Widget* pw2 = new (std::nothrow) Widget; // 如果分配失败,返回空指针
if (pw2 == nullptr) ... // 这个测试可能成功
条款50:了解new和delete的合理替换时机
定制operator new和operator delete的理由:
用来检测运行上的错误 :如果将"new 所得内存"delete 掉却不幸失败,会导致内存泄漏;如果在"new 所得内存"身上多次 delete 则会导致未定义行为。如果令operator new持有一串动态分配所得地址,而operator delete将地址从中移除,就很容易检测出上述错误用法
另外自定义new分配超额内存,在额外空间放置特定签名/byte pattern。在delete时检查是否不变;反之,肯定存在"overruns"(写入点在分配区块尾部之后)或"unferruns"(写入点在分配区块头部之前),delete也可log那个指针。例如:
cpp
static const int signature = 0xDEADBEEF; // 调试"魔数"
using Byte = unsigned char;
void* operator new(std::size_t size) {
using namespace std;
size_t realSize = size + 2 * sizeof(int); // 分配额外空间以塞入两个签名
void* pMem = malloc(realSize); // 调用 malloc 取得内存
if (!pMem) throw bad_alloc();
// 将签名写入内存的起点和尾端
*(static_cast<int*>(pMem)) = signature;
*(reinterpret_cast<int*>(static_cast<Byte*>(pMem) + realSize - sizeof(int))) = signature;
return static_cast<Byte*>(pMem) + sizeof(int); // 返回指针指向第一个签名后的内存位置
}
这段代码不保证内存对齐,许多地方不遵守c++规范,见条款51
收集使用上的统计数据 : 定制 new 和 delete 动态内存的相关信息:分配区块的大小分布,寿命分布,FIFO(先进先出)、LIFO(后进先出)或随机次序的倾向性,不同的分配/归还形态,使用的最大动态分配量等等。
增加分配和归还的速度 :泛用型分配器通常比定制分配器慢。类专属的分配器可以做到"区块尺寸固定",例如 Boost 提供的 Pool 程序库。又例如,编译器所带的内存管理器是线程安全的,但如果你的程序是单线程的,你也可以考虑写一个不线程安全的分配器来提高速度
降低缺省内存管理器带来的空间额外开销 :泛用型分配器通常比定制分配器使用更多内存。因为常常在每一个分配区块身上招引某些额外开销。针对小型对象而开发的分配器(例如 Boost 的 Pool 程序库)本质上消除了这样的额外开销
弥补缺省分配器中的非最佳内存对齐 :许多计算机体系架构要求特定的类型必须放在特定的内存地址上,如果没有奉行这个约束条件,可能导致运行期硬件异常,或者访问速度变低。std::max_align_t用来返回当前平台的最大默认内存对齐类型,对于malloc分配的内存,其对齐和max_align_t类型的对齐大小应当是一致的,但若对malloc返回的指针进行偏移,就没有办法保证内存对齐
C++11 中,内存对齐相关方法
将相关对象成簇集中 :如果知道特定的某个数据结构往往被一起使用,又希望在处理这些数据时将"内存页错误(page faults)"的频率降至最低,可以考虑为此数据结构创建一个堆,将它们成簇集中在尽可能少的内存页上。一般可以使用 placement new 达成这个目标条款52
获得非传统的行为:如分配和归还共享内存,这些事情只能被 C API 完成,则可以将 C API 封在 C++ 的外壳里,写在定制的 new 和 delete 中
条款 51:编写 new 和 delete 时需固守常规
编写自己的new:存不足时必须不断调用 new-handler,如果无法供应客户申请的内存,就抛出std::bad_alloc异常。即使客户需求为0字节,operator new也得返回一个合法的指针
cpp
void* operator new(std::size_t size) {
using namespace std;
if (size == 0) // 处理0字节申请
size = 1; // 将其视为1字节申请
while (true) {
if (...) // 如果分配成功
return ...; // 返回指针指向分配得到的内存
// 如果分配失败,调用目前的 new-handler
auto globalHandler = get_new_handler(); // since C++11
if (globalHandler) (*globalHandler)();
else throw std::bad_alloc();
}
}
如果子类未声明自己的operator new,会从父类继承过来,使的子类使用了父类new分配方式。但子类与父类的大小多数时候是不同的,因此成员函数版本:
cpp
void* Base::operator new(std::size_t size) {
if (size != sizeof(Base))
return ::operator new(size); // 转交给标准的 operator new 进行处理
...
}
此时无需检测大小是否为0,因为类必须有非零大小条款39
如果要实现operator new[],即array new,唯一要做的就是分配一块未加工的原始内存。因为无法对尚未存在的元素对象做任何事,甚至无法计算含有多少个对象
编写自己的delete:删除空指针永远安全
cpp
void operator delete(void* rawMemory) noexcept {
if (rawMemory == 0) return;
// 归还 rawMemory 所指的内存
}
成员函数版本
cpp
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 也要写 placement delete
placement new:如果你的new接收的参数除了必定有的size_t外还有其他
cpp
void* operator new(std::size_t, std::ostream& logStream);
auto pw = new (std::cerr) Widget;
elete同理
当创建对象时,会先进行new函数,然后调用构造函数,如果构造出现异常,就需要delete,否则内存泄漏 但客户手上的指针仍未指向该被归还的内存,因此由c++系统本身调用delete。系统需要知道哪个delete该被调用
当抛出异常时,运行期系统会寻找参数个数和类型都与 operator new 相同的某个 operator delete。 placement delete 只有在 placement new 的调用构造函数异常时才会被系统调用,即使对一个用 placement new 申请出的指针使用 delete,也绝不会调用 placement delete。因此如果要处理 placement new 相关的内存泄漏问题,我们必须同时提供一个正常版本的 delete 和 placement 版本的 delete。前者用于构造期间无异常抛出,后者用于构造期间有异常抛出
cpp
class Widget {
public:
static void* operator new(std::size_t size, std::ostream& logStream); // placement new
static void operator delete(void* pMemory); // delete 时调用的正常 operator delete
static void operator delete(void* pMemory, std::ostream& logStream); // placement delete
};
还要注意同名函数遮掩调用的问题
cpp
class Base {
public:
static void* operator new(std::size_t size, std::ostream& logStream);
...
};
auto pb = new Base; // 无法通过编译!
auto pb = new (std::cerr) Base; // 正确
同理,子类的operator new会遮掩global和父类继承的operator new版本:
cpp
class Derived : public Base {
public:
static void* operator new(std::size_t size);
...
};
auto pd = new (std::clog) Derived; // 无法通过编译!
auto pd = new Derived; // 正确
除非目的就是禁用,否则要确保这些默认形式对定制类型依然可用
cpp
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和delete
cpp
class StadardNewDeleteForms{
public:
// normal new/delete
static void* operator new(std::size_t size){
return ::operator new(size);
}
static void operator delete(void* pMemory) noexcept {
::operator delete(pMemory);
}
// placement new/delete
static void* operator new(std::size_t size, void* ptr) {
return ::operator new(size, ptr);
}
static void operator delete(void* pMemory, void* ptr) noexcept {
::operator delete(pMemory, ptr);
}
// nothrow new/delete
static void* operator new(std::size_t size, const std::nothrow_t& nt) {
return ::operator new(size,nt);
}
static void operator delete(void* pMemory,const std::nothrow_t&) noexcept {
::operator delete(pMemory);
}
};
凡是需要自定义的class,可以继承该类并使用using声明式条款33