C++語言程式設計筆記 - 第12章 - 異常處理

oddmarmot發表於2020-12-11

第12章 異常處理

12.1 異常處理的基本思想

程式執行中的有些錯誤是可以預料但不可避免的,例如記憶體空間不足、硬碟上的檔案被移動、印表機未連線好等由系統執行環境造成的錯誤。這是要力爭做到允許使用者排除環境錯誤,繼續程式的執行;至少要給出提示資訊。這就是異常處理程式的任務

在一個大型軟體中,發現錯誤的函式往往不具備處理錯誤的能力,這時,它就引發一個異常,希望它的呼叫者能夠捕獲這個異常並處理這個錯誤。若呼叫者無法處理這個錯誤,那麼還可以繼續傳遞給上級呼叫者去處理,這種傳遞會一直持續到異常被處理為止。如果程式始終沒有處理這個異常,最終它會被傳遞到C++執行系統那裡,執行系統捕獲異常後通常只是簡單地終止這個程式

**C++的異常處理機制使得異常的引發和處理不必在同一函式中。**這樣,底層函式可以著重解決具體問題,而不必過多地考慮對異常的處理。上級呼叫者可以在適當的位置設計對不同型別異常的處理。


12.2 C++異常處理的實現

C++語言提供對處理異常情況的內部支援。try,throw和catch語句就是C++語言中用於實現異常處理的機制

12.2.1 異常處理的語法

throw 表示式語法:

throw 表示式

try塊語法:

try
   複合語句
catch(異常宣告)
    複合語句
catch(異常宣告)
    複合語句
    ...

throw語句:如果某段程式中發現了自己無法處理的異常,就可以使用throw表示式來拋擲這個異常,將它拋擲給呼叫者。throw的運算元表示異常型別在語法上與return語句的運算元相似。如果程式中有多種要拋擲的異常,應該用不同的運算元型別來互相區別

try語句:try子句後的複合語句是程式碼的保護段如果預料某段程式程式碼(或對某個函式的呼叫)有可能發生異常,就將它放在try子句之後。如果這段程式碼(或被調函式)執行時真的遇到異常情況,其中的throw表示式就會拋擲這個異常。

catch語句:catch子句後的複合語句是異常處理程式捕獲由throw表示式拋擲的異常。異常宣告部分指明瞭子句處理的異常的型別異常引數名稱,它與函式的形參是類似的,可以是某個型別的值,也可以是引用其中的型別可以是任何有效的資料型別,包括C++的類當異常被拋擲後,catch子句便依次被檢查,若某個catch子句的異常宣告的型別與被拋擲的異常型別一致,則執行該段異常處理程式。如果異常型別宣告是一個省略號(...),catch子句便處理所有型別的異常,這段處理程式必須是try塊的最後一段處理程式

提示:異常宣告的形式與函式形參的宣告類似,catch子句的異常宣告中也允許只指明型別,而不給出異常引數名稱,只是在這種情況下在複合語句中就無法訪問該異常物件了。


異常處理的執行過程:

(1)程式通過正常的順序執行到達try語句,然後執行try塊內的保護段;

(2)如果在保護段執行期間沒有引起異常,那麼跟在try塊後的catch子句就不執行。程式從異常被拋擲的try塊後跟隨的最後一個catch子句之後的語句繼續執行下去;

(3)程式執行到一個throw表示式時,一個異常物件會被建立。若異常的丟擲點本身在一個try子句內,則該try語句後的catch子句會按順序檢查異常型別是否與宣告的型別匹配;若異常丟擲點本身不在任何try子句內,或丟擲的異常與各個catch子句所宣告的型別皆不匹配,則結束當前函式的執行,回到當前函式的呼叫點,把呼叫點作為異常的丟擲點,然後重複這一過程

(4)如果始終未找到與被拋擲異常匹配的catch子句最終main函式會結束執行,則執行庫函式terminate將被自動呼叫,而函式terminate的預設功能是終止程式

(5)如果找到了一個匹配的catch子句,則catch子句後的複合語句會被執行。複合語句執行完畢後,當前的try塊(包括try子句和一系列catch子句)即執行完畢,即只要找到一個匹配的異常型別,後面的異常處理都將被忽略

