大纲:
1、普通函数异常发生,防止局部资源泄漏
2、构造函数异常发生,防止局部资源泄漏
3、禁止析构函数发生的异常传递到调用者
4、抛出异常、传递参数、调用虚函数差别
5、比较异常传递方式
6、慎用异常规格
1、普通函数异常发生,防止局部资源泄漏
异常处理机制:
没有任何的异常捕获机制,将导致程序的控制权到调用者上,原有程序逻辑将不会被执行。相反,如果有异常捕获机制,我们就可以控制程序的执行逻辑。
局部资源,这里指代指针所指向堆资源。
解决方法:
用对象代替指针,对象的析构函数内释放资源。当函数返回时(正常/异常),函数体内的局部对象确保会调用其析构函数来释放资源。
标准类库中auto_ptr<T>可用来支持上述阐述:
- 构造函数内,使指针指向堆对象
- 析构函数内,释放堆对象及其占用空间
借鉴auto_ptr<T>的实现方法,对于任何资源,我们将其关联到一个对象上,析构函数内实现资源的释放。
2、构造函数异常发生,防止局部资源泄漏
构造函数内何时发生异常?
在构造函数内尝试分配内存并初始化对象,内存不够,将引发异常,此时程序的控制权将转至调用构造函数的地方。构造函数没有完全被执行,C++对于不完整的对象,在离开其生存空间时,将不会执行对象的析构函数,资源泄漏发生了。
解决方法:
在构造函数内部捕获到异常,释放资源,在向上传递异常。而不是直接将异常向上传递,一旦向上传递,再也无法释放类内的资源。
对于类中数据成员,类对象成员在进入到构造函数体内前就已经被初始化了,完整对象将由编译器调用其析构函数释放(生命周期结束时)。不完整对象资源释放工作在构造函数内完成。
因此,对于一个包含堆资源的类,构造函数内需要捕获可能存在的异常并清理内存并抛出异常。创建类对象时(即调用构造函数时),也需要捕获异常(new operator 失败,如何delete 内存空间,没想明白)
类对象初始化时,类成员在进入构造函数体之前,编译器会先调用其默认构造函数为其初始化。常量指针的初始化一定要在初始化列表中进行,因此 ,当面对常量指针指向动态 分配资源时,无法再初始化列表中使用try catch来捕获异常。
这里可以使用私有函数,在其内动态分配资源,并进行异常捕获,返回指向动态资源的指针。初始化列表中使用私有函数进行初始化。
但上面的方式,使得构造函数实现分散在多个函数,不利于后期维护。
另一解决方法,不使用try catch捕获异常,也能保证资源被释放,使用智能指针。构造列表的调用中出现了异常调用,在此之前已完成初始化的智能指针能调用对象自身析构函数完成资源释放。
3、禁止析构函数发生的异常传递到调用者
何时调用析构函数:
正常情况下,局部对象生命周期结束,使用delete operator
异常情况下,异常机制将会调用栈堆空间内对象的析构函数
为什么禁止析构函数抛出异常?
其一,在编写析构函数时,需要考虑最坏的情况:由异常机制调用,如果此时析构函数再抛出异常,编译器将调用terminate函数,终止程序运行。
其二,析构函数用来释放资源的,可能异常抛出点,还有资源没有被释放,因此需要在析构函数内捕获到异常,继续释放资源。
4、抛出异常、传递参数、调用虚函数差别
抛出异常、传递参数途径相同:支持传值、引用、指针,
不同点:
一、系统执行操作不同
**1、被抛出的异常对象必须被复制(产生一个临时对象)。**因为在抛出异常时,栈空间内的遍历被释放了。如果异常对象不被复制,catch子句参数将指向被释放空间。
异常对象拷贝调用静态类型的ctor。
在catch子句抛出异常时的两种写法:重抛异常和复制异常对象在抛出的差别,重抛异常效率更高,如果异常对象时派生类,那么虚函数性质得以保持,否则丧失。
2、与参数传递相比,catch子句可以使用普通引用类型,而非必须使用const引用(临时对象)。
3、不建议使用指针传递,必须保证指针所指向的对象是堆对象或是全局变量。
二、类型转换局限性:
参数传递过程支持隐式类型转换,异常匹配仅支持两种类型转换:
1、基类与派生类对象转换,捕获基类异常的catch子句也可用来处理派生类异常。且可以用在传值、传引用、传指针方式上。
2、类型化指针转变为无类型指针,无类型指针可以用来处理任何类型指针异常的情况。
三、异常匹配次序性
程序中异常捕获声明顺序决定了catch子句的执行(最先适合法,一旦基类类型匹配写在派生类之前,派生类型匹配无法被执行到),这与虚函数执行相反,虚函数执行优先考虑动态类型,执行最佳匹配类型的虚函数(最优适合法)。
比较异常传递方式:
1、指针传递,仅复制指针,不用复制异常对象,但必须确保指针所指向的异常对象时堆对象或是全局对象。问题在于,调用抛出指针异常函数的人,不知道指针所指向的时全局对象or 堆对象,不确定是否需要delete对象。
四个标准异常:
bad_alloc operator new 失败
bad_cast dynamic_cast转换失败
bad_typeid dynamic_cast对空指针处理
bad_exception unexpected函数
2、值传递
类似参数传递,虽然临时对象是派生类型对象,但由于catch子句是基类类型,故catch子句使用的是slicing派生类对象,没有虚函数特性。
3、引用传递
仅仅执行一次拷贝构造函数生成临时对象,无slicing问题,总而言之,推荐使用引用捕获异常。
慎用异常规格
异常规格:类似于异常规格文档,明确告诉使用代码内存在的异常情况,编译器一旦监测到不在规格范围的异常(函数内调用另一函数,此函数抛出的异常),将调用expected函数,expected函数缺省行为调用terminate函数,terminate 函数缺省行为是abort函数。abort 函数将不会释放栈空间内的变量。
没有声明异常规格的函数,可以抛出任何类型异常。
1、不混用模板和异常规格
避免在带有类型参数的模板中使用异常规格,不同类型参数的对象抛出不同的异常类型。
2、带有异常规格函数调用不带有异常规格函数,需要去除该函数的异常规格声明。
3、重定义unexpected 函数行为,unexpected 函数抛出bad_exception 类型,可更为抛出其他异常类型,或是重抛异常而不调用terminate 函数,在上层代码捕获bad_expection异常。(更改缺省的unexpected行为是个好方法!)
总结:
将try 和异常规格使用限制在需要用的地方,提高运行效率。