当你写一个 catch
子句时,必须确定让异常通过何种方式传递到 catch
子句里。你可以有三个选择:
1.指针2.传值3.引用
通过指针
通过指针方式捕获异常在理论上这种方法的实现对于这个过程来说是效率最高的。因为在传递异常信息时,只有采用通过指针抛出异常的方法才能够做到不拷贝对象。比如:
class exception { ... }; // 来自标准 C++库(STL)
// 中的异常类层次
// (参见条款 12)
void someFunction()
{
static exception ex; // 异常对象
...
throw &ex; // 抛出一个指针,指向 ex
...
}
void doSomething()
{
try
{
someFunction(); // 抛出一个 exception*
}
catch (exception *ex)
{ // 捕获 exception*;
... // 没有对象被拷贝
}
}
为了能让程序正常运行,程序员定义异常对象时必须确保当程序控制权离开抛出指针的函数后,对象还能够继续生存。全局与静态对象都能够做到这一点,但是程序员很容易忘记这个约束。如果真是如此的话,他们会这样写代码:
void someFunction()
{
exception ex; // 局部异常对象;
// 当退出函数的生存空间时
// 这个对象将被释放。
...
throw &ex; // 抛出一个指针,指向已被释放的对象
...
}
这样做很不好,因为处理这个异常的 catch 子句接受到的指针,其指向的对象已经不再存在。
另一种抛出指针的方法是建立一个堆对象:
void someFunction()
{
...
throw new exception; // 抛出一个指针,指向一个在堆中建立的对象
...
}
这避免了捕获一个指向已被释放对象的指针的问题,但是 catch
子句的作者又面临一个令人头疼的问题:
他们是否应该删除他们接受的指针?如果是在堆中建立的异常对象,那他们必须删除它,否则会造成资源泄漏。如果不是在堆中建立的异常对象,他们绝对不能删除它,否则程序的行为将不可预测。该如何做呢
这是不可能知道的,所以你最好避开它。
而且,通过指针捕获异常也不符合 C++语言本身的规范。四个标准的异常bad_alloc
(当operator new
不能分配足够的内存时,被抛出),bad_cast
(当dynamic_cast
针对一个引用( reference
)操作失败时,被抛出),bad_typeid
(当dynamic_cast
对空指针进行操作时,被抛出)和 bad_exception
(用于 unexpected
异常)都不是指向对象的指针,所以你必须通过值或引用来捕获它们。
…
…
通过值
通过值捕获异常可以解决上述的问题,例如异常对象删除的问题和使用标准异常类型的问题。但是当它们被抛出时系统将对异常对象拷贝两次。而且它会产生 slicing problem,即派生类的异常对象被做为基类异常对象捕获时,那它的派生类行为就被切掉了(sliced off)。
比如:
class exception
{ // 如上,这是
public: // 一个标准异常类
virtual const char * what() noexcept;
// 返回异常的简短描述.
};
class runtime_error: //也来自标准 C++异常类
public exception { ... };
class Validation_error: // 客户自己加入个类
public runtime_error
{
public:
virtual const char * what() throw();
// 重新定义在异常类中
... //虚拟函数
}; //
void someFunction() // 抛出一个 validation
{ // 异常
...
if (a validation 测试失败)
{
throw Validation_error();
}
...
}
void doSomething()
{
try
{
someFunction(); // 抛出 validation异常
}
catch (exception ex) //捕获所有标准异常类或它的派生类
{
cerr << ex.what(); // 调用 exception::what(),
... // 而不是 Validation_error::what()
}
}
调 用 的 是 基 类 的 what
函 数 , 即 使 被 抛 出 的 异 常 对 象 是 runtime_error
或 Validation_error
类型并且它们已经重新定义了这个虚拟函数。这种 slicing 行为绝不是你所期望的。
通过引用
最后剩下方法就是通过引用捕获异常。通过引用捕获异常能使你避开上述所有问题。不像通过指针捕获异常,这种方法不会有对象删除的问题而且也能捕获标准异常类型。也不象通过值捕获异常,这种方法没有slicing problem
,而且异常对象只被拷贝一次。
通过引用改下如下:
void someFunction() // 抛出一个 validation
{ // 异常
...
if (a validation 测试失败)
{
throw Validation_error();
}
...
}
void doSomething()
{
try
{
someFunction(); // 抛出 validation异常
}
catch (exception& ex) //通过引用捕获异常
{
cerr << ex.what(); //现在调用的是 Validation_error::what()
}
}
这里没有对 throw
进行任何改变,仅仅改变了 catch
子句,给它加了一个&符号。然而这个微小的改变能造成了巨大的变化,因为 catch
块中的虚拟函数能够如我们所愿那样工作了:调用的 Validation_erro
函数是我们重新定义过的函数
总结
通过引用捕获异常,能避免为是否删除异常对象而烦恼,能够避开slicing异常对象,能够捕获标准异常类型;减少异常对象需要被拷贝的数目。