Υπερφόρτωση τελεστών 19 Νοεμβρίου 2012 1 Γενικά Στα προηγούμενα είδαμε ότι ορίζοντας μία κλάση, ορίζουμε ένα νέο τύπο τον οποίο μπορούμε να χρησιμοποιήσουμε για να δηλώσουμε αντικείμενα αυτής της νέας κλάσης-τύπου όπως ακριβώς δηλώνουμε μεταβλητές των βασικών τύπων της γλώσσας. Η κλάση cplex για παράδειγμα υποθετικά αναπαριστά έναν μιγαδικό αριθμό. Με τα όσα είπαμε μέχρι στιγμής σχετικά με τον ορισμό μεθόδων, η μόνη μας εναλλακτική για να ορίσουμε την πρόσθεση μιγαδικών αριθμών είναι η μέθοδος add του παραδείγματος. Προφανώς, θα ήταν απλούστερο προκειμένου να προσθέσουμε δύο αντικείμενα αυτού του τύπου να γράφαμε a+b αντί για a.add(b). Η C++ δίνει τη δυνατότητα για κάτι τέτοιο μέσω του μηχανισμού υπερφόρτωσης τελεστών (operator overloading). Με αυτόν το μηχανισμό μπορούμε να ορίσουμε νέες σημασίες για ήδη υπάρχοντες τελεστές της γλώσσας, όπως π.χ. για τον τελεστή της πρόσθεσης +. Υπερφορτώνοντας τους τελεστές μπορούμε να αλλάξουμε τη σημασία τους αλλά όχι και το συντακτικό της γλώσσας, δηλαδή οι τελεστές π.χ. / και *= είναι δυαδικοί τελεστές, δηλαδή παίρνουν δύο ορίσματα ενώ ο τελεστής ++ είναι μοναδιαίος, δηλαδή παίρνει μόνο ένα. Για να υπερφορτώσουμε έναν τελεστή γράφουμε μία μέθοδο (δηλαδή μέσα στην κλάση) ή μία συνάρτηση (δηλαδή έξω από την κλάση) η οποία υλοποιεί τη νέα λειτουργία που θέλουμε να έχει ο τελεστής. Εφόσον μιλάμε για μεθόδους και συναρτήσεις θα πρέπει να ακολουθήσουμε το βασικό συντακτικό αυτών των σχημάτων της γλώσσας δηλαδή ένα σχήμα σαν το παρακάτω: 1 return_type method_function_name( parameter_list) 2 {... } όπου δηλαδή έχουμε ένα κομμάτι κώδικα μέσα σε άγκιστρα το οποίο καλείται με το όνομα method_function_name, παίρνει μια λίστα παραμέτρων parameter_list μέσα σε παρενθέσεις και επιστρέφει μία τιμή τύπου return_type (ή void). Κανονικά οι μέθοδοι ή οι συναρτήσεις που θα χρησιμοποιούσαμε για να υλοποιήσουμε τους υπερφορτωμένους τελεστές θα έπρεπε να παίρνουν τόσες παραμέτρους όσες και τα ορίσματα του τελεστή. Αν όμως χρησιμοποιήσουμε μία μέθοδο, δηλαδή μέσα στο σώμα της κλάσης, τότε το πρώτο όρισμα του τελεστή παραλείπεται και εννοείται ότι είναι το αντικείμενο για το οποίο καλείται ο τελεστής. Αν επιλέξουμε να δημιουργήσουμε μία συνάρτηση, δηλαδή έξω από την κλάση, πρέπει να αναφέρουμε και τα δύο ορίσματα. Οι μέθοδοι και οι συναρτήσεις που υπερφορτώνουν έναν τελεστή πρέπει προφανώς να έχουν ένα όνομα το οποίο να εμπεριέχει το όνομα του τελεστή. Καθώς όμως 1
οι τελεστές είναι σύμβολα τα οποία δεν επιτρέπεται να χρησιμοποιούνται σε ονόματα συναρτήσεων, προκειμένου να υπερφορτώσουμε έναν τελεστή, π.χ. το + γράφουμε ως όνομα της συνάρτησης ή μεθόδου υπερφόρτωσης τη δεσμευμένη λέξη operator ακολουθούμενη από το όνομα του τελεστή. Το ζήτημα του επιστρεφόμενου τύπου εξαρτάται από τη σημασιολογία που θέλουμε να δώσουμε στον υπερφορτωμένο τελεστή και θα δούμε σχετικά παραδείγματα στη συνέχεια. 2 Υπερφόρτωση δυαδικών τελεστών Οι δυαδικοί τελεστές παίρνουν δύο ορίσματα. Έτσι λοιπόν, σύμφωνα με την τελευταία παράγραφο της προηγούμενης ενότητας μπορούμε: Να υπερφορτώσουμε τέτοιους τελεστές γράφοντας μια μέθοδο με μία μόνο παράμετρο μέσα στο σώμα της κλάσης. Σε αυτήν την περίπτωση, ο πρώτος τελεστέος του δυαδικού τελεστή θεωρείται ότι είναι το αντικείμενο για το οποίο καλείται ο τελεστής ενώ ο δεύτερος τελεστέος είναι η μία και μοναδική παράμετρος της μεθόδου υπερφόρτωσης. Να τους υπερφορτώσουμε γράφοντας μία συνάρτηση εξω από το σώμα της κλάσης. Σε αυτήν την περίπτωση, πρέπει η συνάρτηση να παίρνει δύο παραμέτρους: η πρώτη είναι ο αριστερός τελεστέος του τελεστή ενώ η δεύτερη ο δεξιός. Τα παραπάνω θα γίνουν πιο σαφή με τα παρακάτω παραδείγματα. Θεωρήστε ότι έχουμε τη γνωστή κλάση cplex σε ένα πρόγραμμα όπως το παρακάτω: 1 # include <iostream > 2 using namespace std; 3 4 class cplex { 5 private: 6 float re, im; 7 8 public: 9 cplex() { re = im = 0; } 10 cplex( float _re, float _im) { re = _re; im = _im; } 11 12 void print() { cout << "(" << re << ", " << im << "j)" << endl; } 13 void add( cplex b) { re += b. re; im += b. im; } 14 }; 15 16 int main() 17 { 18 cplex a(3,4); 19 cplex b(2,5); 20 a.add(b); 21 a.print(); 22 23 return 0; 2
24 } Στις γραμμές 4-14 έχουμε ορίσει την κλάση, με έναν constructor για να μπορούμε να δίνουμε αρχικές τιμές στα μέλη δεδομένων re και im και επίσης έχουμε ορίσει μία μέθοδο print για να μπορούμε να τυπώνουμε αντικείμενα αυτής της κλάσης. Αν δεν υπήρχε η δυνατότητα υπερφόρτωσης τελεστών και θέλαμε να γράψουμε μία μέθοδο η οποία να πρόσθετε σε ένα αντικείμενο ένα άλλο, θα έπρεπε να γράψουμε μία μέθοδο σαν τη μέθοδο add της γραμμής 13. Εκεί δηλώνουμε ότι αν καλέσουμε την μέθοδο add για ένα αντικείμενο με παράμετρο ένα άλλο, τότε στα μέλη δεδομένων του πρώτου αντικειμένου θα προστεθούν τα αντίστοιχα μέλη του αντικειμένου-παραμέτρου. Έτσι λοιπόν, ορίζουμε δύο αντικείμενα στις γραμμές 18 και 19 και μετά καλούμε τη μέθοδο για το πρώτο από αυτά στη γραμμή 20. Καλώντας τη μέθοδο print για το αντικείμενο a στη γραμμή 21, βλέπουμε ότι τα μέλη του από 3 και 4 πήραν τις τιμές 5 και 9 αντίστοιχα. Ας δούμε πώς θα μπορούσαμε να πετύχουμε το ίδιο αποτέλεσμα, υπερφορτώνοντας κάποιον τελεστή. Κατ αρχάς μπορούμε να υπερφορτώσουμε οποιονδήποτε δυαδικό τελεστή της C++ είτε ταιριάζει είτε όχι με την σημασιολογία των τελεστών της C++. Δηλαδή θα μπορούσαμε για να κάνουμε την παραπάνω πράξη πρόσθεσης να υπερφορτώσουμε τον τελεστή - ή τον τελεστή %. Προφανώς είναι καλό για να είναι πιο κατανοητό το πρόγραμμα και βολική στη χρήση της η κλάση να χρησιμοποιήσουμε κάποιον τελεστή που να ταιριάζει καλύτερα με την πράξη που θέλουμε να κάνουμε. Στην περίπτωσή μας, αυτό που θέλουμε να κάνουμε είναι να πραγματοποιήσουμε μια πρόσθεση αλλάζοντας ταυτόχρονα τον έναν από τους δύο τελεστέους. Αυτή είναι η σημασιολογία του τελεστή += της C++ οπότε είναι λογικό να χρησιμοποιήσουμε αυτόν. Ο τελεστής αυτός παίρνει δύο ορίσματα, δηλαδή θα θέλαμε να τον χρησιμοποιούμε κάπως έτσι: a += b;. Έχουμε λοιπόν σύμφωνα με την προηγούμενη συζήτηση τις εξής δύο εναλλακτικές: 2.1 Υπερφόρτωση μέσα στην κλάση Να γράψουμε τον κώδικα ως μέθοδο της κλάσης, ορίζοντας την μέσα στο σώμα της κλάσης. Σε αυτήν την περίπτωση η μέθοδος θα παίρνει μία μόνο παράμετρο παρόλο που ο τελεστής είναι δυαδικός και χρησιμοποιείται ως a += b;. Αυτό γίνεται γιατί ο πρώτος τελεστέος της πράξης (στο παράδειγμά μας το a) εννοείται ότι είναι το ίδιο το αντικείμενο για το οποίο καλείται ο τελεστής. Η μία παράμετρος που θα παίρνει η μέθοδος υπερφόρτωσης του τελεστή αντιστοιχεί στον δεύτερο τελεστέο της πράξης (στο παράδειγμά μας το b). Έχοντας ξεκαθαρίσει το πλήθος των παραμέτρων του τελεστή και επίσης έχοντας ως δεδομένο ότι το όνομα της μεθόδου θα είναι operator ακολουθούμενο από τον τελεστή +=. Προς το παρόν θα θεωρήσουμε ότι δεν χρειάζεται η νέα μέθοδος να επιστρέφει κάτι οπότε θα την αφήσουμε να επιστρέφει void¹. Συνοψίζοντας λοιπόν, μπορούμε να προσθέσουμε τον παρακάτω κώδικα 1 void operator += ( cplex other) { re += other. re; im += other. im; } αμέσως μετά τη γραμμή 13 του παραδείγματος και επίσης να αντικαταστήσουμε την εντολή a.add(b); της γραμμής 20 με την εντολή a += b;. Αν γράψετε το πρόγραμμα όπως περιγράφτηκε προηγουμένως θα παρατηρήσετε ότι η μέθοδος add που είχε γραφτεί στη γραμμή 13 είναι στην ουσία ολόιδια με τη γραμμή που προσθέσαμε με μόνη εξαίρεση το όνομα της μεθόδου που αντί για add έγινε operator+=. ¹Θα δούμε παρακάτω ότι μπορούμε να κάνουμε κάποιες παραπάνω σκέψεις σχετικά με αυτό το θέμα 3
Ας δούμε αναλυτικά τι συμβαίνει με την εντολή a += b;: Η γλώσσα αντιλαμβάνεται ότι ο τελεστής += εφαρμόζεται σε αντικείμενα κλάσεων που έχει ορίσει ο χρήστης. Έτσι λοιπόν δεν εφαρμόζει την ενσωματωμένη σημασιολογία της C++ για τον τελεστή αυτόν αλλά θεωρεί ότι πρόκειται για κλήση μιας μεθόδου του αριστερού τελεστέου με παράμετρο τον δεξιό, δηλαδή για κλήση μιας μεθόδου του αντικειμένου a με παράμετρο το b. Εφόσον υπάρχει η μέθοδος operator+= στην κλάση στην οποία ανήκει το αντικείμενο a, την καλεί με παράμετρο το b. Έτσι τελικά, στην ουσία πρόκειται απλώς για μια κλήση μεθόδου ενός αντικειμένου με ένα βολικότερο για μας συντακτικό. Για να πειστείτε, αντικαταστήστε την κλήση a += b; της γραμμής 20 με αυτήν: a.operator+=(b);. Θα παίξει κανονικότατα... Ας δούμε και ένα άλλο παράδειγμα λίγο διαφορετικό. Ας υποθέσουμε ότι θέλουμε να υπερφορτώσουμε τον τελεστή + έτσι ώστε να προσθέτει μιγαδικούς κάπως έτσι a + b;. Η κατάσταση μοιάζει με την περίπτωση a += b;. Και στις δύο περιπτώσεις έχουμε δυαδικούς τελεστές οι οποίοι επιδρούν σε δύο cplex αντικείμενα. Οπότε και εδώ, όπως και πριν, αν κάνουμε την υπερφόρτωση μέσα στην κλάση θα έχουμε μία μέθοδο με ένα όρισμα. Η διαφορά είναι ότι ο τελεστής += αλλάζει τις τιμές του αριστερού τελεστέου του ενώ ο + όχι. Στην περίπτωση του + δηλαδή θα πρέπει να αφήσουμε ανέπαφα τα μέλη re και im των τελεστέων, να υπολογίσουμε το άθροισμά τους και να δημιουργήσουμε ένα νέο αντικείμενο cplex το οποίο και να επιστρέψουμε. Για το σκοπό αυτό θα πρέπει να καλέσουμε ρητά τον constructor που χρειαζόμαστε για να κατασκευάσουμε το νέο αντικείμενο: 1 cplex operator +( cplex other) { return cplex( re + other.re, im + other. im); } 2.2 Υπερφόρτωση έξω από την κλάση Στην περίπτωση που επιλέξουμε να υπερφορτώσουμε τον τελεστή έξω από την κλάση πρέπει να πάρουμε υπόψη μας ότι ο αριστερός τελεσταίος δεν υπονοείται άρα πρέπει να τον αναφέρουμε ρητά. Δηλαδή η λίστα ορισμάτων του υπερφορτωμένου τελεστή θα έχει δύο παραμέτρους:(cplex left, cplex right) Τελικά λοιπόν, θα μπορούσαμε να γράψουμε κάτι σαν αυτό κάτω από τη γραμμή 14 που τελειώνει το σώμα της κλάσης: 1 void operator +=( cplex left, cplex right) 2 { 3 left. re += right. re; left. im += right. im; 4 } Παρατηρήστε ότι στην προηγούμενη εκδοχή γράφαμε re += δηλαδή αναφερόμασταν στο μέλος re χωρίς να αναγράφουμε το όνομα του αντικειμένου ακολουθούμενο από τελεία. Αυτό γινόταν γιατί εφόσον ο κώδικας ήταν γραμμένος μέσα στην κλάση, υπονοείτο ότι αν αναφέραμε ένα όνομα μέλους χωρίς να γράφουμε το όνομα του αντικειμένου εννούσαμε το αντικείμενο για το οποίο γινόταν η κλήση. Σε αυτήν την περίπτωσή όμως, δεν συμβαίνει κάτι τέτοιο. Το αντικείμενο για το οποίο γίνεται η κλήση αναφέρεται ρητά στη λίστα παραμέτρων (cplex left) οπότε αν θέλουμε να αναφερθούμε στα μέλη του πρέπει να το κάνουμε ρητά: left.re. Αν μεταγλωττίσετε τον παραπάνω κώδικα θα σας δώσει μηνύματα λάθους. Τα μηνύματα θα έχουν να κάνουν με τα δικαιώματα πρόσβασης στα private μέλη των αντικειμένων. Σύμφωνα με όσα ξέρουμε για τα δικαιώματα πρόσβασης, η παραπάνω συνάρτηση δεν έχει δικαίωμα να προσπελάσει τα ιδιωτικά μέλη re και im των αντικειμένων left και right. Πώς θα μπορούσαμε να ξεπεράσουμε 4
το πρόβλημα; Ένας τρόπος θα ήταν να γράψουμε accessors και modifiers με τους οποίους θα δίναμε δικαιώματα ανάγνωσης και τροποποίησης στα εσωτερικά μέλη των αντικειμένων της κλάσης cplex. Τελικά αν γράφαμε public μεθόδους σαν τις παρακάτω: 1 float getre() { return re; } 2 float getim() { return im; } 3 void setre( float _re) { re = _re; } 4 void setim( float _im) { im = _im; } Θα μπορούσαμε να τροποποιήσουμε τον κώδικα του υπερφορτωμένου τελεστή κάπως έτσι ώστε να δουλέψει: 1 void operator +=( cplex left, cplex right) 2 { 3 left. setre(left. getre() + right. getre()); 4 left. setim(left. getim() + right. getim()); 5 } Το μειονέκτημα αυτής της προσέγγισης είναι ότι θέλοντας να δώσουμε πρόσβαση στα εσωτερικά μέλη για το σκοπό ενός μόνο υπερφορτωμένου τελεστή, αναγκαζόμαστε να δώσουμε πρόσβαση σε όλο τον κόσμο εφόσον πλέον μπορούμε να καλέσουμε τις public μεθόδους getre, setre() κτλ. από οποιοδήποτε σημείο του κώδικα. Επίσης ο κώδικας του υπερφορτωμένου τελεστή γίνεται σαφώς πιο πολύπλοκος. Μια πιο λογική προσέγγιση θα ήταν να δώσουμε δικαίωμα πρόσβασης στα private μέλη της κλάσης μόνο στον υπερφορτωμένο τελεστή. Η C++ μας δίνει τη δυνατότητα να το κάνουμε δηλώνοντας τον κώδικα του υπερφορτωμένου τελεστή ως friend (φίλου) μέσα στην κλάση. Φίλοι μιας κλάσης μπορεί να είναι τόσο άλλες κλάσεις όσο και συναρτήσεις. Για να δηλώσουμε μία κλάση ή μία συνάρτηση ως φίλο μίας κλάσης πρέπει μέσα στο σώμα της κλάσης να γράψουμε τη λέξη friend ακολουθούμενο από το όνομα της άλλης κλάσης ή την επικεφαλίδα της συνάρτησης που θέλουμε να κάνουμε φίλο. Στην περίπτωσή μας αρκεί να γράψουμε μέσα στην κλάση 1 friend void operator +=( cplex left, cplex right); Τώρα το πρόγραμμα μεταγλωττίζεται κανονικά. Παρόλα αυτά είναι αρκετά πιο σύνθετο από την περίπτωση της προηγούμενης ενότητας όπου υπερφορτώσαμε τον τελεστή μέσα στην κλάση. Για ποιο λόγο λοιπόν να θέλουμε να υπερφορτώσουμε έναν τελεστή έξω από μία κλάση; Θυμηθείτε ότι όταν υπερφορτώνουμε έναν δυαδικό τελεστή στην ουσία γράφουμε μία μέθοδο η οποία θα κληθεί για ένα συγκεκριμένο αντικείμενο: τον αριστερό τελεστέο. Αν όμως ο αριστερός τελεστέος δεν είναι αντικείμενο ή δεν είναι αντικείμενο που έχουμε στη διάθεσή μας τον κώδικά του δεν μπορούμε να τον υπερφορτώσουμε μέσα στην κλάση στην οποία ανήκει είτε γιατί δεν υπάρχει τέτοια κλάση είτε γιατί δεν μπορούμε να αλλάξουμε τον κώδικά της. Το παρακάτω παράδειγμα θα κάνει τα προηγούμενα πιο σαφή. Υποθέστε ότι έχουμε το παρακάτω: 1 class cplex { 2 private: 3 float re, im; 4 5 public: 5
6 cplex() { re = im = 0; } 7 cplex( float _re, float _im) { re = _re; im = _im; } 8 9 void operator +=( cplex b) { re += b. re; im += b. im; } 10 cplex operator +( cplex other) { return cplex( re + other.re, im + other. im); } 11 }; Έχουμε δηλαδή τη γνωστή μας κλάση και έχουμε υπερφορτώσει τον τελεστή + για να προσθέτουμε μιγαδικούς. Έτσι λοιπόν, αν έχουμε δύο αντικείμενα cplex a, b; μπορούμε να προσθέσουμε το ένα στο άλλο γράφοντας a + b;. Δυστυχώς, αν γράψουμε a + 2; το πρόγραμμα δεν μεταγλωττίζεται. Αποφασίζουμε λοιπόν να υπερφορτώσουμε τον τελεστή + άλλη μία φορά, αλλά με όρισμα έναν απλό αριθμό. Έτσι λοιπόν γράφουμε 1 void operator +( float other) { re += other; } και τελειώσαμε. Τώρα μπορούμε να γράφουμε a + 2; και να παίζει μια χαρά. Θυμηθείτε ότι όταν γράφουμε a + 2; είναι σαν να γράφουμε a.operator+(2); δηλαδή να καλούμε μία μέθοδο του αντικειμένου a με παράμετρο έναν float. Αν όμως πάμε να γράψουμε το προφανές 2 + a; το πρόγραμμα δεν μεταγλωττίζεται. Ο λόγος είναι ότι για τους float δεν έχει κανείς υπερφορτώσει τον τελεστή + για να παίρνει όρισμα μιγαδικό και φυσικά ούτε και θα μπορούσε καθώς οι float δεν είναι αντικείμενα κάποιας κλάσης. Μία άλλη περίπτωση είναι η υπερφόρτωση του τελεστή << ώστε να μπορούμε να τυπώνουμε αντικείμενα κάπως έτσι: cout << a;. Ούτε εδώ μπορούμε να κάνουμε υπερφόρτωση μέσα στην κλάση καθώς δεν έχουμε τον κώδικα που υλοποιεί το αντικείμενο cout (ή πιο σωστά δεν θα έπρεπε να τον πειράξουμε). Οπότε για να καλύψουμε περιπτώσεις σαν τις δύο παραπάνω θα έπρεπε να γράψουμε τις παρακάτω συναρτήσεις έξω από το σώμα της κλάσης 1 cplex operator +( float f, cplex a) 2 { 3 return cplex(f + a.re, a. im); 4 } 5 6 ostream& operator <<( ostream& ost, cplex a) 7 { 8 return ost << "(" << a. re << ", " << a. im << "j)"; 9 } και μέσα στο σώμα της κλάσης να τις δηλώσουμε ως φίλες με τις παρακάτω εντολές: 1 friend cplex operator +( float f, cplex a); 2 friend ostream& operator <<( ostream& ost, cplex a); 3 Υπερφόρτωση μοναδιαίων τελεστών Οι μοναδιαίοι τελεστές είναι γενικά απλούστερη περίπτωση. Μιας και παίρνουν ένα και μόνο όρισμα, αυτό αφορά κλάσεις που έχουμε γράψει οι ίδιοι οπότε η υπερφόρτωση γίνεται μέσα στο σώμα της κλάσης. Οι πιο συνηθισμένοι μοναδιαίοι τελεστές είναι οι +, -, ~,!, ++ και --. Οι 4 πρώτοι 6
γράφονται πριν το όρισμά τους οπότε για να τους υπερφορτώσουμε γράφουμε τα γνωστά χωρίς κανένα όρισμα στη λίστα παραμέτρων. Π.χ. για να υπερφορτώσουμε τον μοναδιαίο - γράφουμε το παρακάτω 1 cplex operator -() { return cplex(-re, -im); } Παρατηρήστε ότι οι 4 πρώτοι τελεστές γράφονται πάντα πριν το όρισμά τους. Κάτι ανάλογο μπορούμε να γράψουμε και για τους τελεστές μοναδιαίας αύξησης και μείωσης ++ και --. Αν π.χ. γράψουμε την μέθοδο void operator++() {... μπορούμε να γράψουμε κώδικα σαν αυτόν ++a;, δηλαδή να υπερφορτώσουμε τον προθεματικό τελεστή μοναδιαίας αύξησης. Τότε όμως, πώς θα μπορούσαμε να υπερφορτώσουμε τον μεταθεματικό τελεστή μοναδιαίας αύξησης, για να μπορούμε δηλαδή να γράψουμε a++; Η λύση εδώ είναι ένα τέχνασμα: Για να μπορεί ο μεταγλωττιστής να ξεχωρίσει ότι αναφερόμαστε στον μεταθεματικό τελεστή δηλώνουμε ένα εικονικό όρισμα (δεν πρόκειται να χρησιμοποιηθεί) στον ορισμό της υπερφόρτωσης: 1 void operator++(int) {... } Συνοψίζοντας: Σχεδόν όλοι οι μοναδιαίοι τελεστές γράφονται πριν το όρισμά τους. Οπότε για να τους υπερφορτώσουμε γράφουμε την ανάλογη μέθοδο χωρίς παραμέτρους. Το ίδιο κάνουμε και για τους προθεματικούς τελεστές ++ και --, δηλαδή γράφουμε τις μεθόδους υπερφόρτωσης χωρίς παραμέτρους. Για να υπερφορτώσουμε τους μεταθεματικούς ++ και -- γράφουμε την αντίστοιχη μέθοδο αλλά στη λίστα παραμέτρων βάζουμε ένα εικονικό όρισμα το οποίο μάλιστα αφού δεν χρησιμοποιείται δεν χρειάζεται να του δώσουμε όνομα, γράφουμε δηλαδή κάτι τέτοιο: (int). Το τελικό πρόγραμμα με τις υπερφορτώσεις που έχουμε συζητήσει μέχρι στιγμής καθώς και κάποιες εντολές για να δούμε τα αποτελέσματα των τελεστών μας είναι το παρακάτω: 1 # include <iostream > 2 using namespace std; 3 4 class cplex { 5 private: 6 float re, im; 7 8 public: 9 cplex() { re = im = 0; } 10 cplex( float _re, float _im) { re = _re; im = _im; } 11 12 void operator +=( cplex b) { re += b. re; im += b. im; } 13 cplex operator + ( cplex other) { return cplex( re + other.re, im + other. im); } 14 15 cplex operator -() { return cplex(-re, -im); } 16 cplex operator ++() { return cplex(++re, im); } 17 cplex operator ++( int) { return cplex( re++, im); } 7
18 friend cplex operator +( float f, cplex a); 19 friend ostream& operator <<( ostream& ost, cplex a); 20 21 }; 22 23 cplex operator +( float f, cplex a) 24 { 25 return cplex(f + a.re, a. im); 26 } 27 28 ostream& operator <<( ostream& ost, cplex a) 29 { 30 return ost << "(" << a. re << ", " << a. im << "j)"; 31 } 32 33 int main() 34 { 35 cplex a(3,4); 36 cplex b(2,5); 37 cout << "a: " << a << endl; 38 cout << "b: " << b << endl; 39 cout << "a + b: " << a + b << endl; 40 cout << "2 + a: " << 2 + a << endl; 41 cout << "-a: " << -a << endl; 42 cout << " ++a:" << ++a << endl; 43 cout << "a: " << a << endl; 44 cout << "a++:" << a++ << endl; 45 cout << "a: " << a << endl; 46 47 return 0; 48 } 4 Ο προσδιοριστής const Σε πολλές περιπτώσεις θέλουμε να δηλώσουμε ότι κάποια ποσότητα στο πρόγραμμά μας πρέπει να μένει σταθερή, δηλαδή να μην επιτρέπεται σε κανέναν να τη μεταβάλλει. Η έννοια της σταθερότητας μπορεί να έχει αρκετές ενδιαφέρουσες προεκτάσεις. 4.1 Σταθερές μεταβλητές και παράμετροι Ας υποθέσουμε ότι θέλουμε να χρησιμοποιήσουμε σε ένα πρόγραμμά μας τον αριθμό π, δηλαδή τον μισό του λόγου της περιφέρειας ενός κύκλου προς την ακτίνα του. Αυτή η ποσότητα είναι σταθερή οπότε δεν θα θέλαμε σε ένα πρόγραμμα σαν το παρακάτω 1 float pi = 3.141592; 8
2 pi = 5; να επιτραπεί η εκτέλεση της δεύτερης εντολής. Για να πετύχουμε αυτόν το σκοπό δηλώνουμε τη μεταβλητή που μας ενδιαφέρει να μένει σταθερή και προσθέτουμε τον προσδιοριστή const αμέσως πριν ή αμέσως μετά το όνομα του τύπου, δηλαδή γράφουμε 1 const float pi = 3.141592; και πλέον δεν επιτρέπεται η ανάθεση άλλης τιμής στην μεταβλητή pi. Ο γενικός κανόνας για τον προσδιοριστή const είναι ότι αναφέρεται σε αυτό που είναι αμέσως προς τα αριστερά του, εκτός αν δεν υπάρχει κάτι στα αριστερά οπότε αναφέρεται σε αυτό που βρίσκεται προς τα δεξιά. Δηλαδή, η δήλωση const float pi; σημαίνει ότι ακριβώς και η float const pi; δηλαδή ότι η pi είναι τύπου σταθερός float δηλαδή ένας float που δεν επιτρέπεται να αλλάξει. Η δήλωση float const * f σημαίνει ότι η f είναι ένας δείκτης σε σταθερό float, δηλαδή μπορούμε να αλλάξουμε την τιμή του δείκτη f αλλά όχι τα περιεχόμενα της θέσης μνήμης στην οποία δείχνει. Η δήλωση float * const f σημαίνει ότι ο δείκτης f είναι σταθερός δηλαδή δείχνει πάντα την ίδια θέση μνήμης γιατί δεν μπορούμε να τον αλλάξουμε αλλά τα περιεχόμενα της θέσης μνήμης στην οποία δείχνει μπορούν να μεταβληθούν. Επίσης η δήλωση float const * const f σημαίνει ότι ο f είναι ένας σταθερός δείκτης, δηλαδή δεν μπορούμε να τον κάνουμε να δείχνει σε άλλη θέση μνήμης και επίσης τα περιεχόμενα της θέσης μνήμης αυτής είναι σταθερά. Το ίδιο μπορεί να γίνει και για παραμέτρους συναρτήσεων και μεθόδων, δηλαδή ενώ το παρακάτω επιτρέπεται 1 void f(int a) 2 { a = 12; } το επόμενο χτυπάει 1 void f( const int a) 2 { a = 12; } Η έννοια της σταθερότητας που αναφέραμε παραπάνω θα μπορούσε να ονομαστεί φυσική σταθερότητα. Δηλαδή, δηλώνοντας μια μεταβλητή ή παράμετρο ως const η γλώσσα δεν επιτρέπει τη μεταβολή της στη μνήμη με την έννοια ότι κανένα από τα bits που αντιστοιχούν σε αυτήν τη μεταβλητή δεν επιτρέπεται να αλλάξει τιμή. Υπάρχει και μια άλλη έννοια που θα μπορούσε να αποδοθεί στη σταθερότητα και θα μπορούσαμε να την ονομάσουμε λογική σταθερότητα. Σύμφωνα με αυτήν, μια ποσότητα θα μπορούσε να θεωρείται αμετάβλητη ακόμα και αν άλλαζαν οι τιμές της αρκεί να συνέχιζε να πληρεί κάποιες ιδιότητες. Ένα παράδειγμα: υποθέστε ότι γράφετε μια κλάση η οποία να αναπαριστά ρητούς αριθμούς, δηλαδή πηλίκα ενός αριθμητή και ενός παρονομαστή. Το πλεονέκτημα αυτής της αναπαράστασης είναι ότι δεν χάνουμε δεκαδικά ψηφία σε περιπτώσεις πράξεων που δεν δίνουν στρογγυλό αποτέλεσμα όπως για παράδειγμα το είκοσι διά έξι. Αν προσπαθήσουμε να το αναπαραστήσουμε ως δεκαδικό γράφεται 3.33333... αλλά καθώς ο υπολογιστής δεν μπορεί να αναπαραστήσει άπειρα δεκαδικά ψηφία χάνουμε σε ακρίβεια. Οπότε προτιμούμε την αναπαράσταση αυτού του αριθμού ως ένα ζευγάρι (20,6). Ας υποθέσουμε επίσης ότι γράφουμε μία κλάση για να αναπαριστά αυτά τα ζευγάρια. Αν γράψουμε μία μέθοδο που κάνει απλοποίηση αυτή θα μπορούσε να αλλάξει τις τιμές του προηγούμενου ζευγαριού και από (20,6) να τις κάνει (10,3). Σε αυτήν την περίπτωση, τα bits που αναπαριστούσαν τις τιμές αυτές 9
έχουν αλλάξει. Δηλαδή το αντικείμενο δεν παρέμεινε σταθερό από φυσική άποψη. Αλλά από λογική άποψη, συνεχίζει να αναπαριστά τον ίδιο αριθμό οπότε παραμένει υπ αυτήν την έννοια σταθερό. Οπότε, όταν λέμε ότι ένα αντικείμενο είναι const, η γλώσσα απαγορεύει την απ ευθείας μεταβολή των μελών του, δηλαδή επιβάλλει φυσική σταθερότητα. Αυτό γίνεται εύκολα γιατί η C++ μπορεί να τσεκάρει κάτι τέτοιο. Αυτό που δεν μπορεί όμως να τσεκάρει εύκολα είναι αν οι μέθοδοι που γράφουμε για τα αντικείμενα τα αφήνουν σταθερά από λογική άποψη ή όχι. Για να βοηθήσουμε τη γλώσσα, δηλώνουμε ποιες μέθοδοι δεν μταβάλλουν από λογική άποψη τα αντικείμενα, παραθέτοντας τον προσδιοριστή const αμέσως μετά τις παρενθέσεις μέσα στις οποίες μπαίνει η λίστα ορισμάτων της μεθόδου. Για παράδειγμα, ο υπερφορτωμένος τελεστής πρόσθεσης που αναφέραμε στην κλάση cplex, δεν μεταβάλλει το αντικείμενο για το οποίο καλείται. Για να το δηλώσουμε θα έπρεπε να γράψουμε 1 cplex operator + ( cplex other) const {... } Ανακεφαλαιώνοντας λοιπόν, αν δηλώσουμε ένα αντικείμενο const απαγορεύεται η απ ευθείας αλλαγή των μελών δεδομένων του και επιτρέπεται η κλήση μεθόδων τύπου const όπως ο υπερφορτωμένος τελεστής πρόσθεσης παραπάνω. Κλείνουμε το θέμα με μία παρατήρηση, η οποία μπορεί να θεωρηθεί ψιλά γράμματα. Αν αφήσουμε τον υπερφορτωμένο τελεστή πρόσθεσης όπως παραπάνω ίσως κάποτε διαπιστώσουμε το εξής πρόβλημα: Αν υποθέσουμε ότι έχουμε δύο αντικείμενα a και b τύπου cplex και με δεδομένα τα παραδείγματα που έχουμε μέχρι στιγμής παρουσιάσει, τότε εντολές σαν την παρακάτω επιτρέπονται. 1 (a+b) = a; Ίσως δεν είναι προφανές γιατί αυτό δεν θα έπρεπε να επιτρέπεται. Αν δοκιμάσετε να το κάνετε με δύο ακέραιες μεταβλητές η C++ θα σας δώσει μήνυμα λάθους. Αν δοκιμάστε να μεταγλωττίσετε το παρακάτω 1 int x,y; 2 (x+y) = x; θα πάρετε το μήνυμα λάθους lvalue required as left operand of assignment, δηλαδή για να αποθηκεύσουμε μία τιμή με τον τελεστή = κάπου, πρέπει αυτό που βρίσκεται αριστερά από το ίσον να είναι μια νόμιμη lvalue δηλαδή μια περιοχή μνήμης στην οποία να έχει δικαίωμα εγγραφής ο προγραμματιστής. Όταν λοιπόν λέμε (a+b) = a; κάνουμε το εξής λάθος: Το άθροισμα a+b δεν αντιστοιχεί σε κάποια ονοματισμένη θέση μνήμης την οποία μπορούμε αργότερα να προσπελάσουμε. Το αποτέλεσμα a+b είναι ένα προσωρινό αντικείμενο που αρχικοποιείται από τον υπερφορτωμένο τελεστή της πρόσθεσης, επιστρέφεται από αυτόν και ζει για λίγο στην στοίβα από όπου σύντομα καταστρέφεται. Οπότε για να είμαστε πιο ακριβείς, το πρόβλημα με την ανάθεση (a+b) = a; δεν είναι ότι δεν θα έπρεπε να επιτρέπεται γιατί δεν μπορεί να πραγματοποιηθεί αλλά γιατί δεν έχει νόημα να πραγματοποιηθεί αφού μετά δεν μπορούμε να έχουμε πρόσβαση στην περιοχή της στοίβας που αποθηκεύτηκε το αποτέλεσμα. Μπορούμε να αποφύγουμε τέτοιες αβλεψίες δηλώνοντας ότι το αντικείμενο που επιστρέφει ο υπερφορτωμένος τελεστής της πρόσθεσης είναι και αυτό const κάπως έτσι: 1 const cplex operator + ( cplex other) const {... } 10