ΚΛΗΡΟΝΟΜΙΚΟΤΗΤΑ ΚΑΙ ΠΟΛΥΜΟΡΦΙΣΜΟΣ Γεώργιος Παπαϊωάννου (2013-14) gepap@aueb.gr
Περιγραφή: Επέκταση κλάσεων στη C++ Τροποποίηση μεθόδων (method overriding) Κληρονομικότητα και κατασκευαστές, καταστροφείς, τελεστές Virtual Μέθοδοι Αφηρημένες κλάσεις (abstract classes) Πολλαπλή κληρονομικότητα (multiple inheritance) Τελευταία ενημέρωση: Αύγουστος 2013 Εισαγωγή - 2
class Point { private: int x, y; void setx(int newx) { x = newx; void sety(int newy) { y = newy; int getx() const { return x; int gety() const { return y; void display() const { cout << "x: " << x << ", y: " << y << endl; Point(int newx = 0, int newy = 0): x(newx), y(newy) { Point(const Point& original): x(original.x), y(original.y) { int main() { Point p1(1, 2); p1.display(); 3
class LabeledPoint : public Point { string label; // Πρόσθετη μεταβλητή-μέλος. // Νέες μέθοδοι: void setlabel(const string& newlabel) { label = newlabel; string getlabel() const { return label; LabeledPoint(int newx = 0, int newy = 0, // Κατασκευαστής. const string& newlabel = "label"); int main() { LabeledPoint p2(1, 2, "test"); p2.setx(3); p2.sety(4); p2.setlabel("other"); cout << p2.getlabel() << endl; p2.display(); // Κληρονομημένες μέθοδοι. // Πρόσθετη μέθοδος. // Τυπώνει «other». // Κληρονομημένη. Δεν τυπώνει το label. 4
class LabeledPoint : public Point { string label; void setlabel(const string& newlabel) { label = newlabel; string getlabel() const { return label; LabeledPoint(int newx = 0, int newy = 0, const string& newlabel = "label"); void display() const; // Τροποποίηση μεθόδου. // (Υπάρχει και στην Point.) // Τώρα τυπώνεται και το label: void LabeledPoint::display() const { cout << "label: " << label << ", "; // cout << x << y; // Λάθος. Ιδιωτικά μέλη της Point. // cout << getx() << gety(); // ΟΚ, αλλά καλύτερα το ακόλουθο: Point::display(); // Κλήση της αρχικής μορφής της μεθόδου. 5
LabeledPoint::LabeledPoint(int newx, int newy, const string& newlabel) : Point(newX, newy), // Κλήση του κατασκευαστή της Point. label(newlabel) // Αρχικοποίηση μεταβλητής. { // x = NewX; // Λάθος. Δεν έχουμε πρόσβαση στα ιδιωτικά μέλη της Point. // y = NewY; Κατά τη δημιουργία ενός αντικειμένου της παράγωγης κλάσης, καλείται πρώτα ο κατασκευαστής του προγόνου. Εάν ο κατασκευαστής της παράγωγης κλάσης δεν περιέχει ρητή κλήση κατασκευαστή του προγόνου, καλείται ο προεπιλεγμένος κατασκευαστής του. Στο συγκεκριμένο παράδειγμα, χωρίς ρητή κλήση κατασκευαστή της Point, όλα τα αντικείμενα LabeledPoint θα είχαν x = 0, y = 0. 6
Η κληρονομικότητα δεν είναι ο μόνος τρόπος να επαναχρησιμοποιήσουμε μια υπάρχουσα τάξη. Η κληρονομικότητα αντιστοιχεί σε σχέσεις is-a. Ένα αντικείμενο LabeledPoint είναι ένα είδος Point. Ένα αυτοκίνητο είναι ένα είδος οχήματος. Συχνά μας χρειάζονται σχέσεις has-a (ή part-of). Ένα αντικείμενο Line περιέχει δύο αντικείμενα Point. Ένα αυτοκίνητο έχει τέσσερις ρόδες. 7
class Line { Point start, end; string label; Line(int x1, int y1, int x2, int y2, const string& s) : start(x1, y1), end(x2, y2), label(s) { void display() const { cout << label << endl; start.display(); end.display(); int main() { Line myline(10, 20, 40, 50, "test"); myline.display(); 8
Η κλήση των κατασκευαστών γίνεται φωλιασμένα (base class derived class) Η κλήση των καταστροφέων γίνεται κι αυτή φωλιασμένα, αλλά με την αντίστροφη σειρά (derived class base class) Αν δε δηλώνεται διαφορετικά, καλείται ο default constructor της κλάσης βάσης Αν δεν έχει οριστεί default constructor και έχουμε δηλώσει κάποιον κατασκευαστή με ορίσματα, πρέπει να καλέσουμε αυτών (από τη λίστα αρχικοποίησης) Ο καταστροφέας μιας βασικής κλάσης καλείται αυτόματα 9
class A { A() {cout<<"a+"; ~A() {cout<<"a-"; class B : public A { B() {cout<<"b+"; ~B() {cout<<"b-"; void main( int argc, char* argv[]) { B * b = new B(); delete b; Έξοδος: Α+Β+Β-Α- Δέσμευση Μνήμης: (Β * b = new B()) Κλήση Κατασκευαστή Προγόνου: A() Κλήση Κατασκευαστή: B() Κλήση Καταστροφέα: ~B() Κλήση Καταστροφέα Προγόνου: ~A() Αποδέσμευση Μνήμης 10
Είναι καλή πρακτική η κάθε κλάση να δεσμεύει τη μνήμη για τα δικά της πεδία (όπου απαιτείται) και να τα αποδεσμεύει η ίδια Δηλαδή να μην περιμένει ότι κάποια κλάση απόγονος θα αναλάβει αυτή τη δουλειά 11
class A { char * buffer; A(int size) { buffer= new char[size]; ~A() { delete [] buffer; class B : public A { B() : A(10) { ~B() { class A { protected: A() { ~A() { char * buffer; class B : public A { B(int size) : A() { buffer = new char[size]; ~B() { delete [] buffer; class A { char * buffer; A(int size) {buffer = new char[size]; ~A() {delete [] buffer; class B : public A { B() : A(10) { ~B() {delete [] buffer; 12
Αν έχουμε κατασκευαστή αντιγραφής σε μια παράγωγη κλάση, τότε καλείται ο αντίστοιχος κατασκευαστής αντιγραφής της βασικής κλάσης Ο κατασκευαστής αντιγραφής της βασικής κλάσης χρησιμοποιεί μόνο το τμήμα της παράγωγης κλάσης που αντιστοιχεί στα πεδία της βασικής κλάσης για την αρχικοποίησή του 13
class A { char * buffer; int size; A(const int size) : size(size) { buffer = new char[size]; A(const A & src) : size(src.size) { buffer = new char[size]; ~A() {delete [] buffer; class B : public A { char fill_char; B(const int size, const char fill) : A(size), fill_char(fill) { B(const B & src) : fill_char(src.fill_char), ~B() { A(src){ char * buffer int size char * buffer int size char fill_char Πεδία της Β Πεδία της Α 14
Όπως είδαμε, μπορούμε να τροποποιήσουμε μια μέθοδο μιας παράγωγης κλάσης. Όμως: Η καινούρια μέθοδος ανήκει στην παράγωγη κλάση και είναι στατικά συσχετισμένη μαζί της. Κοινώς: Συνυπάρχει η μέθοδος της βασικής κλάσης με την μέθοδο της παράγωγης κλάσης! Ανάλογα με το πώς είναι δηλωμένο ένα στιγμιότυπο μια δεδομένη στιγμή, θα κληθεί και η αντίστοιχη μέθοδος 15
class Person { string name; Person(const string& namein) : name(namein) { void print() const { cout << name ; class Student : public Person { string code; Student(const string& namein, const string& codein) : Person(nameIn), code(codein) { void print() const { Person::print(); cout << code; 16
int main() { Person p("νίκος"); p.print(); // Τυπώνει «Νίκος». Student s("γιώργος", "p30100"); s.print(); // Τυπώνει «Γιώργοςp30100». Student* p_student = &s; Person* p_person = &s; // Δείκτης Student* // Δείκτης Person* που δείχνει // σε ένα Student (το s). // Επιτρέπεται γιατί η Student // είναι παράγωγη της Person. p_person->print(); // Τυπώνει «Γιώργος». Χρησιμοποιεί την // print της Person, παρ' όλο που ο // δείκτης δείχνει σε αντικείμενο // Student. 17
Ένας δείκτης βασικής κλάσης (π.χ. Person*) επιτρέπεται να δείχνει σε αντικείμενο παράγωγης κλάσης (π.χ. Student). Αν όμως κληθεί χρησιμοποιώντας το δείκτη της βασικής κλάσης μια μέθοδος (π.χ. print) που τροποποιήθηκε στην παράγωγη κλάση, χρησιμοποιείται η μορφή που είχε η μέθοδος στη βασική τάξη. Δηλαδή το ποια μορφή της μεθόδου θα χρησιμοποιηθεί καθορίζεται από τον τύπο του δείκτη. Συχνά θέλουμε, όμως, να καθορίζεται από τον τύπο (κλάση) του αντικειμένου στο οποίο δείχνει ο δείκτης. Για να συμβεί αυτό πρέπει να δηλώσουμε τη μέθοδο ως εικονική (virtual). 18
class Person { string name; Person(const string& namein) : name(namein) { virtual void print() const { cout << name ; class Student : public Person { string code; Student(const string& namein, const string& codein) : Person(nameIn), code(codein) { // Δε χρειάζεται να ξαναγράψουμε virtual: void print() const { Person::print(); cout << code; 19
int main() { // Η print είναι τώρα εικονική. Person p("νίκος"); Student s("γιώργος", "p30100"); Person* p_person = &s; // Δείκτης Person* που δείχνει στο s. p_person->print(); // Τώρα τυπώνει «Γιώργοςp30100». // Δηλαδή χρησιμοποιεί την // print της Student. // Το ποια μέθοδος θα // χρησιμοποιηθεί το καθορίζει τώρα ο // τύπος του αντικειμένου, όχι ο τύπος // του δείκτη. 20
Στην απλή τροποποίηση μεθόδου, το ποια μέθοδος θα κληθεί καθορίζεται κατά τη μεταγλώττιση του κώδικα με βάση τον τύπο της μεταβλητής Όταν μια μέθοδος είναι virtual, η πραγματική μέθοδος που θα κληθεί καθορίζεται κατά την εκτέλεση Για να μπορέσει να γίνει αυτό, όταν μεταγλωττίζεται ο κώδικας αντί να προσδιοριστεί η θέση στη μνήμη (Offset) που βρίσκεται η μέθοδος προς κλήση, δηλώνεται μια μεταβλητή «δείκτη προς μέθοδο», το περιεχόμενο της οποίας καθορίζεται με τη δέσμευση του αντικειμένου 21
Non-virtual print() method class Person Κώδικας της print() main () Στιγμιότυπο s (Student) string name string code class Student pointer to base class Κώδικας της print() Κλήση συνάρτησης ( Person::print() ) int main() { Student s("γιώργος", "p30100"); Person* p_person = &s; p_person->print(); 22
Virtual print() method class Person Κώδικας της virtual print() class Student pointer to base class Κώδικας της virtual print() main () Κλήση συνάρτησης (p_print) int main() { Στιγμιότυπο s (Student) (void*)(void) p_print string name string code Student s("γιώργος", "p30100"); Person* p_person = &s; p_person->print(); 23
H τροποποιημένη μέθοδος δεν επισκιάζει πάντα την μέθοδος της βασικής κλάσης. Αυτό εξαρτάται από το αν μια μέθοδος είναι εικονική ή όχι Όλες οι τροποποιημένες μέθοδοι είναι εικονικές Εμείς καθορίζουμε πότε μια μέθοδος είναι εικονική Το ποια μέθοδος θα κληθεί καθορίζεται κατά την εκτέλεση του κώδικα μόνο για τις εικονικές μεθόδους Το ποια μέθοδος θα κληθεί καθορίζεται πάντα κατά την εκτέλεση του κώδικα Για τις υπόλοιπες, καθορίζεται κατά τη μεταγλώττιση 24
Είδαμε παραπάνω πως ο compiler προκειμένου να υλοποιήσει το μηχανισμό των virtual μεθόδων, δημιουργεί δεδομένα μέσα στα στιγμιότυπα τύπου «δείκτη σε συνάρτηση» (μέθοδο) Αυτό γενικά επιτρέπεται στη C και στη C++: Μπορούμε να δηλώνουμε δείκτες σε συναρτήσεις ως μεταβλητές Μπορούμε να περνάμε ως ορίσματα και να επιστρέφουμε δείκτες σε συναρτήσεις Μπορούμε να χειριζόμαστε κανονικά τους δείκτες σε συναρτήσεις ως δεδομένα 25
Δήλωση δείκτη σε συνάρτηση: RETURN_VAL (*POINTER_NAME) (ARG_LIST) Παράδειγμα: void printdecorated(string name) {cout << \ <<name<< \ ; void printundecorated(string name) {cout <<name; bool use_decoration = false; cin >> use_decoration; void (*print_func)(string) = nullptr; printf_func = use_decoration? printdecorated : printundecorated; // Έχουμε ορίσει μια κλάση StilizedOutput με κατασκευαστή που // παίρνει ως όρισμα void (*)(string) StilizedOutput * output = new StilizedOutput(printf_func); output->print( Nikolas ); output->print( Maria ); 26
27 void display(const Person& pers) { pers.print(); // Η print είναι εικονική. Το ποια print θα // χρησιμοποιηθεί το καθορίζει ο τύπος του // αντικειμένου, όχι ο τύπος της αναφοράς. int main() { Person p("νίκος"); Student s("γιώργος", "p30100"); display(p); // Τυπώνει «Νίκος». display(s); // Τυπώνει «Γιώργοςp30100».
void display(person pers) { pers.print(); // Η print είναι εικονική, αλλά κατά τη // μεταβίβαση δημιουργείται ένα τοπικό // αντίγραφο του pers που είναι Person, // ακόμα κι αν το αρχικό ήταν Student. // Το τοπικό αντίγραφο δεν έχει τα // επιπλέον μέλη της Student). // Το ποια μορφή της print() θα // χρησιμοποιηθεί το καθορίζει ο τύπος του // τοπικού αντιγράφου (Person). int main() { Person p("νίκος"); Student s("γιώργος", "p30100"); display(p); // Τυπώνει «Νίκος». display(s); // Τυπώνει «Γιώργος», σαν να ήταν το s Person. 28
Όπως αναφέρθηκε, οι καταστροφείς των κληρονομημένων κλάσεων καλούν αυτόματα αναδρομικά και τους καταστροφείς των προγόνων Τι γίνεται αν δε θέλουμε να συμβεί αυτό; Σε ποιες περιπτώσεις μπορεί να μη θέλουμε; Το πρόβλημα προκύπτει συνήθως όταν: Καταστρέφουμε αντικείμενο που έχει δηλωθεί ως πρόγονος της πραγματικής κλάσης που είναι 29
class Employee { string name; Employee (string e_name) {name=e_name; class Manager : public Employee { Employee ** employees; unsigned int num_employees; Manager (string m_name) : Employee(m_name), employees(nullptr) { void addsubordinate(employee * employee) {... ~Manager() {delete [] employees; // πρέπει να διαγράψω τη λίστα // των υπαλλήλων όταν καταστρέψω // το αντικείμενο τύπου Manager 30
int main() { Employee * staff[3]; staff[0] = new Employee( Μαρία ); staff[1] = new Employee( Γιάννης ); staff[2] = new Manager( Δήμητρα ); staff[2]->addsubordinate(staff[0]); staff[2]->addsubordinate(staff[1]);... for (int i=0; i<3; i++) { delete staff[i]; Δε διαχωρίζεται από την αρχή ο ακριβής τύπος του «υπαλλήλου». Έχω μια κοινή λίστα με όλους Δημιουργώ ένα στιγμιότυπο derived class του οποίου όμως ο δηλωμένος τύπος παραμένει Employee Για όλα θα κληθεί ο καταστροφέας του δηλωμένου τύπου: ~Employee() Δε θα κληθεί ο καταστροφέας της derived class Manager για τη μεταβλητή staff[2] Δε θα αποδεσμευθεί ποτέ η μνήμη του πεδίου Manager::employees 31
class Employee { string name; Employee (string e_name) {name=e_name; virtual ~Employee() { // μπορώ να το δηλώσω είτε εδώ είτε // στον απόγονο που πραγματικά // χρειάζεται το virtual καταστροφέα class Manager : public Employee { Employee ** employees; unsigned int num_employees; Manager (string m_name) : Employee(m_name), employees(nullptr) { void addsubordinate(employee * employee) {... ~Manager() {delete [] employees; // Ο καταστροφέας έχει δηλωθεί // virtual στον πρόγονο οπότε // συνεχίζει να ισχύει η δήλωση 32
int main() { Employee * staff[3]; staff[0] = new Employee( Μαρία ); staff[1] = new Employee( Γιάννης ); staff[2] = new Manager( Δήμητρα ); staff[2]->addsubordinate(staff[0]); staff[2]->addsubordinate(staff[1]);... for (int i=0; i<3; i++) { delete staff[i]; Τώρα πλέον θα κληθεί ο καταστροφέας της κλάσης που πραγματικά είναι το staff[2] 33
Αν θέλετε μια τάξη να μπορεί να χρησιμοποιηθεί ως βασική για παράγωγες τάξεις, κάντε τον καταστροφέα της εικονικό. Έστω κι αν το σώμα του είναι κενό. Προσοχή: Οι κατασκευαστές απαγορεύεται να είναι εικονικοί. Αν θέλετε μια μέθοδος να μπορεί να τροποποιηθεί σε παραγόμενες τάξεις, κάντε την εικονική. Δεν είναι προεπιλογή της C++, επειδή η χρήση εικονικών μεθόδων κάνει τον εκτελέσιμο κώδικα πιο αργό και απαιτεί περισσότερη μνήμη. 34
Οι καθαρά εικονικές μέθοδοι δεν έχουν κυρίως μέρος. Υπόσχονται ότι θα οριστούν σε παράγωγες κλάσεις Μια κλάση που περιέχει τουλάχιστον μια καθαρά εικονική μέθοδο λέγεται αφηρημένη Δεν επιτρέπεται η δημιουργία αντικειμένων αφηρημένων κλάσεων Οι αφηρημένες κλάσεις χρησιμοποιούνται συχνά ως προδιαγραφές (Interfaces) ειδικότερων κλάσεων: Μπορούμε να προδιαγράψουμε στις αφηρημένες κλάσεις μεθόδους που είναι κοινές στις παράγωγες κλάσεις τους. 35
// Αφηρημένη κλάση: προδιαγράφει τη μορφή των μεθόδων και ενδεχομένως, // των πεδίων, αλλά δε δίνει συγκεκριμένη υλοποίηση, ούτε καν κενή: // Υποχρεώνει τις παράγωγες κλάσεις να προσδιορίσουν την υλοποίηση class Vehicle { protected: float horsepower; virtual void calculatehorsepower() = 0; Class Car : public Vehicle { calculatehorsepower() { // Συγκεκριμένη υλοποίηση μεθόδου... // του (interface) Vehicle Class Truck : public Vehicle { calculatehorsepower() { // Συγκεκριμένη υλοποίηση μεθόδου... // του (interface) Vehicle 36
Στη c++ επιτρέπεται η πολλαπλή κληρονομικότητα κλάσεων: Μια κλάση μπορεί να προέρχεται από πάνω από μία ισότιμες κλάσεις Η παράγωγη κλάση κληρονομεί τα πεδία και τις μεθόδους όλων των βασικών κλάσεων Μία παράγωγη κλάση μπορεί να προέρχεται από δύο ή παραπάνω κλάσεις με κοινό πρόγονο Σε περίπτωση ταυτόσημων μεθόδων και πεδίων, ορίζεται σαφής μηχανισμός για το ποιάς βασικής κλάσης τις μεθόδους καλούμε ανά πάσα στιγμή 37
class A { /*... */ class B { /*... */ class C { /*... */ class X : public A, private B, public C { /*... */ class L { /*... */ // indirect base class class B2 : public L { /*... */ class B3 : public L { /*... */ class D : public B2, public B3 { /*... */ class L { /*... */ // indirect base class class B2 : virtual public L { /*... */ class B3 : virtual public L { /*... */ class D : public B2, public B3 { /*... */ 38
Αν κατά την πολλαπλή κληρονομικότητα προκύπτει κοινή κλάση από 2+ κληρονομικά μονοπάτια, τότε το κάθε μονοπάτι θεωρεί δική του έκδοση της κοινής κλάσης, Εκτός αν ορίσουμε τον κοινό πρόγονο ως virtual class, οπότε όλες οι κλάσεις αναφέρονται στην ίδια κοινή βασική κλάση virtual virtual 39
Οι παράγωγη κλάση κληρονομεί την ένωση των πεδίων και μεθόδων των προγόνων Αν υπάρχει κοινή ονομασία πεδίων και υπογραφή συναρτήσεων, εξειδικεύουμε την κλάση στην οποία αναφερόμαστε με:.classname::method() για μεθόδους και.classname::field για πεδία (παράδειγμα στην επόμενη διαφάνεια) Συνεχίζουμε να μπορούμε να τροποποιήσουμε (override) κληρονομημένες μεθόδους 40
class USBDevice { private: long m_lid; USBDevice(long lid) : m_lid(lid) { long GetID() { return m_lid; class NetworkDevice { private: long m_lid; NetworkDevice(long lid) : m_lid(lid){ long GetID() { return m_lid; class WirelessAdapter: public USBDevice, public NetworkDevice { WirelessAdapter(long lusbid, long lnetworkid) : USBDevice(lUSBID), NetworkDevice(lNetworkID) { int main() { WirelessAdapter c54g(5442, 181742); cout << c54g.getid(); // ποια GetID() θα κληθεί; Compilation Error return 0; 41
class USBDevice { private: long m_lid; USBDevice(long lid) : m_lid(lid) { long GetID() { return m_lid; class NetworkDevice { private: long m_lid; NetworkDevice(long lid) : m_lid(lid){ long GetID() { return m_lid; class WirelessAdaptor: public USBDevice, public NetworkDevice { WirelessAdaptor(long lusbid, long lnetworkid) : USBDevice(lUSBID), NetworkDevice(lNetworkID) { int main() { WirelessAdaptor c54g(5442, 181742); cout << c54g.usbdevice::getid(); // OK! return 0; 42
Η πολλαπλή κληρονομικότητα γενικά βασίζεται στο επιχείρημα ότι οι βασικές κλάσεις είναι όσο το δυνατό αποσυσχετισμένες: Δεν έχει νόημα να κάνουμε πολλαπλή κληρονομικότητα σε κλάσεις της ίδιας κληρονομικής αλυσίδας Για να έχουμε 2 ανεξάρτητες βασικές κλάσεις, αυτές θα πρέπει να διαφέρουν και εννοιολογικά, αλλιώς θα είχαν σχέση προγόνου - απογόνου Από κακό σχεδιασμό ή κοινούς προγόνους, μπορούν να προκύψουν σοβαρά προβλήματα (είδαμε ένα στις 2 προηγούμενες διαφάνειες) 43
Στην πολλαπλή κληρονομικότητα σκεφτόμαστε τις βασικές κλάσεις ως πρότυπα κλάσεων περισσότερο παρά ως πλήρως υλοποιημένες κλάσεις: Οι βασικές κλάσεις στην πολλαπλή κληρονομικότητα πρέπει να: προσδιορίζουν αφηρημένες ιδιότητες Ισοδυναμούν με τα Interfaces της Java 44
Geometry Drawable vector<vec3> points virtual void draw() = 0 Surface float area Color paint virtual void calculatearea()=0 void setcolor(color c) PhysicsObject vector<force*> forces void addforce(force *) virtual void calculatespeed(float dt) Solid float mass virtual void calculatemass()=0 Box virtual void calculatearea() virtual void draw() virtual void calculatemass() Sphere virtual void calculatearea() virtual void draw() virtual void calculatemass()
Pure virtual μέθοδοι: virtual ret_type methodname(params)=0; Abstract κλάσεις μόνο με pure virtual methods: Συνεχίζουν να λέγονται class, είναι στην ουσία interfaces Επιτρέπεται η πολλαπλή κληρονομικότητα Abstract μέθοδοι: abstract ret_type methodname(params); Abstract κλάσεις μόνο με pure virtual methods: Λέγονται interfaces Δεν επιτρέπεται η πολλαπλή κληρονομικότητα αλλά γίνεται μέσω της υλοποίησης interfaces 46