English (United Kingdom) Hebrew

קורס:"++C"

שיעור 11: ירושה מרובה,  RTTI וממשקים

[ <<< הקודם ] [ תוכן עניינים ] [ הבא >>> ]



מתוך הספר: ++C - מדריך מקצועי       ++C - מדריך מקצועי
תוכן עניינים


מבוא

ירושה מרובה (Multiple Inheritance) הינה הרחבה של מנגנון הירושה: ניתן לרשת ממספר מחלקות, ובכך לרשת את כלל הממשקים והפונקציונליות שלהן.

באופן עקרוני, ירושה מרובה אינה שונה בהרבה מירושה יחידנית (Single Inheritance): מחלקה יכולה לרשת ממספר מחלקות את מכלול הגדרות הטיפוסים, הקבועים, המשתנים והפונקציות.

כמו כן קיים יחס פולימורפי בין כל אחת ממחלקות הבסיס למחלקה הנגזרת - קיימת התאמת מצביעים וכן אפשרות לדרוס פונקציות וירטואליות.

הבעיה הקשה בירושה מרובה היא מצבי קונפליקט: פונקציות בעלות שם זהה במספר מחלקות בסיס, איתחול, מחלקת בסיס מרובה וכו'.

נהוג להפריד בין שני סוגי ירושה עקרוניים:

  - ירושת מימוש: מחלקה היורשת ממחלקה אחרת את הגדרות המשתנים והפונקציות שלה.
  - ירושת ממשק: מחלקה היורשת ממשק - כלומר ממחלקה מופשטת שכל הפונקציות שלה וירטואליות טהורות.

כפי שנראה, ירושת מימוש מרובה היא נושא בעייתי מאוד בשפתC++  עקב הקונפליקטים הנוצרים לעיתים תכופות בירושת תכונות ופונקציות ממספר מחלקות.

לעומת זאת ירושת ממשק מרובה אינה גורמת לקונפליקטים והיא שימושית בהרבה מערכות מונחות עצמים.

ירושה מרובה

נחזור לתכנית הדוגמא לניהול כח אדם מהפרק הקודם. תרשים המחלקות:

בעיה: המחלקה Lecturer מחזיקה וקטור של סטודנטים המועסקים כאסיסטנטים בשירות המוסד האקדמי. אולם סטודנטים אלו אמורים לקבל גם שכר.

פתרון: נוסיף מחלקה בשם Assistant המייצגת סטודנט העובד במוסד האקדמי, כלומר מקבל שכר. כיצד נייצג סטודנט זה? ניתן להשתמש בירושה מרובה להגדרת המחלקה החדשה היורשת הן מ- Student והן מ- Employee:

הסבר: המחלקה Assistant יורשת הן מ- Employee והן מ- Student ולכן כוללת את התכונות של שתיהן. כמו כן היא גם יורשת את הפונקציות המוגדרות בשתיהן כגון: regCourse(), setSalary().

המחלקה Assistant יורשת גם פונקציות בעלות חתימה זהה משתי מחלקות הבסיס :

 
 Student::print(),                 Employee::print()
 Student::calc_credit(),      Employee::calc_credit() 
Student::calc_credit(),       Employee::calc_credit()
 

כמו כן המחלקה Assistant יורשת את ההגדרות של ה"סבא" שלה - Person : ת.ז., שם , כתובת, פונקצית הדפסה Person::print(), חישוב זיכויים Person::calc_credit() וחישוב חיובים Person::calc_debit().

ב- C++ מבצעים ירושה מרובה ע"י רשימה של מספר מחלקות בסיס. המחלקה Assistant מוגדרת כך:

 
class Assistant : public Student, public Employee
{
          ...
};
 

נתחיל בגירסה פשוטה של ירושה מרובה של Assistant:

 
class Assistant : public Student, public Employee
{
public:
          Assistant() {}
          virtual void print() const 
          {
                   cout << "Printing Assistant data: *************************";
                   Student::print();
                   Employee::print();
          }
          virtual int calc_credit() const 
                   { return Student::calc_credit() + Employee::calc_credit(); }
          virtual int calc_debit() const 
                   { return Student::calc_credit() + Employee::calc_debit(); }
};
 

