ירושה - רב צורתיות  Polymorfhisim 

 

 


תוכן הפרק

 

משתני התייחסות ומצביעים

פונקציות וירטואליות

פונקציות וירטואליות טהורות ומחלקות אבסטרקטיות

שימוש במחלקה אבסטרקטית כמפרידה בין מימוש ל מנשק

פקודות למניעת הכללה מרובה   #ifndef #define #endif

virtual destructor

העתקת עצם וירטואלית

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

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

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

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

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

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

בקרת הגישה   public protected private

לסיכום

סוג תורשה

run-time binding     Vtbl

 

                         ·        ירושה מאפשרת להגדיר מחלקות (טיפוסים) בצורה היררכית, כלומר טיפוס הכללי ביותר (משותף לכולם ולכן מכיל הכי פחות) ולאחר מכן מחלקות יותר ספציפיות (מכילות יותר פרטים)

                         ·        כל מחלקת בסיס היא הצורה הכללות של כל המחלקות היורשות ממנה - זאת כמובן אם הירושה היא subtyping  כלומר מבטאת יחס "סוג של".

 

·        הגדרת טיפוסים בהיררכיה מאשפרת לנו לכתוב אלגוריתמים ברמה מופשטת (abstract)  אפילו מבלי לדעת על אילו עצמים מדובר בדיוק.

·        רב צורתיות (polymorphisim)  היא האפשרות לכתוב אלגוריתם כללי אשר הפרטים המדויקים נקבעים על פי סוג העצמים שעליהם מופעל האלגוריתם.

·        דוגמא לרב צורתיות:  a + b.  הרעיון הכללי הוא לחבר ערכי שני משתנים. אותה פעולת חיבור מתנהגת שונה אם מדובר במשתנים מטיפוס  int  או משתנים מטיפוס   double. זהו מקרה פשוט שבו במהדר יכול לקבוע באיזו פעולת חיבור מדובר. אנו נראה כי קיימים מצבים בהם אפשר לקבוע בדיוק באיזו פעולה מדובר רק בזמן ריצת התוכנית ממש. וכן נכיר את המנגנון של  C++  המאפשר לבצע קביעה זו.

·        שיטת עבודה מצויה ב  C++  להגדיר ספריות של אלגוריתמים כלליים ולהתחבר אליהם. (דוגמא MFC)

//--------------------------------------------------------------------

// file: shapes.h

const double PI = 3.1415926535897932385;

 

class Shape {

    int _x, _y;

    int _color;

public:

     Shape(int x, int y, int c) : _x(x), _y(y), _color(c) {}

     void set_color(int c) { _color = c; }

};

 

class Circle : public Shape {

    int _radius;

public:

    Circle (int x, int y, int c, int r)

        : Shape(x, y, c), _radius(r) {}

    double area() const { return PI * _radius * _radius; }

};

 

class Rect : public Shape {

    int _length, _width;

public:

    Rect(int x, int y, int c, int l, int w)

        : Shape(x, y, c), _length(l), _width(w) {}

    double area() const { return _length * _width; }

};

חזרה להתחלה

משתני התייחסות ומצביעים

·        כאשר משתנה הוא מסוג התיחסות ( reference  ) למחלקת בסיס, המשתנה יכול להתיחס בפועל לעצם מסוג מחלקת הבסיס או לעצם מסוג מחלקה היורשת מעצם הבסיס!

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

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

 

#include "shapes.h"

void main()

{

    Shape  s(1, 2, 3);

    Circle c(1, 2, 3, 4);

    Rect   r(1, 2, 3, 5, 6);

   

    Shape& s_ref1 = s;

    Shape& s_ref2 = c;

    Shape& s_ref3 = r;

    s_ref1.set_color(0);  // set color of all shapes to color 0

    s_ref2.set_color(0);

    s_ref3.set_color(0);

    s_ref2.area(); // Error: s_ref2 dosn't know it refrence to Circle

    s_ref3.area(); // Error: s_ref2 dosn't know it refrence to Rect

    Circle& c_ref1 = c; // OK

    Circle& c_ref2 = s; // Error: can't reference to a base class.

}

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

 

·        מצביע מתנהג בדומה להתיחסות בעניין היכולת להצביע למחלקת בסיס ולמחלקה היורשת.

·        נכתוב עתה אלגוריתם כללי המקבל מערך של מצביעים לצורות וצובע את כל הצורות בצבע מסויים

·        אין אפשרות להגדיר מערך של התיחסויות כאשר רוצים להגדיר מערך של עצמים פולימורפיים חייבים לעבוד עם מערך של מצביעים למחלקת         הבסיס.

#include "shapes.h"

 

// a- is an array of refrences to Shape

// n- number of elemnts in the array

// c- color shapes are set to

 

void set_shapes_color(Shape* a[], int n, int c)