細節:當以下條件之一成立時,丟擲的異常與一個catch子句中宣告的異常型別匹配:

(1)catch子句中宣告的異常型別就是丟擲異常物件的型別或其引用

(2)catch子句中宣告的異常型別是丟擲異常物件的型別的公共基類或其引用

(3)丟擲的異常型別和catch子句中宣告的異常型別皆為指標型別並且前者到後者可隱含轉換


12.2.2 異常介面宣告

為了加強程式的可讀性,使函式的使用者能夠方便地直到所使用的函式會拋擲哪些異常,可以在函式的宣告中列出這個函式可能拋擲的所有異常型別。例如:

void fun() throw(A,B,C,D);	//函式fun()能且只能拋擲型別A,B,C,D及其子型別的異常

**如果在函式的宣告中沒有包括異常介面宣告,則此函式可以拋擲任何型別的異常。**例如:

void fun();	//可以拋擲任何型別的異常

一個不拋擲任何型別異常的函式可以進行如下形式的宣告:

void fun() throw();	//不拋擲任何型別異常的函式的宣告

細節:如果一個函式丟擲了它的異常介面宣告所不允許丟擲的異常,那麼unexpected函式會被呼叫,該函式的預設行為是呼叫terminate函式中止程式,使用者也可以定義自己的unexpected函式,替換預設的函式。


12.2.3 異常處理的構造與析構

在程式中,找到一個匹配的catch異常處理後,如果catch子句的異常宣告是一個值引數,則其初始化方式是複製被拋擲的異常物件;如果catch子句的異常宣告是一個引用,則其初始化方式是使該引用指向異常物件

C++異常處理的真正功能,不僅在於它能夠處理各種不同型別的異常,還在於它具有為異常拋擲前構造的所有區域性物件自動呼叫解構函式的能力。

棧的解旋異常被丟擲後,從進入try塊(與截獲異常的catch子句相對應的那個try塊)起,到異常被拋擲前,這期間在棧上構造(且尚未析構)的所有物件都會被自動析構,析構的順序與構造的順序相反。這一過程稱為棧的解旋。

一個在catch子句中宣告異常引數(catch子句的引數)的例子:

catch (MyException & e) {...}

其中,也可以不宣告異常引數(e)。在很多情況下只要通知處理程式有某個特定型別的異常已經產生就足夠了。但是在需要訪問異常物件時就要宣告異常引數(e),否則將無法訪問catch處理程式子句中的那個物件。例如:

catch (MyException) {
	//在這裡將無法訪問異常物件
}

用一個不帶運算元的throw表示式可以將當前正在被處理的異常再次拋擲,這樣一個表示式只能出現在一個catch子句中或在catch子句內部呼叫的函式中。再次拋擲的異常物件是源異常物件(不是副本)。例如:

try{
    throw MyException("some exception");
}catch(...){	//處理所有異常
    //...
    throw;		//將異常傳給某個其他處理器
}

12.2.4 標準程式庫異常處理

C++標準提供了一組標準異常類,這些類以基類Exception開始,標準程式庫丟擲的所有異常,都派生於該基類。基類Exception提供一個成員函式what(),用於返回錯誤資訊(返回型別為const char*,在基類Exception中,what()函式的宣告如下:

virtual const char* what() const throw();

標準異常類的繼承關係:Exception類直接派生出bad_alloc,bad_cast,bad_typeid,bad_exception,ios_base::failure,runtime_error,logic_error類。其中,runtime_error類又直接派生出underflow_error,overflow_error,range_error類;logic_error類又直接派生出out_of_range,length_error,invalid_argument,domain_error類。runtime_error表示那些難以被預先檢測到地異常,而logic_error表示那些可以在程式中被預先檢測到的異常,也就是說如果小心地編寫程式,logic_error類的異常能夠避免

runtime_error和logic_error兩個類及其派生類,都有一個接收const string &型引數的建構函式。在構造異常物件時需要將具體的錯誤資訊傳遞給該函式,如果呼叫該物件的what()函式,就可以得到構造時提供的錯誤資訊。

C++標準庫各種異常類所代表的異常見下表:

異常類標頭檔案異常的含義
bad_allocexceptionnew動態分配空間失敗
bad_castnew執行dynamic_cast失敗
bad_typeidtypeinfo對某個空指標p執行typeid
bad_exceptiontypeinfo當某個函式fun()因在執行過程中丟擲了異常宣告所不允許的異常而呼叫unexcepted()函式時,若unexcepted()函式又一次丟擲了fun()的異常宣告所不允許的異常,且fun()的異常宣告列表中有bad_exception,則會有一個bad_exception異常在fun()的呼叫點被丟擲
ios_base::failureios用來表示C++的輸入輸出流執行過程中發生的錯誤
underflow_errorstdexcept算術運算時向下溢位
overflow_errorstdexcept算數運算時向上溢位
range_errorstdexcept內部計算時發生作用域的錯誤
out_of_rangestdexcept表示一個引數值不在允許的範圍之內
length_errorstdexcept嘗試建立一個長度超過最大允許值的物件
invalid_argumentstdexcept表示向函式傳入無效引數
domain_errorstdexcept執行一段程式所需要的先決條件不滿足

一些程式語言規定只能拋擲某個類的派生類(例如Java中允許拋擲的類必須派生自Exception類),C++雖然沒有這項強制的要求,但仍然可以這樣實踐。例如,在程式中可以使得所有丟擲的異常皆派生自Exception(或者直接丟擲標準程式庫提供的異常型別,或者從標準程式庫提供的異常類派生出新的類),這樣會帶來很多方便


例子:編寫一個計算三角形面積的函式,函式的引數為三角形三邊邊長a,b,c,可以用Heron公式計算三角形面積,在計算三角形面積的函式中需要判斷輸入的引數a,b,c是否構成一個三角形,若三個邊長不能構成三角形,則需要丟擲異常。下面是源程式:

#include<iostream>
#include<cmath>
#include<stdexcept>
using namespace std;

//給出三角形三條邊的長度,計算三角形的面積
double area(double a, double b, double c) throw (invalid_argument) {
	//判斷三角形的三條邊長是否為正
	if (a <= 0 || b <= 0 || c <= 0)
		throw invalid_argument("the side length should be positive");
	//判斷三角形的三條邊長是否滿足三角不等式
	if (a + b <= c || b + c <= a || a + c <= b)
		throw invalid_argument("the side length should fit the triangle inequation");
	//由Heron公式計算三角形面積
	double s = (a + b + c) / 2;
	return sqrt(s * (s - a) * (s - b) * (s - c));
}

int main() {
	double a, b, c;
	cout << "Please input the side lengths of a triangle: ";
	cin >> a >> b >> c;
	try {
		double s = area(a, b, c);
		cout << "Area: " << s << endl;
	}
	catch (exception& e) {
		cout << "Error: " << e.what() << endl;
	}
	return 0;
}

程式執行結果1:

Please input the side lengths of a triangle: 3 4 5
Area: 6

程式執行結果2:

Please input the side lengths of a triangle: 0 5 5
Error: the side length should be positive

程式執行結果3:

Please input the side lengths of a triangle: 1 2 4
Error: the side length should fit the triangle inequation

C++標準程式庫對於異常處理做了如下保證:C++標準程式庫在面對異常時,保證不會發生資源洩露,也不會破壞容器的不變特性

(1)對於以結點實現為基礎的容器,如list,set,multiset,map,multimap,如果結點構造失敗,容器應當保持不變。同樣需要保證刪除結點操作不會失敗。在順序關聯容器中插入多個元素時,為保證資料的有序排列,應當保證如果插入不成功,則容器元素不做任何改動。對於刪除操作,確保刪除成功。比如,對於列表容器,除了remove()remove_if()merge()sort()unique()之外的所有操作,要麼成功,要麼對容器不做任何改動。

(2)對於以陣列實現為基礎的容器,如vector,deque,在插入或刪除元素時,由於有時需要呼叫複製建構函式和複製賦值運算子,當這些操作失敗而丟擲異常時,容器的不變性不能被保證。除此之外,對於這些容器的不變特性的保證程度與以結點實現為基礎的容器相同。


相關文章