内存释放
一般而言,new表达式形式大致如下:1
Widget* pw = new Widget;
其中一共调用了两个函数:operator new以及Widget的默认构造函数。
如果operator new被成功调用,但默认构造函数却抛出了异常,我们应该释放分配的内存并让它恢复原状。客户并没有这个能力,因为pw此时尚未被赋值,客户并不知道已分配的内存的地址,该任务需要C++运行期系统完成。
解决方法很简单:调用operator new所对应的那个operator delete。听起来容易,但如果我们曾经声明过带有附加参数的operator new,对应的delete就不好找了。
问题实例
假设现有一个operator new接受一个ostream,用来记录信息,同时具备一个正常形式的class专属delete:1
2
3
4
5
6
7class Widget{
public:
...
static void* operator new(size_t size,ostream& log) throw(bad_alloc);//非正常形式new
static void operator delete(void* pMemory,size_t size) throw();//正常形式的delete
...
}
这个设计存在问题,但我们在探究原因之前,必须先了解相关术语。
placement new
如果一个operator new除了size_t之外还接受其他参数,那它就是一个placement版本。
众多placement版本中最受欢迎的是“接受一个指针指向对象被构造之处”:1
void* operator new(size_t,void* pMemory) throw();
它是如此地知名以至于我们默认提及placement new即为该版本。
解决方案
显然,在实例中,带有ostream参数的placement new并没有与之对应的delete,所以一旦构造函数抛出异常,系统无法做到自恢复。所以我们应该为它补充上placement delete:1
void operator delete(void*,ostream&) throw();
名称遮蔽
成员函数的名称会掩盖其外部作用域的相同名称(Effective C++ 34),如果你的base class中只声明了一个placement new,客户将无法使用正常形式的operator new,delete同理。与此类似的,derived class中的operator news会遮蔽base版本与继承而来的版本。
为了避免名称遮蔽,我们需要了解到C++在global作用域提供以下形式的operator new:1
2
3void* operator new(size_t) throw(bad_alloc);
void* operator new(size_t,void*) throw();
void* operator new(size_t,const nothrow_t&) throw();
如果我们在class内声明任何形式的operator new,它们都会遮掩这些标准形式,除非你的本意就是禁止使用标准形式,否则请确保它们可用。确保可用的方法是:建立一个base class,内含所有正常形式的new与delete。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class StandardNewDeleteForms{
public:
//normal
static void* operator new(size_t size) throw(bad_alloc){
return ::operator new(size);
}
static void operator delete(void* pMemory) throw(){
return ::operator delete(pMemory);
}
//placement
static void* operator new(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
static void* operator new(size_t size,const nothrow_t& nt) throw(){
return ::operator new(size,nt);
}
static void* operator delete(void* pMemory,const nothrow_t& nt) throw(){
return ::operator delete(pMemory);
}
};
当你需要自定义new与delete时,只需要继承该类并使用using机制,即可避免名称遮蔽。
总结
- placement new和placement delete必须同时存在且匹配
- 自定义版本不得遮蔽正常版本