Gotcha #61: Checking for Allocation Failure
Gotcha条款61:检查内存分配失败的情形
有些问题就是不应该问,“某个内存分配动作是否成功”正属其一。
让我们来看看C++中的内存分配曾经是个什么样子。下面这段代码仔细检查了每次内存分配的成功与否:
bool error = false;
String **array = new String *[n];
if( array ) {
for( String **p = array; p < array+n; ++p ) {
String *tmp = new String;
if( tmp )
*p = tmp;
else {
error = true;
break;
}
}
}
else
error = true;
if( error )
handleError();
这种编码风格会带来许多麻烦。如果由此就可以检测出所有可能的内存分配失败的话,那或许还值得一做。非也。很不幸,String constructor本身可能会遇到内存分配错误,而且并没有什么好方法能把这个错误传出构造函数。一个可能的(但前景并不看好的)办法是,(出错时)让String constructor使String object处在某种可接受的错误状态中,并设置一个flag使其能够被该类别的用户检测到。即使假设我们可以接触String的实现代码以便实现这种行为,这种方法还是给代码的原作者以及往后所有的维护者留下了另一个需要测试的状况。
或者我们可以忽略测试。上述那样做所涉及的错误检查代码几乎不会在一开始就能完全编写正确,而且在经过一段时期的维护之后就几乎不可能是正确的了。更好的解决方法是,根本就不做检查:
String **array = new String *[n];
for( String **p = array; p < array+n; ++p )
*p = new String;
这段代码更简短、更清晰、更快捷,而且是正确的。New的标准行为就是,在分配失败事件发生时抛出一个bad_alloc exception。这让我们可以将分配失败时需要的错误处理代码封装起来,使其与余下的程序隔离开来,形成一个更干净、更清晰,一般也更有效的设计。
由于使用new时要么成功要么抛出异常,因此无论如何尝试对new的标准用法之结果进行检查都不可能测出失败:
int *ip = new int;
if( ip ) { // 条件总是为 true
// . . .
}
else {
// 永远也不会执行到这里
}
但我们可以采用operator new标准的”nothrow”版本,其会在失败时返回一个null pointer:
int *ip = new (nothrow) int;
if( ip ) { // 条件几乎总是 true
// . . .
}
else {
// 几乎不会执行到这里
}
然而,这样做又重新带回了new的老旧语义所牵涉的问题,连带着那丑恶语法引起的伤害。最好避免使用这种笨拙的向后兼容的诡异方法,直接采用能抛出异常的new进行设计和编码。
运行期系统还自动处理了分配失败时的一种特别恶劣的问题。回忆以前所提到过的,new operator实际上确立了两个函数调用:一个对operator new函数的调用,用以分配存储空间;接着是唤起构造函数,用以对分配的空间进行初始化:
Thing *tp = new Thing( arg );
如果我们捕获了一个bad_alloc exception,我们就知道出现了内存分配错误,但错误出现在哪里呢?错误可能发生在为Thing进行的存储空间原始分配动作中,也可能发生在Thing的构造函数中。在第一种情况下,我们并没有需要进行去配的内存,因为tp尚未被设为任何东西。在第二种情况下,我们就应该将tp所指向的(尚未被初始化的)内存返回给heap。然而,要我们确定是哪一种情况是很困难甚至不可能的。
幸运的是,运行期系统会为我们处理这种情况。如果为Thing进行的存储空间原始分配动作成功了,而Thing constructor失败并抛出任何异常,那么运行期系统会调用适当的operator delete(见Gotcha条款62)来归还(已分配但尚未被初始化的)存储空间。