異常處理這部分內容其實並不屬於OPP的技術,僅僅是C++對程序出錯的處理。
異常處理
程序中常見的錯誤有兩大類:語法錯誤和運行錯誤。在編譯時,編譯系統能發現程序中的語法錯誤。
在設計程序時,應當事先分析程序運行時可能出現的各種意外的情況,並且分別制訂出相應的處理方法,這就是程序的異常處理的任務。
在運行沒有異常處理的程序時,如果運行情況出現異常,由於程序本身不能處理,程序只能終止運行。如果在程序中設置了異常處理機制,則在運行情況出現異常時,由於程序本身已規定了處理的方法,於是程序的流程就轉到異常處理代碼段處理。
異常(exception)是運行時(run-time)的錯誤,通常是非正常條件下引起的,例如,下標(index)越界、new操作不能正常分配所需內存。
C語言中,異常通常是通過被調用函數返回一個數值作為標記的。
C++采取的辦法是:如果在執行一個函數過程中出現異常,可以不在本函數中立即處理,而是發出一個信息,傳給它的上一級(即調用它的函數),它的上級捕捉到這個信息后進行處理。如果上一級的函數也不能處理,就再傳給其上一級,由其上一級處理。如此逐級上送,如果到最高一級還無法處理,最后只好異常終止程序的執行。
C++中,函數可以識別標記為異常的條件,然后通告發生了異常。
這種通告異常的機制稱為拋出異常(throwing an exception)。
為什么要使用異常處理?
1.常規代碼與錯誤處理代碼的分離
用偽代碼描述下面這個程序的各部分功能
readFile
{
open the file;
determine its size;
allocate that much memory;
read the file into memory;
close the file;
}
What happens if the file can't be opened?
What happens if the length of the file can't be determined?
What happens if enough memory can‘t be allocated(分配)?
What happens if the read fails(失敗)?
What happens if the file can't be closed?
面對這些可能發生的問題我們需要在編寫程序的時候就要考慮到,如果采用非異常處理的方法會得到下面的代碼:
errorCodeType readFile { initialize errorCode = 0; open the file; if (theFileIsOpen) { determine the length of the file; if (gotTheFileLength) { allocate that much memory; if (gotEnoughMemory) { read the file into memory; if (readFailed) { errorCode = -1; } } else { errorCode = -2; } } else { errorCode = -3; } close the file; if (theFileDidntClose && errorCode == 0) { errorCode = -4; } else { errorCode = errorCode and -4; } } else { errorCode = -5; } return errorCode; }
我們為了確定到底在代碼執行的那一個部分出錯,嵌套了太多的if語句,這顯然是我們不能接受的。如果采用C++的異常處理得到下面的代碼:
readFile { try { open the file; determine its size; allocate that much memory; read the file into memory; close the file; } catch (fileOpenFailed) { doSomething; } catch (sizeDeterminationFailed) { doSomething; } catch (memoryAllocationFailed) { doSomething; } catch (readFailed) { doSomething; } catch (fileCloseFailed) { doSomething; } }
實現了常規代碼與錯誤處理代碼的分離。
2.在調用棧中傳播異常
method1
{
call method2;
}
method2
{
call method3;
}
method3
{
call readFile;
}
如果有上面這樣的函數嵌套,我們想要確定到底在那一個函數中出錯,如果采用非異常處理的方法會得到下面的代碼:
method1 { errorCodeType error; error = call method2; if (error) doErrorProcessing; else proceed; } errorCodeType method2 { errorCodeType error; error = call method3; if (error) return error; else proceed; } errorCodeType method3 { errorCodeType error; error = call readFile; if (error) return error; else proceed; }
異常處理方式:
method1 { try { call method2; } catch (exception e) { doErrorProcessing; } } method2 { try { call method3; } catch (exception e) { doErrorProcessing; } } method3 { try { readFile; } catch (exception e) { doErrorProcessing; } }
3.對不同的錯誤類型進行分類
catch (FileNotFoundException e) { ... } catch (IOException e) { … …. } … catch (Exception e) //A (too) general exception handler { ... }
異常處理方式
C++中,try與catch用於實現異常的處理。
C++處理異常的機制是由3個部分組成的,即檢查(try)、拋出(throw)和捕捉(catch)。
把需要檢查的語句放在try塊中,throw用來當出現異常時發出一個異常信息,而catch則用來捕捉異常信息,如果捕捉到了異常信息,就處理它。
Catch塊可以任何順序排列。
一個關鍵要求是catch必須定義在try塊之后,在try塊中可能會有異常被拋出(發生)。
異常與catch是以類型來進行匹配的。
其中異常也分兩種,一種是系統定義的異常,另一種是用戶自定義的異常。
1.系統定義異常
例如下面的程序,刪除字符串中的一部分內容,一旦要刪除的內容造成越界,系統就會拋出異常。
#include <iostream> #include <string> #include <stdexcept> using namespace std; int main() { string s = "Mike Jackson"; int index, len; while(true) { cout << "Enter index and length to erase: "; cin >> index >> len; try { s.erase(index, len); } catch ( out_of_range ) // out_of_range is a system-defined type { continue; } cout<<s; break; } return 0; }
3.用戶自定義異常
下面的代碼為了得到正確的數組第i個數的值,需要自己按照數組的規模進行限制。
const int MaxSize = 1000; float arr[MaxSize]; enum out_of_bounds { underflow, overflow }; float& access( int i ) { if ( i < 0 ) throw underflow; if ( i > MaxSize ) throw overflow; return arr[i]; } void g( ) { // … try { val = access( k ); } catch( out_of_bounds t) { if ( t == underflow ) { cerr << "arr: underflow…aborting\n"; exit( EXIT_FAILURE ); } if( t == overflow ) { cerr << "arr: overflow…aborting\n"; exit( EXIT_FAILURE ); } //… } }
通常,如果一個函數拋出了一個異常,但沒有對應的catch處理它,則系統通過調用函數unexpected 函數去處理它。
實際上(In effect), unexpected 是沒有被程序員處理的異常的缺省處理者。
函數嵌套檢測異常處理
int main( ) { try { f1( ); } catch(double) { cout<<″OK0!″<<endl; } cout<<″end0″<<endl; return 0; } void f1( ) { try { f2( ); } catch(char) { cout<<″OK1!″; } cout<<″end1″<<endl; } void f2( ) { try { f3( ); } catch(int) { cout<<″Ok2!″<<endl; } cout<<″end2″<<endl; } void f3( ) { double a=0; try { throw a; } catch(float) { cout<<″OK3!″<<endl; } cout<<″end3″<<endl; }
其異常處理如圖所示:
程序運行結果如下:
OK0! (在主函數中捕獲異常)
end0 (執行主函數中最后一個語句時的輸出)
該程序在f3函數的時候拋出了一個異常,但f3函數沒有去捕獲,f2、f1也都沒有捕獲,在主函數的時候被捕獲到。
(2) 如果將f3函數中的catch子句改為catch(double),而程序中其他部分不變,則程序運行結果如下:
OK3! (在f3函數中捕獲異常)
end3 (執行f3函數中最后一個語句時的輸出)
end2 (執行f2函數中最后一個語句時的輸出)
end1 (執行f1函數中最后一個語句時的輸出)
end0 (執行主函數中最后一個語句時的輸出)
該程序在f3函數的時候拋出了一個異常,立刻被f3函數捕獲,將執行完f3函數,f3函數的執行完成意味着主函數、f1、f2的調用完成。
(3)如果在此基礎上再將f3函數中的catch塊改為
catch(double) { cout<<″OK3!″<<endl; throw; }
程序運行結果如下:
OK3! (在f3函數中捕獲異常)
OK0! (在主函數中捕獲異常)
end0 (執行主函數中最后一個語句時的輸出)
該程序在f3函數的時候拋出了一個異常,立刻被f3函數捕獲,然后又拋出一個新的異常,這個異常直到主函數才被捕獲。
異常處理實例
編寫一個計算三角形面積的函數,函數的參數為三角形三邊邊長a、b、c,可以用Heron公式計算:
#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 || c + a <= 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
下面介紹異常處理需要注意的幾點:
(1) 首先把可能出現異常的、需要檢查的語句或程序段放在try后面的花括號中。被檢測的函數必須放在try塊中,否則不起作用。
(2) try塊和catch塊作為一個整體出現,catch塊是try-catch結構中的一部分,必須緊跟在try塊之后,不能單獨使用,在二者之間也不能插入其他語句。但是在一個try-catch結構中,可以只有try塊而無catch塊。
程序開始運行后,按正常的順序執行到try塊,開始執行try塊中花括號內的語句。如果在執行try塊內的語句過程中沒有發生異常,則catch子句不起作用,流程轉到catch子句后面的語句繼續執行。一個try-catch結構中只能有一個try塊,但卻可以有多個catch塊,以便與不同的異常信息匹配。
(3)try和catch塊中必須有用花括號括起來的復合語句,即使花括號內只有一個語句,也不能省略花括號。
(4) 如果在執行try塊內的語句(包括其所調用的函數)過程中發生異常,則throw運算符拋出一個異常信息。throw拋出異常信息后,流程立即離開本函數,轉到其上一級的函數(main 函數)。
throw拋出什么樣的數據由程序設計者自定,可以是任何類型的數據。
(5) 這個異常信息提供給try-catch結構,系統會尋找與之匹配的catch子句。
(6) 在進行異常處理后,程序並不會自動終止,繼續執行catch子句后面的語句。
(7) catch只檢查所捕獲異常信息的類型,而不檢查它們的值。因此如果需要檢測多個不同的異常信息,應當由throw拋出不同類型的異常信息。異常信息可以是C++系統預定義的標准類型,也可以是用戶自定義的類型(如結構體或類)。
(8) 如果在catch子句中沒有指定異常信息的類型,而用了刪節號“…”,則表示它可以捕捉任何類型的異常信息,如:
catch(…)
{
cout<<″OK″<<endl;
}
try-catch結構可以與throw出現在同一個函數中,也可以不在同一函數中。當throw拋出異常信息后,首先在本函數中尋找與之匹配的catch,如果在本函數中無try-catch結構或找不到與之匹配的catch,就轉到離開出現異常最近的try-catch結構去處理。
(9) 如果throw拋出的異常信息找不到與之匹配的catch塊,那么系統就會調用一個系統函數terminate,使程序終止運行。