一、 綜述
我很少敢為自己寫的東西弄個詳解的標題,之所以這次敢于這樣,自然還算是有點底氣的。并且也以此為動力,督促自己好好的將這兩個東西研究透。
當年剛開始工作的時候,第一個工作就是學習breakpad的源代碼,然后了解其原理,為公司寫一個ExceptionHandle的庫,以處理服務器及客戶端的未處理異常(unhandle exception),并打下dump,以便事后分析,當年這個功能在有breakpad的示例在前時,實現難度并不大,無非就是調用了SetUnhandledExceptionFilter等函數,讓windows在出現未處理異常時讓自己的回調函數接管操作,然后利用其struct _EXCEPTION_POINTERS*ExceptionInfo的指針,通過MiniDumpWriteDump API將Dump寫下來。但是仍記得,那時看到《Windows 核心編程》第五部分關于結構化異常處理的描述時那種因為得到新鮮知識時的興奮感,那是我第一次這樣接近Windows系統的底層機制,如同以前很多次說過的,那以后我很投入的捧著讀完了《Windows 核心編程》,至今受益匪淺。當時也有一系列一邊看源代碼一邊寫下心得的時候,想想,都已經一年以前的事情了。
《讀windows核心編程,結構化異常部分,理解摘要》
《Breakpad在進程中完成dump的流程描述》
《Breakpad 使用方法理解文檔》
直到最近,為了控制服務器在出現異常時不崩潰,(以前是崩潰的時候打Dump),對SEH(windows結構化異常)又進行了進一步的學習,做到了在服務器出現了異常情況(例如空指針的訪問)時,服務器打下Dump,并繼續運行,并不崩潰,結合以前也是我寫的監控系統,通知監控客戶端報警,然后就可以去服務器上取回dump,并分析錯誤,這對服務器的穩定性有很大的幫助,不管我們對服務器的穩定性進行了多少工作,作為C++程序,偶爾的空指針訪問,幾乎沒有辦法避免。。。。。。但是,這個工作,對這樣的情況起到了很好的緩沖作用。在這上面工作許久,有點心得,寫下來,供大家分享,同時也是給很久以后的自己分享。
二、 為什么需要異常
《Windows核心編程》第4版第13章開頭部分描述了一個美好世界,即所編寫的代碼永遠不會執行失敗,總是有足夠的內存,不存在無效的指針。。。。但是,那是不存在的世界,于是,我們需要有一種異常的處理措施,在C語言中最常用的(其實C++中目前最常用的還是)是利用錯誤代碼(Error Code)的形式。
這里也為了更好的說明,也展示一下Error Code的示例代碼:
Error Code常用方式:
1.最常用的就是通過返回值判斷了:
比如C Runtime Library中的fopen接口,一旦返回NULL,Win32 API中的CreateFiley一旦返回INVALID_HANDLE_VALUE,就表示執行失敗了。
2.當返回值不夠用(或者攜帶具體錯誤信息不夠的)時候,C語言中也常常通過一個全局的錯誤變量來表示錯誤。
比如C Runtime Library中的errno 全局變量,Win32 API中的GetLastError,WinSock中的WSAGetLastError函數就是這種實現。
既然Error Code在這么久的時間中都是可用的,好用的,為什么我們還需要其他東西呢?
這里可以參考一篇比較淺的文章?!?/span>錯誤處理和異常處理,你用哪一個》,然后本人比較欽佩的pongba還有一篇比較深的文章:《錯誤處理(Error-Handling):為何、何時、如何(rev#2)》,看了后你一定會大有收獲。當pongba列出了16條使用異常的好處后,我都感覺不到我還有必要再去告訴你為什么我們要使用異常了。
但是,這里在其無法使用異常的意外情況下,(實際是《C++ Coding Standards: 101 Rules, Guidelines, and Best Practices》一書中所寫)
一, 用異常沒有帶來明顯的好處的時候:比如所有的錯 誤都會在立即調用端解決掉或者在非常接近立即調用端的地方解決掉。
二, 在實際作了測定之后發現異常的拋出和捕獲導致了顯著的時間開銷:這通常只有兩種情 況,要么是在內層循環里面,要么是因為被拋出的異常根本不對應于一個錯誤。
很明顯,文中列舉的都是完全理論上理想的情況,受制于國內的開發環境,無論多么好的東西也不一定實用,你能說國內多少地方真的用上了敏捷開發的實踐經驗?這里作為現實考慮,補充幾個沒有辦法使用異常的情況:
一. 所在的項目組中沒有合理的使用RAII的習慣及其機制,比如無法使用足夠多的smart_ptr時,最好不要使用異常,因為異常和RAII的用異常不用RAII就像吃菜不放鹽一樣。這一點在后面論述一下。
二. 當項目組中沒有使用并捕獲異常的習慣時,當項目組中認為使用異常是奇技淫巧時不要使用異常。不然,你自認為很好的代碼,會在別人眼里不可理解并且作為異類,接受現實。
三、 基礎篇
先回顧一下標準C++的異常用法
1. C++標準異常
只有一種語法,格式類似:
try
{
}
catch()
{
}
經常簡寫為try-catch,當然,也許還要算上throw。格式足夠的簡單。
以下是一個完整的例子:
MyException:
#include <string>
#include <iostream>
using namespace std;
class MyException : public exception
{
public:
MyException(const char* astrDesc)
{
mstrDesc = astrDesc;
}
string mstrDesc;
};
int _tmain(int argc, _TCHAR* argv[])
{
try
{
throw MyException("A My Exception");
}
catch(MyException e)
{
cout <<e.mstrDesc <<endl;
}
return 0;
}
這里可以體現幾個異常的優勢,比如自己的異常繼承體系,攜帶足夠多的信息等等。另外,雖然在基礎篇,這里也講講C++中異常的語義,
如下例子中,
throwException:
#include <string>
#include <iostream>
using namespace std;
class MyException : public exception
{
public:
MyException(const char* astrDesc)
{
mstrDesc = astrDesc;
}
MyException(const MyException& aoOrig)
{
cout <<"Copy Constructor MyException" <<endl;
mstrDesc = aoOrig.mstrDesc;
}
MyException& operator=(const MyException& aoOrig)
{
cout <<"Copy Operator MyException" <<endl;
if(&aoOrig == this)
{
return *this;
}
mstrDesc = aoOrig.mstrDesc;
return *this;
}
~MyException()
{
cout <<"~MyException" <<endl;
}
string mstrDesc;
};
void exceptionFun()
{
try
{
throw MyException("A My Exception");
}
catch(MyException e)
{
cout <<e.mstrDesc <<" In exceptionFun." <<endl;
e.mstrDesc = "Changed exception.";
throw;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
try
{
exceptionFun();
}
catch(MyException e)
{
cout <<e.mstrDesc <<" Out exceptionFun." <<endl;
throw;
}
return 0;
}
輸出:
Copy Constructor MyException
A My Exception In exceptionFun.
~MyException
Copy Constructor MyException
A My Exception Out exceptionFun.
~MyException
可以看出當拋出C++異常的copy語義,拋出異常后調用了Copy Constructor,用新建的異常對象傳入catch中處理,所以在函數中改變了此異常對象后,再次拋出原異常,并不改變原有異常。
這里我們經過一點小小的更改,看看會發生什么:
throwAnotherException
#include <string>
#include <iostream>
using namespace std;
class MyException : public exception
{
public:
MyException(const char* astrDesc)
{
mstrDesc = astrDesc;
}
MyException(const MyException& aoOrig)
{
cout <<"Copy Constructor MyException" <<endl;
mstrDesc = aoOrig.mstrDesc;
}
MyException& operator=(const MyException& aoOrig)
{
cout <<"Copy Operator MyException" <<endl;
if(&aoOrig == this)
{
return *this;
}
mstrDesc = aoOrig.mstrDesc;
return *this;
}
~MyException()
{
cout <<"~MyException" <<endl;
}
string mstrDesc;
};
void exceptionFun()
{
try
{
throw MyException("A My Exception");
}
catch(MyException e)
{
cout <<e.mstrDesc <<" In exceptionFun." <<endl;
e.mstrDesc = "Changed exception.";
throw e;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
try
{
exceptionFun();
}
catch(MyException e)
{
cout <<e.mstrDesc <<" Out exceptionFun." <<endl;
throw;
}
return 0;
}
這里和throwException程序的唯一區別就在于不是拋出原有異常,而是拋出改變后的異常,輸出如下:
Copy Constructor MyException
A My Exception In exceptionFun.
Copy Constructor MyException
Copy Constructor MyException
~MyException
~MyException
Changed exception. Out exceptionFun.
~MyException
你會發現連續的兩次Copy Constructor都是改變后的異常對象,這點很不可理解。。。。。。。因為事實上一次就夠了。但是理解C++的Copy異常處理語義就好理解了,一次是用于傳入下一次的catch語句中的,還有一次是留下來,當在外層catch再次throw時,已經拋出的是改變過的異常對象了,我用以下例子來驗證這點:
throwTwiceException
#include <string>
#include <iostream>
using namespace std;
class MyException : public exception
{
public:
MyException(const char* astrDesc)
{
mstrDesc = astrDesc;
}
MyException(const MyException& aoOrig)
{
cout <<"Copy Constructor MyException" <<endl;
mstrDesc = aoOrig.mstrDesc;
}
MyException& operator=(const MyException& aoOrig)
{
cout <<"Copy Operator MyException" <<endl;
if(&aoOrig == this)
{
return *this;
}
mstrDesc = aoOrig.mstrDesc;
return *this;
}
~MyException()
{
cout <<"~MyException" <<endl;
}
string mstrDesc;
};
void exceptionFun()
{
try
{
throw MyException("A My Exception");
}
catch(MyException e)
{
cout <<e.mstrDesc <<" In exceptionFun." <<endl;
e.mstrDesc = "Changed exception.";
throw e;
}
}
void exceptionFun2()
{
try
{
exceptionFun();
}
catch(MyException e)
{
cout <<e.mstrDesc <<" In exceptionFun2." <<endl;
throw;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
try
{
exceptionFun2();
}
catch(MyException e)
{
cout <<e.mstrDesc <<" Out exceptionFuns." <<endl;
throw;
}
return 0;
}
輸出如下,印證了我上面的說明。
Copy Constructor MyException
A My Exception In exceptionFun.
Copy Constructor MyException
Copy Constructor MyException
~MyException
~MyException
Changed exception. In exceptionFun2.
~MyException
Copy Constructor MyException
Changed exception. Out exceptionFuns.
上面像語言律師一樣的討論著C++本來已經足夠簡單的異常語法,其實簡而言之,C++總是保持著一個上次拋出的異常用于用戶再次拋出,并copy一份在catch中給用戶使用。
但是,實際上,會發現,其實原有的異常對象是一直向上傳遞的,只要你不再次拋出其他異常,真正發生復制的地方在于你catch異常的時候,這樣,當catch時使用引用方式,那么就可以避免這樣的復制。
referenceCatch
#include <string>
#include <iostream>
using namespace std;
class MyException : public exception
{
public:
MyException(const char* astrDesc)
{
mstrDesc = astrDesc;
}
MyException(const MyException& aoOrig)
{
cout <<"Copy Constructor MyException: " <<aoOrig.mstrDesc <<endl;
mstrDesc = aoOrig.mstrDesc;
}
MyException& operator=(const MyException& aoOrig)
{
cout <<"Copy Operator MyException:" <<aoOrig.mstrDesc <<endl;
if(&aoOrig == this)
{
return *this;
}
mstrDesc = aoOrig.mstrDesc;
return *this;
}
~MyException()
{
cout <<"~MyException" <<endl;
}
string mstrDesc;
};
void exceptionFun()
{
try
{
throw MyException("A My Exception");
}
catch(MyException& e)
{
cout <<e.mstrDesc <<" In exceptionFun." <<endl;
e.mstrDesc = "Changed exception.";
throw;
}
}
void exceptionFun2()
{
try
{
exceptionFun();
}
catch(MyException e)
{
cout <<e.mstrDesc <<" In exceptionFun2." <<endl;
throw;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
try
{
exceptionFun2();
}
catch(MyException e)
{
cout <<e.mstrDesc <<" Out exceptionFuns." <<endl;
throw;
}
return 0;
}
上例中,使用引用方式來捕獲異常,輸出如下:
A My Exception In exceptionFun.
Copy Constructor MyException: Changed exception.
Changed exception. In exceptionFun2.
~MyException
Copy Constructor MyException: Changed exception.
Changed exception. Out exceptionFuns.
~MyException
完全符合C++的引用語義。
基本可以發現,做了很多無用功,因為try-catch無非是一層迷霧,其實這里復制和引用都還是遵循著原來的C++簡單的復制,引用語義,僅僅這一層迷霧,讓我們看不清楚原來的東西。所以,很容易理解一個地方throw一個對象,另外一個地方catch一個對象一定是同一個對象,其實不然,是否是原來那個對象在于你傳遞的方式,這就像這是個參數,通過catch函數傳遞進來一樣,你用的是傳值方式,自然是通過了復制,通過傳址方式,自然是原有對象,僅此而已。
另外,最終總結一下,《C++ Coding Standards》73條建議Throw by value,catch by reference就是因為本文描述的C++的異常特性如此,所以才有此建議,并且,其補上了一句,重復提交異常的時候用throw;
四、 參考資料
1. Windows核心編程(Programming Applications for Microsoft Windows),第4版,Jeffrey Richter著,黃隴,李虎譯,機械工業出版社
2. MSDN—Visual Studio 2005 附帶版,Microsoft
3. 錯誤處理和異常處理,你用哪一個,apollolegend
4. 錯誤處理(Error-Handling):為何、何時、如何(rev#2),劉未鵬