前言
上一节描述了何时需要自定义operator new与operator delete,但并没有描述自定义时应当遵循何种规则。实际上这些规则并不难奉行,只是不太直观。
operator new
operator new共有四个要求:
- 返回正确的值
- 内存不足时必须调用new-handling函数
- 考虑0内存需求
- 避免掩盖global new(虽然这更偏近class接口要求)。
operator new 返回值
一般而言,operator new返回值很简单,分配成功则返回一个指向那块内存的指针,反之则遵循Effective C++ 50的约定,并抛出一个bad_alloc异常。
其实也不是特别简单,因为它实际上不只一次尝试分配内存,并在每一次失败后都调用new-handling函数。这里假设new-handling也许能够释放一些内存,当只有指向new-handling函数的指针是nullptr时,operator new才会抛出异常。
0内存需求
c++规定,即使客户需要0bytes,也必须提供合法指针而不得返回nullptr。为了满足约定,有伪代码实现如下:
1 | void* operator new(size_t size) throw(bad_alloc){ |
其中不太科学的地方就是将new-handling函数指针设置为nullptr后又迅速恢复了它,这是因为我们别无他法来获取当前的new-handling函数。这种操作技巧在单线程下拙劣且有效,多线程环境可能需要某种lock来确保new_handling函数背后的某种数据结构不遭破坏。
operator new与继承体系
在上述实例中我们没有考虑过继承。一般而言,定制内存管理器的常见理由是针对某个class的对象进行优化,而不是对其derived class对象作出优化。也就是说,针对class X而设计的operator new其行为只是刚好为sizeof(X)的对象而设计。众所周知,dc对象一般要大于bc对象:
1 | class Base{ |
针对继承,我们有补救措施如下:
1 | void* Base::operator new(size_t size) throw(bad_alloc){ |
看起来这种实现似乎没有考虑size==0的情况,但是实际上它考虑了,因为所有独立式对象必须有大小(见Effective C++ 40)因此sizeof(bc)无论如何都不会为0。
operator new[]
如果我们需要实现new[],我们要做的事情只有一个:分配一块raw memory。因为我们既不知道这个array具体有多少元素,也不知道其元素的大小(因为dc的对象往往比bc的对象大),另外传递给new[]的size_t参数未必就是真正的元素个数,因为实际上array会有一部分负责存放其元素个数,它们也需要占据内存。
operator delete
operator delete的情况简单一些,你只需要记住“删除nullptr永远安全即可”:
1 | void operator delete(void* rawMemory) throw(){ |
当其作为class的member函数时,它只需要像new操作一样,检查一下sizeof是否有误即可:
1 | class Base{ |
总结
- operator new内部有一个死循环,并在其中尝试分配内存,如果它不能分配,则调用new-handler,同时它应该能够处理0 bytes申请。class member版本则应该处理“比正确大小大或者小的申请”。
- operator delete应该在收到nullptr时不做任何事,class版本还应该处理“比正确大小大或者小的申请”。