天天看點

C++異常處理機制1、概述2、詳細介紹及使用3、異常處理的内部機制4、noexcept

1、概述

異常是指存在于運作時的反常行為,這些行為超出了函數正常功能的範圍。當程式的某部分檢測到一個它無法處理的問題時,需要用到異常處理。此時,檢測出問題的部分應該發出某種信号以表明程式遇到了故障,無法繼續下去了,而且信号的發出方無須知道故障将在何處得到解決。一旦發出異常信号,檢測出問題的部分也就完成了任務。

C++語言中,異常處理包含了一下三個部分:

1、throw表達式,異常檢測部分使用了throw表達式來表示它遇到了無法處理的問題。我們說throw引發了異常。

2、try語句塊,異常處理部分使用try語句塊處理異常。try語句塊以關鍵字try開始,并以一個或多個catch子句結束。try語句塊中代碼抛出的異常通常被某個catch子句處理。catch子句也被稱為異常處理代碼。

3、一套異常類,用于在throw表達式和相關catch子句之間傳遞異常的具體資訊。

2、詳細介紹及使用

throw表達式引發一個異常。使用時包含throw關鍵字,然後緊跟抛出的異常類型。如下:

throw errtype;	//抛出一個異常,抛出的異常類型為 errtype,其實就是異常類
           

try語句塊的通用形式是:

try
{
	program-statements	//要捕捉異常的代碼
}
catch(errtype1)
{
	......	//對errtype1類型的異常,處理函數
}
catch(errtype1)
{
	......	//對errtype2類型的異常,處理函數
}
           

關鍵字try之後,緊跟一個塊,使用花括号括起來的語句,是程式的正常邏輯。try用來捕捉正常邏輯裡面運作時抛出的異常。

跟在try塊之後的是一個或多個catch子句。catch子句包含三個部分:關鍵字catch、括号内的對象的聲明、以及一個處理子產品。catch按照錯誤類型的不同而進行不同的處理。

3、異常處理的内部機制

異常處理機制使得我們能夠将問題的檢測與解決過程分離開來。程式的一部分負責檢測問題的出現,然後解決該問題的任務傳遞給程式的另一部分。

3.1、抛出異常

我們通過 throw 關鍵字來引發一個異常。被抛出的異常類型以及目前的調用鍊共同決定了哪段處理代碼将被用來處理該異常。被選中的處理代碼是在調用鍊中與抛出對象類型比對的最近的處理代碼。其中,根據抛出對象的類型和内容,程式的異常抛出部分将會告知異常處理部分到底發生了什麼錯誤。

當執行一個throw時,跟在throw後面的語句将不再被執行。相反,程式的控制權從throw轉移到與之比對的catch子產品。該catch可能是一個函數中的局部catch,也可能位于直接或間接調用了發生異常的函數的另一個函數中。控制權從一處轉移到另一處,這有兩個重要的含義:

1、沿着調用鍊的函數可能會提早退出。

2、一旦程式開始執行異常處理代碼,則沿着調用鍊建立的對象将被銷毀。

因為跟在throw後面的語句将不再被執行,是以throw語句的用法有點類似于return語句:它通常作為條件語句的一部分或者作為某個函數的最後一條語句。

3.1.1、棧展開

在複雜系統中,程式在遇到抛出異常的代碼前,其執行路徑可能已經經過了多個try語句塊。例如,一個try語句塊可能調用了包含另一個try語句塊的函數,新的try語句塊可能調用了包含又一個try語句塊的新函數,以此類推。

當抛出一個異常時,程式暫停目前函數的執行過程并立即開始尋找與異常比對的catch子句。當throw出現在一個try語句塊内時,檢查與該try塊關聯的catch子句。如果找到了比對的catch,就是用該catch處理異常。如果這一步沒找到比對的catch且該try語句嵌套在其他try塊中,則繼續檢查與外層try比對的catch子句,以此類推。

上述過程被稱為棧展開過程。棧展開過程沿着嵌套函數的調用鍊不斷查找,知道找到了與異常比對的catch子句為止;或者也可能一直沒找到比對的catch,則退出主函數後查找過程終止。

如果找到比對的catch子句,則程式進入該子句并執行其中的代碼。當執行完這個catch子句後,找到與try塊關聯的最後一個catch子句之後的點,并從這裡繼續執行。

如果沒有找到比對的catch子句,程式将調用标準函數terminate,該函數與系統有關,一般情況下,執行該函數将導緻程式非正常退出。

3.1.2、棧展開過程中對象被自動銷毀

在棧展開的過程中,位于調用鍊上的語句塊可能會提前退出。通常情況下,程式在這些塊中建立了一些局部對象。我們知道,塊退出後它的局部對象也将随之銷毀,這條規則對于棧展開過程同樣适用。

如果棧展開過程中退出了某個塊,編譯器将負責確定在這個塊中建立的對象能被正确的銷毀。如果某個局部對象的類型是類類型,則該對象的析構函數将被自動調用。這個就是之前整理C++ RAII資源管理的底層支援。

3.1.3、異常對象