{

    for (int i = 0; i < n; i++)

        a[i]->set_color(c);

}

 

void main()

{

    Shape  s(1, 2, 3);

    Circle c(1, 2, 3, 4);

    Rect   r(1, 2, 3, 5, 6);

   

    Shape* shapes[3] = {&s, &c, &r};

   

    set_shapes_color(shapes, 3, 0);

}

 

חזרה להתחלה


פונקציות וירטואליות 

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

·                    לשכיר ולמנהל פעולה בעלת משמעות זהה  salary()   רק אופן החישוב שונה.

 

class Employee {

protected:

    double _hour_rate;

    int    _num_of_hours;

public:

    Employee(double hr, int noh)

        : _hour_rate(hr), _num_of_hours(noh) {}

    double salary() const { return _hour_rate * _num_of_hours; }

};

 

class Manager : public Employee {

protected:

    int _bonus;

public:

    Manager(double hr, int noh, int b)

        : Employee(hr, noh), _bonus(b) {}

    double salary() const

       { return (_hour_rate + _bonus ) * _num_of_hours; }

};

 

#include <iostream.h>

void main()

{

    Employee e(10.2, 80);

    Manager  m(20.4, 80, 60);

 

    cout << e.salary() << endl;

    cout << m.salary() << endl << endl;

 

    Employee* pe = &e;

    cout << pe->salary() << endl; // fine

    pe = &m;

                               // bad: pe uses Employee::salary()

    cout << pe->salary() << endl << endl;

 

    Manager* pm = &m;

    cout << pm->salary() << endl; // fine: pm uses Manager::salary()

    // pm = &e; - compile error: can't point to base of Manager

}

//----Output-----

816

6432

 

816

1632

 

6432

 

                         ·        רוצים לכתוב אלגוריתם כללי המחשב סה"כ שכר של כל העובדים.

                         ·        כתיבת אלגוריתם כללי דורש הגדרת מערך של מצביעים למחלקת הבסיס והפעלת אותה הפונקציה על כל העצמים. במקרה שלנו אנו צריכים להגדיר מערך של מצביעים ל  Employee

                         ·        בעיה:  כאשר מפעילים את הפעולה  salary()  על מצביע ל Employee  אנו מקבלים את הפעלת הפונקציה Employee::salary()  בלי קשר אם המצביע מצביע לעצם מסוג מנהל או שכיר.

                         ·        אמנם, מצביע ל Manager  מפעיל את הפונקציה Manager::salary()  אבל הוא אינו יכול להצביע לעצמים מטיפוס מחלקת הבסיס. כלומר נוכל להגדיר רק מערך של מנהלים אך אנו מעוניינים במערך של כל סוגי השכירים (שכיר ומנהל).


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

 

double tot_sals(Employee* emps[], int n)

{

    int sum = 0;

    for (int i = 0; i < n; i++)

        sum += emps[i]->salary();

    return sum;

}

                         ·        בדוגמא זו אין המהדר יכול לזהות מהו טיפוסי העצם האמיתי של כל כתובת של  Employee.

                         ·        מכיוון שיתכן כי התכולה המדויקת המערך emps  נקבעת סופית רק בזמן ריצת התוכנית הרי אין אפשרות כלל למהדר לקבוע מראש איזו פונקציה להפעיל.

                         ·        אנו זקוקים למנגנון שיקבע בזמן ריצת התוכנית אוזו גירסא של  salary() להפעיל. מנגנון כזה מכונה  קישור בזמן ריצה או run-time binding  .

                         ·        כאשר מוסיפים לפני פונקציה במחלקת הבסיס את המילה   virtual  הכוונה שעבור פונקציה זו המהדר מוסיף מידע (אזה נראה בהמשך) המסייע לו להפעיל את מנגנון הקישור בזמן ריצה עבור אותה הפונקציה.

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

                         ·        גם לפונקציה וירטואלית חייבים להגדיר גוף.

class Employee {

protected:

    double _hour_rate;

    int    _num_of_hours;

public:

    Employee(double hr, int noh)

        : _hour_rate(hr), _num_of_hours(noh) {}

    virtual double salary() const { return _hour_rate * _num_of_hours; }

};

 

class Manager : public Employee {

protected:

    int _bonus;

public:

    Manager(double hr, int noh, int b)

        : Employee(hr, noh), _bonus(b) {}

    double salary() const

       { return (_hour_rate + _bonus ) * _num_of_hours; }

};

 

#include <iostream.h>

void main()

{

    Employee e(10.2, 80);

    Manager  m(20.4, 80, 60);

 

    Employee* pe = &e;

    cout << pe->salary() << endl; // fine

    pe = &m;

    cout << pe->salary() << endl; // fine: Manager::salary()

 

    Employee* a[2] = {&e, &m};

    cout << tot_sals(a, 2) << endl;

}

//----Output-----

816

6432

