380 likes | 527 Views
תרגול מס' 11. הורשה פולימורפיזם. הורשה. דוגמה תחביר יחס is-a החלפת פונקציות. דוגמה. מנשק גרפי מורכב מרכיבים שונים הקרויים Widgets לכל הרכיבים יש תכונות משותפות למשל רוחב, גובה, מיקום לרכיבים מסוימים יש תכונות ייחודיות פעולה המתבצעת לאחר לחיצה על Button קריאת הערך השמור ב- Entry
E N D
תרגול מס' 11 הורשה פולימורפיזם
הורשה דוגמה תחביר יחס is-a החלפת פונקציות מבוא לתכנות מערכות - 234122
דוגמה • מנשק גרפי מורכב מרכיבים שונים הקרויים Widgets • לכל הרכיבים יש תכונות משותפות • למשל רוחב, גובה, מיקום • לרכיבים מסוימים יש תכונות ייחודיות • פעולה המתבצעת לאחר לחיצה על Button • קריאת הערך השמור ב-Entry • עצם מסוג חלון יצטרך לשמור את כל הרכיבים הנמצאים בו Entry Radio button Check button Button מבוא לתכנות מערכות - 234122
דוגמה • אם ננסה לממש מחלקות עבור Widgets ניתקל במספר בעיות: • שכפול קוד בין ה-Widgets השונים • אין דרך נוחה להחזיק את כל ה-Widgets בתוך Window • איך היינו פותרים את הבעיה ב-C? מה החסרונות של פתרון זה? classButton { intx, y; intwidth, height; stringtext; //... public: intgetWidth(); intgetHeight(); voidonClick(); //... }; classEntry { intx, y; intwidth, height; stringtext; //... public: intgetWidth(); intgetHeight(); stringgetValue(); //... }; classWindow { Array<Button> buttons; Array<Entry> entries; // ... for every type // ... }; מבוא לתכנות מערכות - 234122
הורשה • ניתן להשתמש בהורשהכדי לציין שמחלקה מסוימת יורשת את התכונות של מחלקת בסיס (מחלקת אב) כלשהי • ניצור מחלקה עבור Widget אשר תייצג את התכונות המשותפות לכל ה-Widgets • כל Widget יירש את התכונות המשותפות ויוסיף את התכונות הייחודיות לו classWidget { intx, y, width, height; stringtext; // ... public: intgetWidth(); intgetHeight(); // ...}; classButton : publicWidget { // ... public: voidonClick(); // ...}; classEntry : publicWidget { // ... public: stringgetValue(); // ...}; מבוא לתכנות מערכות - 234122
הורשה - תחביר classA { public: intn; intf() { returnn; }};classB : publicA { public: intm; intg() { returnm + n; }};intmain() { Bb; b.n = 5; b.m = 4; cout << b.f() << endl; cout << b.g() << endl; return 0;} • כדי לציין שמחלקה B יורשת את מחלקה Aנוסיף אחרי שם המחלקה B“: public A” • לעצם מטיפוס B יהיו את כל השדות והמתודות המוגדרים ב-A בנוסף לאלו המוגדרים בו • נהוג לסמן הורשה בתרשימים על ידי חץ היוצא מהמחלקה היורשת אל מחלקת האב, למשל: A Widget B Button Entry מבוא לתכנות מערכות - 234122
בקרת גישה בהורשה classA { intn; protected: intf() { returnn; } public: voidset(int i) { n = i; }};classB : publicA { public: intg() { return f(); }};intmain() { Bb; b.n = 5; // error b.set(5); // o.k. b.f(); // error cout << b.g() << endl; return 0;} • מתודות ושדות פרטיים של מחלקת האב אינם נגישים ממחלקות יורשות • ניתן להגדיר שדות ומתודות כ-protected, מתודות ושדות אלו יהיו: • נגישים ממחלקות יורשות • אינם נגישים משאר הקוד • בעזרת protected ניתן להגדיר מנשק נפרד לקוד המשתמש במחלקה על ידי הורשה מהמנשק לקוד חיצוני • כמו תמיד, נעדיף להסתיר את מימוש המחלקה: • נעדיף protected על public • נעדיף private על protected מבוא לתכנות מערכות - 234122
בקרת גישה בהורשה classA { // ... }; classB : publicA { // ... }; classC : publicB { // ... }; A private members protected members public members B private members protected members public members C private members protected members public members User code מבוא לתכנות מערכות - 234122
הורשה - is a classA { intn; public: intf() { returnn; } voidset(int i) { n = i; } };classB : publicA { public: intg() { returnf() + 1; } };voidh(A& a) { a.f(); }intmain() { Bb; A& a = b; // o.k. A* ptra = new B; // o.k. h(b); return 0;} • הורשה מממשת יחס “is a”בין המחלקה היורשת למחלקת האב • “a button is a widget” - כפתור הוא רכיב גרפי • אם B יורש מ-A נוכל להשתמש בעצם מטיפוס B בכל מקום בו ניתן להשתמש בעצם מטיפוס A • B&יוכל לשמש כ-A& • כתובת של עצם מסוג Bיכולה לשמש ככתובת של עצם מסוג A • בעזרת הורשה נוכל להתייחס לעצמים מטיפוסים שונים דרך החלק המשותף להם • Array<Widget*> יחיד יאחסן את כל ה-widgets B הוא סוג של A מבוא לתכנות מערכות - 234122
הורשה - בנאים והורסים classA { intn; public: A(int n) : n(n) {} A() : n(0) {} }; classB : publicA { intm; public: B(int n) : A(n), m(n) {} B() : m(1) {} }; • בנאים והורסים אינם עוברים בירושה • אתחול ושחרור מחלקת הבסיס מתבצע בדומה לאתחול ושחרור שדות • ברשימת האתחול של B נקרא הבנאי של A • מחלקת הבסיס מאותחלת לפני השדות • אם לא מופיעה קריאה מפורשת לבנאי של A ייקרא A() • אם לא מוגדר בנאי A() תתקבל שגיאת קומפילציה • B אינו מאתחל שדות של A, אתחולם מתבצע על ידי A • לאחר הריסת כל השדות של של B ייקרא ~A() • בנאי ההעתקה ואופרטור= הנוצרים על ידי הקומפיילר קוראים להעתקה/השמה של מחלקת האב הקומפיילר יוסיף קריאה ל-A() מבוא לתכנות מערכות - 234122
הורשה - זמני קריאה classX{ intn; public: X(int n) : n(n) { cout<< "X::X():" << n << endl; } ~X() { cout<< "X::~X():" << n << endl;}};classA{ Xx1; public: A(int n) : x1(n) { cout<< "A::A()" << endl;} ~A() { cout<< "A::~A()" << endl; }}; • מה מדפיס הקוד הבא? classB : publicA { Xx2; public: B(int m, int n) : A(m), x2(n) { cout<< "B::B()" << endl; } ~B() { cout<< "B::~B()" << endl; } }; intmain() { B b(1, 2); cout << "=========" << endl; return 0; } מבוא לתכנות מערכות - 234122
דוגמה - MultiStack • ברצוננו ליצור את המחלקה MultiStackאשר תומכת בכל הפעולות של מחסנית רגילה (push, pop, top) ובפעולה נוספת popkאשר מוציאה את k האיברים האחרונים שהוכנסו למחסנית • קיימות שלוש דרכים לפתרון הבעיה: • כתיבת MultiStack מחדש • שכפול קוד ועבודה מיותרת • שימוש ב-Stack כשדה של MultiStack • נצטרך "לתפור" ידנית את המתודות • MultiStack תירש את Stack • קוד אשר עובד עם Stack יוכל לעבודגם עםMultiStack classStack{int* data; intsize; intnextIndex;public:Stack(int size = 100); Stack(constStack& stack); ~Stack(); Stack& operator=(constStack& s);voidpush(constint& n); voidpop(); int& top(); constint& top()const; intgetSize()const;classFull{};classEmpty{};}; מבוא לתכנות מערכות - 234122
MultiStack classMultiStack : publicStack { public: MultiStack(int size); voidpopk(int k); classNotEnoughNumbers {}; }; MultiStack::MultiStack(int size) : Stack(size) {} voidMultiStack::popk(int k) { if (getSize() < k) { throwNotEnoughNumbers(); } for(inti = 0; i < k; ++i ) { pop(); } } • עלינו לממש ב-MultiStack רק את הבנאי, popk וחריגה חדשה • האם התשובה משתנה אם השדה nextIndexהיה מוגדר כ-protected? • השימוש ב-protected יכול לעזור ליורשים אך מוסיף תלויות במימוש • בדרך כלל נעדיף להימנע מחשיפת המימוש למחלקות יורשות voidMultiStack::popk(int k) { if (nextIndex < k) { throwNotEnoughNumbers();}nextIndex-= k;} מבוא לתכנות מערכות - 234122
הגדרה מחדש של מתודות classPoint{ intx, y; public:Point(int x, int y); voidprint() const;};voidPoint::print() const { cout << x << "," << y << endl;} • מחלקות יורשות יכולות להגדיר מחדש גם פונקציות קיימות • פעולה זו קרויה overriding • במקרה זה הפונקציה של מחלקת האב מוסתרת על ידי הפונקציה החדשה • כדי לקרוא לפונקציה הישנה מציינים את ה-namespace של מחלקת האב לפני הקריאה לפונקציה classLabeledPoint : publicPoint { stringlabel; public:LabeledPoint(string s, int x, inty); voidprint() const;};voidLabeledPoint::print() const { cout << label << ": "; Point::print();} voidf() { LabeledPoint p("origin", 0, 0); p.print(); // origin: 0,0} מבוא לתכנות מערכות - 234122
הורשה סיכום • מחלקה B יכולה לרשת את התכונות של מחלקה A על ידי שימוש בהורשה • אם B יורשת מ-A היא מקבלת את כל השדות והמתודות שלה • לא ניתן לגשת מ-B לחלקים פרטיים של A • ניתן להגדיר שדות ומתודות כ-protected כדי לאפשר למחלקה יורשת לגשת אליהם • אם B יורשת מ-A אז B is an A וניתן להשתמש ב-B& כ-A& וב-B* כ-A* • אתחול ושחרור מחלקת הבסיס מתבצע כמו אתחול ושחרור של שדה • מומלץ להסתיר את מימוש המחלקה ככל הניתן, גם ממחלקות יורשות • ניתן להגדיר מחדש מתודות של מחלקת האב במחלקת הבן מבוא לתכנות מערכות - 234122
פולימורפיזם פונקציות וירטואליות מחלקות מופשטות (Abstract classes) חריגות והורשה מבוא לתכנות מערכות - 234122
פולימורפיזם • פולימורפיזם(רב-צורתיות) מאפשרת יצירת קוד יחיד המתאים לטיפוסים שונים • אם B נראה כמו Aומתנהג כמו Aאז הוא יוכל להחליף את A • ניתן לכתוב קוד פולימורפיעבור טיפוס A אשר יעבודלכל טיפוס B אשר יורש מ-A • נשתמש בהורשה כדי לייצג חיות שונות בעזרת מחלקות כך שנוכל לכתוב קוד פולימורפי עבור כל סוגי החיות חיה:משקל, גובה, ת. לידה, הדפס סוגי מזון, השמע קול מבוא לתכנות מערכות - 234122
פולימורפיזם classAnimal { intage, weight; public: Animal(int age, int weight); intgetAge() const; voidmakeSound() const { cout << endl; } // no sound by default; };classDog: publicAnimal { public: Dog(int age, int weight); voidmakeSound() const { cout << "vufvuf" << endl; } }; • ניצור את המחלקה Animal ואת המחלקה Dog אשר תירש ממנה • מה ידפיס הקוד הבא? • בעיה: • הקומפיילר בוחר איזו פונקציה תיקרא בזמן הקומפילציה (קישור סטטי) • הפונקציה שאנו רוצים שתרוץ תלויה בטיפוס בזמן הריצה Dog* d = new Dog(3,4); Animal* a = d; a->makeSound(); d->makeSound(); Animal::makeSound Dog::makeSound מבוא לתכנות מערכות - 234122
פונקציות וירטואליות classAnimal { intage, weight; public: Animal(int age, int weight); intgetAge() const; virtual voidmakeSound() const { cout << endl; } // no sound by default; };classDog: publicAnimal { public: Dog(int age, int weight); voidmakeSound() const { cout << "vufvuf" << endl; } }; • ניתן להכריז על פונקציה כוירטואלית במחלקת האב: • במקרה זה הקומפיילר ייצור קוד אשר יבחר את הפונקציה המתאימה לטיפוס בזמן הריצה (קישור דינאמי) • כעת בקריאה a->makeSound()תתבצע הפונקציה המתאימה • אם פונקציה מוכרזת כוירטואלית אז היא וירטואלית בכל המחלקות היורשות מבוא לתכנות מערכות - 234122
פונקציות וירטואליות classDog: publicAnimal { public: Dog(int age, int weight); voidmakeSound() const { cout << "vufvuf" << endl; } };classCat: publicAnimal { public: Cat(int age, int weight); voidmakeSound() const { cout << "miao" << endl; } };classFish: publicAnimal { public: Fish(int age, int weight); }; // the default makeSound is OK voidfoo() { Animal* animals[3]; animals[0] = new Dog(3,4); animals[1] = new Fish(1,1); animals[2] = new Cat(2,2); for (int i = 0; i < 3; ++i) { animals[i]->makeSound(); delete animals[i]; } } vufvuf miao מה ייקרא כאן? classAnimal { intage, weight; public: Animal(int age, int weight); virtual~Animal() {} // ... }; אם מתכננים להשתמש בפולימורפיזם חייביםליצור הורס וירטואלי מבוא לתכנות מערכות - 234122
מחלקות אבסטרקטיות • במקרים רבים מחלקת האב אינה טיפוס שלם בפני עצמה • ניתן להגדיר פונקציה כ-pure virtualעל ידי הוספת “= 0”בסוף הכרזתה • פונקציה וירטואלית טהורה אינה ממומשת במחלקת האב • מחלקה המכילה פונקציה וירטואליתטהורה נקראת מחלקה אבסטרקטית • לא ניתן ליצור עצם מטיפוס המחלקה • חייבים ליצור עצם מטיפוס היורש ממנה • ניתן ליצור מצביעים ורפרנסיםלמחלקות אבסטקרטיות • למעשה שימוש בפונקציה וירטואלית משמעותי רק עבורמצביעים או רפרנסים classShape { intcenter_x, center_y; public: Shape(int x, int y) : center_x(x), center_y(y) {} virtual~Shape() {} virtualdoublearea() const = 0; }; area היא פונקציה וירטואלית טהורה Shape היא מחלקה אבסטרקטית, לא ניתן ליצור עצם מטיפוס Shape מבוא לתכנות מערכות - 234122
מחלקות אבסטרקטיות classShape{intcenter_x, center_y; public: Shape(int x, int y) : center_x(x), center_y(y) {} virtual~Shape() {} virtualdoublearea() const = 0;}; classCircle : publicShape {intradius; public: Circle(int x, int y, int radius) : Shape(x,y), radius(radius) {} virtualdoublearea() const { returnradius*radius*PI;}}; classSquare : publicShape {intedge; public: Square(int x, int y, int edge) : Shape(x,y), edge(edge) {} virtualdoublearea() const { returnedge*edge;}}; void foo() {Shape* shapes[N]; // an array of squares & circles// initialization ... doubletotalArea=0; for (int i = 0; i < N; ++i) { totalArea += shapes[i]->area(); }cout << totalArea << endl;} מבוא לתכנות מערכות - 234122
שליחת הודעות לעצמים classWidget { //... virtualvoidredraw(); }; classWindow { Array<Widget*> children; //... public: voidredraw(); }; voidWindow::redraw() { for(inti=0;i<children.size();++i) { children[i]->redraw(); } } • פונקציות וירטואליות מפרידותאת ההודעההנשלחת לעצם מהפעולהשמתבצעת בפועל • ההודעה area גורמת לחישוב שונה לכל צורה • במקום לשאול עצם מיהו ולהגיד לו מה לעשות בהתאם - פשוט שולחים לו את ההודעה והיא תפורש בהתאם לזהותו • קוד אשר משתמש בפולימורפיזם מחליף שימוש ב-if ו-switch בקריאות לפונקציות וירטואליות • קור קצר יותר ונקי יותר • קל להוסיף מקרים חדשים הוספת Widget חדש אינה דורשת שינויים ב-Window מבוא לתכנות מערכות - 234122
פולימורפיזם והעתקת עצמים • פולימורפיזם והעברה by-value (או העתקת עצמים בכלל) אינם משתלבים: • עצם המוחזק “by value” (כלומר לאכרפרנס או מצביע) לא יכול להיות בעלטיפוס שונה בזמן הריצה • שימוש ב-copy c’tor יוצר עצם מהטיפוס המוגדר לפי שמו • לכן העתקת Animal יוצרת Animal חדש בהתבסס על חלק ה-Animal של a • לא ניתן ליצור העתק Shape של s כי Shapeאבסטרקטית • כאשר משתמשים בהורשה עושים זאת עם רפרנסים ומצביעים • הדבר נכון גם להעברת והחזרת ערכים voidfoo(Animal& a, Shape& s) { Animal copy = Animal(a); copy.makeSound(); a.makeSound();Shape copy2 = Shape(s); // error } מבוא לתכנות מערכות - 234122
חריגות ופולימורפיזם class Stack {// ...classException : publicstd::exception {}; classFull : publicException {}; classEmpty : publicException {}; }; • השימוש בפולימורפיזםמאפשר הגדרת היררכיהשל חריגות • ניתן לתפוס חריגות לפי מחלקתהאב שלהן וכך לאפשר תפיסהשל חריגות ספציפיות או חריגות מקבוצהכללית יותר • חשוב לתפוס חריגות עם &, למה? • הספריה הסטנדרטית מגדירה מספר חריגותשלכולן אב משותף - std::exception • מומלץ שחריגות יירשו את exception(בעקיפין או ישירות) voidf() { try { Stack s(100); do_stuff(s); } catch (Stack::Full& e) { cerr << "Not enough room"; } catch (Stack::Exception& e) { cerr << "Error with stack"; } catch (std::exception& e) { cerr << e.what() << endl; } } what היא פונקציה וירטואלית המוגדרת ב-std::exception מבוא לתכנות מערכות - 234122
חריגות ופולימורפיזם classAException {}; classBException: publicAException {}; classCException: publicAException{}; • אם מספר כללי catch מתאימים לתפיסת חריגה ייבחר הכלל אשר מופיע ראשון • לכן מתחילים מהמקרה הספציפי ביותרעד לכללי ביותר (לכן שימוש ב-“...” תמיד יופיע אחרון) • מה יודפס בכל אחת מהפונקציות? voidh() { try { throwAException(); } catch (BException& b) { cout << "B caught"; } catch (CException& c) { cout << "C caught"; } catch (AException& a) { cout << "A caught"; } } voidf() { try { throwCException(); } catch (BException& b) { cout << "B caught"; } catch (CException& c) { cout << "C caught"; } catch (AException& a) { cout << "A caught"; } } voidg() { try { throwCException(); } catch (BException& b) { cout << "B caught"; } catch (AException& a) { cout << "A caught"; } catch (CException& c) { cout << "C caught"; } } מבוא לתכנות מערכות - 234122
שימוש נכון בהורשה • כאשר יוצרים מחלקה B היורשת את A חשוב להקפיד:בכל מקום שבו ניתן להשתמש ב-A יהיה ניתן להשתמש ב-B • נניח שנרצה להוסיף מחלקה עבוראליפסה • האם עיגול הוא סוג של אליפסה? • לא, בגלל המתודה setAB עיגול לא יכוללעשות את כל מה שאליפסה עושה • המחלקה היורשת צריכה להוסיףעלהתנהגות מחלקת האב • לא להסתיר • לא לשנות • במקרה שלנו Ellipse ו-Circle צריכות לרשתישירות את Shape classEllipse : publicShape {inta,b;public: Ellipse(int x, int y, int a, int b); virtualdoublearea() const; voidsetAB(int a, int b);};classCircle : publicEllipse{public:Circle(int x, int y, int radius) : Ellipse(x,y,r,r) {}}; voidbreak_stuff(Ellipse& e) { e.setAB(1,2);} e יכול להיות Circle! מבוא לתכנות מערכות - 234122
פולימורפיזם - סיכום • כדי לאפשר התנהגות פולימורפית למספר מחלקות היורשות ממחלקת אב משותפת משתמשים בפונקציות וירטואליות • כאשר מריצים פונקציה וירטואלית נבחרת הפונקציה המתאימה בזמן ריצה • ניתן להשתמש בפולימורפיזם כדי להעלים if ו-switch מהקוד ולהחליף בקוד פשוט יותר וקל יותר להרחבה • ניתן להגדיר מחלקות אבסטרקטיות שחלק בעלות פונקציות וירטואליות טהורות • פונקציות וירטואליות דורשות שימוש במצביעים או רפרנסים • נהוג להגדיר את כל החריגות בהיררכיה של הורשות כדי לאפשר תפיסה של קבוצת חריגות כללית בקלות מבוא לתכנות מערכות - 234122
המרות ו-typeid המרות ב-C++ typeid מבוא לתכנות מערכות - 234122
המרות ב-++C • ניתן ב-C++ להמיר עם תחביר של C או תחביר של בנאי: (Type)varאוType(var); • שתי המרות אלו שקולות • להמרות אלו חסרונותרבים: • המרות מסוג זה הן ערבוב של משמעויות: • המרה בין מצביעים עם קשר הורשה ביניהם מוגדרת, אך המרה בין מצביעים שאין קשר ביניהם אינה מוגדרת • ניתן בטעות להמיר עצם קבוע (const) ללא קבוע - התוצאה יכולה להיות לא מוגדרת • קשה לדעת איזו המרה התבצעה- המרות בין מצביעים מתקמפלות תמיד, אך משמעותן עלולה להשתנות בגלל שינויים ללא התראות מהקומפיילר • קשה לזהות את ההמרות בקוד או לחפשאותן • לשם כך מוגדרות ב-C++ המרות מיוחדות הפותרות את בעיות אלו מבוא לתכנות מערכות - 234122
המרות • ב-C++ קיימים ארבעה סוגי המרות: • static_cast: ממירה בין שני עצמים בעזרת בנאי או בין מצביעים/רפרנסים שיש ביניהם קשר של הורשה • נכונות ההמרה נבדקת בזמן הקומפילציה • בהמרה ממחלקת אב למחלקת בן המשתמש אחראי על הנכונות • dynamic_cast: ממירה בין מצביעים/רפרנסים • נכונות ההמרה נבדקת בזמן ריצה ומוחזרת שגיאה במקרה של כשלון • const_cast: מסירה const מהטיפוס • reinterpret_cast: ממירה בין של שני טיפוסים על ידי פירוש מחדש של הביטים • משמשת למשחקי ביטים ודיבוג מבוא לתכנות מערכות - 234122
המרות - static_cast voidf(int& n, A& base) { double d = static_cast<double>(n); B& derived = static_cast<B&>(base); B* ptr = static_cast<B*>(&base); double* ptr2 = static_cast<double*>(&n); C& unrelated = static_cast<C&>(base);} • המרה סטטית מאפשרת המרה ביןעצמים בעזרת המרה מוגדרת(ע"י בנאי או אופרטור המרה) • במקרה שאין המרה מוגדרת תתקבלשגיאת קומפילציה (בדומה להמרהרגילה) • המרה סטטית מאפשרת המרת מצביעים ורפרנסים בין טיפוסים בעלי קשר הורשה • אם אין קשר בין הטיפוסים בהמרה סטטית תתקבל שגיאת קומפילציה • בהמרה סטטית ממצביע של מחלקת האב למחלקת הבן שאינה נכונה תתקבל התנהגות לא מוגדרת מבוא לתכנות מערכות - 234122
המרות - dynamic_cast • המרה דינאמית משמשת להמרה בין מצביעים ורפרנסים תוך כדי בדיקה בזמן ריצה של נכונות ההמרה • בהמרה דינאמית של מצביעיםיוחזר NULL אם ההמרה אינה אפשרית • בהמרה דינאמית של רפרנסיםתיזרק std::bad_cast אם ההמרה אינה אפשרית • המרה דינאמית עובדת רק על טיפוסים פולימורפיים (בעלי פונקציה וירטואלית אחת לפחות) • משתמשים בהמרה דינאמית כדי להמיר בבטחה ממחלקת האב למחלקה יורשת voidg(int n, A& base, C& c) { B* ptr = dynamic_cast<B*>(&base); if (ptr == NULL) { cerr << "base is not a B"; } try{ B& derived = dynamic_cast<B&>(base); } catch (std::bad_cast& e) { cerr << "base is not a B"; } double* ptr2 = dynamic_cast<double*>(&n); A& unrelated = dynamic_cast<A&>(c);} מבוא לתכנות מערכות - 234122
typeid #include<typeinfo> //... classBase { virtualvoidv() {} }; classDerived : publicBase{};voidf() { Base* ptrb = new Base; Base* ptrd= new Derived; cout << typeid(ptrb).name() << endl; cout << typeid(ptrd).name() << endl; cout << typeid(*ptrb).name() << endl; cout << typeid(*ptrd).name() << endl; if (typeid(*ptrd) != typeid(Base)) { cout << "diff"; } return 0; } • האופרטור typeidמקבל שם טיפוס או ערך ומחזיר עצם מטיפוס type_info המתאר את שם הטיפוס • ניתן להשוות type_info • ניתן לקבל מחרוזת המתארת את שם טיפוס (טוב למטרות דיבוג) • מומלץ להימנע משימוש ב-typeid • שימוש בשאלה "איזה טיפוס אתה?" ובחירה בקוד לפיו מחזירה את החסרונות של קוד ללא פולימורפיזם מבוא לתכנות מערכות - 234122
המרות ו-typeid - סיכום • המנעומשימוש בהמרות ו-typeid - הם גורמים לקוד שקל להכניס בו באגים וקשה לשנות אותו • השתמשו בפונקציות וירטואליות כדי לבצע את מה שדרוש במקום לבדוק איזה טיפוס יש לעצם • אם חייבים לבצע המרה השתמשו ב-static_cast • אם חייבים להמיר ממחלקת אב לבן השתמשו ב-dynamic_cast • המנעומשימוש ב-typeid או מימוש מנגנון דומה בעצמכם • המנעומהמרות בסגנון של C מבוא לתכנות מערכות - 234122
העשרה - מימוש הורשה מימוש יחס “is a” מימוש פונקציות וירטואליות מבוא לתכנות מערכות - 234122
העשרה - מימוש הורשה class A { inta; intb; public: intf(); intg(); }; classB : publicA { intc; intd; public: inth(); }; • כיצד יכול הקומפיילר לממש את ההתנהגות של “B is an A” ? • מבנה העצם בזיכרון ייראה כך: • תחילה השדות של מחלקת האב • אחריהם השדות הנוספים של מחלקת הבן • מבנה זה מאפשר לקוד המקבל את כתובת תחילת העצם להתייחס אליו כאל עצם ממחלקת האב • כל השדות עבור עצם של מחלקת האב נמצאים במקומם עצם מסוג A A a b מי שמסתכל על תחילת העצם רואה A בכל מקרה מי שמסתכל על תחילת העצם רואה A בכל מקרה עצם מסוג B A B a b c d מבוא לתכנות מערכות - 234122
העשרה - פונקציות וירטואליות class A { inta; intb; public: virtualintf(); virtualintg();}; classB : publicA { intc; intd; public: virtualintf(); }; • כדי לאפשר קריאה לפונקציה הנכונה בזמן ריצה הקומפיילר משתמש במצביעים לפונקציות • לכל מחלקה נשמרת טבלה (הקרויה vtbl) עם מצביעים עבור הפונקציות הוירטואליות המתאימות ביותר לטיפוס • התא הראשון של כל עצם יכיל מצביע (הקרוי vptr) אשר יצביע לטבלה המתאימה ביותר לעצם • עבור קריאה לפונקציה וירטואלית הקומפיילר ייצור קוד דומה לזה: vtbl for B B::f vtbl for A עצם מסוג A A::g A::f A A::g vptr a b עצם מסוג B A B voidcall_f(A* constthis) { this->vptr[0](this); } vptr a b c d להמחשה בלבד מבוא לתכנות מערכות - 234122