异常处理机制 --- 相知篇 (四)

来源:互联网 发布:淘宝导航栏图 编辑:程序博客网 时间:2024/05/17 08:56

-----------------------------------------------------------------------------------------------------------------------------


C++的异常对象按引用方式被传递



         转自 http://se.csai.cn/ExpertEyes/No147.htm
 
  上一篇文章详细讨论了C++的异常对象按值传递的方式,本文继续讨论另外的一种的方式:引用传递。


异常对象在什么时候构造?

  其实在上一篇文章中就已经讨论到了,假如异常对象按引用方式被传递,异常对象更应该被构造出一个临时的变量。因此这里不再重复讨论了。

异常对象按引用方式传递

  引用是C++语言中引入的一种数据类型形式。它本质上是一个指针,通过这个特殊的隐性指针来引用其它地方的一个变量。因此引用与指针有很多相似之处,但是引用用起来较指针更为安全,更为直观和方便,所以C++语言建议C++程序员在编写代码中尽可能地多使用引用的方式来代替原来在C语言中使用指针的地方。这些地方主要是函数参数的定义上,另外还有就是catch到的异常对象的定义。

  所以异常对象按引用方式传递,是不会发生对象的拷贝复制过程。这就导致引用方式要比传值方式效率高,此时从抛出异常、捕获异常再到异常错误处理结束过程中,总共只会发生两次对象的构造过程(一次是异常对象的初始化构造过程,另一次就是当执行throw语句时所发生的临时异常对象的拷贝复制的构造过程)。而按值传递的方式总共是发生三次。看看示例程序吧!如下:

