阿龙的学习笔记---More Effective C++---第三章:异常Exception

  • 前言
     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的使用限制于非用不可,并且只在真正异常时才抛出。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值