• 第9课 C++异常处理机制


    一. 回顾C++异常机制

    (一)概述

        1. 异常处理是C++的一项语言机制,用于在程序中处理异常事件(也被称为导常对象)。

        2. 异常事件发生时,使用throw关键字抛出异常表达,抛出点称为异常出现点,由操作系统为程序设置当前异常对象。然后执行程序当前异常处理代码块。

        3. 在包含异常出现点的最内层try块,依次匹配catch语句中的异常对象若匹配成功,则执行catch块内的异常处理语句,然后接着执行try…catch…块之后的代码

        4.如果在当前try…catch…块内找不到匹配该异常对象的catch语句,则由更外层的try…catch…块来处理该异常。如果当前函数的所有try…catch…都不匹配该异常,则递归回退到调用栈的上一层去处理该异常。如果一直退出main()函数都不能处理该异常,则调用系统函数terminate()来终止程序。

    (二)异常对象

      1. 异常对象是一种特殊的对象。编译器依据异常抛出表达式构造异常对象(即异常对象总是被拷贝)。对象的类型是由表达式所表示对象的静态编译类型决定的。如Parent& rObj = Child; throw rObj;时会抛出Parent类型的异常对象。

      2. 异常对象存放在内存特殊位置,该位置既不是栈也不是堆,在Windows中是放在线程信息TIB中该对象由异常机制负责创建和释放!g++和vc下存储区域处理略有差异)。

      3. 异常对象不同于函数的局部对象,局部对象在函数调用结束后就被自动销毁,而异常对象将驻留在所有可能激活的catch语句都能访问到的内存空间中。当异常对象与catch语句成功匹配后,在该catch语句的结束处被自动析构

      4.在函数中返回局部变量的指针或引用几乎肯定会造成错误。同理,在throw语句中抛出局部变量的指针或引用也几乎是错误的。

    【编程实验】捕获异常对象(按值、按引用和按指针)

    #include <iostream>
    #include <string>
    using namespace std;
    
    class MyException
    {
    public:
        MyException() { cout << "MyException():" << this << endl; }
        MyException(const MyException&) { cout << "MyException(const MyException&):" << this << endl; }
    
        ~MyException() { cout << "~MyException():" << this << endl; }
    
        void what() { cout << "MyException: this = " << this << endl; }
    };
    
    class MyChildExcept : public MyException
    {
    public:
        MyChildExcept() { cout << "MyChildExcept():" << this << endl; }
        MyChildExcept(const MyChildExcept&) { cout << "MyChildExcept(const MyChildExcept&):" << this << endl; }
    
        ~MyChildExcept() { cout << "~MyChildExcept():" << this << endl; }
    
        void what() { cout << "MyChildExcept: this = " << this << endl; }
    };
    
    void func_local()
    {
        // throw 局部对象
        MyException localEx;
        throw localEx;   //尽管localEx是个局部对象,但这里会将其复制构造出一个异常对象,并存储在TIB中。而不是真正的将局部对象抛出去!
    }
    
    void func_temp()
    {
        //throw 临时对象
        MyException();       //临时对象1
        throw MyException(); //编译器会将这个临时对象直接存储在线程TIB中,成为异常对象(注意与临时对象1存储位置一般相距较远!)
    }
    
    void func_ptr()
    {
        //throw 指针
        throw new MyException(); //注意:异常对象是复制的堆对象而来的指针(存在内存泄漏风险!!!)
    }
    
    void func_again()
    {
        MyChildExcept child;
        MyException& re = child; //注意抛出的是re的静态类型的异常对象,即MyException,而不是MyChildExcept;
        throw re;
    }
    
    int main()
    {
        cout << "----------------------------------catch by value------------------------------" << endl;
        //按值捕获
        try {
            func_local();        //throw MyExecption()
        }
        catch (MyException e) {  //复制异常对象,须额外进行一次拷贝!
            cout << "catch (MyException e)" << endl;
            e.what();
        }
    
        cout << "--------------------------------catch by reference----------------------------" << endl;
        //按引用捕获
        try {
            func_temp();
        }
        catch (MyException& e) { //直接引用异常对象,无须拷贝
            cout << "catch (MyException& e)" << endl;
            e.what();
        }
    
        cout << "---------------------------------catch by pointer-----------------------------" << endl;
        //按指针捕获
        try {
            func_ptr();
        }
        catch (MyException* e) { //按指针捕获(可能造成内存泄漏)
            cout << "catch (MyException* e)" << endl;
            e->what();
            delete e;  //释放堆对象,防止内存泄漏
        }
    
        cout << "------------------------------------throw again-------------------------------" << endl;
        //二次抛异常
        try {
            try {
                func_again();
            }
            catch (MyException& e) {
                e.what();
    
                //注意以下两种方式不同
                //1. 在throw后指定异常对象为e
                //throw e; //e会继续复制一份,并抛出复制的异常对象而e本身会被释放!
    
                //2.throw后不指定任何对象,只要是在catch中捕获的,一律抛出去。
                throw;    //此时,e本身再被抛出去。不会另外构造异常对象。
            }
        }
        catch (MyException& e) {
            e.what();
        }
    
        return 0;
    }
    
    
    /*输出结果(g++编译环境下的输出)
    ----------------------------------catch by value------------------------------
    MyException():0x61fe7f    //localEx对象
    MyException(const MyException&):0x29978f8   //throw时将localEx复制给异常对象
    ~MyException():0x61fe7f   //释放localEx
    MyException(const MyException&):0x61feaf    //将异常对象复制给catch中的e对象
    catch (MyException e)
    MyException: this = 0x61feaf
    ~MyException():0x61feaf      //释放catch中的e对象
    ~MyException():0x29978f8     //释放异常对象
    --------------------------------catch by reference----------------------------
    MyException():0x61fe7f   //创建临时对象1
    ~MyException():0x61fe7f  //释放临时对象1
    MyException():0x29978f8  //throw MyException()时,会将这个临时对象直接创建在TIB中,与临时对象1不在同一地址段中
    catch (MyException& e)   //按引用传递,e直接引用异常对象,少了一次拷贝
    MyException: this = 0x29978f8
    ~MyException():0x29978f8  //释放异常对象
    ---------------------------------catch by pointer-----------------------------
    MyException():0x299c638   //throw new Exception() ,先在堆中创建一个Exception对象,再将指针throw出去。
    catch (MyException* e)
    MyException: this = 0x299c638
    ~MyException():0x299c638   //手动调用delete后,释放堆中的Exception对象。
    ------------------------------------throw again-------------------------------
    MyException():0x61fe7b
    MyChildExcept():0x61fe7b
    MyException(const MyException&):0x29978f8  //异常对象,这里是re的静态类型,即MyException,而不是MyChildExcept!!!
    ~MyChildExcept():0x61fe7b
    ~MyException():0x61fe7b
    MyException: this = 0x29978f8  //内层catch到的异常对象
    MyException: this = 0x29978f8  //外层catch到的异常对象,注意与内层是同一对象
    ~MyException():0x29978f8   //释放异常对象
    */

    二、异常规格

    (一)C++0x与C++11异常规格声明方式的不同

      1. void func() throw() { ... } // throw()声明该函数不会产生异常(C++0x)

      2. void func() throw(int, double) { ... } //可能产生int或double类型异常(C++0x)

      3. void func() noexcept { ... } // noexcept声明该函数不会产生异常(C++11)

      4. void func() noexcept(常量表达式) { ... } //由表达式决定是否产生异常(C++11)

    (二)noexcept和throw()异常规格声明的区别

      1. 当函数后面加noexcept和throw()时均表示该函数不会产生异常。

      2. 当使用throw()这种传统风格声明时,如果函数抛出了异常,异常处理机制会进行栈回溯,寻找(一个或多个)catch语句来匹配捕获。如果没有匹配的类型,会调用std::unexcepted函数,但是std::unexcepted本身也可能抛出异常,如果它抛出的异常对当前的异常规格是有效的,异常传递和栈回溯会像以前那样继续进行。这意味着如果使用throw()来声明,编译器几乎没有机会做优化,甚至会产生的代码会很臃肿、庞大。因为:

        ①栈必须保存在回退表中;

        ②所有对象的析构函数必须被正确调用(按照对象构建相反的顺序析构对象)。

        ③编译器可能引入新的传播栅栏(propagation barriers)、引入新的异常表入口,使得异常处理的代码变得庞大。

        ④内联函数的异常规格可能无效。

        3. 当使用noexcept时,std::terminate()函数会被立即调用,而不是调用std::unexcepted()因此栈不回溯,这为编译器的优化提供了空间

        4. 总之,如果知道函数绝对不会抛出任何异常,应该使用noexcept,而不是throw() 。

    三、noexcept关键字

    (一)noexcept异常规格语法

      1. noexcept()声明函数不会抛出任何异常。(注意throw()声明不抛出动态异常时,会保证进行栈回溯,可能调用std::unexcepted)。

      2. noexcept(true)、noexcept(false)前者与noexcept()等价,后者表示函数可能抛出异常。

      3. noexcept(表达式):其中的表达式是可按语境转换为bool类型的常量表达式。若表达式求值为true,则声明函数为不抛出任何异常。若为false则表示函数可能抛出异常。

    (二)noexcept在函数指针、虚函数中的使用规则

      1. noexcept与函数指针:

        (1)规则1:如果为函数指针显式声明了noexcept规格,则该指针只能指向带有noexcept的同种规格函数。

        (2)规则2:如果未声明函数指针的异常规则(即隐式说明可能抛异常),则该指针可以指向任何函数(即带noexcept或不带noexcept函数)。

      2. noexcept与虚函数:

        (1)如果基类虚函数声明不抛异常,则子类也必须做出相同的承诺。即子类也须带上noexcept。

        (2)如果基类虚函数声明可能抛异常,则子类可以抛异常,也可以不抛异常。

     (三)注意事项

      1. noexcept声明是函数接口的组成部分,这意味着调用方可能会对它有依赖。(但不能依靠noexcept来构成函数的重载)

      2. 相对于不带noexcept声明的函数,带有noexcept声明的函数有更多机会得到优化。但大多数函数是异常中立的,即不具备noexcept性质。

      3. 不抛异常的函数,允许调用潜在抛出异常的函数。异常如果没有被阻止传播,最终会调用std::terminate来终止程序。

      4. noexcept对于移动操作、swap、内存释放函数和析构函数最有价值默认地,operator delete、operator delete[]和析构函数都隐式具备了noexcept性质,但可以重新显式指定为可能抛出异常。

    【编程实验】noexcept测试

    #include <iostream>
    #include <type_traits>
    using namespace std;
    
    //以下两个函数不能构成重载
    //void func(int) noexcept(true){}
    //void func(int) noexcept(false){}
    
    //1. 异常规格与函数指针
    void func1(int) noexcept(true) {};        //不抛出异常
    void func2(int) noexcept(false) {}; //可能抛出异常
    void func3(int){}
    
    //2. noexcept与虚函数
    class Parent
    {
    public:
        virtual void func1() noexcept {}      //不抛异常
        virtual void func2() noexcept(false) {} //可能抛异常
    };
    
    class Child : public Parent
    {
    public:
        //基类承诺不抛异常,则子类也必须承诺!
        //void func1() {};  //error, Parent::func1() 承诺不抛异常了
    
        //基类声明为可能抛异常,则子类可以抛,也可以不抛异常
        void func2() noexcept(true) {} //ok, 子类不抛异常
        //void func2() {};     //ok,子类可以抛异常
    };
    
    //3. 有条件的noexcept
    //3.1 Swap:交互两个数组的元素。
    //只要交互元素时不抛异常,但交换整个数组就不抛异常。因此Swap函数是否为noexcept取决于交互元素
    template<typename T, size_t N>
    void Swap(T(&a)[N], T(&b)[N]) noexcept(noexcept(std::swap(*a, *b))) //*a、*b为首元素
    {
        for (int i = 0; i < N; ++i) {
            std::swap(a[i], b[i]);
        }
    }
    template<typename T, size_t N>
    void printArray(T(&a)[N])
    {
        for (int i = 0; i < N; ++i) {
            cout << a[i] << " ";
        }
        cout << endl;
    }
    
    //3.2 func
    //func_destructor函数抛不抛异常取决于T的析构函数。如果T析构会抛异常则func_destructor也会抛异常,反之不抛异常。
    template<typename T>
    void func_destructor(const T&) noexcept(noexcept((std::declval<T>().~T()))) {} 
    
    struct CA
    {
        ~CA(){ throw 1; } //注意析构函数默认为noexcept(true)
    };
    
    struct CB
    {
        ~CB()noexcept(false){ throw 2; }
    };
    
    struct CC
    {
        CB b;
    };
    
    //3.3 pair
    template<class T1, class T2>
    class MyPair
    {
        T1 first;
        T2 second;
    public:
    
        //swap函数是否为noexcept,取决于交互first和second的过程是否为noexcept
        void swap(MyPair& p) noexcept(noexcept(std::swap(first, p.first)) &&
                                      noexcept(std::swap(second, p.second)))
        {
        }
    };
    
    //4. noexcept
    void Throw() { throw 1; }        //可能抛异常:异常会被向外层传递出去
    void NoBlockThrow() { Throw(); } //可能抛异常:异常会被向外层传递出去
    void BlockThrow() noexcept { Throw(); } //不抛异常,但此函数实际会抛异常,异常会阻止传递,程序中止
    
    int main()
    {
        //1. noexcept与函数指针
        //1.1规则1:
        void(*pf1)(int) noexcept = func1; //正确
        //void(*pf2)(int) noexcept = func2; //error;异常规格不同!
        //void(*pf3)(int) noexcept = func3; //error,带noexcept的指针,只能指向带同种noexcept规格的函数
        
        //1.2 规则2:
        void(*pf4)(int) = func1;  //or,pf3未带noexcept,可以指向任何函数
        void(*pf5)(int) = func2;  //ok
        void(*pf6)(int) = func3;  //ok
    
        //2. noexcept与虚函数(见Child类)
    
        //3. 有条件的noexcept
    
        //3.1 交换数组
        int a[] = { 1,2,3,4,5 };
        int b[] = { 6,7,8,9,10 };
    
        Swap(a, b); //Swap函数是否为noexcept,取决于std::swap(*a,*b)函数是否为noexcept
        printArray(a);
        printArray(b);
    
        //3.2 析构函数
        try {
            CB temp;
            func_destructor(temp); //分别测试CA, CB, CC类。此处,由于CB析构函数为可能抛出异常,因此
                                   //抛出会被继续抛出而被catch(...)语句捕获,程序不会被中止
        }
        catch (...) {
            cout << "catch destructor exception" << endl;
        }
    
        //4. noexcept与异常传递测试
        try {
            Throw(); //这里可增加测试NoBlockThrow、BlockThrow函数
        }catch (...) {
            cout <<"catch exception..." << endl;
        }
        
        return 0;
    }
    /*输出结果:
    6 7 8 9 10
    1 2 3 4 5
    catch destructor exception
    catch exception...
    */
  • 相关阅读:
    《一个程序员的奋斗史》猜“封面+页数”结果揭晓!!
    软中断小结
    【嵌入式Linux学习七步曲之第五篇 Linux内核及驱动编程】Linux内核抢占实现机制分析
    《一个程序员的奋斗史》猜“封面+页数”结果揭晓!!
    深入理解linux内核自旋锁
    Linux 2.6 内核定时器
    在用户空间发生中断时,上下文切换的过程
    Linux 中断总结
    Linux内核抢占实现机制分析
    寒假Day23:Git初步创建版本库
  • 原文地址:https://www.cnblogs.com/5iedu/p/11270922.html
Copyright © 2020-2023  润新知