-
前言
exception异常无法被忽略,其他方式例如“返回状态码”或者“设置状态变量”等方式,不能保证调用者一定会按照这样的方式使用。而抛出异常,如果不处理,程序则会自动停止。而设计“exception-safe”的程序也需要一定的章程。
-
9. 利用析构函数避免资源泄露
- 本章所讲的就是智能指针的思路,在一段程序中,new和delete之间如果抛出了exception但没有被catch,那么程序会立刻终止,但是资源并没有被delete,造成了资源的泄露。
- 但是局部变量在程序终止时仍然会调用其析构函数,如果我们将资源销毁放在局部变量的析构函数中,那么则能保证如果有异常也会销毁资源,防止泄露。
- 使用C++提供的auto_ptr则是这样的机制。
-
10. 在构造函数中防止资源泄露
-
假如在一个class中,我们在构造函数中调用new来动态创建其他的class,那么需要在析构函数中对这些对象进行清理。
比如这个例子,person类中可能有照片Image类和声音Voice类的信息,所以再构造函数中,如果有,我们则通过new来动态分配一个。在析构函数中,我们对其进行delete(delete NULL也不会出现问题,所以不判断是否空)。
class person{ public: person(string& name, string& image, string& voice) : name(name), personal_image(NULL), personal_voice(NULL) { if(image != "") personal_image = new Image(); if(voice != "") personal_voice = new Voice(); } ~person(){ delete personal_image; delete personal_voice; } private: string name; Image* personal_image; Voice* personal_voice; }
-
假如我们创建一个Person类的局部变量,在构造函数的第二个new的过程中抛出了异常exception,但personal_image对象已经构造了。由于构造函数并没有完成,这个Person对象不会被创建,因此也不会调用析构函数。那么在构造函数中已经new的Image对象则会造成内存泄露。
void CreateTom(){ person Tom("Tom", "Image", "Voice"); ... }
就算你在heap中用new运算符生成一个person对象,并使用try-catch语句,仍然不能避免。如下例,由于构造函数并没有完成,所以Tom依然是NULL,你对Tom进行delete并不能起到任何作用。
person* Tom = 0; try{ Tom = new person("Tom", "Image", "Voice"); } catch(...){ delete Tom; throw; }
-
如果你要想避免,直接的方法就是在构造函数中想到这种情况,然后加上try-catch语句。在catch中清理资源,然后继续将异常传播。
person::person(string& name, string& image, string& voice) : name(name), personal_image(NULL), personal_voice(NULL) { try{ if(image != "") personal_image = new Image(); if(voice != "") personal_voice = new Voice(); } catch(...){ delete personal_image; delete personal_voice; throw; //这个不能忘记!!!要将异常传播出去,因为构造函数的异常并没有解决。 } }
-
如果你觉得多个delete在析构函数中还要重复,那么可以写一个清理函数。
-
如果你必须要在初始化列表中对其进行初始化,而不是在构造函数体内对其赋值,那么由于初始化列表只接受表达式,所以可以将try-catch写进一个函数中,在初始化列表中调用那个函数。
-
那么更优秀的方法则是上一条讲的资源管理的方法,局部变量不会资源泄露,采用智能指针auto_ptr,在智能指针离开作用域时,exception处理机制会将局部变量销毁,而智能指针析构函数会自动销毁动态分配的资源,防止资源泄露。在析构函数中也不需要加上delete了。
-
-
11. 禁止析构函数抛出异常
- 这个好像在Effective C++中讲过。
- 析构函数平时正常的调用这一点不成问题。但是当对象在exception传播过程中的栈展开处理机制中被销毁而调用析构函数时,如果你再次抛出异常,则会导致程序的停止,可能局部变量都没有被销毁完成。
- 所以我们可以在析构函数中将所有异常用try-catch捕捉,关在析构函数内部。
- 好像还有其他种方法,可以参见之前讲过的。
-
12. 了解 “抛出一个异常” 和 “传递一个函数参数” 之间的差别
-
catch异常声明 和 函数参数传递 的样子长得很像。他们都可以有三种:by value、by reference、by pointer。
-
不同处之零:函数调用完成会返回调用端,而catch不会返回抛出的位置。
-
不同处之一:函数的调用参数如果以by reference传递,则是传递过去这个东西本身;但catch如果是by reference,仍然是变量的副本。因为控制权离开区域之后不会返回,其中的局部变量都没用了。
-
注意,抛出时复制动作是以静态类型而定的。
-
在catch语句中,如果要将异常继续传播,以下两种方式都可以。
catch( Widget& w){ dosomething(); throw; } catch( Widget& w){ dosomething(); throw w; }
最好是使用第一种,他直接将异常抛出,而不是进行复制。并且不只是效率上不同,假如是一个DerivedWidget 类,也会被捕捉到,第一种则依然是按照DerivedWidget类抛出,而第二种则变成了Widget类抛出。
-
-
第二个不同:在参数类型匹配规则上不同,函数中假如参数是一个float,我传递一个int过去,编译器会隐式转化为float来匹配这个函数的参数类型。
但catch异常则不会这样隐式转换,他的转换规则只有两种:一种是继承关系可以转换被匹配,如果catch一个base-class,我throw一个继承的derived-class,则可以匹配到,即针对父类的catch,可以捕捉到其继承类的异常;另一种是void* 可以匹配任意指针类型
-
第三个不同:catch子句以其出现的顺序被编译器按顺序检测,如果符合则catch。如果是先捕捉父类Widget&,再捕捉子类DerivedWidget,那么子类永远不会被匹配到,因为父类总是能catch。
所以注意在设计时,要将继承类放在前面。
-
-
13. 以by reference的方式捕捉异常
- 上面提到,跟函数类似,可以以三种方式在catch中声明来捕捉异常。
- 首先,by pointer的方式是不可取的,虽然不会发生复制,看似效率最高,但如果是局部变量,则直接离开作用域就析构了。全局变量倒是可以,但是可能会忘记,再复杂点如果是在heap上分配的,还需负责销毁。更重要的是,官方的一些标准exception都是对象而不是指针。
- 第二个是by value,直接传值,首先他需要两次复制,效率上低。重要的是,在抛出继承体系时,可能会出现切割(slicing)问题:假如抛出一个derived类,catch中通过by-value捕捉一个base类型,那么此时这个异常会变成base类型,失去其派生部分,调用虚函数也会调用base的虚函数,这不是我们想看到的。
- 所以,最好采用by reference的方式,一是只会复制一次,再者不会有切割问题。就算是base&捕捉到了derived&,那么还是保留derived&特性。
-
14. 明智运用exception specification
-
exception specification是一种语法,跟在函数声明后面,明确指出此函数可以抛出什么类型的异常。
-
这种语法可以使得编译器对其看到的错误进行报错,给出提示。但如果在运行期间,程序违反了这个规则抛出其他类型错误,那么则造成程序调用 unexpected() 函数,然后会直接调用 terminate() 程序中止,甚至连局部变量销毁的任务都不会完成。所以这样是非常不可取的。
-
编译器一般只会对这个语法作局部性检查,就是说如果调用一个函数,此函数会抛出其他类型的异常,编译器是不会报错的。
void function_s(){ ... } void function_A() throw(int){ ... function_s(); //这里有可能抛出其他类型的异常,则会出大问题。 }
-
避免技术1:不要为template设计加上exception specification ,因为你不知道某些类型究竟会不会抛出什么类型的异常。
-
避免技术2:如上面的例子,如果一个有exception specification的函数,调用没有exception specification的函数,则不可取。
-
避免技术3:没看太懂,大概是 重写unexpected() ,将所有未预期的异常转换为一个自己定义的异常,然后再exception specification中加上这个类型,任何其他类型的异常都会变成这个异常被抛出,不会违反exception specification。
-
-
15. 了解异常处理的成本
- 假如你的程序中一点都没有异常处理,你也必须付出最低的成本,因为C++支持异常处理,他的一些机制编译器就得遵循。
- 使用try语句会带来成本,会使你的代码膨胀大约5%-10%,程序效率上也差不多。
- exception specification的成本与try差不多。
- 抛出异常导致函数返回与函数正常返回,差距可能在三个数量级,这是很大的冲击。
- 所以:在适当的时候使用异常机制。对try和exception specification的使用限制于非用不可,并且只在真正异常时才抛出。