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<<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异常说明

  • 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<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头文件:定义了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层又将异常类分为:运行时错误逻辑错误

  • 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;
    }
}

 

  • 8
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在 C++ 中,异常处理使用 `try`、`catch` 和 `throw` 关键字来实现。 `try` 块包含可能引发异常的代码。如果在 `try` 块内发生异常,则会跳转到 `catch` 块。`catch` 块是用于处理异常的代码块。 下面是一个简单的例子,演示如何使用 `try` 和 `catch` 语句处理异常: ```cpp try { int x = 10; int y = 0; if (y == 0) { throw "除数不能为0"; } int result = x / y; cout << "结果是:" << result << endl; } catch (const char* msg) { cout << "发生异常:" << msg << endl; } ``` 在上面的代码中,我们首先定义了两个整数 x 和 y,然后检查 y 是否等于 0。如果 y 等于 0,则使用 `throw` 关键字抛出一个异常,其中包含一条错误消息。在 `catch` 块中,我们捕获并处理该异常,并打印错误消息。 除了 `catch` 块外,还可以使用 `finally` 块来完成异常处理。`finally` 块在 `try` 或 `catch` 块执行完毕后都会执行,无论是否发生异常。下面是一个包含 `finally` 块的例子: ```cpp try { int x = 10; int y = 0; if (y == 0) { throw "除数不能为0"; } int result = x / y; cout << "结果是:" << result << endl; } catch (const char* msg) { cout << "发生异常:" << msg << endl; } finally { cout << "程序执行完毕" << endl; } ``` 在上面的代码中,如果 y 等于 0,则会抛出一个异常。在 `catch` 块中,我们捕获并处理该异常,并打印错误消息。无论是否发生异常,都会在 `finally` 块中打印一条消息,表明程序执行完毕。 总的来说,`try`、`catch` 和 `throw` 关键字是 C++ 中非常重要的异常处理机制,可以帮助我们更好地处理代码中可能发生的异常情况。 ### 回答2: try catch throw 是 C 语言中处理异常的机制。 try 块用于包含可能出现异常的代码块,catch 块用于捕获并处理异常,throw 用于抛出异常。 在 try 块中,我们可以放置一段可能会引发异常的代码。当异常发生时,程序会立即跳转到最近的 catch 块进行处理。catch 块中可以通过制定异常类型来捕获指定类型的异常,也可以使用省略号来表示可以捕获任意类型的异常。 在 catch 块中,我们可以执行一些特定的操作来处理异常,比如打印错误信息、恢复现场、释放资源等。也可以选择继续抛出异常以便更高层的 catch 块继续捕获和处理。如果没有合适的 catch 块来处理异常,那么程序会终止执行。 throw 用于主动抛出异常。通过 throw,我们可以在代码中显式地抛出异常对象,而不需等待异常的自动发生。一个 throw 表达式后的执行路径会立即跳转到最近的 catch 块进行处理。在 catch 块中,我们可以选择是否继续抛出异常或者中止程序。 使用 try catch throw 可以帮助我们处理程序中的异常,使程序能更好地控制和处理各种可能的错误情况,提高程序的健壮性和可靠性。在 C 语言中,try catch throw 并不是原生支持的,但可以借助库函数或者自定义宏进行模拟实现。 ### 回答3: try catch throw 是一种在C语言中处理异常的机制。 在C语言中,异常处理是通过错误码返回来实现的。但是在一些特殊情况下,我们可能需要更加灵活和可控的异常处理方式。而try catch throw 机制提供了这样的功能。 try catch throw 结合了三个关键字,分别是trycatchthrowtrytry块用来包含可能会抛出异常的代码。当try块中的代码出现异常时,异常会被抛出。 catchcatch块用来捕获和处理try块中抛出的异常。catch块包含了对异常的处理代码,以便程序可以进行相应的处理操作。 throwthrow关键字用来在try块中主动抛出异常。当程序遇到throw关键字时,即可引发异常,并将其传递给catch块进行处理。 利用try catch throw机制,我们可以更好地控制代码的异常处理逻辑,提高程序的健壮性和可靠性。当程序发生异常时,可以通过catch块捕获异常,并进行相应的处理。而且,由于throw关键字的存在,我们还可以在适当的时候主动抛出异常,以便在代码中进行异常处理。 总之,try catch throw机制是C语言中一种比较高级和灵活的异常处理方式。它可以帮助我们更好地管理和控制程序中的异常情况,提高代码的可靠性和稳定性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值