C++:16---异常机制(throw、try、catch)

  • 1.概念:异常处理是一种允许两个独立开发的程序组件在程序执行时遇到不正常的情况相互通信的工具

  • 2.异常检测和异常处理的方式
    • throw表达式:程序遇到了错误或者无法处理的问题,使用throw引发异常
    • try、catch语句块:以关键字tyr开始,并以一个或多个catch子句结束。它们也被称为异常处理代码

一、throw

  • 1.概念:程序的异常检测部分使用throw表达式引发一个异常
  • 2.格式:
    • throw  表达式;
    • 表达式可以为:整型、浮点型、字符、字符串、类、函数......
  • 3.注意事项
    • 当执行throw时,跟在throw后面的语句将不再被执行。程序的执行权将转移到与之匹配的catch语句块中
    • 如果一条throw表达式解引用一个基类指针,而这个指针指向于派生类对象,则抛出的对象被切掉的一部分是基类部分中的。会在下面详细讲解

二、try、catch语句块

  • 1.格式
try{
  ...
}
catch(){
  ...
}
catch(){
  ...
}
  • 2.注意事项
    • try和catch都不可以省去花括号,尽管后面只有一条语句也不能省去
    • 在try和catch组合中,try最多只有一个,catch可以有多个
    • 嵌套:try和catch语句块中都可以再嵌套try、catch语句块组合
    • try中使用throw抛出一个异常时,跳转到参数类型与throw后面表达式类型相对应的catch语句块中,throw后面的语句将不再执行
    • 栈展开:下面介绍

三、catch的相关知识

catch的参数

  • ①若catch的参数为类对象,则:
    • 若参数为非引用类型,在catch语句块中实际上改变的是局部副本,不改变传入的异常对象本身。相反,如果参数为引用类型,则在语句块内改变参数,也就是改变对象本身
    • 如果catch的参数为基类类型,则我们可以使用派生类类型的异常对象对其进行初始化。如果是非引用类型,则异常对象将被切掉一部分,若是引用类型,则以常规的方式绑定到异常对象上。如果传入的参数与某个继承有关,最好将参数定义为引用类型
    • 重点:catch参数是基类类型,catch无法使用派生类特有的成员

catch的书写顺序

  • ①若多个catch与句之间存在着继承关系,则:
    • 继承链最低端的类放在前面,继承链最顶端的类放在后面

catch语句重新抛出

  • 概念:有时,一条单独的catch语句不能完整地处理某个异常,会将传递的异常继续传递给外层try、catch组合或者上一层的函数处理
  • 语法格式:throw;   //只是一条throw,不包含任何表达式
  • throw;只能出现在catch语句或者由catch语句直接或间接调用的函数之内
  • 如果catch参数是引用类型,在catch语句中改变参数值,下一条catch接受的是改变后的参数。代码如下
try
{
    try{
    }
    catch(A &a){
        a.data=100;
        throw;  //将异常抛出给外层处理,因为a为引用,因此抛出后a.data=100
    }
}
catch(A a){
    a.data=666;
    throw;      //将异常抛出给外层处理,因为a不是引用,因此抛出后a.data依然等于100
}

捕获所有异常

  • 概念:有时候,我们希望将所抛出的异常不论是什么类型,都将其捕获,但是又不知道其类型。为了解决这个问题,我们使用省略号作为异常参数声明
  • 格式:catch(...){}
  • 注意:catch(...)可以与其它catch组合使用,但是必须放在最后面,因为后面的catch永远都不会执行
  • 捕获所有异常通常与重新抛出配合使用,但不是必须
try
{
}
catch(...)
{
    //处理某些操作后
    throw;//重新抛出异常
}

四、构造函数的异常处理

  • 1.概念
    • 我们知道,在进入构造函数的函数体之前,我们要先执行初始化列表。但是如果try、catch语句块放在构造函数体内,初始化列表如果出现异常,函数体内的try语句块还未生效,所以无法捕获异常。为了解决这种情况,我们必须将构造函数写成函数try语句块,也称为函数测试体
    • 函数try语句块既能处理初始化列表,也能处理构造函数体
  • 2.格式
    • try跟在构造函数的值初始化列表的冒号之前,catch跟在构造函数后
class A
{
    char* stuName;
public:
    A(int len)try:stuName(new char[len])
    {
        if(len<=0)
            throw length_error("长度过低");    
    }
    catch(length_error error)
    {cout<<error.what()<<endl;}
}

