异常结构
如果程序的某部分产生一个无法处理的问题时,就需要用到异常处理,检测出异常的部分(发出方)应该发出某种信号(异常)表示程序遇到故障无法继续下去,发出方发出异常就完成自己的工作,无需关注故障在何处解决,如何解决。
C++异常处理包括:
- throw表达式:引发(raise)异常
- try语句块:处理异常
- exception异常类:异常的具体信息。 ```cpp
// C++基本的异常处理模块。
void func(){ try { ……; // 程序代码 throw runtime_error(“runtime_error”); // 引发runtime_error异常 // 类似return,不会再执行下面部分代码 ……; // 程序代码 } catch (Exception1 e1) { // 异常声明 ……; // 处理异常Exception1代码 } catch (Exception2 e2) { // 异常声明 ……; // 处理异常Exception2代码 } ……; // 程序代码,如果被上面任一catch命中,则执行为catch函数体之后 // 立即执行此处代码。 }
<a name="ZBHYU"></a># 异常处理假设throw引发了一个异常,异常处理过程如下:```cppvoid func1(){try{ // try1func2();......; // 代码code11}catch(Exception1 e){......; // 代码code12}......; // 代码code13}void func2(){try{ // try21try { // try22......; // 代码code21throw ExceptionX("XXX"); // 引发异常,这里的ExceptionX仅作示范,可以是任何异常......; // 代码code22}catch(Exception e){......; // 代码code23}......; // 代码code24}catch(Exception2 e){......; // 代码code25}......; // 代码code26}int main(){try { // tryMainfunc1(); //......; // 代码code01}catch(Exception e){......; // 代码code02}......; // 代码code03}// 假设以上代码中,只引发了func2中的那个异常。当引发该异常时://// 若在try22中捕获,则执行代码为:(异常引发以后的代码)// code23 -> code24 -> code26 -> code11 -> code13 -> code01 -> code03//// 若在try21中捕获,则执行代码为:// code25 -> code26 -> ......//// 若在try1中捕获,则执行代码为:// code12 -> code13 -> ......//// 若在tryMain中捕获,则执行代码为:// code02 -> code03//// 若没有被任何try catch捕获,则执行代码为:// terminate(); // 系统库函数,行为和平台相关,一般是让程序非正常退出。//// 上述过程叫做栈展开过程。
标准异常exception
异常对象如果是:
- 类类型时,必须有以下函数:
- public的析构函数。
- public的拷贝构造函数。
- public的移动构造函数。
- 数组、函数类型时:
- 转换成指针类型。
异常对象位于编译器管理的空间,因为要确保调用哪个catch都能访问到它,处理完毕之后销毁。
throw runtime_error("god damn it"); //抛出一个异常对象BaseError* err = new DerivedError("god damn it");throw *err; //抛出的是err的BaseError部分,DerivedError特有部分将被抛弃。
C++标准库定义了一组异常类,在4个头文件中:
- exception:所有异常父类,只报告异常发生,不会提供任何额外信息。
- bad_exception:处理无法预期的异常时非常有用。
- exception:最常见问题
- runtime_error:不能通过读取代码检测到的异常。
- range_error:生成结果超出有意义的值域范围
- overflow_error:计算上溢
- unerflow_error:计算下溢
- logic_error:逻辑错误,可以通过读取代码检测到的异常。
- domain_error:参数对应的结果值不存在
- invalid_argument:无效参数
- length_error:试图创建一个超出该类型最大长度的对象
- out_of_range:使用一个超出有效范围的值,如vector的下标访问。
- bad_alloc:new抛出。
- bad_cast:dynamic_cast抛出。
- bad_typeid:typeid抛出。
这些异常的继承结构:
常见几个异常类的结构:
class exception {public:exception() = default; // 默认构造函数,exception e; 默认初始化。exception(const exception&); // 拷贝构造函数。exception& operator=(const exception&); // 拷贝赋值运算符virtual ~exception();virtual const char* what() noexcept; // 返回用于初始化异常对象的信息。}class bad_cast : public exception {public:bad_cast() = default;}class bad_alloc : public exception {public:bad_alloc() = default;}class runtime_error : public exception {public:runtime_error() = delete; // !!注意没有默认构造函数,必须带参数构造。runtime_error(const char*); //runtime_error(string); //}class logic_error : public exception {public:logic_error() = delete; // !!注意没有默认构造函数,必须带参数构造。logic_error(const char*); //logic_error(string); //}
捕获异常catch
catch(exception e){ // 异常声明,值传递(拷贝副本)// 假设传来的异常类型是DerivedException,继承自exception// e将只有传来对象的exception部分。发生截取情况......}catch(exception &e){ // 异常声明,引用传递(同一个对象)// 引用、指针都不会发生上面的截取情况。// 最好是定义成引用或者指针类型。......}catch(const exception &e){ // 这是最佳异常声明方式,const类型引用传递(同一个对象)// 引用、指针都不会发生上面的截取情况。......throw; // 重新抛出异常,只能出现catch中,或者catch中调用的函数中。// 否则terminate,如果是引用、指针类型,可以将修改后的异常抛出。}catch(...){ // 捕获所有异常}
传入的异常对象与catch形参的转换规则:
- 允许非const像const转换
- 允许派生类向基类的转换
- 允许数组转换成数组指针
- 允许函数转换成函数指针
- 除此之外的都不行:
- 算术类型转换:int转double之类,都不允许,必须精确匹配。
根据这个转换规则,我们应该把派生类放前面,基类放后面,因为派生类可以匹配到基类。
在函数体内try catch无法捕获执行初始值列表产生的异常。下面就是专门针对构造函数的try catch,可以捕获初始值列表异常,但是无法捕获构造函数参数的异常,这应该在调用处来处理。
Class::Class(int a, int b) try // 捕获初始值列表异常:m_a(a),m_b(b){}catch(const std::bad_alloc &e){}
定义新异常
#include <iostream>#include <exception>class MyException : public Exception {public:const char* what() override {return "this is my exception";}};int main(){try{throw MyException();}catch(const Exception& e){ // 最好是用引用、形式,避免不必要的拷贝。std::cout << e.what() << std::endl;}return 0;}
不抛出说明noexcept
不抛出异常说明,有什么用?明确告诉外界,我这个函数对异常的态度有两个:
- 1、我绝对不会产生异常,是安全可以放心使用的,不用为处理调用我可能产生的异常而做额外的工作;
- 2、使用者不要考虑我产生的异常:在noexcept函数中抛出异常是必然会程序terminate。
总之可以让外界好配合,对程序有一定提升优化,如编译器。
void recoup(int) noexcept; // 不会抛出异常,做了不抛出声明nothrowing,如果真抛出,则直接terminatevoid alloc(int); // 可能抛出异常void fuck() noexcept; // 在fuck的声明和定义中都必须加上。void fuck() noexcept{} // 在fuck的声明和定义中都必须加上。void fuck() throw(); // 这是老版的不抛出说明auto fuck() noexcept -> int{// 必须在尾置类型之前。}// 在typedef、using类型别名中不能出现void (*pf1)(int) noexcept = fuck; // 可以在函数指针声明和定义中出现。// fuck必须是noexcept否则报错class a{void fuck() const noexcept; // 必须在const引用限定符之后void fuck() noexcept final; // 必须在final之前void fuck() noexcept override; // 必须在override之前virtual void fuck() noexcept = 0; // 必须在虚函数=0之前virtual fuck() noexcept; // 父类的虚函数是noexcept,子类相应的必须显式noexcept}
上面的noexcept是一个说明符号,它也可以是一个运算符。
noexcept(fuck(i)); // fuck函数是否不会抛出异常,是返回true,否返回falsenoexcept(e); // 当e函数和内部所有用到的函数都做了不抛出说明// 且e没有throw语句,返回true,否则falsevoid f() noexcept(noexcept(g())); // f和g的异常说明一致