7248

                         ·        אני יכול לכתוב את הפונקציה  tot_sals()  עוד לפני שידועים לנו כל סוגי ה  Employee בתוכנית.

                         ·        אפשר להוסיף עוד סוגים של Employee  והפונקציה tot_sals()  תמשיך תעבוד בצורה תקיה גם עבורם (בתנאי שיגדירו מחדש, אם צריך, את הפונקציה  salary())

חזרה להתחלה

 


פונקציות וירטואליות טהורות ומחלקות אבסטרקטיות

בדוגמא הבאה מגדירים היררכיה של צורות:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


                         ·     כל צורה מישורית יש לה

                         ·     מאפיינים: קורדינטות מיקום  x, y   וצבע  color

                         ·     וכן לכל צורה מישורית מעוניינים שתהיה אפשרות למדוד את שיטחה

                         ·     למעגל

                         ·     מאפיין נוסף: רדיוס

                         ·     פעולה: מדידת שטח המעגל

                         ·     למלבן

                         ·     מאפיינים נוספים: אורך ורוחב.

                         ·     פעולה: מדידת שטח של מלבן.

 

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

                         1.   מחלקות קונקרטיות (concrete class) - מחלקה שבה לכל פונקציה ידוע האלגוריתם המדויק. כלומר אפשר לכתוב את גוף הפונקציה. בדוגמא אצלנו  מלבן ומעגל.

                         2.   מחלקה  אבסטרקטית  (abstract class) -  מחלקה שבה קיימות חלק מהפונקציות שהן תכונות משותפות לכל היורשים. אבל ברמה המופשטת כרגע אין יודעים את האלגוריתם. בדוגמא שלנו: במחלקה צורה מישורית הפונקציה  area()   היא תכונה משותפת לכל צורה מישורית אבל לא יודעים כיצד לבצע אותה (בכל צורה קונקרטית החישוב שונה)

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

                          ·    מחלקה אבסטרקטית היא מחלקה שיש בה פונקציה אבסטרקטית אחת או יותר.

                          ·    אין יוצרים עצמים מטיפוס מחלקות אבסטרקטיות. יצירת עצם בתוכנית מטיפוס  PlanarShape  ודאי טעות (לוגית) כי לא יתכן שקיימת במציאות צורה מישורית ללא הגדרה קונקרטית (מלבן מעגל וכו').

                          ·    מגדירים מחלקה אבסטרקטית כמנשק  (interface) משותף לכל היורשים (הם מגדירים מחדש את הפונקציות האבסטרקטיות). מטרת המנשק לאפשר לכתוב אלגוריתמים כלליים. לדוגמא: אצלנו אפשר לכתוב אלגוריתם כללי לחישוב סך כל השטחים של כל הצורות המישוריות.

 


//--------------------------------------------------------------------

// file: shapes.h

const double PI = 3.1415926535897932385;

 

// abstreact base class

class PlanarShape {

    int _x, _y;

    int _color;

public:

     PlanarShape(int x, int y, int c) : _x(x), _y(y), _color(c) {}

     void set_color(int c) { _color = c; }

     virtual double area() const = 0; // pure virtual function

};

 

// concrete classes

class Circle : public PlanarShape {

    int _radius;

public:

    Circle (int x, int y, int c, int r)

        : PlanarShape(x, y, c), _radius(r) {}

    double area() const { return PI * _radius * _radius; }

};

 

class Rect : public PlanarShape {

    int _length, _width;

public:

    Rect(int x, int y, int c, int l, int w)

        : PlanarShape(x, y, c), _length(l), _width(w) {}

    double area() const { return _length * _width; }

};

 

·        פונקציה וירטואלית  virtual double area() = 0; היא פונקציה וירטואלית טהורה. (pure-virtual)

·        אין מגדירים גוף לפונקציה ירטואלית טהורה.

·        מחלקה שמכילה פונקציה וירטואלית טהורה אחת או יותר מכונה מחלקה אבסטרקטית.

·        אי אפשר ליצור עצמים מטיפוס מחלקה אבסטרקטית.

 

double sum_area(PlanarShape* a[], int n)

{

    double sum = 0.0;

    for (int i = 0; i < n; i++)

        sum += a[i]->area();

    return sum;

}

 

void main()

{

    // PlanarShape s(1, 2, 0) - Error: can't create abstract object

    Circle c1(1, 2, 0, 4), c2(0, 0, 0, 5);

    Rect r1(0, 0, 0, 2, 3), r2(3, 4, 4, 4, 4);

    PlanarShape* as[4] = {&r1, &r2, &c1, &c2};

   

    cout << sum_area(as, 4);

}

//----Outpout-----

150.805

חזרה להתחלה

 


שימוש במחלקה אבסטרקטית כמפרידה בין מימוש ל מנשק

·        אנו מגדירים קבוצה של 4 תווים  a b c d

·        בקבוצה יכולים להופיע לכל היותר ארבעת התווים ורק עותק אחד של כל תו.

·        אנו מגדירים מחלקה אבסטרקטית set4c   שהיא מנשק לקבוצת 4 התווים. במחלקות מנשק בדרך כלל יש רק אוסף של פונקציות וירטואליות טהורות ואין כלל אלגוריתמים.

·        אנו מגדירים שני מימושים באמצעות שתי מחלקות היורשות ממחקלת המנשק.

1.                     set4c_n  מחלקה המכילה רק משתנה מסוג int  ובאמצעות עבודה עם 4 מספרים ראשוניים היא מנהלת את הקבוצה.

2.                     set4c_a  מחלקה המכילה מערך של משתנים בוליאנים שכל אחד אומר האם תו קיים או לא בקבוצה.

·                    אנו כותבים מספר אלגוריתמים בהתבסס על מחלקת המנשק בלבד.

 

 

כאשר רוצים לתאר מחלקות בתרשים נוהגים:

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

·                    אם מחלקה או פעולה הן אבסטרקטיות כותבים את שם המחלקה או הפעולה באותיות מוטות (italic).

·                    לפני מאפיין שהוא public  רושמים +  ולפני מאפיין שהוא  private  רושמים -

·                    חץ מסמל ירושה ממחלקת: יוצא מהמחלקה הנגזרת וראש החת מצביע למחלקת הבסיס.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 


 

// file: set4c.h

// class set4c - interface class for

// set of {a, b, c, d} implemented by char a[4];

// interface compleatly hides set implementation

 

 

#ifndef __SET4C__   // protection essetial for this program

#define __SET4C__

 

 

class set4c{

public:

    virtual bool find(char key) = 0;

    virtual void insert(char key) = 0;

    virtual void erase(char key) = 0;

    virtual void clear() = 0;

};

 

 

void show(set4c& ps); // show set in format {a, b, c, d}

                      // abstract algorithm using only interface

void read(set4c& ps); // clears set and reads one word e.g. abd

                      // and fill set with a b d elements

bool equal(set4c& s1, set4c& s2); // checks if sets are equal.

#endif

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

·        יש כאן גם הצהרה על שלושה אלגוריתמים   show, read, equal  שנכתבים רק על בסיס המנשק מבלי לדעת על מהן המחלקות הקונקרטיות של העצמים שעליהם הם יופעלו.

חזרה להתחלה

 פקודות למניעת הכללה מרובה   #ifndef #define #endif

·                    כאשר עובדים עם מספר קבצי H  חייבים להגן עליהם בפני הכללה כפולה.

#ifndef שם יחודי לקובץ    

#define שם יחודי לקובץ

.....

 

#endif

·        כל הפקודות המתחילות ב  #  נקראות פקודות קדם מעבד. פקודות אלו עורכות את הטקסט של הקובץ לפני שהמהדר מתחיל לעבור עליו.

·        כזכור #include "file"  מכלילה את תוכן הקובץ במקום השורה.

·        השורה    #ifndef name   היא תנאי שפירושו: אם לא הוגדר השם  name  תשאיר את כל הטקסט עד הסימן  #endif  ואם השם  name  כבר הוגדר מחק את כל הטקסט.

·        #define name  מגדירה את השם  name  עבור הקדם מעבד.

 

לכן אם מבצעים  #include   מספר פעמים לאותו קובץ  H  הרי בגלל ההגנה נמחקים כל הקבצים, פרט לראשון.  השם  AAA  חייב להיות יחודי לכל קובץ H  ולכן בוחרים אותו דומה במשהו לשם הקובץ

 

#ifndef AAA    

#define AAA

 

Line Callout 2: הכל נמחק ונשאר רק 
text
פעם אחת
text

 

text

 

 
#endif

 

#ifndef AAA  

#define AAA

 

text

 

#endif

 


 

// file: set4c_n.h

// implementation of set4c

// set of {a, b, c, d} using storage of a single int n

// we use 4 prime numbers: 2 3 5 7  each represents a letter a b c d

// for adding a to set n *= 2, b n*= 3 etc.

 

#ifndef __SET4C_N__  

#define __SET4C_N__

 

#include "set4c.h"

 

class set4c_n : public set4c {

    int _n;  // stores multiplications of primes

    int _key_to_prime(char key);

public:

    set4c_n() : _n(1) {}

    bool find(char key);

    void insert(char key);

    void erase(char key);

    void clear() { _n = 1; }

};

 

#endif

 

 

// file: set4c_n.h

// implementation of set4c

// set of {a, b, c, d} using storage of a single int n

// we use 4 prime numbers: 2 3 5 7  each represents a letter a b c d

// for adding a to set n *= 2, b n*= 3 etc.

 

#ifndef __SET4C_A__  

#define __SET4C_A__

 

#include "set4c.h"

 

class set4c_a : public set4c {

    bool _a[4];  // t/f if a b c d are present

public:

    set4c_a();

    bool find(char key);

    void insert(char key);

    void erase(char key);

    void clear();

};

 

#endif

·                    כאן מצהירים על שתי מחלקות הממשות את המנשק  set4c  על ידי ירושה ממנו

 

 

 

 


 

// file: set4c.cpp

// implementing general algorithms according to

// interface

#include <iostream.h>

#include "set4c.h"

 

// show set in format {a, b, c, d}

// abstract algorithm using only interface

void show(set4c& ps)

{

   

    cout << '{';

    bool first_print = true;

    for (char c = 'a'; c <= 'd'; c++)

        if (ps.find(c))

        {

            if(!first_print)

                cout << ", ";

            first_print = false;

            cout << c;

        }

    cout << '}';

}

 

// clears set and reads one word e.g. abd

// and fill set with a b d elements

void read(set4c& ps)

{

    char w[5];

    cin >> w;       // read the word

    int i = 0;

    while (w[i])    // insert each char to set

        ps.insert(w[i++]);

}

 

// checks if sets are equal.

bool equal(set4c& s1, set4c& s2)

{

    for (char c = 'a'; c <= 'd'; c++)

        if (s1.find(c) != s2.find(c))

            return false;

    return true;

}

 


 

// file: set4c_n.cpp

// implementation of class set4c_n

#include "set4c_n.h"

 

int set4c_n::_key_to_prime(char key)

{

    int p;

    switch(key) {

    case 'a': p = 2;

        break;

    case 'b': p = 3;

        break;

    case 'c': p = 5;

        break;

    case 'd': p = 7;

        break;

    }

    return p;

}

 

bool set4c_n::find(char key)

{

    if ('a' <= key && key <= 'd')

    {

        int p = _key_to_prime(key);

        if (_n / p == double(_n) / p)  // p devides _n

            return true;

    }

    return false;

}

 

void set4c_n::insert(char key)

{

    if ('a' <= key && key <= 'd')

    {

        int p = _key_to_prime(key);

        if (_n / p == double(_n) / p)  // p devides _n

            return;                    // already exsist!

        _n *= p;

    }

}

 

void set4c_n::erase(char key)

{

    if ('a' <= key && key <= 'd')

    {

        int p = _key_to_prime(key);

        if (_n / p == double(_n) / p)  // p devides _n

            _n /= p;                   // remove p factor

    }   

}

 


 

 

// file: set4c_n.cpp

// implementation of class set4c_n

#include "set4c_a.h"

 

set4c_a::set4c_a()

{   _a[0] = _a[1] = _a[2] = _a[2] = false;  }

 

bool set4c_a::find(char key)

{

    if ('a' <= key && key <= 'd')

    {

        int i = key - 'a';

        if (_a[i])

            return true;

    }

    return false;

}

 

void set4c_a::insert(char key)

{

    if ('a' <= key && key <= 'd')

    {

        int i = key - 'a';

        _a[i] = true;

    }

}

 

void set4c_a::erase(char key)

{

    if ('a' <= key && key <= 'd')

    {

        int i = key - 'a';

        _a[i] = false;

    }

}

 

void set4c_a::clear()

{

    _a[0] = _a[1] = _a[2] = _a[2] = false;   

}

 


//--------------------------------------------------------------------

// file: set_main

// using two different implementations of set4c

#include <iostream.h>

#include "set4c_n.h"  // we have multiple include of set4c.h

#include "set4c_a.h"  // threfore we have to use #ifndef ...

 

void main()

{  

    // set4c   s; - error: can't create object of abstract class

    set4c_n sn;

    set4c_a sa;

    show(sn);  // algorithms are independent of

    show(sa);  // concrete class.

    read(sn);  // even if we add new set4c class we

    read(sa);  // don't have to recompile set4c.cpp

    show(sn);

    show(sa);

    if (equal(sn, sa))

        cout << "sets are equal!" << endl;

    else

        cout << "sets are not equal!" << endl;

}

חזרה להתחלה

virtual destructor

·      בעיה: כאשר קיים מצביע למחלקת בסיס ומופעל  עליו   delete  אין קוראים להורס של המחלקה הנגזרת.

·      Text Box: // GOOD!!!
class B {
public:
    B() {} 
    virtual ~B() {}
};

class D : public B {
	int *a;
public:
	D() { a = new int[10]; }
	~D() { delete[] a; }
    void f(p2d c) {}
};


void main()
{
    B* p = new D;
    // calling ~D() here!
    delete p;  
}

Text Box: // BAD!!!
class B {
public:
	B() {} 
	~B() {}
};

class D : public B {
	int *a;
public:
	D() { a = new int[10]; }
	~D() { delete[] a; }
    void f(p2d c) {}
};


void main()
{
    B* p = new D;
    // ~D()  is not called here!
    delete p;  
}

פתרון: להגדיר את ההורס במחלקת הבסיסי כוירטואלי

 


חזרה להתחלה

 

העתקת עצם וירטואלית

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

 

class B {

    int a;

public:

     B(int a) : a(a) {}

    B(const B& b) : a(b.a) {}  // copy constructor

 

    virtual B* clone() const { return new B(*this); }

};

 

class D : public B {

   int n;

public:

   D(int n, int a) : B(a) , n(n) {}

   D(const D& d) : B(d),  n(d.n) {} // copy constructor

                    // a B& can reference to object of type D

 

   B* clone() const { return new D(*this); }

};

 

void main()

{

    B *p1, *p2;

    p1 = new D(1, 2);

    p2 = p1->clone();  // creates a copy of a D object.

}

 

חזרה להתחלה

 

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

·      אסור לרשת פעמיים מאותה המחלקה, כלומר אסור:     class A : public B, public B {};

·      אבל אפשר לקבל עקיפין את אותו  המקרה.

·      סטודנט וגם עובד הם סוג של אדם.

·      מתרגל Tutor  הוא גם סוג של עובד וגם סוג של סטודנט. כאילו יורש פעמיים את Person.

 

 

 

 

 

 

דוגמא:

#include <string.h>

#include <iostream.h>

class A {

protected:

   int x;

public:

   A(int a=0) : x(a) {}

};

 

class B : public A {

protected:

   char c;

public:

   B(char a = '?') : c(a) {}

};

 

class C : public A {

protected:

   float f;

public:

   C(float num = 3.6) : f(num) {}

};

 

class Derived: public B, public C {

protected:

   char name[40];

public:

   Derived(char string[] = "hello") { strcpy(name, string); }

   void display(void)

   {

/* output:

0

0

?

3.6

hello

*/

 

 
      cout << B::x << endl << C::x << endl;

      cout << c << endl;

      cout << f << endl << name << endl;

   }

};

 

void main()

{

   Derived d;

   d.display();

}

·      ללא שימוש ב  ::  היתה טעות הידור שכן הפונקציה  disply()  מבקשת להדפיס את  x  שזהותו דו משמעית.

 

 

class A {

public:

   virtual void f() {}

};

 

class B : public A {

public:

   virtual void f() { }

};

 

class C : public A {

public:

};

 

class Derived: public B, public C {

public:

        //fix that by: void f() { C::f(); /*B::f(); */ }

};

 

void main()

{

   Derived *dp = new Derived;

   dp->f(); // Compile Time Error! (ambiguous)

}

/* compiler Erroers:

     Derived::f' is ambiguous

     could be the 'f' in base 'B' of class 'Derived'

     or the 'f' in base 'A' of base 'C' of class 'Derived'

*/

·      המהדר איננו יודע לאיזו  f()  לפנות – זו שבאה מצד הבסיס A או מצד הבסיס B.

·      תיקון אפשרי הוא להצהיר על פונקציה  f()  ב  Derived, אשר תקרא לגרסא המתאימה.

חזרה להתחלה

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

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

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

class A {};

class B : public A {};

class C : public A {};

class D : public B , public C {};

·      ללא שימוש במחלקת בסיס וירטואלית המחלקה D מכילה שני עותקים של המחלקה  A.

·      לא ניתו לפנות ישירות למשתנה מתוך A  מבלי להשתמש ב B::   או  C::   בכדי להבחין בין שני המופעים.

 

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

·      כדי שהמחלקה היורשת תכיל רק עותק יחיד של מחלקת הבסיס משתמשים במחלקת בסיס וירטואלית.

 

class A {};

class B : virtual public A {};

class C : virtual public A {};

class D : public B , public C {};

q       כעת יש ל  C  רק עותק יחיד של  A.

·      ניתן לערבב מחלקות בסיס וירטואליות ולא וירטואליות

class A {};

class B : virtual public A {};

class C : virtual public A {};

class D : public A {};

class E : public B , public C, public D {};

 

q       כעת יש ל  E  שני עותקים של  A,  אחד מאוחד (של B ו C ) ואחד של D.

 

נראה מה קרה לדוגמא הקודמת אם משתמשים ב  virtual base class

#include <string.h>

#include <iostream.h>

class A {

protected:

   int x;

public:

   A(int a=0) : x(a) {}

};

 

class B : public A {

protected:

   char c;

public:

   B(char a = '?') : c(a) {}

   void display(void)  { cout << x << ' ' << c << endl; }

};

 

class C : public A {

protected:

   float f;

public:

   C(float num = 3.6) : f(num) {}

   void display(void) { cout << x << ' ' << f << endl; }

};

 

 

 

 

 

 

 

 

 

 

class Derived: public B, public C {

protected:

   char name[40];

public:

   Derived(char string[] = "hello") { strcpy(name, string); }

   void display(void)

   { cout << x << ' ' << c << ' ' << f << ' ' << name << endl; }

};

 

/* output:

b: 0 ?

c: 0 3.6

d: 0 ? 3.6 hello

*/

 

 
void main()

{

   B b;

   C c;

   Derived d;

   cout << "b: "; b.ddisplay();  

   cout << "c: "; c.ddisplay();

   cout << "d: "; d.ddisplay();

}

חזרה להתחלה

 

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

·      בירושה רגילה מחלקה מאתחלת את כל מחלקות הבסיסי שלה. וכל מחלקת בסיסי מאתחלת את הבסיס שלה וכו'.  אבל במקרה של מחלקת בסיס וירטואלית – כאילו רוצים לאתחל את הבסיס היחיד מספר פעמים.

דוגמא:

class A {

   int a;

public:

   A(int a) : a(a) {}

};

 

class B : virtual public A {

   int b;

public:

   B(int a, int b) : A(a), b(b) {}

};

 

class C : virtual public A {

   int c;

public:

   C(int a, int c) : A(a), c(c) {}

};

 

class D: public B, public C {

public:

   D(int a, int b, int c) : B(a, b), C(a, c) {}

};

 

void main()

{

   D  d(1, 2, 3); // compile Erorr: must initialize base A

}

·            בפועל המהדר לא מאתחל כלל את הבסיס   A  אלא משאירו לאא מאותחל (טעות הידור) פתרונות לזה:

1)      להגדיר  default constructor  ל  A:      A(int a=0) : a(a) {}

