7 異常與錯誤處理
7.1 異常
異常是C++語言的一個強大特性,在正確使用之前需要深入瞭解,以及使用異常代碼的上下文。
原則7.1 減少不必要的異常
說明:異常對編碼技能要求更高,使用中容易出錯,首先從安全性角度考慮,盡量少用或者不用異常。
相比返回錯誤,異常的優點:
異常可以集中捕捉,錯誤偵測與演算法處理相分離,演算法邏輯更清晰;而返回錯誤在每個返回點都
要進行檢測與錯誤處理,代碼邏輯分散。
異常的約束更強,使用者不能忽略拋出的異常,否則程式預設會被終止,而返回錯誤則可能被忽略。
異常的缺點也很明顯:
必須檢查所有調用點是否可能拋出異常,在拋出後必須正確處理狀態和資源變數等,否則可能導
致對象狀態不正確或者資源流失等。例如:如果f()依次調用了g()和h(),h拋出被f捕獲的異常,
g就要當心了,避免資源流失。
必須清楚可能拋出的所有異常,並在合適的地方捕捉,如果遺漏通常會導致程式被終止。
使用異常很難評估程式的控制流程,代碼很難調試。
目標檔案變大,編譯時間延長,效能下降。
若對異常缺乏充分理解,可能會在不恰當的時候拋出異常, 或在不安全的地方從異常中恢複。
適用異常的幾個情境:
出現“不應該出現的”失敗,且不能被忽略必須處理,比如分配記憶體失敗。
上層應用決定如何處理在底層嵌套函數中 “不可能出現的”失敗。
錯誤碼難以通過函數的傳回值或參數返回,比如流。
許多第三方C++庫使用異常,必須在系統邊界與第三方C++庫結合處使用異常便於跟這些庫整合。
在測試架構中使用異常很方便。
規則7.1 構造和解構函式不能拋出異常
說明:如果構造和解構函式執行失敗則無法安全地撤銷和復原,故這些函數不能向外拋出異常。
為了降低複雜性,建議在這類函數中實現最簡單的邏輯。
規則7.2 通過傳值的方式拋出,通過引用的方式捕獲
說明:拋出異常時,如果拋出指標,誰釋放指標就成為問題。捕捉時如果是傳值,會存在拷貝,拷貝可
能不成功(比如異常是由於記憶體耗盡造成的),而且拷貝得不到衍生類別對象,因為在拷貝時,衍生類別對象
會被切片成為基類對象。
規則7.3 確保拋出的異常一定能被捕捉到
說明:異常未被捕捉到,系統的預設行為是終止程式運行,所以要確保程式產生的異常都能被捕捉。
規則7.4 確保異常發生後資源不泄漏
說明:異常發生後,當前代碼執行序列被打斷,需要查看分配的記憶體、檔案和核心控制代碼等資源是否正確
釋放,避免資源泄漏,尤其每個可能的返回點是否正確釋放資源。
樣本:如下代碼存在記憶體流失
int PortalTransformer::transRLS
{
RLS_Service* service = NULL;
NEW( service, RLS_Service );
parser->adoptDocument();//失敗時會拋異常
//....
delete service;
service =NULL;
return 0;
}
調用adoptDocument出現的異常沒有在函數transRLS裡面被捕獲,而是在父函數裡面捕獲了異常的派生
類。如果發生異常,則NEW( service, RLS_Service )分配的記憶體流失。
解決方案:在函數transRLS裡面捕獲adoptDocument的異常,如果發生異常,則刪除指標service 。
規則7.5 獨立編譯模組或子系統的外部介面禁止拋異常
說明:異常處理沒有普遍通用的二進位標準,所以不允許跨模組拋異常。
7.2 錯誤處理策略
原則7.2 建立合理的錯誤處理策略
說明:這裡所說的錯誤指執行階段錯誤,並非模組內部的編程和設計錯誤。模組內部的編程和設計錯誤
應該通過斷言標記。
在設計早期確定錯誤處理策略,包括:鑒別,嚴重程度,錯誤檢查,錯誤處理,錯誤傳遞,錯誤報表
方案。
錯誤鑒別:對每個實體(函數、類、模組),記錄該實體內部和外部的不變式、前置條件、後置條件以
及它支援的錯誤安全性保證。
錯誤嚴重程度:對於每個錯誤,標明嚴重層級。
錯誤檢查:對於每個錯誤,記載哪些代碼負責檢查它。
錯誤處理:對於每個錯誤,標明負責處理它的代碼。
錯誤報表:對於每個錯誤,標明合適的報告方法。
錯誤傳遞:對每個模組,標明使用什麼編程機制傳遞錯誤,如C++異常、CORBA異常、傳回值。
錯誤處理策略應該只在模組邊界改變。如果模組內外所使用的策略不同,則所有模組入口函數都要直
接負責由內到外的策略轉換。例如,在一個內部使用C++異常,但提供C語言的API邊界的模組中,所有
C語言的API必須用catch(„)捕獲所有異常並將其轉換為錯誤碼。
原則7.3 離錯誤最近的地方處理錯誤或轉換錯誤
說明:當函數檢查到一個自己無法解決的錯誤,而且會使函數無法繼續執行的時候,就應該報告錯誤。
如果缺乏處理的上下文,應該向上傳播錯誤。
規則7.6 錯誤發生時,至少確保符合基本保證;對於交易處理,至少符合強保證;對於原子操作,符
合無錯誤保證
說明:基本保證是指訪問對象時的狀態都是正確的;強保證是對基本保證的增強,不僅要狀態正確,
而且當失敗時狀態要復原到操作前的狀態,要麼成功要麼什麼都不做;無錯誤保證是不能出現失敗。
編碼中嚴格遵循此原則,會極大提升程式的健壯性。
符合基本保證的程式碼範例:如下代碼是解析輸入資料流到對象,流拋出異常方式呈報錯誤
void CMessage::Parse(IStream* input)
{
try
{
m_uiMessageLen = input.ReadInteger();//失敗會拋出異常
if (0 == m_uiMessageLen || m_uiMessageLen>MAX_MESSAGE_LEN)
{
throw invalid_argument("Invalid message len in CMessage::Parse ");
}
m_pMessage = new char[m_uiMessageLen];//失敗拋出異常
input.Read(m_pMessage, m_uiMessageLen);//失敗拋出異常
//.....
}
catch (const exception &exp){
ResetContent();//把對象的所有欄位都設定為無效
throw exp;
}
return;
}
上例確保對象欄位的值要麼都有效要麼都無效,不會出現部分有效部分無效的情況,否則必須處理異
常被拋出時的對象狀態,給調用者帶來麻煩,容易遺漏。故介面應該至少符合基本保證。
可以把該函數改造為符合強保證,如下:
void CMessage::Parse(IStream* input)
{
CMessage temp;
try
{
temp .m_uiMessageLen = input.ReadInteger();//失敗會拋出異常
if (0 == temp .m_uiMessageLen || temp .m_uiMessageLen>MAX_MESSAGE_LEN)
{
throw invalid_argument("Invalid messageLen in CMessage::Parse ");
}
temp .m_pMessage = new char[temp .m_uiMessageLen];//失敗拋出異常
input.Read(temp .m_pMessage, temp .m_uiMessageLen);//失敗拋出異常
//.....
}
catch (const exception &exp){
temp .ResetContent();//把對象的所有欄位都設定為無效
throw exp;
}
swap(temp);//成功後執行swap
return;
}
失敗後,對象的狀態不受影響;而成功後,執行的swap必須符合無錯誤保證,否則此函數是無法支援
強保證的。