ראינו קודם כי יכולים להיות כמה מימושים שונים לאותו ה-.
בנוסף, מדי פעם נרצה להגדיר שני דברים שונים כתת דברים של יצור אחר.
הקונספט של הוא של טיפוסים ותתי טיפוסים ובזה אנו עומדים להתמקד עכשיו.
נניח ויש לנו יישום HR ויש לנו שני סוגים של עובדים: מהנדסים ואנשי מכירות.
נניח והיינו מתארים את שניהם במחלקה אחת, היינו מגיעים לבעיה, למהנדס יש תארים ולאיש המכירות אין בעוד שלאיש המכירות יש ולמהנס אין.
אם נכתוב מחלקה אחת, נגיע לבלבול:
class Employee
{
public:
// ...
string getName() const;
void giveRaise(int amout);
enum Type { ENGINEER, SALESPERSON };
private:
string name;
Date birth;
int salary;
Type type;
Set<string> degrees; // for an engineer
double comissionRate; // for a sales person
};
גם לכתוב שתי מחלקות לא היה טוב שכן רוב הקוד זהה ורוב המימוש זהה למעט כמה דברים אחדים. בנוסף, פה אנו לא רואים את הקשר בין שני המחלקות שהוא זה ששניהם עובדים של החברה. בנוסף, כל פונקציה שרוצה לקבל עובד צריכה ליצור שני העמסות.
את הבעיה הזו נרצה לתקן גם כן היום.
הפתרון הנאיבי הוא לצור מחלקה בשם ולשים שדה שלה במחלקות ו- ואז נשתמש בשיטות שלנו במחלקות החיצוניות יותר בשיטות של . (נניח של פשוט יקרא ל- של ) זה גם לא טוב במיוחד.
ב- נוכל להגדיר המרה אוטומטית בין ו- ל- ואז כל פונקציה שמשתמשת ב- נוכל לשלוח אליה מהמחלקות האחרות.
זה גם קצת מציק.
ירושה
הפתרון לבעיות שראינו עד כה הוא ירושה.
ירושה מתחלקת לשני חלקים: משהו שאנו מתחייבים בתור תת טיפוס ומה שאנו מקבלים במתנה בתור תת טיפוס.
class Employee
{
public:
// ...
string getName() const;
void giveRaise(int amount);
private:
string name;
Date birth;
int salary;
};
class Engineer : public Employee
{
public:
// engineer-only functions ...
private:
Set<string> degrees;
};
השימוש יהיה נחמד למדי:
void f(Engineer& engineer)
{
engineer.addDegree("B.Sc. Computer Science");
engineer.giveRaise(10);
}
תכף גם נראה למה חשוב לשלוח את המחלקה כרפרנס.
יש גם גרסה של ירושה עם אבל עליה לא נדבר בקורס.
המתנה שאנו מקבלים בתור תת טיפוס היא כל השדות והשיטות של טיפוס האב.
עבור שתי מחלקות כאשר יורשת מ- נגיד כי:
א. יורשת מ-.
ב. B derives from A
ג. היא מחלקת הבן ו- מחלקת האב.
ד. A is the base class and B is the derived class
ה. A is the superclass and B is the subclass.
ב- אנו נסמן ירושה עם ראש חץ ריק.
שליטה בגישה
במחלקת הבן, אנו לא יכולים לגשת לשדות והשיטות הפרטיות של מחלקת האב.
אך מה אם אנו רוצים לאפשר גישה לחלק מהשדות והשיטות אך ורק מהמחלקות שיורשות אך לא משאר העולם? נוכל לבצע את זה באמצעות סוג המידע וזה נראה כך:
class Employee
{
public:
string getName() const;
void giveRaise(int amount);
private:
string name;
Date birth;
protected:
int salary;
};
class Engineer : public Employee
{
public:
void addReview(string text, int score);
private:
Set<string> reviews;
};
void Engineer::addReview(string text, int score)
{
reviews.add(text);
if (score == 5)
salary += 1000;
}
void f(Engineer& engineer)
{
engineer.salary = 1000; // ERROR!!!!!
}
ב- בירושה אנו נדרשים להשתמש במצביעים ורפרנסים.
בכך, אם יורשת מ- אזי, ניתן להשתמש ב- כ- וניתן להשתמש ב- כ-. אך לא הפוך
Engineer engineer("Jane Doe");
SalesPerson salesPerson("John Doe");
Employee employee("John Smith");
Employee& ref = engineer; // ok
Employee* ptr = &salesPerson; // ok
Engineer* ptr2 = &employee; // ERROR!
Employee = copy = engineer; // compiles, but probebly a bug
Employee* array[] = {
&engineer, &salesPerson, &employee
};
בנאים במחלקות יורשות
בנאים לא עוברים בירושה ובנוסף הבנאי של הבן חייב לקרוא לבנאי של האב.
הסיבה לחלק הראשון היא שהבנאי היא אינה שיטה אלא יצור אחר, היא בעצם שיטה של המחלקה ולא של האובייקט. (קיימות שפות שבהן יש ירושה של הבנאים - היא לא אחת מהן)
Employee::Empyloyee(string name)
: name(name), salary(STARTING_SALARY)
{}
Engineer::Engineer(string name)
: Employee(name), degrees()
{}
אם קיים למחלקת האב בנאי ברירת מחדל ואנו לא קוראים לבנאי בבנאי של הבן, הבנאי ברירת המחדל יקרא.
הדרך היחידה למנוע ירושה ב- היא למנוע קיום של בנאים ציבוריים(בנאי פרטי זה סבבה)
כמו לכל מחלקה, גם למחלקה יורשת יש בנאי העתקה ואופרטור השמה באופן אוטומטי, אשר קוראים לבנאי ההעקתה ואופרטור ההשמה של מחלקת האב ואז ממשיכים כמו בנאי העתקה ואופרטור השמה ברירת מחדל רגילים.
הורסים במחלקות יורשות
גם הורס ברירת מחדל נוצר אוטומטית במחלקות יורשות, רק שפה, קודם כל נקרא הקוד בהורס של המחלקה היורשת, אז נקראים ההורסים של שדות המחלקה היורשת ואז נקרא ההורס של המחלקה של האב(ואז נהרסים השדות של מחלקת האב)
ההבדל פה מבנאים, הוא שלא צריך לקרוא ידנית להורס של האב.
Engineer::~Engineer()
{
// ...
// no need to call ~Employee()
}
class Engine
{
public:
Engine() { cout << "Engine ctor" << endl; }
~Engine() { cout << "Engine dtor" << endl; }
};
class Battery
{
public:
Battery() { cout << "Battery ctor" << endl; }
~Battery() { cout << "Battery dtor" << endl; }
};
class Car
{
Engine engine;
public:
Car() { cout << "Car ctor" << endl; }
~Car() { cout << "Car dtor" << endl; }
};
class ElectricCar : public Car
{
Battery battery;
public:
ElectricCar() { cout << "ElectricCar ctor" << endl; }
~ElectricCar() { cout << "ElectricCar dtor" << endl; }
};
// MAIN
int main()
{
ElectricCar car;
cout << "========" << endl;
return 0;
}
עבור הקוד הנ"ל נקבל את הפלט:
Engine ctor
Car ctor
Battery ctor
ElectricCar ctor
========
ElectricCar dtor
Battary dtor
Car dtor
Engine dtor
דריסה של שיטות
מה אם אנו רוצים דרך שונה לחשב את ההעלה של כל סוג של עובד?
נוכל לדרוס את השיטה במחלקות הבנים ובכך להשיג את ההתנהגות שאנו רוצים.
void Employee::giveRaise(int amount)
{
salary += amount;
}
class SalesPerson : public Employee
{
public:
// ...
void giveRaise(int amount);
};
void SalesPerson::giveRaise(int amount)
{
Employee::giveRaise(amount + comissionRate * sales);
}
לא ניתן לדרוס שדות!!!!
אך מה קורה אם יש לנו מערך למצביעים של , ואנו קוראים על אחד האיברים, איזו שיטה צריכה להיקרא? הזאת של או הזאת של המחלקה היורשת המתאימה?
פתרון ראשון
הפתרון הראשון די מגעיל והוא דורש השמה בתצורה כזו:
Employee* employees[] = { ... };
for (int i = 0; i < n; ++i)
{
Employee *e = employees[i];
((SalesPerson*)e)->giveRaise(100);
}
זה מגעיל ויכול ליצור מלאאא בגים! אנו לא רוצים את הפתרון הראשון.
דרך לפתור את הבעיות עם המצביעים היא להשתמש ב- שהוא החלפה טובה יותר של השמה של ביצור חדיש יותר של . בתצורה זו, השמה בין שני דברים לא קשורים לא תתקמפל.
Employee* employees[] = { ... };
for (int i = 0; i < n; ++i)
{
if (employees[i]->getType() == "manager")
static_cast<SalesPerson*>(cmployees[i])->giveRaise(100);
}
זה יותר טוב, אך אנו עדיין לא אוהבים את זה כל כך.
הפתרון הנכון: שיטות וירטואליות
במקום לבדוק לאיזה סוג של עובד אנו רוצים לקרוא, אנו נגיד למהדר לבחור בשבילנו ע"י הגדרת פונקציה ורטואלית.
פונקציה וירטואלית היא בעצם אמירה למהדר שקריאה לפונקציה היא דרך פוינטרים והיא דורשת לבדוק את מחלקות הבנים.
הדבר הזה עובר דרך מצביעים לפונקציות.
class Employee
{
public:
// ...
virtual void giveRaise(int amount);
};
class SalesPerson : public Employee
{
public:
// ...
void giveRaise(int amount);
};
int func()
{
SalsesPerson salesPerson("John Doe");
Employee* emp = &salesPerson;
emp->giveRaise(1000);
return 0;
}
ניתן לא לדרוס שיטות במחלקת הבן, ואז נקראת אוטומטית השיטה של מחלקת האב.
המילה השמורה
כדי להבטיח שכאשר אנו דורסים שיטה אנו מתכוונים לדרוס אותה, אנו יכולים להוסיף לסוף ההצהרה את המילה השמורה .
לא חייבים לעשות את זה, אבל בקורס הזה חייבים לעשות את זה.
class Employee
{
public:
virtual void giveRaise(int amount);
};
class QASpecialist : public Employee
{
public:
// ...
void giveRaise(int amount) override;
};
מחלקה אבסטרקטית
בהרבה מקרים מחלקת האב מייצגת קונספט אבסטרקטי שאין לו מימוש בפני עצמו.
במקרים כאלו אנו יכולים להגדיר פונקציה וירטואלית לגמרי() אשר אין לה מימוש במחלקת האב.
class Employee
{
public:
// ...
virtual void giveRaise(int amount) = 0;
};
class SalesPerson : public Employee
{
public:
// ...
void giveRaise(int amount) override;
};
מחלקה אשר יש לה לפחות שיטה וירטואלית לגמרי אחת נקראת אבסטרקטית.
לא ניתן לייצר אובייקטים של מחלקות אבסטרקטיות בפני עצמם, אלא רק דרך הבנים של המחלקה.
ניתן לייצר פויינטרים אל מחלקות אבסטרקטיות.
אם הבן לא מממש את הפונקציה הוירטואלית לגמרי, אז גם אי אפשר ליצור אובייקט שלנו ונדרש ליצור אותו דרך הילדים שלו, שהם הנכדים של מחלקת האב.
נזכיר כי בנאים הם לא באמת שיטה ולכן הם אף פעם לא וירטואליים.
⟸ אין משמעות לבנאי וירטואלי שכן אין וירטואליות בייצור של משהו ידוע.
לעומת זאת, הורס תמיד ורטואלי שכן קודם נקרא ההורס של האב ואז של הבן.
⟸ תמיד תצהירו על ההורס כוירטואלי
class Employee
{
// ...
public:
virtual ~Employee() {}
// ...
};
מחלקת ממשק -
מחלקה אבסטרקטית נקראת ממשק אם יש לך אך ורק פונקציות וירטואליות לגמרי(ללא מימוש).
ממשק מתאר את ההתנהגות הבסיסית של מחלקה מבלי לתאר איזשהו מימוש מדוייק.
class Shape
{
public:
virtual ~Shape() {}
virtual void rotate(double angle) = 0;
virtual void draw() const = 0;
virtaul double area() const = 0;
};
אנו דיברנו כבר על שכן דיברנו כבר על תכנות גנרי ואמרנו שזה .
בקורס הזה אנו קוראים ל- פשוט .
הרעיון של פולימורפיזם הוא פשוט שכאשר אנו יוצרים מחלקה יורשת חדשה, שום חלק במחלקת האב לא צריך להשתנות.
כל קוד שקשור לתת קטגוריה ספציפית נמצא אך ורק במחלקה של התת קטגוריה.
פולימורפיזם ובנאי העתקה
נזכר כי אם יש לנו מחלקת אב ומחלקת בן ואנו כותבים את הקוד הבא:
Engineer eng("Bjarne Stroustrup");
Employee emp = eng;
הבנאי שנקרא בשורה השנייה הוא בנאי ההעתקה של ולכן, כל הדברים שקשורים ספציפית ל- נעלמים כאילו לא היו. גם השיטות שיקראו הן המימושים של ולא של .
בפוינטרים אין את הבעיה הזו, אך איך אנו יכולים לבצע העתקה של אובייקט ללא קריאה לבנאי ההעתקה?
(אנו לא רוצים לקרוא לבנאי ההעתקה כי אנו לא בטוחים בדיוק מאיזה סוג האובייקט, שכן זוהי כל הפואנטה של )
כדי לפתור בעיה זאת אנו ניצור שיטה וירטואלית לגמרי בשם אשר תחזיר פוינטר לאובייקט.
היא תראה משהו כזה:
class Shape
{
// ...
public:
virtual Shape* clone() const = 0;
// ...
};
class Circle : public Shape
{
// ...
public:
Circle(double radius);
Shape* clone() const override;
// ...
};
Shape* Circle::clone() const
{
return new Circle(*this);
}
int main()
{
Shape* ptr = new Circle(0,0,3);
Shape* copy = ptr->clone();
}
כשצריך סוג ספציפי של יצור
לפעמים אנו רוצים לדעת באיזה סוג מחלקה אנחנו, נניח אם יש לנו פעולה שמתעסקת עם שדות מסויימים של תת מחלקה מסויימת (נניח אם יש לנו מחלקת אב של יחס ומחלקת בן של פונקציה ואנו רוצים לעשות משהו רק עבור פונקציות)
נשים לב כי אם יש לנו מצביע של אנו לא נוכל לקרוא לשיטה שמוגדרת רק ב- שכן אנו לא ידועים אם זה באמת מהנדס או לא.
משהו נוסף שלא נעשה הוא להוסיף שיטה אשר בעזרתה נבדוק איזה סוג זה כי זה מגעיל.
הפתרון לבעיה המוצעת פה היא שימוש בסוגים דינאמים וב-.
ה- משים אובייקט מסוג של מחלקת אב לאובייקט מסוג טיפוס של הבן והיא תחזיר אם זה לא הגיוני
void printDegrees(const Employee* employees[], int n)
{
for (int i = 0; i < n; ++i)
{
const Engineer* eng = dynamic_cast<const Engineer*>(employees[i]);
if (eng != nullptr)
{
eng->printDegrees();
}
}
}
ה- יעבוד אך ורק אם במחלקת האב יש פונקציה ורטואלית אחת לפחות.
סיכום השמות()
ראינו כי לבצע השמות כמו שעשינו ב- זה לא טוב כלל וכלל!!!!
const double height = 175.3;
int* height_ptr = (int*) &height; //NOOOOOOOOO
יש לנו ב- שני סוגים של השמות:
- מאפשר לנו להמיר בין שני סוגים שידועים בזמן ההמרה, כלומר, לא מחלקות עם ירושה.
כדאי להשתמש באלו רק עבור טיפוסים פרימיטביים. - מאפשר לנו להמיר בין שני מצביעים או רפרנסים שיש להם איזשהו קשר ירושתי שכזה.
האחד הזה נבדק במהלך זמן הריצה.
ירושה ושגיאות
ראינו שגיאות באחד מהפרקים הקודמים וכעת נוכל לשפר את יכולת ההתמודדות שלנו עם שגיאות.
בבלוק התפיסה של סוג מתחשב גם בכל הבנים שלו, נניח אם אנחנו תופסים זה יתפוס גם ו- וכו'.
class GradesException
{
public:
virtual ~GradesException() {}
};
class NotEnrolled : public GradesException {};
class NoSuchStudent : public GradesException {};
void GradesSystem::printGrade(string student, int course)
{
try
{
cout << "grade is: " getGrade(student, course);
}
catch (const NotEnrolled&)
{
cerr << studentName << " is not registered";
}
catch (const GradesException&)
{
cerr << "Data not avilable";
}
}
בספריה הסטנדרטית מוגדרות לנו שגיאות טיפוסיות וכדאי לבצע מהן ירושה עבור השגיאות שלנו.
class exception
{
public:
exception();
exception(const exception&);
exception& operator=(const exception&);
virtual ~exception();
virtual const char* what() const noexcept; // noexcpet says there wont be an exception comming from this function
};
יש לנו שני סוגים בסיסיים של שגיאות:
- - מייצג את השגיאות שהקורא לפונקציה יכול היה לגלות עליהן, לדוגמה: .
- - מייצג את השגיאות שקורות כאשר הפונקציות רצות ולא היה ניתן לדעת עליהן בהקדם(שגיאות משתמש נניח)
הסיווג הזה לא מוגדר היטב ולכן לא כל כך חשוב.