הסבר: המחלקה Assistant אינה מגדירה תכונות נוספות כלשהן. היא דורסת את הפנוקציות הוירטואליות

    print() - מבצעת קריאה לפונקציות של מחלקות הבסיס
    calc_credit() - מבצעת חיבור של calc_credit() של שתי מחלקות הבסיס
    calc_debit() - מבצעת חיבור של calc_debit() של שתי מחלקות הבסיס

דוגמא לשימוש במחלקה:

 
          Assistant a1;
          a1.regCourse(Student::MATH);                              // Student::regCourse()
          a1.regCourse(Student::ALGORITHMS);             // Student::regCourse()
          a1.setSal(5000);                                                         // Employee::setSal()
          a1.print();                                                        // Assistant::print()
 

והפלט:

 
Printing Assistant data: ********************
 
Person:
Student: Courses = 1000,1003,
 
Person:
Employee:: salary=5000
Press any key to continue
 

יש לשים לב שהפונקציה Person::print() נקראת פעמיים! היא נקראת פעם מתוך Student::print() ופעם מתוך Employee::print().

מחלקת בסיס מרובה וירושה וירטואלית

העצם שהגדרנו בסעיף הקודם אינו מכיל ערכים של מחלקת הבסיס Person. נניח שהיינו רוצים לקבוע אותם ע"י הפונקציה Person::set() כך:

 
          Assistant a1;
          a1.set(5028850, "a2", "address of a2");      // error: ambiguous
 

מתקבלת הודעת שגיאה!

בכדי להבין את השגיאה, נתבונן בפרישת עצם המחלקה Assistant בזיכרון:

כלומר, עצם מסוג Assistant יורש את המחלקה Person פעמיים: פעם מצד Student ופעם מצד Employee. המהדר מודיע לכן על שגיאה מכיוון שאינו יכול להכריע מבין שתי ורסיות הפונקציות (שהן למעשה זהות).

הפתרון לבעיה זו הוא להשתמש בירושה וירטואלית: מחלקות הבסיס Student ו- Employee צריכות לרשת מ- Person בירושה וירטואלית ע"י המציין virtual :

 
class Student : virtual public Person
{
          ...
};
 
class Employee : virtual public Person
{
          ...
};
 

וכעת המחלקה Assistant יורשת את Person פעם אחת בלבד:

וכעת גם ניתן לקרוא לפונקציות של מחלקת הבסיס Person ללא חשש מקונפליקט:

 
          Assistant a1;
          a1.set(5028850, "a1", "address of a1");     // Person::set()
          a1.regCourse(Student::MATH);                              // Student::regCourse()
          a1.regCourse(Student::ALGORITHMS);             // Student::regCourse()
          a1.setSal(5000);                                                         // Employee::setSal()
          a1.print();                                                        //Assistant::print()
 

והפלט:

 
Printing Assistant data: **********************
 
Person:5028850,a1,address of a1
Student: Courses = 1000,1003,
 
Person:5028850,a1,address of a1
Employee:: salary=5000
 

עדכון המחלקה Lecturer

לאחר שיצרנו את המחלקה Assistant, נעדכן את המחלקה Lecturer להכיל וקטור של Assistants ולא Students:

 
class Lecturer : public Employee
{
public:
          Lecturer() {m_degree=0;}
          Lecturer(const long id, const char* name, const char *addr, int sal, int degree) 
          : Employee(id,name,addr,sal) , m_degree(degree) {}
          void addAssistant(Assistant &a) { m_assistants.push_back(a); }
          virtual void print() const 
          {
                   Employee::print();
                   cout << "Lecturer: degree=" << m_degree;
                   if(!m_assistants.empty())         {
                            cout << ", Asistants: " ;
                            for(int i=0; i<m_assistants.size(); i++)
                                      cout << m_assistants[i].getName() << ", ";
                   }
 
                   cout << endl;
          }
private:
          int m_degree;
        vector<Assistant> m_assistants;
};
 