2)      או לאתחל את הבסיס המשותף A ישירות מתוך  D:  
D(int a, int b, int c) : A(a), B(a, b), C(a, c) {}

 

 

חזרה להתחלה

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

בתורשה וירטואלית פונקציה, שמוגדרת במחלקת הבסיסי המשותפת (וירטואלית) ואשר היא  overridden במחלקה הנגזרת, "נשלטת" על ידי האחרונה ואין כאן בעיות של רב משמעיות, כפי שראינו בדוגמא הקודמת.

class A {

public:

   virtual void f() {}

};

 

class B : virtual public A {

public:

   virtual void f() { }

};

 

class C : virtual public A {

public:

};

 

class Derived: public B, public C {

public:

        //fix that by: void f() { C::f(); /*B::f(); */ }

};

 

void main()

{

   Derived *dp = new Derived;

   dp->f(); // calling B::f() no ambiguity

}

חזרה להתחלה

  בקרת הגישה   public protected private

 

שם של member (משתנה או פונקציה) במחלקה יכול להיות  public, protected, private.

               ·      אם  private  מותר להשתמש בשם רק בתוך פונקציות חברות במחלקה או פונקציות שהוצהרו friend בתוך המחלקה.

               ·      אם  protected מותר להשתמש בשם בתוך פונקציות חברות במחלקה או פונקציות שהוצהרו friend בתוך המחלקה. בנוסף מותר להשתמש בשם בתוך פונקציות חברות במחלקות הנגזרות מהמחלקה או פונקציות שהוצהרו friend בתוך מחלקות שנגזרו מהמחלקה.

               ·      אם  public  אפשר להשתמש בשם בכל פונקציה

 

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

               ·      פונקציות ונתונים למימוש פנימי של המחלקה (private).

               ·      פונקציות ונתונים למימוש פנימי של מחלקות היורשות מהמחלקה (protected).

               ·      פונקציות ונתונים המשמשים כמנשק למחלקה  (public)  המיועד לשימוש כולם.

 

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

 

