一、为什么需要异常?
1.程序不可能不出错
2.传统的错误处理机制存在缺陷
1)通过返回值表示错误
缺点:错误处理流程比较复杂,逐层判断,代码臃肿。
优点:函数调用路径中所有的局部对象(栈对象)都能被右花括号正确地析构,不会内存泄漏。
2)通过远程跳转处理错误
优点:不需要逐层判断,一步到位处理错误,代码精炼。
缺点:函数调用路径中的局部对象失去被析构机会,形成内存泄漏。
3.C++的异常结合两种传统错误处理的优点,同时规避了它们的缺省,既可以在形式上实现一步到位的错误处理,无需逐层判断返回值,同时又能保证所有局部对象得到正确的析构。
二、异常语法
1.异常抛出:throw 异常对象;
1)可以抛出基本类型的对象
throw -1;
throw "打开文件失败!";
2)可以抛出类类型的对象
FileException ex ("打开文件失败!");
throw ex;
throw FileException ("打开文件失败!");
3)不要抛出局部对象的指针
FileException ex ("打开文件失败!");
throw &ex; // 不要!
2.异常捕获:
try {
可能引发异常的操作;
}
catch (异常类型1& ex) {
针对异常类型1的处理;
}
catch (异常类型2& ex) {
针对异常类型2的处理;
}
...
catch (...) {
针对其它异常的处理;
}
3.catch子句根据异常对象的类型自上至下顺序匹配,而非最优匹配,因此对子类类型异常的捕获不要放在对基类类型异常的捕获后面,否则前者将被后者提前截获。
4.建议在catch子句中使用引用接收异常对象,避免拷贝构造的性能开销,同时可以减少引发新异常的风险。
5.可以被throw子句抛出的对象类型,必须支持深拷贝,否则所抛出异常对象将和安全区中的副本形成内存耦合,可能在后续操作中引发未定义的后果。
三、异常说明
1.无论是全局函数还是类的成员函数,可以在其原型中增加异常说明,意在说明该函数所可能抛出的异常类型。
返回类型 函数名 (形参表) [const] throw (异常类型表) { … }
void foo (string const& file, string const& size, int x, int y)
throw (FileException, MemoryException,
DivByZeroException) {
...
}
2.函数的异常说明是一种承诺,即表示该函数所抛出异常不会超出所说明的范围。如果该函数真的抛出了异常说明以外的异常类型,该异常无法被此函数的调用者所捕获,而是继续向上层抛出,直至被系统捕获,中止进程。
3.函数异常说明的两种极端形式
1)不写异常说明,表示可以抛出任何异常;
2)空异常说明,throw (),表示不会抛出任何异常。
4.如果函数的声明和定义分开书写,一定要保证其异常说明严格一致,但异常类型的顺序无所谓。
5.如果基类中的虚函数带有异常说明,那么该函数在子类中的覆盖版本不能说明比基类版本抛出更多的异常,否则将因为放松throw限定而导致编译失败。
四、构造函数和析构函数中的异常
1.构造函数可以抛出异常,而且某些情况下还必须抛出异常。构造函数抛出异常,对象将会被不完整构造,这样的对象其析构函数永远不会被执行。因此在构造函数抛出异常之前,就需要手动销毁所有在异常产生之前动态分配的资源,除非使用了智能指针(auto_ptr)。
2.析构函数最好不要抛出异常
class A {
~A (void) {
...
throw ...;
...
}
}
try {
A a;
// ...
}
catch (exception& ex) {
...
}
在两种情况下,类的析构函数会被调用:
1)正常销毁对象,离开作用域或显式delete;
2)在异常传递的堆栈辗转开解过程中,由异常处理系统负责销毁对象;
对于第二种情况,异常正处于激活状态,而析构函数又抛出了异常,并试图将流程转移至析构函数之外,这时C++将通过std::terminate()函数,令进程终止,以防止因为堆栈辗转开解过程的无限递归导致栈空间溢出。
void foo (void) {
...
throw 1;
...
}
class A {
~A (void) {
try {
foo ();
}
catch (...) {}
}
};
在析构函数中,执行任何可能引发异常的操作,都尽量把异常在析构函数内部消化掉,防止其从析构函数中被继续抛出。