預練武功,先修心法
看看金庸武俠小說中的那些少年俊才,在開始都是不一定有很高的技巧,但總是在年少的時候就或得到高人指點,或得到武林內功祕籍,並且都是心地善良。作爲一名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_;
}
}