390 likes | 662 Views
תרגול מס ' 10. הורשה פולימורפיזם. הורשה. דוגמה תחביר יחס is-a החלפת פונקציות. דוגמה. מנשק גרפי מורכב מרכיבים שונים הקרויים Widgets לכל הרכיבים יש תכונות משותפות למשל רוחב, גובה, מיקום לרכיבים מסוימים יש תכונות ייחודיות פעולה המתבצעת לאחר לחיצה על Button
E N D
תרגול מס' 10 הורשה פולימורפיזם
הורשה דוגמה תחביר יחס 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(int i = 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