קוד הבדיקה:

 
          Assistant a1;
          a1.set(5028850, "a1", "address of a1");      // Person::set()
          a1.regCourse(Student::MATH);                              // Student::regCourse()
          a1.regCourse(Student::ALGORITHMS);             // Student::regCourse()
          a1.setSal(5000);                                                         // Employee::setSal()
          a1.print();                                                        // Assistant::print()
 
          Assistant a2(1342350, "a2", "address of a2", 3000);       
          a2.regCourse(Student::MATH);
          a2.print();
 
          Lecturer l1(5028850, "l1", "address of l1", 4000, 3);
          l1.addAssistant(a1);
          l1.addAssistant(a2);
          l1.print();
 

איתחול בירושה מרובה

נותרה עדיין נקודה אחת: כיצד ניתן לאתחל את נתוני מחלקות הבסיס השונות מהמחלקה הנגזרת? בפרט, מה אם קיימים מספר מסלולי איתחול למחלקת הבסיס הוירטואלית המשותפת?

המחלקה הנגזרת ביותר (Assistant) יכולה לאתחל את מחלקת הבסיס הוירטואלית המשותפת (Person) באחת משתי הדרכים:

  • אם היא קוראת ל- consrtuctor מסוים של הבסיס, זה מבוצע באופן מפורש
  • אם היא אינה קוראת ל- constructor של הבסיס נקרא constructor המחדל

לדוגמא, במחלקה Assistant מעוניינים להגדיר constructor המקבל פרמטרים, בנוסף ל- constructor המחדל:

 
class Assistant : public Student, public Employee
{
public:
          Assistant() {}
          Assistant(const long id, const char* name, const char *addr, int sal) : 
                   Person(id,name,addr) ,
                   Student(id,name,addr), 
                   Employee(id,name,addr, sal)   {}
          virtual void print() const 
          {
                   cout << "Printing Assistant data: *************************";
                   Student::print();
                   Employee::print();
          }
          virtual int calc_credit() const { return Student::calc_credit() + Employee::calc_credit(); }
          virtual int calc_debit() const { return Student::calc_credit() + Employee::calc_debit(); }
};
 

סדר האיתחול

סדר הפעלת ה- constructors הוא תמיד עפ"י סדר ההכרזה על מחלקות הבסיס. כלומר, אם המחלקה Assistant הוכרזה כך

 
class Assistant : public Student, public Employee
 

אזי תמיד ייקרא ה- constructor של Student לפני זה של Employee. ומכיוון ש- Person מחלקת הבסיס של Student, ה- constructor שלה נקרא קודם. לסיכום, באיתחול עצם מהמחלקה Assistant סדר האיתחול הוא:

    1. קריאה ל- constructor של Person - בגירסת המחדל או בגירסה אחרת שנקראה מפורשות ע"י Assistant.
    2. קריאה ל- constructor של Student
    3. קריאה ל- constructor של Employee
    4. ביצוע גוף ה- constructor של Assistant

תרגול

קרא/י סעיף זה בספר ובצע/י את תר' 11-1.

RTTI

RTTI הוא קיצור של "Run Time Type Information" שמשמעותו קבלת מידע על טיפוסי עצמים בזמן ריצה.

מנגנון זה מאפשר לתכנית לקבל בזמן ריצה מידע על עצמים הנוגע לאופן הכרזתם בזמן הידור.

סוגי המידע הבסיסיים המסופקים ע"י מנגנון  RTTI :

     1. האם עצם נתון הוא ממחלקה נתונה?
     2. האם שני עצמים נתונים הם מאותה מחלקה?
     3. מהו שם המחלקה של עצם נתון?
     4. האם ניתן להמיר מצביע לעצם אחד למצביע מטיפוס אחר? כלומר, האם קיימת התאמת מצביעים בירושה המאפשרת לבצע המרה בטוחה?

מנגנון RTTI מספק את המידע שבשאלות 1-3 באמצעות המחלקה type_info והאופרטור typeid. המרה בטוחה (סעיף 4) מבוצעת ע"י האופרטור dynamic_cast.

מנגנון RTTI מסתמך על מנגנון הפולימורפיזם הקיים בשפה, ולכן הוא זמין רק עבור מחלקות המכילות לפחות פונקציה וירטואלית אחת, כלומר שיש להן VTBL.