異常對象是一種特殊的對象,編譯器使用異常抛出表達式來對異常對象進行拷貝初始化。而且該類必須含有一個可通路的析構函數、一個可通路的拷貝或移動構造函數。如果該表達式是數組類型或函數類型,則表達式将被轉換成與之對應的指針類型。

異常對象位于由編譯器管理的空間中,編譯器確定無論最終調用的是哪個catch子句都能通路該空間。當異常處理完畢後,異常對象被銷毀。

如我們所知,當一個異常被抛出時,沿着調用鍊的塊将依次退出直到找到與一場比對的處理代碼。如果退出了某個塊,則同時釋放塊中局部對象使用的記憶體。是以,抛出一個指向局部對象的指針是一個錯誤行為。

當我們抛出一條表達式時,該表達式的靜态編譯時類型(編譯器已知類型)決定了異常對象的類型。我們必須牢記這一點,因為很多情況下程式抛出的表達式類型來自某個繼承體系。如果一條throw表達式解引用一個基類指針,而該指針實際指向的是派生類對象,則抛出的對象将被切掉一部分,隻有基類部分被抛出。

3.2、捕獲異常

catch子句中的異常聲明看起來像是隻包含一個形參的函數形參清單。像在形參清單中一樣,如果catch無須通路抛出的表達式的話,則我們可以忽略捕獲形參的名字。

當進入一個catch語句後,通常異常對象初始化異常聲明中的參數。和函數的參數類似,如果catch的參數類型是非引用類型,則該參數是異常對象的一個副本,在catch語句内改變該參數實際上改變的是局部副本而非異常對象本身;相反,如果參數是引用類型,則和其他引用參數一樣,改參數是異常對象的一個别名,此時改變參數也就是改變異常對象。

3.2.1、查找比對的處理代碼

在搜尋catch語句的過程中,我們最終找到的catch是第一個與異常比對的catch語句,而未必是異常的最佳比對。是以,越是專門的catch越應該置于整個catch清單的前端。

因為catch是按照其出現順序逐一進行比對的,雖然要求異常的類型和catch聲明的類型是精确比對的,但是還是允許以下的類型轉換:

  1. 允許從非常量向常量類型的轉換
  2. 允許從派生類向基類的轉換
  3. 數組被轉換成指向數組類型的指針,函數被轉換成指向該函數類型的指針。

3.2.2、重新抛出

有時,單獨一個catch語句不能完整的處理某個異常。在執行了某些校正操作之後,目前的catch可能會決定由調用鍊的更上一層的函數接着處理異常。通過在catch語句中,直接調用throw進行異常的重新抛出,這裡的throw重新抛出,隻是一條throw語句,不包含任何表達式。如下:

try{....}
catch( errtype )
{
	......	//進行了某些處理
	throw;	//從新抛出異常
}
           

空的throw語句隻能出現在catch語句或catch語句直接或間接低昂的函數之内。如果在處理代碼之外的區域遇到了空throw語句,編譯器将調用 terminate。

3.2.3、捕獲所有異常

有時我們希望不論抛出的異常是什麼類型,程式都能統一捕捉它們。我們使用省略号作為異常聲明,這樣的處理代碼稱為捕獲所有異常的處理代碼,形如catch(…)。一條捕獲所有異常的語句可以與任意類型的異常比對。

catch(…)既能單獨出現,也能與其它幾個catch語句一起出現。如果與其它catch語句出現,則catch(…)必須在最後的位置。如下:

try{ ... }
catch(errtype)
{
	......	//對抛出rrtype類型的錯誤進行處理
}
catch(...)
{
	......	//對所有的異常進行處理
	throw;	//catch(...) 一般會與重新抛出異常一起使用
}
           

4、noexcept

對于使用者及編譯器來說,預先知道某個函數不會抛出異常顯然大有裨益。

首先,知道函數不會抛出異常有助于簡化調用函數的代碼。

其次,如果編譯器确認函數不會抛出異常,它就能執行某些特殊的優化操作。

在C++ 11 中,我們通過提供 noexcept 說明指定某個函數不會抛出異常。其形式是關鍵字 noexcept緊跟在函數的參數清單後面,用以辨別該函數不會抛出異常:

void recoup( int ) noexcept;	//不會抛出異常
void alloc( int )				//可能抛出異常
           

需要說明的是:noexcept隻是一個說明,如果函數說明了是一個noexcept函數,但是函數内部又含有throw語句或者調用了可能抛出異常的其它函數,編譯器将順利編譯通過。

是以可能出現這樣一種情況:盡管函數聲明了不會抛出異常,但實際上還是抛出了。一旦一個noexcept函數抛出了異常,程式就會調用terminate以確定遵守不在運作時抛出異常的承諾。上述過程對是否執行棧展開未作約定,是以noexcept可以用在兩種情況下:一是我們确認函數不會抛出異常,二是我們根本不知道該如何處理異常。

noexcept 還有其它的一些用法,這裡就不展開了,我們之後再進行整理。

感謝大家,我是假裝很努力的YoungYangD(小羊)。

參考資料:

《C++ primer》

《More Effective C++》

繼續閱讀