在程序設計過程中,我們總是希望自己設計的程序是天衣無縫的,但這幾乎又是不可能的。即使程序編譯通過,同時也實現了所需要的功能,也並不代表程序就已經完美無缺了,因為運行程序時還可能會遇到異常,例如當我們設計一個為用戶計算除法的程序時,用戶很有可能會將除數輸入為零,又例如當我們需要打開一個文件的時候確發現該文件已經被刪除了……類似的這種情況很有很多,針對這些特殊的情況,不加以防范是不行的。
我們通常希望自己編寫的程序能夠在異常的情況下也能作出相應的處理,而不至於程序莫名其妙地中斷或者中止運行了。在設計程序時應充分考慮各種異常情況,並加以處理。
在C++中,一個函數能夠檢測出異常並且將異常返回,這種機制稱為拋出異常。當拋出異常後,函數調用者捕獲到該異常,並對該異常進行處理,我們稱之為異常捕獲。
C++新增throw
關鍵字用於拋出異常,新增catch
關鍵字用於捕獲異常,新增try
關鍵字嘗試捕獲異常。通常將嘗試捕獲的語句放在 try{ }
程序塊中,而將異常處理語句置於 catch{ }
語句塊中。
C++提供的異常處理機制可以允許程序員自己對異常進行捕獲,根據捕獲到的異常類型,自己進行處理,這樣使得程序員對於自己的程序具有更大的控制權限。
異常處理需要以下三個關鍵字:try、throw、catch。
基本的異常處理程序框架
try
{
//可能出現異常的代碼塊
}
catch(類型名1 [形參名]) //這裡形參名可以不出現
{
//捕獲到 類型名1 的異常時的異常處理程序
}
catch(類型名1 [形參名])
{
//捕獲到 類型名2 的異常時的異常處理程序
}
...
catch(...)
{
//三個點 表示可以捕獲任何異常
}
首先一個異常的拋出使用 throw,語法為:
throw 表達式
使用的時候,將可能會拋出異常的語句塊包含在try{}
語句塊中,如果try{}
語句塊中的程序發現了異常並且拋出此異常的話,那麼這個異常可以被catch{}
語句進行捕獲並處理,捕獲和處理的條件是被拋棄的異常的類型與catch
語句的異常類型相匹配。由於C++使用數據類型來區分不同的異常,因此在判斷異常時,throw
語句中的表達式的值就沒有實際意義,而表達式的類型就特別重要。
C++可以拋出普通內置類型的異常
#include //包含頭文件
#include
double fuc(double x, double y) //定義函數
{
if(y==0)
{
throw y; //除數為0,拋出異常
}
return x/y; //否則返回兩個數的商
}
void main()
{
double res;
try //定義異常
{
res=fuc(2,3);
cout<<"The result of x/y is : "<
上面的程序當除數為0的時候,就拋出一個類型為
double
的異常,然後下面的catch{}
捕獲異常之後進行處理。
拋出自定義異常類型
異常類型可以使自定義的異常類,並且在程序中拋出該異常類。
#include
#include
#include
// 內存洩露檢測機制
#define _CRTDBG_MAP_ALLOC
#ifdef _DEBUG
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif
// 自定義異常類
class MyExcepction
{
public:
// 構造函數,參數為錯誤代碼
MyExcepction(int errorId)
{
// 輸出構造函數被調用信息
std::cout << "MyExcepction is called" << std::endl;
m_errorId = errorId;
}
// 拷貝構造函數
MyExcepction( MyExcepction& myExp)
{
// 輸出拷貝構造函數被調用信息
std::cout << "copy construct is called" << std::endl;
this->m_errorId = myExp.m_errorId;
}
~MyExcepction()
{
// 輸出析構函數被調用信息
std::cout << "~MyExcepction is called" << std::endl;
}
// 獲取錯誤碼
int getErrorId()
{
return m_errorId;
}
private:
// 錯誤碼
int m_errorId;
};
int main(int argc, char* argv[])
{
// 內存洩露檢測機制
_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
// 可以改變錯誤碼,以便拋出不同的異常進行測試
int throwErrorCode = 110;
std::cout << " input test code :" << std::endl;
std::cin >> throwErrorCode;
try
{
if ( throwErrorCode == 110)
{
MyExcepction myStru(110);
// 拋出對象的地址 -> 由catch( MyExcepction* pMyExcepction) 捕獲
// 這裡該對象的地址拋出給catch語句,不會調用對象的拷貝構造函數
// 傳地址是提倡的做法,不會頻繁地調用該對象的構造函數或拷貝構造函數
// catch語句執行結束後,myStru會被析構掉
throw &myStru;
}
else if ( throwErrorCode == 119 )
{
MyExcepction myStru(119);
// 拋出對象,這裡會通過拷貝構造函數創建一個臨時的對象傳出給catch
// 由catch( MyExcepction myExcepction) 捕獲
// 在catch語句中會再次調用通過拷貝構造函數創建臨時對象復制這裡傳過去的對象
// throw結束後myStru會被析構掉
throw myStru;
}
else if ( throwErrorCode == 120 )
{
// 不提倡這樣的拋出方法
// 這樣做的話,如果catch( MyExcepction* pMyExcepction)中不執行delete操作則會發生內存洩露
// 由catch( MyExcepction* pMyExcepction) 捕獲
MyExcepction * pMyStru = new MyExcepction(120);
throw pMyStru;
}
else
{
// 直接創建新對象拋出
// 相當於創建了臨時的對象傳遞給了catch語句
// 由catch接收時通過拷貝構造函數再次創建臨時對象接收傳遞過去的對象
// throw結束後兩次創建的臨時對象會被析構掉
throw MyExcepction(throwErrorCode);
}
}
catch( MyExcepction* pMyExcepction)
{
// 輸出本語句被執行信息
std::cout << "執行了 catch( MyExcepction* pMyExcepction) " << std::endl;
// 輸出錯誤信息
std::cout << "error Code : " << pMyExcepction->getErrorId()<< std::endl;
// 異常拋出的新對象並非創建在函數棧上,而是創建在專用的異常棧上,不需要進行delete
//delete pMyExcepction;
}
catch ( MyExcepction myExcepction)
{
// 輸出本語句被執行信息
std::cout << "執行了 catch ( MyExcepction myExcepction) " << std::endl;
// 輸出錯誤信息
std::cout << "error Code : " << myExcepction.getErrorId()<< std::endl;
}
catch(...)
{
// 輸出本語句被執行信息
std::cout << "執行了 catch(...) " << std::endl;
// 處理不了,重新拋出給上級
throw ;
}
// 暫停
int temp;
std::cin >> temp;
return 0;
}
下面的例子也能很好的說明
class NumberParseException {};
//判斷字符串是否為數字
bool isNumber(char * str) {
using namespace std;
if (str == NULL)
return false;
int len = strlen(str);
if (len == 0)
return false;
bool isaNumber = false;
char ch;
for (int i = 0; i < len; i++) {
if (i == 0 && (str[i] == '-' || str[i] == '+'))
continue;
if (isdigit(str[i])) {
isaNumber = true;
} else {
isaNumber = false;
break;
}
}
return isaNumber;
}
//不是數字的話就拋出異常
int parseNumber(char * str) throw(NumberParseException) {
if (!isNumber(str))
throw NumberParseException();
return atoi(str);
}
異常的接口類型
為了加強程序的可讀性,使函數的用戶能夠方便地知道所使用的函數會拋出哪些異常,在接口聲明的時候就需要將throw的異常標注出來。
列出可能拋出的所有異常
void fun() throw(A, B, C, D);
這表明函數fun()可能並且只可能拋出類型(A, B, C, D)及其子類型的異常。
拋出任何類型的異常
void fun();
這表明該函數可以拋出任何類型的異常
不會拋出任何類型異常
void fun() thow();
這表明該函數不會拋出任何類型的異常
捕獲異常
捕獲異常的代碼一般如下:
try {
throw E(); //在try中拋出了類型為E的異常
}
catch (H h) {
//何時我們可以能到這裡呢
}
1.如果H和E是相同的類型
2.如果H是E的基類
3.如果H和E都是指針類型,而且1或者2對它們所引用的類型成立
4.如果H和E都是引用類型,而且1或者2對H所引用的類型成立
從原則上來說,異常在拋出時被復制,我們最後捕獲的異常只是原始異常的一個副本,所以我們不應該拋出一個不允許拋出一個不允許復制的異常。
此外,我們可以在用於捕獲異常的類型加上const,就像我們可以給函數加上const一樣,限制我們,不能去修改捕捉到的那個異常。
還有,捕獲異常時如果H和E不是引用類型或者指針類型,而且H是E的基類,那麼h對象其實就是H h = E(),最後捕獲的異常對象h會丟失E的附加攜帶信息。
將異常重新拋出
當我們捕獲了一個異常,卻發現無法處理,這種情況下,我們會做完局部能夠做的事情,然後再一次拋出這個異常,讓這個異常在最合適的地方地方處理。例如:
void downloadFileFromServer() {
try {
connect_to_server();
//...
}
catch (NetworkException) {
if (can_handle_it_completely) {
//處理網絡異常,例如重連
} else {
throw;
}
}
}
這個函數是從遠程服務器下載文件,內部調用連接到遠程服務器的函數,但是可能存在著網絡異常,如果多次重連無法成功,就把這個網絡異常拋出,讓上層處理。
重新拋出是采用不帶運算對象的
throw
表示,但是如果重新拋出,又沒有異常可以重新拋出,就會調用terminate()
;
假設
NetworkException
有兩個派生異常叫FtpConnectException
和HttpConnectException
,調用connect_to_server
時是拋出HttpConnectException
,那麼調用downloadFileFromServer
仍然能捕捉到異常HttpConnectException
。
標准異常
到了這裡,你已經基本會使用異常了,可是如果你是函數開發者,並需要把函數給別人使用,在使用異常時,會涉及到自定義異常類,但是C++標准已經定義了一部分標准異常,請盡可能復用這些異常
雖然C++標准異常比較少,但是作為函數開發者,盡可能還是復用c++標准異常,作為函數調用者就可以少花時間去了解的你自定義的異常類,更好的去調用你開發的函數。