לצורך שימוש במנגנוני RTTI יש לבצע את ההכללה

 
#include <typeinfo>
 

המחלקה type_info והאופרטור typeid

המחלקה type_info היא לב מנגנון RTTI: מחלקה זו מייצגת את מידע הטיפוס הניתן לשליפה עבור עצם נתון. קבלת עצם מהמחלקה type_info מבוצעת באמצעות האופרטור typeid.

לדוגמא, אם מוגדרת המחלקה הבאה,

 
class X
{
          int i;
public:
          virtual void f() {i++;}
};
 

ומוגדר עצם מהמחלקה כך,

 
X       x1;
 

אז ניתן לקבל את עצם ה- type_info המתאים של x1 כך:

 
typeid(x1);                  // return type_info of x1
 

מה ניתן לבצע עם העצם המוחזר מסוג type_info? לשם כך נסתכל בשירותים שמספקת המחלקה type_info, המוכרזת בערך כך:

 
class type_info 
{
public:
          int    operator==(const type_info& rhs) const; 
          int    operator!=(const type_info& rhs) const; 
          int    before(const type_info& rhs) const; 
          const         char* name() const;                   
          virtual ~type_info();                                                               
private:
          type_info(const type_info&);                         // cannot copy type_info
          type_info& operator=(const type_info&);   // cannot assign type_info
          // ...
};
 

כלומר, ניתן להשוות בין טיפוסי עצמים ע"י האופרטורים "==" ו "=!", וכן ניתן לשלוף את שם המחלקה של עצם נתון ע"י הפונקציה name().

דוגמאות:

  - האם עצם נתון הוא ממחלקה נתונה?
 
          if(typeid(x1)==typeid(X))
                   cout << "x1 is of type X" << endl;
 
  - האם שני עצמים נתונים הם מאותה מחלקה?
 
          if(typeid(x1)==typeid(x2))
                   cout << "x1 and x2 are of the same class" << endl;
          else
                   cout << "x1 and x2 are not of the same class" << endl;
 
  - מהו שם המחלקה של עצם נתון?
 
          cout << typeid(x1).name() << endl;
 

כפי שניתן לראות, האופרטור typeid() שולף את עצם ה- type_info המתאים של עצם נתון או של מחלקה נתונה.

יש לשים לב שהאופרטור typeid מופעל על העצם בזמן ריצה, ועל פי ה- VPTR שלו המצביע ל- VTBL (ראה/י פרק 10) שולף את עצם ה- type_info המתאים.

כלומר, אם למשל נתונה מחלקה Y הנגזרת מ- X,

 
class Y : public X
{
          float j;
public:
          virtual void g() { j--;}
};
 

אז ניתן להפעיל את האופרטור על מצביע פולימורפי. לדוגמא, פונקציה גלובלית כלשהי המקבלת מצביע ל- X יכולה לבדוק לאיזה טיפוס מדוייק שייך העצם:

 
void func(X *px)
{
          if(typeid(*px)==typeid(X))
                   cout << "*px is of type X" << endl;
          else
                   cout << "*px is of type Y" << endl;
 
          px->f();
};
 

האופרטור dynamic_cast

האופרטור dynamic_cast מאפשר לבצע המרות טיפוסים בטוחות, תוך בדיקה בזמן ריצה שההמרה אכן חוקית.

לדוגמא, נתונה פונקציה המקבלת כפרמטר מצביע למחלקה X שלעיל, והיא מעוניינת לקרוא לפונקציה g() של המחלקה Y במידה והעצם המוצבע הוא מסוג Y:

 
void call_g(X *px)
{
          if(typeid(*px)==typeid(Y))
                   ((Y *)px)->g();
};
 

הסבר: אם תוצאת ההשוואה של טיפוס העצם המוצבע עם Y חיובית, אזי ממירים את המצביע ע"י casting ל "Y *", וקוראים לפונקציה g().

מלבד המסורבלות שבשיטה זו, היא כוללת חסרון משמעותי: נניח שקיימת מחלקה Z הנורשת מ- Y,

 
class Z : public Y
{
          //...
};
 

