Chap 17 異常處理 目之與形,吾不知其異也,而盲者不能自見, 耳之與形,吾不知其異也,而聾者不能自聞; 心之與形,吾不知其異也,而狂者不能自得。 形之與形亦辟矣,而物或間之邪? 《莊子﹒雜篇 卷八上第二十三庚桑楚》 一個完整的程式即使遇到了異常狀況,也要可以正常結束。
Chap 17 異常處理 17.1 異常及其特性 17.2 異常處理的基本語法 17.3 異常的處理過程 17.4 丟擲enum實例做為異常物件 17.5 丟擲類別所定義的物件
17.1 異常 (exceptions) 異常指的是所有可以造成電腦無法正常處理的狀 況。 經常遇到的異常狀況可以分為五種: 1. 資源不足: 又分為「記憶體不足」 (out of memory) 和「儲存空間耗盡」兩種情況。 2. 開檔失敗: 又分為「讀檔」和「存檔」兩種情況。 3. 越界: 索引超過上限或低於下限。 4. 除數為零: 在進行數值除法運算時,經常造成當機。 5. 使用者輸入錯誤
異常的幾個特性 1. 發生時,電腦無法正常地繼續執行工作。 2. 在程式寫作時,無法預知何時會發生。 3. 無法避免,但是可以預先做好因應對策。
當異常發生的可能結果 1. 無預警地突然結束或當機。 2. 程式自動結束,但沒有任何相關訊息。 3. 發出警告訊息後程式自行正常結束。 4. 自行跳過異常部份,但其後的運算可能沒有意義。 5. 通知使用者異常發生的種類,並引導使用者逐步排除異常後繼續工作。
17.2 異常處理的基本語法 C++ 提供了try (嚐試),throw (丟擲),和 catch (捕捉) 三個關鍵字。 把所有可能發生異常的敘述都放在「try區 塊」(try block)。
throw敘述 (throw statement) 和catch區塊 在所有可能發生異常的敘述之前,都可以使 用「throw敘述」丟擲出一個異常訊息物件 (the exception message object)。 通常「throw敘述」都不直接在「try區塊」 之內,而是在「try區塊所呼叫的函數」內。 「catch區塊」用來承接由相關「throw敘 述」所丟出來的訊息物件。
實例: 以「異常處理」的方式阻止程式進行除以零的運算。 我們將程式區分為Exception.cpp和FncLib.cpp兩 個檔案: 1. 檔案Exception.cpp為主程式,包括呼叫函數Divide() 的「try 區塊」,以及準備接收訊息物件的「catch 區塊」。 2. 檔案FncLib.cpp包括的是函數Divide() 的定義。當異常 (除數為零) 發生時,執行「throw敘述」以丟擲出字串訊息物件: throw "除數為零!";
範例程式 檔案 Exception.cpp // Exception.cpp #include <iostream> using namespace std; // --- 函數 Divide() 的原型 ---------------------- float Divide(float, float); // --------- 主程式 ----------------------------- int main() { float M, N; try // 「try 區塊」的開頭 cout << "請輸入兩個數字,\n" << endl; cout << "請輸入第一個數字 :" << endl;
cin >> M; cout << "請輸入第二個數字 :" << endl; cin >> N; cout << "您輸入了 : " << M << " 和 " << N << endl; cout << "這兩個數字相除的結果是: " << Divide(M,N) << endl; } // 「try 區塊」的結尾 catch (char* pError) // 「catch 區塊」的開頭 { cerr << "發生異常: " << pError << endl; } // 「catch 區塊」的結尾 return 0; }
範例程式 檔案 FncLib.cpp // FncLib.cpp #include <cmath> using std::fabs; const float Epsilon = 1.0E-16; // --- 定義函數 Divide() ---------------------------- float Divide(float a, float b) { if (fabs(b) <= Epsilon) throw "除數為零!"; // 「throw 敘述」 return a / b; }
操作結果 兩個典型操作過程的記錄: 操作1 操作2 請輸入兩個數字, 請輸入第一個數字: 23.5 請輸入第二個數字: 7.92 您輸入了 : 23.5 和 7.92 這兩個數字相除的結果是: 2.96717 656.3 您輸入了 : 656.3 和 0 發生異常: 除數為零!
堆疊 (stack) 加入、移出的動作遵守「後進先出」的原 則,就好像我們堆積碗盤的過程,因此稱 為堆疊。
呼叫堆疊(call stack)因為函數閒互相呼叫而發生的變化 假設Fnc0() 內的敘述呼叫了Fnc1(),而Fnc1() 內 的敘述又呼叫了Fnc2(),則形成的堆疊(stack)就 會是:
17.3 異常的處理過程
執行throw敘述 它所在的函數立即執行return的動作,並同時叫 用解構函數 (destructor)。 如果在返回後的函數中找不到吻合的「catch區 塊」,將持續展開堆疊(unwinding the stack)。 如果找不到相吻合的「catch區塊」,就會被預 設的處理函數攔截,並在發出以下的訊息後立即 自動結束程式: Some exceptions happened.
捕捉所有遺漏異常物件的「catch區塊」 異常物件之區塊 (catch-all block),語法如下: catch (...) { // 捕捉之後的動作敘述放在這裏 } 其中三個點「...」代表「所有可能」的意思。
17.4 丟擲enum實例做為異常物件 例如: 宣告enum資料型態ErrorStates: enum ErrorStates {OK, OverFlow, UnderFlow}; 定義一個ErrorStates實例,稱為State並初始化為OK: ErrorStates State = OK;
在異常發生前改變State的值並將其擲出 例如: State = OverFlow; throw State;
在「catch區塊」前,宣告與enum資料型態吻 合的實例 catch (ErrorStates Error) { // 捕捉之後的敘述 }
使用enum的異常處理範例 在每一次有可能超過陣列元素索引的上下限前,以 「異常處理」的方式阻止程式繼續運算。 程式區分為EnumEx.cpp和EnumFncLib.cpp兩個檔案: 1. 檔案EnumEx.cpp為主程式,包括呼叫函數ShowVectorElement() 的「try區塊」,以及準備接收訊息物件的「catch區塊」。 檔案EnumFncLib.cpp包括函數ShowVectorElement( ) 的定義。當異常(除數為零)發生時,執行「throw敘述」。
範例程式 檔案 EnumEx.cpp // EnumEx.cpp #include <iostream> using namespace std; float ShowVectorElement(float [],int); // 函數的原型 // 定義 enum資料型態ErrorStates enum ErrorStates {OK, OverFlow, UnderFlow}; // ---- 主程式 ----------------------- int main() { const int Size = 5; float V[Size]; int M; for (int i=0; i< Size; i++) V[i] = float(0.5+ i*i)/2.5;
try // 「try 區塊」的開頭 { cout << "請輸入一個數字(在 0 至 " << Size << " 範圍內):" << endl; cin >> M; cout << "您輸入了: " << M << endl; cout << "V[" << M << "]" << " 的值是: " << ShowVectorElement(V, M) << endl; } // 「try 區塊」的結尾
catch (ErrorStates Error) // 「catch 區塊」的開頭 { if (Error==OverFlow) cerr << "發生異常: OverFlow" << endl; if (Error==UnderFlow) cerr << "發生異常: UnderFlow" << endl; } // 「catch 區塊」的結尾 catch (...) // catch-all 區塊的開頭 {cerr << "發生了預計範圍之外的異常!" << endl; } // catch-all 區塊的結尾 return 0; }
範例程式 檔案 EnumFncLib.cpp // EnumFncLib.cpp #include <cmath> using std::fabs; // 定義 enum資料型態ErrorStates enum ErrorStates {OK, OverFlow, UnderFlow}; const float Epsilon = 1.0E-16; extern const int Size = 5;
// 函數 ShowVectorElement() 的定義 float ShowVectorElement(float A[],int N) { ErrorStates State = OK; // 定義enum實例 State if (N >= Size) State = OverFlow; throw State; // 「throw 敘述」 } if (N < 0) State = UnderFlow; throw State; // 「throw 敘述」 return A[N];
操作結果 三個典型操作過程的記錄: 操作 1 操作2 操作3 請輸入一個數字 (在 0 至 5 範圍內): 3 您輸入了: 3 V[3] 的值是: 3.8 5 您輸入了: 5 發生異常: OverFlow -2 您輸入了 : -2 發生異常: UnderFlow
17.5 丟擲類別 (Class) 所定義的物件(Object) class OverFlow{ }; // 不需要有具體內容 class UnderFlow{ }; 在異常發生處,其「throw敘述」可以直接寫成: throw OverFlow(); // ( ) 表示在執行 //「throw敘述」時 throw UnderFlow(); // 同時呼叫建構函數 //(constructor)
接收物件的「catch區塊」 例如: catch (OverFlow) // 接收端只要寫出類別名稱 { } catch (UnderFlow) // 捕捉UnderFlow物件之後的敘述
使用物件的異常處理範例 程式區分為Common.h,ObjEx.cpp和ObjFncLib.cpp三個檔 案: 1. 檔案Common.h為標頭檔,包括共同需要的名稱空間和函數的原型。 名稱空間ExcNameSpace定義了常數Size和Epsilon,以及OverFlow和UnderFlow兩種類別。 2. 檔案ObjEx.cpp為主程式,包括「try區塊」,以及準備接收訊息物件的「catch區塊」。 3. 檔案ObjFncLib.cpp內有函數的定義。 當異常 (除數為零) 發生時,執行「throw敘述」以丟擲出OverFlow或UnderFlow兩種類別產生的物件 (object)。
範例程式 檔案 Common.h //Common.h #ifndef COMMON_H #define COMMON_H #include <iostream> #include <iomanip> #include <math> using namespace std;
namespace ExcNameSpace { class OverFlow { }; class UnderFlow { }; const int Size = 5; const float Epsilon = 1.0E-16; } // - 函數 ShowVectorElement() 的原型 float ShowVectorElement(float [],int); #endif
範例程式 檔案 ObjEx.cpp // ObjEx.cpp #include "Common.h" using namespace ExcNameSpace; // ----主程式----------------------- int main() { const int Size = 5; float V[Size]; int M; for (int i=0; i< Size; i++) V[i] = float(0.5+ i*i)/2.5;
try // 「try 區塊」的開頭 { cout << "請輸入一個數字(在 0 至 " << Size << " 範圍內):" << endl; cin >> M; cout << "您輸入了: “ << M << endl; cout << "V[" << M << "]" << " 的值是: " << ShowVectorElement(V, M) << endl; } // 「try 區塊」的結尾 catch (OverFlow) cerr << "發生異常: OverFlow" << endl; }
catch (UnderFlow) { cerr << "發生異常: UnderFlow" << endl; } catch (...) cerr << "發生了預計範圍之外的異常!" << endl; return 0;
範例程式 檔案 ObjFncLib.cpp // ObjFncLib.cpp #include "Common.h" using namespace ExcNameSpace; // 函數 ShowVectorElement() 的定義 float ShowVectorElement(float A[],int N) { if (N>=Size) { throw OverFlow(); } // 「throw 敘述」 if (N < 0) { throw UnderFlow(); } // 「throw 敘述」 return A[N]; }
操作結果 程式的操作方式及功能都和17.4節相同。以下是三個典型操作過程的記錄: 操作 1 操作 2 操作 3 請輸入一個數字 (在 0 至 5 範圍內): 4 您輸入了: 4 V[4] 的值是: 6.6 6 您輸入了: 6 發生異常: OverFlow -1 您輸入了 : -1 發生異常: UnderFlow
「catch區塊」的編排次序 「特例」異常放在前面,「通例」異常放在後面。 例如: 絕大部分丟擲物件的語法都可以被丟擲字串的語法 所取代。 catch (Zero) { //捕捉Zero之後的敘述 } catch (TooSmall) { //捕捉TooSmall之後的敘述 } catch (WrongSize) { //捕捉WrongSize之後的敘述 } 絕大部分丟擲物件的語法都可以被丟擲字串的語法 所取代。