320 likes | 496 Views
Object Oriented Programming. Tirgul 10: Exceptions. למה צריך Exceptions ?. אחד העקרונות של OOP הוא חלוקת האחריות בין האובייקטים השונים, וכן בתוך אובייקט – בין הפונקציות השונות. החלוקה למחלקות תתבצע בד"כ ע"פ חלוקה הגיונית של "כלים" שאנחנו צריכים ע"מ לממש את התוכנית שלנו.
E N D
Object Oriented Programming Tirgul 10: Exceptions
למה צריך Exceptions? • אחד העקרונות של OOP הוא חלוקת האחריות בין האובייקטים השונים, וכן בתוך אובייקט – בין הפונקציות השונות. • החלוקה למחלקות תתבצע בד"כ ע"פ חלוקה הגיונית של "כלים" שאנחנו צריכים ע"מ לממש את התוכנית שלנו. • בתוך המחלקה, החלוקה לפונקציות תתבצע בד"כ לפי תחומי אחריות, כאשר לכל פונקציה יש את תחום האחריות שעליו היא אחראית (כמובן שבמקרה שניתן לחלק תחום אחריות ראשי אחד למספר תתי תחומים – ניתן לחלק את הפונקציה שלנו למספר פונקציות שיקראו ע"י הפונקציה בעלת תחום האחריות הראשי).
למה צריך Exceptions? • אבל מה קורה כאשר פונקציה אינה מסוגלת לבצע את הפעולה שהיא התבקשה לבצע? • נניח שפונקציה B כלשהי לא מסוגלת לבצע פעולה מסוימת שהתבקשה. תמיד תהיה פונקציה A שביקשה מ-B לבצע את אותה הפעולה (גם ל-main() יש את מערכת ההפעלה).נרצה שכאשר פונקציה B אינה מסוגלת לבצע את מה שהיא התבקשה לבצע – שתודיע על כך לפונקציה A שקראה לה.
למה צריך Exceptions? • הפונקציה A קראה לפונקציה B משום ש-A אחראית על פעולה מסוימת ש-B היא חלק ממנו.אם B אינה מסוגלת לבצע את תפקידה, אזי גם A לא מסוגלת לבצע את תפקידה. • נצפה מ-A לאחד מ-2 דברים: • לנסות לפעול בצורה שונה ע"מ לבצע את הנדרש ממנה. • להתריע לפונקציה שקראה ל-A על כך ש-A אינה מסוגלת לבצע את תפקידה.
למה צריך Exceptions? • פיתרון 1: שימוש בערך מוחזר. • חסרונות: • לא תמיד יש ערך מוחזר פנוי בשביל לציין טעות. • בעייתי לדווח את מהות הטעות. • ל-Ctor ו-Dtor אין ערכים מוחזרים. • מחייב לבדוק בכל פעם את הערך המוחזר ולהוסיף בכל מקום קוד שיפעל בהתאם. • מחייב לדעת בכל נקודה בקוד מה המשמעות של כל ערך שגיאה שחוזר.
למה צריך Exceptions? • מנגנון ה-Exceptions נותן לנו 2 יתרונות עיקריים: • התראה על תקלות בזמן הריצה. • מתן אפשרות לתוכנית להתאוששמהתקלות ולהמשיך לפעול בצורה תקינה.
מנגנון ה-Exceptions • מנגנון ה-Exceptions מורכב מ-3 חלקים: • throw – "זריקת" החריגה ע"י הפונקציה שאינה מסוגלת לבצע את שנדרשה. • try – בפונקציה הקוראת נסמן ב"ניסיון" את החלק שבו עלולה להיות פונקציה שלא תבצע את תפקידה. • catch – הפונקציה הקוראת "תופסת" את עצם החריגה ומטפלת בו בהתאם.
דוגמא 1 int main () } Fraction f1; cin>>f1; Fraction f2(1,1); Fraction f3 = f2/f1; cout<<"f3 is: "<<f3<<endl; } 1st run: input:1/1output: f3 is: 1/1 2nd run: input: 0/1 output: f3 is:1/1 • ניזכר באובייקט Fraction: Fraction::Fraction (int nn, int dd) : m_iNom (nn), m_iDenom (dd) { if (m_iDenom == 0) m_iDenom = 1; reduce (); } constFraction operator / (constFraction &f1, constFraction &f2) { int nn = f1.m_iNom * f2.m_iDenom; int dd = f1.m_iDenom * f2.m_iNom; returnFraction (nn, dd); }
דוגמא 1 constFraction operator / (constFraction &f1, constFraction &f2) { if (f2.m_iNom == 0) throw string("devision by 0"); int nn = f1.m_iNom * f2.m_iDenom; int dd = f1.m_iDenom * f2.m_iNom; returnFraction (nn, dd); } int main () { Fraction f1; cin>>f1; Fraction f2(1,1); try { Fraction f3 = f2/f1; cout<<"f3 is: "<<f3<<endl; } catch (string errorStr) { cerr<<"Error: "<<errorStr<<endl; { } • נרצה שחילוק ב-0 יתריע לנו על תקלה כלשהי: • כיצד ייראה ה-main?
דוגמא 2 //Product.h #ifndef PRODUCT_H #define PRODUCT_H #include <iostream> #include <string> usingnamespace std; classProduct { char m_name[10]; int m_quantity; float m_price; public: Product() {} void init (char* name, int quantity, float price); void print(); }; #endif //Product.cpp #include "Product.h“ voidProduct::init (char* name, int quantity, float price) { if (strlen(name) > 10) throw string("name’s too long"); strcpy(m_name ,name); if (quantity<0 || quantity>100) throw quantity; m_quantity=quantity; if (price<0) throw price; m_price=price; { voidProduct::print() { cout<<m_name<<'\t'<<m_quantity <<'\t'<<m_price<<endl; { • אם יש לנו מספר אפשרויות זריקה, נוכל לאפשר לתפוס אתכולם ע"ירצף של פקודות catch שיתפסו אובייקטים שונים:
int main () { char name[100]; int quantity; float price; Product store[3]; for (int i=0; i<3; i++) { try{ cout << "enter a product number " << i << endl; cin>>name>>quantity>>price; store[i].init(name,quantity,price); { catch (string errStr) { cout << "Error: “ << errStr << endl;; i--; { catch (float f) { cout << "Error: price can't be negative\n"; i--; { catch (int quantity) { if (quantity<0) cout << "Error: quantity must be zero or more\n"; if (quantity>100) cout << "Error: quantity can't be more then 100\n”; i--; { } for (int i=0; i<3; i++) store[i].print(); return 0; } דוגמא 2 enter a product number 0 abcd 10 12.99 enter a product number 1 aaabbbcccddd 10 13.00 Error: name to long enter a product number 1 abcde -12 12 Error: quantity must be 0 or more enter a product number 1 abcde 10 -1.99 Error: price can't be negative enter a product number 1 abcde 10 1.99 enter a product number 2 abcde 101 1.99 Error: quantity can't be more then 100 enter a product number 2 abcde 99 19.99 abcd 10 12.99 abcde 10 1.99 abcde 99 19.99
double Divide (double a, double b) { if (b == 0) throw "Divide by zero"; else return a/b; { void CallDivide() { try { cout<<"5/2="<<Divide(5,2)<<endl; cout<<"5/0="<<Divide(5,0)<<endl; cout<<"7/3="<<Divide(7,3)<<endl; { catch (const char* e) { cout<<"Exception caught at: "; cout<<"CallDivide function"<<endl; //try to fix the exception throw; //re-throw the exception to //the calling function. } { דוגמא 3 – Re-throw int main() { try { CallDivide(); } catch (const char* e) { cout<<"Exception caught at: "; cout<<"main function"<<endl; cout<<e<<endl; } catch( ... ) { cout<<"Unknown Exception!"<<endl; } cout<<"End program"<<endl; return 0; { //OUTPUT: 5/2=2.5 Exception caught at: CallDivide function Exception caught at: main function Divide by zero End program
void f2() { cout<<"f2(): Before throw"<<endl; throw string("some exception"); cout<<"f2(): After throw"<<endl; } void f1() { cout<<"f1(): Before throw"<<endl; f2(); cout<<"f1(): After throw"<<endl; } int main() { try { f1(); { catch (string errStr) { cerr<<"Exception: "<<errStr<<endl; { cout<<"main(): After catch block"<<endl; return 0; } דוגמא 4 • אם אין לנו בלוק catch שיתפוס אתהזריקה – אז היא נזרקת הלאה עד שתיתפס: //OUTPUT: f1(): Before throw f2(): Before throw Exception: some exception main(): After catch block
void f2() throw (string) { cout<<"f2(): Before throw"<<endl; throw string("some exception"); cout<<"f2(): After throw"<<endl; } void f1() { cout<<"f1(): Before throw"<<endl; f2(); cout<<"f1(): After throw"<<endl; } int main() { try { f1(); { catch (string errStr) { cerr<<"Exception: "<<errStr<<endl; { cout<<"main(): After catch block"<<endl; return 0; } הכרזה על חריגות throw (int) { • ניתן להצהיר בזמן ההצהרה עלהפונקציה על כך שהפונקציה שלנו זורקת חריגה. • הכרזות אלו נבדקות בזמן ריצה! • אם תיזרק חריגה שלא הצהרנועליה – התוכנית תסתיים עם הודעת שגיאה. (לא עובד ב-VS!) //OUTPUT: f1(): Before throw f2(): Before throw terminate called after throwing an instance of 'std::string' Abort (core dumped)
///////////////////////////////// // Exceptions Handling Classes ///////////////////////////////// classBaseException { public: virtualvoid print()=0; }; classderrivedException1 : publicBaseException { public: void print() {cout<<"derrivedException1\n";} }; classderrivedException2 : publicderrivedException1 { public: void print() {cout<<"derrivedException2\n";} }; classderrivedException3 : publicBaseException { public: void print() {cout<<"derrivedException3\n";} }; void f1(){ throw (derrivedException2()); }; היררכיית חריגות int main(){ try { f1(); { catch (BaseException ex) { ex.print(); { catch (derrivedException1 ex) { ex.print(); { catch (derrivedException2 ex) { ex.print(); { catch (derrivedException3 ex) { ex.print(); { return 0; { //OUTPUT: error: cannot allocate an object of abstract type 'BaseException‘ because the following virtual functions are pure within 'BaseException': virtual void BaseException::print()
///////////////////////////////// // Exceptions Handling Classes ///////////////////////////////// classBaseException { public: virtualvoid print()=0; }; classderrivedException1 : publicBaseException { public: void print() {cout<<"derrivedException1\n";} }; classderrivedException2 : publicderrivedException1 { public: void print() {cout<<"derrivedException2\n";} }; classderrivedException3 : publicBaseException { public: void print() {cout<<"derrivedException3\n";} }; void f1(){ throw (derrivedException2()); }; היררכיית חריגות int main(){ try { f1(); { catch (derrivedException1 ex) { ex.print(); { catch (derrivedException2 ex) { ex.print(); { catch (derrivedException3 ex) { ex.print(); { return 0; { //OUTPUT: derrivedException1 הקומפיילר נותן על כך אזהרה! שימו לב לא לקבל אזהרות כאלה בתרגיל!
///////////////////////////////// // Exceptions Handling Classes ///////////////////////////////// classBaseException { public: virtualvoid print()=0; }; classderrivedException1 : publicBaseException { public: void print() {cout<<"derrivedException1\n";} }; classderrivedException2 : publicderrivedException1 { public: void print() {cout<<"derrivedException2\n";} }; classderrivedException3 : publicBaseException { public: void print() {cout<<"derrivedException3\n";} }; void f1(){ throw (derrivedException2()); }; היררכיית חריגות int main(){ try { f1(); { catch (derrivedException2 ex) { ex.print(); { catch (derrivedException1 ex) { ex.print(); { catch (derrivedException3 ex) { ex.print(); { return 0; { //OUTPUT: derrivedException2
///////////////////////////////// // Exceptions Handling Classes ///////////////////////////////// classBaseException { public: virtualvoid print()=0; }; classderrivedException1 : publicBaseException { public: void print() {cout<<"derrivedException1\n";} }; classderrivedException2 : publicderrivedException1 { public: void print() {cout<<"derrivedException2\n";} }; classderrivedException3 : publicBaseException { public: void print() {cout<<"derrivedException3\n";} }; void f1(){ throw (derrivedException3()); }; היררכיית חריגות void f2(){ try { f1(); { catch (derrivedException1 d_ex1) { cout<<"catch f2"<<endl; throw (d_ex1); { { int main(){ try { f2(); { catch (BaseException& ex) { cout<<"catch main\n"; ex.print(); { return 0; { //OUTPUT: catch main derrivedException3
///////////////////////////////// // Exceptions Handling Classes ///////////////////////////////// classBaseException { public: virtualvoid print()=0; }; classderrivedException1 : publicBaseException { public: void print() {cout<<"derrivedException1\n";} }; classderrivedException2 : publicderrivedException1 { public: void print() {cout<<"derrivedException2\n";} }; classderrivedException3 : publicBaseException { public: void print() {cout<<"derrivedException3\n";} }; void f1(){ throw (derrivedException2()); }; היררכיית חריגות void f2(){ try { f1(); { catch (derrivedException1 d_ex1) { cout<<"catch f2"<<endl; throw (d_ex1); { { int main(){ try { f2(); { catch (BaseException& ex) { cout<<"catch main\n"; ex.print(); { return 0; { //OUTPUT: catch f2 catch main derrivedException1
היררכית חריגות • ישנה היררכית חריגות הקיימת במערכת עצמה. • האב של היררכיה זו הוא האובייקט exception.אובייקט זה מכיל פונקציה וירטואלית בשם what() המחזירה מחרוזת המתארת את החריגה. • רוב אובייקטי השגיאה נמצאים בקבצי הספרייה <stdexcept> ו-<exception>.
היררכיית חריגות - דוגמא //Fraction.cpp classDivideByZeroException : publicruntime_error { public: DivideByZeroException() : runtime_error( "attempted to divide by zero" ) {} }; constFractionoperator / (constFraction &f1, constFraction &f2) { if (f2.m_iNom == 0) throw (DivideByZeroException()); int nn = f1.m_iNom * f2.m_iDenom; int dd = f1.m_iDenom * f2.m_iNom; returnFraction (nn, dd); } istream& operator >> (istream &is, Fraction &frac) { char divSign; is >> frac.m_iNom >> divSign >> frac.m_iDenom; if (frac.m_iDenom == 0) throw (DivideByZeroException()); frac.reduce (); return is; } • נחזור שוב לאובייקט Fraction.ביצענו תיקון באופרטור החילוק,אך אופרטור הקלטעדייןמאפשר לקלוטמכנה 0... int main(){ Fraction f1; bool fracOK = false; while (!fracOK){ try { cin>>f1; fracOK=true; { catch (runtime_error& err) { cout<<err.what()<<endl; } { cout<<f1; return 0; } //INPUT:// OUTPUT: 1/0 attempted to divide by zero 2/0 attempted to divide by zero 2/2 1/1
היררכיית חריגות - דוגמא • ישנו עוד מקום אחד חשוב מאוד שעלינו להוסיף בו חריגה... • ה-Constractor! • נשים לב שמכיוון שבנאי אינו יכול להחזיר ערך – הדרך היחידה להודיע על אי הצלחה בבנייה של אובייקט היא ע"י זריקת חריגה. //Fraction.cpp classDivideByZeroException : publicruntime_error { public: DivideByZeroException() : runtime_error( "attempted to divide by zero" ) {} }; Fraction::Fraction (int nn, int dd) : m_iNom (nn), m_iDenom (dd) { if (m_iDenom == 0) throw (DivideByZeroException()); reduce (); }
מימוש חריגות ע"י המהדר • המהדר מממש את מנגנון החריגות ע"י פירוק מחסנית הקריאות לפונקציה כאשר נזרקת חריגה. • כאשר פונקציה זורקת חריגה מחסנית הקריאות נפרקת עד שמגיעים לפונקציה שיודעת לטפל בחריגה. • בזמן פריקת המחסנית משתחררים כל המשתנים המקומיים שהוגדרו ונקרא ה-Dtor שלהם.
מימוש חריגות ע"י המהדר– זריקה מ-Ctor classA{ public: A(){cout<<"Ctor A()"<<endl;} ~A(){cout<<"Dtor A()"<<endl;} }; classB : publicA{ public: B(){cout<<"Ctor B()"<<endl; throw 1;} ~B(){cout<<"Dtor B()"<<endl;} }; int main(){ try {B b;} catch (int i){}; return 0; { • כאשר אנחנו זורקים חריגהמה-Ctor של בן, יופעלוה-Dtor-ים של ההורים: • נשים לב שאם הייתה לנוזריקת חריגה ב-Dtor של A – אזי יהיו לנו 2 חריגות"באוויר" ולכן אסור לנו לזרוקחריגות מתוך ה-Dtor.אם נקרא מה-Dtor לפונקציה שעלולהלזרוק חריגה – עלינו להקיף אותהבבלוקי try, catch מתאימים ע"מ שלא תיזרק מאיתנו! • אבל מה קורה עם משתנים שהוקצו דינאמית? // OUTPUT: Ctor A() Ctor B() Dtor A()
#include <iostream> #include <stdexcept> usingnamespace std; void f2(){ string s2("local string"); char *p2 = new char[256]; //… throw runtime_error("some error"); delete[] p2; { void f1(){ string s1("local string"); string* ps = new string("dynamic string"); f2(); delete ps; { int main(){ try {f1();} catch (runtime_error err){ cerr<<"Ex: "<<err.what()<<endl; { return 0; } מימוש חריגות ע"י המהדר Heap string(“dynamic string”) string (“local string”) char* p2 f2(): string (“local string”) char* ps f1(): main(): כאשר נזרקת החריגה – אף אחד לא משחרר את המשתנים שהקצנו דינאמית.
מימוש חריגות ע"י המהדר • הפיתרון: STL + auto_ptr<T>. • ראשית, כאשר אנחנו צריכים container – נשתמש באובייקטי STL שונים. אובייקטים אלה יכולים להיות משתנים מקומיים של הפונקציה ולכן ישוחררו בזמן זריקת החריגה. • כעת, בזמן זריקת החריגה p2 הוא משתנה מקומי ולכן ישתחרר בזמן פירוק מחסנית הקריאות. void f2(){ string s2("local string"); char *p2 = new char[256];//... throw runtime_error("some error"); delete[] p2; { void f2(){ string s2("local string"); vector<char> p2(256); //... throw runtime_error("some error"); {
מימוש חריגות ע"י המהדר void f1(){ string s1("local string"); string* ps = new string("dynamic string"); f2(); delete ps; { void f1(){ string s1("local string"); auto_ptr<string>ps(new String(“dynamic string”); f2(); { • הפיתרון: STL + auto_ptr<T>. • כאשר נרצה להקצות משתנה יחיד נשתמש ב-auto_ptr. • הרעיון שעומד מאחורי auto_ptr הוא זה: • מצביע זה יהיה אובייקט של הפונקציה.לכן יופעל ה-Dtor שלו כאשר תיזרקחריגה והוא יפעיל את ה-Dtor שלהאובייקט שהוא מצביע עליו. template <class T> classauto_ptr { T* ptr; public: explicit auto_ptr(T* p = 0) : ptr(p) {} ~auto_ptr() {delete ptr;} T& operator*() {return *ptr;} T* operator->() {return ptr;} // ...};
מימוש חריגות ע"י המהדר template <class T>auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T>& rhs) { if (this != &rhs) { delete ptr; ptr = rhs.ptr; rhs.ptr = NULL; } return *this; } • auto_ptr הוא גרסא מאוד מוגבלת של "מצביע חכם". • המגבלה שלו מתבטאת בעיקר באופי ה"הרסני" של פעולות ההעתקה שלו. • שימו לב שפעולת ההעתקה "מחקה" את המצביע הקודם ועכשיו הוא מצביע על NULL. • דבר דומה קורה גם ב-C.Ctor! • לכן, כאשר אתם רוצים להעביר משתנה auto_ptr לפונקציה – זכרו לעשות זאת by ref! (אחרת תאבדו את המצביע המקורי!)
זריקת חריגה מה-Ctor • בעיות דומות קורות לנו גם כאשר נזרקים חריגות מה-Ctor.גם שם, בהחלט יכול להיות שנאתחל כל מיני משתנים בצורה דינאמית לצורך שימוש בהם, ובהחלט יכול להיות שאחת הפעולות ב-Ctor (או פונקציה כלשהי שנפעיל ממנו) תזרוק חריגה.גם במקרה זה – הקצאות דינאמיות שהוקצו עד לנקודה זו לא ישוחררו. • לכן, גם כאן נעדיף שימוש במצביעים "חכמים" וב-STL.
זריקת חריגה מרשימת אתחול #include <iostream> #include <stdexcept> #include <vector> using namespace std; class X{ vector <char> v1; vector <int> v2; public: X (long s1, int s2); }; X::X(long s1, int s2) try : v1(s1), v2(s2) //initialization list {} // constructor body catch (exception ex){ cerr << "Ex: “ << ex.what() << endl; } int main(){ X(99999999999,3); return 0; } • מה נעשה כאשר נזרקת לנו חריגה מתוך Ctor של אובייקט ברשימת האתחול? // OUTPUT: Ex: St9exception terminate called after throwing an instance of 'std::bad_alloc' what(): St9bad_alloc Abort (core dumped)
זריקת חריגה מאופרטור new #include <iostream> #include <stdexcept> #include <vector> using namespace std; void f(){ char* pc = new char[999999999999]; //... } int main(){ try{ f(); } catch (bad_alloc& ex){ cerr << "Ex: “ << ex.what() << endl; } return 0; } // OUTPUT: Ex: St9bad_alloc #include <iostream> #include <stdexcept> #include <vector> using namespace std; void f(){ char *pc = new (nothrow) char[9999999999999]; if (pc == NULL) cout << "pc2=NULL“ << endl; //... } int main(){ try{ f(); } catch (bad_alloc& ex){ cerr << "Ex: “ << ex.what() << endl; } return 0; } // OUTPUT: pc2=NULL
זריקת חריגה מאופרטור Ctor \ new? • שימו לב שכאשר אנחנו מקצים מערך של אובייקטים אנחנו יכולים לקבל 2 סוגי שגיאות... • שגיאת bad_alloc מהאופרטור new אם לא ניתן להקצות את כמות העצמים שרצינו. • שגיאת runtime_error מכל אחד מהעצמים שהגדרנו. • במקרה כזה – נצרך להוסיף בלוקי catch מתאימים עבור סוגי השגיאה השונים. • שימו לב כמה חשוב לזרוק שגיאה מסוג התואם בצורה אמיתית את סוג השגיאה שקרתה (!) אחרת לא נדע מהם סוגי השגיאות שנצטרך להתמודד איתם.