וקוראים לפונקציה call_g() עם מצביע לעצם מטיפוס Z,

 
call_g(new Z);
 

תוצאת ההשוואה בפונקציה תהיה שלילית:

 
          if(typeid(*px)==typeid(Y))
 

למרות שניתן לקרוא לפונקציה g() עבור עצם מסוג Z !

לפתרון בעיה זו נעשה שימוש באופרטור dynamic_cast :

 
void call_g(X *px)
{
          Y *py = dynamic_cast<Y *>(px);
          if(py)
                   py->g();
};
 

הסבר: האופרטור dynamic_cast מופעל בזמן ריצה וממיר את המצביע הנתון לו כפרמטר לטיפוס המבוקש:

 
               dynamic_cast<Y *>(px);
 

במידה וההמרה חוקית - כלומר המצביע (px) הוא מהטיפוס המבוקש (Y *) או מטיפוס נגזר ממנו (Z*) - האופרטור מחזיר את המצביע לאחר ההמרה. אחרת, מוחזר 0.

לכן, ניתן לבדוק ע"י הערך שהוחזר והוצב ב- py אם אינו 0 ולבצע את הקריאה לפונקציה g() בהתאם:

 
          if(py)
                   py->g();
 

המרת reference

האופרטור dynamic_cast מאפשר לבצע גם המרת reference לעצם, ולא רק מצביע. אלא שכאן, אם ההמרה נכשלת לא ניתן להחזיר 0 - לכן נעשה שימוש במנגנון החריגות, שנכיר בפרק 12: במידה וההמרה נכשלת האופרטור dynamic_cast זורק חריגה מטיפוס bad_cast.

לדוגמא, ניתן לכתוב את הפונקציה call_g() כך:

 
void call_g(X &rx)              // reference version
{
          try {
                   Y &ry = dynamic_cast<Y &>(rx);
                   ry.g();
          } 
          catch (bad_cast) {    // cast failed
                   cout << "cannot cast: rx is not of type Y" << endl; 
          } 
};
 

כעת ניתן להשתמש בפונקציה כך:

 
          X x1;
          Z z1;
 
          call_g(x1);                   // cast fails
          call_g(z1);          // OK: calls z2.g()
 

אנו נרחיב בנושא החריגות בפרק 12.

סיכום ההמרות ב- C++

קיימים מספר כווני המרה של מצביעים או references :

  • המרת מצביע מחלקה נגזרת למצביע מחלקת בסיס (Up Cast) - זוהי המרה טבעית וחוקית עפ"י כלל התאמת מצביעים בירושה.
לדוגמא, אם נתונה היררכיית הירושה

אז ניתן להעביר לפונקציה המצפה לקבל כפרמטר מצביע לבסיס,
 
void f(A *pa);
 
מצביע מסוג הנגזרת
 
f(new B);
 
  • המרת מצביע מחלקת בסיס למצביע מחלקה נגזרת (Down Cast) - זוהי המרה לא טבעית ויש לבצעה באופן בטוח ע"י האופרטור dynamic_cast.
לדוגמא, עבור הירושה הנ"ל ניתן לבצע
 
void f(A *pa)
{
          B *pb = dynamic_cast<B *> (pa);
          if(pb)
                   // use as B *
}
 
  • המרה בין מצביעים לעצמים ממחלקות "אחיות" (Cross Cast) - זוהי המרה המתרחשת במצב של ירושה מרובה: גם המרה זו דורשת שימוש באופרטור dynamic_cast בכדי להיות בטוחה.
לדוגמא, אם נתונה היררכיית הירושה
ניתן לבצע שימוש ב- dynamic_cast להמרה ממצביע A למצביע B:
 
void f(A *pa)
{
          B *pb = dynamic_cast<B *> (pa);
          if(pb)          // pb points to C object 
                   // use as B*
}
 

אופרטורי המרה סטטיים נוספים

C++ כוללת מספר אופרטורי המרה סטטיים - כלומר כאלו המתבצעים בזמן הידור ולא בזמן ריצה - ולכן אלו אינם אופרטורים בטוחים. אופרטורים אלו הם בנוסף לאופרטור ההמרה הסטטית הלא בטוחה שנורשה משפת C.

  • static_cast - אופרטור המרה המבצע בדיקות בזמן הידור:
 
          A *pa;
          // ...
          B *pb = static_cast<B *>(pa);      // OK, not safe at rumtime
 
