Gotcha #62: Replacing Global New and Delete
Gotcha条款62:替换Global New和Global Delete
将operator new、operator delete、array new亦或array delete的标准global版本替换为自定制版本,这几乎从来都不是个好主意——即使C++标准允许你这么做。这些函数的标准版本一般都针对通用目的(general-purpose)之存储管理做了极大优化,而用户自定义的替代版本则不大会做得更好了。(然而,针对特定的类别或类别阶层体系采用(自定制的)成员函数形式的操作来定制其内存管理,则通常是合理的。)
如果operator new和operator delete针对特定目的之实现版本作出了与标准版本相异的行为,其就可能引入臭虫,因为许多标准程序库和第三方程序库的正确性皆依赖于这些函数缺省的标准实现版本。
比较安全的方案是对global版本的operator new等函数进行重载,而不是替代它们。假设我们要以特定的字符样式(character pattern)填充新分配的存储空间:
void *operator new( size_t n, const string &pat ) {
char *p = static_cast<char *>(::operator new( n ));
const char *pattern = pat.c_str();
if( !pattern || !pattern[0] )
pattern = "\0"; // note: two null chars
const char *f = pattern;
for( int i = 0; i < n; ++i ) {
if( !*f )
f = pattern;
p[i] = *f++;
}
return p;
}
该operator new版本接收一个字串样式作为引数,并将其拷贝到新分配的存储空间中。经由重载解析,编译器就可以区分标准operator new与我们自己的“接收两个引数之版本”。
string fill( "<garbage>" );
string *string1 = new string( "Hello" ); // 标准版本
string *string2 =
new (fill) string( "World!" ); // 重载的版本
标准中还定义了一个重载的operator new版本;除了以size_t作为第一引数之外,该版本还接收一个void*型别作为第二引数。该实现只是简单的返回第二引数。(其中的throw()语法是一个exception-specification(异常规范),意味该函数不会传播出任何异常。在后述讨论及一般情况下,都可以安然忽略之。)
void *operator new( size_t, void *p ) throw()
{ return p; }
这就是标准的placement new,用于在特定的位置空间建构一个对象。(其不同之处在于,标准的“单引数operator new”可以被替换,而试图替换placement new则是非法的。)本质上来说,我们会将其用于“让编译器误以为调用了一个构造函数”的场合。比如说,对于一个嵌入式应用,我们或许想在某个特定的硬件地址上建构一个“status register(状态寄存器)”对象:
class StatusRegister {
// . . .
};
void *regAddr = reinterpret_cast<void *>(0XFE0000);
// . . .
// 在regAddr的位置放一个register object
StatusRegister *sr = new (regAddr) StatusRegister;
自然,经由placement new创建的对象必须在某个时刻被销毁。然而,由于placement new并未真正的分配内存(译注:其只是在指定位置放入对象,并未进行内存分配),因此也必须保证在销毁时没有内存被删除。回忆一下,delete operator的行为是:在调用operator delete函数(以便归还存储空间)之前,首先唤起“欲删除对象”之析构函数。对于“对象是经由placement new进行‘空间分配’”的情形,为了避免任何尝试归还内存空间的动作,我们在销毁对象时必须对析构函数进行显式的(explicit)调用(译注:这正是delete operator所做的第一步操作,第二步“调用operator delete函数”的操作就不用去做了)。
sr->~StatusRegister(); // 显式的调用dtor, 不调用operator delete函数
Placement new和explicit destruction(显式析构操作)显然是非常有用的特性,但倘若不保守并谨慎的使用它们,显然也是非常危险的。(详见Gotcha条款47中一个来自标准程序库的例子。)
应注意,当我们重载operator delete时,这些重载版本绝不会被“使用标准delete形式的表达式”唤起。
void *operator new( size_t n, Buffer &buffer ); // 重载版本的new
void operator delete( void *p,
Buffer &buffer ); // 对应的重载版本之delete
// . . .
Thing *thing1 = new Thing; // 使用标准的operator new
Buffer buf;
Thing *thing2 = new (buf) Thing; // 使用重载版本的operator new
delete thing2; // 不对, 应该使用重载版本的delete
delete thing1; // 正确, 使用标准的operator delete
相应的,对于经由placement new创建的对象,我们不得不显式的(explicitly)调用该对象的析构函数,然后直接明了的调用适当的operator delete函数,以便显式的将对象的存储空间进行去配:
thing2->~Thing(); // 正确, 销毁Thing
operator delete( thing2, buf ); // 正确, 使用重载版本的delete
实际当中,经由“global operator new之重载版本”分配的存储空间经常错误的经由“global operator new之标准版本”被去配。一个避免这种错误的方法是保证:任何经由“global operator new之重载版本”分配的存储空间都是经由“global operator new之标准版本”来获取存储空间(译注:意即,在“global operator new之重载版本”的实现中,通过调用“global operator new之标准版本”来获取空间,详见本条款开头的示例)。这正是前述第一个重载实现版本(译注:指的正是本条款开头那个“以特定的字符样式(character pattern)填充新分配的存储空间”的例子)所用的方法,其能与“global operator delete之标准版本”相配合并运作正常:
string fill( "<garbage>" );
string *string2 = new (fill) string( "World!" );
// . . .
delete string2; // 运作正常!
一般来说,global operator new的重载版本要么就不分配任何存储空间,要么就应该只简单的包裹(wrap)global operator new的标准版本(译注:如本条款开头那个例子所示,重载版本是标准版本的一个wrapper)。
通常情况下,最好的方案是全然避免对“处于global scope的内存管理operator functions”做手脚,代之以“operator new、operator delete、array new、array delete的成员函数版本”来定制类别或类别阶层体系的内存管理操作。
在Gotcha条款61的结尾我们提到过,若从new表达式中的初始化操作传出一个异常,运行期系统会唤起一个“适当的”operator delete函数:
Thing *tp = new Thing( arg );
如果Thing的分配动作成功了但构造函数抛出异常,那么运行期系统将会唤起一个适当的operator delete函数来归还tp所指向的未经初始化的内存。在上例中,这个“适当的operator delete”要么是global版本的operator delete(void*),要么就是一个具有相同形式的成员函数版本。然而不同的operator new即意味着不同的operator delete:
Thing *tp = new (buf) Thing( arg );
此时,适当的operator delete应该是“双引数版本”operator delete(void*,Buffer&),与Thing分配操作所使用的“operator new之重载版本”相对应;这正是运行期系统会唤起的版本。
C++在定义内存管理的行为方面给予了颇大的弹性,伴之以复杂性作为代价。标准的“global operator new”和“global operator delete”便足以满足多数需求。因此,我们应该仅在确实需要的情况下才采用更复杂的方案。