Template and exception-xử lý Exception
Xử lý exception là phương thức thống nhất của C++ giúp lập trình viên quản lý theo cùng một phong cách đối với các lỗi phát sinh bởi chương trình vào lúc chạy. Nhờ phương thức này, chúng ta có thể quy định các thủ tục cần được thực hiện một ...
Xử lý exception là phương thức thống nhất của C++ giúp lập trình viên quản lý theo cùng một phong cách đối với các lỗi phát sinh bởi chương trình vào lúc chạy. Nhờ phương thức này, chúng ta có thể quy định các thủ tục cần được thực hiện một cách tự động trong từng trường hợp lỗi. Điểm quan trọng nhất mà phương thức xử lý exception là sự bảo đảm cho chương trình không chấm dứt tức thời, đột ngột hoặc nhảy tới đoạn mã thực hiện không đúng.
Các từ khóa try, catch và throw:
Xử lý exception của C++ dùng ba câu lệnh được thêm vào ngôn ngữ C++, đó là try, catch và throw. Ba từ khóa này tạo nên một cơ chế cho phép các chương trình thông báo với nhau về các vấn đề bất thường và nghiêm trọng xảy ra. Trong đoạn mã của chúng ta có ba nới tham gia vào việc phát hiện exception:
Khối try đánh dấu đoạn mã mà chúng ta nghi ngờ có thể xảy ra sai lầm khi chạy.
Khối catch nằnm ngay liền sau khối try, chứa đoạn mã lo giải quyết vấn đề bất thường xảy ra gọi là exception handler. Có thể có nhiều lệnh catch, mỗi lệnh chuyên một loại exception. Trong mỗi khối catch có thể khai báo cho biết loại exception nào chúng ta muốn giải quyết.
Lệnh throw là làm thế nào đoạn mã có vấn đề thông báo exception handler biết vấn đề phải giải quyết.
Nếu ở bất cứ chỗ nào trong chương trình, chúng ta phát hiện một điều kiện gây ra chương trình phải thoát khối try nhảy về một exception handler, chúng ta có thể nén ra exception riêng của chúng ta bằng cách dùng lệnh throw. Lệnh throw sử dụng cú pháp tương tự như lệnh return cho phép chúng ta nén ra bất cứ biến nào đặc biệt (hoặc biểu thức gán) mà chúng ta muốn. Chú ý rằng kiểu biến chúng ta ném ra phải khớp với kiểu được khai báo trong khối catch đâu đó.
try
{
…………….
}
catch( <exception declaration> )
{
…………….
}
catch( <exception declaration> )
{
…………….
}
Ví dụ 9.5: Minh họa các từ khóa try, catch và throw (Các chương trình về exception phải chạy trên Borland C++ 4.0 trở lên)
CT9_5.CPP1: //Chương trình 9.52: #include <iostream.h>3:4: int main()5: {6: cout<<"Start"<<endl;7: try8: {9: cout<<"Inside block try"<<endl;10: throw 100;11: cout<<"This will not execute"<<endl;12: }13: catch(int I) 14: {15: cout<<"Caught One! Number is "<<I<<endl;16: }17: cout<<"End"<<endl;18: return 0;19: } | ||
Chúng ta chạy ví dụ 9.5, kết quả ở hình 9.5
Hình 9.5: Kết quả của ví dụ 9.5
Trong ví dụ 9.5, khối try chỉ có vài câu lệnh (dòng 7 đến 12), trong đó có lệnh:
throw 100; //Ném ra trị 100
trị này sẽ được bắt bởi khối catch ngay sau khối try. Chính nhờ khối này chúng ta ứng phó với lỗi xảy ra. Đây chính là exception handler. Nó chỉ in trị bắt lỗi được ra màn hình. Sau hoạt động của exception handler, dòng điều khiển của chương trình không trở về câu lệnh ở dòng 11 mà tiếp tục với câu lệnh sau khối catch (dòng 17).
Như vậy một sai lầm xảy ra, và một exception được ném ra trong khối try thì quyền điều khiển sẽ nhảy về khối try và bắt đầu dò tìm trên các khối catch, khối catch nào sẽ giải quyết "bệnh tình" của mình (phần còn lại của khối try sẽ không được thi hành).
Nếu khối catch nào khớp với loại exception được ném ra thì đoạn mã trong khối catch này sẽ được thi hành, và việc thi hành chấm dứt ở cuối khối catch.
Nếu không có khối catch nào khớp với loại exception được ném ra thì chương trình sẽ dò tìm trên call stack (stack chứa các hàm được gọi) đi tìm lần lên cấp cao các exception handler thích hợp được tìm thấy.
Nếu không có exception nào được ném ra thì các catch sẽ không được thi hành (Hàm terminate() được gọi, hàm này mặc định gọi hàm abort()).
Biến được khai báo trong câu lệnh catch có thể thuộc bất cứ kiểu dữ liệu nào chúng ta muốn ném ra. Đây có thể là kiểu dữ liệu đơn giản (int, char *, …) hoặc bất cứ lớp nào chúng ta đã định nghĩa. Ngoài ra chúng ta có thể sử dụng ba dấu chấm (…) như là kiểu exception trong khối catch để xử lý bất kỳ kiểu của exception nào và chỉ dùng ở cuối đối với một khối try. Exception handler sử dụng ba dấu chấm:
catch(…)
{
……………..
}
và được gọi là ellipsis handler.
Chú ý:
Bên trong exception handler chương trình có thể tiếp tục hoặc kết thúc bằng cách gọi hàm exit() hoặc abort().
Nếu bên trong khối try có đối tượng được tạo ra thì khi chương trình bị đổi hướng do lệnh throw thì đối tượng sẽ bị hủy trước khi chương trình bị đổi hướng.
Các tình huống xử lý exception:
Khi một exception không bị bắt
Nếu một exception được ném ra trong trường hợp sau khối try không có exception handler không khớp thì hàm terminate() được gọi nhằm kết thúc chương trình.
Ví dụ 9.6:
CT9_6.CPP1: //Chương trình 9.62: #include <iostream.h>3:4: int main()5: {6: cout<<"Start"<<endl;7: try8: {9: cout<<"Inside try block"<<endl;10: throw 100;11: cout<<"This will not execute"<<endl;12: }13: catch(long I) 14: {15: cout<<"Caught One! Number is "<<I<<endl;16: }17: cout<<"End"<<endl;18: return 0;19: } | ||
Chúng ta chạy ví dụ 9.6, kết quả ở hình 9.6
Hình 9.6: Kết quả của ví dụ 9.6
Ném một exception trong hàm bất kỳ
Chúng ta có thể ném một exception từ trong bất kỳ hàm nào miễn là hàm đó sẽ được gọi trong khối try.
Ví dụ 9.7:
CT9_7.CPP1: //Chương trình 9.72: #include <iostream.h>3:4: void MyTest(int Test)5: {6: cout<<"Inside MyTest(), Test is "<<Test<<endl;7: if (Test)8: throw Test;9: }10:11: int main()12: {13: cout<<"Start"<<endl;14: try15: {16: cout<<"Inside try block"<<endl;17: MyTest(0);18: MyTest(1);19: MyTest(2);20: }21: catch(int I)22: {23: cout<<"Caught One! Number is "<<I<<endl;24: }25: cout<<"End"<<endl;26: return 0;27: } | ||
Chúng ta chạy ví dụ 9.7, kết quả ở hình 9.7
Hình 9.7: Kết quả của ví dụ 9.7
Trị được ném ra không phải là trị trả về của hàm và có thể thuộc kiểu bất kỳ tuỳ chúng ta chọn (không liên quan gì đến kiểu trả về của hàm). Chú ý rằng lệnh return phục hồi stack về trạng thái trước khi gọi hàm, trong khi lệnh throw tái lập stack như trước khi vào khối try.
Khối try bên trong hàm
Chúng ta có thể đặt khối try bên trong hàm bất kỳ để xử lý exception lúc chạy sinh bởi đoạn mã nào đó thuộc hàm.
Ví dụ 9.8:
CT9_8.CPP1: //Chương trình 9.82: #include <iostream.h>3:4: void MyHandler(int Test)5: {6: try7: {8: if (Test)9: throw Test;10: }11: catch(int I)12: {13: cout<<"Caught One! Ex. #"<<I<<endl;14: }15: }16:17: int main()18: { 19: cout<<"Start"<<endl;20: MyHandler(1);21: MyHandler(2);22: MyHandler(0);23: MyHandler(3);24: cout<<"End"<<endl;25: return 0;26: } | ||
Chúng ta chạy ví dụ 9.8, kết quả ở hình 9.8
Hình 9.8: Kết quả của ví dụ 9.8
Nhiều khối catch
Chúng ta có thể tạo nhiều exception handler. Mỗi exception handler có nhiệm vụ bắt một exception thuộc kiểu nào đó.
Ví dụ 9.9:
CT9_9.CPP1: //Chương trình 9.92: #include <iostream.h>3:4: void MyHandler(int Test)5: {6: try7: {8: if (Test)9: throw Test;10: else11: throw "Value is zero";12: }13: catch(int I)14: {15: cout<<"Caught One! Ex. #"<<I<<endl;16: }17: catch(char *Str)18: {19: cout<<"Caught a string:"<<Str<<endl;20: }21: }22:23: int main()24: {25: cout<<"Start"<<endl;26: MyHandler(1);27: MyHandler(2);28: MyHandler(0);29: MyHandler(3);30: cout<<"End"<<endl;31: return 0;32: } | ||
Chúng ta chạy ví dụ 9.9, kết quả ở hình 9.9
Hình 9.9: Kết quả của ví dụ 9.9
Bắt exception có kiểu bất kỳ
Chúng ta có thể tạo ra một exception handler có khả năng bắt exception có kiể bất kỳ, đây chính là ellipsis handler.
Ví dụ 9.10:
CT9_10.CPP1: //Chương trình 9.102: #include <iostream.h>3:4: void MyHandler(int Test)5: {6: try7: {8: if (Test==0)9: throw Test;10: if (Test==1)11: throw 'a';12: if (Test==2)13: throw 123.34;14: if (Test==3)15: throw "Test";16: }17: catch(int)18: {19: cout<<"Caught int"<<endl;20: }21: catch(double)22: {23: cout<<"Caught double"<<endl;24: }25: catch(...)26: {27: cout<<"Caught One"<<endl;28: }29: }30:31: int main()32: {33: cout<<"Start"<<endl;34: MyHandler(0);35: MyHandler(1);36: MyHandler(2);37: MyHandler(3);38: cout<<"End"<<endl;39: return 0;40: } | ||
Chúng ta chạy ví dụ 9.10, kết quả ở hình 9.10
Hình 9.10: Kết quả của ví dụ 9.10
Nén exception từ một exception handler
C++ cho phép một exception handler ném một exception mà nó bắt được. Muốn vậy, trong exception handler chúng ta chỉ ghi câu lệnh:
throw;
Bằng cách như vậy, một exception handler chuyển exception mà nó bắt được cho khối try khác chứa nó.
Ví dụ 9.11:
CT9_11.CPP1: //Chương trình 9.112: #include <iostream.h>3:4: void MyHandler() 5: {6: try7: {8: throw "Hello!";9: }10: catch(char *)11: {12: cout<<"Caught inside MyHandler()"<<endl;13: throw; //Ném tiếp ra ngoài14: }15: }16:17: int main()18: {19: cout<<"Start"<<endl;20: try21: {22: MyHandler();23: }24: catch(char *)25: {26: cout<<"Caught inside main()"<<endl;27: }28: cout<<"End"<<endl;29: return 0;30: } | ||
Chúng ta chạy ví dụ 9.11, kết quả ở hình 9.11
Hình 9.11: Kết quả của ví dụ 9.11
Trong chương trình ở ví dụ 9.11, khối catch(char *) trong hàm MyHandler() bắt được một chuỗi và ném tiếp ra ngoài. Do vậy, khi hàm MyHandler() được gọi bên trong khối try ở hàm main(), khối catch(char *) trong hàm main() lại bắt được chuối đó.
Với cơ chế này, chúng ta có thể tạo ra khả năng xử lý exception theo nhiều cấp bằng cấu trúc try/catch lồng nhau. Những gì cấp dưới (catch bên trong) không giải quyết thì chuyển lên cấp trên (catch bên ngoài) để dứt điểm.
Hạn chế về kiểu của một exception
Như đã nói, exception được ném ra từ trong một hàm không có liên quan gì đến trị trả về của hàm đó. Exception có thể thuộc kiểu tùy ý. Tuy nhiên, nếu cần chúng ta cũng có thể quy định kiểu của exception. Điều này gọi là đặc tả exception (exception specification). Để thực hiện hạn chế, chúng ta phải thêm throw vào định nghĩa hàm. Dạng tổng quát:
<return type> <function name> ( <parameter list> ) throw ( <type list> )
{
……………
}
Trong đó chỉ có các exception có kiểu trong danh sách <type list> (phân cách bằng dấu phẩy) là được ném ra từ hàm. Ngược lại, việc ném ra các exception thuộc kiểu khác là chương trình kết thúc bất thường (gọi hàm unexpected(), mặc định hàm này gọi hàm abort()).
Nếu <type list> rỗng thì cấm ném bất kỳ cái gì ra bên ngoài. Nếu hàm đang xét cố tình vi phạm thì chương trình sẽ kết thúc ngay bởi hàm unexpected().
Ví dụ 9.12:
CT9_12.CPP1: //Chương trình 9.122: #include <iostream.h>3:4: void MyHandler(int Test) throw (int,char,double)5: {6: if (Test==0)7: throw Test;8: if (Test==1)9: throw 'a';10: if (Test==2)11: throw 123.34;12: }13:14: int main()15: {16: cout<<"Start"<<endl;17: try18: {19: MyHandler(0);20: }21: catch(int)22: {23: cout<<"Caught int"<<endl;24: }25: catch(double)26: {27: cout<<"Caught double"<<endl;28: }29: catch(char)30: {31: cout<<"Caught char"<<endl;32: }33: cout<<"End"<<endl;34: return 0;35: } | ||
Chúng ta chạy ví dụ 9.12, kết quả ở hình 9.12
Hình 9.12: Kết quả của ví dụ 9.12
Chú ý rằng quy định vừa nêu có hiệu lực đối với exception ném ra khỏi hàm tức là trong trường hợp hàm được gọi bên trong một khối try. Đối với trường hợp khối try đặt bên trong, exception được ném và bắt bên trong hàm vẫn không bị hạn chế.
Ví dụ:
Ví dụ 9.13: Chương trình sau là một ví dụ về xử lý exception để kiểm tra lỗi "chia cho zero".
CT9_13.CPP1: //Chương trình 9.132: #include <iostream.h>3: #include <string.h>4:5: //Định nghĩa một lớp DivideByZeroError để sử dụng trong xử lý exception6: //cho việc ném một exception với một phép chia cho zero.7: class DivideByZeroError8: {9: private:10: char Message[15];11: public:12: DivideByZeroError()13: {14: strcpy(Message, "Divide by zero");15: }16: void PrintMessage() const17: {18: cout << Message;19: }20: };21:22: //Định nghĩa hàm thương số. Được sử dụng để minh họa ném23: //một exception khi lỗi chia cho zero bắt gặp24: float Quotient(int Num1, int Num2)25: {26: if (Num2 == 0)27: throw DivideByZeroError();28: return (float) Num1 / Num2;29: }30:31: int main()32: {33: cout << "Enter two integers to get their quotient: ";34: int Number1, Number2;35: cin >> Number1 >> Number2;36: try37: {38: float result = Quotient(Number1, Number2);39: cout << "The quotient is: " << result << endl;40: }41: catch (DivideByZeroError Error)42: { 43: cout << "ERROR: ";44: Error.PrintMessage();45: cout << endl;46: return 1; //Kết thúc bởi vì có lỗi47: }48: return 0; //Kết thúc bình thường49: } | ||
Chúng ta chạy ví dụ 9.13, kết quả ở hình 9.13
Hình 9.13: Kết quả của ví dụ 9.13
Toàn bộ các ví dụ về exception (từ ví dụ 9.5 đến 9.13) được đưa vào một project của Borland C++ 4.0 với tên file nén là Project_BC4.zip hoặc workspace trong Visual C++ 6.0 với tên file nén là Project_VC6.zip.