异常结构

如果程序的某部分产生一个无法处理的问题时,就需要用到异常处理,检测出异常的部分(发出方)应该发出某种信号(异常)表示程序遇到故障无法继续下去,发出方发出异常就完成自己的工作,无需关注故障在何处解决,如何解决。
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函数体之后 // 立即执行此处代码。 }

  1. <a name="ZBHYU"></a>
  2. # 异常处理
  3. 假设throw引发了一个异常,异常处理过程如下:
  4. ```cpp
  5. void func1(){
  6. try{ // try1
  7. func2();
  8. ......; // 代码code11
  9. }
  10. catch(Exception1 e){
  11. ......; // 代码code12
  12. }
  13. ......; // 代码code13
  14. }
  15. void func2(){
  16. try{ // try21
  17. try { // try22
  18. ......; // 代码code21
  19. throw ExceptionX("XXX"); // 引发异常,这里的ExceptionX仅作示范,可以是任何异常
  20. ......; // 代码code22
  21. }
  22. catch(Exception e){
  23. ......; // 代码code23
  24. }
  25. ......; // 代码code24
  26. }
  27. catch(Exception2 e){
  28. ......; // 代码code25
  29. }
  30. ......; // 代码code26
  31. }
  32. int main(){
  33. try { // tryMain
  34. func1(); //
  35. ......; // 代码code01
  36. }
  37. catch(Exception e){
  38. ......; // 代码code02
  39. }
  40. ......; // 代码code03
  41. }
  42. // 假设以上代码中,只引发了func2中的那个异常。当引发该异常时:
  43. //
  44. // 若在try22中捕获,则执行代码为:(异常引发以后的代码)
  45. // code23 -> code24 -> code26 -> code11 -> code13 -> code01 -> code03
  46. //
  47. // 若在try21中捕获,则执行代码为:
  48. // code25 -> code26 -> ......
  49. //
  50. // 若在try1中捕获,则执行代码为:
  51. // code12 -> code13 -> ......
  52. //
  53. // 若在tryMain中捕获,则执行代码为:
  54. // code02 -> code03
  55. //
  56. // 若没有被任何try catch捕获,则执行代码为:
  57. // terminate(); // 系统库函数,行为和平台相关,一般是让程序非正常退出。
  58. //
  59. // 上述过程叫做栈展开过程。

标准异常exception

异常对象如果是:

  • 类类型时,必须有以下函数:
    • public的析构函数。
    • public的拷贝构造函数。
    • public的移动构造函数。
  • 数组、函数类型时:
    • 转换成指针类型。

异常对象位于编译器管理的空间,因为要确保调用哪个catch都能访问到它,处理完毕之后销毁。

  1. throw runtime_error("god damn it"); //抛出一个异常对象
  2. BaseError* err = new DerivedError("god damn it");
  3. 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抛出。

这些异常的继承结构: C  _异常(exception) - 图1常见几个异常类的结构:

  1. class exception {
  2. public:
  3. exception() = default; // 默认构造函数,exception e; 默认初始化。
  4. exception(const exception&); // 拷贝构造函数。
  5. exception& operator=(const exception&); // 拷贝赋值运算符
  6. virtual ~exception();
  7. virtual const char* what() noexcept; // 返回用于初始化异常对象的信息。
  8. }
  9. class bad_cast : public exception {
  10. public:
  11. bad_cast() = default;
  12. }
  13. class bad_alloc : public exception {
  14. public:
  15. bad_alloc() = default;
  16. }
  17. class runtime_error : public exception {
  18. public:
  19. runtime_error() = delete; // !!注意没有默认构造函数,必须带参数构造。
  20. runtime_error(const char*); //
  21. runtime_error(string); //
  22. }
  23. class logic_error : public exception {
  24. public:
  25. logic_error() = delete; // !!注意没有默认构造函数,必须带参数构造。
  26. logic_error(const char*); //
  27. logic_error(string); //
  28. }

捕获异常catch

  1. catch(exception e){ // 异常声明,值传递(拷贝副本)
  2. // 假设传来的异常类型是DerivedException,继承自exception
  3. // e将只有传来对象的exception部分。发生截取情况
  4. ......
  5. }
  6. catch(exception &e){ // 异常声明,引用传递(同一个对象)
  7. // 引用、指针都不会发生上面的截取情况。
  8. // 最好是定义成引用或者指针类型。
  9. ......
  10. }
  11. catch(const exception &e){ // 这是最佳异常声明方式,const类型引用传递(同一个对象)
  12. // 引用、指针都不会发生上面的截取情况。
  13. ......
  14. throw; // 重新抛出异常,只能出现catch中,或者catch中调用的函数中。
  15. // 否则terminate,如果是引用、指针类型,可以将修改后的异常抛出。
  16. }
  17. catch(...){ // 捕获所有异常
  18. }

传入的异常对象与catch形参的转换规则:

  • 允许非const像const转换
  • 允许派生类向基类的转换
  • 允许数组转换成数组指针
  • 允许函数转换成函数指针
  • 除此之外的都不行:
    • 算术类型转换:int转double之类,都不允许,必须精确匹配。

根据这个转换规则,我们应该把派生类放前面,基类放后面,因为派生类可以匹配到基类。

在函数体内try catch无法捕获执行初始值列表产生的异常。下面就是专门针对构造函数的try catch,可以捕获初始值列表异常,但是无法捕获构造函数参数的异常,这应该在调用处来处理。

  1. Class::Class(int a, int b) try // 捕获初始值列表异常
  2. :m_a(a)
  3. ,m_b(b){
  4. }
  5. catch(const std::bad_alloc &e){
  6. }

定义新异常

  1. #include <iostream>
  2. #include <exception>
  3. class MyException : public Exception {
  4. public:
  5. const char* what() override {
  6. return "this is my exception";
  7. }
  8. };
  9. int main(){
  10. try{
  11. throw MyException();
  12. }
  13. catch(const Exception& e){ // 最好是用引用、形式,避免不必要的拷贝。
  14. std::cout << e.what() << std::endl;
  15. }
  16. return 0;
  17. }

不抛出说明noexcept

不抛出异常说明,有什么用?明确告诉外界,我这个函数对异常的态度有两个:

  • 1、我绝对不会产生异常,是安全可以放心使用的,不用为处理调用我可能产生的异常而做额外的工作;
  • 2、使用者不要考虑我产生的异常:在noexcept函数中抛出异常是必然会程序terminate。

总之可以让外界好配合,对程序有一定提升优化,如编译器。

  1. void recoup(int) noexcept; // 不会抛出异常,做了不抛出声明nothrowing,如果真抛出,则直接terminate
  2. void alloc(int); // 可能抛出异常
  3. void fuck() noexcept; // 在fuck的声明和定义中都必须加上。
  4. void fuck() noexcept{} // 在fuck的声明和定义中都必须加上。
  5. void fuck() throw(); // 这是老版的不抛出说明
  6. auto fuck() noexcept -> int{// 必须在尾置类型之前。
  7. }
  8. // 在typedef、using类型别名中不能出现
  9. void (*pf1)(int) noexcept = fuck; // 可以在函数指针声明和定义中出现。
  10. // fuck必须是noexcept否则报错
  11. class a{
  12. void fuck() const noexcept; // 必须在const引用限定符之后
  13. void fuck() noexcept final; // 必须在final之前
  14. void fuck() noexcept override; // 必须在override之前
  15. virtual void fuck() noexcept = 0; // 必须在虚函数=0之前
  16. virtual fuck() noexcept; // 父类的虚函数是noexcept,子类相应的必须显式noexcept
  17. }

上面的noexcept是一个说明符号,它也可以是一个运算符。

  1. noexcept(fuck(i)); // fuck函数是否不会抛出异常,是返回true,否返回false
  2. noexcept(e); // 当e函数和内部所有用到的函数都做了不抛出说明
  3. // 且e没有throw语句,返回true,否则false
  4. void f() noexcept(noexcept(g())); // f和g的异常说明一致