• C++异常处理


    1.传统的C语言错误处理方法



        (1)在函数中返回一个错误信息。比如Linux经常返回-1代表错误。
        也可以设定一个全局的变量,比如errno
        (2)用信号函数signal和raise捕捉信号
        (3)用setjmp和longjmp两个非局部跳转函数,这种方法很困难,耦合度也很高,因为它和goto不一样,他会跳转到其他地方,而不是局部地点。
        以上三种方法都有自己的问题。第一个显得代码过于冗长,第二个信号处理标准不一致就会出很大问题,第三个问题更大,因为它会跳到栈区的其他地方,不会自动调用析构函数,行为危险,结果未知。


    2.C++的异常处理方式


    2.1 try-throw-catch结构


       try代码块负责运行正常代码,在代码中的错误会通过throw抛出,catch负责接受抛出的错误,做出相应的处理。

    (1)throw:抛出异常

    1. #include
    2. #include
    3. //抛出一个异常
    4. //设置一个错误类
    5. class MyError{
    6. private:
    7. const char* const data;
    8. public:
    9. MyError(const char* const msg = 0):data(msg){}//构造函数
    10. };
    11. void f()
    12. {
    13. throw MyError("something bad happened");
    14. }
    15. int main()
    16. {
    17. f();
    18. }
    19. //f()函数抛出一个错误,生成了MyError对象,这是程序创建的一个对象的拷贝,f()函数返>回了这个对象。

    2.2.捕获异常 catch


    catch(type value)

    {

            some code       

    }


    2.3.try块


    在try块中编写的代码,所产生的错误都会被根据程序员所设计的throw所抛出,程序会返回到错误开始的地方,如果不对错误做出处理,程序到此就结束了。
        try的使用方法为
        try{
            代码
        }


    2.4异常处理器


        处理抛出的异常的地方就叫异常处理器,他会接住throw抛出的异常,异常处理对继承和多态同样有效

    使用方法:
        try{
            代码
        }
        catch(类型1 id1)
        {
            处理方法1
        }
        catch(类型2 id2)
        {
            处理方法2
        }

        ……
        异常抛出后,会一一匹配catch,如果类型符合,就能够执行catch块的语句

        异常被抛出以后,会按照顺序匹配异常处理器,一旦找到能够匹配的,就会开始处理,不会继续匹配下一个,即使下面的更加合适。
        匹配一个异常,并不要求异常和处理器之间完全相关,一个对象或者是指向派生类对象的引用都会与其基类处理器匹配。如果不是引用或者指针,而是派生类对象和基类相匹配,
        则会发生截断,派生类对象会被截断,与基类相匹配。

    1. #include
    2. class X{
    3. public:
    4. class Trouble{};
    5. class Small:public Trouble{};
    6. class Big:public Trouble{};
    7. void fun(){throw Big();}
    8. };
    9. int main(){
    10. X x;
    11. try{
    12. x.fun();
    13. }
    14. catch(X::Trouble&)
    15. {
    16. std::cout << "捕获到Trouble"<
    17. }
    18. catch(X::Small&)
    19. {
    20. std::cout << "捕获到Small"<
    21. }
    22. catch(X::Big&)
    23. {
    24. std::cout << "捕获到Big"<
    25. }
    26. return 0;
    27. }

    输出:捕获到Trouble

    Small,Big都是Trouble的派生类,抛出Big异常处理的时候,却被第一个异常处理器捕获,不会被第三个捕获。

    所以我们在处理异常的时候,可以把基类处理器放在最后面捕获一些未定位的错误。
        注意:异常匹配过程不会发生类型转化,以下面的程序为例子

    定义了两个类,Except1和Except2,其中Except2定义了一个自动转换类型的函数。

    在抛出异常后,异常处理器接受到抛出的错误类型,会匹配到Except1而不是Except2,它不会自动把Except1转化为Except2。

    1. #include
    2. using std::cout;
    3. class Except1{};
    4. class Except2{
    5. public:
    6. Except2(const Except1&){}//类型转换,将Except1转换为其他类型
    7. };
    8. void fun()
    9. {
    10. throw Except1();//抛出一个异常,类型是Except1
    11. }
    12. int main()
    13. {
    14. try{
    15. fun();
    16. }
    17. catch(Except2&)
    18. {
    19. cout << "inside catch(except2)\n";
    20. }
    21. catch(Except1&)
    22. {
    23. cout << "inside catch(except1)\n";
    24. }
    25. return 0;
    26. }

    运行结过将会是:inside catch(except1)


    2.4.1捕获所有的异常


        catch(…)
        {

            some code
        }

        这个表示捕获所有的异常,但是不会接受任何参数,可以把它放在异常处理器的最后面。


    2.4.2 不捕获异常


        抛出的异常不能被忽略,需要被捕获,否者就会出错。
        如果没有异常处理器可以捕获抛出的异常,则会进行下面的处理
        (1)terminate()函数
        如果没有可以匹配异常的异常处理器,则会调用这个函数,这个函数调用了C语言标准库中的abort()函数,程序不会被正常终止,不会调用析构函数
        除了异常不会被捕获会调用terminate()函数,还有另外两种情况会调用这个函数
        第一种情况:局部对象的析构函数抛出异常
        第二种情况:静态对象的构造函数或者析构函数抛出异常
        (2)set_terminate()函数
        程序员可以使用set_terminate定义自己的terminate()函数,但是在调用结束后,仍然会调用默认的terminate()函数。
        使用方法如下。

    1. #include
    2. #include
    3. using namespace std;
    4. void terminator()
    5. {
    6. cout << "terminator 回来了\n";
    7. }
    8. //设置一个函数指针,指向set_terminator
    9. void (*Myterminator)(void) = set_terminate(terminator);
    10. class Botch
    11. {
    12. public:
    13. class Fruit{};
    14. void fun(){
    15. cout << "Botch::f()"<
    16. throw Botch();//抛出Botch类型的错误
    17. }
    18. ~Botch(){throw 'c';//抛出一个char类型的错误}
    19. };
    20. int main()
    21. {
    22. try{
    23. Botch b;
    24. b.fun();
    25. }
    26. catch(...)
    27. {
    28. cout << "inside catch(...)" << endl;
    29. }
    30. return 0;
    31. }

    set_terminate函数是一个这样的函数 void set_terminate(void(*func)(void));
    其中func是自己定义的
    析构函数抛出异常,catch获取了异常,先调用了自己定义的set_terminate()。但是运行结果和我们预料的不一致。

    输出:
    Botch::f()
    terminator 回来了
    Aborted (core dumped
    )

    第一次调用fun函数的时候,确实调用了自定义的terminate函数,但是析构函数所产生的异常却仍然调用了默认的terminate函数,终止了程序。

    说明,析构函数抛出的异常,只会调用默认的terminate函数。我们在编写代码的时候,应该避免让构造函数抛出异常。


    二.清理


    2.1 构造函数抛出异常


    有时候异常发生了,我们需要对资源进行清理,比如在构造函数中抛出异常,由于构造函数并没有完成,异常发生后,它不会自动调用析构函数清理资源

    1. #include
    2. using namespace std;
    3. class Cat{
    4. public:
    5. Cat(){ cout << "cat()" <
    6. ~Cat(){ cout << "~cat()" << endl;}
    7. };
    8. class Dog{
    9. public:
    10. void* operator new(size_t size)
    11. {
    12. cout << "Dog模拟内存分配"<
    13. throw 44;
    14. }
    15. void operator delete(void* p)
    16. {
    17. cout << "DOg模拟删除内存"<
    18. ::operator delete(p);
    19. }
    20. };
    21. class UseResource{
    22. private:
    23. Dog* bp;
    24. Cat* op;
    25. public:
    26. UseResource(int count = 1)
    27. {
    28. cout << "UseResource()"<
    29. op = new Cat[count];
    30. bp = new Dog;
    31. }
    32. ~UseResource()
    33. {
    34. cout << "~useResource()"<
    35. delete [] op;
    36. delete bp;
    37. }
    38. };
    39. int main()
    40. {
    41. try{
    42. UseResource ur(3);
    43. }
    44. catch(int n)
    45. {
    46. cout << "n = "<< n << endl;
    47. cout << "inside hander"<
    48. }
    49. return 0;
    50. }

     运行结果:
    UseResource()
    cat()
    cat()
    cat()
    Dog模拟内存分配
    n = 44
    inside hander

    cat的构造函数被调用,Dog却在分配内存的时候抛出一个异常,导致UseResource的构造函数没有结束,所以最后的析构函数没有调用,cat分配的资源仍然没有被释放


    2.2管理资源的方法



        为了防止资源泄露,可以使用两种办法来来分配资源
        (1)在构造函数里捕获异常,用于释放资源
        (2)在对象的构造函数分配资源,并且在对象的析构函数中释放资源
      

    1. #include
    2. #include
    3. using namespace std;
    4. template<class T,int sz = 1>
    5. class PWrap
    6. {
    7. private:
    8. T* ptr;
    9. public:
    10. class RangeError{};//异常类
    11. PWrap()
    12. {
    13. ptr = new T[sz];
    14. cout << "PWrap constructor"<
    15. }
    16. ~PWrap()
    17. {
    18. delete [] ptr;
    19. cout << "PWrap destructor" << endl;
    20. }
    21. T& operator[](int i)
    22. {
    23. if(i >= 0 && i < sz)
    24. return ptr[i];
    25. throw RangeError();
    26. }
    27. };
    28. class Cat{
    29. public:
    30. Cat(){cout << "Cat()" << endl;}
    31. ~Cat(){cout << "~Cat()"<
    32. void g(){}
    33. };
    34. class Dog{
    35. public:
    36. void* operator new[](size_t)
    37. {
    38. {
    39. cout << "动态创建Dog对象"<
    40. throw 44;
    41. }
    42. void operator delete[](void* p)
    43. {
    44. cout << "删除分配的Dog空间"<
    45. ::operator delete[] (p);
    46. }
    47. };
    48. class UseResource{
    49. public:
    50. UseResource(){cout << "UseResource()" << endl;}
    51. ~UseResource(){cout << "~UseResource()"<
    52. void f(){cats[1].g();}
    53. private:
    54. PWrap3> cats;
    55. PWrap dog;
    56. };
    57. int main()
    58. {
    59. try{
    60. UseResource ur;
    61. }
    62. catch(int)
    63. {
    64. cout << "inside handler" << endl;
    65. }
    66. catch(...)
    67. {
    68. cout << "inside catch(...)" << endl;
    69. }
    70. return 0;
    71. }


    这种方法的好处在于在创建UseResource对象之前,指针类就已经被创建了,所需要创建的对象嵌入到了指针对象中。当Dog出现异常时,cat的析构函数被调用了,就不会有
    内存泄露。
    输出:
    Cat()
    Cat()
    Cat()
    PWrap constructor
    动态创建Dog对象
    ~Cat()
    ~Cat()
    ~Cat()
    PWrap destructor
    inside handler

    这样的方法实际上也用于智能指针。

    2.3 函数级的try块

    1. #include
    2. using namespace std;
    3. int main()try
    4. {
    5. throw "主函数抛出异常";
    6. }
    7. catch(const char* msg){
    8. cout << msg << endl;
    9. }

    在函数后面使用try,然后紧跟着函数使用catch接受抛出的异常,这种方法也可以用在类的内部,比如用在构造函数里面。


    3.标准异常



    3.1 异常类



    C++标准中规定了异常,程序员可以直接使用他们,在构造自己的异常类的时候可以用他们当作基类
    所有的标准异常都是从exception类派生出来,定义在头文件,exception类有两个派生类,一个是logic_error,另一个是runtime_error
    派生出来的这两个类,都有构造函数,使用了string,他们会把错误消息用string保存,我们能够通过函数what()获取到这个信息。
    而logic_error和runtime_error也派生出来了其他的异常类,可以查看相关资料


    3.2 异常规格说明



    有时候我们需要知道函数抛出的异常类型,这时我们可以遵循这个规格提示程序员。
    规格说明写在函数后面,用关键字throw标注。比如:
    void fun() throw(int,float,const char*)
    {
    }
    这说明这个函数如果抛出异常,会抛出这三种类型的异常
    void fun();意味这可能会抛出任何类型的异常
    void fun() throw();意味不会抛出任何类型的异常


    3.3 unexcepted()函数和set_unexcepted()函数



    (1)如果抛出的异常不在说明的列表中,则系统会自动调用unexcept()函数,而这个函数会调用之前提到的terminate()函数。
    (2)set_unexcepted()函数的原型是这样的: void set_unexcepted(void (*) (void))
    程序员可以用它调用自己设计的异常函数,而不必使用系统默认的unexcepted()函数。
    使用自己设定的unexcepted()函数,可以重新抛出一个异常,让下面的代码接受。
    如果依然没有可以匹配的异常处理器,则会发生两种情况:
    (a).如果异常规则列表中含有bad_exception类,怎会把异常转化为bad_exception对象,然后程序回到调用的位置重新开始匹配异常
    (b).如果什么都匹配不到,则调用默认的unexpected()函数。

    1. #include
    2. #include
    3. using namespace std;
    4. //定义两个异常类
    5. class A{};
    6. class B{};
    7. //定义一个自己的terminate
    8. void my_terminate()
    9. {
    10. cout << "my_terminate" << endl;
    11. }
    12. //定义自己的unexcept()
    13. void my_unexpectedA()
    14. {
    15. cout << "my_unexpectedA"<
    16. throw A();
    17. }
    18. void my_unexpected()
    19. {
    20. cout << "my_unexpected" << endl;
    21. throw ;
    22. }
    23. //定义几个函数
    24. void f1()
    25. {
    26. cout << "f1执行"<
    27. throw B();
    28. }
    29. void f2() throw(A)//标准说明要抛出异常A
    30. {
    31. cout << "f2执行"<
    32. f1();//调用抛出异常B的函数
    33. }
    34. void f3()throw(A,bad_exception)
    35. {
    36. cout << "f3执行"<
    37. f1();
    38. }
    39. int main()
    40. {
    41. set_terminate(my_terminate);
    42. set_unexpected(my_unexpectedA);
    43. try{
    44. cout << "第一个try"<
    45. f2();//调用f2()。抛出B错误,与异常标准列表不符合,则会调用自定义的unexcepted,然后抛出错误A,则错误被捕获
    46. }
    47. catch(A&)
    48. {
    49. cout << "catch A from f2()"<
    50. }
    51. set_unexpected(my_unexpected);
    52. try{
    53. f3();//抛出错误B,与列表不符合,但是列表包含了bad_exception类,因此转化为这个类,下面可以捕捉到这个错误
    54. }
    55. catch(bad_exception&)
    56. {
    57. cout << "catch bad_exception from f3"<
    58. }
    59. try{
    60. f2();//抛出的错误B无法捕获,因此调用默认的unexcepted()终止程序。
    61. }
    62. catch(...)
    63. {
    64. cout << "catch(...)"<
    65. }
    66. return 0;
    67. }


    输出:

    第一个try
    f2执行
    f1执行
    my_unexpectedA
    catch A from f2()
    f3执行
    f1执行
    my_unexpected
    catch bad_exception from f3
    f2执行
    f1执行
    my_unexpected
    my_terminate
    Aborted (core dumped)


    继承中使用标准异常说明
    派生类的异常列表需要比父类的列表更窄。

    写累了,这一段略过

  • 相关阅读:
    SpringBoot分布式框架
    主攻“量子计算+元宇宙”:NTT DATA于六个国家设立创新中心
    【机器学习】线性分类【上】广义线性模型
    2022百度之星初赛第三场-三个因子
    rerank来提升RAG的准确度的策略
    80211 TIM流量指示图 附C语言实现
    Java多关键词分级搜索实现
    window像mac一样使用快捷键(AutoHotkey + SharpKeys)
    备忘录模式
    AcWing.505 火柴排队(离散化&逆序对)
  • 原文地址:https://blog.csdn.net/weixin_42581560/article/details/127851121