Exception Handling trong C++
Trong Java chắc chắn bạn sẽ không lạ lẫm gì với exception handling , một cơ chế giúp cho bạn dễ dàng xử lý các trường hợp, tình huống lỗi mà chương trình gặp phải trong quá trình thực thi. Thế còn trong C++ thì sao ? Đối với ANSI C++ thì tính năng này hòan tòan đã được tích hợp và cho phép bạn một cơ chế linh họat và dễ dàng cho việc xứ lý các tình huống lỗi. Bài viết này sẽ nhằm giới thiệu với các bạn exception handling trong ANSI C++ và bàn đến những lợi ích mà exception handling mang lại so với cách xử lý lỗi truyền thống.
1. Giới thiệu về Exception
Exception là gì ? Nghĩa chính xác của từ exception là những trường hợp ngọai lệ, chúng ta không gọi nó là các lỗi thực thi vì nó thực sự chưa xảy ra, chúng ta chỉ đơn giản kiểm tra và bắt các trường hợp ngọai lệ mà chúng ta biết chắc chắn sẽ gây ra lỗi Chúng ta sẽ thấy trong một ví dụ cụ thể sau. Trong một chương trình mộ phỏng các phản ứng hạt nhân, chúng ta cần đưa vào các tham số phương trình hoặc lưu lượng các chất cho thích hợp, dựa trên đó mà chương trình sẽ mô phỏng và tính tóan chính xác tầm ảnh hưởng của phản ứng, thế nhưng trong một số trường hợp chúng ta đưa vào các tham số hoặc lưu lượng quá mức làm cho phản ứng vượt quá mức không chính xác thì khi đó chương trình cần thông báo cho chúng ta lỗi ví dụ như :Tác hại nghiêm trọng Không thực hiện được Khi đó buộc chúng ta phải hủy bỏ phản ứng và quay trở lại vị trí ban đầu là nhập vào các tham số và lưu lượng chính xác. Chương trình phải làm việc kiểm tra và bắt hết các trường hợp ngọai lệ và nguy hiểm trước khi cho nguy hiểm đó có thể xảy ra. Nếu như chương trình cho phép phản ứng diễn ra rồi mới kiểm tra tầm tác hại thì coi như nó cũng tiêu tùng luôn rồi còn gì !!!!
2. Exception Handling
Hãy xét một đọan code nhỏ sau:
C++ Code:
if (kiem_tra_tham_so() && kiem_tra_luu_luong()) { . tien hanh phan ung } else { in thong bao loi va quay lai }
Đọan code trên là cách bắt lỗi thông thường nhưng với cách bắt lỗi trên thì rất kém hiệu quả vì sao ? Hãy xét ví dụ như việc kiểm tra quá trình phản ứng cần thông qua 10 quá trình kiểm tra khác nhau, khi bất kì trong một quá trình nào không đúng thì buộc phải hủy bỏ và quay lại vị trí ban đầu,
C++ Code:
SYSTEM_WORK = OK; while (1) { nhap cac tham so; if (kiem_tra_A()) SYSTEM_WORK = OK; else SYSTEM_WORK = FAIL; if (SYSTEM_WORK == OK && kiem_tra_B()) if . if (SYTEM_WORK == OK) { tien_hanh_phan_ung; exit ; } }
Bạn sẽ thấy là đoạn code trên rất là cồng kềnh và không đẹp thế nên ANSI C++ đã đưa vào một cơ chế trong phần ngôn ngữ cơ chế để chúng ta kiểm tra và bắt các trường hợp ngọai lệ một cách hiệu quả hơn, cơ chế sử dụng chủ yếu 3 từ khóa là try , catch và throw Xét đọan code trên bằng cách dùng exception handling
C++ Code:
try { if (!kiem_tra_A()) throw A fail; if (!kiem_tra_B()) throw B fail; . } catch (string exception) { }
Bạn sẽ thấy là đọan code trên nhìn nó gọn và logic hơn rất nhiều và tránh cho chúng ta rất nhiều trường hợp rắc rối không cần thiết với cách thức truyền thống. Trong phần đọan mã sau từ khóa try thì chương trình sẽ kiểm tra nếu như có bất kì một exception nào xảy ra hoặc là throw exception được rồi thì chương trình sẽ lập tức chấm dứt và chuyển đến phần đoạn mã của từ khóa catch, từ khóa catch sẽ nhận tham số là các exception cần bắt và in ra thông báo lỗi
3. Exception Handling trong C++
Đối với lệnh throw thì tham số là bất kì kiểu dữ liệu nào, từ int, string, class, struct, enum .v.v trong C++ bạn đều có thể dùng được với throw và dựa trên kiểu dữ liệu đó mà lệnh catch sẽ bắt chính xác cho từng exception. Ví dụ nếu chúng ta kiểm tra một phép chia cho một số khác 0 và nhỏ hơn 100 như sau:
C++ Code:
Bi giờ chúng ta hãy thử đối với một class
C++ Code:
class Exception { public: string CODE; Exception(string _code) : CODE(_code) {} }; try { if (!kiem_tra_A()) throw Exception(A that bai); if (!kiem_tra_B()) throw Exception(B that bai); } catch (Exception e) { }
Rất ư là tiện lợi phải không ? Có thể nói exception handling là một cơ chế rất mạnh trong các ngôn ngữ lập trình hiện đại, viec thuần thục ứng dụng tính năng này sẽ giúp chương trình của bạn rõ ràng hơn, ít cồng kềnh hơn và tiết kiệm được chi phí, nâng cao năng suất lao động. Điều ràng buộc duy nhất là C++ compiler của bạn phải tương thích với ANSI C++ mới có thể hỗ trợ được tính năng này.
Bài viết không tránh khỏi các lỗi hoặc nhầm lẫn bằng cách này hay cách khác, rất mong sự góp ý của các bạn để bài viết được hòan thiện hơn, xin cám ơn.
Bài viết sưu tầm...
Một người nào đó coi thường ý thức kỷ luật cũng có nghĩa là người đó đã coi thường tương lai số phận của chính bản thân người đó.Email: kevin[@]congdongcviet.com | CC to: info[@]congdongcviet.com
Phone: 0972 89 7667
Những người coi thường ý thức kỷ luật sẽ không bao giờ có được sự thành công trong sự nghiệp hoặc bị sự thiếu kỷ luật làm tiêu tan sự nghiệp.
Từ khóa throw
Ta có thể sử dụng từ khóa throw để ném bất kì giá trì nào để miêu tả 1 exception.
Từ khóa throw không nhất thiết phải nằm trong khối try. Ta có thể ném exception và để cho các caller xử lý exception.
Khi sử dụng throw để ném 1 exception, luồng thực thi tạm dừng và tìm khối try, nếu không có khối try hoặc khối try không có khối catch xử lý exception phù hợp, thì caller sẽ chịu trách nhiệm xử lý exception, nếu caller này không xử lý được, thì caller trên nữa sẽ xử lý, nếu exception không được xử lý, chương trình sẽ bị ngừng. Quá trình này được gọi là unwinding the stack.
Ví dụ .
C++ Code:
throw -1; // throw a literal integer value throw ENUM_INVALID_INDEX; // throw an enum value throw "Can not take square root of negative number"; // throw a literal char* string throw dX; // throw a double variable that was previously defined throw MyException("Fatal Error"); // Throw an object of class MyException
Ví dụ . Minh họa quá trình unwinding the stack.
C++ Code:
#include <iostream> using namespace std; void Last() // called by Third() { throw -1; // <-- luồng thực thi chương trình dừng ở đây } void Third() // called by Second() { Last(); // chả thấy try nào ở đây, ném lên tiếp } void Second() // called by First() { try { Third(); // ném lên đây } catch(double) // có 1 thằng catch xử lý kiểu double, nhưng đnag cần kiểu int, vậy ném lên tiếp { } } void First() // called by main() { try { Second(); // ném lên tới đây } catch (int) // có khối catch xử lý exception kiểu int { } catch (double) { } cout << "End First" << endl; // <-- luồng thực thi chương trình tiếp tục ở đây, nên in tiếp cái này } int main() { try { First(); } catch (int) { } return 0; }
Output :
Start main
Start First
Start Second
Start Third
Start Last
Last throwing int exception
First caught int exception
End First
End main
Giải thích :
Để ý rằng, exception được ném từ hàm Last(), luồng thực thi chương trình tạm dừng, hàm Last() không có khối try, nên việc xử lý exception được đẩy lên hàm gọi hàm Last(), là hàm Third(), hàm Third() cũng không có khối try, nên việc xử lý exception được đẩy lên hàm gọi hàm Third(), là hàm Second(), hàm Second() có khối try và khối catch xử lý exception kiểu double, nhưng để ý lại rằng, hàm Last() ném exception kiểu int ( throw -1; ), nên việc xử lý exception này tiếp tục được đẩy lên hàm gọi hàm Second(), là hàm First(), tại đây, có khối try và khối catch xử lý exception kiểu int, vậy nên exception được xử lý tại đây. Và luồng thực thi chương trình được tiếp thoajtaij vi trị đánh dấu như trên code minh họa.
Để ý tiếp, trong hàm main cũng có khối try và khối catch xử lý exception kiểu int, nhưng exception đã được xử lý tại hàm First(), vậy nên khối catch tại hàm main không cần xử lý nữa.
Khối catch
Mỗi khối catch phải chịu trách nhiệm xử lý 1 kiểu exception nào đó hoặc xử lý tất cả các exception.
Ví dụ .
C++ Code:
int main() { try { // Statements that may throw exceptions you want to handle now go here throw -1; } catch (int) // bắt exception kiểu int { // Any exceptions of type int thrown within the above try block get sent here } catch (double) // bắt exception kiểu double { // Any exceptions of type double thrown within the above try block get sent here } return 0; }
Output :
We caught an exception of type int
Bắt tất cả các exception :
C++ Code:
Ta nên sử dụng catch tất cả exception trong hàm main để tránh việc chương trình bị kết thúc và ta có cơ hội lưu dữ liệu, thông báo lỗi.
Exception và hàm thành viên
Ta nên sử dụng exception trong các hàm thành viên, hàm khởi tạo thay thế việc báo lỗi thông qua tham số truyền bởi tham chiếu hay giá trị return. Vì khi 1 exception được ném, việc tạo đối tượng sẽ bị hủy bỏ và hàm hủy sẽ không được gọi, khác với khi báo lỗi thông qua tham số truyền bởi tham chiếu hay qua giá trị return, đối tượng vẫn được khởi tạo 1 cách không như mong muốn.
Ta có thể sử dụng std::exception để miêu tả cụ thể lỗi gì đã xảy ra.
Đặc biệt nên sử dụng std::exception để bắt các exception khi sử dụng thư viện chuẩn. Bởi ta chưa chắc đã biết chính xác kiểu exception sẽ được ném khi sử dụng các thư viện chuẩn.
Ví dụ .
C++ Code:
try { // do some stuff with the standard library here } catch (std::exception &cException) { }
Exception và thừa kế
C++ xử lý exception theo tính đa hình trong thừa kế.
Ví dụ .
C++ Code:
Trong trường hợp này, ouput sẽ là :
caught Base
Vì C++ xử lý exception theo thứ tự của khối lệnh catch, khi gặp khối lệnh catch xử lý kiểu Base, vì Derived cũng là 1 Base, nên khối lệnh xử lý Base sẽ xử lý.
Để khối catch xử lý Derived xử lý exception này, ta chỉ việc đảo vị trí các khối catch :
C++ Code:
Một vài lưu ý khi sử dụng exception
Dọn dẹp tài nguyên
Ví dụ . Thao tác file.
C++ Code:
try { OpenFile(strFilename); WriteFile(strFilename, strData); CloseFile(strFilename); } catch (FileException &cException) { }
Trong ví dụ trên, nếu WriteFile() thất bại, hàm này ném 1 FileException, khi đó, chúng ta đã mở file rồi, và ở đây tả chỉ thông báo lỗi mà chưa đóng file lại.
Bởi vậy, ta nên :
C++ Code:
try { OpenFile(strFilename); WriteFile(strFilename, strData); CloseFile(strFilename); } catch (FileException &cException) { // Make sure file is closed CloseFile(strFilename); // Then write error }
Ví dụ . Cấp phát động.
C++ Code:
try { Person *pJohn = new Person("John", 18, E_MALE); ProcessPerson(pJohn); delete pJohn; } catch (PersonException &cException) { }
Ở ví dụ trên, nếu hàm ProcessPerson() ném PersonException, thì khối lệnh catch sẽ không có cách nào để giải phóng bộ nhớ mà pJohn trỏ tới, vì pJohn được khai báo cục bộ trong khối try.
Có 2 cách để giải quyết vấn đề này :
- Khai báo ngoài khối try.
C++ Code:
Person *pJohn = NULL; try { pJohn = new Person("John", 18, E_MALE); ProcessPerson(pJohn ); delete pJohn; } catch (PersonException &cException) { delete pJohn; }
- Sử dụng std::auto_ptr.
C++ Code:
#include <memory> // for std::auto_ptr try { pJohn = new Person("John", 18, E_MALE); auto_ptr<Person> pxJohn(pJohn); // pxJohn now owns pJohn ProcessPerson(pJohn); // when pxJohn goes out of scope, it will delete pJohn } catch (PersonException &cException) { }
std::auto_ptr sẽ tự động giải phóng bộ nhớ mà nó trỏ tới khi ra ngoài phạm vi.
Không nên sử dụng std::auto_ptr để trỏ tới 1 mảng, vì std::auto_ptr sử dụng delete để giải phóng bộ nhớ, nó không sử dụng delete[].
std::auto_ptr không có phiên bản nào để tự động giải phóng bộ nhớ của 1 mảng, ta nên sử dụng std::vector, nó sẽ tự giải phóng khi ra ngoài phạm vi.
Exception và hàm hủy
Không như hàm khởi tạo, không nên ném exception trong hàm hủy.
Nếu 1 exception được ném từ 1 hàm hủy trong quá trình unwinding thư stack, trình biên dịch không biết nên tiếp tục quá trình unwinding the stack hay nên xử lý exception mới. Kết quả là chương trình bị ngừng.
Nên ghi log thay vì ném exception trong hàm hủy.
Đã được chỉnh sửa lần cuối bởi luc13aka47 : 03-10-2012 lúc 01:16 PM.
Email: vu.xana@gmail.com
Blog: lêthanhvũ.blogspot.com