האופרטור בודק בזמן הידור שלא מופרים כללי  const/ volatile וירושת private. לדוגמא:
 
          const A *pa;
          // ...
          B *pb = static_cast<B *>(pa);      // compile time error
 
השגיאה המתקבלת נובעת מכך שלא ניתן להמיר מצביע const למצביע non-const. שגיאה דומה תתקבל אם B יורש מ- A ירושת private.
  • const_cast -  אופרטור המאפשר להמיר מצביע const למצביע non-const. לדוגמא:
 
          const A *cpa;
          // ...
          A *pa = const_cast<A *>(cpa);     
 
  • reinterpret_cast - דומה מאוד לאופרטור ההמרה של שפת C. יתרונו הוא בכך שהוא מפורש יותר, וכן הוא אינו מפר כללי const או volatile. דוגמא:
 
          A *pa = (A *) 0x34590;                                                          // C
          A *pa = reinterpret_cast<A *>(0x34590);         // C++
 

ממשקים (Interfaces)

ממשק הוא מחלקה אבסטרקטית שכל הפונקציות שלה וירטואליות טהורות. כמו כן, ממשק יכול לכלול פונקציות סטטיות ומשתנים סטטיים, אם כי, רצוי להמעיט באלו.

לדוגמא, נגדיר ממשק למחלקות הצורה (Shape) שראינו בפרק 10 בשם Fillable:

 
class Fillable
{
public:
          virtual void fill(int color) = 0;
};
 

ממשק זה מתאר יכולת של מילוי צורה בצבע.

הצורות היכולות לרשת ממשק זה הן Circle ו- Rect אך לא  Poly ולא Line. תרשים המחלקות ייראה כעת כך:

הסבר: הממשק Fillable כולל פונקציה יחידה, fill(), וירטואלית טהורה. כאשר מחלקה יורשת מממשק, אנו אומרים שהיא מממשת (Realize) אותו: Circle ו- Rect יורשות מ- Shape ומממשות את Fillable.

ב- UML הממשק מתואר בדומה למחלקה, עם המאפיין (stereotype) <<interface>>. כמו כן, מימוש הממשק מצויין ע"י חץ עם ראש משולש, בדומה לירושה, אך בסיגנון מקווקו.

נראה כעת את מימוש הממשק במחלקה Circle:

 
class Circle : public Shape, public Fillable
{
          int              m_radius;
        int          m_fill;
public:
          Circle() { m_radius=0; m_fill=0; }
          ...
          virtual void draw() const
          { 
               win().fillcolor(m_fill); 
                   win().circle(getPos().x(), getPos().y(), m_radius);
          } 
        virtual void fill(int color) { m_fill = color; }
};
 

המחלקה Circle מממשת את Fillable ע"י שמירת משתנה המתאר את צבע המילוי. לפני ביצוע פעולת הציור, נקבע צבע זה בחלון ע"י פונקצית המילוי של המחלקה WinG, fillcolor() (ראה/י פרק 10).

דוגמא לקוד משתמש:

 
          Circle c1(Point(200, 180), 40);
          c1.fill(RED);
          c1.draw();
 

החלון המתקבל:

ממשקים כבסיס לתכנות מבוזר

הממשק הוא הבסיס לתכנות מונחה עצמים מבוזר ולטכנולוגיות רכיבים. דוגמאות למערכות תכנות מונחה עצמים מבוזר העושות שימוש בממשקים:

  • MS-DCOM - מערכת של חברת מיקרוסופט לתכנות מבוזר מעל מערכות Windows.
  • CORBA - תקן למערכות מבוזרות מעל שפות תכנות, מחשבים ומערכות הפעלה כלשהם.
  • JAVA-RMI - מנגנון ב- Java למערכות מבוזרות עצמים מעל מכונות מדומות של Java.