五、栈展开

  • 概念try中throw抛出的异常,后面若没有相对应匹配的catch语句块,则将异常传递给外层try匹配的catch语句处理,如果还是找不到匹配的catch,则退出当前的函数,将异常传递给当前函数的外层函数继续寻找。(外层函数指调用此try、catch组合的所在函数的函数),若一直传到main函数,main函数也处理不了,则程序就会调用标准库函数terminate,此函数将终止程序的执行

演示案例

  • 下面的代码,若我们分别输入:
    • 输入0:inDate中将throw抛出的"value == 0"传递给main函数中的try语句,有相对应的catch匹配,执行printf("main::char*异常---%s\n", str);。
    • 输入-60:因为<-50,inDate函数里面的try语句抛出throw me;后面没有相对应的catch语句块相匹配,所以将异常传递到main函数中,有相对应的catch匹配,执行 printf("main::MyExcp异常---%s\n", m.getMyExcpStr());
    • 输入22:调用f函数,f函数中throw 3.13;抛出后在inDate中处理,inDate中没有catch语句可以处理,再传递给main函数处理,main函数也处理不了。程序最终调用标准库函数terminal终止程序执行
class MyExcp
{
    char srcArr[128];
public:
    MyExcp() { strcpy(srcArr, "this is myExcp class\n"); }
    char const * getMyExcpStr() const { return srcArr; }
};
void inDate();
void main()
{
    try{
        inDate();
    }
    catch (char *str){
        printf("main::char*异常---%s\n", str);
    }
    catch (MyExcp m){
        printf("main::MyExcp异常---%s\n", m.getMyExcpStr());
    }
}
void f(int v)
{
    if (v == 11)
        throw v;
    if (v == 22)
        throw 3.13;
}
void inDate()
{
    int val;
    scanf("%d",&val);
    if (val == 0)
        throw "value == 0";
    try{
        if (val < -50){
            MyExcp me;
            throw me;
        }
        if (val > 50){
            throw "value > 50";
        }
        else{
            f(val);
        }
	}
    catch (int i){
        printf("inDate::int异常---%d\n",i);
	}
    catch (char *str){
        printf("inDate::char*异常---%s\n", str);
	}
}

五、throw指定异常说明

  • 1.概念函数可以在函数体的参数列表圆括号后加上throw限制,用来说明函数可以抛出什么异常
  • 2.书写格式
    • 建议函数的声明、定义都写上
    • 我们可以在函数指针的声明和定义中指定throw
    • throw异常说明应该出现在函数的尾指返回类型之前
    • 在类成员函数中,应该出现在const以及引用限定符之后,而在final、override、虚函数=0之前
  • 3.格式:举几个例子
void fun();//可以抛出所有异常(函数的正常形式)
void fun()throw(int);//可以抛出int类型的异常
void fun()throw(int,double);//可以抛出int、double类型的异常
void fun()throw();//不可以抛出异常
  • 4.与异常指定说明不符合的情况
    • 即使函数指定了throw异常说明,但是函数体内如果还是抛出异常,或是抛出与throw异常说明中不对应的异常,程序不会报错
    • 编译器在编译时不会检查throw异常说明,尽管说明了,但抛出了还是不会出错
void fun()throw(int)
{
    throw "Hello";//抛出字符串异常,不报错
}

六、noexcept异常说明

七、一些重要的注意事项

1.栈展开过程中局部对象自动销毁

  • 我们知道,语句块在结束之后,块内的局部对象会自动销毁
  • 栈展开中也是如此,如果栈展开中退出了某个块,代表该块生命周期已经结束,语句块中的局部对象也会被销毁(自动调用析构函数)
  • 例如下面的代码,对象v将会自动调用析构函数,自动销毁
int main()
{
    vector<int> v(1,100);
    throw ...;//抛出异常
}

2.析构函数与异常的关系

  • 上面介绍过,栈展开过程中对象会自动调用析构函数销毁
  • 析构函数中不可以再放置try语句块,很危险。原因:若析构函数中放置try语句块,其后面释放资源等操作可能就不会执行,后果很危险

3.不可抛出局部对象的指针

  • 上面介绍过,退出了某个块,则同时释放该块中局部对象使用的内存。如果抛出了一个局部对象的指针,则在执行相对应的catch语句块之前,该对象已经被销毁了。因此,抛出一个指向局部对象的指针是错误的。(原理类似于函数不能返回一个局部对象的指针)
