`

C++异常:rethrow【转】

 
阅读更多
C++异常rethrow【转】
http://se.csai.cn/ExpertEyes/200801031114531905.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异常对象,名称为:"<<<":"<<< p="" endl;<=""> <<":"<<<>
  }
  MyException (const MyException& old_e)
  {
  m_name = old_e.m_name;
  number = ++count;
  cout << "拷贝一个MyException异常对象,名称为:"<<<":"<<< p="" endl;<=""> <<":"<<<>
  }
  operator= (const MyException& old_e)
  {
  m_name = old_e.m_name;
  number = ++count;
  cout << "赋值拷贝一个MyException异常对象,名称为:"<<<":"<<< p="" endl;<=""> <<":"<<<>
  }
  virtual ~ MyException ()
  {
  cout << "销毁一个MyException异常对象,名称为:" <<<":"<<< p="" 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<<<"捕获到一个myexception*类型的异常,名称为:"<<< p=""> <<"捕获到一个myexception*类型的异常,名称为:"<<<>
  cout<<"下面重新抛出异常"<<< p=""> <<>
  // 异常对象重新被抛出
  throw;
  }
  }
  // 异常对象再次按值传递
  catch(MyException e)
  {
  cout<<<"捕获到一个myexception*类型的异常,名称为:"<<< p=""> <<"捕获到一个myexception*类型的异常,名称为:"<<<>
  }
  }
  程序运行的结果是:
  构造一个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<<<"捕获到一个myexception*类型的异常,名称为:"<<< p=""> <<"捕获到一个myexception*类型的异常,名称为:"<<<>
  cout<<"下面重新抛出异常"<<< p=""> <<>
  // 异常对象重新被抛出
  throw;
  }
  }
  // 注意,这里改为按引用传递的方式
  catch(MyException& e)
  {
  cout<<<"捕获到一个myexception*类型的异常,名称为:"<<< p=""> <<"捕获到一个myexception*类型的异常,名称为:"<<<>
  }
  }
  程序运行的结果是:
  构造一个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<<<"捕获到一个myexception*类型的异常,名称为:"<<< p=""> <<"捕获到一个myexception*类型的异常,名称为:"<<<>
  cout<<"下面重新抛出异常"<<< p=""> <<>
  // 异常对象重新被抛出
  throw;
  }
  }
  // 这里改为按引用传递的方式
  catch(MyException& e)
  {
  cout<<<"捕获到一个myexception*类型的异常,名称为:"<<< p=""> <<"捕获到一个myexception*类型的异常,名称为:"<<<>
  }
  }
  程序运行的结果是:
  构造一个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<<<"捕获到一个myexception*类型的异常,名称为:"<<< p=""> <<"捕获到一个myexception*类型的异常,名称为:"<<<>
  cout<<"下面重新抛出异常"<<< p=""> <<>
  // 异常对象重新被抛出
  throw;
  }
  }
  // 这里按值传递的方式
  catch(MyException e)
  {
  cout<<<"捕获到一个myexception*类型的异常,名称为:"<<< p=""> <<"捕获到一个myexception*类型的异常,名称为:"<<<>
  }
  }
  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<<<"捕获到一个myexception*类型的异常,名称为:"<<< p=""> <<"捕获到一个myexception*类型的异常,名称为:"<<<>
  cout<<"下面重新抛出异常"<<< p=""> <<>
  //重新抛出异常
  throw;
  // 或者这样重新抛出异常
  throw e;
  }
  catch(MyException& ex)
  {
  }
  // 永远也逃不出我的魔掌
  catch(...)
  {
  }
  }
  }
  catch(MyException& e)
  {
  cout<<<"捕获到一个myexception*类型的异常,名称为:"<<< p=""> <<"捕获到一个myexception*类型的异常,名称为:"<<<>
  }
  }
  呵呵,通过上面的方式,可以让当前的catch block的处理得以安全保障,防止可能潜在的异常再次出现,而导致上层可能处理不了其它的意外异常,而引发程序崩溃。所以说,C++异常模型的确是非常灵活,功能也非常强大。
  总结
  (1) 异常重新的被抛出,它的处理过程虽然稍微略有些复杂,但是总体上还是比较易于理解的。它更不会影响并破坏前面章节中所讲述过的许多规则,它们的处理策略和思想是相一致的;
  (2) 再次强调,异常对象“按引用传递”的方式是综合性能最佳的方式。
  对C++中的异常处理机制的阐述,到此暂告一个段落,从下篇文章开始,主人公阿愚将继续开阔自身的视野,以对异常处理编程进入一个更加广泛的探讨之中。各位程序员朋友们,继续吧!
分享到:
评论

相关推荐

    C++Exception 异常处理 源码

    在C++编程中,异常处理是一项关键的错误处理机制,它允许程序员在程序运行时捕获和处理意外的情况。...提供的源码应该包含了如何在实际程序中使用这些概念的例子,是学习和理解C++异常处理的好资源。

    C++中的意外处理技术

    这三个关键字共同构成了C++异常处理的基础框架。 1. **`try` 块**:`try`块是包含可能抛出异常的代码的部分。如果在`try`块内部的代码抛出了一个异常,控制权将立即转移到相应的`catch`块。 ```cpp try { // 可能...

    实验7(异常).zip

    通过这个实验,你将加深对C++异常处理的理解,学会编写健壮的代码来处理运行时错误,从而使你的程序更加稳定和可靠。记得在实践中不断探索和改进,因为异常处理是成为一名优秀的C++程序员不可或缺的一部分。

    Bjarne Stroustrup的FAQ:C++的风格与技巧

    如果想要恢复异常之前的执行流程,可以在try块中使用`noexcept`指定或在catch块中使用`rethrow`来重新抛出异常。 ##### (17) 为什么C++中没有相当于realloc()的函数? C++没有直接提供`realloc()`这样的函数,因为...

    cps-future:C ++ FuturePromises实现,大致基于Future.pm

    错误处理基于异常的错误处理依赖于std :: current_exception和std :: rethrow_exception。 如果您预计会遇到许多错误情况,这些速度可能会很慢。其他实施std :: future 理想情况下,我们可以将核心库功能用于期货...

    ebt_limit.rar_limit

    在IT行业中,异常处理是程序设计中的重要组成部分,特别是在C++等编程语言中。这个"ebt_limit.rar_limit"的测试案例聚焦于正确处理重新抛出(rethrow)异常的场景。下面我们将深入探讨相关知识点。 首先,让我们...

    The C++ Programming Language(ch 14)

    ### 异常处理在C++中的应用 #### 14.1 错误处理 [except.error] 正如第8.3节所述,库作者可以检测到运行时错误,但通常不知道如何处理这些错误。而库的用户可能知道如何应对这类错误,但却无法检测到它们——否则...

    C++模拟题5答案.docx

    本C++模拟试卷涉及的知识点包括:静态成员函数的特性、构造函数的作用、变量的作用域、构造函数的调用次数、运算符重载、成员函数的调用规则、内联函数、解决二义性问题的方法、访问控制权限、模板、异常处理、指针...

    异常机制分析

    #### 异常的重新抛出(`rethrow`) 异常的重新抛出是指在`catch`块中再次抛出异常的行为。这通常发生在两种情况下:一种是在当前`catch`块无法处理异常时,另一种是在处理过程中触发了新的异常。异常的重新抛出只能...

    linux程序栈回溯

    通过捕获`std::current_exception`,然后使用`std::rethrow_exception`,可以重新触发异常并进行栈回溯。 在生产环境中,为了不影响服务,可以配置核心转储(core dump)。当程序崩溃时,系统会生成一个包含了程序...

    Visual C ++中的有效异常处理

    10. **异常传播**:如果一个函数在其调用的子函数中捕获了异常,它可以选择重新抛出(rethrow)该异常,这使得异常可以沿着调用栈向上传播,直到找到合适的处理程序。 通过有效地利用Visual C++的异常处理机制,...

Global site tag (gtag.js) - Google Analytics