C++ 程序抛出异常后执行顺序说明
1 析构函数中是否可以抛出异常
首先我们看一个常见的问题,析构函数中是否可以抛出异常。答案是C++标准指明析构函数不能、也不应该抛出异常!
C++异常处理模型是为C++语言量身设计的,更进一步的说,它实际上也是为C++语言中面向对象而服务的。
C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。
那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源, 这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分。
下面我们来看看析构函数中不能抛出异常的两个理由:
1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
那么当无法保证在析构函数中不发生异常时, 该怎么办?
其实还是有很好办法来解决的。那就是把异常完全封装在析构函数内部,决不让异常抛出函数之外。这是一种非常简单,也非常有效的方法。
//析构函数 ~Class() { try{ } catch(){ //这里可以什么都不做,只是保证catch块的程序抛出的异常不会被扔出析构函数之外。 } }
2 程序抛出异常后会怎样
下面我们通过一个程序来观察当程序中抛出异常了是否会调用析构函数,异常抛出中throw()后面的语句是否还会执行。
程序如下,我们创建一个类,然后构造一个类对象,当抛出异常我们看程序是否会进入析构函数以及throw()抛出异常后面的程序:
#include<iostream> using namespace std; class setTry{ public: setTry(){ //构造函数 cout << "start!" << endl; // 1 } ~setTry(){ //析构函数 cout << "end!" << endl; // 4 } void dosomething(){ cout << "do something!" << endl; //类方法 } }; int main(void) { setTry newOne; try{ throw("error!"); //直接抛出异常 newOne.dosomething(); } catch (char* one){ //接收char*类异常 cout << one << endl; // 2 } catch (...){ //接收其他类型异常 cout << "..." << endl; } cout << "return 0!"<<endl; // 3 return 0; }
上面程序运行结就是按标注的1、2、3、4步骤输出的,结果如下图所示:
从运行结果就可以看出,抛出异常try内部的throw()后面程序不会再执行,而try外部后面的程序会继续执行。另外,析构函数在生存期结束也会被调用。
补充:C++异常捕获和处理
0. 写在前面
异常,让一个函数可以在发现自己无法处理的错误时抛出一个异常,希望它的调用者可以直接或者间接处理这个问题。而传统错误处理技术,检查到一个错误,返回退出码或者终止程序等等,此时我们只知道有错误,但不能更清楚的知道哪种错误,因此,使用异常,就把错误和处理分开来,由库函数抛出异常,由调用者捕获这个异常,调用者就可以知道程序函数库调用出现错误了,并去处理,而是否终止程序就把握在调用者手里了。
1. 异常的抛出和处理
1. 异常处理的语句
try区段:这个区段中包含了可能发生异常的代码,在发生了异常之后,需要通过throw抛出。
throw子句:throw 子句用于抛出异常,被抛出的异常可以是C++的内置类型(例如: throw int(1);),也可以是自定义类型。
catch子句:每个catch子句都代表着一种异常的处理。catch子句用于处理特定类型的异常。
例2:
#include <iostream> using namespace std; void Test1() { try { char* p = new char[0x7fffffff]; //抛出异常 } catch (exception e) { cout << e.what() << endl; //捕获异常,然后程序结束 } } int main() { Test1(); system("pause"); return 0; }
结果:
当使用new进行开空间时,申请内存失败,就会抛出异常,此时捕获到异常时,就可告诉使用者是哪里的错误,便于修改
2. 异常的处理规则
异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个处理代码。
被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
抛出异常后会释放局部存储对象,所以被抛出的对象也就还给系统了,throw表达式会初始化一个抛出特殊的匿名对象,异常对象由编译管理,异常对象在传给对应的catch处理之后撤销。
例2:
class Exception//异常类 { public: Exception(const string& msg, int id) { _msg = msg; _id = id; } const char* What() const { return _msg.c_str(); } protected: string _msg; int _id; }; template<size_t N = 10> class Array { public: int& operator[](size_t pos) { if (pos >= N) { Exception e("下标不合法", 1); //出了这个作用域,抛出的异常对象就销毁了,这时会生成一个匿名对象先接受这个对象,并传到外层栈帧。 throw e; } return a[pos]; } protected: int a[N]; }; int f() { try { Array<> a; a[11]; } catch (exception& e) { cout << e.what() << endl; //类型不匹配,找离抛出异常位置最近且类型匹配的那个。 } return 0; } int main() { try { f(); } catch (Exception& e) { cout << e.What() << endl; } system("pause"); return 0; }
结果:
f()函数中捕获的异常是标准库里面的异常,但抛出异常的对象是自己定义的异常类,故类型不匹配,找离抛出异常最近的且类型匹配的Exception
3. 异常处理栈展开
1.在try的语句块内声明的变量在外部是不可以访问的,即使是在catch子句内也不可以访问。
2.栈展开(寻找异常处理(exception handling)代码)
栈展开会沿着嵌套函数的调用链不断查找,知道找到了已抛出的异常匹配的catch子句。如果在最后还是没有找到对应的catch子句的话,则退出主函数后查找过程终止,程序调用标准函数库的terminate()函数,终止该程序的执行
具体过程:
当一个exception被抛出的时候,控制权会从函数调用中释放出来,并需找一个可以处理的catch子句
对于一个抛出异常的try区段,程序会先检查与该try区段关联的catch子句,如果找到了匹配的catch子句,就使用这个catch子句处理这个异常。
没有找到匹配的catch子句,如果这个try区段嵌套在其他try区段中,则继续检查与外层try匹配的catch子句。如果仍然没有找到匹配的catch子句,则退出当前这个主调函数,并在调用了刚刚退出的这个函数的其他函数中寻找。
3. catch子句的查找:
catch子句是按照出现的顺序进行匹配的(以例2来说,异常先会匹配catch(exception e)子句,然后在匹配 catch (Exception e)子句,一步一步的栈展开)。在寻找catch子句的过程中,抛出的异常可以进行类型转换,但是比较严格:
允许从非常量转换到常量的类型转换(权限缩小)
允许从派生类到基类的转换。
允许数组被转换成为指向数组(元素)类型的指针,函数被转换成指向该函数类型的指针(降级问题)
标准算术类型的转换(比如:把bool型和char型转换成int型)和类类型转换(使用类的类型转换运算符和转换构造函数)。
4. 异常处理中需要注意的问题
如果抛出的异常一直没有函数捕获(catch),则会一直上传到c++运行系统那里,导致整个程序的终止
一般在异常抛出后资源可以正常被释放,但注意如果在类的构造函数中抛出异常,系统是不会调用它的析构函数的,处理方法是:如果在构造函数中要抛出异常,则在抛出前要记得删除申请的资源。
异常处理仅仅通过类型而不是通过值来匹配的,所以catch块的参数可以没有参数名称,只需要参数类型。
函数原型中的异常说明要与实现中的异常说明一致,否则容易引起异常冲突。
应该在throw语句后写上异常对象时,throw先通过Copy构造函数构造一个新对象,再把该新对象传递给 catch.
注:那么当异常抛出后新对象如何释放?
异常处理机制保证:异常抛出的新对象并非创建在函数栈上,而是创建在专用的异常栈上,因此它才可以跨接多个函数而传递到上层,否则在栈清空的过程中就会被销毁。所有从try到throw语句之间构造起来的对象的析构函数将被自动调用。但如果一直上溯到main函数后还没有找到匹配的catch块,那么系统调用terminate()终止整个程序,这种情况下不能保证所有局部对象会被正确地销毁。
catch块的参数推荐采用地址传递而不是值传递,不仅可以提高效率,还可以利用对象的多态性。另外,派生类的异常扑获要放到父类异常扑获的前面,否则,派生类的异常无法被扑获。
编写异常说明时,要确保派生类成员函数的异常说明和基类成员函数的异常说明一致,即派生类改写的虚函数的异常说明至少要和对应的基类虚函数的异常说明相同,甚至更加严格,更特殊。