Προγραμματισμός Υπολογιστών με C++ ( 2012-13 ) 14η διάλεξη Ίων Ανδρουτσόπουλος http://www.aueb.gr/users/ion/ 1
Τι θα ακούσετε σήμερα Εικονικές μέθοδοι και πολυμορφισμός με χρήση δεικτών ή αναφορών. Εικονικοί καταστροφείς. Καθαρά εικονικές μέθοδοι και αφηρημένες τάξεις. Περισσότερες πληροφορίες για την 3η εργασία. 2
Τροποποίηση μεθόδου σε παράγωγη τάξη 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; } }; 3
Η ανάγκη για εικονικές μεθόδους int main() { Person p("νίκος"); p.print(); // Τυπώνει «Νίκος». Student s("γιώργος", "p30100"); s.print(); // Τυπώνει «Γιώργοςp30100». Person* ptr1 = &p; // Δείκτης Person* που δείχνει στο p. // Ισοδύναμο με (*ptr1).print(): ptr1->print(); // Τυπώνει «Νίκος». Student* ptr2 = &s; // Δείκτης Student* που δείχνει στο s. ptr2->print(); // Τυπώνει «Γιώργοςp30100». Person* ptr3 = &s; // Δείκτης Person* που δείχνει σε ένα // student (το s). Επιτρέπεται γιατί η Student // είναι παράγωγη της Person. ptr3->print(); // Τυπώνει «Γιώργος». Χρησιμοποιεί την // print της Person, παρ' όλο που ο δείκτης } // δείχνει σε αντικείμενο Student. 4
Τι συμβαίνει; Ένας δείκτης βασικής τάξης (π.χ. Person*) επιτρέπεται να δείχνει σε αντικείμενο παράγωγης τάξης (π.χ. Student). Αν όμως κληθεί χρησιμοποιώντας το δείκτη της βασικής τάξης μια μέθοδος (π.χ. print) που τροποποιήθηκε στην παράγωγη τάξη, χρησιμοποιείται η μορφή που είχε η μέθοδος στη βασική τάξη. Δηλαδή το ποια μορφή της μεθόδου θα χρησιμοποιηθεί καθορίζεται από τον τύπο του δείκτη. Συχνά θέλουμε, όμως, να καθορίζεται από τον τύπο (τάξη) του αντικειμένου στο οποίο δείχνει ο δείκτης. Για να συμβεί αυτό πρέπει να δηλώσουμε τη μέθοδο ως εικονική (virtual). 5
Εικονικές μέθοδοι 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; } }; 6
Χρήση εικονικών μεθόδων int main() { // Η print είναι τώρα εικονική. Person p("νίκος"); Student s("γιώργος", "p30100"); Person* ptr1 = &p; // Δείκτης Person* που δείχνει στο p ptr1->print(); // Τυπώνει «Νίκος». Student* ptr2 = &s; // Δείκτης Student* που δείχνει στο s. ptr2->print(); // Τυπώνει «Γιώργοςp30100». Person* ptr3 = &s; // Δείκτης Person* που δείχνει στο s. ptr3->print(); // Τώρα τυπώνει «Γιώργοςp30100». // Δηλαδή χρησιμοποιεί την // print της Student. Το ποια μέθοδος θα // χρησιμοποιηθεί το καθορίζει τώρα ο // τύπος του αντικειμένου, όχι ο τύπος } // του δείκτη. 7
Πολυμορφισμός Χρησιμοποιώντας δείκτες και εικονικές μεθόδους μπορούμε με τον ίδιο κώδικα να επιτυγχάνουμε διαφορετική συμπεριφορά, ανάλογα με τις τάξεις των αντικειμένων που χειρίζεται ο κώδικας κάθε φορά. Αυτό λέγεται πολυμορφισμός. Μπορεί να επιτευχθεί και με αναφορές αντί για δείκτες. Κατά μία άποψη, η υπερφόρτωση συναρτήσεων (πολλαπλές μορφές για διαφορετικούς τύπους/αριθμούς ορισμάτων) είναι και αυτή μια μορφή πολυμορφισμού. 8
Πολυμορφισμός με δείκτες void display(const Person* const pers) { pers->print(); // Η print είναι εικονική. Το ποια print θα // χρησιμοποιηθεί το καθορίζει ο τύπος του // αντικειμένου, όχι ο τύπος του δείκτη. } int main() { Person p("νίκος"); Student s("γιώργος", "p30100"); display(&p); // Τυπώνει «Νίκος». display(&s); // Τυπώνει «Γιώργοςp30100». } Η ίδια συνάρτηση (display) συμπεριφέρεται διαφορετικά, ανάλογα με την τάξη του ορίσματος (ανάλογα με το αν το όρισμα είναι Student ή Person). 9
Πολυμορφισμός με αναφορές void display(const Person& pers) { pers.print(); // Η print είναι εικονική. Το ποια print θα // χρησιμοποιηθεί το καθορίζει ο τύπος του // αντικειμένου, όχι ο τύπος της αναφοράς. } int main() { Person p("νίκος"); Student s("γιώργος", "p30100"); display(p); // Τυπώνει «Νίκος». display(s); // Τυπώνει «Γιώργοςp30100». } 10
Δεν δουλεύει με μετ/βαση κατά τιμή 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. } 11
Η ανάγκη εικονικών καταστροφέων class Person { string name; Person(const string& namein) : name(namein) {} virtual void print() const { cout << name ; } }; class Student : public Person { string code; int* storage; Student(const string& namein, const string& codein, unsigned n) : Person(nameIn), code(codein), storage(new int[n]) {} void print() const { Person::print(); cout << code; } ~Student() { delete []storage; } 12 };
Η ανάγκη εικονικών καταστροφέων int main() { // Δυναμική καταχώριση μνήμης: Person* ps = new Student("Γιώργος", "p30100", 10); // Ο δείκτης είναι τύπου Person. Αν ο καταστροφέας δεν είναι // εικονικός, καλείται ο καταστροφέας της Person, που δεν // απελευθερώνει τη μνήμη του storage: delete ps; // Θέλουμε να καλείται ο καταστροφέας της Student, ο // οποίος καλεί αυτόματα και τον καταστροφέα της Person. // Δηλαδή θέλουμε το ποιος καταστροφέας θα κληθεί να το // καθορίζει ο τύπος του αντικειμένου στο οποίο δείχνει ο // δείκτης, όχι ο τύπος του δείκτη. } 13
Εικονικοί καταστροφείς class Person { string name; Person(const string& namein) : name(namein) {} virtual void print() const { cout << name ; } virtual ~Person() {} // Θα είναι εικονικός και ο καταστροφέας }; // της παραγόμενης τάξης. class Student : public Person { string code; int* storage; Student(const string& namein, const string& codein, unsigned n) : Person(nameIn), code(codein), storage(new int[n]) {} ~Student() { delete []storage; } 14 };
Δύο πολύ σημαντικές συμβουλές Αν θέλετε μια τάξη να μπορεί να χρησιμοποιηθεί ως βασική για παράγωγες τάξεις, κάντε τον καταστροφέα της εικονικό. Έστω κι αν το σώμα του είναι κενό. Προσοχή: Οι κατασκευαστές απαγορεύεται να είναι εικονικοί. Αν θέλετε μια μέθοδος να μπορεί να τροποποιηθεί σε παραγόμενες τάξεις, κάντε την εικονική. Δεν είναι προεπιλογή της C++, επειδή η χρήση εικονικών μεθόδων κάνει τον εκτελέσιμο κώδικα πιο αργό και απαιτεί περισσότερη μνήμη. 15
Η τάξη KNNClassifier της 3ης εργασίας class ΚΝNClassifier { InstancePool memory; // Της 2ης εργασίας.... // Η συνάρτηση απόστασης: static float distance(const Instance& inst1, const Instance& inst2); // Το trainingpool αντιγράφεται στο memory: void train(const InstancePool& trainingpool); // Κατάταξη νέου Instance για το οποίο δεν γνωρίζουμε // την κατηγορία (true ανν spam): bool classify(const Instance& inst) const; }; 16
Χρήση της ΚNNClassifier int main() { InstancePool trainingpool; ifstream trainfile("training_data.txt"); // Της 1ης εργασίας. trainfile >> trainingpool; KNNClassifier knn; knn.train(trainingpool); //... Φτάνει νέο μήνυμα. Το μετατρέπω σε Instance inst... if( knn.classify(inst) ) { cout << "spam"; } else { cout << "legitimate"; } } 17
Μια συνάρτηση αξιολόγησης unsigned evaluate(knnclassifier& knn, const InstancePool& trn, const InstancePool& test) { unsigned errors = 0; knn.train(trn); for(unsigned i = 0; i < test.getnumberofinstances(); i++) { if( knn.classify(test[i])!= test[i].getcategory() ) { errors++;} } return errors; } 18
Αξιολόγηση άλλων ταξινομητών Π.χ. BaselineClassifier στην 3η εργασία. Κατατάσσει όλα τα νέα μηνύματα στην κατηγορία που ήταν πιο συχνή στα παραδείγματα εκπαίδευσης. Τα πηγαίνουμε καλύτερα από το «χαζό» αυτό ταξινομητή; Π.χ. αφελής ταξινομητής Bayes στην 3η εργασία. Κατατάσσει κάθε νέο μήνυμα στην κατηγορία που θεωρεί περισσότερο πιθανό να ανήκει. Πώς θα πετύχουμε η ίδια συνάρτηση αξιολόγησης να δουλεύει με ταξινομητές οποιουδήποτε τύπου; Δηλαδή η evaluate να μπορεί να αξιολογήσει ταξινομητές και των τριών τάξεων. 19
Ιεραρχία τάξεων ταξινομητών Classifier KNNClassifier BaselineClassifier NaiveBayesClassifier class Classifier { virtual void train(const InstancePool& training) { /* κενό */ } virtual bool classify(const Instance& inst) const { return false; } virtual ~Classifier() {} // βλ. προηγούμενες συμβουλές }; 20
Ιεραρχία τάξεων ταξινομητών class KNNClassifier : public Classifier {... // Πρόσθετα ιδιωτικά μέλη. static unsigned short distance(...); // Nέα δημόσια μεθόδος. void train (const InstancePool& training); // Τροποποίηση, ώστε να bool classify(const Instance& inst) const; // χρησιμοποιείται ο knn. }; class BaselineClassifier : public Classifier { void train(const InstancePool& training); // Τροποποίηση, ώστε να bool classify(const Instance& inst) const; // επιστρέφεται η κατηγο- }; // ρία που είναι συχνότερη στο training. class NaiveBayesClassifier : public Classifier {... }; }; 21
Γενικευμένη συνάρτηση αξιολόγησης unsigned evaluate(classifier& c, const InstancePool& trn, const InstancePool& test) { unsigned errors = 0; c.train(trn); for(unsigned i = 0; i < test.getnumberofinstances(); i++) { if( c.classify(test[i])!= test.getcategory() ) {errors++;} } return errors; } Το ποια μορφή των train και test θα χρησιμοποιηθεί εξαρτάται από την τάξη του ταξινομητή (οι δύο μέθοδοι είναι εικονικές). Μέσω της αναφοράς c μπορώ να καλέσω όλες τις μεθόδους της Classifier (όχι όμως π.χ. τη distance(), έστω και αν το c αναφέρεται 22 σε αντικείμενο knnclassifier).
Καθαρά εικονικές μέθοδοι class Classifier { // Οι train και classify είναι «καθαρά εικονικές» στην Classifier: virtual void train(const InstancePool& trpool) = 0; virtual bool classify(const Instance& inst) const = 0; virtual ~Classifier() {} // Οι καταστροφείς δεν επιτρέπεται να }; // είναι καθαρά εικονικοί. Οι καθαρά εικονικές μέθοδοι δεν έχουν κυρίως μέρος. Υπόσχονται ότι θα οριστούν σε παράγωγες τάξεις. Μια τάξη που περιέχει τουλάχιστον μια καθαρά εικονική μέθοδο λέγεται αφηρημένη. Δεν επιτρέπεται η δημιουργία αντικειμένων αφηρημένων τάξεων. Οι αφηρημένες τάξεις χρησιμοποιούνται συχνά ως προδιαγραφές ειδικότερων τάξεων. Μπορούμε να προδιαγράψουμε στις αφηρημένες τάξεις μεθόδους που είναι κοινές στις παράγωγες τάξεις τους. 23