Download presentation
Presentation is loading. Please wait.
1
תרגול מס' 11 הורשה פולימורפיזם
2
דוגמה תחביר יחס is-a החלפת פונקציות
הורשה דוגמה תחביר יחס is-a החלפת פונקציות מבוא לתכנות מערכות
3
דוגמה מנשק גרפי מורכב מרכיבים שונים הקרויים Widgets
Entry מנשק גרפי מורכב מרכיבים שונים הקרויים Widgets לכל הרכיבים יש תכונות משותפות למשל רוחב, גובה, מיקום לרכיבים מסוימים יש תכונות ייחודיות פעולה המתבצעת לאחר לחיצה על Button קריאת הערך השמור ב-Entry עצם מסוג חלון יצטרך לשמור את כל הרכיבים הנמצאים בו Radio button Check button Button מבוא לתכנות מערכות
4
דוגמה אם ננסה לממש מחלקות עבור Widgets ניתקל במספר בעיות:
אין דרך נוחה להחזיק את כל ה-Widgets בתוך Window איך היינו פותרים את הבעיה ב-C? מה החסרונות של פתרון זה? class Button { int x, y; int width, height; string text; //... public: int getWidth(); int getHeight(); void onClick(); }; class Entry { int x, y; int width, height; string text; //... public: int getWidth(); int getHeight(); string getValue(); }; class Window { Array<Button> buttons; Array<Entry> entries; // ... for every type // ... }; ב-C ניתן לנסות את הפתרון הבא: ניצור ADT עבור Widget וכל אחד מרכיבים הספציפיים יחזיק שדה ל-Widget את הרשימה של כל ה-Widgets נוכל לשמור בעזרת void* החסרונות של הפתרון יהיו: צריך "לתפור" את הפונקציונליות של Widget אל המנשק של כל אחד מה-ADT שניצור השימוש ב-void* רגיש לבאגים, צריך לדעת איך להוציא כל Widget מהרשימה בצורה בטוחה בלי להתבלבל אפשר לשכלל את הפתרון של C בכל מיני דרכים (Widget שמחזיק שדה פנימי מטיפוס void* עבור ההרחבה) אבל החסרונות עדיין נשארים - כתיבת קוד ידני והמרות בזמן ריצה. מבוא לתכנות מערכות
5
הורשה class Widget { int x, y, width, height; string text; // ... public: int getWidth(); int getHeight(); // ... }; ניתן להשתמש בהורשה כדי לציין שמחלקה מסוימת יורשת את התכונות של מחלקת בסיס (מחלקת אב) כלשהי ניצור מחלקה עבור Widget אשר תייצג את התכונות המשותפות לכל ה-Widgets כל Widget יירש את התכונות המשותפות ויוסיף את התכונות הייחודיות לו מונחים שונים לתיאור הורשה - אם B יורשת מ-A אומרים: B inherits A B extends A B is derived from A B is a subclass of A A is a superclass of B A is a base class of B שאלות שצצות: האם יש הורשה שאינה public? כן, אבל היא פחות שימושית ומחוץ לחומר הקורס. האם ניתן לרשת ממספר מחלקות? כן, אך זה יעיל לשימושים מתקדמים יותר וגם מחוץ לחומר הקורס. class Button : public Widget { // ... public: void onClick(); // ... }; class Entry : public Widget { // ... public: string getValue(); // ... }; מבוא לתכנות מערכות
6
הורשה - תחביר class A { public: int n; int f() { return n; } }; class B : public A { public: int m; int g() { return m + n; } }; int main() { B b; b.n = 5; b.m = 4; cout << b.f() << endl; cout << b.g() << endl; return 0; } כדי לציין שמחלקה B יורשת את מחלקה A נוסיף אחרי שם המחלקה B “: public A” לעצם מטיפוס B יהיו את כל השדות והמתודות המוגדרים ב-A בנוסף לאלו המוגדרים בו נהוג לסמן הורשה בתרשימים על ידי חץ היוצא מהמחלקה היורשת אל מחלקת האב, למשל: A B Button Entry Widget מבוא לתכנות מערכות
7
בקרת גישה בהורשה class A { int n; protected: int f() { return n; } public: void set(int i) { n = i; } }; class B : public A { public: int g() { return f(); } }; int main() { B b; b.n = 5; // error b.set(5); // o.k. b.f(); // error cout << b.g() << endl; return 0; } מתודות ושדות פרטיים של מחלקת האב אינם נגישים ממחלקות יורשות ניתן להגדיר שדות ומתודות כ-protected, מתודות ושדות אלו יהיו: נגישים ממחלקות יורשות אינם נגישים משאר הקוד בעזרת protected ניתן להגדיר מנשק נפרד לקוד המשתמש במחלקה על ידי הורשה מהמנשק לקוד חיצוני כמו תמיד, נעדיף להסתיר את מימוש המחלקה: נעדיף protected על public נעדיף private על protected מבוא לתכנות מערכות
8
בקרת גישה בהורשה A private members protected members public members B
class A { // ... }; class B : public A { class C : public B { A private members protected members public members B private members protected members public members User code C private members protected members public members מבוא לתכנות מערכות
9
הורשה - is a הורשה מממשת יחס “is a” בין המחלקה היורשת למחלקת האב
class A { int n; public: int f() { return n; } void set(int i) { n = i; } }; class B : public A { int g() { return f() + 1; } }; void h(A& a) { a.f(); } int main() { B b; 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 מונחים מקובלים לתיאור היחס is-a: B is an A B is a type of A B is a subtype of A B הוא סוג של A מבוא לתכנות מערכות
10
הקומפיילר יוסיף קריאה ל-A()
הורשה - בנאים והורסים class A { int n; public: A(int n) : n(n) {} A() : n(0) {} }; class B : public A { int m; B(int n) : A(n), m(n) {} B() : m(1) {} בנאים והורסים אינם עוברים בירושה אתחול ושחרור מחלקת הבסיס מתבצע בדומה לאתחול ושחרור שדות ברשימת האתחול של B נקרא הבנאי של A מחלקת הבסיס מאותחלת לפני השדות אם לא מופיעה קריאה מפורשת לבנאי של A ייקרא A() אם לא מוגדר בנאי A() תתקבל שגיאת קומפילציה B אינו מאתחל שדות של A, אתחולם מתבצע על ידי A לאחר הריסת כל השדות של של B ייקרא ~A() בנאי ההעתקה ואופרטור= הנוצרים על ידי הקומפיילר קוראים להעתקה/השמה של מחלקת האב הקומפיילר יוסיף קריאה ל-A() מבוא לתכנות מערכות
11
הורשה - זמני קריאה מה מדפיס הקוד הבא? class X { int n; public:
X(int n) : n(n) { cout << "X::X():" << n << endl; } ~X() { cout << "X::~X():" << n << endl; } }; class A { X x1; A(int n) : x1(n) { cout << "A::A()" << endl; } ~A() { cout << "A::~A()" << endl; } }; מה מדפיס הקוד הבא? class B : public A { X x2; public: B(int m, int n) : A(m), x2(n) { cout << "B::B()" << endl; } ~B() { cout << "B::~B()" << endl; }; int main() { B b(1, 2); cout << "=========" << endl; return 0; X::X():1 A::A() X::X() 2 B::B() ========= B::~B() X::~X():2 A::~A() X::~X():1 מבוא לתכנות מערכות
12
דוגמה - MultiStack ברצוננו ליצור את המחלקה MultiStack אשר תומכת בכל הפעולות של מחסנית רגילה (push, pop, top) ובפעולה נוספת popk אשר מוציאה את k האיברים האחרונים שהוכנסו למחסנית קיימות שלוש דרכים לפתרון הבעיה: כתיבת MultiStack מחדש שכפול קוד ועבודה מיותרת שימוש ב-Stack כשדה של MultiStack נצטרך "לתפור" ידנית את המתודות MultiStack תירש את Stack קוד אשר עובד עם Stack יוכל לעבוד גם עם MultiStack class Stack { int* data; int size; int nextIndex; public: Stack(int size = 100); Stack(const Stack& stack); ~Stack(); Stack& operator=(const Stack& s); void push(const int& n); void pop(); int& top(); const int& top() const; int getSize() const; class Full {}; class Empty {}; }; מבוא לתכנות מערכות
13
MultiStack עלינו לממש ב-MultiStack רק את הבנאי, popk וחריגה חדשה
class MultiStack : public Stack { public: MultiStack(int size); void popk(int k); class NotEnoughNumbers {}; }; MultiStack::MultiStack(int size) : Stack(size) {} void MultiStack::popk(int k) { if (getSize() < k) { throw NotEnoughNumbers(); } for(int i = 0; i < k; ++i ) { pop(); עלינו לממש ב-MultiStack רק את הבנאי, popk וחריגה חדשה האם התשובה משתנה אם השדה nextIndex היה מוגדר כ-protected? השימוש ב-protected יכול לעזור ליורשים אך מוסיף תלויות במימוש בדרך כלל נעדיף להימנע מחשיפת המימוש למחלקות יורשות void MultiStack::popk(int k) { if (nextIndex < k) { throw NotEnoughNumbers(); } nextIndex -= k; } מבוא לתכנות מערכות
14
הגדרה מחדש של מתודות class Point { int x, y; public: Point(int x, int y); void print() const; }; void Point::print() const { cout << x << "," << y << endl; } מחלקות יורשות יכולות להגדיר מחדש גם פונקציות קיימות פעולה זו קרויה overriding במקרה זה הפונקציה של מחלקת האב מוסתרת על ידי הפונקציה החדשה כדי לקרוא לפונקציה הישנה מציינים את ה-namespace של מחלקת האב לפני הקריאה לפונקציה קריאה של הפונקציה המוגדרת מחדש לפונקציה אותה היא מסתירה שכיחה מאחר ובד"כ הפונקציה החדשה מוסיפה על ההתנהגות של הפונקציה הקודמת כדי לקרוא לאופרטור שמגידירם מחדש יש להשתמש בתחביר של קריאה לפונקציה רגילה ולא בתחביר האופרטור, למשל A::operator+(...) בד"כ כתיבה מחדש של פונקציות מתבצעת רק עבור פונקציות וירטואליות. העמסת פונקציה המקבלת פרמטרים אחרים מזאת אשר במחלקת האב תסתיר את הפונקציה של מחלקת האב. מומלץ לא לערבב overriding ו-overloading. התוצאות קשות לצפייה ומזמינות באגים. אפשר להגדיר שדות מחדש, אך זה מתכון לבאגים ואין לזה את המשמעות שיש להגדרה מחדש פונקציות (כמו שנראה בנושא הבא) class LabeledPoint : public Point { string label; public: LabeledPoint(string s, int x, int y); void print() const; }; void LabeledPoint::print() const { cout << label << ": "; Point::print(); } void f() { LabeledPoint p("origin", 0, 0); p.print(); // origin: 0,0 } מבוא לתכנות מערכות
15
הורשה סיכום מחלקה B יכולה לרשת את התכונות של מחלקה A על ידי שימוש בהורשה אם B יורשת מ-A היא מקבלת את כל השדות והמתודות שלה לא ניתן לגשת מ-B לחלקים פרטיים של A ניתן להגדיר שדות ומתודות כ-protected כדי לאפשר למחלקה יורשת לגשת אליהם אם B יורשת מ-A אז B is an A וניתן להשתמש ב-B& כ-A& וב-B* כ-A* אתחול ושחרור מחלקת הבסיס מתבצע כמו אתחול ושחרור של שדה מומלץ להסתיר את מימוש המחלקה ככל הניתן, גם ממחלקות יורשות ניתן להגדיר מחדש מתודות של מחלקת האב במחלקת הבן מבוא לתכנות מערכות
16
פונקציות וירטואליות מחלקות מופשטות (Abstract classes) חריגות והורשה
פולימורפיזם פונקציות וירטואליות מחלקות מופשטות (Abstract classes) חריגות והורשה מבוא לתכנות מערכות
17
חיה: משקל, גובה, ת. לידה, הדפס סוגי מזון, השמע קול
פולימורפיזם פולימורפיזם (רב-צורתיות) מאפשרת יצירת קוד יחיד המתאים לטיפוסים שונים אם B נראה כמו A ומתנהג כמו A אז הוא יוכל להחליף את A ניתן לכתוב קוד פולימורפי עבור טיפוס A אשר יעבוד לכל טיפוס B אשר יורש מ-A נשתמש בהורשה כדי לייצג חיות שונות בעזרת מחלקות כך שנוכל לכתוב קוד פולימורפי עבור כל סוגי החיות חיה: משקל, גובה, ת. לידה, הדפס סוגי מזון, השמע קול פולימורפיזם במדעי המחשב מוזכר בדרך כלל בהקשר של פולימורפיזם של תכנות מונחה עצמים הקרוי גם פולימורפיזם על ידי הורשה או על ידי subtyping. קיימים ארבעה סוגים של פולימורפיזם בקוד, כולם כבר ידועים לנו: על ידי העמסה על ידי המרות על ידי תבניות על ידי הורשה שני הסוגים הראשונים פחות מועילים מאחר והם דורשים התעסקות פרטנית לכל טיפוס. מבוא לתכנות מערכות
18
פולימורפיזם ניצור את המחלקה Animal ואת המחלקה Dog אשר תירש ממנה
class Animal { int age, weight; public: Animal(int age, int weight); int getAge() const; void makeSound() const { cout << endl; } // no sound by default; }; class Dog: public Animal { Dog(int age, int weight); cout << "vuf vuf" << endl; } }; ניצור את המחלקה Animal ואת המחלקה Dog אשר תירש ממנה מה ידפיס הקוד הבא? בעיה: הקומפיילר בוחר איזו פונקציה תיקרא בזמן הקומפילציה (קישור סטטי) הפונקציה שאנו רוצים שתרוץ תלויה בטיפוס בזמן הריצה Dog* d = new Dog(3,4); Animal* a = d; a->makeSound(); d->makeSound(); מקושרות סטטית - הכתובת אליה קופצים לתחילת ביצוע הפונקציה נקבעת בזמן הקומפילציה המונחים סטטי ודינאמי בהקשר של תכנות מתייחסים לזמן הקומפילציה לעומת זמן הריצה: סטטי - זמן הקומפילציה דינאמי - זמן ריצת התכנית Animal::makeSound Dog::makeSound מבוא לתכנות מערכות
19
פונקציות וירטואליות ניתן להכריז על פונקציה כוירטואלית במחלקת האב:
class Animal { int age, weight; public: Animal(int age, int weight); int getAge() const; virtual void makeSound() const { cout << endl; } // no sound by default; }; class Dog: public Animal { Dog(int age, int weight); void makeSound() const { cout << "vuf vuf" << endl; } }; ניתן להכריז על פונקציה כוירטואלית במחלקת האב: במקרה זה הקומפיילר ייצור קוד אשר יבחר את הפונקציה המתאימה לטיפוס בזמן הריצה (קישור דינאמי) כעת בקריאה a->makeSound() תתבצע הפונקציה המתאימה אם פונקציה מוכרזת כוירטואלית אז היא וירטואלית בכל המחלקות היורשות הקומפיילר פעיל רק בזמן הקומפילציה, כדי לאפשר קישור דינמי הקומפיילר כותב קוד "מסובך" יותר כך שבזמן הריצה תתבצע קפיצה לפונקציה הנכונה בהתאם לטיפוס הדינמי של המשתנה. מבוא לתכנות מערכות
20
אם מתכננים להשתמש בפולימורפיזם חייבים ליצור הורס וירטואלי
פונקציות וירטואליות class Dog: public Animal { public: Dog(int age, int weight); void makeSound() const { cout << "vuf vuf" << endl; } }; class Cat: public Animal { Cat(int age, int weight); cout << "miao" << endl; }; class Fish: public Animal { Fish(int age, int weight); }; // the default makeSound is OK void foo() { 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]; } vuf vuf miao אם ההורס אינו מוגדר כוירטואלי אז הקומפיילר אמנם ייקרא להורס אוטומטית עם קריאת delete אבל ל-Animal::~Animal(). שגיאה בהרצת ההורס תגרום לקוד לא מוגדר, בפרט היא בטוח תגרום להורס של החיה הספציפית לא להיקרא ולחלק מהקוד לא להתבצע. אם ההורס מוגדר כוירטואלי הקומפיילר ייצור קוד אשר יבחר את ההורס המתאים בזמן הריצה. מה ייקרא כאן? class Animal { int age, weight; public: Animal(int age, int weight); virtual ~Animal() {} // ... }; אם מתכננים להשתמש בפולימורפיזם חייבים ליצור הורס וירטואלי מבוא לתכנות מערכות
21
מחלקות אבסטרקטיות במקרים רבים מחלקת האב אינה טיפוס שלם בפני עצמה
ניתן להגדיר פונקציה כ-pure virtual על ידי הוספת “= 0” בסוף הכרזתה פונקציה וירטואלית טהורה אינה ממומשת במחלקת האב מחלקה המכילה פונקציה וירטואלית טהורה נקראת מחלקה אבסטרקטית לא ניתן ליצור עצם מטיפוס המחלקה חייבים ליצור עצם מטיפוס היורש ממנה ניתן ליצור מצביעים ורפרנסים למחלקות אבסטקרטיות למעשה שימוש בפונקציה וירטואלית משמעותי רק עבור מצביעים או רפרנסים class Shape { int center_x, center_y; public: Shape(int x, int y) : center_x(x), center_y(y) {} virtual ~Shape() {} virtual double area() const = 0; }; שימו לב להבדל בין הכזרהעל פונקציה (; בסוף שם הפונקציה) לבין מימוש ריק של הפונקציה ( {} בסוף שם הפונקציה) אם תכריזו על ההורס הוירטואלי ותשימו ; לא יימצא לו מימוש בשלב הקישור ותתקבל שגיאה. ברצוננו לקבל הורס ריק ולכן אנחנו מממשים אותו בתוך המחלקה כריק {} - חסר קוד מאחר והוא הוגדר כוירטואלי הקומפיילר ידע לקרוא להורס הנכון, וההורס יהיה וירטואלי לכל המחלקות היורשות. לכן יהיה בטוח לחלוטין לקרוא ל-delete על מצביע ל-Shape. ניתן לתת מימוש גם לפונקציה שהיא pure virtual, כדי לתת איזה מימוש משותף למבקרה שמחלקות הבנים לא יממשו את הפונקציה הנ"ל (מוש אופייני יכלול זריקת חריגה) area היא פונקציה וירטואלית טהורה Shape היא מחלקה אבסטרקטית, לא ניתן ליצור עצם מטיפוס Shape מבוא לתכנות מערכות
22
מחלקות אבסטרקטיות class Shape { int center_x, center_y; public:
Shape(int x, int y) : center_x(x), center_y(y) {} virtual ~Shape() {} virtual double area() const = 0; }; class Circle : public Shape { int radius; public: Circle(int x, int y, int radius) : Shape(x,y), radius(radius) {} virtual double area() const { return radius*radius*PI; } }; class Square : public Shape { int edge; public: Square(int x, int y, int edge) : Shape(x,y), edge(edge) {} virtual double area() const { return edge*edge; } }; void foo() { Shape* shapes[N]; // an array of squares & circles // initialization ... double totalArea=0; for (int i = 0; i < N; ++i) { totalArea += shapes[i]->area(); } cout << totalArea << endl; } מבוא לתכנות מערכות
23
הוספת Widget חדש אינה דורשת שינויים ב-Window
שליחת הודעות לעצמים class Widget { //... virtual void redraw(); }; class Window { Array<Widget*> children; public: void redraw(); void Window::redraw() { for(int i=0;i<children.size();++i) { children[i]->redraw(); } פונקציות וירטואליות מפרידות את ההודעה הנשלחת לעצם מהפעולה שמתבצעת בפועל ההודעה area גורמת לחישוב שונה לכל צורה במקום לשאול עצם מיהו ולהגיד לו מה לעשות בהתאם - פשוט שולחים לו את ההודעה והיא תפורש בהתאם לזהותו קוד אשר משתמש בפולימורפיזם מחליף שימוש ב-if ו-switch בקריאות לפונקציות וירטואליות קור קצר יותר ונקי יותר קל להוסיף מקרים חדשים הוספת Widget חדש אינה דורשת שינויים ב-Window מבוא לתכנות מערכות
24
פולימורפיזם והעתקת עצמים
פולימורפיזם והעברה by-value (או העתקת עצמים בכלל) אינם משתלבים: עצם המוחזק “by value” (כלומר לא כרפרנס או מצביע) לא יכול להיות בעל טיפוס שונה בזמן הריצה שימוש ב-copy c’tor יוצר עצם מהטיפוס המוגדר לפי שמו לכן העתקת Animal יוצרת Animal חדש בהתבסס על חלק ה-Animal של a לא ניתן ליצור העתק Shape של s כי Shape אבסטרקטית כאשר משתמשים בהורשה עושים זאת עם רפרנסים ומצביעים הדבר נכון גם להעברת והחזרת ערכים void foo(Animal& a, Shape& s) { Animal copy = Animal(a); copy.makeSound(); a.makeSound(); Shape copy2 = Shape(s); // error } מבוא לתכנות מערכות
25
what היא פונקציה וירטואלית המוגדרת ב-std::exception
חריגות ופולימורפיזם class Stack { // ... class Exception : public std::exception {}; class Full : public Exception {}; class Empty : public Exception {}; }; השימוש בפולימורפיזם מאפשר הגדרת היררכיה של חריגות ניתן לתפוס חריגות לפי מחלקת האב שלהן וכך לאפשר תפיסה של חריגות ספציפיות או חריגות מקבוצה כללית יותר חשוב לתפוס חריגות עם &, למה? הספריה הסטנדרטית מגדירה מספר חריגות שלכולן אב משותף - std::exception מומלץ שחריגות יירשו את exception (בעקיפין או ישירות) void f() { 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; } תפיסה ללא & היא תפיסה by value - יווצר העתק מהטיפוס המדויק הרשום בעזרת copy c’tor כתוצאה מכך לא יישמר הטיפוס האמיתי של החריגה. שימו לב שאם מעתיקים עצם בעזרת c’tor נוצר עצם מטיפוס המוגדר על ידי ה-c’tor. what היא פונקציה וירטואלית המוגדרת ב-std::exception מבוא לתכנות מערכות
26
חריגות ופולימורפיזם class AException {}; class BException : public AException {}; class CException : אם מספר כללי catch מתאימים לתפיסת חריגה ייבחר הכלל אשר מופיע ראשון לכן מתחילים מהמקרה הספציפי ביותר עד לכללי ביותר (לכן שימוש ב-“...” תמיד יופיע אחרון) מה יודפס בכל אחת מהפונקציות? void f() { try { throw CException(); } catch (BException& b) { cout << "B caught"; } catch (CException& c) { cout << "C caught"; } catch (AException& a) { cout << "A caught"; } void g() { try { throw CException(); } catch (BException& b) { cout << "B caught"; } catch (AException& a) { cout << "A caught"; } catch (CException& c) { cout << "C caught"; } void h() { try { throw AException(); } catch (BException& b) { cout << "B caught"; } catch (CException& c) { cout << "C caught"; } catch (AException& a) { cout << "A caught"; } מבוא לתכנות מערכות
27
שימוש נכון בהורשה e יכול להיות Circle!
כאשר יוצרים מחלקה B היורשת את A חשוב להקפיד: בכל מקום שבו ניתן להשתמש ב-A יהיה ניתן להשתמש ב-B נניח שנרצה להוסיף מחלקה עבור אליפסה האם עיגול הוא סוג של אליפסה? לא, בגלל המתודה setAB עיגול לא יכול לעשות את כל מה שאליפסה עושה המחלקה היורשת צריכה להוסיף על התנהגות מחלקת האב לא להסתיר לא לשנות במקרה שלנו Ellipse ו-Circle צריכות לרשת ישירות את Shape class Ellipse : public Shape { int a,b; public: Ellipse(int x, int y, int a, int b); virtual double area() const; void setAB(int a, int b); }; class Circle : public Ellipse { public: Circle(int x, int y, int radius) : Ellipse(x,y,r,r) {} }; void break_stuff(Ellipse& e) { e.setAB(1,2); } e יכול להיות Circle! מבוא לתכנות מערכות
28
פולימורפיזם - סיכום כדי לאפשר התנהגות פולימורפית למספר מחלקות היורשות ממחלקת אב משותפת משתמשים בפונקציות וירטואליות כאשר מריצים פונקציה וירטואלית נבחרת הפונקציה המתאימה בזמן ריצה ניתן להשתמש בפולימורפיזם כדי להעלים if ו-switch מהקוד ולהחליף בקוד פשוט יותר וקל יותר להרחבה ניתן להגדיר מחלקות אבסטרקטיות שחלק מהפונקציות שלהן הן וירטואליות טהורות פונקציות וירטואליות דורשות שימוש במצביעים או רפרנסים נהוג להגדיר את כל החריגות בהיררכיה של הורשות כדי לאפשר תפיסה של קבוצת חריגות כללית בקלות מבוא לתכנות מערכות
29
המרות ו-typeid המרות ב-C++ typeid מבוא לתכנות מערכות
30
(Type)var או Type(var);
המרות ב-++C ניתן ב-C++ להמיר עם תחביר של C או תחביר של בנאי: (Type)var או Type(var); שתי המרות אלו שקולות להמרות אלו חסרונות רבים: המרות מסוג זה הן ערבוב של משמעויות: המרה בין מצביעים עם קשר הורשה ביניהם מוגדרת, אך המרה בין מצביעים שאין קשר ביניהם אינה מוגדרת ניתן בטעות להמיר עצם קבוע (const) ללא קבוע - התוצאה יכולה להיות לא מוגדרת קשה לדעת איזו המרה התבצעה - המרות בין מצביעים מתקמפלות תמיד, אך משמעותן עלולה להשתנות בגלל שינויים ללא התראות מהקומפיילר קשה לזהות את ההמרות בקוד או לחפש אותן לשם כך מוגדרות ב-C++ המרות מיוחדות הפותרות את בעיות אלו מבוא לתכנות מערכות
31
המרות ב-C++ קיימים ארבעה סוגי המרות:
static_cast: ממירה בין שני עצמים בעזרת בנאי או בין מצביעים/רפרנסים שיש ביניהם קשר של הורשה נכונות ההמרה נבדקת בזמן הקומפילציה בהמרה ממחלקת אב למחלקת בן המשתמש אחראי על הנכונות dynamic_cast: ממירה בין מצביעים/רפרנסים נכונות ההמרה נבדקת בזמן ריצה ומוחזרת שגיאה במקרה של כשלון const_cast: מסירה const מהטיפוס reinterpret_cast: ממירה בין של שני טיפוסים על ידי פירוש מחדש של הביטים משמשת למשחקי ביטים ודיבוג מבוא לתכנות מערכות
32
המרות - static_cast void f(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); } המרה סטטית מאפשרת המרה בין עצמים בעזרת המרה מוגדרת (ע"י בנאי או אופרטור המרה) במקרה שאין המרה מוגדרת תתקבל שגיאת קומפילציה (בדומה להמרה רגילה) המרה סטטית מאפשרת המרת מצביעים ורפרנסים בין טיפוסים בעלי קשר הורשה אם אין קשר בין הטיפוסים בהמרה סטטית תתקבל שגיאת קומפילציה בהמרה סטטית ממצביע של מחלקת האב למחלקת הבן שאינה נכונה תתקבל התנהגות לא מוגדרת מבוא לתכנות מערכות
33
המרות - dynamic_cast המרה דינאמית משמשת להמרה בין מצביעים ורפרנסים תוך כדי בדיקה בזמן ריצה של נכונות ההמרה בהמרה דינאמית של מצביעים יוחזר NULL אם ההמרה אינה אפשרית בהמרה דינאמית של רפרנסים תיזרק std::bad_cast אם ההמרה אינה אפשרית המרה דינאמית עובדת רק על טיפוסים פולימורפיים (בעלי פונקציה וירטואלית אחת לפחות) משתמשים בהמרה דינאמית כדי להמיר בבטחה ממחלקת האב למחלקה יורשת void g(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) { double* ptr2 = dynamic_cast<double*>(&n); A& unrelated = dynamic_cast<A&>(c); } מבוא לתכנות מערכות
34
typeid #include <typeinfo> //... class Base { virtual void v() {} }; class Derived : public Base {}; void f() { 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 שימוש בשאלה "איזה טיפוס אתה?" ובחירה בקוד לפיו מחזירה את החסרונות של קוד ללא פולימורפיזם עבור gcc יודפס: P4Base 4Base 7Derived diff typeid מחושב בזמן קומפילציה עבור חלק מהביטויים (למשל שם טיפוס) ודורש חישוב בזמן ריצה עבור רפרנסים לטיפוסים פולימורפיים. בשביל לעבוד נכון עבור זיהוי רפרנס של מחלקה יורשת על הטיפוס להיות פולימורפי (כמו עבור dynamic_cast) מבוא לתכנות מערכות
35
המרות ו-typeid - סיכום המנעו משימוש בהמרות ו-typeid - הם גורמים לקוד שקל להכניס בו באגים וקשה לשנות אותו השתמשו בפונקציות וירטואליות כדי לבצע את מה שדרוש במקום לבדוק איזה טיפוס יש לעצם אם חייבים לבצע המרה השתמשו ב-static_cast אם חייבים להמיר ממחלקת אב לבן השתמשו ב-dynamic_cast המנעו משימוש ב-typeid או מימוש מנגנון דומה בעצמכם המנעו מהמרות בסגנון של C מבוא לתכנות מערכות
36
מימוש יחס “is a” מימוש פונקציות וירטואליות
העשרה - מימוש הורשה מימוש יחס “is a” מימוש פונקציות וירטואליות מבוא לתכנות מערכות
37
העשרה - מימוש הורשה class A { int a; int b; public: int f(); int g(); }; class B : public A { int c; int d; public: int h(); }; כיצד יכול הקומפיילר לממש את ההתנהגות של “B is an A” ? מבנה העצם בזיכרון ייראה כך: תחילה השדות של מחלקת האב אחריהם השדות הנוספים של מחלקת הבן מבנה זה מאפשר לקוד המקבל את כתובת תחילת העצם להתייחס אליו כאל עצם ממחלקת האב כל השדות עבור עצם של מחלקת האב נמצאים במקומם עצם מסוג A A a b מי שמסתכל על תחילת העצם רואה A בכל מקרה מי שמסתכל על תחילת העצם רואה A בכל מקרה עצם מסוג B A 𝚫B a b c d מבוא לתכנות מערכות
38
העשרה - פונקציות וירטואליות
class A { int a; int b; public: virtual int f(); virtual int g(); }; class B : public A { int c; int d; public: virtual int f(); }; כדי לאפשר קריאה לפונקציה הנכונה בזמן ריצה הקומפיילר משתמש במצביעים לפונקציות לכל מחלקה נשמרת טבלה (הקרויה vtbl) עם מצביעים עבור הפונקציות הוירטואליות המתאימות ביותר לטיפוס התא הראשון של כל עצם יכיל מצביע (הקרוי vptr) אשר יצביע לטבלה המתאימה ביותר לעצם עבור קריאה לפונקציה וירטואלית הקומפיילר ייצור קוד דומה לזה: A::f A::g vtbl for A עצם מסוג A A vptr a b B::f A::g vtbl for B עצם מסוג B A 𝚫B void call_f(A* const this) { this->vptr[0](this); } vptr a b c d להמחשה בלבד מבוא לתכנות מערכות
Similar presentations
© 2025 SlidePlayer.com. Inc.
All rights reserved.