void main()  {      try      {          {              throw MyException();          }      }      // 注意:这里是定义了引用的方式      catch(MyException& e)      {          cout<<"捕获到一个MyException类型的异常,名称为:"<<e.GetName()<<endl;      }  }  

  程序运行的结果是:
  构造一个MyException异常对象,名称为:none
  拷贝一个MyException异常对象,名称为:none
  销毁一个MyException异常对象,名称为:none
  捕获到一个MyException类型的异常,名称为:none
  销毁一个MyException异常对象,名称为:none
  程序的运行结果是不是显示出:异常对象确实是只发生两次构造过程。并且在执行catch block之前,局部变量的异常对象已经被析构销毁了,而属于临时变量的异常对象则是在catch block执行错误处理完毕后才销毁的。

那个被引用的临时异常对象究竟身在何处?

  呵呵!这还用问吗,临时异常对象当然是在栈中。是的没错,就像发生函数调用时,与引用类型的参数传递一样,它也是引用栈中的某块区域的一个变量。但请大家提高警惕的是,这两处有着非常大的不同,其实在一开始讨论异常对象如何传递时就提到过,函数调用的过程是有序的的压栈过程,请回顾一下《第9集 C++的异常对象如何传送》中函数的调用过程与“栈”那一节的内容。栈是从高往低的不断延伸扩展,每发生一次函数调用时,栈中便添加了一块格式非常整齐的函数帧区域(包含参数、返回地址和局部变量),当前的函数通过ebp寄存器来寻址函数传入的参数和函数内部的局部变量。因此这样对栈中的数据存储是非常安全的,依照函数的调用次序(call stack),在栈中都有唯一的一个对应的函数帧一层层地从上往下整齐排列,当一个函数执行完毕,那么最低层的函数帧清除(该函数作用域内的局部变量都析构销毁了),返回到上一层,如此不断有序地进行函数的调用与返回。

  但发生异常时的情况呢?它的异常对象传递却并没有这么简单,它需要在栈中把异常对象往上传送,而且可能还要跳跃多个函数帧块完成传送,所以这就复杂了很多,当然即便如此,只要我们找到了源对象数据块和目标对象数据块,也能很方便地完成异常对象的数据的复制。但现在最棘手的问题是,如果采用引用传递的方式将会有很大的麻烦,为什么?试想!前面多次提到的临时异常对象是在那里构造的?对象数据又保存在什么地方?毫无疑问,对象数据肯定是在当前(throw异常的函数)的那个函数帧区域,这是处于栈的最低部,现在假使匹配到的catch block是在上层(或更上层)的函数中,那么将会导致出现一种现象:就是在catch block的那个函数(执行异常处理的模块代码中)会引用下面抛出异常的那个函数帧中的临时异常对象。主人公阿愚现在终于恍然大悟了(不知阅读到此处的C++程序员朋友们现在领会了作者所说的意思没有!如果还没有,自己动手画画栈图看看),是啊!确是如此,这太不安全了,按理说当执行到catch block中的代码时,它下面的所有的函数帧(包括抛出异常的哪个函数帧)都将会无效,但此时却引用到了下面的已经失效了的函数帧中的临时异常对象,虽说这个异常对象还没有被析构,但完全有可能会发生覆盖呀(栈是往下扩展的)!

  怎么办!难道真的有可能会发生覆盖吗?那就太危险了。朋友们!放心吧!实际情况是绝对不会发生覆盖的。为什么?哈哈!编译器真是很聪明,它这里采用了一点点技巧,巧妙的避免的这个问题。下面用一个跨越了多个函数的异常的例子程序来详细阐述之,如下:

void test2()  {      throw MyException();  }  void test()  {      test2();  }  void main()  {      try      {          test();      }      catch(MyException& e)      {          cout<<"捕获到一个MyException类型的异常,名称为:"<<e.GetName()<<endl;      }      cout<<"那个临时的异常对象应该是在这之前析构销毁"<<endl;  }  
 
  怎样来分析呢?当然最简单的方法是调试一下,跟踪它的ebp和esp的变化。首先在函数调用的地方和抛出异常的地方设置好断点,F5开始调试,截图如下:

  纪录一下ebp和esp的值(ebp 0012FF70;esp 0012FEF8),通过ebp和esp可以确定main函数的函数帧在栈中位置,F5继续,截图如下:
  同样也纪录一下ebp和esp的值(ebp 0012FE9C;esp 0012FE08),通过ebp和esp可以看出栈是往下扩展,此时的ebp和esp指向抛出异常的test2函数的函数帧在栈中位置,F5继续,此时抛出异常,控制进入main函数中的catch(MyException& e)中,截图如下:
  请注意了,现在ebp恢复了main函数在先前时的函数帧在栈中位置,但esp却并没有,它甚至比刚刚抛出异常的那个test2函数中的esp还要往下,这就是编译器编译程序时耍的小技巧,当前ebp和esp指向的函数帧实际上并不是真正的main函数原来的哪个函数帧,它实际上包含了多个函数的函数帧,因此catch block执行程序时当然不会发生覆盖。我们还是看看异常对象所引用指向的临时的变量究竟身在何处。截图如下:
  哈哈!e指向了0x0012fe7c内存区域,再看看上面的抛出异常的test2函数的函数帧的ebp和esp的值。结果0x0012fe7c恰好是ebp 0012FE9C和esp 0012FE08之间。
不过阿愚又开始有点疑惑了,哦!这样做岂不是破坏了函数的帧栈吗,结果还不导致程序崩溃呀!呵呵!不用担心,F5继续,截图如下:
  当离开了catch block作用域之后,再看看ebp和esp的值,是不是和最开始的那个main函数进入时的ebp和esp一模一样,哈哈!恢复了,厉害吧!先暂时不管它是如何恢复的,总之ebp和esp都是得以恢复了,而且同时catch block执行时也不会发生异常对象的覆盖。这就解决了异常对象按引用传递时可能存在的不安全隐患。
  引用方式下,异常对象会发生对象切片吗?

  当然不会,要不测试一下,把上一篇文章中的对应的那个例子改为按引用的方式接受异常对象。示例如下:

void main()  {      try      {          {              MyMemoryException ex_obj1("ex_obj1");              cout <<endl<< "抛出一个MyMemoryException类型的异常" <<endl<<endl;              throw ex_obj1;          }      }      // 注意这里引用的方式了      // 还会发生了对象切片吗?       catch(MyException& e)      {          // 调用虚函数,验证一下这个异常对象是否发生了对象切片          cout<<endl<<e.Test_Virtual_Func()<<endl<<endl;      }  }  


  程序运行的结果是:
  构造一个MyException异常对象,名称为:ex_obj1
  构造一个MyMemoryException异常对象,名称为:ex_obj1
  抛出一个MyMemoryException类型的异常
  构造一个MyException异常对象,名称为:none
  拷贝一个MyMemoryException异常对象,名称为:ex_obj1
  销毁一个MyMemoryException异常对象,名称为:ex_obj1
  销毁一个MyException异常对象,名称为:ex_obj1
  这是MyMemoryException类型的异常对象
  销毁一个MyMemoryException异常对象,名称为:ex_obj1
  销毁一个MyException异常对象,名称为:ex_obj1


总结
  (1) 被抛出的异常对象都是临时的局部变量;
  (2) 异常对象至少要被构造二次; 
  (3) catch 后面带的异常对象的作用域仅限于catch bock中; 
  (4) 按引用方式传递不会发生异常对象的切片。
  下一篇文章讨论C++的异常对象被按指针的方式传递。继续吧!




-----------------------------------------------------------------------------------------------------------------------------------


C++的异常对象按指针方式被传递



         本文转自 http://se.csai.cn/ExpertEyes/No148.htm
 
  上两篇文章分别详细讨论了C++的异常对象按值传递和按引用传递的两种方式,本文继续讨论最后的一种的方式:按指针传递。


异常对象在什么时候构造?

  1、与按值和按引用传递异常的方式相比,在按指针传递异常的方式下,异常对象的构造方式有很大的不同。它必须是在堆中动态构造的异常对象,或者是全局性static的变量。示例程序如下:

void main()  {      try      {          // 动态在堆中构造的异常对象          throw new MyMemoryException("ex_obj1");      }      catch(MyException* e)      {          cout<<endl<<"捕获到一个MyException*类型的异常,名称为:"<<e->GetName()<<endl;          delete e;      }  }  

  2、注意,通过指针方式传递的异常对象不能是局部变量,否则后果很严重,示例如下:

void main()  {      try      {          // 局部变量,异常对象          MyMemoryException ex_obj1("ex_obj1");          // 抛出一个指针类型的异常          // 注意:这样做很危险,因为ex_obj1这个对象离开了这个作用域即          // 析构销毁          throw &ex_obj1;      }      catch(MyException* e)      {          // 下面语句虽然不会导致程序崩溃,但是e->GetName()取得的结果          // 也是不对的。          cout<<endl<<"捕获到一个MyException*类型的异常,名称为:"<<e->GetName()<<endl;          // 这条语句会导致程序崩溃          // delete e;      }  }  

  程序运行的结果是:
  构造一个MyException异常对象,名称为:ex_obj1
  构造一个MyMemoryException异常对象,名称为:ex_obj1
  销毁一个MyMemoryException异常对象,名称为:ex_obj1
  销毁一个MyException异常对象,名称为:ex_obj1
  捕获到一个MyException*类型的异常,名称为:
  异常对象按指针方式被传递
  指针是历史悠久的一种数据类型形式,这可以追溯到C语言和PASCAL语言中。异常对象按指针方式传递,当然更是不会发生对象的拷贝复制过程。所以这种方式传递异常对象是效率最高的,它从抛出异常、捕获异常再到异常错误处理结束过程中,总共只会发生唯一的一次对象的构造过程,那就是异常对象最初在堆中的动态创建时的初始化的构造过程。也看看示例程序吧!如下:

void main()  {      try      {          // 动态在堆中构造的异常对象          throw new MyMemoryException("ex_obj1");      }      // 注意:这里是定义了按指针方式传递异常对象      catch(MyException* e)      {          cout<<endl<<"捕获到一个MyException*类型的异常,名称为:"<<e->GetName()<<endl;          delete e;      }  }  
 
  程序运行的结果是:
  构造一个MyException异常对象,名称为:ex_obj1
  构造一个MyMemoryException异常对象,名称为:ex_obj1
  捕获到一个MyException*类型的异常,名称为:ex_obj1
  销毁一个MyMemoryException异常对象,名称为:ex_obj1
  销毁一个MyException异常对象,名称为:ex_obj1
  呵呵!程序的运行结果是不是显示出异常对象只有一次的构造过程。挺好挺好!


异常对象什么时候被销毁

  异常对象动态地在堆上被创建,同时它也要动态的被销毁,否则就必然会发生内存泄漏。那么异常对象应该在什么时候被销毁比较合适呢?当然应该是在catch block块中处理完毕后再销毁它才比较合理。示例如下:

void main()  {      try      {          // 动态在堆中构造的异常对象          throw new MyMemoryException("ex_obj1");      }      catch(MyException* e)      {          cout<<endl<<"捕获到一个MyException*类型的异常,名称为:"<<e->GetName()<<endl;          // 这里需要显示的删除异常对象          delete e;      }  }  

  指针方式下,异常对象会发生对象切片吗?
  当然不会,试都不用试,阿愚非常有把握确信这一点。
总结
  (1) 被抛出的异常对象不能是局部变量或临时变量,必须是在堆中动态构造的异常对象,或者是全局性static的变量;
  (2) 异常对象只会被构造一次; 
  (3) catch 后面带的异常对象的作用域仅限于catch bock中; 
  (4) 异常对象动态地在堆上被创建,同时它也要动态的被销毁。
  至此为止,C++的异常对象的传递的三种方式(指针、传值和引用)都已经讨论过了,主人公阿愚到此算是松了一大口气,终于把这块比较难肯一点的骨头给拿下了(呵呵!^_^)。为了更进一步巩固这些知识。下一篇文章准备对这三种方式来一个综合性的大比较!去看看吧!




------------------------------------------------------------------------------------------------------------------------------------


C++异常对象三种方式传递的综合比较


         本文转自http://se.csai.cn/ExpertEyes/No149.htm
 
  上几篇文章已经分别对C++的异常对象的几种不同的传递方式进行了详细地讨论。它们可以被分为按值传递,按引用传递,以及按指针传递等三种方式,现在该是对它们进行全面盘点总结的时候了。希望这种对比、总结及分析对朋友们理解这三种方式的各种区别有所帮助。


 按值传递引用传递指针传递语法catch(std::exception e)catch(std::exception& e)catch(std::exception* e)如何抛出异常?①throw exception()
②exception ex;throw ex;
③throw ex_global;①throw exception()
②exception ex;throw ex;
③throw ex_global;
①throw new exception();异常对象的构造次数三次二次一次效率低中高异常对象什么时候被销毁①局部变量离开作用域时销毁
②临时变量在catch block执行完毕后销毁
③catch后面的那个类似参数的异常对象也是在catch block执行完毕后销毁①局部变量离开作用域时销毁
②临时变量在catch block执行完毕后销毁异常对象动态地在堆上被创建,同时它也要动态的被销毁,销毁的时机是在catch block块中处理完毕后进行发生对象切片可能会不会不会安全性较低,可能会发生对象切片很好低,依赖于程序员的能力,可能会发生内存泄漏;或导致程序崩溃综合性能差好一般易使用性好好一般


 按值传递引用传递指针传递语法catch(std::exception e)catch(std::exception& e)catch(std::exception* e)如何抛出异常?①throw exception()
②exception ex;throw ex;
③throw ex_global;①throw exception()
②exception ex;throw ex;
③throw ex_global;
①throw new exception();异常对象的构造次数三次二次一次效率低中高异常对象什么时候被销毁①局部变量离开作用域时销毁
②临时变量在catch block执行完毕后销毁
③catch后面的那个类似参数的异常对象也是在catch block执行完毕后销毁①局部变量离开作用域时销毁
②临时变量在catch block执行完毕后销毁异常对象动态地在堆上被创建,同时它也要动态的被销毁,销毁的时机是在catch block块中处理完毕后进行发生对象切片可能会不会不会安全性较低,可能会发生对象切片很好低,依赖于程序员的能力,可能会发生内存泄漏;或导致程序崩溃综合性能差好一般易使用性好好一般


  至此,对C++中的异常处理机制与模型已经进行了非常全面的阐述和分析,包括C++异常的语法,C++异常的使用技巧,C++异常与面向对象的相互关系,以及异常对象的构造、传递和最后析构销毁的过程。

  主人公阿愚现在已经开始有点小有成就感了,他知道自己对她(C++中的异常处理机制)已有了相当深入的了解,并且把她完全当成了一个知己,在自己的编程生涯中再也开始离不开她了。而且他与她的配合已经变得十分的默契,得心应手。但是有时好像还是有点迷糊,例如,对于在C++异常重新被抛出时(rethrow),异常对象的构造、传递和析构销毁的过程又将如何?有哪些不同之处?,要想了解更多的细节,程序员朋友们!请跟主人公阿愚进入到下一篇文章中,GO!




------------------------------------------------------------------------------------------------------------------------------------



再探C++中异常的rethrow



        本文转自http://se.csai.cn/ExpertEyes/No150.htm
 
  在相遇篇中的《第5集 C++的异常rethrow》文章中,已经比较详细讨论了异常重新被抛出的处理过程。但是有一点却并没有叙述到,那就是C++异常重新被抛出时(rethrow),异常对象的构造、传递和析构销毁的过程会有哪些变化和不同之处。为了精益求精,力求对每一个细节都深入了解和掌握,下面再全面阐述一下各种不同组合情况下的异常构造和析构的过程。

  大家现在知道,异常的重新被抛出有两种方式。其一,由于当前的catch block块处理不了这个异常,所以这个异常对象再次原封不动地被重新抛出;其二,就是在当前的catch block块处理异常时,又激发了另外一个异常的抛出。另外,由于异常对象的传递方式有三种:传值、传引用和传指针。所以实际上这就导致了有6种不同的组合情况。下面分别阐述之。

异常对象再次原封不动地被重新抛出

  1、首先讨论异常对象“按值传递”的方式下,异常对象的构造、传递和析构销毁的过程有何不同之处?毫无疑问,在异常被重新被抛出时,前面的一个异常对象的构造和传递过程肯定不会被影响,也即“按值传递”的方式下,异常被构造了3次,异常对象被“按值传递”到这个catch block中。实际上,需要研究的是,当异常被重新被抛出时,这个异常对象是否在离开当前的这个catch block域时会析构销毁掉,并且这个异常对象是否还会再次被复制构造?以及重新被抛出的异常对象按什么方式被传递?看如下例程:

class MyException  {  public:      MyException (string name="none") : m_name(name)      {          number = ++count;          cout << "构造一个MyException异常对象,名称为:"<<m_name<<":"<<number<< endl;      }      MyException (const MyException& old_e)      {          m_name = old_e.m_name;          number = ++count;          cout << "拷贝一个MyException异常对象,名称为:"<<m_name<<":"<<number<< endl;      }      operator= (const MyException& old_e)      {          m_name = old_e.m_name;          number = ++count;          cout << "赋值拷贝一个MyException异常对象,名称为:"<<m_name<<":"<<number<< endl;      }      virtual ~ MyException ()       {          cout << "销毁一个MyException异常对象,名称为:" <<m_name<<":"<<number<< endl;      }      string GetName()       {          char tmp[20];          memset(tmp, 0, sizeof(tmp));          sprintf(tmp, "%s:%d", m_name.c_str(), number);          return tmp;      }      virtual string Test_Virtual_Func() { return "这是MyException类型的异常对象";}  protected:      string m_name;      int number;      static int count;  };  int MyException::count = 0;  void main()  {      try      {          try          {              // 抛出一个异常对象              throw MyException("ex_obj1");          }          // 异常对象按值传递          catch(MyException e)          {              cout<<endl<<"捕获到一个MyException*类型的异常,名称为:"<<e.GetName()<<endl;              cout<<"下面重新抛出异常"<<endl<<endl;              // 异常对象重新被抛出              throw;          }      }      // 异常对象再次按值传递      catch(MyException e)      {          cout<<endl<<"捕获到一个MyException*类型的异常,名称为:"<<e.GetName()<<endl;      }  }  

  程序运行的结果是:
  构造一个MyException异常对象,名称为:ex_obj1:1
  拷贝一个MyException异常对象,名称为:ex_obj1:2
  拷贝一个MyException异常对象,名称为:ex_obj1:3
  销毁一个MyException异常对象,名称为:ex_obj1:1
  捕获到一个MyException*类型的异常,名称为:ex_obj1:3
  下面重新抛出异常
  拷贝一个MyException异常对象,名称为:ex_obj1:4
  销毁一个MyException异常对象,名称为:ex_obj1:3
  捕获到一个MyException*类型的异常,名称为:ex_obj1:4
  销毁一个MyException异常对象,名称为:ex_obj1:4
  销毁一个MyException异常对象,名称为:ex_obj1:2
  通过上面的程序运行结果,可以很明显地看出,异常对象在被重新抛出时,又有了一次拷贝复制的过程,瞧瞧!正常情况下,按值传递异常的方式应该是有3次构造对象的过程,可现在有了4次。那么这个异常对象在什么时候又再次被复制构造的呢?仔细分析一下,其实也不难明白, “异常对象ex_obj1:1”是局部变量;“异常对象ex_obj1:2”是临时变量;“异常对象ex_obj1:3”是第一个(内层的)catch block中的参数变量。当在catch block中再次throw异常对象时,它会即刻准备离开当前的catch block域,继续往上搜索对应的catch block模块,找到后,即完成异常对象的又一次复制构造过程,也即把异常对象传递给上一层的catch block域中。之后,正式离开内层的catch block域,并析构销毁这个catch block域中的异常对象ex_obj1:3,注意此时,属于临时变量形式的异常对象ex_obj1:2并没有被析构,而是直至到后一个catch block处理完后,先析构销毁异常对象ex_obj1:4,再才销毁异常对象ex_obj1:2。整个程序的执行流程如图14-1所示。

图14-1异常对象构造和销毁的过程
  下面来一步一步看它的流程,第①步执行的操作,及执行完它之后的状态,如图14-2所示。它构造了一个局部变量形式的异常对象和拷贝复制了一个临时变量形式的异常对象。



图14-2 第一步,抛出异常
  由于第①步执行的抛出异常的操作,因此找到了相应的catch block后,便执行第②步复制异常对象的过程,如图14-3所示。



图14-3 第二步,复制异常到catch block域
  第②步复制异常对象完毕后,便进入到第③步,离开原来抛出异常的作用域,如图14-4所示。这一步的操作是由系统所完成的,主要析构销毁这个当前作用域已经构造过的对象,其中也包括属于局部变量的异常对象。



图14-4 第三步,析构局部变量
  接下来正式进入到catch block的异常处理模块中,如图14-5所示。



图14-5 第四步,异常处理模块中
  当在异常模块中再次原封不动地把原来的异常对象重新抛出,那么系统将会继续下一次的查找catch block模块的过程,如图14-6所示。注意,它这里并不会再次复制一个另外的临时异常对象,而只是在新的catch block模块中完成一次异常对象的复制过程。



图14-6 第五步,又一次复制异常到catch block域
  同样,在复制完毕异常对象以后。程序控制流又会回到原来的作用域去销毁局部变量。如图14-7所示。注意,这里并不会析构销毁临时变量的异常对象,而只是销毁当前作用域内部的局部变量,如“异常对象ex_obj1:3”。



图14-7 第六步,离开前面的catch block作用域,并析构该作用域范围内的局部变量
  再接下来就是进入到又一次的异常处理模块中,如图14-8所示。注意,此时系统中存在
“异常对象ex_obj1:2”和“异常对象ex_obj1:4”。



图14-8 第七步,又一次的异常处理模块中
  最后就是全部处理完毕,如图14-9所示。注意,它先析构销毁“异常对象ex_obj1:4”,再才销毁“异常对象ex_obj1:2”。



图14-9 第八步,销毁另外的两个异常对象
  通过以上可以清晰地看出,在“按值传递”的方式下,异常对象的被重新rethrow后,它的执行过程虽然与正常抛出异常的情况虽然有所差异,但是在原理上,它们完全是相一致的,而且异常的rethrow是可以不断地向上抛出,就好像是接力赛一样,同时,每再抛出一次后,异常对象将会被复制构造一次。所以说,这里更进一步说明了异常对象的“按值传递”的方式是效率很低的。但是亲爱的程序员朋友们,大家是否也象那个有点傻气,但好像又有些灵气的主人公阿愚一样,联想到了另外一种有些奇怪的组合方式,那就是如果异常对象第一次是“按值传递”的方式,但第二、第三次,甚至后来的更多次,是否可以按其它方式(如“按引用传递” 的方式)呢?如果可以,那么又会出现什么结果呢?还是先看看示例吧!上面的程序只作了一点改动,如下:

void main()  {      try      {          try          {              // 抛出一个异常对象              throw MyException("ex_obj1");          }          // 异常对象按值传递          catch(MyException e)          {              cout<<endl<<"捕获到一个MyException*类型的异常,名称为:"<<e.GetName()<<endl;              cout<<"下面重新抛出异常"<<endl<<endl;              // 异常对象重新被抛出              throw;          }      }      // 注意,这里改为按引用传递的方式      catch(MyException& e)      {          cout<<endl<<"捕获到一个MyException*类型的异常,名称为:"<<e.GetName()<<endl;      }  }  
 
  程序运行的结果是:
  构造一个MyException异常对象,名称为:ex_obj1:1
  拷贝一个MyException异常对象,名称为:ex_obj1:2
  拷贝一个MyException异常对象,名称为:ex_obj1:3
  销毁一个MyException异常对象,名称为:ex_obj1:1
  捕获到一个MyException*类型的异常,名称为:ex_obj1:3
  下面重新抛出异常
  销毁一个MyException异常对象,名称为:ex_obj1:3
  捕获到一个MyException*类型的异常,名称为:ex_obj1:2
  销毁一个MyException异常对象,名称为:ex_obj1:2
  哈哈!主人公阿愚非常开心。因为它果然不出所料,是完全可以的,而且结果也合乎情理。异常对象还是只构造了三次,并未因为异常的再次抛出,而多复制构造一次异常对象。实际上,大家已经知道,控制异常对象的传递方式是由catch block后面的参数所决定,所以对于无论是最初抛出的异常,还是异常在catch block块被再次抛出,它们无须来关心,也控制不了。在最初抛出的异常时,完成两次异常对象的构造过程,其中最重要的是临时的异常对象,它是提供向其它参数形式的异常对象复制构造的原型,也即异常在不断接力地被抛出之后,如果上层的某个catch block定义“按值传递”的方式,那么系统就会从这个临时变量的异常对象复制一份;如果上层的某个catch block定义“按引用传递”的方式,那么系统会把引用指向这个临时变量的异常对象。而这个临时变量的异常对象,只有在最后一个catch block块(也即没有再次抛出)执行处理完毕之后,才会把这个异常对象予以析构销毁(实际上,在这里销毁是最恰当的,因为异常的重新被抛出,表明这个异常还没有被处理完毕,所以只有到最后一个catch block之后,这个临时变量的异常对象才真正不需要了)。

  另外,还有一点需要进一步阐述,那就是上层的某个catch block定义“按值传递”的方式下,系统从临时变量的异常对象所复制一份参数形式的异常对象,它一定会在它这个作用域无效时,把它给析构销毁掉。


  2、接下来,讨论异常对象“按引用传递”的方式下,异常对象的构造、传递和析构销毁的过程有何不同之处?其实这在刚才已经详细讨论过了,不过,还是看看例程来验证一下,如下:

void main()  {      try      {          try          {              // 抛出一个异常对象              throw MyException("ex_obj1");          }          // 这里改为按引用传递的方式          catch(MyException& e)          {              cout<<endl<<"捕获到一个MyException*类型的异常,名称为:"<<e.GetName()<<endl;              cout<<"下面重新抛出异常"<<endl<<endl;              // 异常对象重新被抛出              throw;          }      }      // 这里改为按引用传递的方式      catch(MyException& e)      {          cout<<endl<<"捕获到一个MyException*类型的异常,名称为:"<<e.GetName()<<endl;      }  }  

  程序运行的结果是:
  构造一个MyException异常对象,名称为:ex_obj1:1
  拷贝一个MyException异常对象,名称为:ex_obj1:2
  销毁一个MyException异常对象,名称为:ex_obj1:1
  捕获到一个MyException*类型的异常,名称为:ex_obj1:2
  下面重新抛出异常
  捕获到一个MyException*类型的异常,名称为:ex_obj1:2
  销毁一个MyException异常对象,名称为:ex_obj1:2
  结果不出所料,异常对象永远也只会被构造两次。所以异常对象“按引用传递”的方式,是综合性能最好的一种方式,效率既非常高(仅比“按指针传递”的方式多一次),同时也很安全和友善直观(这一点比“按指针传递”的方式好很多)。另外,这里也同样可以把“按引用传递”的方式和“按值传递”的方式相混合,代码示例如下:

void main()  {      try      {          try          {              // 抛出一个异常对象              throw MyException("ex_obj1");          }          // 这里按引用传递的方式          catch(MyException& e)          {              cout<<endl<<"捕获到一个MyException*类型的异常,名称为:"<<e.GetName()<<endl;              cout<<"下面重新抛出异常"<<endl<<endl;              // 异常对象重新被抛出              throw;          }      }      // 这里按值传递的方式      catch(MyException e)      {          cout<<endl<<"捕获到一个MyException*类型的异常,名称为:"<<e.GetName()<<endl;      }  }  
 
  3、最后,讨论异常对象“按指针传递”的方式下,异常对象的构造、传递和析构销毁的过程有何不同之处?其实这种方式不需要过多讨论,因为异常对象“按指针传递”的方式下,异常对象永远也只会需要被构造一次,实际上,它被传递只是一个32bit的指针值而已,不会涉及到异常对象的拷贝复制过程。但是有一点是需要注意的,那就是对异常对象的析构销毁必须要放在最后一个catch block处理完之后,中间层的catch block是决不应该delete掉这个一般在堆中分配的异常对象。

  catch block块处理异常时,又激发了另外一个异常的抛出

  呵呵!表面上看起来,这种情况下会很复杂,因为好像前面一个异常错误还没有被处理完,又引发了另外的一个异常错误,岂不是很麻烦呀!其实不然,系统对这种接力方式的异常重新抛出的处理策略往往很简单,那就是系统认为,当在catch block的代码执行过程中,如果抛出另一个异常,而导致控制流离开此catch block域,那么前一个异常会被认为处理完毕,并释放临时的异常对象,同时产生下一个异常的搜索catch block过程和异常处理的过程等。也即就是说,系统会把这种异常的重新抛出情况,认为是两次分离的异常。虽然它们是连在一起,并能够形成异常的接力抛出,但是处理上,它们完全是被分开进行的。所以说,这种情况下,往往会产生后一次异常对前一次异常的覆盖。


另一种特殊的形式的异常被重新抛出

  前面我们所讨论的异常被重新抛出,它们都会导致控制流离开catch block模块,也即整个异常的接力处理过程是分层进行的。但实际上,异常的重新抛出后,是可以把它局限于当前的catch block域内,让它逃离不开这个作用域。也即通过在catch block再潜套一个try catch块,示例代码如下:

void main()  {      try      {          try          {              // 动态在堆中构造的异常对象              throw MyException("ex_obj1");          }          catch(MyException& e)          {              // 这里再潜套一个try catch块              try              {                  cout<<endl<<"捕获到一个MyException*类型的异常,名称为:"<<e.GetName()<<endl;                  cout<<"下面重新抛出异常"<<endl<<endl;                  //重新抛出异常                  throw;                  // 或者这样重新抛出异常                  throw e;              }              catch(MyException& ex)              {              }              // 永远也逃不出我的魔掌              catch(...)              {              }          }      }      catch(MyException& e)      {          cout<<endl<<"捕获到一个MyException*类型的异常,名称为:"<<e.GetName()<<endl;      }  }  

  呵呵,通过上面的方式,可以让当前的catch block的处理得以安全保障,防止可能潜在的异常再次出现,而导致上层可能处理不了其它的意外异常,而引发程序崩溃。所以说,C++异常模型的确是非常灵活,功能也非常强大。
总结
  (1) 异常重新的被抛出,它的处理过程虽然稍微略有些复杂,但是总体上还是比较易于理解的。它更不会影响并破坏前面章节中所讲述过的许多规则,它们的处理策略和思想是相一致的;
  (2) 再次强调,异常对象“按引用传递”的方式是综合性能最佳的方式。
  对C++中的异常处理机制的阐述,到此暂告一个段落,从下篇文章开始,主人公阿愚将继续开阔自身的视野,以对异常处理编程进入一个更加广泛的探讨之中。各位程序员朋友们,继续吧!



原创粉丝点击