class X {

private:

   enum {A, B};

   void f(int);

   int a;

};

 

void X::f(int i)

{

   if (i < A) f(i + B);

   a++;

}

 

 

 

 

void g(X& x)

{

   int i = X::A; // ERROR: X::A is private.

   x.f(2);       // ERROR: X::f(int) is private.

   x.a++;        // ERROR: X::a is private.

}

 

class X {

// private by default

   int priv;

protected:

   int prot;

public:

   int publ;

   void m();

};

 

void X::m()

{

    priv = 1;  // OK

    prot = 2;  // OK

    publ = 3;  // OK

}

 

class Y : public X {

   void mderived();

};

 

void Y::mderived()

{

    priv = 1;  // ERORR: priv is private

    prot = 2;  // OK: prot is protected and mderived() is a member

               //     of the derived class Y

    publ = 3;  // OK: publ is public.

}

 

 

void f(Y* p)

{

    p->priv = 1;  // ERORR: priv is private

    p->prot = 2;  // ERROR: prot is protected and f()

                  // is not a friend or a member of X or Y

    p->publ = 3;  // OK: publ is public.

}

 

לסיכום
  1. בירושה מסוג  public  כאילו הגדרנו את הבסיס בחלק ה  public  במחלקה הנגזרת. לכן הרשאות הגישה לשמות בבסיס לא משתנות: מה שהיה private בבסיס אין אליו גישה מהמחלקה הנגזרת, מה שהיה protected  בבסיס יש גישה אליו רק מפונקציות של המחלקה הנגזרת ושל המחלקות הנגזרת ממנה ומה שהיה public  נשאר  public  גם במחלקה הנגזרת.
  2. בירושה מסוג  protected  כאילו הגדרנו את הבסיס בחלק ה  protected  במחלקה הנגזרת. לכן הרשאות הגישה לשמות בבסיס: מה שהיה private בבסיס אין אליו גישה מהמחלקה הנגזרת, מה שהיה protected  ו public בבסיס יש גישה אליו רק מפונקציות של המחלקה הנגזרת ושל המחלקות הנגזרת ממנה.
  3. בירושה מסוג  private  כאילו הגדרנו את הבסיס בחלק ה  private  במחלקה הנגזרת. לכן הרשאות הגישה לשמות: מה שהיה private בבסיס אין אליו גישה מהמחלקה הנגזרת, מה שהיה protected  ו public  בבסיס יש גישה אליו רק מפונקציות של המחלקה הנגזרת אך לא מהפונקציות של המחלקות היורשות מהמחלקה הנגזרת.

 

