C++ 异常处理机制
try 和异常处理
“异常”指的是运行时期程序的异常状态,比如说数据库断开连接、用户输入类型错误等。异常处理经常用于检测运行时程序本身不能直接处理的问题,且这些问题导致程序无法正常运行。程序异常检测部分需要发送相关的信号,当程序由于某些异常无法正确运行的时候。进一步讲,检测部分需要在不知道不知道程序的那一部分可以处理这种异常条件下,来发射有关异常的信号。
C++中,异常检测分为3部分:
- throw expressions : 检测模块用于暗示这里存在不能处理的异常模块,a throw raises an exception
- try blocks : 异常已处理模块用来解决异常的。以
try
开始,跟随多个catch
块结束 - exception classes : 用来传递
throw
和catch
块直接发生的一些状态。
throw 表达式:
一般来说,如果程序的某一部分出现了不正常的部分,并且我们不好直接通过返回终止来停止该模块,则使用throw
表达式:
class Sales_item {};
Sales_item item1, item2;
if(item1.isbn() != item2.isbn()) {
throw runtime_error("Data must refer to same ISBN");
}
std::cout << item1 + item2 << std::endl;
如果第4行表达式为真,那么抛出std::runtime_error
这个异常,并终止当前函数的运行,把控制权交给异常处理模块。
try 模块:
基本格式:
try {
// program statements
} catch(/*exception-declaration*/) {
// handler-statement
} catch(/*exception-delaration*/) {
// handler-statement
} // ....
program statements和普通的C++模块一样,变量作用域等都类比于普通C++模块。在try
块内声明的局部变量等,在catch
内都不可见。
在复杂的系统中,可能会出多个try
块嵌套的情况,即一个try
调用一个包含try
的函数,以此类推。搜索处理异常的顺序与函数调用的顺序相反,即最里层的try
和catch
先处理产生的异常,如果不能处理,包含该层try
和catch
的函数终止,并由调用该函数的函数的异常处理模块进行处理,以此类推,直到合适的catch
处理异常。如果没有合适的catch
处理异常,那么系统会执行库函数terminate
。这至少可以保证程序不会跳过异常继续执行。
一般来说, 异常处理机制会导致程序的不正常结束,这会存在一些隐患,即使有很多异常处理技术来避免这些,要写出异常处理安全的代码还是很难的。
标准异常:
C++标准库给我们提供了一些常见的异常类型,一般我们需要的时候直接查阅即可。
举例:
#include <exception>
#include <iostream>
#include <string>
int main() {
try {
int a = 0;
std::cin >> a;
if(a > 10) {
throw std::overflow_error("stackoverflow!");
}
std::cout << a << std::endl;
} catch(std::string str) {
std::cout << str << std::endl;
}
return 0;
}
异常处理机制
抛出异常
当抛出异常的时候,throw
后面的语句将不在执行,程序的控制权将从throw
部分转到catch
部分。调用异常处理机制后,会有两个隐含的可能执行过程:
- 调用的函数可能会提前的结束
- 当执行一个异常处理过程时,沿着函数调用链过程中产生的对象会被销毁
因为throw
后面的语句不执行,throw
就相当于一个return
,throw
经常放在函数的最后的部分,或者作为函数条件块的一部分。
Stack Unwinding:
当有异常抛出的时候,当前函数的执行过程会挂起,程序会寻找匹配的catch
块。如果throw
出现在一个try
块内部,那么程序会检查与try
块匹配的catch
块,如果能找到匹配的catch
,就进行异常处理;否则,如果当前的try
嵌套在其他的try
内部,搜索会到外层的try
,如果最终找不到匹配的catch
,当前函数的执行过程会结束,搜索过程会向调用该函数的函数中搜索。
在调用函数过程抛出异常,搜索过程类似在函数内部抛出异常的过程,如果找到了就执行;找不到的话,调用过程会结束,搜索过程在执行调用操作的函数中搜索。
Stack Unwinding就是在不断的向外层嵌套函数搜索catch
的过程,直到找到匹配的catch
或者主函数的执行退出为止。
如果匹配的catch
找到了,那么就执行catch
解决异常,之后程序会回到与catch
相匹配的try
块中。但是,如果找不到合适的catch
,那么程序会调用terminate
函数,终止所有的程序运行。因此,不能解决的异常会终止程序的运行。
Objects Are Automatically Destroyed during Stack Unwinding:
在stack unwinding过程中,调用链的区块会可能会提前结束。一般来说,这些块会创建一些局部对象,这些局部对象会随着块的销毁而销毁。如果这些局部对象有析构函数,那么析构函数会自动执行。
如果异常出现在构造函数中,那么该对象可能只是局部构造。即使那些局部构造的对象,编译器也可以保证销毁他们。
Destructors and Exceptions:
析构函数中如果出现抛出异常,在异常后面释放资源的部分将不被执行。stack unwinding过程中,如果出现新的异常,那么整个程序会终止。因此,析构函数最好不要出现异常。标准库的析构函数不会有异常出现。
The Exception Object:
编译器使用一个抛出的表达式类型的复制对象来初始化一个exception object,因此,throw
中的表达式必须有完整的类型。更进一步说,如果表达式是一个类,那么这个类必须有可获得的构造和析构函数;如果表达式是数组或者函数,那么必须有已知相匹配的指针类型。
exception object的空间由编译器单独管理,这保证了一个异常可以被catch
捕获。异常对象在异常被完全解决后,自动销毁。对局部对象抛出异常会产生错误,因为异常解决的过程中,块会沿着调用链被销毁,同样的,块中创建的局部对象也会被销毁,此时对这些局部对象使用catch
已经没有意义。
当我们抛出异常时,静态的、编译时的类型决定了异常的类型。
捕获异常
catch
的参数列表类似于函数的参数列表。可以传值或者传引用,这和函数参数的用法一致,但是不能传右值。如果传递的是基类的类型,那么该类型可以被它的派生类类型初始化。
Finding a Matching Handler:
catch
匹配的原则不是找最合适的,而是寻找第一个匹配异常类型的。因此,要把列表的catch
中最特殊的放在列表的最前面。对与派生类异常的处理要放在基类异常处理的前面。
异常参数传递和函数参数传递有些不同,区别如下:
- 可以从非
const
转换到const
。抛出一个非const
类型的对象可以匹配到一个const
的参数 - 可以从派生类转换到基类
- 数组类型被转换到数组的指针,函数类型被转换到函数的指针
Rethrow:
一个catch
把自己的异常传递给另一个catch
处理的过程称为rethrow
。该过程的调用仅需要在catch
中声明一个不跟随任何表达式的throw
即可。这种空throw
只能在catch
块中出现,在其他地方出现会使程序调用terminate
而终止。
catch (my_error &eObj) {
// specifier is a reference type
eObj.status = errCodes::severeErr; // modifies the //exception object
throw; // the status member of the exception object is //severeErr
} catch (other_error eObj) { // specifier is a nonreference //type
eObj.status = errCodes::badErr;
// modifies the local copy only
throw; // the status member of the exception object is /unchanged
}
The Catch-All Handler:
捕获所有可能的异常。
void manip() {
try {
// actions that cause an exception to be thrown
} catch (...) {
// work to partially handle the exception
throw;
}
}
经常和rethow连用,他可以自己调用自己,直到所有的异常解决。如果和其他的catch
连用,捕获所有异常的操作必须在最后。因为他后面的所有捕获操作都不执行。
Function try Blocks and Constructors
function try block后跟随的catch
用于处理构造初始化过程中的异常,这种异常不能被一般的catch处理
。 给出如下的方法:
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il) try :
data(std::make_shared<std::vector<T>>(il)) {
/* empty body */
} catch(const std::bad_alloc &e) {
handle_out_of_memory(e);
}
这里只是提及一个思路,具体使用的时候查找有关手册即可。
使用noexcept显式声明
显式声明函数不会抛出异常可以简化编码的任务和优化编译器的任务。一般来说格式为:
void recoup(int) noexcept; // won’t throw
void alloc(int); // might throw
noexcept
必须出现在一个函数的所有的声明和有关的定义出现的地方,否则就不要使用该关键字。
noexcept的参数
noexcept
的可选参数必须是可以转换成bool
类型的。true
表示不会抛出异常,false
会抛出异常
void recoup(int) noexcept(true); // recoup won’t throw
void alloc(int) noexcept(false); // alloc can throw
noexcept作为操作符使用
noexcept
可以作为一个一元操作符使用。它返回一个右值bool
表达式来表明操作数是否可能抛出异常。
比如:
noexcept(recoup(i)) // true if calling recoup can’t throw, false otherwise
更一般的说:
noexcept(e)
返回true
,如果e
调用的所有函数都有不抛出异常的符号,并且e本身也没用throw
;否则返回false
。
异常声明的指针、虚函数和复制控制
定义的指向非异常函数的函数指针,只能指向非异常函数;而定义的指向异常的函数指针,可以指向非异常函数和异常函数,但是指向的非异常函数也可能抛出异常,即使该函数声明了非异常。
比如:
// both recoup and pf1 promise not to throw
void (*pf1)(int) noexcept = recoup;
// ok: recoup won’t throw; it doesn’t matter that pf2 might
void (*pf2)(int) = recoup;
pf1 = alloc; // error: alloc might throw but pf1 said it wouldn’t
pf2 = alloc; // ok: both pf2 and alloc might throw
类中的虚函数的异常会被子类的函数继承,即子类函数的异常特性继承基类函数的异常特性。
class Base {
public:
virtual double f1(double) noexcept; // doesn’t throw
virtual int f2() noexcept(false); // can throw
virtual void f3(); // can throw
};
class Derived : public Base {
public:
double f1(double); // error: Base::f1 promises not to throw
int f2() noexcept(false); // ok: same specification as Base::f2
void f3() noexcept; // ok: Derived f3 is more restrictive
};
基本的继承规则在例子中给出了。也就是说,父类中不能抛出异常的,子类必须要声明成不能抛出异常的,其他的可以自由转换。
自定义异常类的继承
自己定义的非标准异常类可以自定义继承,在这里不详细解释,具体需要时再查阅有关手册。