1. C++的new和delete操作时的异常处理
今天,我们开始学习C++的new和delete操作时的异常处理。首先,我将介绍标准C++运行库对new和delete操作的支持。然后,介绍伴随着这些支持的异常。
1.1 New和Delete表达式
当写
B *p = new D;
这里,B和D是class类型,并且有构造和析构函数,编译器实际产生的代码大约是这样的:
B *p = operator new(sizeof(D));
D::D(p);
过程是:
l new操作接受D对象的大小(字节为单位)作为参数。
l new操作返回一块大小足以容纳一个D对象的内存的地址。
l D的缺省构造函数被调用。这个构造函数传入的this指针就是刚刚返回的内存地址。
l 最终结果:*p是个完整构造了的对象,静态类型是B,动态类型是D。
相似的,语句
delete p;
差不多被编译为
D::~D(p);
operator delete(p);
D的析构函数被调用,被传入的this指针是p;然后delete操作释放被分配的内存。
new操作和delete操作其实是函数。如果你没有提供自己的版本,编译器会使用标准C++运行库头文件<new>中申明的版本:
void *operator new(std::size_t);
void operator delete(void *);
和其它标准运行库函数不同,它们不在命名空间std内。
因为编译器隐含地调用这些函数,所以它必须知道如何寻找它们。如果编译器将它们放在特别的空间内(如命名空间std),你就无法申明自己的替代版本了。因此,编译器按绝对名字从里向外进行搜索。如果你没有申明自己的版本,编译器最终将找到在<new>中申明的全局版本。
这个头文件包含了8个new/delete函数:
//
// new and delete
//
void *operator new(std::size_t);
void delete(void *);
//
// array new and delete
//
void *operator new[](std::size_t);
void delete[](void *);
//
// placement new and delete
//
void *operator new(std::size_t, void *);
void operator delete[](void *, void *);
//
// placement array new and delete
//
void *operator new[](std::size_t, void *);
void operator delete[](void *, void *);
前两个我已经介绍了。接下来两个分配和释放数组对象,而最后四个根本不分配和释放任何东西!
1.2 数组new和数组delete
new[]操作被这样的表达式隐含调用:
B *p = new D[N];
编译器对此的实现是:
B *p = operator new[](sizeof(D) * N + _v);
for (std::size_t _i(0); _i < N; ++_i)
D::D(&p[_i]);
前一个例子分配和构造单个D对象,这个例子分配和构造一个有N个D对象的数组。注意,传给new[]操作的字节大小是sizeof(D)*N + _v,所有对象的总大小加_v。在这里, _v是数组分配时的额外开销。
如你所想,
delete[] p;
实现为:
for (std::size_t _i(_N_of(p)); _i > 0; --_i)
D::~D(&p[i-1]);
operator delete[](p);
这里,_N_of(p)是个假想词,它依赖于你的编译器在检测*p中的元素个数时的实现体系。
和p = new D[N]不同(它明确说明了*p包含N个元素),delete[] p没有在编译期明确说明*p元素个数。你的程序必须在运行期推算元素个数。C++标准没有强制规定推算的实现体系,而我所见过的编译器共有两种实现方法:
l 在*p前面的字节中保存元素个数。其存储空间来自于new[]操作时_v字节的额外开销。
l 由标准运行库维护一个私有的N对p的映射表。
1.3 Placement New 和 Placement Delete
关键字new可以接受参数:
p = new(arg1, arg2, arg3) D;
(C++标准称这样的表达式为 “new with placement”或“placement new”,我马上会简单地解释原因。)这些参数会被隐含地传给new操作函数:
p = operator new(sizeof(D), arg1, arg2, arg3);
注意,第一个参数仍然是要生成对象的字节数,其它参数总是跟在它后面。
标准运行库定义了一个new操作的特别重载版本,它接受一个额外参数:
void *operator new(std::size_t, void *);
这种形式的new操作被如下的语句隐含调用:
p = new(addr) D;
这里,addr是某些数据区的地址,并且类型兼容于void *。
addr传给这个特别的new操作,这个特别的new操作和其它new操作一样返回将被构造的内存的地址,但不需要在自由内存区中再申请内存,它直接将addr返回:
void *operator new(std::size_t, void *addr)
{
return addr;
}
这个返回值然后被传给D::D作构造函数的this指针。
就这样,表达式
p = new(addr) D;
在addr所指的内存上构造了一个D对象,并将p赋为addr的值。这个方法让你有效地指定新生成对象的位置,所以被叫作“placement new”。
这个new的额外参数形式最初被设计为控制对象的位置的,但是C++标准委员会认识到这样的传参体系可以被用于任意用途而不仅是控制对象的位置。不幸的是,术语“placement”已经被根据最初目的而制订,并适用于所有new操作的额外参数的形式,即使它们根本不试图控制对象的位置。
所以,下面每个表达式都是placement new的一个例子:
new(addr) D; // calls operator new(std::size_t, void *)
new(addr, 3) D; // calls operator new(std::size_t, void *, int)
new(3) D; // calls operator new(std::size_t, int)
即使只有第一个形式是一般被用作控制对象位置的。
1.4 placement Delete
现在,只要认为 placement delete 是有用处的就行了。我肯定会讲述理由的,可能就在接下来的两篇内。
Placement new操作和placement delete操作必须成对出现。一般来说,每一个
void *operator new(std::size_t, p1, p2, p3, ..., pN);
都对应一个
void operator delete(void *, p1, p2, p3, ..., pN);
根据这条原则,标准运行库定义了
void operator delete(void *, void *);
以对应我刚讲的placement new操作。
1.5 数组New和数组Delete
基于对称,标准运行库也申明了placement new[]操作和placement delete[]操作:
void *operator new[](std::size_t, void *);
void operator delete[](void *, void *);
如你所料:placement new[]操作返回传入的地址,而placement delete[]操作的行为和我没有细述的placement delete操作行为几乎一样。
1.6 异常
现在,我们把这些new/delete和异常结合起来。再次考虑这条语句:
B *p = new D;
当其调用new操作而没有分配到足够内存时将发生什么?
在C++的黑暗年代(1994年及以前),对大部分编译器而言,new操作将返回NULL。这曾经是对C的malloc函数的合理扩展。幸运的是,我们现在生活在光明的年代,编译器强大了,类被设计得很漂亮,而编译运行库的new操作会抛异常了。
前面,我展示了在<new>中出现的8个函数的申明。那时,我做了些小手脚;这里是它们的完整形式:
namespace std
{
class bad_alloc
{
// ...
};
}
//
// new and delete
//
void *operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void *) throw();
//
// array new and delete
//
void *operator new[](std::size_t) throw(std::bad_alloc);
void operator delete[](void *) throw();
//
// placement new and delete
//
void *operator new(std::size_t, void *) throw();
void operator delete(void *, void *) throw();
//
// placement array new and delete
//
void *operator new[](std::size_t, void *) throw();
void operator delete[](void *, void *) throw();
在这些new操作族中,只有非placement形式的会抛异常(std::bad_alloc)。这个异常意味着内存耗尽状态,或其它内存分配失败。你可能奇怪为什么placement形式不抛异常;但记住,这些函数实际上根本不分配任何内存,所以它们没有分配问题可报告。
没有delete操作抛异常。这不奇怪,因为delete不分配新内存,只是将旧内存还回去。
1.7 异常消除
相对于会抛异常的new操作形式,<new>中也申明了不抛异常的重载版本:
namespace std
{
struct nothrow_t
{
// ...
};
extern const nothrow_t nothrow;
}
//
// new and delete
//
void *operator new(std::size_t, std::nothrow_t const &) throw();
void operator delete(void *, std::nothrow_t const &) throw();
//
// array new and delete
//
void *operator new[](std::size_t, std::nothrow_t const &) throw();
void operator delete[](void *, std::nothrow_t const &) throw();
这几个函数也被认为是new操作和delete操作的placement形式,因为它们也接收额外参数。和前面的控制对象分配位置的版本不同,这几个只是让你分辨出抛异常的new和不抛异常的new。
#include <iostream>
#include <new>
using namespace std;
int main()
{
int *p;
//
// 'new' that can throw
//
try
{
p = new int;
}
catch(bad_alloc &)
{
cout << "'new' threw an exception";
}
//
// 'new' that can't throw
//
try
{
p = new(nothrow) int;
}
catch(bad_alloc &)
{
cout << "this line should never appear";
}
//
return 0;
}
注意两个new表达式的重要不同之处:
p = new int;
在分配失败时抛std::bad_alloc,而
p = new(nothrow) int;
在分配失败时不抛异常,它返回NULL(就象malloc和C++黑暗年代的new)。
如果你不喜欢nothrow的语法,或你的编译器不支持,你可以这样达到同样效果:
#include <new>
//
// function template emulating 'new(std::nothrow)'
//
template<typename T>
T *new_nothrow() throw()
{
T *p;
try
{
p = new T;
}
catch(std::bad_alloc &)
{
p = NULL;
}
return p;
}
//
// example usage
//
int main()
{
int *p = new_nothrow<int>(); // equivalent to 'new(nothrow) int'
return 0;
}
这个模板函数与它效仿的new(nothrow)表达式同有一个潜在的异常安全漏洞。现在,我将它作为习题留给你去找出来。(恐怕没什么用的提示:和placement delete有关。)
1.8 小结
new和delete是怪兽。和typeid一起,它们是C++中仅有的会调用标准运行库中函数的关键字。即使程序除了main外不明确调用或定义任何函数,new和delete语句的出现就会使程序调用运行库。如我在这儿所示范的,调用运行库将经常可能抛异常或处理异常。
本篇的例程中的代码和注释是用于我对C++标准的解释的。不幸的是,如我以前所说,Microsoft的Visual C++经常不遵守C++标准。在下一篇中,我将揭示Visual C++的运行库对new和delete的支持在什么地方背离了C++标准。我将特别注意在对异常的支持上的背离,并且将展示怎么绕过它们。