דוגמאות לטכנולוגיות רכיבים העושות שימוש בממשקים:

  • MS-COM/ActiveX  - מפרט ו- API של חברת מיקרוסופט לטכנולוגית רכיבים מעל Windows.
  • Java Enterprise Beans - מפרט ו- API של חברת Sun לטכנולוגית הרכיבים Beans מעל מכונות Java.

מערכות מבוזרות עושות שימוש בשתי תכונות חשובות שמספק ממשק:

  • אי-תלות של קוד לקוח במימוש המחלקה. אם קוד הלקוח מפעיל את שירותי המחלקה דרך ממשק אליה, הוא אינו צריך לעבור הידור מחדש בכל שינוי, הוספה או מחיקה של הגדרות משתנים ופונקציות.
כל עוד כותרות הפונקציות הוירטואליות שהוכרזו בממשק לא השתנו קוד הלקוח יפעל באופן תקין גם ללא הידור מחודש.
  • תאימות בינרית (Binary Compatibility) גבוהה. קוד לקוח המבצע קריאות לפונקציות של מחלקה מספרייה מסוימת צריך להתחשב במנגנון הקידוד של שמות הפונקציות בספרייה זו.
מנגנון זה הוא תלוי מהדר, ולכן הלקוח צריך לעבור הידור עם אותו מהדר שבו הודרו קבצי הספרייה. לעומת זאת, קריאות לפונקציות וירטואליות מבוצעות ע"י מנגנון ה- VTBL ולכן אינם תלויי מהדר.

מעבדה

בצע/י את המעבדה שבסוף פרק זה.

סיכום

  • ירושה מרובה (Multiple Inheritance) הינה הרחבה של מנגנון הירושה: ניתן לרשת מיותר ממחלקה אחת, ובכך לרשת ממשקים ופונקציונליות ממספר מחלקות.
  • נהוג להפריד בין שני סוגי ירושה עקרוניים:
  - ירושת מימוש: מחלקה היורשת ממחלקה אחרת את הגדרות המשתנים והפונקציות שלה.
  - ירושת ממשק: מחלקה היורשת ממשק - כלומר ממחלקת בסיס אבסטרקטית, שכל הפונקציות שלה וירטואליות טהורות.
ירושת מימוש מרובה היא נושא בעייתי עקב הקונפליקטים הנוצרים לעיתים תכופות בירושת תכונות ופונקציות ממספר מחלקות. לעומת זאת ירושת ממשק מרובה אינה גורמת לקונפליקטים והיא שימושית בהרבה מערכות מונחות עצמים.
  • משתמשים בירושה וירטואלית (Virtual Inheritance) בכדי לפתור בעיות של מחלקת בסיס מרובה (Multiple Same Base): ציון virtual בירושה מנחה את המהדר שלא להכיל את מחלקת הבסיס יותר מפעם אחת בכל מבנה של ירושה מרובה.
  • RTTI (Run Time Type Information) הוא מנגנון המאפשר לתכנית לקבל בזמן ריצה מידע על עצמים הנוגע לאופן הכרזתם בזמן הידור:
  - המחלקה type_info מייצגת את מידע הטיפוס הניתן לשליפה עבור עצם נתון.
  - קבלת עצם מהמחלקה type_info מבוצעת באמצעות האופרטור typeid.
  - האופרטור dynamic_cast מאפשר לבצע המרות טיפוסים בטוחות, תוך בדיקה בזמן ריצה שההמרה אכן חוקית.
  • C++ כוללת מספר אופרטורים סטטיים נוספים:
    static_cast - אופרטור המרה המבצע בדיקות בזמן הידור שלא מופרים כללי  const/ volatile וירושת private.
    const_cast -  אופרטור המאפשר להמיר מצביע const למצביע non-const.
    reinterpret_cast - דומה לאופרטור ההמרה של שפת C, אך  אינו מפר כללי const או volatile.
  • ממשק הוא מחלקה אבסטרקטית שכל הפונקציות שלה וירטואליות טהורות, ובנוסף, היא יכולה להכיל פונקציות סטטיות ומשתנים סטטיים. הממשק הוא הבסיס לתכנות מונחה עצמים מבוזר ולטכנולוגיות רכיבים.


[ <<< הקודם ] [ תוכן עניינים ] [ הבא >>> ]