Κληρονομικότητα. 1 Γενικά. 2 Απλή κληρονομικότητα. 15 Ιανουαρίου 2013

Σχετικά έγγραφα
Τελικό τεστ - απαντήσεις

2.1 Αντικειµενοστρεφής προγραµµατισµός

Εισαγωγή σε αντικειμενοστραφή concepts. Και λίγη C#

Υπερφόρτωση τελεστών

ΠΟΛΥΜΟΡΦΙΣΜΟΣ. 4.1 Κληρονομικότητα και Αρχή της Υποκατάστασης

Σύνθεση και Κληρονομικότητα

Σύνθεση και Κληρονομικότητα

Προγραμματισμός ΙI (Θ)

ΥΠΟΛΟΓΙΣΤΕΣ ΙΙ. Τι είναι ; Συναρτήσεις. Παράδειγμα #1. double convert ( double cm ) { double inch;

public void printstatement() { System.out.println("Employee: " + name + " with salary: " + salary);

Pascal. 15 Νοεμβρίου 2011

3ο σετ σημειώσεων - Πίνακες, συμβολοσειρές, συναρτήσεις

Προγραμματισμός Ι. Κλάσεις και Αντικείμενα. Δημήτρης Μιχαήλ. Τμήμα Πληροφορικής και Τηλεματικής Χαροκόπειο Πανεπιστήμιο

Γραφικά υπολογιστών Εργαστήριο 9 Κλάσεις στην Python. Σκοπός της 9ης άσκησης είναι να μάθουμε αντικειμενοστρεφή προγραμματισμό στην Python.

Δομημένος Προγραμματισμός

Δομημένος Προγραμματισμός. Τμήμα Επιχειρηματικού Σχεδιασμού και Πληροφοριακών Συστημάτων

Ερωτήσεις και απαντήσεις στα θέματα του κανονισμού κατάρτισης

Τι είναι κλάση Κλάση

Με τι ασχολείται ο αντικειμενοστραφής προγραμματισμός

Προβλήματα, αλγόριθμοι, ψευδοκώδικας

2 Ορισμός Κλάσεων. Παράδειγμα: Μηχανή για Εισιτήρια. Δομή μιας Κλάσης. Ο Σκελετός της Κλάσης για τη Μηχανή. Ορισμός Πεδίων 4/3/2008

Προγράμματα με δομή Κληρονομικότητας

Κλάσεις και αντικείμενα #include <iostream.h<

ΑΠΛΗ ΚΛΗΡΟΝΟΜΙΚΟΤΗΤΑ

ΥΠΟΛΟΓΙΣΤΕΣ ΙI. Άδειες Χρήσης. Συναρτήσεις I Διδάσκοντες: Αν. Καθ. Δ. Παπαγεωργίου, Αν. Καθ. Ε. Λοιδωρίκης

ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΗΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΣ

Κληρονομικότητα. Παύλος Εφραιμίδης pefraimi <at> ee.duth.gr. Java Κληρονομικότητα 1

Στη C++ υπάρχουν τρεις τύποι βρόχων: (a) while, (b) do while, και (c) for. Ακολουθεί η σύνταξη για κάθε μια:

Κλάσεις και Αντικείµενα

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Αντικείμενα με πίνακες. Constructors. Υλοποίηση Στοίβας

Ψευδοκώδικας. November 7, 2011

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Κλάσεις και Αντικείμενα Αναφορές

ΠΤΥΧΙΑΚΗ ΕΡΓΑΣΙΑ ΘΕΜΑ: «Σχεδιασμός και ανάπτυξη ιστοσελίδων ηλεκτρονικής εκπαίδευσης (e learning) σε θέματα αντικειμενοστραφή προγραμματισμού.

Παύλος Εφραιµίδης. Java. Κληρονοµικότητα

Οντοκεντρικός Προγραμματισμός

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Μαθήματα από τα εργαστήρια

21. ΦΥΛΛΟ ΕΡΓΑΣΙΑΣ 4 - ΔΗΜΙΟΥΡΓΩΝΤΑΣ ΜΕ ΤΟ BYOB BYOB. Αλγόριθμος Διαδικασία Παράμετροι

Διάλεξη 16-17: Πολυμορφισμός (Polymorphism) Διδάσκων: Παναγιώτης Ανδρέου

Διαδικασιακός Προγραμματισμός

«ΕΙΔΙΚΑ ΘΕΜΑΣΑ ΣΟΝ ΠΡΟΓΡΑΜΜΑΣΙΜΟ ΤΠΟΛΟΓΙΣΩΝ» Κεφάλαιο 4: Αντικειμενοςτρεφήσ Προγραμματιςμόσ

ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΗΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΣ

Συναρτήσεις. Υποπρόγραμμα

Περιληπτικά, τα βήματα που ακολουθούμε γενικά είναι τα εξής:

Ανάπτυξη και Σχεδίαση Λογισμικού

Εντολές ελέγχου ροής if, for, while, do-while

Αντικειμενοστρέφεια. Henri Matisse, Harmony in Red, Κωστής Σαγώνας Νίκος Παπασπύρου

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Στατικές μέθοδοι και μεταβλητές Εσωτερικές κλάσεις

5ο σετ σημειώσεων - Δείκτες

ΠΛΗΡΟΦΟΡΙΚΗ ΙΙ (JAVA) 11/3/2008

Κλήση Συναρτήσεων ΚΛΗΣΗ ΣΥΝΑΡΤΗΣΕΩΝ. Γεώργιος Παπαϊωάννου ( )

Προγραμματισμός Υπολογιστών με C++

Σύντομες εισαγωγικές σημειώσεις για την. Matlab

3 Αλληλεπίδραση Αντικειμένων

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Κλάσεις και Αντικείμενα

Γραφικά υπολογιστών Εργαστήριο 10 Εισαγωγή στα Sprites

Εισαγωγή στον Προγραμματισμό

Δομημένος Προγραμματισμός (ΤΛ1006)

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Αντικείμενα με πίνακες. Constructors. Υλοποίηση Στοίβας

Προγραμματισμός Υπολογιστών με C++

ΠΑΝΕΠΙΣΤΗΜΙΟ AΙΓΑIΟΥ & ΑΕΙ ΠΕΙΡΑΙΑ Τ.Τ. Τμήματα Ναυτιλίας και Επιχειρηματικών Υπηρεσιών & Μηχ. Αυτοματισμού ΤΕ. Εισαγωγή στη Python

Ανάπτυξη και Σχεδίαση Λογισμικού

ΚΕΦΑΛΑΙΟ 5. Κύκλος Ζωής Εφαρμογών ΕΝΟΤΗΤΑ 2. Εφαρμογές Πληροφορικής. Διδακτικές ενότητες 5.1 Πρόβλημα και υπολογιστής 5.2 Ανάπτυξη εφαρμογών

B. Ενσωμάτωση Ιθαγενών Μεθόδων

Σημειωματάριο Δευτέρας 30 Οκτ. 2017

Προγραμματισμός Η/Υ (ΤΛ2007 )

Σημειωματάαριο Δευτέρας 16 Οκτ. 2017

Αντικειμενοστραφής Προγραμματισμός

ΠΑΝΕΠΙΣΤΗΜΙΟ ΘΕΣΣΑΛΙΑΣ ΣΧΟΛΗ ΘΕΤΙΚΩΝ ΕΠΙΣΤΗΜΩΝ ΤΜΗΜΑ ΠΛΗΡΟΦΟΡΙΚΗΣ

Αντικειμενοστρεφής Προγραμματισμός

Προγραμματισμός Ι. Δείκτες. Δημήτρης Μιχαήλ. Τμήμα Πληροφορικής και Τηλεματικής Χαροκόπειο Πανεπιστήμιο

ΚΕΦΑΛΑΙΟ 10 ΥΠΟΠΡΟΓΡΑΜΜΑΤΑ

. Μεθοδολογία Προγραμματισμού. Μοτίβα σχεδίασης (Design Patterns) Νικόλαος Πεταλίδης. Εισαγωγή Εαρινό Εξάμηνο 2014

ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΣ Η/Υ Ακαδημαϊκό έτος ΤΕΤΡΑΔΙΟ ΕΡΓΑΣΤΗΡΙΟΥ #4

Κεφάλαιο 10 ο Υποπρογράµµατα

Τι χρειάζεται ένας φοιτητής για τη σωστή παρακολούθηση και συμμετοχή στο μαθημα;

Εργαστήρια Αριθμητικής Ανάλυσης Ι. 7 ο Εργαστήριο. Διανύσματα-Πίνακες 2 ο Μέρος

7. ΕΙΣΑΓΩΓΗ ΣΤΙΣ ΣΥΝΑΡΤΗΣΕΙΣ

Pascal, απλοί τύποι, τελεστές και εκφράσεις

Μεταβλητές τύπου χαρακτήρα

Συναρτήσεις και διαδικασίες

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Κλάσεις και Αντικείμενα Constructors

ΕΡΓΑΣΙΕΣ ΟΝΤΟΚΕΝΤΡΙΚΟΥ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ

Συναρτήσεις. Εισαγωγή

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Δημιουργία Κλάσεων και Αντικειμένων

Abstract classes, Interfaces ΦΡΟΝΤΙΣΤΗΡΙΟ JAVA

Πληροφορική 2. Γλώσσες Προγραμματισμού

ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΣ Η/Υ Ακαδημαϊκό έτος ΤΕΤΡΑΔΙΟ ΕΡΓΑΣΤΗΡΙΟΥ #2

Σχεδίαση-Ανάπτυξη Εφαρμογών Πληροφορικής - Εβδομάδα 1

12. ΑΛΦΑΡΙΘΜΗΤΙΚΑ. υο είδη αλφαριθµητικών Τα αλφαριθµητικά της C πίνακες τύπου char Ta αντικείµενα της κλάσης string

ΚΑΤΑΣΚΕΥΑΣΤΕΣ ΑΝΤΙΓΡΑΦΗΣ

3.1 Αριθμητικοί και Λογικοί Τελεστές, Μετατροπές Τύπου (Casting)

Προγραμματισμός Η/Υ 1 (Εργαστήριο)

Προγραμματισμός Η/Υ (ΤΛ2007 )

Εισαγωγή στην Αριθμητική Ανάλυση

ΤΕΧΝΙΚΕΣ ΑΝΤΙΚΕΙΜΕΝΟΣΤΡΑΦΟΥΣ ΠΡΟΓΡΑΜΜΑΤΙΣΜΟΥ. Αναφορές Στοίβα και Σωρός Μνήμης Αντικείμενα ως ορίσματα

Προγραμματισμός Ι. Εισαγωγή στην C++ Δημήτρης Μιχαήλ. Τμήμα Πληροφορικής και Τηλεματικής Χαροκόπειο Πανεπιστήμιο

ΚΛΗΡΟΝΟΜΙΚΟΤΗΤΑ ΚΑΙ ΠΟΛΥΜΟΡΦΙΣΜΟΣ

Κατανεμημένα Συστήματα

Pascal. 26 Οκτωβρίου 2011

Η Γλώσσα Προγραµµατισµού C++ (The C++ Programming Language)

Transcript:

Κληρονομικότητα 15 Ιανουαρίου 2013 1 Γενικά Μία από τις βασικές σχεδιαστικές αρχές των αντικειμενοστρεφών γλωσσών προγραμματισμού είναι η δυνατότητα επαναχρησιμοποίησης κώδικα. Ένας από τους μηχανισμούς που δίνουν αυτήν τη δυνατότητα είναι η κληρονομικότητα. Σε γενικές γραμμές, αν θέλουμε μία λειτουργικότητα η οποία σε μεγάλο βαθμό καλύπτεται από μία υπάρχουσα κλάση αλλά όχι τελείως, μπορούμε να δηλώσουμε μία νέα κλάση ως παιδί της αρχικής. Αυτομάτως η νέα κλάση κληρονομεί όλα τα χαρακτηριστικά του γονιού της (μέλη δεδομένων και μεθόδους). Τα πλεονεκτήματα είναι πολλά. Κατ αρχάς δεν χρειάζεται να υλοποιήσουμε λειτουργίες οι οποίες έχουν ήδη κωδικοποιηθεί παλιότερα. Μπορούμε να κάνουμε οσεσδήποτε αλλαγές θέλουμε στη νέα κλάση χωρίς να αλλάξουμε την παλιά άρα όσα προγράμματα χρησιμοποιούσαν την παλιά κλάση συνεχίζουν να δουλεύουν κανονικά. Αν χρειαστεί να γίνει κάποια αλλαγή στην αρχική κλάση, αυτή κληρονομείται αυτόματα από την καινούρια με μία απλή μεταγλώττιση. Η κληρονομικότητα παρέχει δυνατότητες που δεν μπορούν να γίνουν κατανοητές χωρίς την έννοια του πολυμορφισμού με την οποία ασχολούμαστε παρακάτω. Μία παραγόμενη κλάση μπορεί να κληρονομεί από περισσότερες από μία κλάσεις-γονιούς. Σχετικά με την ορολογία της κληρονομικότητας: Η αρχική κλάση ονομάζεται γονιός (parent), υπερκλάση (superclass) ή βασική (base). Η νέα κλάση ονομάζεται παιδί (child), υποκλάση (subclass) ή παραγόμενη (derived). Οι όροι υπερκλάση και υποκλάση είναι ελαφρά παραπλανητικοί γιατί η νέα κλάση, εφόσον κληρονομεί όλα τα χαρακτηριστικά της παλιάς και πιθανώς διαθέτει και ορισμένα καινούρια, είναι μεγαλύτερη από την παλιά οπότε ο χαρακτηρισμός υποκλάση είναι ίσως άδικος. Αρκετά συχνά οι σχέσεις κληρονομικότητας ανάμεσα σε κλάσεις απεικονίζονται ως ένα διάγραμμα στο οποίο οι κλάσεις-γονιοί απεικονίζονται ψηλότερα από τις κλάσεις-παιδιά και από κάθε κλάσηπαιδί ξεκινάει ένα βέλος προς την κλάση-γονιό. Τα παραδείγματα του φυλλαδίου μπορείτε να τα βρείτε στο αρχείο examples_5.zip. 2 Απλή κληρονομικότητα Ας υποθέσουμε ότι θέλουμε να υλοποιήσουμε ένα πρόγραμμα το οποίο χειρίζεται υπαλλήλους μίας εταιρείας και ότι καταλήγουμε σε μία κλάση σαν την παρακάτω (το πλήρες πρόγραμμα είναι στο αρχείο ex_5_1a.cc: 1 class employee { 1

2 public: 3 string name; 4 5 employee( string n) { name = n; } 6 void work() { cout << " employee " << name << " works" << endl;} 7 void print() { cout << "name: " << name << endl; } 8 void have_break() { cout << name << " has a break" << endl; } 9 }; Έχοντας την παραπάνω κλάση, μπορούμε να δηλώσουμε αντικείμενα τύπου employee, να καλέσουμε τις μεθόδους τους κτλ. Αν προσθέταμε για παράδειγμα στη main τα παρακάτω 1 employee e1(" Papadopoulos"); 2 employee e2(" Papaspyrou"); 3 e1.print(); 4 e1.work(); 5 e2.print(); 6 e2.work(); θα βλέπαμε στην οθόνη name: Papadopoulos employee Papadopoulos works name: Papaspyrou employee Papaspyrou works Αφού το πρόγραμμά μας έχει βγει στην παραγωγή, ο πελάτης ζητάει μία προσθήκη: να γίνονται κάποιοι ειδικοί χειρισμοί για τους προϊσταμένους. Οι προϊστάμενοι για παράδειγμα μπορούν να δίνουν διαταγές. Αν δεν υπήρχε η κληρονομικότητα, τότε θα έπρεπε να τροποποιήσουμε τον υπάρχοντα κώδικα για τους υπαλλήλους. Θα μπορούσαμε για παράδειγμα να προσθέσουμε μία ακόμα μεταβλητή-σημαία θα είχε μια ειδική τιμή από την οποία θα προέκυπτε αν ο συγκεκριμένος εργαζόμενος είναι υπάλληλος (π.χ. τιμή 0) ή προϊστάμενος (π.χ. τιμη 1). Όλες οι μέθοδοι που θα έπρεπε να διαφοροποιηθούν θα άλλαζαν ώστε υπό συνθήκες ανάλογες με την τιμή αυτής της μεταβλητής να εκτελούν τις εντολές που αντιστοιχούν σε έναν απλό υπάλληλο ή σε έναν προϊστάμενο. Αυτή η λύση είναι προφανώς άκομψη και καθόλου επεκτάσιμη. Αν εμφανιζόταν και μια τρίτη ή τέταρτη κτλ. κατηγορία εργαζομένων το πρόγραμμα θα γινόταν μια σειρά από if, θα ήταν δυσνόητο και πολύ δύσκολο στη συντήρησή του. Επίσης, η τροποποίηση του υπάρχοντα κώδικα σχεδόν σίγουρα θα εισήγαγε σφάλματα σε μία εφαρμογή που τουλάχιστον για τους απλούς υπαλλήλους λειτουργούσε. Μια άλλη προσέγγιση θα ήταν να αφεθεί η κλάση employee στην ησυχία της, να δημιουργηθεί ένα αντίγραφο το οποίο θα ονομαζόταν manager και πάνω σε αυτό να γίνονταν οι όποιες τροποποιήσεις έπρεπε να γίνουν. Και αυτή η προσέγγιση πάσχει. Αν στο μέλλον χρειαζόταν να γίνει μία μετατροπή σε κάποια από τις κοινές μεθόδους των δύο κλάσεων, τότε θα έπρεπε ο προγραμματιστής να είναι υπεύθυνος ώστε αυτή η αλλαγή να γίνει με τον ίδιο τρόπο σε όλα τα σημεία που θα έπρεπε. Αυτό είναι άκομψο, βαρετό και πηγή σφαλμάτων. Επίσης, τα if που στην προηγούμενη προσέγγιση θα έμπαιναν μέσα στην κλάση, τώρα θα έπρεπε να μπουν στο κυρίως πρόγραμμα το οποίο θα έπρεπε να χειρίζεται τις δύο κατηγορίες υπαλλήλων ως διαφορετικές έννοιες ακόμα και σε περιπτώσεις που δεν θα είχαν διαφορές. 2

Η προσέγγιση της κληρονομικότητας είναι πιο υγιής: Ορίζουμε τη νέα κλάση ως απόγονο της παλιάς, ό,τι είναι ίδιο το αφήνουμε ως έχει, αλλάζουμε μόνο ότι είναι διαφορετικό και προσθέτουμε τα νέα χαρακτηριστικά που θέλουμε. Για παράδειγμα στην παρακάτω υλοποίηση, λέμε ότι ο manager είναι ένας υπάλληλος όπως και οι άλλοι και ότι επιπλέον μπορεί να δίνει εντολές (αν καλέσουμε τη μέθοδο command). 1 class manager : public employee { 2 public: 3 manager( string n) : employee(n) {} 4 void command() { cout << " manager " << name << " commands" << endl; } 5 }; Παρατηρήστε ότι στη δήλωση της κλάσης manager δεν αναφέραμε τίποτε για τις μεθόδους work, print ούτε για το μέλος name. Όλα αυτά κληρονομούνται ως έχουν από την κλάση manager. Αν δηλαδή σε ένα άλλο μέρος του προγράμματός μας γράψουμε τα παρακάτω: 1 manager m(" Agriopoulos"); 2 m.print(); 3 m.work(); 4 m. have_break (); 5 m.command(); θα δούμε στην οθόνη αυτά: name: Agriopoulos employee Agriopoulos works Agriopoulos has a break manager Agriopoulos commands Παρατηρήστε επίσης τον τρόπο που ορίζεται ο constructor της κλάσης manager στη γραμμή 3: manager (string n) : employee(n). Μετά από τη λίστα παραμέτρων, γράφουμε την άνω-κάτω τελεία, το όνομα της βασικής κλάσης και μέσα σε παρενθέσεις τα ορίσματα που θέλουμε να δώσουμε στον constructor της βασικής κλάσης. Πριν δηλαδή ξεκινήσουν να εκτελούνται οι όποιες εντολές του constructor της κλάσης manager (μέσα σε άγκιστρα στο τέλος της γραμμής), καλείται ο constructor της κλάσης employee και έτσι αρχικοποιείται ο employee που κρύβει μέσα του ο manager. 3 Upcasting Όπως αναφέρθηκε προηγούμενα, ένα αντικείμενο της κλάσης manager είναι ότι και ένα αντικείμενο της κλάσης employee. Επομένως δεν θα έπρεπε, κατά μία έννοια, να υπάρχει πρόβλημα αν σε ένα κομμάτι κώδικα που γράφτηκε για ένα αντικείμενο τύπου employee, δώσουμε ένα αντικείμενο τύπου manager. Για παράδειγμα, η παρακάτω συνάρτηση παίρνει ως όρισμα ένα employee και για αυτό καλεί τη μέθοδο print (το πλήρες πρόγραμμα είναι στο αρχείο ex_5_2a.cc). 1 void f( employee e) { 2 e.print(); 3 } 3

Μπορούμε να καλέσουμε αυτήν τη μέθοδο με παράμετρο ένα employee ή ένα manager. Πράγματι, το παρακάτω πρόγραμα 1 int main() 2 { 3 employee e1(" Papadopoulos"); 4 manager m(" Agriopoulos"); 5 6 f(e1); 7 f(m); 8 } δίνει στην οθόνη name: Papadopoulos name: Agriopoulos Δηλαδή μπορούμε αντί για παράμετρο employee να δώσουμε παράμετρο manager και αυτό είναι λογικό αφού ένας manager είναι επίσης και employee. Βέβαια, η C++ πρέπει να πραγματοποιήσει κάποιες μετατροπές για να είναι τεχνικά εφικτό, αυτό που μας φαίνεται λογικό. Αυτός ο τύπος μετατροπής λέγεται upcasting (μετατροπή προς τα πάνω) και είναι στην ουσία η μετατροπή ενός αντικειμένου σε ένα άλλου τύπου, που να βρίσκεται όμως πιο ψηλά στην ιεραρχία των κλάσεων (θυμηθείτε ότι οι κλάσεις-παιδιά στα διαγράμματα ιεραρχιών εμφανίζονται χαμηλότερα από τις κλάσεις-γονείς). Είναι σημαντικό να προσέξετε μία λεπτομέρεια: Μπορούμε μεν να δώσουμε ένα αντικείμενο τύπου manager σε μία συνάρτηση που περιμένει ένα employee αλλά αυτό δεν σημαίνει ότι η συνάρτηση παίρνει ως παράμετρο το πρωτότυπο αντικείμενο. Η συνάρτηση τελικά χειρίζεται το τοπικό αντικείμενο e τύπου employee το οποίο απλώς φτιάχτηκε ώστε να έχει στα μέλη δεδομένων του τις ίδιες τιμές που είχε και το αρχικό αντικείμενο τύπου manager στα αντίστοιχα πεδία. Ισχύουν δηλαδή οι κανόνες της μεταβίβασης ορισμάτων κατ αξία σε συναρτήσεις. 4 Override μεθόδων Είδαμε ότι οι παράγωγες κλάσεις κληρονομούν όλα τα χαρακτηριστικά των βασικών κλάσεων. Στην περίπτωση που κάποια από τις μεθόδους της βασικής κλάσης δεν είναι κατάλληλη για τη νέα κλάση μπορούμε να την υπερβούμε (override) ορίζοντας μία καινούρια. Αν για παράδειγμα η μέθοδος print της κλάσης employee πρέπει να αλλάξει ώστε να τυπώνει ένα αστέρι πριν τυπώσει τα στοιχεία του υπαλλήλου, μπορούμε να την υπερβούμε ορίζοντας μία καινούρια print μέσα στην κλάση manager. Δηλαδή προσθέτουμε τον παρακάτα ορισμό μέσα στην κλάση manager (το πλήρες πρόγραμμα είναι στο αρχείο ex_5_2b.cc). 1 void print() { cout << "* " << "name: " << name << endl; } Τώρα, αν στη main γράψουμε κάτι σαν το παρακάτω 1 e1.print(); 2 m.print(); 4

η έξοδος θα είναι name: Papaspyrou * name: Agriopoulos Σε πολλές περιπτώσεις, όταν πρέπει να υπερβούμε μία συνάρτηση, δεν χρειάζεται να γίνουν μεγάλες αλλαγές. Στο παραπάνω παράδειγμα η μόνη αλλαγή ήταν ένα αστεράκι. Το κακό ήταν ότι χρειάστηκε να ξαναγραφτούν και εντολές οι οποίες δεν χρειαζόταν να αλλάξουν. Σε τέτοιες περιπτώσεις είναι συχνά χρήσιμο να καλέσουμε τη μέθοδο της βασικής τάξης μέσα από τη μέθοδο της παράγωγης. Για να γίνει αυτό γράφουμε το όνομα της βασικής τάξης ακολουθούμενο από δύο άνω κάτω τελείες και το όνομα της μεθόδου. Δηλαδή η μέθοδος print της manager θα μπορούσε να γραφτεί έτσι: 1 void print() { 2 cout << "* "; 3 employee::print(); 4 } Είναι σημαντικό να θυμάται κανείς ότι ο μηχανισμός υπέρβασης συναρτήσεων δουλεύει όπως περιγράφτηκε παραπάνω, εφόσον το αντικείμενο είναι ρητά δηλωμένο ως αντικείμενο του παράγωγου τύπου. Στην περίπτωση που ένα αντικείμενο κάποιου παράγωγου τύπου, μετατραπεί σε αντικείμενο κάποιου βασικού με τον μηχανισμό του upcasting, τότε δεν πραγματοποιείται υπέρβαση αλλά καλείται η μέθοδος της βασικής κλάσης. Δηλαδή το παρακάτω παράδειγμα 1 void f( employe e) 2 { 3 e.print(); 4 } 5 6 int main() 7 { 8 employee e1(" Tasos"); 9 manager m1(" Mitsos"); 10 11 e1.print(); 12 m1.print(); 13 f(e1); 14 m(e1); 15 } θα τυπώσει στην οθόνη name: Tasos * name: Mitsos name: Tasos name: Mitsos Θα δούμε στην επόμενη ενότητα τον τρόπο που μπορούμε να κάνουμε την C++ να αναγνωρίζει μεθόδους στις οποίες έχει γίνει override και να καλεί τη μέθοδο που αντιστοιχεί στην παράγωγη 5

κλάση, ακόμα και όταν η κλήση γίνεται μέσω μιας μεταβλητής (για την ακρίβεια ενός δείκτη) που έχει ως τύπο τη βασική κλάση. 5 Εικονικές συναρτήσεις Στην προηγούμενη ενότητα είδαμε ότι αν κάνουμε override στη μέθοδο print και δώσουμε το αντικείμενο τύπου manager ως παράμετρο στη συνάρτηση f, τότε η f, λόγω του ότι δέχεται παράμετρο τύπου employee, δεν καλεί τη μέθοδο print που έγινε override στη manager αλλά τη αντίστοιχη μέθοδο της employee. Ο λόγος ήταν ότι το αντικείμενο manager μετατράπηκε σε employee με το μηχανισμό upcasting. Ας υποθέσουμε τώρα ότι κάνουμε override τη μέθοδο work και ας δούμε πώς θα μπορούσαμε να κάνουμε τη συνάρτηση f να καλέσει τη σωστή εκδοχή της work ανάλογα με τον πραγματικό τύπο του αντικειμένου που της δίνεται ως παράμετρος, δηλαδή αν της δίνεται ένα employee να καλεί τη work του employee, ενώ αν της δίνεται ένα manager να καλεί τη work εκείνου. Για να διορθωθεί η κατάσταση θα πρέπει να κάνουμε δύο αλλαγές στο πρόγραμμα (το πλήρες πρόγραμμα βρίσκεται στο αρχείο ex_5_2_c.cc): Να αλλάξουμε τη συνάρτηση f έτσι ώστε να μην παίρνει την παράμετρό της κατ αξία αλλά κατ όρισμα. Με άλλα λόγια να χρησιμοποιήσουμε δείκτες ή ακόμα καλύτερα αναφορές, δηλαδή να αλλάξουμε την επικεφαλίδα της σε void f(employee& e). Να αλλάξουμε τη δήλωση της μεθόδου work στη βασική κλάση employee προσθέτοντας τη λέξη virtual στην αρχή της μεθόδου κάπως έτσι: virtual void work(... Το πρώτο από τα δύο παραπάνω θα μπορούσε ίσως να το υποψιαστεί κανείς: Εφόσον η παράμετρος e της συνάρτησης f είναι τοπική μεταβλητή, πρέπει να δημιουργηθεί ένα αντικείμενο e τύπου employee. Από τη στιγμή που αυτό το αντικείμενο δημιουργηθεί, είναι απλώς και μόνο employee ακόμα και αν φτιάχτηκε ως αντίγραφο ενός manager και από τη στιγμή που δημιουργείται χάνει κάθε σύνδεση με το αρχικό αντικείμενο. Για να μπορέσει η συνάρτηση f να εξετάσει και να ανακαλύψει ότι το αντικείμενο που της δίνεται ως παράμετρος δεν είναι απλώς ένα employee, θα πρέπει να έχει ένα δείκτη προς το αρχικό αυτό αντικείμενο. Το δεύτερο από τα παραπάνω είναι ένα εντελώς νέο χαρακτηριστικό της C++ σε σχέση με τη C και άλλες procedural γλώσσες προγραμματισμού. Η δεσμευμένη λέξη virtual ενημερώνει το μεταγλωττιστή ότι ναι μεν τα αντικείμενα τύπου employee έχουν μία μέθοδο work την οποίουν κληρονομούν ως έχει όλα τα αντικείμενα από κλάσεις-παιδιά της employee αλλά είναι πιθανό, κάποιες από τις κλάσεις παιδιά να αλλάξουν αυτόν τον ορισμό. Επομένως, όποτε καλείται η μέθοδος work για κάποιο αντικείμενο οπουδήποτε στην ιεραρχία κάτω από την employee ο μεταγλωττιστής θα πρέπει να εξετάσει τον πραγματικό τύπο του αντικειμένου για να ανακαλύψει ποια από τις μεθόδους work θα κληθούν. Η πραγματική σημασία των virtual συναρτήσεων είναι η εξής: Μπορούμε να γράψουμε ένα κομμάτι κώδικα όπως π.χ. αυτό 1 void f( employee& e) { e.work(); } 6

και μετά, χωρίς να αλλάξουμε τίποτα σε αυτό το κομμάτι, να αλλάξουμε τον κώδικα που εκτελείται απλώς αλλάζοντας την παράμετρο. Ή αλλιώς: Ο προγραμματιστής-πάροχος που έγραψε την παραπάνω συνάρτηση και την κλάση employee, δίνει τη δυνατότητα σε οποιονδήποτε άλλον προγραμματιστή-πελάτη να εκτελέσει εντολές της επιλογής του, απλώς κληρονομώντας από την κλάση employee τοποθετώντας τις εντολές αυτές μέσα στη μέθοδο work της νέας κλάσης. Η αλλιώς, ένας προγραμματιστής-πάροχος, γράφει σήμερα ένα πρόγραμμα το οποίο θα εκτελέσει τις εντολές που θα γράψει αύριο ένας προγραμματιστής πελάτης. 6 Πολλαπλή κληρονομικότητα Μία κλάση μπορεί να κληρονομεί από περισσότερες από μία βασικές κλάσεις. Υποθέστε ότι έχουμε μία κλάση που αναπαριστά ένα fax και μία κλάση που αναπαριστά έναν εκτυπωτή. Μπορούμε να δημιουργήσουμε μία κλάση που αναπαριστά ενα πολυμηχάνημα κληρονομώντας και από τις δύο (το πλήρες πρόγραμμα βρίσκεται στο αρχείο ex_5_3_a.cc): 1 class fax { 2 public: 3 void send() { cout << "Send a fax" << endl; } 4 void receive() { cout << " Receive a fax" << endl; } 5 void status() { cout << " print the fax status" << endl; } 6 }; 7 8 class printer { 9 public: 10 void print() { cout << " Print a document" << endl; } 11 void status() { cout << " print the printer status" << endl; } 12 }; 13 14 class fax_printer : public fax, public printer { 15 public: 16 }; 17 18 int main() 19 { 20 fax_printer m; 21 m.send(); 22 m.receive(); 23 m.print(); 24 // m. status(); // Lathos, ambiguous 25 } Η κλάση fax_printer κληρονομεί όλες τις μεθόδους των κλάσεων από τις οποίες κληρονομεί χωρίς να χρειαστεί να αναφέρουμε οτιδήποτε. Ο μηχανισμός είναι απλός και προφανής και συνήθως δουλεύει χωρίς προβλήματα αλλά δείτε τι συμβαίνει αν προσπαθήσετε να καλέσετε τη μέθοδο τη μέθοδο status για ένα αντικείμενο fax_printer: Ο μεταγλωττιστής θα παραπονεθεί ότι η κλήση αυτή 7

είναι αμφίσημη (ambiguous) γιατί δεν μπορεί να ξέρει αν θα πρέπει να καλέσει τη μέθοδο status της κλάσης fax ή εκείνην της κλάσης printer. Σε αυτήν την περίπτωση πρέπει να κάνουμε override της μεθόδου status στην παράγωγη κλάση fax_printer ώστε να καλείται αυτή. Στη συνέχεια χρησιμοποιούμε τον τελεστή :: για να καλέσουμε τη μέθοδο status της κάθε βασικής κλάσης. Η μέθοδος status της κλάσης fax_printer θα μπορούσε να είναι κάπως έτσι (το πλήρες πρόγραμμα βρίσκεται στο αρχείο ex_5_3_b.cc): 1 void status() { 2 fax::status(); 3 printer::status(); 4 } Τώρα η m.status(); δουλεύει κανονικά. 7 Εικονικές βασικές κλάσεις Σκεφτείτε ένα άλλο παράδειγμα: Θέλετε να γράψετε ένα πρόγραμμα διαχείρισης συσκευών σε έναν εργασιακό χώρο. Αποφασίζετε να γράψετε μία κλάση η οποία θα αναπαριστά δικτυακα fax, την network_fax. Επίσης μία για δικτυακούς σαρωτές, την network_scanner. Έχοντας πρόσφατα στο μυαλό σας τα σχετικά με την κληρονομικότητα, σκέφτεστε ότι εφόσον πρόκειται και στις δύο περιπτώσεις για δικτυακές συσκευές, θα πρέπει τόσο στη μία όσο και στην άλλη κλάση να κρατάτε πληροφορίες σχετικές με τη δικτυακή υπόστασή τους, ας πούμε την IP και την MAC διεύθυνση. Για να μην γράφετε τα ίδια πράγματα δυο φορές, αποφασίζετε να φτιάξετε μια βασική κλάση, την network_device στην οποία θα έχετε τέτοιου είδους πληροφορίες και από την οποία θα κληρονομούν και οι δύο κλάσεις. Σύμφωνα με αυτό το σκεπτικό, γράφετε το παρακάτω (αρχείο ex_5_4a.cc): 1 class net_device { 2 public: 3 string ip_address; 4 string mac_address; 5 6 net_device( string i, string m) { 7 ip_address = i; 8 mac_address = m; 9 } 10 11 void print() { cout << " net_device: " << ip_address << " " << mac_address << endl 12 }; 13 14 class network_fax : public net_device { 15 public: 16 network_fax( string i, string m) : net_device(i, m) {} 17 void send() { cout << "send a fax" << endl; } 18 void receive() { cout << " receive a fax" << endl; } 19 void print() { cout << " network fax. network info: "; net_device:: print(); } 8

20 }; 21 22 class network_scanner : public net_device { 23 public: 24 network_scanner( string i, string m) : net_device(i, m) {} 25 void scan() { cout << "scan a document" << endl; } 26 void print() { cout << " network scanner. network info: "; net_device:: print(); } 27 }; 28 29 int main() 30 { 31 network_fax f(" 192.168.64.1", " 00: aa: bb: cc: dd: ee"); 32 network_scanner s(" 192.168.64.1", " 00: aa: bb: cc: dd: ee"); 33 34 f.print(); 35 s.print(); 36 37 return 0; 38 } Σύντομα, ανακαλύπτετε ότι υπάρχουν και δικτυακά πολυμηχανήματα τα οποία λειτουργούν τόσο όσο σαρωτές όσο και ως φαξ. Για να μην ξαναγράφετε τα ίδια πράγματα, αποφασίζετε να φτιάξετε μία κλάση που θα αναπαριστά τέτοιες συσκευές η οποία θα κληρονομεί και την network_fax και την network_scanner (αρχείο ex_5_4b.cc): 1 class network_fax_scanner : public network_fax, public network_scanner { 2 public: 3 network_fax_scanner( string i, string m) : 4 network_fax(i, m), network_scanner(i, m) {} 5 void print() { 6 network_fax:: print(); 7 network_scanner:: print(); 8 } 9 }; Αν τώρα στη main δηλώσετε ένα αντικείμενο αυτού του τύπου και το τυπώσετε όπως παρακάτω 1 int main() 2 { 3 network_fax_scanner multi(" 192.168.64.1", " 00: aa: bb: cc: dd: ee"); 4 multi.print(); 5 } Στην οθόνη θα δείτε το εξής: network fax. network info: net_device: 192.168.64.1 00:aa:bb:cc:dd:ee network scanner. network info: net_device: 192.168.64.1 00:aa:bb:cc:dd:ee Το οποίο είναι αυτό που περιμένατε να δείτε. Υπάρχει όμως μια λεπτομέρεια: Η νέα κλάση κληρονομεί από την network_scanner η οποία κληρονομεί από τη network_device άρα περιέχει την IP και την 9

MAC διεύθυνση. Το ίδιο όμως συμβαίνει και με την κλάση network_fax. Δηλαδή, τα πολυμηχανήματα που θα φτιάχνετε θα περιέχουν δύο φορές την IP και δύο φορές την MAC διεύθυνση. Για να πειστείτε, μπορείτε να προσθέσετε τη μέθοδο where στην κλάση πολυμηχάνημα, η οποία τυπώνει τη θέση μνήμης στην οποία είναι αποθηκευμένη η IP διεύθυνση που αντιστοιχεί στο network_fax και την IP διεύθυνση που αντιστοιχεί στο network_scanner. 1 void where() { 2 cout << "&( network_scanner:: ip_address): " << &( network_scanner:: ip_address) << e 3 cout << "&( network_fax:: ip_address): " << &( network_fax:: ip_address) << endl; 4 } Σε κάποιον υπολογιστή που εκτελέστηκε ένα πρόγραμμα που καλούσε τη where για ένα τέτοιο αντικείμενο, στην οθόνη εμφανίστηκε το παρακάτω: &(network_scanner::ip_address): 0x7ffff4923a70 &(network_fax::ip_address): 0x7ffff4923a60 Δηλαδή, ενώ προφανώς τα δικτυακά χαρακτηριστικά είναι κοινά σε ένα πολυμηχάνημα, ο τρόπος με τον οποίο περιγράφτηκαν οι ιεραρχικές σχέσεις, οδήγησε σε δύο αντίγραφα των δικτυακών ιδιοτήτων της συσκευής. Για να αντιμετωπιστούν τέτοιες καταστάσεις, η C++ δίνει τη δυνατότητα να δηλώνονται τέτοιες βασικές κλάσεις ως εικονικές. Δηλαδή αν γράφοντας ένα πρόγραμμα, αποφασίσουμε ότι κάποια από τις βασικές μας κλάσεις είναι τέτοια που δεν θα έπρεπε να δημιουργείται ένα αντικείμενο για αυτήν για κάθε φορά που εμφανίζεται σε ένα ιεραρχικό δέντρο αλλά απλώς να δημιουργείται ένα κοινό αντίγραφο για κάθε αντικείμενο που αντιστοιχεί σε μία παράγωγη κλάση, τότε δηλώνουμε τη βασική κλάση ως εικονική, γράφοντας τη δεσμευμένη λέξη virtual πριν από το όνομα της κλάσης όταν δηλώνουμε την σχέση κληρονομικότητας. Για παράδειγμα, για να δηλώσουμε την κλάση network_device ως εικονική όταν κληρονομούν από αυτήν τόσο η network_fax και η network_scanner όσο και η network_fax_scanner, γράφουμε αντίστοιχα 1 class network_fax : public virtual net_device 2 class network_scanner : public virtual net_device 3 class network_fax_scanner : public virtual net_device, 4 public network_fax, public network_scanner 8 Γνήσιες εικονικές συναρτήσεις και αφηρημένες βασικές κλάσεις Μία από τις σημαντικότερες χρήσεις των εικονικών συναρτήσεων είναι η σχεδίαση διασυνδέσεων για βιβλιοθήκες (API). Για παράδειγμα, υποθέστε ότι θέλετε να γράψετε μια βιβλιοθήκη που θα βοηθάει μαθηματικούς να μελετούν (μαθηματικές) συναρτήσεις. Μία από τις λειτουργικότητες που θα έχει η βιβλιοθήκη σας θα είναι να δείχνει στην οθόνη τις γραφικές παραστάσεις των συναρτήσεων που θα ζητούνται. Αποφασίζετε ότι θα φτιάξετε μία συνάρτηση ή μέθοδο plot η οποία θα παίρνει ως παράμετρο μία μαθηματική συνάρτηση και θα ζωγραφίζει τη γραφική της παράσταση. Έχετε ήδη φτιάξει μια συνάρτηση draw η οποία παίρνει δύο παράμέτρους x και y και ανάβει το πίξελ με συντεταγμένες x και y στην οθόνη. Επίσης έχετε αποφασίσει ότι οι συναρτήσεις που θα δίνονται ως 10

παράμετροι στην plot θα πρέπει να έχουν μία μέθοδο compute η οποία θα παίρνει ως παράμετρο ένα x και θα επιστρέφει το f(x). Σύμφωνα με όλα αυτά η plot θα μπορούσε μέσα σε ένα βρόχο, για διάφορα x, να καλεί την f.compute(x) για να υπολογίσει το f(x) και έτσι να έχει αρκετά ζεύγη τιμών (x,y) τα οποία και να απεικονίσει στην οθόνη: 1 void plot( function& f) 2 { 3 for (x = 0; x < 100; ++x) { 4 int y = f. compute(x); 5 draw(x, y); 6 } 7 } Για να δουλέψουν όλα αυτά, θα έπρεπε οι παράμετροι που θα δίνουν οι προγραμματιστές-πελάτες στην plot να έχουν μία μέθοδο compute. Αυτό είναι εύκολο να γίνει: Ορίζετε μια κλάση function η οποία διαθέτει μία virtual μέθοδο compute και στο εγχειρίδιο της βιβλιοθήκης σας, δίνετε την οδηγία ότι οι κλάσεις που θα γράφουν οι πελάτες σας θα πρέπει να κληρονομούν από την κλάση function και να κάνουν override την μέθοδο compute. Η function θα μπορούσε να είναι αυτή: 1 class function { 2 //... 3 virtual int compute(int x) {} 4 }; Τι κώδικα όμως θα έπρεπε να βάλετε στο σώμα της compute; Η λογική απάντηση θα ήταν, τον κώδικα που υπολογίζει την τιμή της συνάρτησης. Το πρόβλημα όμως είναι ότι η κλάση αυτή δεν αναπαριστά κάποια συγκεκριμένη συνάρτηση οπότε ο όποιος κώδικας θα ήταν αυθαίρετος. Για την ακρίβεια, η κλάση αυτή δεν έπρεπε να χρησιμοποιηθεί για την κατασκευή αντικειμένων. Ο σκοπός της είναι να δώσει μια αφαιρετική περιγραφή του πώς θα έπρεπε να είναι οι κλάσεις που περιγράφουν συναρτήσεις έτσι ώστε οι προγραμματιστές-πελάτες να διευκολυνθούν να γράψουν τις κλάσεις τους κληρωνομώντας από αυτήν. Το καλύτερο που θα έπρεπε να γίνει, θα ήταν να δηλωθεί με κάποιο τρόπο ακριβώς αυτό: Ότι η μέθοδος compute στην πραγματικότητα δεν υπάρχει ως υλοποίηση της κλάσης function. Στην C++ μπορούμε να το κάνουμε αυτό δηλώνοντας τη μέθοδο compute ως γνήσια εικονική συνάρτηση (pure virtual function) και αυτό γίνεται παραλείποντας το σώμα της και γράφοντας = 0; αμέσως μετά τη λίστα παραμέτρων, δηλαδή έτσι: 1 virtual int compute(int x) = 0; Μια άμεση επίπτωση είναι ότι εφόσον η κλάση function δεν δίνει ορισμό για την compute δεν μπορούν να υπάρχουν αντικείμενα της κλάσης αυτής. Τέτοιες κλάσειες λέγονται αφηρημένες βασικές κλάσεις (abstract base classes) και μοναδικός τους σκοπός είναι να κληρονομούνται από άλλες, υποχρεώνοντας ταυτόχρονα τις παράγωγες κλάσεις να ορίσουν τις γνήσιες εικονικές συναρτήσεις. Μία κλάση μπορεί να έχει μία ή περισσότερες γνήσιες εικονικές συναρτήσεις. Αρκεί μία για να χαρακτηρίσει την κλάση ως αφηρημένη βασική. Οι παράγωγες κλάσεις δεν είναι υποχρεωτικό να ορίσουν όλες τις γνήσιες εικονικές συναρτήσεις μιας αφηρημένες βασικής κλάσης. Αν όμως δεν τις ορίσουν όλες, είναι και εκείνες αφηρημένες δηλαδή δεν μπορούν να παράγουν αντικείμενα. Μπορείτε να δείτε ένα πιο λεπτομερές παράδειγμα στο αρχείο ex_5_5.cc. 11