חזרה להתחלה

 

משתנה/פונקציה שהוא בבסיס

סוג תורשה

public

protected

private

public

public בנגזרת

protected בנגזרת

private בנגזרת

protected

protected בנגזרת

protected בנגזרת

private בנגזרת

private

איו גישה בנגזרת

איו גישה בנגזרת

איו גישה בנגזרת

 

חזרה להתחלה

 run-time binding     Vtbl

class Shape {

public:

    void SetPos(int xx, int yy) { x = xx; y = yy; }

    void SetColor(int c) { color = c; }

    virtual void Show() const {} // do nothing

 

private:

    int  x, y;

    long color;

};

Shape::vtbl

 
 

 

 

 

int  x, y;

long color;

 

 
 

 

 

 


Shape* p = new Shape;

p->Show();  // calls:  Shape::Show()

 

  1. לכל מחלקה המכילה פונקציה וירטואלית אחת או יותר, קיים נתון נוסף שהוא מצביע ל  vtbl. אם בוחנים את גודל המחלקה על ידי אופרטור  sizeof ניתן להבחין בכך.
  2. ברגע שנוצר עצם חדש מטיפוס  Shape,  המצביע  vtbl ptr  מקבל את הכתובת של טבלאת מיקומי הפונקציות הוירטואליות של המחלקה  Shape.
  3. קריאה לפונקציה וירטואלית נעשית תמיד דרך הטבלא  vtbl.
  4. אם יצרנו עצם מטיפוס  Shape  הרי הכתובת הנמצאת ב  vtbl ptr  היא של טבלאת הפונקציות הוירטואליות של  shape. לכן, אם יש לנו מצביע ל  Shape  ואנו מפעילים דרכו פונקציה וירטואלית Show()  הרי נגשים לטבלא ומפעילים פונקציה של  Shape::Show().

 

 

 

