天天看點

C++錯誤處理

常言道,要學打架,先學挨打。想打人,就要有被人打的覺悟。寫程式,自然也要有程式運作出錯的覺悟。遊戲程式設計,就從錯誤處理開始。

什麼是錯誤處理?我想應該就是發現錯誤、解決錯誤的過程。什麼是錯誤?比如我調用某個函數,那總會有一個期望的結果。但在實際運作的時候,有可能與期望的結果相符合,也有可能與期望的結果不符合。後面一種情況,就算作是“錯誤”。一個函數要麼有報告錯誤的責任,要麼確定自己不會出現錯誤(沒有絕對的不出錯,但有些函數比如std::swap等,除了堆棧溢出、硬體故障之外幾乎沒有出錯的可能了,是以認為它不會出錯。但凡是涉及到new操作的,由于記憶體配置設定可能失敗,是以都是有可能出錯的)。如果兩者都沒有,那我們的程式就危險了。

聽别人的總結,報告錯誤有三種方法。一是函數直接傳回錯誤代碼;二是把錯誤代碼儲存起來,讓外界用GetLastError之類的函數去擷取;三是抛出一個異常。我發現其實還有第四種辦法:傳入一個回調函數,在錯誤時調用它。

舉例:COM風格的代碼喜歡用HRESULT來表示各種錯誤。Windows API中的多數函數,以及OpenGL都喜歡GetLastError這樣的方式。C++标準的new則又用異常來表示記憶體配置設定失敗。C++還有一個函數set_unexcepted,在産生未聲明的異常時調用回調函數。可見,四種方法在C/C++的經典代碼中都有出現,最後搞得不知道該用哪種好了。

從性能上講,抛出異常的開銷似乎要大一點,但我這裡說“似乎”大一點,這個問題我不确定。異常處理有代價,錯誤代碼的判斷同樣有代價,兩種代價孰輕孰重,我想或許還要看看調用堆棧的深度。每次調用函數都去判斷錯誤代碼的話,效率也不見得高(調用層次很深的話,層層判斷是很麻煩的。并且大量判斷會對CPU指令流水線造成壓力)。

錯誤代碼和GetLastError兩種方式比較相似,都是需要在函數傳回之後進行判斷。有人說GetLastError會存線上程安全問題,不過其實這可以用TLS(線程局部存儲)去解決。但是如果不小心,函數的傳回值被忽略了,則錯誤可能不會被及時處理,并且編譯器不會發出任何有用的警告。

回調函數的性能看起來是最高的,但是它也不像想象的那麼好——它的控制權太弱了。其它三種方式都是函數傳回之後再進行處理,程式控制要靈活得多。但是回調函數的方式,發生錯誤後進入回調函數時,産生錯誤的那個函數并沒有傳回。此時要再想跳轉到别的位置去執行代碼,總會覺得力不從心。

或許應該更傾向于用異常來報告錯誤。原因很簡單:代碼簡短(判斷語句少),這個理由就足夠了。據說使用異常的代碼也比使用傳回值的代碼更容易進行白盒測試。另外,異常不像傳回值那樣容易被忽略,也不像回調函數那樣控制力不足。如果從JAVA來看的話,采用傳回值來報告錯誤的設計貌似已經不多見了。看來在不考慮性能的情況下,使用異常來報告錯誤應該是目前的最佳選擇。(前面也說了,即使考慮性能,估計仍然是最佳選擇)

由于C++不是自動垃圾回收的,在異常機制面前,資源的配置設定釋放顯得尤為重要。異常安全是值得考慮的問題(搜尋一下“異常安全”)。盡可能的使用RAII的方法會有幫助。

此外還有一個問題,就是連結。假設兩個子產品采用不同的編譯器(或者相同的編譯器,但是設定不一樣),則一個子產品所抛出的異常可能無法被另一個子產品所識别。為此,需要確定程式的所有子產品都采用相同編譯器、相同設定去編譯。這在一定程度上限制了使用範圍。

雖然推薦使用異常來報告錯誤,但也要設法在一定程度上減少異常的數量,實際上就是減少錯誤的出現機會。舉例來說,走路的時候,如果前方可能有個坑,應該怎麼辦呢?方案1:走一步試試,有坑就抛出異常,沒坑就啥事兒沒有。方案2:先仔細檢視是否真的有坑,然後決定是否要踏進去。選擇哪種方案取決于“坑存在的可能性”。如果坑存在的可能性較大,則方案1就是不明智的(如果真的踩中了坑,則代價慘重)。如果坑存在的可能性小,則方案2就是不明智的(瞻前顧後,畏首畏尾)。需要根據情況選擇合适的處理方式。

小結:1. 使用異常來報告錯誤。2. 注意異常安全,盡可能的使用RAII。3. 如果某個錯誤發生的機率較高,則盡可能進行主動的檢查,而不要讓異常頻繁的被抛出。“異常僅僅在異常情況下才出現”。

說明一下,此文拷貝自http://hi.baidu.com/zhlz01/item/eab08bcce2699d1cb77a2427

原文的作者就是我本人,是以算作是原創,不是轉載。這是我在2010年時候寫的一篇老文。