C++异常处理的三个境界

原创 2011年01月07日 17:41:00
2005年5月份,我转正后1个月,组里组织我们到青岛旅游,那个时候我正在看Exceptional C++这本书,有一个章节一直看不懂,就打印了带到青岛去了,嘿嘿,旅游还是有助于激发灵感的,在旅馆里我终于看懂了,回来以后总结了一个PPT。这个PPT很有特点,因为我做了一个Q版。时光飞逝阿,转眼2年多过了,房价又涨了好多,本来可以买两室两厅的钱只够一室一厅了。特重写C++异常处理献给我们可爱的房地产开发商.
 
Exception-Safety Issues and Techniques
预练武功,先修心法
看看金庸武侠小说中的那些少年俊才,在开始都是不一定有很高的技巧,但总是在年少的时候就或得到高人指点,或得到武林内功秘籍,并且都是心地善良。作为一名C++的入门弟子,同样,在碰到异常这个主题时,有两条要铭记在心:
1. 异常安全不是口号,从一开始就要贯彻执行。
在设计程序时就要考虑的异常的处理,不要给自己借口,说以后碰到了再说吧。千万不能懒惰!等把代码都写的七七八八了,再来添加异常处理。
2. 要有怀疑一切的谨慎态度,要知道,所有的语句都是靠不住的。
如果你拿不出100%的信心说这段代码一定exceptional-safe,那这段代码就是异常不安全的。
总之,首先要培养 异常安全的意识。
两个概念:
Exception-Safe
这里的safe就是说从表现给外部的功能上来说,我总是正常工作了。至于我为了正常工作,饿了肚子,取消了和女朋友的约会之类的事情,外部的调用代码你们不需要关心。
Exception-Neutral:
我只是个传话筒,我虽然是个基层领导,但是我不作为,一线的员工有什么意见,我一字不改的转达给我的上司。下层函数抛上来的异常,我原封不动的throw给上层函数。但是并不是所有的基层领导都是不作为的,他们要把底层员工的意思包装一下,整理一下…然后再汇报给上层,这就叫做non-fully exception-neutral.
new和delete: 你们到底做了什么?!
new和delete的两步曲,我想大家都了解的:分配内存和调用构造函数/调用析构函数和释放内存。本着怀疑一切的态度,如果分配内存失败了呢,如果构造函数失败了呢,如果析构函数失败了呢。就new和delete的层次上,编译器已经帮我们做了很多工作,如果我们希望new和delete是Exception-Safe的,最好打开编译器的/GX开关。
我们来看一段小程序
template <class T>
Stack<T>::Stack()
:v_(0),     //这个Stack底层用数组实现,数组指针初始化为0
vsize_(10), //希望为数组分配可以容纳10个元素的内存
vused_(0)   //现在这个Stack为空
{
 v_ = new T[vsize_]; //initial allocation
}
分析一下这段代码
1.这段代码是exception-neutral的。如果new抛异常,Stack的构造函数只是原封不动的将下层异常throw给上层。
2.这段代码没有内存泄漏。如果内存分配了,但是某个T的构造函数失败,/GX会保证new分配的内存会被释放
3.这段代码的状态是合理的。如果内存分配失败,std::bad_alloc被抛出,Stack的构造函数失败,这是合理的。如果内存分配成功,第N(N<10)个T对象的构造函数失败,/GX会保证前面N-1个已经被成功构造的对象的析构函数被调用,并且释放内存。所以整体状态也是合理的。
好了,正式进入修炼,本心法一共有3层
第一层: 我指保证没有泄漏资源,其他的就难说了。
可以参考两本书:《Coping with Exceptions》 和 《More Effective C++》
怎么能保证我能够到达第一层呢? 
第一层还是挺简单的,只要不泄漏资源(内存)就可以了,一个常用的技巧是把一定要在函数退出时需要释放的资源封装在一个临时对象中,这样可以保证在函数退出时临时对象析构时将资源释放。
为了保证第一层还有一句话是这样讲的:
恶魔!抛出异常的析构函数。为什么这样说呢,并不是说析构函数不能抛出异常,原因是这样的,如果析构函数是由于用户代码造成的,那不要紧。但是有时候析构函数是由/GX以后由编译器添加的代码调用的,看这样的情况:
CMyObject* p = new CMyObject[10];
如果内存被成功分配,在调用第7个CMyObject的构造函数的时候有失败,编译器为了保证行为合理,会依次调用前面成功的6个对象的析构函数然后释放内存,如果在这个时候前面6个析构函数中任何一个失败(有异常抛出),就会调用Terminate终止程序,这是我们不愿意看到的。为了保证这一点,我们常常看到这样的函数声明:
void operator delete[](void*) throw();
void operator delete[](void*,size_t) throw();
 