class PlanarShape : public Shape {

public:

    virtual double Area() const { return 0;}

PlanarShape::vtbl

 
};

 

 

 

 

 

 

 


PlanarShape* p = new PlanarShape;

Shape* p1 = p;

p1->Show();  // calls:  Shape::Show()

p->Area();   // calls:  PlanarShape::Aria()

 

class Rect : public PlanarShape {

public:

    virtual void Show() const {draw_rect(x, y, x+length, y+width);}

    virtual double Area() const { return length*width; }

private:

    int length, width;

};

Rect::vtbl

 
 

 

 

 

 

 

 

 

 

 


PlanarShape* p = new Rect;

Shape* p1 = p;

p1->Show();  // calls:  Rect::Show()

p->Area();   // calls:  Rect::Aria()

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

 

  1. אם קוראים לפונקציה וירטואלית בתוך גוף ה  constructor  תמיד הפונקציה המופעלת היא הפונקציה השייכת למחלקה של ה  constructor.
  2. הסיבה היא שמפעילים תחילה את פונקציות האתחול של מחלקת הבסיס. לכן, אם היינו קוראים לפונקציה הוירטואלית ה"נכונה" מתוך מחלקת הבסיס הרי היינו מפעילים פונקציה על עצם שעדיין לא אותחל.

class X {

public:

    X() { Show(); }

    virtual void Show() { cout << "this is X\n"; }

};

 

class Y : public X {

public:

    Y() { Show(); }

    virtual void Show() { cout << "this is Y\n"; }

};

 

void main()

{

    Y a;

    X* p = &a;

    p->Show();

}

 

OUTPUT:

this is X  // first X::X()

this is Y  // than  Y::Y()

this is Y  // "normal" virtual call.

 


חזרה להתחלה