52.内存管理定式

前言

 
上一节描述了何时需要自定义operator new与operator delete,但并没有描述自定义时应当遵循何种规则。实际上这些规则并不难奉行,只是不太直观。


operator new

 
operator new共有四个要求:

  1. 返回正确的值
  2. 内存不足时必须调用new-handling函数
  3. 考虑0内存需求
  4. 避免掩盖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
2
3
4
5
6
7
8
9
10
11
12
13
14
void* operator new(size_t size) throw(bad_alloc){
using namespace std;
if(size==0) size=1;
while(true){
...//尝试分配内存
if(分配成功)
return (一个指向分配内存的指针);
new_handler globalHandler = set_new_handler(nullptr);
set_new_handler(globalHandler);
if(globalHandler)
*globalHandler();
else 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
2
3
4
5
6
7
class Base{
public:
static void* operator new(size_t size) throw(bad_alloc);
...
};
class Derived:public Base {...};//未声明operator new
Derived* p = new Derived;//调用Base::operator new

针对继承,我们有补救措施如下:
1
2
3
4
5
void* Base::operator new(size_t size) throw(bad_alloc){
if(size!=sizeof(Base))
return ::operator new(size);//std::operator new
...
}

看起来这种实现似乎没有考虑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
2
3
4
void operator delete(void* rawMemory) throw(){
if(rawMemory == nullptr) return;
...//释放内存
}

当其作为class的member函数时,它只需要像new操作一样,检查一下sizeof是否有误即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base{
public:
static void* operator new(size_t size) throw(bad_alloc);
static void* operator delete(void* rawMemory,size_t size) throw();
...
};
void* Base::operator delete(void* rawMemory,size_t size) throw();
if(rawMemory==nullptr) return;
if(size!=sizeof(Base)){
::operator delete(rawMemry);
return;
}
...//释放内存
return;
}


总结

  1. operator new内部有一个死循环,并在其中尝试分配内存,如果它不能分配,则调用new-handler,同时它应该能够处理0 bytes申请。class member版本则应该处理“比正确大小大或者小的申请”。
  2. operator delete应该在收到nullptr时不做任何事,class版本还应该处理“比正确大小大或者小的申请”。