תוכן הפרק
פונקציות וירטואליות טהורות ומחלקות אבסטרקטיות
שימוש במחלקה אבסטרקטית כמפרידה בין מימוש ל מנשק
פקודות למניעת הכללה מרובה #ifndef #define #endif
ריבוי אותה מחלקת בסיס בירושה מרובה
בנאי של מחלקות בסיס וירטואליות
דומיננטיות של מחלקת בסיס וירטואלית
בקרת הגישה public protected private
·
ירושה
מאפשרת להגדיר מחלקות (טיפוסים) בצורה היררכית, כלומר טיפוס הכללי ביותר (משותף
לכולם ולכן מכיל הכי פחות) ולאחר מכן מחלקות יותר ספציפיות (מכילות יותר פרטים)
·
כל
מחלקת בסיס היא הצורה הכללות של כל המחלקות היורשות ממנה - זאת כמובן אם הירושה
היא 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 שנכתבים רק על בסיס המנשק מבלי לדעת על
מהן המחלקות הקונקרטיות של העצמים שעליהם הם יופעלו.
·
כאשר
עובדים עם מספר קבצי H חייבים להגן עליהם בפני הכללה כפולה.
#ifndef שם יחודי לקובץ
#define שם יחודי לקובץ
.....
#endif
· כל הפקודות המתחילות ב # נקראות פקודות קדם מעבד. פקודות אלו עורכות את הטקסט של הקובץ לפני שהמהדר מתחיל לעבור עליו.
·
כזכור
#include
"file"
מכלילה את תוכן הקובץ במקום השורה.
·
השורה #ifndef name היא תנאי שפירושו: אם לא הוגדר
השם name תשאיר את כל הטקסט עד הסימן #endif ואם השם name כבר הוגדר מחק את כל הטקסט.
·
#define
name מגדירה את השם name עבור הקדם מעבד.
לכן
אם מבצעים #include מספר פעמים לאותו קובץ H הרי בגלל ההגנה נמחקים כל הקבצים, פרט
לראשון. השם AAA חייב להיות יחודי לכל קובץ H ולכן בוחרים אותו
דומה במשהו לשם הקובץ
#ifndef AAA
#define AAA
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;
}
·
בעיה:
כאשר קיים מצביע למחלקת בסיס ומופעל
עליו delete
אין קוראים להורס של המחלקה הנגזרת.
·
פתרון:
להגדיר את ההורס במחלקת הבסיסי כוירטואלי
·
בעיה: מצביע למחלקת בסיס מצביע לעצם ממחלקה
נגזרת. אני רוצה ליצור עותק של העצם המוצבע. אבל, אינני יודע בדיוק מהו סוג העצם.
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
}
שם
של 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.
}
סוג תורשה
|
||||
public בנגזרת |
protected בנגזרת |
private בנגזרת |
||
protected בנגזרת |
protected בנגזרת |
private בנגזרת |
||
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()
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()
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.