C++:异常的深度解析
Hello,大家好,今天这篇博客是我们关于C++语法部分的倒数第二篇博客了,我们大家在公司中完成一部分代码后,就会产生一个问题,就是这个代码的正确性,今天我们就针对这个检测来看一看,这个异常的分析。
目录
1 异常的概念及使用
1.1 异常的概念
1.2 异常的抛出和捕获
1.3 栈展开
1.4 查找匹配的处理代码
1.5 异常重新抛出
1.6 异常安全问题
1.7 异常规范
2 标准库的异常
1 异常的概念及使用
1.1 异常的概念
1>.异常处理机制允许程序中独立开发部分能够在运行时就出现的问题进行通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的一部分负责检测问题的出现,然后解决问题的任务传递给出现的另一部分,检测环节无须知道问题的处理模块的所有细节。
2>.C语言主要是通过错误码的形式处理错误,错误码本质上就是对错误信息进行分类编号,拿到错误码以后还需要我们自己去查询错误信息,比较麻烦。异常时会抛出一个对象,这个对象可以涵盖更全面的各种信息。
1.2 异常的抛出和捕获
1>.程序出现问题时,我们通过抛出(throw)一个对象来引发一个异常,该对象的类型以及当前的调用链决定了改由哪个catch的处理代码来处理异常。
2>.被选中的处理代码是调用链中与该类型匹配且离抛出异常的位置最近的那一个catch的处理代码。根据抛出对象的类型与内容,程序的抛出异常部分要告知异常处理部分到底发生了什么错误。
3>.当throw执行时,throw后面的语句将不再被执行。程序的执行从throw位置会跳到与之匹配的catch模块,catch可能是同一个函数中的一个局部catch模块,也可能是调用链中的另一个函数中的catch模块,控制权从throw位置转移到了catch模块的位置。这里还有两个重要的含义:1.沿着调用链的函数可能会提早推出;2.一旦程序开始执行异常处理程序,沿着调用链创建的对象都将会自动被编译器销毁。
4>.抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象,这个拷贝的对象会在catch模块结束后就被销毁了。(这里的处理类似于函数的传值返回)
5>.在C++的异常处理过程中,我们常常选择使用try-catch去处理异常,我们这里就先来讲解一下这个try-catch:1.try:表示将有可能出现异常的代码书写在try代码块中;2.catch:try不能单独使用,必须结合catch / finally / catch-finally(这里try结合catch),catch也不能单独使用,必须结合try一起用。
int Divide(int a, int b) { try { if (b == 0)//如果b等于0,就抛异常。 { string s("Divide by zero condition!"); throw s;//这里会将类型为string的对象s抛出去,去找这条调用链中与s这个对象类型匹配且离抛出异常的哪个位置的那一个catch代码块(抛出的并不是s对象,而是s这个异常对象的一个拷贝对象)。 } else { return a / b; } } catch (int errid)//catch这个代码块接收的是int类型的一个对象。 { cout .如果找到匹配的catch模块去处理后,catch模块中的以及后续的代码则会进行执行。上图就是一个栈展开的过程。
1.4 查找匹配的处理代码
1>.一般情况下抛储对象和catch接收的那个对象的类型是完全匹配的,如果有多个类型匹配的catch子句,那么就选择离他位置更近的那个catch子句。
2>.但是也有一些例外,允许从非常量向常量的类型准换,也就是权限缩小;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派生类向基类类型的转换,这一点非常实用,实际中继承体系基本都是用这个方式去设计的。
3>.如果到main函数中,异常人就没有被匹配的话就会被终止程序,不是发生严重错误的情况下,我们是不期望程序最终的,所以一般的main函数中在最后都会使用catch(...),它可以捕获任意类型的异常,但是我们是不知道异常的错误是什么。注:一个try模块我们可以搭配多个catch模块。
//由于时间等等各种原因,我们这里就不一一为大家展示匹配的过程代码了,我们接下来就来模拟设计一个继承的匹配机制。 class person { public: person(const string& name) :_name(name) { } protected: string _name; }; class student :public person { public: student(const string& name, int id) :person(name) , _id(id) { } private: int _id; }; class teacher :public person { public: teacher(const string& name, int teach) :person(name) , _teach(teach) { } private: int _teach; }; void Print() { if (rand() % 5 == 0) { throw student("学号", 20); } else if (rand() % 2 == 0) { throw teacher("工号", 32); } else { throw string(); } } int main() { try { Print(); } catch (const person& p) { }//可以捕捉所有继承了person类型的对象。 catch (...)//可以捕捉任意类型的异常对象。 { } return 0; }//好了,我们这里直接来看Print函数中抛异常的操作,首先看第36到39这段代码,它抛出的student类型的对象,在第55到56这段代码中的catch子句被捕获了,派生类的对象被基类类型的对象给捕获了;再来看第40到43这段代码,它抛出的是一个teacher类型的对象,在第55到56这段代码中的catch子句被捕获了,teacher这个派生类对象被person这个基类对象给捕获了;最后看第44到47这段代码,它所抛出的是一个string类型的对象,是被第57到58这段代码中的catch子句捕获的,第55到56这段代码中的catch子句它主要捕获的是person类型的对象以及继承了person类的派生类对象,string类型与其不匹配,第55到56这段代码中的catch子句捕获不到,而第57到58这段代码中的catch子句可以捕捉到任意类型的异常对象,因此就被第57到58这段代码中的catch子句给捕捉到了。1.5 异常重新抛出
1>.有时catch到一个异常对象后,需要对错误进行分类,其中的某种异常错误需要进行特殊的处理,其他错误则重新抛出异常给外层调用链处理。捕获异常需要重新抛出,直接throw;就可以把捕捉到的对象再次抛出。
void Print() { int a = rand() % 2; try { throw string(); } catch (string& s) { if (a == 1) { throw;//如果a==1的话,就将捕获到的那个string类型的对象再次抛出。 } else { cout .C++98中函数参数列表的后面接throw(),表示该函数不会抛异常,函数参数列表的后面接throw(类型1,类型2,...)表示可能会抛出多种类型的异常,将可能会抛出的类型之间均用逗号分割。3>.C++98的这种方式有点过于复杂,在实践中其实并不好用,C++11中对其进行了简化,函数参数列表后面若加noexcept这个关键字就表示该函数不会抛异常,若啥都不加的话则表示可能会抛出异常。
4>.编译器并不会在编译时去检查noexcept修饰了,也就是说如果一个函数用noexcept修饰了,但是同时又包含了throw语句或者调用的函数可能会抛出异常,编译器还是会顺利通过的(有些编译器可能会报个警告)。但是如果一个声明了noexcept的函数抛出了异常的话,程序便会去调用terminate终止程序。
5>.noexcept(expression)还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会抛出异常的话则返回false,不会的话就会返回true。
void Print()noexcept { int a = 0; cin >> a; if (a == 10) { throw "a==10"; } } int main() { try { Print(); } catch (char* errmsg) { cout