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

一. 回顾C++异常机制

(一)概述

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

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

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

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

(二)异常对象

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

  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...
*/

原文链接: https://www.cnblogs.com/5iedu/p/11270922.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

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

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/392748

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月3日 下午2:57
下一篇 2023年4月3日 下午2:58

相关推荐