第二层:我一切都很好,但还是受了内伤
我们看一段代码,秉着每一条语句都是靠不住的意识来分析一下
template<class T>
void Stack<T>::Push(const T& t)
{
     if(vused_ == vsize_) //内存不够,重新申请,等于判断不会抛异常
     {
          size_t vsize_new = vsize_*2 + 1; //将内存扩大一倍,不会抛异常
          T* v_new         = NewCopy(v_,vsize_,vsize_new); //如果抛异常,内存没有分配,该Stack对象的状态不变(Safe)
          delete[] v_;     // this can’t throw(由第一层保证了)
          v_ = v_new;      // 夺取拥有权,赋值操作不抛异常
          vsize_ = vsize_new;  //赋值操作不抛异常
      }
      ++ vused_;         // 自加操作不抛异常
      v_[vused_] = t;   // 如果复制构造失败,Stack状态就不对了!
}
那不简单,我把最后两条语句改一下不就可以了吗?!
v_[vused_+1] =t;
++vused_;
恩,没有错,从功能表现上来说,这样就既满足了exceptional-safe 又满足了exceptionl-neutral。但是这个Stack还是受了内伤,她的内部状态改变了哦!细心一点,我们来看一下:
假设现在这个stack中指向的内存空间的地址是0×1000(v_=0×1000),内存大小为vsize_=10,已经用掉的内存大小vsize_=10,现在,在这个stack上进行一个push操作。好了,内存不够,按照上面代码的逻辑,假设到v_[vused_+1] =t;之前,所有的操作都成功,那现在stack中的状态是这样的:vsize_ =21, vused_=10,v_ =0×2000,现在v_[vused_+1] = t失败,有异常抛出,好了,从外部看,一切都还是合理的。但是在stack内部,vsize_和v_都被改掉了,内存消耗增加了。这就是“内伤”,后面我会谈到一个叫做shrink to fit的技巧来回收多余的内存。
 
第三层:异常她轻轻的来了,又轻轻的走了…
这是异常处理的最高境界,善后工作做的事情好象从来没有发生过一样,这里面一个基本逻辑就是commit-rollback。这里推荐一本书<SGI Exception-Safe Standard Library Adaptation>,作者是 Dave Abrahams.
怎么操作呢?
纵观人类历史,到处都有这样的案例(就算是程序员,也要好好学历史啊-:)):
找个替死鬼(Temp Object),赚了功劳都是我的(Swap),亏了过错都是他的(Destroy by Exception-Unwinding)!
这里有两个技术细节,我们一个一个来欣赏:
1. 精心制作的copy constructor
王道–〉
Stack::Stack(const Stack&other):StackImpl<T>(other.vused_)
{
   while(vused_ < other.vused_)
   {
        construct(v_ + vused_, other.v_[vused_]);
        ++ vused_;
   }
}
2. 偷天换日的swap
template <class T>
void swap( T& a, T& b )
{
     T  temp(a); a = b; b = temp;
}
void StackImpl<T>::Swap( StackImpl & other)  throw()
 {
       swap( v_,     other.v_ );
       swap( vsize_, other.vsize_ );
       swap( vused_, other.vused_ );
 }
 
现在,push的代码改成–>
void Push( const T& t )
 {
      if( vused_ == vsize_ )      // grow if necessary
      {
           Stack temp( vsize_*2+1 );
           while( temp.Count() < vused_ )
           {
                 temp.Push( v_[temp.Count()] );
           }
           temp.Push( t );
           Swap( temp );
      }
      else
      {
           construct( v_+ vused_, t );
           + + vused_;
      }
}
分析一下成功和失败的情况下stack中的状态变化.
 
<writing…>
版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

C++异常处理编程的三个境界

这是上一次看完Herb  Sutter的《Exceptional C++》 后形成的看法,因为懒于更新Blog,一直没有写下来。 一般讲到三个境界,很多人会联想到……#1见山是山,见水是水#2见山不...

Java异常处理的三个原则

Java中异常提供了一种识别及响应错误情况的一致性机制,有效地异常处理能使程序更加健壮、易于调试。异常之所以是一种强大的调试手段,在于其回答了以下三个问题: 什么出了错?在哪出的错?为什么出错?...

c++课件之异常处理

  • 2014-10-17 17:56
  • 394KB
  • 下载

C与C++中的异常处理

  • 2014-05-14 09:01
  • 530KB
  • 下载

c++异常处理机制

一、 概述C++自身有着非常强的纠错能力,发展到如今,已经建立了比较完善的异常处理机制。C++的异常情况无非两种,一种是语法错误,即程序中出现了错误的语句,函数,结构和类,致使编译程序无法进行。另一种...

C++ Java异常处理比较

  • 2014-06-21 21:12
  • 21KB
  • 下载

C与C++中的异常处理.pdf

  • 2011-02-09 20:16
  • 555KB
  • 下载

C++ 构造/析构函数中的异常处理

C++ 为什么会引入(需要)异常? The C++ Programming Language: 一个库的作者可以检测出发生了运行时错误,但一般不知道怎样去处理它们(因为和用户具体的应用有关)...

C和C++ 异常处理

  • 2010-05-13 00:24
  • 465KB
  • 下载

C与C++中的异常处理

  • 2010-02-27 12:25
  • 464KB
  • 下载
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)