C++ throw、try、catch、noexcept

原文:异常机制(throw、try、catch)

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

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

一、throw

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

二、try、catch语句块

  • 格式
try{
  ...
}
catch(){
  ...
}
catch(){
  ...
}
  • 注意事项
  • 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;//重新抛出异常
}

四、构造函数的异常处理

  • 我们知道,在进入构造函数的函数体之前,我们要先执行初始化列表。但是如果try、catch语句块放在构造函数体内,初始化列表如果出现异常,函数体内的try语句块还未生效,所以无法捕获异常。为了解决这种情况,我们必须将构造函数写成函数try语句块,也称为函数测试体
  • 函数try语句块既能处理初始化列表,也能处理构造函数体
  • 格式:
  • 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<

五、栈展开

  • 概念: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异常说明

  • C++11的标准。类似于取代了throw说明

一、为什么要使用异常说明

  • 对于用户以及编译器来说,预先知道函数不会抛出异常有助于简化调用该函数的代码
  • 如果编译器确认该函数不会抛出异常,就能执行某些特殊的优化操作,而这些优化操作不适用于可能出错的代码

二、noexcept的书写格式

  • 在普通函数函数的参数列表后加上关键字noexcept,告诉编译器和用户概念函数不会抛出异常
void fun()noexcept; //该函数不会抛出异常
void fun();         //该函数可能会抛出异常
  • 函数的声明和定义都加上关键字noexcept
  • 可以在函数指针的声明和定义中指定noexcept
  • throw异常说明应该出现在函数的尾指返回类型之前
  • 在typedef或类型别名中不能出现noexcept
  • 类成员函数中,应该出现在const以及引用限定符之后,而在final、override、虚函数=0之前

三、违反异常说明

  • 概念:编译器在编译时并不会检查函数是否有noexcept说明。如果一个函数定义了关键字noexcept,但是该函数在运行时仍然可以抛出异常或者调用可能抛出异常的其它函数
//尽管该函数显式地调用了noexcept,但是该函数仍然可以编译通过
void f()noexcept
{
	throw exception(); //违反了noexcept的异常说明
}
  • noexcept只是用来说明函数不会抛出异常,但是函数是否会抛出异常与noexcept无关
  • 如果函数抛出了异常,但是程序没有对异常进行处理,则程序就会调用terminate中断程序

四、与throw的兼容性

  • 早期的C++标准设计了throw异常说明,用来在函数后面指定函数可能抛出的异常类型
  • C++11标准的noexcept用来说明函数不会抛出异常(不是强制的)
  • 在下面的语句格式下,throw和noexcept异常说明是等价的
//下面两者具有相同的作用,都是声明函数不会出异常
void fun()noexcept;
void fun()throw();

五、异常说明的实参

  • noexcept说明符接受一个可选的实参,该实参必须为bool类型
  • 如果实参是true,代表函数不会抛出异常。如果实参是false,代表函数可能会抛出异常
  • 格式如下:
void recoup()noexcept(true); //该函数不会抛出异常
void alloc()noexcept(false); //该函数是否会抛出异常不确定

六、noexcept运算符

  • 功能:
  • noexcept是一个一元运算符。类似于sizeof运算符
  • 返回一个bool值,用于表示给定参数的表达式是否会抛出异常。
  • 如果参数不抛出异常,返回true。否则为false
  • 使用格式:
  • 下面的fun函数不会抛出异常,所以返回true
void fun()noexcept
{}
void main()
{
    cout << noexcept(fun())<< endl;//打印true
}
  • noexcept一个小功能可以将两个函数的异常说明规定为相同的格式
void fun()noexcept(noexcept(gun()))//gun函数与fun函数的异常说明一致

七、noexcept异常说明与指针

  • 尽管noexcept不属于函数类型的一部分,但是仍影响函数的使用。规则如下:
  • 规则1:如果我们为某个函数指针做了不抛出异常的说明,则该指针只能指向不抛出异常的函数
  • 规则2:相反,如果我们显示或隐式说明了指针可能会抛出异常,则该指针可以指向任何函数,即使承诺不会抛出异常的函数也可以
void recoup()noexcept(true); //该函数不会抛出异常
void alloc()noexcept(false); //该函数可能会抛出异常
 
void (*pf1)(int) noexcept=recoup;  //正确,pf1与recoup都不会抛出异常
void (*pf2)(int) =recoup;          //正确,recoup不会抛出异常,pf2可能抛出异常,两者互不干扰
 
pf1 = alloc; //错误,alloc可能抛出异常,但是pf1已经说明了它不会抛出异常
pf2 = alloc; //正确,pf2和alloc都可能抛出异常

八、noexcept异常说明与虚函数

  • 规则如下:
  • 规则1:如果一个虚函数承诺它一定不会抛出异常,则后续派生出来的虚函数也必须做出相同的承诺
  • 规则2:反之,如果基类的虚函数允许抛出异常,则派生类的对应函数允许抛出异常,也可以不允许抛出异常
class A
{
public:
    virtual double f1(double)noexcept; //不会抛出异常
    virtual int f2()noexcept(false);   //可能会抛出异常
    virtual void f3();                 //抛出异常
};
 
class B :public A
{
public:
    double f1(double);      //错误,Base::f1承若不会抛出异常
    int f2()noexcept(false);//正确,与Base::f2的异常说明一致
    void f3()noexcept;      //正确,Derived的f3做了更严格的限定,是允许的
};

九、noexcept异常说明与拷贝控制

  • 当编译器合成拷贝控制成员时同时也生成一个异常说明。规则如下:
  • 如果对所有成员和基类的所有操作都承诺了不会抛出异常,则合成的成员是noexcept的
  • 如果合成成员调用的任意一个函数可能抛出异常,则合成的成员是noexcept(false)
  • 如果我们定义一个析构函数但没有为它提供异常说明,则编译器将合成一个。合成的异常说明将与假设由编译器为类合成析构函数时所得的异常说明一致

七、一些重要的注意事项

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

  • 我们知道,语句块在结束之后,块内的局部对象会自动销毁
  • 栈展开中也是如此,如果栈展开中退出了某个块,代表该块生命周期已经结束,语句块中的局部对象也会被销毁(自动调用析构函数)
  • 例如下面的代码,对象v将会自动调用析构函数,自动销毁
int main()
{
    vector 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 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头文件:定义了bad_cast异常类、bad_typeid异常类(当遇到NULL对象时,会抛出这个异常)
  • stdexcept头文件:定义了如下常用的异常类:
exception 最常见的问题
untime_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层又将异常类分为:运行时错误逻辑错误

C++ throw、try、catch、noexcept_第1张图片

  • 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 "<> 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 <

 

你可能感兴趣的:(C++标准库)