预练武功,先修心法
看看金庸武侠小说中的那些少年俊才,在开始都是不一定有很高的技巧,但总是在年少的时候就或得到高人指点,或得到武林内功秘籍,并且都是心地善良。作为一名C++的入门弟子,同样,在碰到异常这个主题时,有两条要铭记在心:
1. 异常安全不是口号,从一开始就要贯彻执行。
在设计程序时就要考虑的异常的处理,不要给自己借口,说以后碰到了再说吧。千万不能懒惰!等把代码都写的七七八八了,再来添加异常处理。
2. 要有怀疑一切的谨慎态度,要知道,所有的语句都是靠不住的。
如果你拿不出100%的信心说这段代码一定exceptional-safe,那这段代码就是异常不安全的。
总之,首先要培养 异常安全的意识。
Exception-Safe
这里的safe就是说从表现给外部的功能上来说,我总是正常工作了。至于我为了正常工作,饿了肚子,取消了和女朋友的约会之类的事情,外部的调用代码你们不需要关心。
Exception-Neutral:
我只是个传话筒,我虽然是个基层领导,但是我不作为,一线的员工有什么意见,我一字不改的转达给我的上司。下层函数抛上来的异常,我原封不动的throw给上层函数。但是并不是所有的基层领导都是不作为的,他们要把底层员工的意思包装一下,整理一下…然后再汇报给上层,这就叫做non-fully exception-neutral.
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个已经被成功构造的对象的析构函数被调用,并且释放内存。所以整体状态也是合理的。
第一层: 我指保证没有泄漏资源,其他的就难说了。
可以参考两本书:《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状态就不对了!
这是异常处理的最高境界,善后工作做的事情好象从来没有发生过一样,这里面一个基本逻辑就是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_;
}
}
template <class T>
void swap( T& a, T& b )
{
T temp(a); a = b; b = temp;
}
{
swap( v_, other.v_ );
swap( vsize_, other.vsize_ );
swap( vused_, other.vused_ );
}
{
if( vused_ == vsize_ ) // grow if necessary
while( temp.Count() < vused_ )
}
temp.Push( t );
Swap( temp );
}
construct( v_+ vused_, t );
+ + vused_;
}
}