異常機制簡(jiǎn)單探討
引 言
我們在編寫(xiě)軟件時(shí)不但要追求代碼的正確性,更要關(guān)注程序的容錯能力,在環(huán)境不正確或操作不當時(shí)不能死機,更不能造成災難性后果。程序運行時(shí)有些錯誤是不可避免的,如內存不足、文件打開(kāi)失敗、數組下標溢出等,這時(shí)要力爭做到排除錯誤,繼續運行。
傳統做法是返回一個(gè)錯誤代碼,調用者通過(guò)if等語(yǔ)句測試返回值來(lái)判斷是否成功。這樣做有幾個(gè)缺點(diǎn):首先,增加的條件語(yǔ)句可能會(huì )帶來(lái)更多的錯誤;其次,條件語(yǔ)句是分支點(diǎn),會(huì )增加測試難度;另外,構造函數沒(méi)有返回值,返回錯誤代碼是不可能的。
C++的異常機制為我們提供了更好的解決方法。異常處理的基本思想是:當出現錯誤時(shí)拋出一個(gè)異常,希望它的調用者能捕獲并處理這個(gè)異常。如果調用者也不能處理這個(gè)異常,那么異常會(huì )傳遞給上級調用,直到被捕獲處理為止。如果程序始終沒(méi)有處理這個(gè)異常,最終它會(huì )被傳到C++運行環(huán)境,運行環(huán)境捕獲后通常只是簡(jiǎn)單地終止這個(gè)程序。異常機制使得正常代碼和錯誤處理代碼清晰地劃分開(kāi)來(lái),程序變得非常干凈并且容易維護。
但是如何合理地使用異常機制來(lái)達到預期的效果呢?MISRA C++給出了一些推薦的規則,幫助程序員更加合理、可靠地實(shí)現異常機制。下面將結合這些規則對異常機制進(jìn)行簡(jiǎn)單的探討。
1 在恰當的場(chǎng)合使用恰當的特性
MISRA C++對異常的第1條規則就是:
規則15-0-1(不容討論):異常機制只能用來(lái)處理錯誤。
異常處理的本質(zhì)是控制流程的轉移,但異常機制是針對錯誤處理的,僅在代碼可能出現異常的情況下使用,不能用來(lái)實(shí)現普通的流程轉移。
例如:
語(yǔ)法不會(huì )阻止你這樣做,但殺雞焉用牛刀。這樣不但會(huì )降低程序的可讀性,也會(huì )帶來(lái)更大的開(kāi)銷(xiāo)。實(shí)際上,用一個(gè)簡(jiǎn)單的if語(yǔ)句就可以實(shí)現上述邏輯。同樣,出于程序流程的清晰性考慮的還有:
規則15-0-3(強制):不允許通過(guò)goto或者switch語(yǔ)句跳轉到try或catch語(yǔ)句塊內。
2 正確地拋出異常
什么時(shí)候,什么地方,拋出什么樣的異常,都是需要仔細考慮的。MISRA C++對此也作了相關(guān)規定。首先,來(lái)看一下拋出異常對象的類(lèi)型中有哪些需要注意的地方。規則15-0-2(推薦):拋出的異常對象不應該是指針類(lèi)型。
如果拋出的異常對象是個(gè)指針類(lèi)型,指向的是動(dòng)態(tài)創(chuàng )建的對象,那么這個(gè)對象應該由哪個(gè)函數來(lái)負責銷(xiāo)毀,什么時(shí)候銷(xiāo)毀,都很不清楚。比如說(shuō),如果是在堆中建立的對象,那通常必須刪除,否則會(huì )造成資源泄漏;如果不是在堆中建立的對象,通常不能刪除,否則程序的行為將不可預測。
規則15-1-2(強制):不能顯式地把NULL作為異常對象拋出。
因為throw(NULL)=tbrow(0),因此NULL會(huì )被當作整型捕獲,而不是空指針常量,這可能與程序員的預期不一致。
通常,很多函數都是基于function-try-block結構的,即函數體整個(gè)包含在一個(gè)函數try塊中。而函數能拋出什么類(lèi)型的異常對象,有以下規定:
規則15-5-2(強制):如果一個(gè)函數聲明時(shí)指定了具體的異常類(lèi)型,那么它只能拋出指定類(lèi)型的異常。
規則15-4-1(強制):如果一個(gè)函數聲明時(shí)指定了異常的類(lèi)型,那么在其他編譯單元里該函數的聲明必須有同樣的指定。
函數的代碼結構如下:返回值類(lèi)型函數名(形參表)throw(類(lèi)型名表){函數體}
如果函數在聲明時(shí)沒(méi)有異常規范,那么它可以?huà)伋鋈我忸?lèi)型的異常對象;如果異常類(lèi)型為空,則表示不拋出任何類(lèi)型異常。注意這兩者之間的區別,前者指沒(méi)有throw(類(lèi)型名表)語(yǔ)句,而后者有throw(類(lèi)型名表),只是類(lèi)型名表為空。但如果聲明時(shí)指定了異常的類(lèi)型,那么它只能拋出指定類(lèi)型的異常。
另外,函數原型中的異常聲明要與實(shí)現中的異常聲明一致,否則會(huì )引起異常沖突。由于異常機制是在運行出現異常時(shí)才發(fā)揮作用的,因此如果函數的實(shí)現中拋出了沒(méi)有在其異常聲明列表中列出的異常,編譯器也許不能檢查出來(lái)。當拋出一個(gè)未在其異常聲明列表里的異常類(lèi)型時(shí),unexpected()函數會(huì )被調用,默認會(huì )導致std::bad_exception類(lèi)型的異常被拋出。如果std::bad_exception不在異常聲明列表里,又會(huì )導致terminate()被調用,從而導致程序結束。
對于什么時(shí)候能拋出異常,則有以下規定:
規則15-3-1(強制):異常只能在初始化之后而且程序結束之前拋出。
在執行main函數體之前,是初始化階段,構造和初始化靜態(tài)對象;在main函數返回后,是終止階段,靜態(tài)對象被銷(xiāo)毀。在這兩個(gè)階段中如果拋出異常,會(huì )導致程序以不定的方式終止(這依賴(lài)于具體的編譯器)。例如:
在這個(gè)例子中,catch塊只能捕獲上面try塊中的異常。如果在對象c的構造函數或析構函數中拋出異常,并不能被main里的catch塊捕獲,而且會(huì )導致程序終止。
除了上述規則,還有以下兩個(gè)規則需要注意:
規則15-1-1(強制):throw語(yǔ)句中的表達式本身不能引發(fā)新的異常。
如果在構造異常對象,或者計算賦值表達式時(shí)引發(fā)新的異常,那么新的異常會(huì )在本來(lái)要拋出的異常之前被拋出,這與程序員的預期不一致。
規則15-1-3(強制):空的throw語(yǔ)句只能出現在catch語(yǔ)句塊中。
空的throw用來(lái)將捕獲的異常再拋出,可以實(shí)現多個(gè)處理程序問(wèn)異常的傳遞。然而,如果在catch語(yǔ)句外用,由于沒(méi)有捕獲到異常,也就沒(méi)有東西可以再拋出,這樣會(huì )導致程序以不定的方式終止(這依賴(lài)具體的編譯器)。
3 合理地處理異常
由于后面的討論多處涉及到“棧展開(kāi)”這個(gè)概念,這里先解釋一下。“棧展開(kāi)”是異常機制中一個(gè)重要的過(guò)程:在逐層查找用來(lái)處理異常的catch子句時(shí),因為異常而退出復合語(yǔ)句和函數定義,這個(gè)過(guò)程被稱(chēng)作“棧展開(kāi)”。隨著(zhù)棧的展開(kāi),在退出的復合語(yǔ)句和函數定義中聲明的局部變量的生命期也結束,而且這些局部類(lèi)對象的析構函數也會(huì )被調用,這樣能保證內存空間得到合理回收。棧展開(kāi)的概念對于理解后面的內容很重要,我們通過(guò)一個(gè)具體例子進(jìn)一步闡述。
當異常發(fā)生時(shí),在函數調用鏈中逐層查找該異常的catch子句。在棧展開(kāi)過(guò)程中函數foo()首先被檢查到,因為產(chǎn)生異常的語(yǔ)句沒(méi)有被放在try塊中,所以不會(huì )在:foo()中查找針對該異常的catch子句。棧展開(kāi)過(guò)程繼續向上遍歷函數調用鏈到達調用foo()的函數。然而在foo()帶著(zhù)這個(gè)未處理的異常退出之前,棧展開(kāi)過(guò)程會(huì )銷(xiāo)毀foo()中所有在異常產(chǎn)生之前被創(chuàng )建的局部類(lèi)對象。結果就是:o1、o2的析構函數被調用,o3已經(jīng)“死亡”,而o4還沒(méi)“出生”。
顧名思義,“異常”就是程序運行出現了非預期的情況,或者說(shuō)錯誤。因此,出現異常必須有針對地處理。對此,MISRA C++首先有如下規定:
規則15-3-4(強制):所有可能的流程中顯式拋出來(lái)的異常都應該有一個(gè)類(lèi)型兼容的處理程序。
規則15-3-2(推薦):至少要有一個(gè)處理程序來(lái)處理所有其他未針對處理的異常。
如果程序拋出一個(gè)沒(méi)有被處理的異常,程序會(huì )終止,而終止前調用棧有沒(méi)有被“展開(kāi)”,動(dòng)態(tài)對象能不能被析構,這些都依賴(lài)于編譯器。上面兩條規則規定了:不但預期拋出的異常要進(jìn)行處理,其他可能被拋出的異常也要有相應的處理措施。請注意規則15-3-4中“類(lèi)型兼容”的字眼,C++有非常靈活的類(lèi)型兼容規則,尤其對于類(lèi)。例如當異常對象是派生類(lèi)時(shí),“兼容類(lèi)型”可以是派生類(lèi),也可以是基類(lèi)。后面我們還會(huì )具體討論這個(gè)問(wèn)題。
一個(gè)try塊后可以有多個(gè)catch塊來(lái)捕獲不同的異常。當出現異常時(shí),catch處理程序按照其在try塊后出現的順序被逐個(gè)檢查,只要找到一個(gè)匹配的異常類(lèi)型,后面的異常處理都被忽略。因此,catch處理程序出現的順序很重要。
規則15-3-6(強制):若一個(gè)try-catch語(yǔ)句塊有多個(gè)處理程序,或者一個(gè)派生類(lèi)和其部分或全部基類(lèi)的function-try-block塊有多個(gè)處理程序,處理程序的順序應該是先派生類(lèi)后基類(lèi)。
規則15-3-7(強制):若一個(gè)try-catch語(yǔ)句塊或者function-try-block塊有多個(gè)處理程序時(shí),catch(…)處理程序(捕獲所有異常)應該放在最后。
這是因為根據類(lèi)型兼容規則,異常對象為派生類(lèi)時(shí)可以被針對基類(lèi)的處理程序所捕獲。如果針對基類(lèi)的處理程序放在前面,后面針對派生類(lèi)的處理程序就不會(huì )被執行到。同理,catch(…)處理程序能捕獲所有類(lèi)型的異常,在其后面所有的異常處理程序都不會(huì )被執行到。
評論