time 
设为首页】【收藏本站
当前位置: 主页 > 程序设计 > C\C++\VC > C++基础 > 第11集 C++的异常对象按引用方式被传递

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

时间:2009-12-04 22:56 点击:1001次 字体:[ ]




  上一篇文章详细讨论了C++的异常对象按值传递的方式,本文继续讨论另外的一种的方式:引用传递。

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

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

  异常对象按引用方式传递

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

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

  void main()

  {

  try

  {

  {

  throw MyException();

  }

  }

  // 注意:这里是定义了引用的方式

  catch(MyException& e)

  {

  cout<<"捕获到一个MyException类型的异常,名称为:"<

  }

  }

  程序运行的结果是:

  构造一个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类型的异常,名称为:"<

  }

  cout<<"那个临时的异常对象应该是在这之前析构销毁"<

  }

  怎样来分析呢?当然最简单的方法是调试一下,跟踪它的ebp和esp的变化。首先在函数调用的地方和抛出异常的地方设置好断点,F5开始调试,截图如下:

   第11集 C++的异常对象按引用方式被传递_www.fengfly.com

  纪录一下ebp和esp的值(ebp 0012FF70;esp 0012FEF8),通过ebp和esp可以确定main函数的函数帧在栈中位置,F5继续,截图如下:

   第11集 C++的异常对象按引用方式被传递_www.fengfly.com

  同样也纪录一下ebp和esp的值(ebp 0012FE9C;esp 0012FE08),通过ebp和esp可以看出栈是往下扩展,此时的ebp和esp指向抛出异常的test2函数的函数帧在栈中位置,F5继续,此时抛出异常,控制进入main函数中的catch(MyException& e)中,截图如下:

   第11集 C++的异常对象按引用方式被传递_www.fengfly.com

  请注意了,现在ebp恢复了main函数在先前时的函数帧在栈中位置,但esp却并没有,它甚至比刚刚抛出异常的那个test2函数中的esp还要往下,这就是编译器编译程序时耍的小技巧,当前ebp和esp指向的函数帧实际上并不是真正的main函数原来的哪个函数帧,它实际上包含了多个函数的函数帧,因此catch block执行程序时当然不会发生覆盖。我们还是看看异常对象所引用指向的临时的变量究竟身在何处。截图如下:

   第11集 C++的异常对象按引用方式被传递_www.fengfly.com

  哈哈!e指向了0x0012fe7c内存区域,再看看上面的抛出异常的test2函数的函数帧的ebp和esp的值。结果0x0012fe7c恰好是ebp 0012FE9C和esp 0012FE08之间。

  不过阿愚又开始有点疑惑了,哦!这样做岂不是破坏了函数的帧栈吗,结果还不导致程序崩溃呀!呵呵!不用担心,F5继续,截图如下:

   第11集 C++的异常对象按引用方式被传递_www.fengfly.com

  当离开了catch block作用域之后,再看看ebp和esp的值,是不是和最开始的那个main函数进入时的ebp和esp一模一样,哈哈!恢复了,厉害吧!先暂时不管它是如何恢复的,总之ebp和esp都是得以恢复了,而且同时catch block执行时也不会发生异常对象的覆盖。这就解决了异常对象按引用传递时可能存在的不安全隐患。

  引用方式下,异常对象会发生对象切片吗?

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

  void main()

  {

  try

  {

  {

  MyMemoryException ex_obj1("ex_obj1");

  cout <

  throw ex_obj1;

  }

  }

  // 注意这里引用的方式了

  // 还会发生了对象切片吗?

  catch(MyException& e)

  {

  // 调用虚函数,验证一下这个异常对象是否发生了对象切片

  cout<

  }

  }

  程序运行的结果是:

  构造一个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++的异常对象被按指针的方式传递。继续吧!



本文地址 : http://www.fengfly.com/plus/view-159896-1.html
标签:
------分隔线----------------------------
最新评论 查看所有评论
发表评论 查看所有评论
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
评价:
表情:
验证码: