1. 程式人生 > >【C++】:異常處理

【C++】:異常處理

C++的異常

1.C語言處理錯誤的方式

  • 終止程式。例如:assert,斷言為則終止程序
  • 返回錯誤碼。程式設計師自己去查詢錯誤碼對應的錯誤資訊,使用廣泛
  • C 標準庫setjmplongjmp組合

2. C++的異常處理

異常是一種處理錯誤

的方式,當一個函式發現自己無法處理的錯誤時就可以丟擲異常,讓函式的直接或間接的呼叫者處理這個錯誤。

  • throw:當問題出現時,程式會丟擲一個異常。通過使用throw關鍵字來完成。
  • catch:在想要處理問題的地方,通過異常處理程式捕獲異常,catch關鍵字用於捕獲異常,也可以有多個catch進行捕獲。
  • try:try塊中的程式碼標識將被啟用的特定異常,它後面通常跟著一個或多個catch 塊

try塊中放置可能丟擲異常的程式碼,try 塊中的程式碼被稱為保護程式碼使用 try/catch 語句的語法如下:

try
{
	//保護的程式碼標識
}
catch (
ExceptionName e1) { // catch 塊 }catch (ExceptionName e2) { // catch 塊 }catch (ExceptionName e3) { // catch 塊 }

3.使用異常方法

3.1 拋異常和捕捉異常

  • 異常是通過丟擲物件而引發的,該物件的型別決定了應該啟用哪個catch的處理程式碼。
  • 被選中的處理程式碼是呼叫中與該物件型別匹配且離丟擲異常位置最近的那一個。
  • 丟擲異常物件後,會生成一個異常物件的拷貝,因為丟擲的異常物件可能是一個臨時物件,所以會生成 一個拷貝物件,這個拷貝的臨時物件會在被catch以後銷燬,類似於函式的傳值返回
  • catch可以捕獲任意型別的異常。
  • 丟擲和捕獲的匹配原則有個例外,並不都是型別完全嚴格匹配,可以丟擲的派生類物件,使用基類捕獲。實際工程中,使用的很多。

在函式呼叫鏈中異常棧展開匹配原則

  • 檢查throw本身是否在try塊內部,如果存在則查詢匹配的catch語句。如果有匹配的,則調到catch的地方進行處理。 如果沒有匹配的catch則退出當前函式棧,繼續在呼叫函式的棧中進行查詢匹配的catch
  • 如果到達main函式的棧,依舊沒有匹配的,則終止程式。這個過程稱為棧展開。實際使用中我們最後都要加一個catch(...)捕獲任意型別的異常,否則當有異常沒捕獲,程式就會直接終止。
  • 找到匹配的catch子句並處理以後,會繼續沿著catch子句後面繼續執行
double Div(double x1, double x2)
{
	if (x2 == 0)
	{
		throw "Div zero mistakes";//丟擲異常
	}
	else
	{
		return x1 / x2;
	}
}
void Test()
{
	try{
		int x1;
		int x2;
		cin >> x1 >> x2;
		Div(x1, x2);
	}
	catch (const char* msg){//捕獲字串型別的異常
		cout << msg << endl;
	}
	catch (int msg){//捕獲整型的異常
		cout << msg << endl;
	}
	catch (...){//捕獲任意型別的異常
		cout << "unkown exception" << endl;
	}
	cout << "繼續執行" << endl;//沿著catch子句後面繼續執行
}
int main()
{
	try {
		Test();
	}
	catch (const char* msg){//匹配且離丟擲異常位置最近的
		cout << msg << endl;
	}
	return 0;
}

3.2 重新丟擲異常

有可能單個的catch不能完全處理一個異常,在進行一些校正處理以後,希望再交給外層的呼叫鏈函式來處理,catch則可以通過重新丟擲將異常傳遞給更上層的函式進行處理。

double Div(double x1, double x2)
{
	if (x2 == 0)
	{
		throw "Div zero mistakes";//丟擲異常
	}
	else
	{
		return x1 / x2;
	}
}
void Test()
{
	int* arr = new int[10];
	try{
		int x1;
		int x2;
		cin >> x1 >> x2;
		Div(x1, x2);
	}
	catch (...){/
		cout << "delete arr[]" << endl;
		delete[] arr;//釋放陣列之後重新丟擲
		throw;
	}
	cout << "繼續執行" << endl;//沿著catch子句後面繼續執行
}
void Func()
{
	
}
int main()
{
	try {
		Test();
	}
	catch (const char* msg){//匹配且離丟擲異常位置最近的
		cout << msg << endl;
	}
	return 0;
}

3.3 異常安全及規範

異常安全:

  • 構造函式完成物件的構造和初始化,最好不要在建構函式中丟擲異常,否則可能導致物件不完整或沒有完全初始化,可能會跳過某些程式碼。
  • 析構函式主要完成資源的清理,最好不要在解構函式內丟擲異常,否則可能導致資源洩漏(記憶體洩漏、控制代碼未關閉等)
  • C++中異常經常會導致資源洩漏的問題,比如在newdelete中丟擲了異常,導致記憶體洩漏,在lockunlock之間丟擲了異常導致死鎖C++經常使用RAII來解決。

異常規範:

  • 異常規格說明的目的是為了讓函式使用者知道該函式可能丟擲的異常有哪些。 可以在函式的後面接 throw(型別),列出這個函式可能拋的所有異常型別
  • 函式的後面接throw(),表示函式不拋異常
  • 若無異常介面宣告,則此函式可以拋任何型別的異常。
//表示這個函式會丟擲int/string/char中的某種型別的異常
void Func1() throw(int, string, char);
//表示這個函式不會丟擲異常 
void Func2() throw();
//表示可以丟擲任意型別的異常
void Func3();

4. 自定義異常

實際使用中,我們都會自定義自己的異常體系進行規範的異常管理,因為一個專案中如果大家隨意拋異常,那麼外層的呼叫者會存在很多catch型別,所以實際中都會定義一套繼承的規範體系。這樣大家丟擲的都是繼承的派生類物件,捕獲一個基類即可。

class Exception { 
protected:    
	string _errmsg;    
	int _id;    
};

class SqlException : public Exception {};

class CacheException : public Exception {};

class HttpServerException : public Exception {};

int main() {
	try{        
		// server.Start();        
		// 丟擲物件都是派生類物件    
	}    
	catch (const Exception& e) //捕獲父類物件就可以    
	{}    
	catch (...){//捕捉任意型別即可       
		cout << "Unkown Exception" << endl;    
	}
	return 0;
}

5.C++標準庫的異常體系

C++ 提供了一系列標準的異常,定義在<exception>中,我們可以在程式中使用這些標準的異常。它們是以父子類層次結構組織起來的,如下圖所示:
在這裡插入圖片描述
下表是對上面層次結構中出現的每個異常的說明:
在這裡插入圖片描述

void Test()
{
	try{
		vector<int> v(10, 5);

		//系統記憶體不夠會拋異常        
		v.reserve(1000000000);
	}
	//父類子類同時存在,嚴格匹配
	catch (const bad_alloc& e)//捕捉子類物件
	{
		cout << e.what << endl;
	}
	catch (const exception& e) //捕獲父類物件   
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "Unkown Exception" << endl;
	}
}

注:父類子類同時存在,嚴格匹配,跳轉到子類的catch

6. C++異常的優缺點

優點:

  • 異常物件定義好之後,相比錯誤碼的方式可以清晰準確的展示出錯誤的各種資訊,甚至可以包含堆疊呼叫的資訊,這樣可以幫助更好的定位程式的bug
  • 返回錯誤碼的傳統方式有個很大的問題就是在函式呼叫鏈中,深層的函式返回了錯誤,那麼我們得層層返回錯誤,這樣最外層才能拿到錯誤。但如果是異常體系,不管那個呼叫函數出錯,都不用檢查返回值,因為丟擲的異常會直接跳到main函式catch捕獲的地方,直接在main函式處理錯誤即可。
  • 很多的第三方庫都包含異常,例如boost、gtest、gmock等常用的庫。
  • 很多測試框架都使用異常,這樣能更好的使用單元測試等進行白盒的測試
  • 部分函式使用異常可以更好處理錯誤。。例如T& operator()函式,如果pos越界了只能使用異常或者終止程式處理,因為T代表的自定義型別沒辦法通過返回值表示錯誤。

缺點:

  • 異常會導致程式的執行流亂跳,非常的混亂,這會導致我們跟蹤除錯時以及分析程式時比較困難。
  • 異常會有一些效能的開銷
  • C++沒有垃圾回收機制,資源需要自己管理。有了異常非常容易導致記憶體洩漏、死鎖等異常安全問題。 這個需要使用RAII來處理資源的管理問題。
  • C++標準庫的異常體系定義得不好,導致大家各自定義各自的異常體系,非常的混亂
  • 異常儘量規範使用,如果隨意拋異常,外層捕獲的使用者catch很多型別。
  • C++異常利大於弊。

異常規範有兩點:

  • 丟擲異常型別都繼承自一個基類
  • 函式是否拋異常、拋什麼異常,都使用 func() throw()的方式規範化。