class A{...省略}
int main()
{
    try{
        A* a=new A;
        throw a;//错误     
    }
}

4.栈展开过程中的内存泄漏

  • 若一个指针对象在释放之前抛出异常,则会造成内存泄漏。
  • 例如下面的指针p虽然被销毁,但是内存没有被释放,内存泄漏。
int main()
{
    int *p=new int[5];
    throw ...;//抛出异常
}
  • 解决办法:在异常发生的时候,自动释放其内存。可以使用智能指针,并传入删除的lambda表达式
shared_ptr<int> p(new int[v.size()], [](int *p) { delete[] p; });

5.throw表达式解引用基类指针

  • throw表达式解引用基类指针,该指针指向的是派生类对象,则抛出的对象会被切除其派生类的部分,只有基类部分被抛出去

八、标准异常

  • 1.概念C++标准库定义了一组类,用于标准库函数遇到的问题。这些异常类可以被使用者调用
  • 2.使用
    • 命名空间:using namespace::std; 或者直接使用 using std::对象的类
    • 各自对应的头文件
  • 3.分类
    • exception头文件:定义了最通用的异常类exception。它只报告异常的发生,不提供任何额外信息,是所有异常类的基类
    • new头文件:定义了bad_alloc异常类(当动态分配内存,内存不足时,抛出这个异常
    • type_info头文件:定义了ban_cast异常类、bad_typeid异常类(当遇到NULL对象时,会抛出这个异常)
    • stdexcept头文件:定义了如下常用的异常类:
exception最常见的问题
runtime_error只有在运行时才能检测出的问题
range_error运行时错误:生成的结果超出了有意义的值域范围
overflow_error运行时错误:计算上溢
underflow_error运行时错误:计算下溢
logic_error程序逻辑错误
domain_error逻辑错误:参数对应的结果值不存在
invalid_argument逻辑错误:无效参数
length_error逻辑错误:试图创建一个超出该类型最大长度的对象
out_of_range逻辑错误:使用一个超出有效范围的值
  • 上面的所有异常类,都有一个共同的成员函数
what();无参数,返回值为类初始化时传入的const char*类型的字符串(代表错误的信息)。该函数一定不会抛出异常
  • 4.各个类之间的继承体系
    • exception仅仅定义了拷贝构造函数、拷贝赋值运算符、一个虚析构函数、一个虚函数what()
    • exception第2层又将异常类分为:运行时错误逻辑错误

  • 5.注意
    • exception、bad_alloc、bad_cast对象只能使用默认初始化,不能提供初始化值
    • 其他异常类型创建时必须提供初始化值。值的类型为const char*类型或者string类型
  • 6.事例
    • 当一个一个catch的参数为exception类型时,这个catch语句块捕获的异常类型是基类型exception以及所有从exception派生的类型(后者是因为派生类可以向基类转换)
    • 使用runtime_error异常类,抛出一个异常类对象
int main()//此事例,简单地判断用户输入的数字小于0之后,如何选择
{
    int num;
    while (cin >> num)
    {
        try {
            if (num < 0)
                throw runtime_error("cin num <0 ");//初始化并抛出
        }
        catch (runtime_error error)//接收runtime_error类型的对象
        {
            cout <<"the exception is "<<error.what() << endl;//打印错误信息
            cout << "Tey Again?Enter y or n:" << endl;
            char select;
            cin >> select;
            if (!cin || select == 'n')
                break;
        }
    }
}

九、继承标准异常实现自己的异常类型

  • 1.使用方式
    • 通过继承某一异常类,并实现基类的相关函数,也可以自己新增函数
    • 我们自己定义的异常类使用方式和标准异常类的使用方式完全一样
  • 2.事例
class CMyArr :public range_error//继承
{
    string Cstr;
public:
    CMyArr(const string& str):range_error(str),Cstr(str){}
    virtual const char* what(){//实现虚函数
        return Cstr.c_str();//string转const char*
    }
};
void main()
{
    try{
        int arr[3] = { 1,2,3 };
        int index;
        cin >> index;
        if (index < 0)
            throw CMyArr("数组下标错误,请重新输入");//抛出异常
    }
    catch (CMyArr m){
        cout <<m.what() << endl;
    }
}
©️2020 CSDN 皮肤主题: 黑客帝国 设计师:上身试试 返回首页