Τελευταία ενημέρωση: 12.08.2018

Μαζί με τις μεθόδους, μπορούμε επίσης να υπερφορτώσουμε τους τελεστές. Για παράδειγμα, ας υποθέσουμε ότι έχουμε την ακόλουθη κλάση Counter:

Μετρητής τάξης (δημόσια τιμή int ( get; set; ) )

Αυτή η κλάση αντιπροσωπεύει κάποιο μετρητή, η τιμή του οποίου αποθηκεύεται στην ιδιότητα Value.

Και ας υποθέσουμε ότι έχουμε δύο αντικείμενα της κλάσης Counter - δύο μετρητές που θέλουμε να συγκρίνουμε ή να προσθέσουμε με βάση την ιδιότητά τους Value χρησιμοποιώντας τυπικές πράξεις σύγκρισης και πρόσθεσης:

Μετρητής c1 = νέος μετρητής ( Τιμή = 23 ); Μετρητής c2 = νέος Μετρητής( Τιμή = 45 ); bool αποτέλεσμα = c1 > c2; Μετρητής c3 = c1 + c2;

Αλλά αυτή τη στιγμή, ούτε η λειτουργία σύγκρισης ούτε η λειτουργία πρόσθεσης είναι διαθέσιμες για αντικείμενα Counter. Αυτές οι λειτουργίες μπορούν να χρησιμοποιηθούν σε έναν αριθμό πρωτόγονων τύπων. Για παράδειγμα, από προεπιλογή μπορούμε να προσθέσουμε αριθμητικές τιμές, αλλά ο μεταγλωττιστής δεν ξέρει πώς να προσθέσει αντικείμενα σύνθετων τύπων - κλάσεις και δομές. Και για αυτό πρέπει να υπερφορτίσουμε τους χειριστές που χρειαζόμαστε.

Η υπερφόρτωση τελεστή συνίσταται στον καθορισμό μιας ειδικής μεθόδου στην κλάση για τα αντικείμενα της οποίας θέλουμε να ορίσουμε έναν τελεστή:

δημόσιος στατικός τελεστής τύπου return_type (παράμετροι) ( )

Αυτή η μέθοδος πρέπει να έχει δημόσιους στατικούς τροποποιητές, καθώς ο υπερφορτωμένος τελεστής θα χρησιμοποιηθεί για όλα τα αντικείμενα αυτής της κλάσης. Ακολουθεί το όνομα του τύπου επιστροφής. Ο τύπος επιστροφής αντιπροσωπεύει τον τύπο του οποίου τα αντικείμενα θέλουμε να ανακτήσουμε. Για παράδειγμα, προσθέτοντας δύο αντικείμενα Counter, αναμένουμε να λάβουμε ένα νέο αντικείμενο Counter. Και ως αποτέλεσμα της σύγκρισης των δύο, θέλουμε να πάρουμε ένα αντικείμενο τύπου bool, το οποίο υποδεικνύει εάν η υπό όρους έκφραση είναι true ή false. Αλλά ανάλογα με την εργασία, οι τύποι επιστροφής μπορεί να είναι οτιδήποτε.

Στη συνέχεια, αντί για το όνομα της μεθόδου, υπάρχει η λέξη-κλειδί τελεστής και ο ίδιος ο τελεστής. Και στη συνέχεια οι παράμετροι παρατίθενται σε αγκύλες. Οι δυαδικοί τελεστές παίρνουν δύο παραμέτρους, οι δυαδικοί τελεστές παίρνουν μία παράμετρο. Και σε κάθε περίπτωση, μία από τις παραμέτρους πρέπει να αντιπροσωπεύει τον τύπο - την κλάση ή τη δομή - στην οποία ορίζεται ο τελεστής.

Για παράδειγμα, ας υπερφορτώσουμε έναν αριθμό τελεστών για την κλάση Counter:

Μετρητής κλάσης ( δημόσιος int Τιμή ( get; set; ) δημόσιος στατικός τελεστής μετρητή +(Μετρητής c1, μετρητής c2) (επιστροφή νέου μετρητή (Τιμή = c1.Value + c2.Value); ) δημόσιος στατικός τελεστής bool >(Μετρητής c1, Μετρητής c2) ( επιστροφή c1.Value > c2.Value; ) δημόσιος στατικός τελεστής bool<(Counter c1, Counter c2) { return c1.Value < c2.Value; } }

Δεδομένου ότι όλοι οι υπερφορτωμένοι τελεστές είναι δυαδικοί - δηλαδή εκτελούνται σε δύο αντικείμενα, υπάρχουν δύο παράμετροι για κάθε υπερφόρτωση.

Εφόσον στην περίπτωση της πράξης πρόσθεσης θέλουμε να προσθέσουμε δύο αντικείμενα της κλάσης Counter, ο τελεστής δέχεται δύο αντικείμενα αυτής της κλάσης. Και επειδή θέλουμε να λάβουμε ένα νέο αντικείμενο Counter ως αποτέλεσμα της πρόσθεσης, αυτή η κλάση χρησιμοποιείται επίσης ως τύπος επιστροφής. Όλες οι ενέργειες αυτού του τελεστή καταλήγουν στη δημιουργία ενός νέου αντικειμένου, η ιδιότητα Value του οποίου συνδυάζει τις τιμές της ιδιότητας Value και των δύο παραμέτρων:

Δημόσιος στατικός τελεστής μετρητή +(Μετρητής c1, μετρητής c2) ( επιστροφή νέου μετρητή ( Τιμή = c1.Value + c2.Value ); )

Έχουν επίσης επανακαθοριστεί δύο τελεστές σύγκρισης. Εάν επαναπροσδιορίσουμε έναν από αυτούς τους τελεστές σύγκρισης, τότε πρέπει επίσης να επαναπροσδιορίσουμε τον δεύτερο από αυτούς τους τελεστές σύγκρισης. Οι ίδιοι οι τελεστές σύγκρισης συγκρίνουν τις τιμές των ιδιοτήτων Value και, ανάλογα με το αποτέλεσμα της σύγκρισης, επιστρέφουν είτε true είτε false.

Τώρα χρησιμοποιούμε υπερφορτωμένους τελεστές στο πρόγραμμα:

Static void Main(string args) ( Counter c1 = new Counter ( Value = 23 ); Counter c2 = new Counter ( Value = 45 ); Bool αποτέλεσμα = c1 > c2; Console.WriteLine(result); // false Counter c3 = c1 + c2;Console.WriteLine(c3.Value); // 23 + 45 = 68 Console.ReadKey(); )

Αξίζει να σημειωθεί ότι αφού ο ορισμός τελεστή είναι ουσιαστικά μέθοδος, μπορούμε να υπερφορτώσουμε και αυτή τη μέθοδο, δηλαδή να δημιουργήσουμε μια άλλη έκδοση για αυτήν. Για παράδειγμα, ας προσθέσουμε έναν άλλο τελεστή στην κλάση Counter:

Δημόσιος στατικός τελεστής int +(Μετρητής c1, int val) (επιστρέφει c1.Value + val; )

Αυτή η μέθοδος προσθέτει την τιμή της ιδιότητας Value και κάποιο αριθμό, επιστρέφοντας το άθροισμά τους. Και επίσης μπορούμε να εφαρμόσουμε αυτόν τον τελεστή:

Μετρητής c1 = νέος μετρητής ( Τιμή = 23 ); int d = c1 + 27; // 50 Console.WriteLine(d);

Λάβετε υπόψη ότι η υπερφόρτωση δεν πρέπει να αλλάξει τα αντικείμενα που μεταβιβάζονται στον χειριστή μέσω παραμέτρων. Για παράδειγμα, μπορούμε να ορίσουμε έναν τελεστή αύξησης για την κλάση Counter:

Δημόσιος στατικός τελεστής μετρητή ++(Μετρητής c1) ( c1.Value += 10; επιστροφή c1; )

Εφόσον ο τελεστής είναι μονομερής, χρειάζεται μόνο μία παράμετρος - ένα αντικείμενο της κλάσης στην οποία ορίζεται αυτός ο τελεστής. Αλλά αυτός είναι ένας εσφαλμένος ορισμός της προσαύξησης, καθώς ο χειριστής δεν πρέπει να αλλάξει τις τιμές των παραμέτρων του.

Και μια πιο σωστή υπερφόρτωση του τελεστή αύξησης θα μοιάζει με αυτό:

Δημόσιος στατικός τελεστής μετρητή ++(Μετρητής c1) (επιστροφή νέου μετρητή ( Τιμή = c1.Τιμή + 10 ); )

Δηλαδή, επιστρέφεται ένα νέο αντικείμενο που περιέχει την προσαυξημένη τιμή στην ιδιότητα Value.

Ταυτόχρονα, δεν χρειάζεται να ορίσουμε χωριστούς τελεστές για προσαύξηση προθέματος και μετάθεμα (καθώς και μείωση), καθώς μια υλοποίηση θα λειτουργήσει και στις δύο περιπτώσεις.

Για παράδειγμα, χρησιμοποιούμε τη λειτουργία αύξησης του προθέματος:

Μετρητής μετρητής = νέος Μετρητής() ( Τιμή = 10 ); Console.WriteLine($"(counter.Value)"); // 10 Console.WriteLine($"((++counter).Value)"); // 20 Console.WriteLine($"(counter.Value)"); // είκοσι

Έξοδος κονσόλας:

Τώρα χρησιμοποιούμε postfix increment:

Μετρητής μετρητής = νέος Μετρητής() ( Τιμή = 10 ); Console.WriteLine($"(counter.Value)"); // 10 Console.WriteLine($"((counter++).Value)"); // 10 Console.WriteLine($"(counter.Value)"); // είκοσι

Έξοδος κονσόλας:

Αξίζει επίσης να σημειωθεί ότι μπορούμε να παρακάμψουμε τους τελεστές true και false. Για παράδειγμα, ας τα ορίσουμε στην κλάση Counter:

Μετρητής κλάσης ( δημόσιος int Τιμή ( get; set; ) δημόσιος στατικός τελεστής bool true(Μετρητής c1) (επιστρέφει c1.Value != 0; ) δημόσιος στατικός τελεστής bool false(Μετρητής c1) (επιστρέφει c1.Value == 0; ) // υπόλοιπο περιεχόμενο της τάξης )

Αυτοί οι τελεστές υπερφορτώνονται όταν θέλουμε να χρησιμοποιήσουμε ένα αντικείμενο του τύπου ως συνθήκη. Για παράδειγμα:

Μετρητής μετρητής = νέος Μετρητής() ( Τιμή = 0 ); if (counter) Console.WriteLine(true); else Console.WriteLine(false);

Κατά την υπερφόρτωση τελεστών, λάβετε υπόψη ότι δεν μπορούν να υπερφορτωθούν όλοι οι χειριστές. Συγκεκριμένα, μπορούμε να υπερφορτίσουμε τους ακόλουθους τελεστές:

    μοναδικοί τελεστές +, -, !, ~, ++, --

    δυαδικούς τελεστές +, -, *, /, %

    πράξεις σύγκρισης ==, !=,<, >, <=, >=

    λογικοί τελεστές &&, ||

    τελεστές ανάθεσης +=, -=, *=, /=, %=

Και υπάρχει ένας αριθμός τελεστών που δεν μπορούν να υπερφορτωθούν, όπως ο τελεστής ισότητας = ή ο τριαδικός τελεστής ?: , και ορισμένοι άλλοι.

Μια πλήρης λίστα με υπερφορτωμένους χειριστές μπορείτε να βρείτε στην τεκμηρίωση msdn

Όταν υπερφορτώνετε τελεστές, να θυμάστε επίσης ότι δεν μπορούμε να αλλάξουμε την προτεραιότητα ενός τελεστή ή τη συσχέτισή του, δεν μπορούμε να δημιουργήσουμε έναν νέο τελεστή ή να αλλάξουμε τη λογική των τελεστών σε τύπους, που είναι η προεπιλογή στο .NET.

Καλή μέρα!

Η επιθυμία να γραφτεί αυτό το άρθρο εμφανίστηκε μετά την ανάγνωση της ανάρτησης, επειδή πολλά σημαντικά θέματα δεν αποκαλύφθηκαν σε αυτό.

Το πιο σημαντικό πράγμα που πρέπει να θυμάστε είναι ότι η υπερφόρτωση χειριστή είναι απλώς ένας πιο βολικός τρόπος για να καλέσετε λειτουργίες, επομένως μην παρασυρθείτε με την υπερφόρτωση του χειριστή. Θα πρέπει να χρησιμοποιείται μόνο όταν διευκολύνει τη σύνταξη κώδικα. Όχι όμως τόσο που να δυσκολεύει το διάβασμα. Άλλωστε, όπως γνωρίζετε, ο κώδικας διαβάζεται πολύ πιο συχνά από ό, τι γράφεται. Και μην ξεχνάτε ότι δεν θα σας επιτραπεί ποτέ να υπερφορτώνετε τελεστές σε συνδυασμό με ενσωματωμένους τύπους, μόνο προσαρμοσμένοι τύποι/κλάσεις μπορούν να υπερφορτωθούν.

Σύνταξη υπερφόρτωσης

Η σύνταξη για την υπερφόρτωση τελεστή είναι πολύ παρόμοια με τον ορισμό μιας συνάρτησης με όνομα [email προστατευμένο], όπου @ είναι το αναγνωριστικό του τελεστή (π.χ. +, -,<<, >>). Σκεφτείτε το απλούστερο παράδειγμα:
class Integer ( private: int value; public: Integer(int i): value(i) () const Ακέραιος τελεστής+(const Integer& rv) const ( return (value + rv.value); ) );
Σε αυτήν την περίπτωση, ο τελεστής πλαισιώνεται ως μέλος της κλάσης, το όρισμα καθορίζει την τιμή που βρίσκεται στη δεξιά πλευρά του τελεστή. Γενικά, υπάρχουν δύο κύριοι τρόποι υπερφόρτωσης τελεστών: καθολικές συναρτήσεις που είναι φιλικές προς την τάξη ή ενσωματωμένες συναρτήσεις της ίδιας της κλάσης. Ποια μέθοδος, για ποιον χειριστή είναι καλύτερος, θα εξετάσουμε στο τέλος του θέματος.

Στις περισσότερες περιπτώσεις, οι τελεστές (εκτός από όρους) επιστρέφουν ένα αντικείμενο ή μια αναφορά στον τύπο στον οποίο αναφέρονται τα ορίσματά του (εάν οι τύποι είναι διαφορετικοί, τότε αποφασίζετε πώς να ερμηνεύσετε το αποτέλεσμα της αξιολόγησης τελεστή).

Υπερφόρτωση Unary Operators

Εξετάστε παραδείγματα υπερφόρτωσης unary τελεστών για την κλάση Integer που ορίστηκε παραπάνω. Ταυτόχρονα, θα τις ορίσουμε ως συναρτήσεις φίλου και θα εξετάσουμε τους τελεστές μείωσης και αύξησης:
κλάση Integer ( private: int value; public: Integer(int i): value(i) () //unary + friend const Ακέραιος& τελεστής+(const Integer& i); //unary - φίλος const Ακέραιος τελεστής-(const Integer& i) . Ακέραιος τελεστής--(Integer& i, int); ); // unary plus δεν κάνει τίποτα. const Ακέραιος& operator+(const Integer& i) ( return i.value; ) const Integer operator-(const Integer& i) ( return Integer(-i.value); ) //prefix version επιστρέφει τιμή μετά την αύξηση const Ακέραιος& operator++(Integer& i) ( i.value++; return i; ) //postfix version επιστρέφει την τιμή πριν από την αύξηση const Ακέραιος τελεστής++(Integer& i, int) ( Integer oldValue(i.value); i.value++; return oldValue; ) //επιστρέφει η έκδοση προθέματος η τιμή μετά τη μείωση const Ακέραιος& operator--(Integer& i) ( i.value--; return i; ) //postfix version επιστρέφει τιμή πριν από τη μείωση const Ακέραιος τελεστής--(Integer& i, int) ( Integer oldValue(i.value ); i .value--; επιστροφή oldValue;)
Τώρα ξέρετε πώς ο μεταγλωττιστής διακρίνει μεταξύ των εκδόσεων προθέματος και μετάθεμα της μείωσης και της αύξησης. Στην περίπτωση που δει την έκφραση ++i, τότε καλείται η συνάρτηση τελεστή++(α). Αν δει i++, τότε καλείται ο τελεστής++(a, int). Δηλαδή, καλείται η συνάρτηση υπερφορτωμένου operator++ και για αυτό χρησιμοποιείται η παράμετρος dummy int στην έκδοση postfix.

Δυαδικοί τελεστές

Εξετάστε τη σύνταξη για την υπερφόρτωση δυαδικών τελεστών. Ας υπερφορτώσουμε έναν τελεστή που επιστρέφει μια τιμή l, ένα υπό όρους χειριστήκαι μια δήλωση που δημιουργεί μια νέα τιμή (τις ορίζουμε συνολικά):
κλάση Integer ( private: int value; public: Integer(int i): value(i) () friend const Ακέραιος τελεστής+(const Ακέραιος& αριστερά, const Ακέραιος& δεξιά); φίλος Ακέραιος& χειριστής+=(Ακέραιος& αριστερά, const Ακέραιος& δεξιά); φίλος; bool operator==(const Integer& αριστερά, const Integer& δεξιά); ); const Ακέραιος τελεστής+(const Ακέραιος& αριστερά, const Ακέραιος& δεξιά) ( return Integer(left.value + right.value); ) Integer& operator+=(Integer& left, const Integer& right) ( left.value += right.value; return left; ) bool operator==(const Integer& left, const Integer& right) ( return left.value == right.value; )
Σε όλα αυτά τα παραδείγματα, οι τελεστές είναι υπερφορτωμένοι για τον ίδιο τύπο, ωστόσο, αυτό δεν απαιτείται. Είναι δυνατόν, για παράδειγμα, να υπερφορτωθεί η προσθήκη του τύπου μας Integer και ενός Float που ορίζεται στην ομοιότητα του.

Ορίσματα και τιμές επιστροφής

Όπως μπορείτε να δείτε, τα παραδείγματα χρησιμοποιούν διάφορους τρόπουςμεταβίβαση ορισμάτων σε συναρτήσεις και επιστροφή τιμών τελεστή.
  • Εάν το όρισμα δεν τροποποιηθεί από τον τελεστή, στην περίπτωση, για παράδειγμα, ενός μοναρίου συν, πρέπει να μεταβιβαστεί ως αναφορά σε μια σταθερά. Σε γενικές γραμμές, αυτό ισχύει σχεδόν για όλους. αριθμητικοί τελεστές(πρόσθεση, αφαίρεση, πολλαπλασιασμός...)
  • Ο τύπος της επιστρεφόμενης τιμής εξαρτάται από τη φύση του τελεστή. Εάν ο χειριστής πρέπει να επιστρέψει μια νέα τιμή, τότε πρέπει να δημιουργηθεί ένα νέο αντικείμενο (όπως στην περίπτωση του δυαδικού συν). Εάν θέλετε να αποτρέψετε την αλλαγή ενός αντικειμένου ως τιμή l, τότε πρέπει να το επιστρέψετε ως const.
  • Οι χειριστές ανάθεσης πρέπει να επιστρέψουν μια αναφορά στο αλλαγμένο στοιχείο. Επίσης, εάν θέλετε να χρησιμοποιήσετε τον τελεστή εκχώρησης σε δομές όπως (x=y).f(), όπου καλείται η συνάρτηση f() για τη μεταβλητή x, αφού της αντιστοιχίσετε το y, τότε μην επιστρέψετε αναφορά σε μια σταθερά, απλώς επιστρέψτε μια αναφορά.
  • Οι λογικοί τελεστές θα πρέπει να επιστρέφουν το int στη χειρότερη και το bool στην καλύτερη.

Βελτιστοποίηση απόδοσης αξίας

Όταν δημιουργείτε νέα αντικείμενα και τα επιστρέφετε από μια συνάρτηση, θα πρέπει να χρησιμοποιείτε τη σημείωση όπως στο παράδειγμα του τελεστή δυαδικού συν που περιγράφεται παραπάνω.
return Integer(left.value + right.value);
Για να είμαι ειλικρινής, δεν ξέρω ποια κατάσταση είναι σχετική με τη C++11, όλα τα παρακάτω επιχειρήματα ισχύουν για τη C++98.
Με την πρώτη ματιά, αυτό μοιάζει με τη σύνταξη για τη δημιουργία ενός προσωρινού αντικειμένου, που σημαίνει ότι δεν υπάρχει διαφορά μεταξύ του παραπάνω κώδικα και αυτού:
Ακέραιος temp(left.value + right.value); θερμοκρασία επιστροφής?
Αλλά στην πραγματικότητα, σε αυτήν την περίπτωση, ο κατασκευαστής θα κληθεί στην πρώτη γραμμή, μετά θα κληθεί ο κατασκευαστής αντιγραφής, ο οποίος θα αντιγράψει το αντικείμενο και, στη συνέχεια, όταν ξετυλιχθεί η στοίβα, θα κληθεί ο καταστροφέας. Όταν χρησιμοποιείται η πρώτη καταχώρηση, ο μεταγλωττιστής δημιουργεί αρχικά ένα αντικείμενο στη μνήμη στο οποίο πρέπει να αντιγραφεί, αποθηκεύοντας έτσι την κλήση στον κατασκευαστή αντιγραφής και στον καταστροφέα.

Ειδικοί Χειριστές

Υπάρχουν τελεστές στη C++ που έχουν συγκεκριμένη σύνταξη και μέθοδο υπερφόρτωσης. Για παράδειγμα, ο τελεστής ευρετηρίου . Ορίζεται πάντα ως μέλος της κλάσης και δεδομένου ότι η συμπεριφορά του αντικειμένου με ευρετήριο ως πίνακας προορίζεται, θα πρέπει να επιστρέψει μια αναφορά.
χειριστής κόμματος
Οι «ειδικοί» τελεστές περιλαμβάνουν και τον τελεστή κόμματος. Καλείται για αντικείμενα που έχουν κόμμα δίπλα τους (αλλά δεν καλείται στις λίστες ορισμάτων συναρτήσεων). Η δημιουργία ενός ουσιαστικού παραδείγματος χρήσης αυτού του τελεστή δεν είναι τόσο εύκολη. Ο Habrauser στα σχόλια του προηγούμενου άρθρου σχετικά με την υπερφόρτωση .
Χειριστής αποαναφοράς δείκτη
Η υπερφόρτωση αυτών των τελεστών μπορεί να δικαιολογηθεί για έξυπνες κατηγορίες δεικτών. Αυτός ο τελεστής ορίζεται απαραίτητα ως συνάρτηση κλάσης και του επιβάλλονται ορισμένοι περιορισμοί: πρέπει να επιστρέψει είτε ένα αντικείμενο (ή μια αναφορά), είτε έναν δείκτη που σας επιτρέπει να έχετε πρόσβαση στο αντικείμενο.
χειριστή ανάθεσης
Ο τελεστής εκχώρησης ορίζεται απαραίτητα ως συνάρτηση κλάσης, επειδή είναι άρρηκτα συνδεδεμένος με το αντικείμενο στα αριστερά του "=". Ο καθολικός καθορισμός του τελεστή εκχώρησης θα καθιστούσε δυνατή την παράκαμψη της τυπικής συμπεριφοράς του τελεστή "=". Παράδειγμα:
κλάση Integer ( private: int value; public: Integer(int i): value(i) () Integer& operator=(const Integer& right) ( // έλεγχος για αυτο-ανάθεση if (this == &right) ( return *this; ) value = right.value; return *this; ) );

Όπως μπορείτε να δείτε, στην αρχή της λειτουργίας, γίνεται έλεγχος για αυτο-ανάθεση. Γενικά, σε αυτή την περίπτωση, η αυτο-ανάθεση είναι ακίνδυνη, αλλά η κατάσταση δεν είναι πάντα τόσο απλή. Για παράδειγμα, εάν το αντικείμενο είναι μεγάλο, μπορείτε να αφιερώσετε πολύ χρόνο κάνοντας περιττή αντιγραφή ή όταν εργάζεστε με δείκτες.

Χειριστές χωρίς υπερφόρτωση
Ορισμένοι τελεστές στη C++ δεν είναι καθόλου υπερφορτωμένοι. Προφανώς, αυτό γίνεται για λόγους ασφαλείας.
  • Ο τελεστής επιλογής μέλους τάξης ".".
  • Δείκτης προς τον τελεστή αποαναφοράς μέλους κλάσης ".*"
  • Δεν υπάρχει τελεστής εκθέσεως στη C++ (όπως στο Fortran) "**".
  • Απαγορεύεται να ορίσετε τους χειριστές σας (είναι πιθανά προβλήματα με την ιεράρχηση προτεραιοτήτων).
  • Η προτεραιότητα χειριστή δεν μπορεί να αλλάξει
Όπως έχουμε ήδη ανακαλύψει, υπάρχουν δύο τρόποι τελεστών - με τη μορφή μιας συνάρτησης κλάσης και με τη μορφή μιας καθολικής συνάρτησης φίλου.
Ο Rob Murray, στο βιβλίο του C++ Strategies and Tactics, έχει προσδιορίσει τις ακόλουθες οδηγίες για την επιλογή της φόρμας χειριστή:

Γιατί αυτό? Πρώτον, ορισμένοι χειριστές είναι αρχικά περιορισμένοι. Γενικά, εάν σημασιολογικά δεν υπάρχει διαφορά στον τρόπο ορισμού ενός τελεστή, τότε είναι καλύτερα να τον τακτοποιήσετε ως συνάρτηση κλάσης για να τονιστεί η σύνδεση, συν, επιπλέον, η συνάρτηση θα είναι ενσωματωμένη (inline). Επιπλέον, μερικές φορές μπορεί να είναι απαραίτητο να αναπαραστήσουμε τον αριστερό τελεστή με ένα αντικείμενο άλλης κλάσης. Ίσως το πιο εντυπωσιακό παράδειγμα είναι ο επαναπροσδιορισμός<< и >> για ροές I/O.

Στο Κεφάλαιο 15, θα εξετάσουμε δύο είδη ειδικών συναρτήσεων: υπερφορτωμένους τελεστές και μετατροπές που καθορίζονται από τον χρήστη. Καθιστούν δυνατή τη χρήση αντικειμένων κλάσης σε εκφράσεις με τον ίδιο διαισθητικό τρόπο όπως τα αντικείμενα των ενσωματωμένων τύπων. Σε αυτό το κεφάλαιο, θα περιγράψουμε πρώτα τις γενικές έννοιες σχεδιασμού για υπερφορτωμένους χειριστές. Στη συνέχεια εισάγουμε την έννοια των φίλων τάξης με ειδικά δικαιώματα πρόσβασης και συζητάμε γιατί χρησιμοποιούνται, εστιάζοντας στον τρόπο υλοποίησης ορισμένων από τους υπερφορτωμένους τελεστές: ανάθεση, συνδρομή, κλήση, βέλος πρόσβασης μέλους τάξης, αύξηση και μείωση και εξειδικευμένοι τελεστές για την τάξη νέους τελεστές και διαγραφή. Μια άλλη κατηγορία ειδικών συναρτήσεων που καλύπτονται σε αυτό το κεφάλαιο είναι οι συναρτήσεις μετατροπής μελών (μετατροπείς), οι οποίες είναι ένα σύνολο τυπικών μετατροπών για έναν τύπο κλάσης. Εφαρμόζονται σιωπηρά από τον μεταγλωττιστή όταν τα αντικείμενα κλάσης χρησιμοποιούνται ως πραγματικά ορίσματα συνάρτησης ή ως τελεστές ενσωματωμένων ή υπερφορτωμένων τελεστών. Το κεφάλαιο ολοκληρώνεται με μια λεπτομερή περίληψη των κανόνων για την επίλυση της υπερφόρτωσης συναρτήσεων, λαμβάνοντας υπόψη τη διαβίβαση αντικειμένων ως ορίσματα, τις συναρτήσεις μέλους κλάσης και τους υπερφορτωμένους τελεστές.

15.1. Υπερφόρτωση χειριστή

Έχουμε ήδη δείξει σε προηγούμενα κεφάλαια ότι η υπερφόρτωση τελεστή επιτρέπει στον προγραμματιστή να εισαγάγει τις δικές του εκδόσεις των προκαθορισμένων τελεστών (βλ. Κεφάλαιο 4) για τελεστές τύπου κλάσης. Για παράδειγμα, η κλάση String στην Ενότητα 3.15 έχει πολλούς υπερφορτωμένους τελεστές. Παρακάτω είναι ο ορισμός του:

#περιλαμβάνω κλάση String; istream& operator>>(istream &, const String &); ροή & χειριστής<<(ostream &, const String &); class String { public: // набор перегруженных конструкторов // для автоматической инициализации String(const char* = 0); String(const String &); // деструктор: автоматическое уничтожение ~String(); // набор перегруженных операторов присваивания String& operator=(const String &); String& operator=(const char *); // перегруженный оператор взятия индекса char& operator(int); // набор перегруженных операторов равенства // str1 == str2; bool operator==(const char *); bool operator==(const String &); // функции доступа к членам int size() { return _size; }; char * c_str() { return _string; } private: int _size; char *_string; };

Η κλάση String έχει τρία σύνολα υπερφορτωμένων τελεστών. Το πρώτο είναι ένα σύνολο τελεστών ανάθεσης:

Πρώτα έρχεται ο τελεστής αντιγραφής. (Συζητήθηκαν λεπτομερώς στην Ενότητα 14.7.) Η ακόλουθη πρόταση υποστηρίζει την εκχώρηση μιας συμβολοσειράς C χαρακτήρων σε ένα αντικείμενο τύπου String:

όνομα συμβολοσειράς; name = "Σέρλοκ"; // χρησιμοποιώντας τον τελεστή τελεστή=(char *)

(Οι χειριστές ανάθεσης εκτός από τις εκχωρήσεις αντιγραφής θα συζητηθούν στην Ενότητα 15.3.)

Το δεύτερο σύνολο έχει μόνο έναν τελεστή - λαμβάνοντας το ευρετήριο:

// υπερφορτωμένος τελεστής ευρετηρίου char& operator(int);

Επιτρέπει σε ένα πρόγραμμα να ευρετηριάζει αντικείμενα της κλάσης String με τον ίδιο τρόπο όπως πίνακες αντικειμένων του ενσωματωμένου τύπου:

Αν (όνομα != "S") κόουτ<<"увы, что-то не так\n";

(Αυτός ο χειριστής περιγράφεται λεπτομερώς στην ενότητα 15.4.)

Το τρίτο σύνολο ορίζει υπερφορτωμένους τελεστές ισότητας για αντικείμενα της κλάσης String. Ένα πρόγραμμα μπορεί να ελέγξει εάν δύο τέτοια αντικείμενα είναι ίσα ή εάν ένα αντικείμενο και μια συμβολοσειρά C είναι ίσα:

// σύνολο υπερφορτωμένων τελεστών ισότητας // str1 == str2; bool operator==(const char *); bool operator==(const String &);

Οι υπερφορτωμένοι τελεστές επιτρέπουν σε αντικείμενα ενός τύπου κλάσης να χρησιμοποιούνται με τους τελεστές που ορίζονται στο Κεφάλαιο 4 και να χειρίζονται τόσο διαισθητικά όσο αντικείμενα ενσωματωμένων τύπων. Για παράδειγμα, αν θέλαμε να ορίσουμε τη λειτουργία της σύνδεσης δύο αντικειμένων String, θα μπορούσαμε να την εφαρμόσουμε ως συνάρτηση μέλους concat(). Αλλά γιατί concat() και όχι, ας πούμε, append(); Το όνομα που επιλέξαμε είναι λογικό και θυμάται εύκολα, αλλά ο χρήστης μπορεί να ξεχάσει αυτό που ονομάσαμε τη συνάρτηση. Το όνομα είναι συχνά πιο εύκολο να θυμάστε εάν ορίσετε έναν υπερφορτωμένο τελεστή. Για παράδειγμα, αντί για concat() θα καλούσαμε τον νέο τελεστή λειτουργίας+=(). Ένας τέτοιος χειριστής χρησιμοποιείται ως εξής:

#include "String.h" int main() ( String name1 "Sherlock"; String name2 "Holmes"; name1 += " "; name1 += name2; if (! (name1 == "Sherlock Holmes")) cout< < "конкатенация не сработала\n"; }

Ένας υπερφορτωμένος τελεστής δηλώνεται στο σώμα της κλάσης ακριβώς όπως μια συνάρτηση κανονικού μέλους, με τη διαφορά ότι το όνομά του αποτελείται από τον τελεστή λέξης-κλειδιού που ακολουθείται από έναν από τους πολλούς προκαθορισμένους τελεστές C++ (βλ. Πίνακα 15-1). Έτσι μπορείτε να δηλώσετε τον τελεστή+=() στην κλάση String:

Κλάση String ( δημόσια: // σύνολο υπερφορτωμένων τελεστών += String& operator+=(const String &); String& operator+=(const char *); // ... private: // ... );

και ορίστε το ως εξής:

#περιλαμβάνω inline String& String::operator+=(const String &rhs) ( // Εάν η συμβολοσειρά που αναφέρεται από rhs δεν είναι κενή εάν (rhs._string) ( String tmp(*this); // εκχωρήστε αρκετή μνήμη για να // κρατήσετε το συνενωμένο των συμβολοσειρών _size += rhs._size; διαγραφή _string; _string = νέος χαρακτήρας[ _size + 1 ]; // αντιγράψτε πρώτα την αρχική συμβολοσειρά στην επιλεγμένη περιοχή // στη συνέχεια προσθέστε στο τέλος τη συμβολοσειρά που αναφέρεται από rhs strcpy(_string, tmp ._string) ; strcpy(_string + tmp._size, rhs._string); ) return *this; ) inline String& String::operator+=(const char *s) ( // Αν ο δείκτης s είναι μη μηδενικός αν (s) ( String tmp(*this); // εκχωρήστε μια περιοχή μνήμης επαρκή // για την αποθήκευση των συγχωνευμένων συμβολοσειρών _size += strlen(s); διαγράψτε _string; _string = νέο char[ _size + 1]; // αντιγράψτε πρώτα τη συμβολοσειρά προέλευσης στο εκχωρημένη περιοχή // στη συνέχεια προσάρτηση στο τέλος της συμβολοσειράς C που αναφέρεται από s strcpy(_string, tmp._string); strcpy(_string + tmp._size, s); ) return *this; )

15.1.1. Μέλη και μη μιας τάξης

Ας ρίξουμε μια πιο προσεκτική ματιά στους τελεστές ισότητας στην κλάση String. Ο πρώτος τελεστής σάς επιτρέπει να ορίσετε την ισότητα δύο αντικειμένων και ο δεύτερος - το αντικείμενο και η συμβολοσειρά C:

#include "String.h" int main() ( String flower; // γράψτε κάτι στη μεταβλητή λουλούδι if (λουλούδι == "κρίνος") // σωστό // ... else if ("tulip" == flower ) // λάθος // ... )

Την πρώτη φορά που χρησιμοποιείτε τον τελεστή ισότητας στο main(), καλείται ο υπερφορτωμένος τελεστής==(const char *) της κλάσης String. Ωστόσο, στη δεύτερη δήλωση if, ο μεταγλωττιστής στέλνει ένα μήνυμα σφάλματος. Τι συμβαίνει?

Ένας υπερφορτωμένος τελεστής που είναι μέλος μιας κλάσης ισχύει μόνο όταν ο αριστερός τελεστής είναι αντικείμενο αυτής της κλάσης. Εφόσον στη δεύτερη περίπτωση ο αριστερός τελεστής δεν ανήκει στην κλάση String, ο μεταγλωττιστής προσπαθεί να βρει έναν ενσωματωμένο τελεστή για τον οποίο ο αριστερός τελεστής μπορεί να είναι μια συμβολοσειρά C και ο δεξιός τελεστής μπορεί να είναι αντικείμενο κλάσης String. Φυσικά, δεν υπάρχει, οπότε ο μεταγλωττιστής λέει ένα σφάλμα.

Αλλά μπορείτε επίσης να δημιουργήσετε ένα αντικείμενο της κλάσης String από μια συμβολοσειρά C χρησιμοποιώντας τον κατασκευαστή κλάσης. Γιατί ο μεταγλωττιστής δεν κάνει σιωπηρά αυτήν τη μετατροπή:

Αν (String("tulip") == λουλούδι) //correct: καλείται ο τελεστής μέλους

Ο λόγος είναι η αναποτελεσματικότητά του. Οι υπερφορτωμένοι τελεστές δεν απαιτούν και οι δύο τελεστές να είναι του ίδιου τύπου. Για παράδειγμα, η κλάση Text ορίζει τους ακόλουθους τελεστές ισότητας:

Κείμενο κλάσης ( public: Text(const char * = 0); Text(const Text &); // σύνολο υπερφορτωμένων τελεστών ισότητας bool operator==(const char *) const; bool operator==(const String &) const; bool operator==(const Text &) const; // ... );

και η έκφραση στο main() μπορεί να ξαναγραφτεί ως εξής:

Αν καλείται (Κείμενο("τουλίπα") == λουλούδι) // Κείμενο::τελεστής==()

Επομένως, για να βρει έναν κατάλληλο τελεστή ισότητας για σύγκριση, ο μεταγλωττιστής θα πρέπει να ψάξει μέσα από όλους τους ορισμούς κλάσεων αναζητώντας έναν κατασκευαστή που μπορεί να μεταφέρει τον αριστερό τελεστή σε κάποιον τύπο κλάσης. Στη συνέχεια, για καθέναν από αυτούς τους τύπους, πρέπει να ελέγξετε όλους τους υπερφορτωμένους τελεστές ισότητας που σχετίζονται με αυτόν για να δείτε εάν κάποιος από αυτούς μπορεί να εκτελέσει τη σύγκριση. Και τότε ο μεταγλωττιστής πρέπει να αποφασίσει ποιος από τους συνδυασμούς που βρέθηκαν του κατασκευαστή και του τελεστή ισότητας (αν υπάρχει) ταιριάζει καλύτερα με τον τελεστή στη δεξιά πλευρά! Εάν απαιτείτε από τον μεταγλωττιστή να εκτελέσει όλες αυτές τις ενέργειες, τότε ο χρόνος μετάφρασης των προγραμμάτων C ++ θα αυξηθεί δραματικά. Αντίθετα, ο μεταγλωττιστής εξετάζει μόνο υπερφορτωμένους τελεστές που ορίζονται ως μέλη της αριστερής κλάσης τελεστών (και των βασικών κλάσεων του, όπως θα δείξουμε στο Κεφάλαιο 19).

Επιτρέπεται, ωστόσο, ο ορισμός υπερφορτωμένων τελεστών που δεν είναι μέλη της κλάσης. Κατά την ανάλυση μιας γραμμής στο main() που προκάλεσε σφάλμα μεταγλώττισης, τέτοιες δηλώσεις λήφθηκαν υπόψη. Έτσι, μια σύγκριση στην οποία η συμβολοσειρά C βρίσκεται στην αριστερή πλευρά μπορεί να γίνει έγκυρη αντικαθιστώντας τους τελεστές ισότητας που είναι μέλη της κλάσης String με τελεστές ισότητας που δηλώνονται στο πεδίο εφαρμογής του χώρου ονομάτων:

Bool operator==(const String &, const String &); bool operator==(const String &, const char *);

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

Λουλούδι == "κρίνος"

ξαναγράφεται από τον μεταγλωττιστή ως:

flower.operator==("κρίνος")

και ο αριστερός τελεστής του flower στον ορισμό ενός υπερφορτωμένου τελεστή μέλους μπορεί να αναφέρεται με αυτό. (Ο δείκτης αυτός εισήχθη στην Ενότητα 13.4.) Στην περίπτωση ενός γενικού υπερφορτωμένου τελεστή, η παράμετρος που αντιπροσωπεύει τον αριστερό τελεστή πρέπει να προσδιορίζεται ρητά.

Μετά η έκφραση

Λουλούδι == "κρίνος"

καλεί τον χειριστή

Bool operator==(const String &, const char *);

Δεν είναι σαφές ποιος τελεστής καλείται για τη δεύτερη περίπτωση χρήσης του τελεστή ισότητας:

«τουλίπα» == λουλούδι

Δεν έχουμε ορίσει έναν τέτοιο υπερφορτωμένο τελεστή:

Bool operator==(const char *, const String &);

Αλλά αυτό είναι προαιρετικό. Όταν ένας υπερφορτωμένος τελεστής είναι μια συνάρτηση σε έναν χώρο ονομάτων, τόσο η πρώτη όσο και η δεύτερη παράμετρός του (αριστερός και δεξιός τελεστής) θεωρούνται πιθανές μετατροπές, δηλ. ο μεταγλωττιστής ερμηνεύει τη δεύτερη χρήση του τελεστή ισότητας ως

Operator==(String("tulip"), flower);

και καλεί τον ακόλουθο υπερφορτωμένο τελεστή για να εκτελέσει τη σύγκριση: bool operator==(const String &, const String &);

Αλλά τότε γιατί παρέχουμε έναν δεύτερο υπερφορτωμένο τελεστή: bool operator==(const String &, const char *);

Η μετατροπή τύπου από μια συμβολοσειρά C στην κλάση String μπορεί επίσης να εφαρμοστεί στον δεξιό τελεστή. Η συνάρτηση main() θα μεταγλωττιστεί χωρίς σφάλματα αν ορίσουμε απλώς έναν υπερφορτωμένο τελεστή στον χώρο ονομάτων που παίρνει δύο τελεστές συμβολοσειράς:

Bool operator==(const String &, const String &);

Είτε παρέχεται μόνο αυτή η δήλωση ή δύο ακόμη:

Bool operator==(const char *, const String &); bool operator==(const String &, const char *);

εξαρτάται από το πόσο μεγάλο είναι το κόστος μετατροπής από μια συμβολοσειρά C σε μια συμβολοσειρά κατά την εκτέλεση, δηλαδή από το «κόστος» των επιπλέον κλήσεων κατασκευαστή σε προγράμματα που χρησιμοποιούν την κλάση String. Εάν ο τελεστής ισότητας χρησιμοποιείται συχνά για τη σύγκριση συμβολοσειρών και αντικειμένων C, τότε είναι καλύτερο να παρέχετε και τις τρεις επιλογές. (Θα επιστρέψουμε στο θέμα της αποτελεσματικότητας στην ενότητα για τους φίλους.

Θα καλύψουμε τη χύτευση σε τύπο κλάσης με κατασκευαστές με περισσότερες λεπτομέρειες στην Ενότητα 15.9. Η ενότητα 15.10 ασχολείται με την επίλυση υπερφόρτωσης συναρτήσεων χρησιμοποιώντας τους περιγραφόμενους μετασχηματισμούς και η ενότητα 15.12 ασχολείται με την επίλυση υπερφόρτωσης τελεστών.)

Λοιπόν, σε ποια βάση λαμβάνεται η απόφαση εάν θα γίνει ένας τελεστής μέλος μιας κλάσης ή μέλος ενός χώρου ονομάτων; Σε ορισμένες περιπτώσεις, ο προγραμματιστής απλά δεν έχει επιλογή:

  • αν ο υπερφορτωμένος τελεστής είναι μέλος μιας κλάσης, τότε καλείται μόνο εάν ο αριστερός τελεστής είναι μέλος αυτής της κλάσης. Εάν ο αριστερός τελεστής είναι διαφορετικού τύπου, ο τελεστής πρέπει να είναι μέλος του χώρου ονομάτων.
  • Η γλώσσα απαιτεί οι τελεστές ανάθεσης ("="), δείκτης (""), κλήσης ("()") και πρόσβασης μέλους ("->") να ορίζονται ως μέλη της κλάσης. Διαφορετικά, εκδίδεται ένα μήνυμα σφάλματος μεταγλώττισης:
// σφάλμα: πρέπει να είναι μέλος της κλάσης char& operator(String &, int ix);

(Ο τελεστής εκχώρησης συζητείται λεπτομερέστερα στην Ενότητα 15.3, λαμβάνοντας ευρετήριο στην Ενότητα 15.4, επίκληση στην Ενότητα 15.5 και τον τελεστή πρόσβασης μέλους βέλους στην Ενότητα 15.6.)

Διαφορετικά, η απόφαση λαμβάνεται από τον σχεδιαστή της τάξης. Οι συμμετρικοί τελεστές, όπως ο τελεστής ισότητας, ορίζονται καλύτερα σε έναν χώρο ονομάτων εάν οποιοσδήποτε τελεστής μπορεί να είναι μέλος της κλάσης (όπως στο String).

Πριν τελειώσουμε αυτή την υποενότητα, ας ορίσουμε τους τελεστές ισότητας για την κλάση String στον χώρο ονομάτων:

Bool operator==(const String &str1, const String &str2) ( if (str1.size() != str2.size()) return false; return strcmp(str1.c_str(), str2.c_str()) ? false: true ; ) inline bool operator==(const String &str, const char *s) ( return strcmp(str.c_str(), s) ? false: true ; )

15.1.2. Ονόματα υπερφορτωμένων χειριστών

Μόνο προκαθορισμένοι τελεστές γλώσσας C++ μπορούν να υπερφορτωθούν (βλ. Πίνακα 15.1).

Πίνακας 15.1. Χειριστές με υπερφόρτωση

+ - * / % ^ & | ~ ! , = < > <= >= ++ -- << >> == != && || += -= /= %= ^= &= |= *= <= >>= () -> ->* νέα νέα διαγραφή διαγραφή

Ο σχεδιαστής μιας κλάσης δεν μπορεί να δηλώσει έναν τελεστή με διαφορετικό όνομα υπερφορτωμένο. Για παράδειγμα, εάν προσπαθήσετε να δηλώσετε τον τελεστή ** για εκπτώσεις, ο μεταγλωττιστής θα δημιουργήσει ένα μήνυμα σφάλματος.

Οι ακόλουθοι τέσσερις τελεστές γλωσσών C++ δεν μπορούν να υπερφορτωθούν:

// μη υπερφορτωμένοι τελεστές:: .* . ?:

Η προκαθορισμένη εκχώρηση χειριστή δεν μπορεί να αλλάξει για ενσωματωμένους τύπους. Για παράδειγμα, δεν επιτρέπεται η παράκαμψη του ενσωματωμένου τελεστή πρόσθεσης ακεραίων για τον έλεγχο του αποτελέσματος για υπερχείλιση.

// σφάλμα: δεν είναι δυνατή η παράκαμψη του ενσωματωμένου τελεστή προσθήκης int int operator+(int, int);

Επίσης, δεν μπορείτε να ορίσετε πρόσθετους τελεστές για ενσωματωμένους τύπους δεδομένων, όπως η προσθήκη operator+ στο σύνολο των ενσωματωμένων λειτουργιών για την προσθήκη δύο συστοιχιών.

Ένας υπερφορτωμένος τελεστής ορίζεται αποκλειστικά για τελεστές μιας κλάσης ή ενός τύπου απαρίθμησης και μπορεί να δηλωθεί μόνο ως μέλος μιας κλάσης ή χώρου ονομάτων, λαμβάνοντας τουλάχιστον μία παράμετρο κλάσης ή τύπου απαρίθμησης (με τιμή ή με αναφορά).

Οι προκαθορισμένες προτεραιότητες χειριστή (βλ. Ενότητα 4.13) δεν μπορούν να αλλάξουν. Ανεξάρτητα από το είδος της κλάσης και την υλοποίηση του τελεστή στη δήλωση

X == y + z;

Ο operator+ εκτελείται πάντα πρώτος, ακολουθούμενος από τον operator==; Ωστόσο, οι παρενθέσεις μπορούν να χρησιμοποιηθούν για την αλλαγή της σειράς.

Πρέπει επίσης να διατηρηθεί ο προκαθορισμένος αριθμός τελεστών. Για παράδειγμα, unary λογικός τελεστήςΤο NOT δεν μπορεί να οριστεί ως δυαδικός τελεστής σε δύο αντικείμενα της κλάσης String. Η ακόλουθη υλοποίηση είναι εσφαλμένη και θα οδηγήσει σε σφάλμα μεταγλώττισης:

// λάθος: ! είναι ένας μοναδικός τελεστής bool!(const String &s1, const String &s2) ( return (strcmp(s1.c_str(), s2.c_str()) != 0); )

Για ενσωματωμένους τύπους, οι τέσσερις προκαθορισμένοι τελεστές ("+", "-", "*" και "&") χρησιμοποιούνται είτε ως μονομερείς είτε ως δυαδικοί τελεστές. Σε οποιαδήποτε από αυτές τις ιδιότητες, μπορεί να υπερνικηθούν.

Όλοι οι υπερφορτωμένοι τελεστές, με εξαίρεση τον operator(), έχουν μη έγκυρα προεπιλεγμένα ορίσματα.

15.1.3. Ανάπτυξη υπερφορτωμένων χειριστών

Οι τελεστές ανάθεσης και διεύθυνσης και ο τελεστής κόμματος έχουν προκαθορισμένη σημασία εάν οι τελεστές είναι αντικείμενα τύπου κλάσης. Αλλά μπορούν επίσης να υπερφορτωθούν. Η σημασιολογία όλων των άλλων τελεστών, όταν εφαρμόζεται σε τέτοιους τελεστές, πρέπει να προσδιορίζεται ρητά από τον προγραμματιστή. Η επιλογή των φορέων παροχής εξαρτάται από την αναμενόμενη χρήση της κλάσης.

Θα πρέπει να ξεκινήσετε ορίζοντας τη δημόσια διεπαφή του. Το σύνολο των συναρτήσεων δημοσίων μελών σχηματίζεται με βάση τις λειτουργίες που πρέπει να εκθέτει η κλάση στους χρήστες. Στη συνέχεια λαμβάνεται μια απόφαση ποιες λειτουργίες θα πρέπει να υλοποιηθούν ως υπερφορτωμένοι χειριστές.

Αφού ορίσετε τη δημόσια διεπαφή της κλάσης, ελέγξτε αν υπάρχει λογική αντιστοιχία μεταξύ λειτουργιών και τελεστών:

  • Το isEmpty() γίνεται ο τελεστής LOGICAL NOT, operator!().
  • Το isEqual() γίνεται ο τελεστής ισότητας, τελεστής==().
  • Η copy() γίνεται τελεστής εκχώρησης, operator=().

Κάθε τελεστής έχει κάποια φυσική σημασιολογία. Έτσι, το δυαδικό + συνδέεται πάντα με την πρόσθεση και η αντιστοίχιση του σε μια παρόμοια πράξη με μια κλάση μπορεί να είναι μια βολική και συνοπτική σημειογραφία. Για παράδειγμα, για έναν τύπο πίνακα, η προσθήκη δύο πινάκων είναι μια απολύτως κατάλληλη επέκταση του δυαδικού συν.

Ένα παράδειγμα κακής χρήσης της υπερφόρτωσης τελεστή είναι ο ορισμός του operator+() ως αφαίρεσης, κάτι που είναι άσκοπο: η μη διαισθητική σημασιολογία είναι επικίνδυνη.

Ένας τέτοιος χειριστής υποστηρίζει εξίσου καλά πολλές διαφορετικές ερμηνείες. Μια άψογα σαφής και τεκμηριωμένη εξήγηση του τι κάνει ο τελεστής+() είναι απίθανο να ευχαριστήσει τους χρήστες της κλάσης String που υποθέτουν ότι χρησιμοποιείται για συνένωση συμβολοσειρών. Εάν η σημασιολογία ενός υπερφορτωμένου τελεστή δεν είναι προφανής, τότε είναι προτιμότερο να μην την παρέχετε.

Η ισοδυναμία της σημασιολογίας ενός σύνθετου τελεστή και η αντίστοιχη ακολουθία απλών τελεστών για ενσωματωμένους τύπους (για παράδειγμα, η ισοδυναμία ενός + ακολουθούμενη από = και ενός += σύνθετου τελεστή) πρέπει να διατηρηθεί ρητά και για μια κλάση. Ας υποθέσουμε ότι και ο τελεστής+() και ο τελεστής=() έχουν οριστεί για τη συμβολοσειρά για να υποστηρίζουν λειτουργίες συνένωσης και αντιγραφής κατά μέλη:

Συμβολοσειρά s1("C"); Συμβολοσειρά s2("++"); s1 = s1 + s2; // s1 == "C++"

Αλλά αυτό δεν αρκεί για να υποστηρίξει τον τελεστή σύνθετης εκχώρησης

S1 += s2;

Θα πρέπει να ορίζεται ρητά έτσι ώστε να διατηρεί την αναμενόμενη σημασιολογία.

Άσκηση 15.1

Γιατί η ακόλουθη σύγκριση δεν καλεί τον υπερφορτωμένο τελεστή ==(const String&, const String&):

"cobble" == "πέτρα"

Άσκηση 15.2

Γράψτε υπερφορτωμένους τελεστές ανισότητας που μπορούν να χρησιμοποιηθούν σε τέτοιες συγκρίσεις:

String != String String != C-string C-string != String

Εξηγήστε γιατί επιλέξατε να εφαρμόσετε μία ή περισσότερες δηλώσεις.

Άσκηση 15.3

Προσδιορίστε εκείνες τις συναρτήσεις μέλους της κλάσης Screen που υλοποιούνται στο Κεφάλαιο 13 (Ενότητες 13.3, 13.4 και 13.6) που μπορεί να υπερφορτωθούν.

Άσκηση 15.4

Εξηγήστε γιατί οι υπερφορτωμένοι τελεστές εισόδου και εξόδου που ορίζονται για την κλάση String στην Ενότητα 3.15 δηλώνονται ως καθολικές συναρτήσεις και όχι ως συναρτήσεις μέλους.

Άσκηση 15.5

Εφαρμόστε υπερφορτωμένους τελεστές εισόδου και εξόδου για την κλάση Screen από το Κεφάλαιο 13.

15.2. Οι φιλοι

Εξετάστε ξανά τους υπερφορτωμένους τελεστές ισότητας για την κλάση String, που ορίζονται στο πεδίο του χώρου ονομάτων. Ο τελεστής ισότητας για δύο αντικείμενα String μοιάζει με αυτό:

Bool operator==(const String &str1, const String &str2) ( if (str1.size() != str2.size()) return false; return strcmp(str1.c_str(), str2.c_str()) ? false: αλήθεια;)

Συγκρίνετε αυτόν τον ορισμό με τον ορισμό του ίδιου τελεστή ως συνάρτηση μέλους:

Bool String::operator==(const String &rhs) const ( if (_size != rhs._size) return false; return strcmp(_string, rhs._string) ? false: true; )

Έπρεπε να τροποποιήσουμε τον τρόπο πρόσβασης στα ιδιωτικά μέλη της κλάσης String. Αφού ο νέος τελεστής ισότητας είναι παγκόσμια λειτουργία, δεν είναι συνάρτηση μέλους, δεν έχει πρόσβαση στα ιδιωτικά μέλη της κλάσης String. Οι συναρτήσεις μέλους size() και c_str() χρησιμοποιούνται για να ληφθεί το μέγεθος ενός αντικειμένου String και η υποκείμενη C-string χαρακτήρων.

Μια εναλλακτική υλοποίηση είναι να δηλωθούν οι τελεστές παγκόσμιας ισότητας ως φίλοι της κλάσης String. Εάν μια λειτουργία ή φορέας δηλώνεται με αυτόν τον τρόπο, του παρέχεται πρόσβαση σε μη δημόσια μέλη.

Η δήλωση φίλου (η οποία ξεκινά με τη λέξη-κλειδί φίλος) εμφανίζεται μόνο εντός του ορισμού της κλάσης. Εφόσον οι φίλοι δεν είναι μέλη της τάξης που δηλώνει φιλίες, δεν έχει σημασία σε ποιο τμήμα - δημόσιο, ιδιωτικό ή προστατευόμενο - δηλώνονται. Στο παρακάτω παράδειγμα, αποφασίσαμε να τοποθετήσουμε όλες αυτές τις δηλώσεις αμέσως μετά την κεφαλίδα της κλάσης:

Συμβολοσειρά κλάσης ( φίλος bool operator==(const String &, const String &); friend bool operator==(const char *, const String &); friend bool operator==(const String &, const char *); public: // ... το υπόλοιπο της κλάσης String);

Σε αυτές τις τρεις γραμμές, τρεις υπερφορτωμένοι τελεστές σύγκρισης που ανήκουν στο παγκόσμιο εύρος δηλώνονται φίλοι της κλάσης String και επομένως, στους ορισμούς τους, μπορείτε να έχετε απευθείας πρόσβαση στα ιδιωτικά μέλη αυτής της κλάσης:

// φίλοι τελεστές έχουν άμεση πρόσβαση σε ιδιωτικά μέλη // της κλάσης String bool operator==(const String &str1, const String &str2) ( if (str1._size != str2._size) return false; return strcmp(str1._string, str2 . _string) ?false: true; ) inline bool operator==(const String &str, const char *s) ( return strcmp(str._string, s) ?false: true; ) // κ.λπ.

Κάποιος θα μπορούσε να υποστηρίξει ότι σε αυτήν την περίπτωση, η άμεση πρόσβαση στα μέλη _size και _string δεν είναι απαραίτητη, καθώς οι ενσωματωμένες συναρτήσεις c_str() και size() είναι εξίσου αποτελεσματικές και εξακολουθούν να διατηρούν την ενθυλάκωση, πράγμα που σημαίνει ότι δεν υπάρχει ιδιαίτερη ανάγκη να δηλώσει τους τελεστές ισότητας για την κλάση String ως φίλους της.

Πώς ξέρετε εάν πρέπει να κάνετε έναν μη μέλος χειριστή φίλο της τάξης ή να χρησιμοποιήσετε λειτουργίες πρόσβασης; Γενικά, ο προγραμματιστής θα πρέπει να διατηρεί στο ελάχιστο τον αριθμό των δηλωμένων λειτουργιών και τελεστών που έχουν πρόσβαση στην εσωτερική αναπαράσταση της κλάσης. Εάν υπάρχουν συναρτήσεις πρόσβασης που παρέχουν ίση απόδοση, τότε θα πρέπει να προτιμώνται, απομονώνοντας έτσι τους τελεστές χώρου ονομάτων από αλλαγές στην αναπαράσταση κλάσης, όπως γίνεται για άλλες συναρτήσεις. Εάν ο προγραμματιστής της κλάσης δεν παρέχει συναρτήσεις πρόσβασης για ορισμένα μέλη και ο τελεστής που δηλώνεται στον χώρο ονομάτων πρέπει να έχει πρόσβαση σε αυτά τα μέλη, τότε η χρήση του μηχανισμού φίλου γίνεται αναπόφευκτη.

Η πιο κοινή χρήση αυτού του μηχανισμού είναι να επιτρέπει σε υπερφορτωμένους χειριστές που δεν είναι μέλη μιας κλάσης να έχουν πρόσβαση στα ιδιωτικά μέλη της. Εάν δεν υπήρχε η ανάγκη διατήρησης συμμετρίας μεταξύ του αριστερού και του δεξιού τελεστή, τότε ο υπερφορτωμένος τελεστής θα ήταν μια συνάρτηση μέλους με πλήρη δικαιώματα πρόσβασης.

Αν και οι δηλώσεις φίλων χρησιμοποιούνται συνήθως σε σχέση με τελεστές, υπάρχουν φορές που μια συνάρτηση σε έναν χώρο ονομάτων, μια συνάρτηση μέλους άλλης κλάσης ή ακόμα και ολόκληρη η τάξηπρέπει να δηλωθούν έτσι. Εάν μια κλάση δηλωθεί ως φίλος της δεύτερης, τότε όλες οι συναρτήσεις μελών της πρώτης κλάσης έχουν πρόσβαση στα μη δημόσια μέλη της άλλης. Ας το εξετάσουμε χρησιμοποιώντας το παράδειγμα συναρτήσεων που δεν είναι τελεστές.

Μια κλάση πρέπει να δηλώσει ως φίλο καθεμία από τις πολλές υπερφορτωμένες συναρτήσεις στις οποίες θέλει να δώσει απεριόριστα δικαιώματα πρόσβασης:

εξωτερικό ostream& storeOn(ostream &, Screen &); εξωτερικό BitMap& storeOn(BitMap &, Screen &); // ... class Screen ( φίλος ostream& storeOn(ostream &, Screen &); φίλος BitMap& storeOn(BitMap &, Screen &); // ... );

Εάν μια συνάρτηση χειρίζεται αντικείμενα δύο διαφορετικών κλάσεων και χρειάζεται πρόσβαση στα μη δημόσια μέλη τους, τότε μια τέτοια συνάρτηση μπορεί είτε να δηλωθεί ως φίλος και των δύο κλάσεων ή να γίνει μέλος της μιας και φίλος της δεύτερης.

Η δήλωση μιας συνάρτησης ως φίλος δύο κλάσεων θα πρέπει να μοιάζει με αυτό:

Παράθυρο τάξης; // αυτή είναι απλώς μια δήλωση της κλάσης Screen ( φίλος bool is_equal(Screen &, Window &); // ... ); class Window ( friend bool is_equal(Screen &, Window &); // ... );

Εάν αποφασίσουμε να κάνουμε τη συνάρτηση μέλος μιας κλάσης και φίλο της δεύτερης, τότε οι δηλώσεις θα κατασκευαστούν ως εξής:

Παράθυρο τάξης; class Screen ( // copy() είναι μέλος της κλάσης Screen Screen& copy(Window &); // ... ); class Window ( // Screen::copy() είναι ένα Window class friend Screen& Screen::copy(Window &); // ... ); Οθόνη& Οθόνη::αντιγραφή(Παράθυρο &) ( /* ... */ )

Μια συνάρτηση μέλους μιας κλάσης δεν μπορεί να δηλωθεί ως φίλος μιας άλλης έως ότου ο μεταγλωττιστής δει τον ορισμό της δικής του κλάσης. Αυτό δεν είναι πάντα δυνατό. Ας υποθέσουμε ότι το Screen πρέπει να δηλώσει ορισμένες συναρτήσεις μέλους του Window ως φίλους του και το Window πρέπει να δηλώσει ορισμένες συναρτήσεις μέλους του Screen με τον ίδιο τρόπο. Σε αυτήν την περίπτωση, ολόκληρη η κλάση Window δηλώνεται ως φίλος της οθόνης:

Παράθυρο τάξης; class Screen (Παράθυρο κλάσης φίλου; // ... );

Τα ιδιωτικά μέλη της κλάσης Screen είναι πλέον προσβάσιμα από οποιαδήποτε συνάρτηση μέλους παραθύρου.

Άσκηση 15.6

Εφαρμόστε τους τελεστές εισόδου και εξόδου που ορίζονται για την κλάση Screen στην Άσκηση 15.5 ως φίλοι και τροποποιήστε τους ορισμούς τους για να αποκτήσετε απευθείας πρόσβαση σε ιδιωτικά μέλη. Ποια υλοποίηση είναι καλύτερη; Εξήγησε γιατί.

15.3. Χειριστής =

Η αντιστοίχιση ενός αντικειμένου σε άλλο αντικείμενο της ίδιας κλάσης γίνεται χρησιμοποιώντας τον τελεστή εκχώρησης αντιγραφής. (Αυτή η ειδική περίπτωση συζητήθηκε στην Ενότητα 14.7.)

Άλλοι τελεστές ανάθεσης μπορούν να οριστούν για μια κλάση. Εάν στα αντικείμενα μιας κλάσης πρέπει να εκχωρηθούν τιμές διαφορετικού τύπου από αυτήν την κλάση, τότε επιτρέπεται να οριστούν τέτοιοι τελεστές που λαμβάνουν παρόμοιες παραμέτρους. Για παράδειγμα, για να υποστηρίξετε την εκχώρηση μιας συμβολοσειράς C σε ένα αντικείμενο String:

Stringcar ("Volks"); αυτοκίνητο = "Studebaker";

παρέχουμε έναν τελεστή που λαμβάνει μια παράμετρο τύπου const char*. Αυτή η λειτουργία έχει ήδη δηλωθεί στην τάξη μας:

Κλάση String ( public: // τελεστής εκχώρησης για char* String& operator=(const char *); // ... private: int _size; char *string; );

Ένας τέτοιος χειριστής υλοποιείται ως εξής. Εάν αντιστοιχιστεί ένας μηδενικός δείκτης σε ένα αντικείμενο String, γίνεται "κενό". Διαφορετικά, του εκχωρείται ένα αντίγραφο της συμβολοσειράς C:

String& String::operator=(const char *sobj) ( // sobj είναι μηδενικός δείκτης εάν (! sobj) ( _size = 0; διαγραφή _string; _string = 0; ) αλλιώς ( _size = strlen(sobj); διαγραφή _string; _string = νέος χαρακτήρας[ _size + 1 ]; strcpy(_string, sobj); ) return *this; )

Η συμβολοσειρά αναφέρεται σε ένα αντίγραφο της συμβολοσειράς C που δείχνει το sobj. Γιατί αντίγραφο; Επειδή δεν μπορείτε να εκχωρήσετε απευθείας το sobj στο μέλος _string:

Χορδή = sobj; // σφάλμα: αναντιστοιχία τύπου

Το sobj είναι δείκτης στο const και επομένως δεν μπορεί να αντιστοιχιστεί σε έναν δείκτη σε "non-const" (βλ. Ενότητα 3.5). Ας αλλάξουμε τον ορισμό του τελεστή εκχώρησης:

String& String::operator=(const *sobj) ( // ... )

Τώρα το _string αναφέρεται απευθείας στη συμβολοσειρά C που απευθύνεται στο sobj. Ωστόσο, αυτό εγείρει άλλα προβλήματα. Θυμηθείτε ότι μια συμβολοσειρά C είναι τύπου const char*. Ο καθορισμός μιας παραμέτρου ως δείκτη σε μια μη σταθερή καθιστά αδύνατη την εκχώρηση:

Αυτοκίνητο = "Studebaker"; // invalid with operator=(char *) !

Άρα δεν υπάρχει επιλογή. Για να αντιστοιχίσετε μια συμβολοσειρά C σε ένα αντικείμενο τύπου String, η παράμετρος πρέπει να είναι τύπου const char*.

Η αποθήκευση στο _string μιας άμεσης αναφοράς στη συμβολοσειρά C που απευθύνεται από το sobj προκαλεί άλλες επιπλοκές. Δεν ξέρουμε τι ακριβώς δείχνει το sobj. Αυτό μπορεί να είναι ένας πίνακας χαρακτήρων που έχει τροποποιηθεί με τρόπο που δεν είναι γνωστός στο αντικείμενο String. Για παράδειγμα:

Char ia = ( "d", "a", "n", "c", "e", "r"); String trap = ia; // trap._string αναφέρεται σε ia ia = "g"; // αλλά δεν χρειαζόμαστε αυτό: // και το ia και το trap._string έχουν τροποποιηθεί

Εάν το trap._string αναφερόταν απευθείας στο ia, τότε το αντικείμενο trap θα παρουσίαζε μια περίεργη συμπεριφορά: η τιμή του θα μπορούσε να αλλάξει χωρίς να καλέσει τις συναρτήσεις μέλους της κλάσης String. Επομένως, πιστεύουμε ότι η εκχώρηση μιας περιοχής μνήμης για την αποθήκευση ενός αντιγράφου της τιμής της συμβολοσειράς C είναι λιγότερο επικίνδυνη.

Σημειώστε ότι ο τελεστής εκχώρησης χρησιμοποιεί το delete. Το μέλος _string περιέχει μια αναφορά σε έναν πίνακα χαρακτήρων που βρίσκεται στο σωρό. Για να αποφευχθεί μια διαρροή, η μνήμη που έχει εκχωρηθεί για την παλιά συμβολοσειρά ελευθερώνεται με τη διαγραφή πριν εκχωρηθεί η μνήμη για τη νέα. Επειδή το _string απευθύνεται σε έναν πίνακα χαρακτήρων, θα πρέπει να χρησιμοποιήσετε την έκδοση πίνακα διαγραφής (βλ. Ενότητα 8.4).

Και μια τελευταία σημείωση για τον χειριστή ανάθεσης. Ο τύπος επιστροφής του είναι μια αναφορά στην κλάση String. Γιατί ένας σύνδεσμος; Το γεγονός είναι ότι για τους ενσωματωμένους τύπους, οι τελεστές εκχώρησης μπορούν να συνδεθούν με αλυσίδα:

// συνένωση τελεστών εκχώρησης int iobj, jobj; iobj = jobj = 63;

Συνδέονται από τα δεξιά προς τα αριστερά, δηλ. στο προηγούμενο παράδειγμα, οι εργασίες γίνονται ως εξής:

iobj = (jobj = 63);

Αυτό είναι επίσης βολικό όταν εργάζεστε με αντικείμενα της κλάσης String: για παράδειγμα, υποστηρίζεται η ακόλουθη κατασκευή:

String ver, ουσιαστικό; ρήμα = ουσιαστικό = "μετρώ";

Η πρώτη εκχώρηση σε αυτήν την αλυσίδα καλεί τον προκαθορισμένο τελεστή για το const char*. Ο τύπος του αποτελέσματος πρέπει να είναι τέτοιος ώστε να μπορεί να χρησιμοποιηθεί ως όρισμα για τον τελεστή αντιγραφής της κλάσης String. Επομένως, αν και η παράμετρος δεδομένου χειριστήείναι τύπου const char *, εξακολουθεί να επιστρέφεται μια αναφορά σε μια συμβολοσειρά.

Οι χειριστές ανάθεσης είναι υπερφορτωμένοι. Για παράδειγμα, η κλάση String έχει αυτό το σύνολο:

// σύνολο υπερφορτωμένων τελεστών ανάθεσης String& operator=(const String &); String& operator=(const char *);

Ένας ξεχωριστός τελεστής εκχώρησης μπορεί να υπάρχει για κάθε τύπο που επιτρέπεται να εκχωρηθεί σε ένα αντικείμενο String. Ωστόσο, όλοι αυτοί οι τελεστές πρέπει να ορίζονται ως συναρτήσεις μέλους της κλάσης.

15.4. Χειριστής λήψης ευρετηρίου

Ο τελεστής λήψης ευρετηρίου () μπορεί να οριστεί σε κλάσεις που αντιπροσωπεύουν μια αφαίρεση του κοντέινερ από το οποίο ανακτώνται μεμονωμένα στοιχεία. Παραδείγματα τέτοιων κοντέινερ είναι η κλάση String, η κλάση IntArray που εισήχθη στο Κεφάλαιο 2 ή το πρότυπο κλάσης διανυσμάτων που ορίζεται στην Τυπική βιβλιοθήκη C++. Ο τελεστής λήψης δείκτη πρέπει να είναι συνάρτηση μέλους της κλάσης.

Οι χρήστες του String πρέπει να μπορούν να διαβάζουν και να γράφουν μεμονωμένους χαρακτήρες του μέλους _string. Θέλουμε να υποστηρίξουμε τον ακόλουθο τρόπο χρήσης αντικειμένων αυτής της κλάσης:

Καταχώρηση συμβολοσειράς ("εξωφρενικό"); Μυκητίαση συμβολοσειράς; για (int ix = 0; ix< entry.size(); ++ix) mycopy[ ix ] = entry[ ix ];

Ο συνδρομητής μπορεί να εμφανίζεται είτε στα αριστερά είτε στα δεξιά του τελεστή εκχώρησης. Για να βρίσκεται στην αριστερή πλευρά, πρέπει να επιστρέψει την τιμή l του ευρετηριασμένου στοιχείου. Για να το κάνουμε αυτό, επιστρέφουμε μια αναφορά:

#περιλαμβάνω inine char& String::operator(int elem) const ( assert(elem >= 0 && elem< _size); return _string[ elem ]; }

Στο ακόλουθο τμήμα, στο μηδενικό στοιχείο του πίνακα χρωμάτων εκχωρείται ο χαρακτήρας "V":

Χρώμα συμβολοσειράς ("βιολετί"); χρώμα[ 0 ] = "V";

Σημειώστε ότι ο ορισμός του τελεστή ελέγχει εάν το ευρετήριο είναι εκτός ορίων στον πίνακα. Η συνάρτηση βιβλιοθήκης C assert() χρησιμοποιείται για αυτό. Είναι επίσης δυνατό να τεθεί μια εξαίρεση που υποδεικνύει ότι η τιμή του elem είναι μικρότερη από 0 ή μεγαλύτερη από το μήκος της συμβολοσειράς C που αναφέρεται από _string. (Η αύξηση και ο χειρισμός εξαιρέσεων συζητήθηκε στο Κεφάλαιο 11.)

15.5. Λειτουργία χειριστή κλήσης

Ο τελεστής κλήσης συνάρτησης μπορεί να υπερφορτωθεί για αντικείμενα τύπου κλάσης. (Έχουμε ήδη δει πώς χρησιμοποιείται όταν συζητάμε τα αντικείμενα συνάρτησης στην Ενότητα 12.3.) Εάν οριστεί μια κλάση που αντιπροσωπεύει μια πράξη, τότε ο αντίστοιχος τελεστής υπερφορτώνεται για να την καλέσει. Για παράδειγμα, για να λάβετε την απόλυτη τιμή ενός αριθμού τύπου int, μπορείτε να ορίσετε την κλάση absInt:

Κλάση absInt ( public: int operator()(int val) ( int αποτέλεσμα = val< 0 ? -val: val; return result; } };

Ο υπερφορτωμένος τελεστής() πρέπει να δηλωθεί ως συνάρτηση μέλους με αυθαίρετο αριθμό παραμέτρων. Οι παράμετροι και οι επιστρεφόμενες τιμές μπορούν να επιτρέπονται οποιουδήποτε τύπου για συναρτήσεις (βλ. Ενότητες 7.2, 7.3 και 7.4). Το operator() καλείται με την εφαρμογή μιας λίστας ορισμάτων σε ένα αντικείμενο της κλάσης στην οποία ορίζεται. Θα δούμε πώς χρησιμοποιείται σε έναν από τους γενικευμένους αλγόριθμους που περιγράφονται στο κεφάλαιο. Στο παρακάτω παράδειγμα, ο γενικός αλγόριθμος transform() καλείται να εφαρμόσει την πράξη που ορίζεται στο absInt σε κάθε στοιχείο του διανύσματος ivec, π.χ. να αντικαταστήσει ένα στοιχείο με την απόλυτη τιμή του.

#περιλαμβάνω #περιλαμβάνω int main() ( int ia = ( -0, 1, -1, -2, 3, 5, -5, 8); διάνυσμα ivec(ia, ia+8); // αντικαταστήστε κάθε στοιχείο με την απόλυτη τιμή του transform(ivec.begin(), ivec.end(), ivec.begin(), absInt()); //...)

Το πρώτο και το δεύτερο όρισμα για τη transform() περιορίζουν το εύρος των στοιχείων στα οποία εφαρμόζεται η λειτουργία absInt. Το τρίτο δείχνει στην αρχή του διανύσματος, όπου θα αποθηκευτεί το αποτέλεσμα της εφαρμογής της πράξης.

Το τέταρτο όρισμα είναι ένα προσωρινό αντικείμενο της κλάσης absInt που δημιουργείται χρησιμοποιώντας τον προεπιλεγμένο κατασκευαστή. Η παρουσίαση του γενικευμένου αλγόριθμου transform() που καλείται από την main() θα μπορούσε να μοιάζει με αυτό:

διάνυσμα typedef ::iterator iter_type; // instantiation of transform() // η πράξη absInt εφαρμόζεται στο διανυσματικό στοιχείο int iter_type transform(iter_type iter, iter_type last, iter_type αποτέλεσμα, absInt func) ( while (iter != last) *result++ = func(*iter++) ; // ονομάζεται absInt::operator() return iter; )

Το func είναι ένα αντικείμενο κλάσης που παρέχει τη λειτουργία absInt που αντικαθιστά ένα int με την απόλυτη τιμή του. Χρησιμοποιείται για την κλήση του υπερφορτωμένου τελεστή() της κλάσης absInt. Το όρισμα *iter μεταβιβάζεται σε αυτόν τον τελεστή, δείχνοντας το στοιχείο του διανύσματος για το οποίο θέλουμε να πάρουμε την απόλυτη τιμή.

15.6. χειριστής βέλους

Ο τελεστής βέλους, ο οποίος επιτρέπει την πρόσβαση μέλους, μπορεί να υπερφορτωθεί για αντικείμενα κλάσης. Πρέπει να οριστεί ως συνάρτηση μέλους και να παρέχει σημασιολογία δείκτη. Η πιο κοινή χρήση αυτού του τελεστή είναι σε κλάσεις που παρέχουν έναν "έξυπνο δείκτη" που συμπεριφέρεται παρόμοια με τους ενσωματωμένους, αλλά παρέχει κάποια πρόσθετη λειτουργικότητα.

Ας υποθέσουμε ότι θέλουμε να ορίσουμε έναν τύπο κλάσης για την αναπαράσταση ενός δείκτη σε ένα αντικείμενο Screen (βλ. Κεφάλαιο 13):

Κλάση ScreenPtr ( // ... private: Screen *ptr; );

Ο ορισμός του ScreenPtr πρέπει να είναι τέτοιος ώστε ένα αντικείμενο αυτής της κλάσης να είναι εγγυημένο ότι δείχνει σε ένα αντικείμενο Screen: σε αντίθεση με τον ενσωματωμένο δείκτη, δεν μπορεί να είναι μηδενικό. Στη συνέχεια, η εφαρμογή μπορεί να χρησιμοποιήσει αντικείμενα τύπου ScreenPtr χωρίς να ελέγξει αν δείχνουν σε κάποιο αντικείμενο Screen. Για να το κάνετε αυτό, πρέπει να ορίσετε την κλάση ScreenPtr με έναν κατασκευαστή, αλλά χωρίς προεπιλεγμένο κατασκευαστή (οι κατασκευαστές συζητήθηκαν λεπτομερώς στην ενότητα 14.2):

Κλάση ScreenPtr ( public: ScreenPtr(const Screen &s) : ptr(&s) ( ) // ... );

Οποιοσδήποτε ορισμός ενός αντικειμένου της κλάσης ScreenPtr πρέπει να περιέχει έναν αρχικοποιητή - ένα αντικείμενο της κλάσης Screen στο οποίο θα αναφέρεται το αντικείμενο ScreenPtr:

ScreenPtr p1; // σφάλμα: Η κλάση ScreenPtr δεν έχει προεπιλεγμένο κατασκευαστή Screen myScreen(4, 4); ScreenPtr ps(myScreen); // σωστά

Προκειμένου η κλάση ScreenPtr να συμπεριφέρεται σαν ενσωματωμένος δείκτης, είναι απαραίτητο να ορίσετε ορισμένους υπερφορτωμένους τελεστές - αποαναφορά (*) και ένα "βέλος" για πρόσβαση σε μέλη:

// υπερφορτωμένοι τελεστές για υποστήριξη της κλάσης συμπεριφοράς δείκτη ScreenPtr ( public: Screen& operator*() ( return *ptr; ) Screen* operator->() ( return ptr; ) // ... ); Ο τελεστής πρόσβασης μέλους είναι μοναδικός, επομένως δεν μεταβιβάζονται παράμετροι σε αυτόν. Όταν χρησιμοποιείται ως μέρος μιας έκφρασης, το αποτέλεσμά της εξαρτάται μόνο από τον τύπο του αριστερού τελεστέου. Για παράδειγμα, στο point->action(); εξετάζεται το είδος του σημείου. Εάν πρόκειται για δείκτη σε κάποιο τύπο κλάσης, τότε ισχύει η σημασιολογία του ενσωματωμένου τελεστή πρόσβασης μέλους. Εάν πρόκειται για αντικείμενο ή για αναφορά σε ένα αντικείμενο, τότε ελέγχεται εάν υπάρχει υπερφορτωμένος τελεστής πρόσβασης σε αυτήν την κλάση. Όταν ορίζεται ένας υπερφορτωμένος τελεστής βέλους, καλείται σε ένα αντικείμενο σημείου, διαφορετικά η εντολή δεν είναι έγκυρη, επειδή ο τελεστής σημείου πρέπει να χρησιμοποιηθεί για να αναφέρεται στα μέλη του ίδιου του αντικειμένου (συμπεριλαμβανομένης της αναφοράς). Ο τελεστής βέλους με υπερφόρτωση πρέπει να επιστρέψει είτε έναν δείκτη σε έναν τύπο κλάσης είτε ένα αντικείμενο της κλάσης στην οποία έχει οριστεί. Εάν επιστραφεί ένας δείκτης, τότε η σημασιολογία του ενσωματωμένου τελεστή βέλους εφαρμόζεται σε αυτόν. Διαφορετικά, η διαδικασία συνεχίζεται αναδρομικά μέχρι να ληφθεί ένας δείκτης ή να εντοπιστεί ένα σφάλμα. Για παράδειγμα, με αυτόν τον τρόπο μπορείτε να χρησιμοποιήσετε το αντικείμενο ps της κλάσης ScreenPtr για πρόσβαση στα μέλη της οθόνης: ps->move(2, 3); Δεδομένου ότι υπάρχει ένα αντικείμενο τύπου ScreenPtr στα αριστερά του τελεστή "βέλος", χρησιμοποιείται ο υπερφορτωμένος τελεστής αυτής της κλάσης, ο οποίος επιστρέφει έναν δείκτη στο αντικείμενο Screen. Στη συνέχεια, ο ενσωματωμένος τελεστής βέλους εφαρμόζεται στην τιμή που ανακτήθηκε για να καλέσει τη συνάρτηση μέλους move(). Το παρακάτω είναι μικρό πρόγραμμαγια να δοκιμάσετε την κλάση ScreenPtr. Ένα αντικείμενο τύπου ScreenPtr χρησιμοποιείται με τον ίδιο τρόπο όπως κάθε αντικείμενο τύπου Screen*: #include #περιλαμβάνω #include "Screen.h" void printScreen(const ScreenPtr &ps) ( cout<< "Screen Object (" << ps->ύψος()<< ", " << ps->πλάτος()<< ")\n\n"; for (int ix = 1; ix <= ps->ύψος(); ++ix) (για (int iy = 1; iy<= ps->πλάτος(); ++iy) cout<get(ix, iy); cout<< "\n"; } } int main() { Screen sobj(2, 5); string init("HelloWorld"); ScreenPtr ps(sobj); // Установить содержимое экрана string::size_type initpos = 0; for (int ix = 1; ix <= ps->ύψος(); ++ix) για (int iy = 1; iy<= ps->πλάτος(); ++iy) ( ps->move(ix, iy); ps->set(init[ initpos++ ]); ) // Εκτύπωση περιεχομένου οθόνης printScreen(ps); επιστροφή 0; )

Φυσικά, τέτοιοι χειρισμοί με δείκτες σε αντικείμενα κλάσης δεν είναι τόσο αποτελεσματικοί όσο η εργασία με ενσωματωμένους δείκτες. Επομένως, ένας έξυπνος δείκτης πρέπει να παρέχει πρόσθετη λειτουργικότητα που είναι σημαντική για την εφαρμογή για να δικαιολογήσει την πολυπλοκότητα της χρήσης του.

15.7. Τελεστές αύξησης και μείωσης

Συνεχίζοντας την ανάπτυξη της εφαρμογής της κλάσης ScreenPtr που εισήχθη στην προηγούμενη ενότητα, ας δούμε δύο ακόμη τελεστές που υποστηρίζονται για ενσωματωμένους δείκτες και που θα ήθελε να έχει ο έξυπνος δείκτης μας: αύξηση (++) και μείωση (--) . Για να χρησιμοποιήσετε την κλάση ScreenPtr για αναφορά στα στοιχεία ενός πίνακα αντικειμένων Screen, πρέπει να προσθέσετε μερικά επιπλέον μέλη.

Αρχικά ορίζουμε ένα νέο μέλος, το μέγεθος, το οποίο είναι είτε μηδέν (που υποδεικνύει ότι το αντικείμενο ScreenPtr δείχνει σε ένα μεμονωμένο αντικείμενο) είτε το μέγεθος του πίνακα που δείχνει το αντικείμενο ScreenPtr. Χρειαζόμαστε επίσης ένα μέλος μετατόπισης που να θυμάται τη μετατόπιση από την αρχή του δεδομένου πίνακα:

Κλάση ScreenPtr ( δημόσια: // ... ιδιωτική: μέγεθος int; // μέγεθος πίνακα: 0 εάν το μόνο αντικείμενο είναι η μετατόπιση int; // μετατόπιση του ptr από την αρχή του πίνακα Screen *ptr; );

Τροποποιήστε τον κατασκευαστή κλάσης ScreenPtr ώστε να αντικατοπτρίζει τη νέα του λειτουργικότητα και τα πρόσθετα μέλη. Ο χρήστης της κλάσης μας πρέπει να περάσει ένα πρόσθετο όρισμα στον κατασκευαστή εάν το αντικείμενο που δημιουργείται δείχνει σε έναν πίνακα:

Κλάση ScreenPtr ( δημόσια: ScreenPtr(Screen &s , int arraySize = 0) : ptr(&s), size (arraySize), offset(0) ( ) private: int size; int offset; Screen *ptr; );

Αυτό το όρισμα καθορίζει το μέγεθος του πίνακα. Για να διατηρήσουμε την ίδια λειτουργικότητα, ας δώσουμε μια προεπιλεγμένη τιμή μηδέν για αυτήν. Έτσι, εάν το δεύτερο όρισμα στον κατασκευαστή παραλειφθεί, τότε το μέλος μεγέθους θα είναι 0 και, επομένως, ένα τέτοιο αντικείμενο θα δείχνει σε ένα μεμονωμένο αντικείμενο οθόνης. Τα αντικείμενα της νέας κλάσης ScreenPtr μπορούν να οριστούν ως εξής:

Screen myScreen(4, 4); ScreenPtr pobj(myScreen); // correct: δείχνει σε ένα αντικείμενο const int arrSize = 10; Οθόνη *parray = new Screen[arrSize]; ScreenPtr parr(*parray, arrSize); // correct: δείχνει σε έναν πίνακα

Τώρα είμαστε έτοιμοι να ορίσουμε υπερφορτωμένους τελεστές αύξησης και μείωσης στο ScreenPtr. Ωστόσο, είναι δύο τύπων: πρόθεμα και μεταθετικό. Ευτυχώς, και οι δύο επιλογές μπορούν να οριστούν. Για έναν τελεστή προθέματος, η δήλωση δεν περιέχει τίποτα απροσδόκητο:

Κλάση ScreenPtr ( public: Screen& operator++(); Screen& operator--(); // ... );

Τέτοιοι τελεστές ορίζονται ως μοναδικές συναρτήσεις τελεστή. Μπορείτε να χρησιμοποιήσετε τον τελεστή προσαύξησης του προθέματος, για παράδειγμα, ως εξής: const int arrSize = 10; Οθόνη *parray = new Screen[arrSize]; ScreenPtr parr(*parray, arrSize); για (int ix = 0; ix

Οι ορισμοί αυτών των υπερφορτωμένων τελεστών δίνονται παρακάτω:

Screen& ScreenPtr::operator++() ( if (μέγεθος == 0) ( cerr<<"не могу инкрементировать указатель для одного объекта\n"; return *ptr; } if (offset >= μέγεθος - 1) ( ορ<< "уже в конце массива\n"; return *ptr; } ++offset; return *++ptr; } Screen& ScreenPtr::operator--() { if (size == 0) { cerr << "не могу декрементировать указатель для одного объекта\n"; return *ptr; } if (offset <= 0) { cerr << "уже в начале массива\n"; return *ptr; } --offset; return *--ptr; }

Για τη διάκριση των τελεστών προθέματος από τους τελεστές postfix, οι δηλώσεις των τελευταίων έχουν μια πρόσθετη παράμετρο τύπου int. Το ακόλουθο τμήμα δηλώνει τις εκδόσεις προθέματος και μεταθέματος των τελεστών αύξησης και μείωσης για την κλάση ScreenPtr:

Κλάση ScreenPtr ( δημόσια: Screen& operator++(); // τελεστές προθέματος Screen& operator--(); Screen& operator++(int); // postfix operators Screen& operator--(int); // ... );

Ακολουθεί μια πιθανή υλοποίηση τελεστών postfix:

Screen& ScreenPtr::operator++(int) ( if (μέγεθος == 0) ( cerr<< "не могу инкрементировать указатель для одного объекта\n"; return *ptr; } if (offset == size) { cerr << "уже на один элемент дальше конца массива\n"; return *ptr; } ++offset; return *ptr++; } Screen& ScreenPtr::operator--(int) { if (size == 0) { cerr << "не могу декрементировать указатель для одного объекта\n"; return *ptr; } if (offset == -1) { cerr <<"уже на один элемент раньше начала массива\n"; return *ptr; } --offset; return *ptr--; }

Σημειώστε ότι δεν είναι απαραίτητο να ονομάσετε τη δεύτερη παράμετρο, καθώς δεν χρησιμοποιείται στον ορισμό του τελεστή. Ο ίδιος ο μεταγλωττιστής αντικαθιστά μια προεπιλεγμένη τιμή για αυτόν, η οποία μπορεί να αγνοηθεί. Ακολουθεί ένα παράδειγμα χρήσης του τελεστή postfix:

Const int arrSize = 10; Οθόνη *parray = new Screen[arrSize]; ScreenPtr parr(*parray, arrSize); για (int ix = 0; ix

Εάν το καλέσετε ρητά, πρέπει να μεταβιβάσετε την τιμή του δεύτερου ακέραιου ορίσματος. Στην περίπτωση της κλάσης ScreenPtr, αυτή η τιμή αγνοείται, επομένως μπορεί να είναι οτιδήποτε:

parr.operator++(1024); // κλήση του χειριστή postfix++

Οι υπερφορτωμένοι τελεστές αύξησης και μείωσης επιτρέπεται να δηλώνονται ως συναρτήσεις φίλου. Αλλάξτε τον ορισμό της κλάσης ScreenPtr ανάλογα:

Κλάση ScreenPtr ( // δηλώσεις μη μελών φίλου Screen& operator++(Screen &); // Prefix operator operators friend Screen& operator--(Screen &); friend Screen& operator++(Screen &, int); // postfix operators friend Screen& operator-- ( Screen &, int); public: // ορισμοί μελών );

Άσκηση 15.7

Γράψτε ορισμούς για τους υπερφορτωμένους τελεστές αύξησης και μείωσης για την κλάση ScreenPtr, υποθέτοντας ότι δηλώνονται φίλοι της τάξης.

Άσκηση 15.8

Το ScreenPtr μπορεί να χρησιμοποιηθεί για την αναπαράσταση ενός δείκτη σε μια σειρά αντικειμένων της κλάσης Screen. Τροποποιήστε τον υπερφορτωμένο τελεστή*() και τον τελεστή >() (βλ. Ενότητα 15.6) έτσι ώστε ο δείκτης σε καμία περίπτωση να μην απευθύνεται σε ένα στοιχείο πριν ή μετά το τέλος του πίνακα. Συμβουλή: Αυτοί οι τελεστές θα πρέπει να χρησιμοποιούν τα νέα μέλη μεγέθους και μετατόπισης.

15.8. Νέοι χειριστές και διαγραφή

Από προεπιλογή, η εκχώρηση ενός αντικειμένου κλάσης από ένα σωρό και η απελευθέρωση της μνήμης που καταλαμβάνει εκτελούνται χρησιμοποιώντας τους καθολικούς τελεστές new() και delete() που ορίζονται στην Τυπική βιβλιοθήκη C++. (Συζητήσαμε αυτούς τους τελεστές στην Ενότητα 8.4.) Αλλά μια κλάση μπορεί επίσης να εφαρμόσει τη δική της στρατηγική διαχείρισης μνήμης παρέχοντας τελεστές μέλη με το ίδιο όνομα. Εάν ορίζονται σε μια κλάση, καλούνται αντί για καθολικούς τελεστές για να εκχωρήσουν και να εκχωρήσουν μνήμη για αντικείμενα αυτής της κλάσης.

Ας ορίσουμε τελεστές new() και delete() στην κλάση Screen.

Ο χειριστής μέλους new() πρέπει να επιστρέψει μια τιμή τύπου void* και να λάβει ως πρώτη του παράμετρο μια τιμή τύπου size_t, όπου size_t είναι ένα typedef που ορίζεται στο αρχείο κεφαλίδας συστήματος. Ιδού η ανακοίνωσή του:

Όταν η new() χρησιμοποιείται για τη δημιουργία ενός αντικειμένου ενός τύπου κλάσης, ο μεταγλωττιστής ελέγχει αν η κλάση έχει ορίσει έναν τέτοιο τελεστή. Εάν ναι, τότε είναι το αντικείμενο που καλείται να εκχωρήσει μνήμη για το αντικείμενο, διαφορετικά καλείται ο καθολικός τελεστής new(). Για παράδειγμα, η ακόλουθη δήλωση

Οθόνη *ps = νέα οθόνη;

δημιουργεί ένα αντικείμενο Screen στο σωρό και αφού αυτή η κλάση έχει έναν τελεστή new(), καλείται. Η παράμετρος size_t του χειριστή αρχικοποιείται αυτόματα στο μέγεθος της οθόνης σε byte.

Η προσθήκη ή η αφαίρεση new() προς ή από μια κλάση δεν έχει καμία επίδραση στον κώδικα χρήστη. Η κλήση νέου φαίνεται ίδια τόσο για τον παγκόσμιο χειριστή όσο και για τον χειριστή μέλους. Εάν η κλάση Screen δεν είχε τη δική της new(), τότε η κλήση θα παρέμενε σωστή, θα κληθεί μόνο ο καθολικός τελεστής αντί για τον τελεστή μέλους.

Με τον τελεστή ανάλυσης καθολικού εύρους, μπορείτε να καλέσετε την καθολική new() ακόμα κι αν η κλάση Screen έχει ορίσει τη δική της έκδοση:

Οθόνη *ps = ::new Screen;

Όταν ο τελεστής διαγραφής είναι δείκτης σε ένα αντικείμενο ενός τύπου κλάσης, ο μεταγλωττιστής ελέγχει εάν ο τελεστής delete() ορίζεται σε αυτήν την κλάση. Αν ναι, τότε είναι αυτός που καλείται να ελευθερώσει τη μνήμη, διαφορετικά, την καθολική έκδοση του χειριστή. Επόμενη οδηγία

Διαγραφή ps?

απελευθερώνει τη μνήμη που καταλαμβάνεται από το αντικείμενο Screen που δείχνει το ps. Επειδή το Screen έχει έναν τελεστή μέλους delete(), αυτός είναι που ισχύει. Μια παράμετρος χειριστή τύπου void* αρχικοποιείται αυτόματα σε ps. Η προσθήκη της delete() σε μια κλάση ή η αφαίρεσή της από εκεί δεν έχει καμία επίδραση στον κώδικα χρήστη. Η κλήση για διαγραφή φαίνεται ίδια τόσο για τον παγκόσμιο χειριστή όσο και για τον χειριστή μέλους. Εάν η κλάση Screen δεν είχε τον δικό της τελεστή delete(), τότε η κλήση θα παρέμενε σωστή, θα κληθεί μόνο ο καθολικός τελεστής αντί για τον τελεστή μέλους.

Με τον τελεστή επίλυσης καθολικού εύρους, μπορείτε να καλέσετε την καθολική delete() ακόμα κι αν η οθόνη έχει ορίσει τη δική της έκδοση:

::διαγραφή ps;

Γενικά, ο τελεστής delete() που χρησιμοποιείται πρέπει να ταιριάζει με τον τελεστή new() που εκχώρησε τη μνήμη. Για παράδειγμα, εάν το ps δείχνει σε μια περιοχή μνήμης που έχει εκχωρηθεί από την καθολική new(), τότε η καθολική delete() θα πρέπει να χρησιμοποιηθεί για να την ελευθερώσει.

Ο τελεστής delete() που ορίζεται για έναν τύπο κλάσης μπορεί να έχει δύο παραμέτρους αντί για μία. Η πρώτη παράμετρος πρέπει να είναι ακόμα τύπου void* και η δεύτερη πρέπει να είναι του προκαθορισμένου τύπου size_t (μην ξεχάσετε να συμπεριλάβετε το αρχείο κεφαλίδας):

Οθόνη κλάσης ( δημόσια: // αντικαθιστά // void operator delete(void *); void operator delete(void *, size_t); );

Εάν υπάρχει μια δεύτερη παράμετρος, ο μεταγλωττιστής την αρχικοποιεί αυτόματα με μια τιμή ίση με το μέγεθος σε byte του αντικειμένου που απευθύνεται από την πρώτη παράμετρο. (Αυτή η επιλογή είναι σημαντική στην ιεραρχία κλάσης όταν ο τελεστής delete() μπορεί να κληρονομηθεί από μια παράγωγη κλάση. Περισσότερα για την κληρονομικότητα συζητούνται στο κεφάλαιο.)

Ας δούμε την υλοποίηση των τελεστών new() και delete() στην κλάση Screen με περισσότερες λεπτομέρειες. Η στρατηγική μας για την κατανομή μνήμης θα βασίζεται σε μια συνδεδεμένη λίστα αντικειμένων οθόνης, την οποία υποδεικνύει το μέλος του freeStore. Κάθε φορά που καλείται ο τελεστής μέλους new(), επιστρέφεται το επόμενο αντικείμενο στη λίστα. Όταν καλείται η delete(), το αντικείμενο επιστρέφει στη λίστα. Εάν, όταν δημιουργείται ένα νέο αντικείμενο, η λίστα που απευθύνεται στο freeStore είναι κενή, τότε ο καθολικός τελεστής new() καλείται να αποκτήσει ένα μπλοκ μνήμης αρκετά μεγάλο ώστε να αποθηκεύει αντικείμενα screenChunk της κλάσης Screen.

Τόσο το screenChunk όσο και το freeStore ενδιαφέρουν μόνο το Screen, επομένως θα τα κάνουμε ιδιωτικά μέλη. Επιπλέον, για όλα τα δημιουργημένα αντικείμενα της κλάσης μας, οι τιμές αυτών των μελών πρέπει να είναι οι ίδιες και επομένως πρέπει να δηλώνονται στατικά. Για να υποστηρίξουμε τη δομή της συνδεδεμένης λίστας των αντικειμένων οθόνης, χρειαζόμαστε ένα τρίτο επόμενο μέλος:

Οθόνη κλάσης ( δημόσια: void *operator new(size_t); void operator delete(void *, size_t); // ... private: Screen *next; static Screen *freeStore; static const int screenChunk; );

Εδώ είναι μια πιθανή υλοποίηση του τελεστή new() για την κλάση Screen:

#include "Screen.h" #include // τα στατικά μέλη αρχικοποιούνται // στα αρχεία προέλευσης του προγράμματος, όχι στα αρχεία κεφαλίδας Οθόνη *Screen::freeStore = 0; const int Screen::screenChunk = 24; void *Screen::operator new(size_t size) ( Screen *p; if (!freeStore) ( // συνδεδεμένη λίστα είναι κενή: get new block // global operator ονομάζεται new size_t chunk = screenChunk * size; freeStore = p = reinterpret_cast< Screen* >(νέο char[κομμάτι]); // συμπεριλάβετε το μπλοκ που προκύπτει στη λίστα για (; p != &freeStore[ screenChunk - 1 ]; ++p) p->next = p+1; p->επόμενο = 0; ) p = freeStore; freeStore = freeStore->next; επιστροφή p; ) Και εδώ είναι η υλοποίηση του τελεστή delete(): void Screen::operator delete(void *p, size_t) ( // εισάγετε το αντικείμενο "deleted" πίσω, // στην ελεύθερη λίστα (static_cast< Screen* >(p))->next = freeStore; freeStore = static_cast< Screen* >(Π); )

Ο τελεστής new() μπορεί να δηλωθεί σε μια κλάση χωρίς την αντίστοιχη delete(). Σε αυτήν την περίπτωση, τα αντικείμενα απελευθερώνονται χρησιμοποιώντας τον καθολικό τελεστή με το ίδιο όνομα. Επιτρέπεται επίσης η δήλωση του τελεστή delete() χωρίς new(): τα αντικείμενα θα δημιουργηθούν χρησιμοποιώντας τον καθολικό τελεστή με το ίδιο όνομα. Ωστόσο, αυτοί οι τελεστές συνήθως υλοποιούνται ταυτόχρονα, όπως στο παραπάνω παράδειγμα, αφού ο σχεδιαστής μιας κλάσης συνήθως χρειάζεται και τα δύο.

Είναι στατικά μέλη της κλάσης, ακόμα κι αν ο προγραμματιστής δεν τα δηλώνει ρητά ως τέτοια, και υπόκεινται στους συνήθεις περιορισμούς για τέτοιες συναρτήσεις μελών: δεν περνούν από αυτόν τον δείκτη και επομένως μπορούν να έχουν μόνο άμεση πρόσβαση στατικά μέλη. (Δείτε τη συζήτηση των στατικών συναρτήσεων μέλους στην Ενότητα 13.5.) Ο λόγος που αυτοί οι τελεστές γίνονται στατικοί είναι επειδή καλούνται είτε πριν κατασκευαστεί το αντικείμενο κλάσης (new()) είτε αφού καταστραφεί (delete()).

Εκχώρηση μνήμης χρησιμοποιώντας τον τελεστή new(), για παράδειγμα:

Screen *ptr = new Screen(10, 20);

// C++ ψευδοκώδικας ptr = Screen::operator new(sizeof(Screen)); Screen::Screen(ptr, 10, 20);

Με άλλα λόγια, ο τελεστής new() που ορίζεται στην κλάση καλείται πρώτα να εκχωρήσει μνήμη για το αντικείμενο και στη συνέχεια αυτό το αντικείμενο αρχικοποιείται με έναν κατασκευαστή. Εάν η new() αποτύχει, δημιουργείται μια εξαίρεση του τύπου bad_alloc και ο κατασκευαστής δεν καλείται.

Ελευθέρωση μνήμης χρησιμοποιώντας τον τελεστή delete(), για παράδειγμα:

Διαγραφή ptr;

ισοδυναμεί με την εκτέλεση των ακόλουθων εντολών με τη σειρά:

// C++ ψευδοκώδικας Screen::~Screen(ptr); Οθόνη::διαγραφή χειριστή(ptr, sizeof(*ptr));

Έτσι, όταν ένα αντικείμενο καταστρέφεται, καλείται πρώτα ο καταστροφέας κλάσης και μετά ο τελεστής delete() που ορίζεται στην κλάση καλείται για να ελευθερώσει τη μνήμη. Αν το ptr είναι 0, τότε δεν καλείται ούτε ο καταστροφέας ούτε η delete().

15.8.1. Νέοι χειριστές και διαγραφή

Ο τελεστής new(), που ορίστηκε στην προηγούμενη υποενότητα, καλείται μόνο όταν εκχωρείται μνήμη για ένα μεμονωμένο αντικείμενο. Έτσι, σε αυτήν την εντολή, η new() της κλάσης Screen ονομάζεται:

// Screen::operator new() ονομάζεται Screen *ps = new Screen(24, 80);

ενώ ο καθολικός τελεστής new() καλείται παρακάτω για να εκχωρήσει μνήμη από το σωρό για μια σειρά αντικειμένων τύπου Screen:

// Screen::operator new() ονομάζεται Screen *psa = new Screen;

Η κλάση μπορεί επίσης να δηλώσει τελεστές new() και delete() για να εργαστεί με πίνακες.

Ο τελεστής μέλος new() πρέπει να επιστρέψει μια τιμή τύπου void* και να λάβει μια τιμή τύπου size_t ως πρώτη παράμετρό του. Ακολουθεί η δήλωσή του για το Screen:

Οθόνη κλάσης ( public: void *operator new(size_t); // ... );

Όταν χρησιμοποιείται new για τη δημιουργία ενός πίνακα αντικειμένων ενός τύπου κλάσης, ο μεταγλωττιστής ελέγχει εάν ο τελεστής new() ορίζεται στην κλάση. Αν ναι, τότε είναι ο πίνακας που καλείται να εκχωρήσει μνήμη για τον πίνακα, διαφορετικά καλείται η καθολική new(). Η ακόλουθη πρόταση δημιουργεί έναν πίνακα δέκα αντικειμένων οθόνης στο ισχίο:

Οθόνη *ps = νέα οθόνη;

Αυτή η κλάση έχει έναν τελεστή new() και γι' αυτό καλείται να εκχωρήσει μνήμη. Η παράμετρος size_t του αρχικοποιείται αυτόματα στην ποσότητα της μνήμης, σε byte, που απαιτείται για τη συγκράτηση δέκα αντικειμένων οθόνης.

Ακόμα κι αν η κλάση έχει έναν τελεστή μέλους new(), ο προγραμματιστής μπορεί να καλέσει την global new() για να δημιουργήσει έναν πίνακα χρησιμοποιώντας τον τελεστή ανάλυσης καθολικού εύρους:

Οθόνη *ps = ::new Screen;

Ο τελεστής delete(), που είναι μέλος της κλάσης, πρέπει να είναι τύπου void και να παίρνει void* ως πρώτη παράμετρο. Δείτε πώς φαίνεται η δήλωσή του για το Screen:

Οθόνη κλάσης ( public: void operator delete(void *); );

Για να διαγράψετε έναν πίνακα αντικειμένων κλάσης, το delete πρέπει να καλείται ως εξής:

Διαγραφή ps?

Όταν ο τελεστής διαγραφής είναι δείκτης σε ένα αντικείμενο ενός τύπου κλάσης, ο μεταγλωττιστής ελέγχει εάν ο τελεστής delete() ορίζεται σε αυτήν την κλάση. Αν ναι, τότε είναι αυτός που καλείται να ελευθερώσει τη μνήμη, αλλιώς, την παγκόσμια εκδοχή του. Μια παράμετρος τύπου void* αρχικοποιείται αυτόματα στη διεύθυνση της αρχής της περιοχής μνήμης όπου βρίσκεται ο πίνακας.

Ακόμα κι αν η κλάση έχει έναν τελεστή μέλους delete(), ο προγραμματιστής μπορεί να καλέσει την καθολική delete() χρησιμοποιώντας τον τελεστή ανάλυσης καθολικού εύρους:

::διαγραφή ps;

Η προσθήκη ή η διαγραφή τελεστών new() ή delete() σε μια κλάση δεν έχει καμία επίδραση στον κωδικό χρήστη: οι κλήσεις τόσο σε παγκόσμιους χειριστές όσο και σε τελεστές μελών φαίνονται ίδιες.

Όταν δημιουργείται ένας πίνακας, πρώτα καλείται η new() για να εκχωρήσει την απαραίτητη μνήμη και, στη συνέχεια, κάθε στοιχείο αρχικοποιείται με έναν προεπιλεγμένο κατασκευαστή. Εάν μια κλάση έχει τουλάχιστον έναν κατασκευαστή, αλλά όχι προεπιλεγμένο κατασκευαστή, τότε η κλήση του τελεστή new() θεωρείται σφάλμα. Δεν υπάρχει σύνταξη για τον καθορισμό αρχικοποιητών στοιχείων πίνακα ή ορισμάτων κατασκευής κλάσεων κατά τη δημιουργία ενός πίνακα με αυτόν τον τρόπο.

Όταν ένας πίνακας καταστρέφεται, ο καταστροφέας κλάσης καλείται πρώτα να καταστρέψει τα στοιχεία και στη συνέχεια ο τελεστής delete() καλείται να ελευθερώσει όλη τη μνήμη. Είναι σημαντικό να χρησιμοποιείτε τη σωστή σύνταξη όταν το κάνετε αυτό. Εάν οι οδηγίες

Διαγραφή ps?

Το ps δείχνει σε μια σειρά αντικειμένων κλάσης, τότε η απουσία αγκύλων θα προκαλέσει την κλήση του καταστροφέα μόνο για το πρώτο στοιχείο, αν και η μνήμη θα ελευθερωθεί εντελώς.

Ο τελεστής μέλους delete() μπορεί να έχει όχι μία, αλλά δύο παραμέτρους, με τη δεύτερη να είναι τύπου size_t:

Οθόνη κλάσης ( δημόσια: // αντικαθιστά // void operator delete(void*); void operator delete(void*, size_t); );

Εάν υπάρχει η δεύτερη παράμετρος, ο μεταγλωττιστής την αρχικοποιεί αυτόματα με μια τιμή ίση με την ποσότητα μνήμης που έχει εκχωρηθεί για τον πίνακα σε byte.

15.8.2. Τελετή τοποθέτησης new() και τελεστή delete()

Ο τελεστής μέλους new() μπορεί να υπερφορτωθεί, με την προϋπόθεση ότι όλες οι δηλώσεις έχουν διαφορετικές λίστες παραμέτρων. Η πρώτη παράμετρος πρέπει να είναι τύπου size_t:

Class Screen ( public: void *operator new(size_t); void *operator new(size_t, Screen *); // ... );

Οι υπόλοιπες παράμετροι αρχικοποιούνται με τα ορίσματα τοποθέτησης που δίνονται κατά την κλήση νέου:

Void func(Screen *start) ( // ... )

Το τμήμα της έκφρασης που έρχεται μετά τη νέα λέξη-κλειδί και περικλείεται σε παρένθεση αντιπροσωπεύει τα ορίσματα τοποθέτησης. Το παραπάνω παράδειγμα καλεί τον τελεστή new(), ο οποίος παίρνει δύο παραμέτρους. Το πρώτο αρχικοποιείται αυτόματα στο μέγεθος της κλάσης Screen σε byte και το δεύτερο στην τιμή του ορίσματος έναρξης τοποθέτησης.

Μπορείτε επίσης να υπερφορτώσετε τον τελεστή μέλους delete(). Ωστόσο, ένας τέτοιος τελεστής δεν καλείται ποτέ από μια έκφραση διαγραφής. Η υπερφορτωμένη delete() καλείται σιωπηρά από τον μεταγλωττιστή εάν ο κατασκευαστής καλείται κατά την εκτέλεση του νέου τελεστή (αυτό δεν είναι τυπογραφικό λάθος, εννοούμε πραγματικά νέο) κάνει μια εξαίρεση. Ας ρίξουμε μια πιο προσεκτική ματιά στη χρήση του delete().

Ακολουθία ενεργειών κατά την αξιολόγηση μιας έκφρασης

Οθόνη *ps = νέα (έναρξη) Οθόνη;

  1. Ο τελεστής new(size_t, Screen*) που ορίζεται στην κλάση καλείται.
  2. Ο προεπιλεγμένος κατασκευαστής της κλάσης Screen καλείται να προετοιμάσει το δημιουργημένο αντικείμενο.

Η μεταβλητή ps αρχικοποιείται με τη διεύθυνση του νέου αντικειμένου Screen.

Ας υποθέσουμε ότι ο τελεστής κλάσης new(size_t, Screen*) εκχωρεί μνήμη χρησιμοποιώντας την καθολική new(). Πώς μπορεί ο προγραμματιστής να διασφαλίσει ότι η μνήμη ελευθερώνεται εάν ο κατασκευαστής που καλείται στο βήμα 2 κάνει μια εξαίρεση; Για να προστατεύσετε τον κωδικό χρήστη από διαρροές μνήμης, θα πρέπει να παρέχετε έναν υπερφορτωμένο τελεστή delete() που καλείται μόνο σε αυτήν την περίπτωση.

Εάν μια κλάση έχει έναν υπερφορτωμένο τελεστή με παραμέτρους των οποίων οι τύποι ταιριάζουν με τους τύπους των παραμέτρων new(), τότε ο μεταγλωττιστής την καλεί αυτόματα στην ελεύθερη μνήμη. Ας υποθέσουμε ότι έχουμε την ακόλουθη έκφραση με τον τελεστή τοποθέτησης new:

Οθόνη *ps = νέα (έναρξη) Οθόνη;

Εάν ο προεπιλεγμένος κατασκευαστής της κλάσης Screen κάνει μια εξαίρεση, τότε ο μεταγλωττιστής αναζητά delete() στο πεδίο Screen. Για να βρεθεί ένας τέτοιος τελεστής, οι τύποι των παραμέτρων του πρέπει να ταιριάζουν με τους τύπους των παραμέτρων της καλούμενης new(). Εφόσον η πρώτη παράμετρος της new() είναι πάντα τύπου size_t και ο τελεστής delete() είναι πάντα void*, οι πρώτες παράμετροι δεν λαμβάνονται υπόψη κατά τη σύγκριση. Ο μεταγλωττιστής αναζητά στην κλάση Screen έναν τελεστή delete() της ακόλουθης φόρμας:

Διαγραφή τελεστή κενού(κενό*, οθόνη*);

Εάν βρεθεί ένας τέτοιος τελεστής, καλείται σε ελεύθερη μνήμη σε περίπτωση που η new() κάνει μια εξαίρεση. (Διαφορετικά, δεν λέγεται.)

Ο σχεδιαστής κλάσης αποφασίζει εάν θα παρέχει μια delete() που αντιστοιχεί σε κάποια new(), ανάλογα με το αν αυτός ο τελεστής new() εκχωρεί ο ίδιος μνήμη ή χρησιμοποιεί ήδη εκχωρημένη μνήμη. Στην πρώτη περίπτωση, η delete() πρέπει να συμπεριληφθεί για την κατανομή της μνήμης εάν ο κατασκευαστής κάνει μια εξαίρεση. αλλιώς δεν χρειάζεται.

Μπορείτε επίσης να υπερφορτώσετε τον τελεστή εκχώρησης new() και τον τελεστή delete() για πίνακες:

Οθόνη κλάσης ( δημόσια: void *operator new(size_t); void *operator new(size_t, Screen*); void operator delete(void*, size_t); void operator delete(void*, Screen*); // ... )

Ο τελεστής new() χρησιμοποιείται όταν η έκφραση που περιέχει new για την εκχώρηση ενός πίνακα έχει τα κατάλληλα ορίσματα κατανομής:

Void func(Screen *start) ( // calling Screen::operator new(size_t, Screen*) Screen *ps = new (start) Screen; // ... )

Εάν ο κατασκευαστής κάνει μια εξαίρεση κατά τη λειτουργία του νέου τελεστή, τότε καλείται αυτόματα η αντίστοιχη delete().

Άσκηση 15.9

Εξηγήστε ποιες από τις ακόλουθες αρχικοποιήσεις είναι λάθος:

Κατηγορία iStack ( δημόσια: iStack(χωρητικότητα int) : _stack(χωρητικότητα), _top(0) () // ... ιδιωτική: int _top; vatcor< int>_σωρός; ) (α) iStack *ps = new iStack(20); (β) iStack *ps2 = new const iStack(15); (γ) iStack *ps3 = νέο iStack[ 100 ];

Άσκηση 15.10

Τι συμβαίνει στις παρακάτω εκφράσεις που περιέχουν new και delete;

Class Exercise ( public: Exercise(); ~Exercise(); ); Άσκηση *pe = νέα Άσκηση; διαγραφή ps?

Τροποποιήστε αυτές τις εκφράσεις έτσι ώστε να καλούνται οι καθολικοί τελεστές new() και delete().

Άσκηση 15.11

Εξηγήστε γιατί ο σχεδιαστής κλάσης πρέπει να παρέχει τον τελεστή delete().

15.9. Μετατροπές που καθορίζονται από το χρήστη

Έχουμε ήδη δει πώς εφαρμόζονται οι μετατροπές τύπων σε τελεστές ενσωματωμένων τύπων: στην Ενότητα 4.14, αυτό το ζήτημα εξετάστηκε χρησιμοποιώντας το παράδειγμα των τελεστών των ενσωματωμένων τελεστών και στην Ενότητα 9.3, χρησιμοποιώντας το παράδειγμα των πραγματικών ορισμάτων του καλείται συνάρτηση για να τα μεταφέρει στους τύπους τυπικών παραμέτρων. Εξετάστε τις ακόλουθες έξι πράξεις προσθήκης από αυτή την άποψη:

Charch; κοντό sh;, int ival; /* ένας τελεστής ανά πράξη * απαιτεί μετατροπή τύπου */ ch + ival; ival + ch; ch+sh; ch+ch; ival + sh; sh + ival;

Οι τελεστές ch και sh επεκτείνονται για να πληκτρολογήσουν int. Κατά την εκτέλεση μιας λειτουργίας, προστίθενται δύο τιμές τύπου int. Η επέκταση τύπου εκτελείται έμμεσα από τον μεταγλωττιστή και είναι διαφανής στο χρήστη.

Σε αυτήν την ενότητα, θα εξετάσουμε πώς ένας προγραμματιστής μπορεί να ορίσει προσαρμοσμένες μετατροπές για αντικείμενα ενός τύπου κλάσης. Τέτοιες μετατροπές που ορίζονται από τον χρήστη επικαλούνται επίσης αυτόματα από τον μεταγλωττιστή, όπως απαιτείται. Για να δείξουμε γιατί χρειάζονται, ας επανεξετάσουμε την κλάση SmallInt που παρουσιάστηκε στην Ενότητα 10.9.

Θυμηθείτε ότι το SmallInt σάς επιτρέπει να ορίσετε αντικείμενα που μπορούν να αποθηκεύουν τιμές από το ίδιο εύρος με τον ανυπόγραφο χαρακτήρα, π.χ. από 0 έως 255 και εντοπίζει σφάλματα εκτός ορίων. Από όλες τις άλλες απόψεις, αυτή η κλάση συμπεριφέρεται ακριβώς όπως ο ανυπόγραφος χαρακτήρας.

Για να μπορούμε να προσθέτουμε και να αφαιρούμε αντικείμενα SmallInt σε άλλα αντικείμενα της ίδιας κλάσης ή σε τιμές ενσωματωμένων τύπων, υλοποιούμε έξι συναρτήσεις χειριστή:

Κατηγορία SmallInt ( τελεστής φίλου+(const SmallInt &, int); τελεστής φίλου-(const SmallInt &, int); τελεστής φίλου-(int, const SmallInt &); τελεστής φίλου+(int, const SmallInt &); δημόσιος: SmallInt(int ival) : value(ival) ( ) operator+(const SmallInt &); operator-(const SmallInt &); // ... private: int value; );

Οι χειριστές μέλη παρέχουν τη δυνατότητα προσθήκης και αφαίρεσης δύο αντικειμένων SmallInt. Οι τελεστές καθολικού φίλου σάς επιτρέπουν να εκτελείτε αυτές τις πράξεις σε αντικείμενα μιας δεδομένης κλάσης και σε αντικείμενα ενσωματωμένων αριθμητικών τύπων. Χρειάζονται μόνο έξι τελεστές επειδή οποιοσδήποτε ενσωματωμένος αριθμητικός τύπος μπορεί να μεταδοθεί σε int. Για παράδειγμα, η έκφραση

επιλύεται σε δύο βήματα:

  1. Η διπλή σταθερά 3,14159 μετατρέπεται στον ακέραιο 3.
  2. Καλείται ο τελεστής+(const SmallInt &,int), ο οποίος επιστρέφει την τιμή 6.

Αν θέλουμε να υποστηρίξουμε bitwise και λογικές πράξεις, καθώς και τελεστές σύγκρισης και σύνθετης ανάθεσης, τότε πόσοι τελεστές πρέπει να υπερφορτωθούν; Δεν μετράς αμέσως. Είναι πολύ πιο βολικό να μετατρέπουμε αυτόματα ένα αντικείμενο της κλάσης SmallInt σε αντικείμενο τύπου int.

Η γλώσσα C++ έχει έναν μηχανισμό που επιτρέπει σε οποιαδήποτε κλάση να καθορίσει ένα σύνολο μετασχηματισμών που να εφαρμόζονται στα αντικείμενά της. Για το SmallInt, θα ορίσουμε ένα αντικείμενο cast σε int. Ιδού η εφαρμογή του:

Κλάση SmallInt ( public: SmallInt(int ival) : value(ival) ( ) // μετατροπέας // SmallInt ==> int operator int() ( return value; ) // δεν χρειάζονται υπερφορτωμένοι τελεστές private: int value; );

Ο τελεστής int() είναι ένας μετατροπέας που υλοποιεί μια μετατροπή που ορίζεται από το χρήστη, σε αυτήν την περίπτωση μεταφέροντας έναν τύπο κλάσης σε έναν δεδομένο τύπο int. Ο ορισμός του μετατροπέα περιγράφει τι σημαίνει μετατροπή και ποιες ενέργειες πρέπει να κάνει ο μεταγλωττιστής για να την εφαρμόσει. Για ένα αντικείμενο SmallInt, το σημείο μετατροπής σε int είναι να επιστρέψει τον αριθμό του τύπου int που είναι αποθηκευμένος στο μέλος τιμής.

Τώρα ένα αντικείμενο της κλάσης SmallInt μπορεί να χρησιμοποιηθεί όπου επιτρέπεται ένα int. Υποθέτοντας ότι δεν υπάρχουν πλέον υπερφορτωμένοι τελεστές και το SmallInt έχει μετατροπέα σε int, η λειτουργία προσθήκης

SmallInt si(3); si+3,14159

επιλύεται σε δύο βήματα:

  1. Ο μετατροπέας κλάσης SmallInt καλείται και επιστρέφει τον ακέραιο αριθμό 3.
  2. Ο ακέραιος αριθμός 3 επεκτείνεται στο 3,0 και προστίθεται στη σταθερά διπλής ακρίβειας 3,14159, η οποία δίνει 6,14159.

Αυτή η συμπεριφορά είναι περισσότερο σύμφωνη με τη συμπεριφορά των ενσωματωμένων τελεστών τύπου σε σύγκριση με τους προηγουμένως καθορισμένους υπερφορτωμένους τελεστές. Όταν ένα int προστίθεται σε ένα διπλό, προστίθενται δύο διπλά (επειδή το int επεκτείνεται σε διπλάσιο) και το αποτέλεσμα είναι ένας αριθμός του ίδιου τύπου.

Αυτό το πρόγραμμα απεικονίζει τη χρήση της κλάσης SmallInt:

#περιλαμβάνω #include "SmallInt.h" int main() ( cout<< "Введите SmallInt, пожалуйста: "; while (cin >> si1) ( cout<< "Прочитано значение " << si1 << "\nОно "; // SmallInt::operator int() вызывается дважды cout << ((si1 >127); "μεγαλύτερο από" : ((si1< 127) ? "меньше, чем " : "равно ")) <<"127\n"; cout << "\Введите SmallInt, пожалуйста \ (ctrl-d для выхода): "; } cout <<"До встречи\n"; }

Το μεταγλωττισμένο πρόγραμμα παράγει τα ακόλουθα αποτελέσματα:

Εισαγάγετε το SmallInt παρακαλώ: 127

Τιμή ανάγνωσης 127

Είναι ίσο με 127

Εισαγάγετε το SmallInt παρακαλώ (ctrl-d για έξοδο): 126

Είναι λιγότερο από 127

Εισαγάγετε το SmallInt παρακαλώ (ctrl-d για έξοδο): 128

Είναι πάνω από 127

Εισαγάγετε το SmallInt παρακαλώ (ctrl-d για έξοδο): 256

*** Σφάλμα εύρους SmallInt: 256 ***

#περιλαμβάνω κλάση SmallInt ( φίλος istream& operator>(istream &is, SmallInt &s); φίλος ostream& operator<<(ostream &is, const SmallInt &s) { return os << s.value; } public: SmallInt(int i=0) : value(rangeCheck(i)){} int operator=(int i) { return(value = rangeCheck(i)); } operator int() { return value; } private: int rangeCheck(int); int value; };

Ακολουθούν οι ορισμοί συνάρτησης μέλους εκτός του σώματος κλάσης:

Istream& operator>>(istream &is, SmallInt &si) ( int ix; is >> ix; si = ix; // SmallInt::operator=(int) return is; ) int SmallInt::rangeCheck(int i) ( /* εάν έχει οριστεί τουλάχιστον ένα bit διαφορετικό από τα πρώτα οκτώ, * τότε η τιμή είναι πολύ μεγάλη, αναφορά και έξοδος αμέσως από */ εάν (i & ~0377) ( cerr< <"\n*** Ошибка диапазона SmallInt: " << i << " ***" << endl; exit(-1); } return i; }

15.9.1. Μετατροπείς

Ένας μετατροπέας είναι μια ειδική περίπτωση μιας συνάρτησης μέλους κλάσης που υλοποιεί μια μετατροπή ενός αντικειμένου που ορίζεται από το χρήστη σε κάποιον άλλο τύπο. Ένας μετατροπέας δηλώνεται στο σώμα της κλάσης καθορίζοντας τον τελεστή λέξης-κλειδιού ακολουθούμενο από τον τύπο προορισμού της μετατροπής.

Το όνομα που ακολουθεί τη λέξη-κλειδί δεν χρειάζεται να είναι το όνομα ενός από τους ενσωματωμένους τύπους. Η κλάση Token που φαίνεται παρακάτω ορίζει πολλούς μετατροπείς. Το ένα χρησιμοποιεί typedef tName για να καθορίσει το όνομα του τύπου και το άλλο χρησιμοποιεί τον τύπο κλάσης SmallInt.

#include "SmallInt.h" typedef char *tName; κλάση Token ( public: Token(char *, int); τελεστής SmallInt() ( return val; ) operator tName() ( return name; ) operator int() ( return val; ) // άλλα δημόσια μέλη ιδιωτικά: SmallInt val; όνομα χαρακτήρα;);

Σημειώστε ότι οι ορισμοί των μετατροπέων σε SmallInt και int είναι οι ίδιοι. Ο μετατροπέας Token::operator int() επιστρέφει την τιμή του μέλους val. Επειδή το val είναι τύπου SmallInt, το SmallInt::operator int() χρησιμοποιείται σιωπηρά για τη μετατροπή του val σε int. Ο ίδιος ο Token::operator int() χρησιμοποιείται σιωπηρά από τον μεταγλωττιστή για να μετατρέψει ένα αντικείμενο τύπου Token σε τιμή τύπου int. Για παράδειγμα, αυτός ο μετατροπέας χρησιμοποιείται για να μεταφέρει σιωπηρά τα πραγματικά ορίσματα t1 και t2 του τύπου Token στον τύπο int της επίσημης παραμέτρου της συνάρτησης print():

#include "Token.h" void print(int i) ( cout< < "print(int) : " < < i < < endl; } Token t1("integer constant", 127); Token t2("friend", 255); int main() { print(t1); // t1.operator int() print(t2); // t2.operator int() return 0; }

Μετά τη μεταγλώττιση και εκτέλεση του προγράμματος θα εμφανιστούν οι ακόλουθες γραμμές:

Εκτύπωση(int) : 127 print(int) : 255

Η γενική άποψη του μετατροπέα είναι η εξής:

operatortype();

όπου ο τύπος μπορεί να είναι ένας ενσωματωμένος τύπος, ένας τύπος κλάσης ή ένα όνομα typedef. Δεν επιτρέπονται οι μετατροπείς όπου ο τύπος είναι πίνακας ή τύπος συνάρτησης. Ο μετατροπέας πρέπει να είναι συνάρτηση μέλους. Η δήλωσή του δεν πρέπει να προσδιορίζει ούτε τον τύπο επιστροφής ούτε τη λίστα παραμέτρων:

Operator int(SmallInt &); // σφάλμα: δεν είναι μέλος της κλάσης SmallInt ( public: int operator int(); // error: return type specified operator int(int = 0); // error: καθορίζεται η λίστα παραμέτρων // ... );

Ο μετατροπέας καλείται ως αποτέλεσμα μιας ρητής μετατροπής τύπου. Εάν η τιμή που μετατρέπεται είναι του τύπου μιας κλάσης που έχει μετατροπέα και ο τύπος αυτού του μετατροπέα καθορίζεται στη λειτουργία cast, τότε ονομάζεται:

#include "Token.h" Token tok("function", 78); // λειτουργική σημείωση: Token::operator SmallInt() ονομάζεται SmallInt tokVal = SmallInt(tok); // static_cast: Token::operator tName() ονομάζεται char *tokName = static_cast< char * >(tok);

Ο μετατροπέας Token::operator tName() ενδέχεται να έχει μια ανεπιθύμητη παρενέργεια. Μια προσπάθεια άμεσης πρόσβασης στο ιδιωτικό μέλος Token::name επισημαίνεται ως σφάλμα από τον μεταγλωττιστή:

Char *tokName = tok.name; // σφάλμα: Token::name είναι ιδιωτικό μέλος

Ωστόσο, ο μετατροπέας μας, επιτρέποντας στους χρήστες να αλλάξουν απευθείας το Token::name, κάνει ακριβώς αυτό που θέλαμε να προστατεύσουμε. Το πιθανότερο είναι ότι δεν θα λειτουργήσει. Ακολουθεί ένα παράδειγμα για το πώς θα μπορούσε να συμβεί μια τέτοια τροποποίηση:

#include "Token.h" Token tok("function", 78); char *tokName = tok; // correct: σιωπηρή μετατροπή *tokname = "P"; // αλλά τώρα το μέλος του ονόματος έχει Punction!

Σκοπεύουμε να επιτρέψουμε την πρόσβαση μόνο για ανάγνωση στο αντικείμενο μετατροπής της κλάσης Token. Επομένως, ο μετατροπέας πρέπει να επιστρέψει έναν τύπο const char*:

Typedef const char *cchar; class Token ( public: operator cchar() ( return name; ) // ... ); // σφάλμα: μετατροπή από char* σε const char* δεν επιτρέπεται char *pn = tok; const char *pn2 = tok; // σωστά

Μια άλλη λύση είναι να αντικαταστήσετε τον τύπο char* στον ορισμό του Token με τον τύπο συμβολοσειράς από την τυπική βιβλιοθήκη C++:

Token κλάσης ( δημόσιο: Token(string, int); τελεστής SmallInt() ( return val; ) operator string() ( return name; ) operator int() ( return val; ) // άλλα δημόσια μέλη ιδιωτικά: SmallInt val; string όνομα; );

Η σημασιολογία του μετατροπέα Token::operator string() είναι να επιστρέψει ένα αντίγραφο της τιμής (όχι έναν δείκτη στην τιμή) της συμβολοσειράς που αντιπροσωπεύει το όνομα του διακριτικού. Αυτό αποτρέπει την τυχαία τροποποίηση του ιδιωτικού ονόματος μέλους της κλάσης Token.

Ο τύπος στόχου πρέπει να ταιριάζει ακριβώς με τον τύπο του μετατροπέα; Για παράδειγμα, ο παρακάτω κώδικας θα καλούσε τον μετατροπέα int() που ορίζεται στην κλάση Token;

εξωτερικό κενό calc(διπλό); Token tok("constant", 44); // Καλείται ο τελεστής int(); Ναι // εφαρμόζεται τυπική μετατροπή int --> double calc(tok);

Εάν ο τύπος στόχου (σε αυτήν την περίπτωση, διπλός) δεν ταιριάζει ακριβώς με τον τύπο του μετατροπέα (στην περίπτωσή μας, int), τότε ο μετατροπέας θα εξακολουθεί να καλείται, υπό την προϋπόθεση ότι υπάρχει μια ακολουθία τυπικών μετατροπών που οδηγεί στον στόχο πληκτρολογήστε από τον τύπο του μετατροπέα. (Αυτές οι ακολουθίες περιγράφονται στην ενότητα 9.3.) Η κλήση της συνάρτησης calc() καλεί Token::operator int() για μετατροπή tok από τύπο Token σε τύπο int. Στη συνέχεια, εφαρμόζεται η τυπική μετατροπή για τη μετάδοση του αποτελέσματος από int σε διπλάσιο.

Μετά από μετασχηματισμό που ορίζεται από το χρήστη, επιτρέπονται μόνο οι τυπικοί. Εάν απαιτείται άλλη μετατροπή που ορίζεται από το χρήστη για να επιτευχθεί ο τύπος στόχου, τότε ο μεταγλωττιστής δεν εφαρμόζει μετατροπές. Ας υποθέσουμε ότι δεν έχει οριστεί τελεστής int() στην κλάση Token, τότε η ακόλουθη κλήση θα είναι λανθασμένη:

extern void calc(int); token tok("δείκτης", 37); // εάν το Token::operator int() δεν έχει οριστεί, // τότε αυτή η κλήση οδηγεί σε ένα σφάλμα μεταγλώττισης calc(tok);

Εάν ο μετατροπέας Token::operator int() δεν έχει οριστεί, τότε η μετάδοση του tok σε int θα απαιτούσε την κλήση δύο μετατροπέων που ορίζονται από το χρήστη. Πρώτον, το πραγματικό όρισμα tok θα πρέπει να μετατραπεί από τον τύπο Token στον τύπο SmallInt χρησιμοποιώντας έναν μετατροπέα

Token::operator SmallInt()

και μετά μεταφέρετε το αποτέλεσμα σε τύπο int - χρησιμοποιώντας επίσης έναν προσαρμοσμένο μετατροπέα

Token::operator int()

Η κλήση στο calc(tok) επισημαίνεται ως σφάλμα από τον μεταγλωττιστή επειδή δεν υπάρχει σιωπηρή μετατροπή από τύπο Token σε τύπο int.

Εάν δεν υπάρχει λογική αντιστοιχία μεταξύ του τύπου του μετατροπέα και του τύπου της κλάσης, ο σκοπός του μετατροπέα ενδέχεται να μην είναι σαφής στον αναγνώστη του προγράμματος:

Ημερομηνία τάξης ( δημόσια: // προσπαθήστε να μαντέψετε ποιο μέλος επιστρέφεται! τελεστής int(); private: int μήνας, ημέρα, έτος; );

Ποια τιμή πρέπει να επιστρέψει ο μετατροπέας int() της κλάσης Date; Ανεξάρτητα από το πόσο καλοί είναι οι λόγοι για αυτήν ή εκείνη την απόφαση, ο αναγνώστης θα χάσει τον τρόπο χρήσης των αντικειμένων της κλάσης Date, αφού δεν υπάρχει προφανής λογική αντιστοιχία μεταξύ αυτών και των ακεραίων. Σε τέτοιες περιπτώσεις είναι προτιμότερο να μην ορίζετε καθόλου τον μετατροπέα.

15.9.2. Κατασκευαστής ως μετατροπέας

Ένα σύνολο κατασκευαστών κλάσεων που λαμβάνουν μία μόνο παράμετρο, όπως το SmallInt(int) της κλάσης SmallInt, ορίζει ένα σύνολο σιωπηρών μετατροπών σε τιμές SmallInt. Για παράδειγμα, ο κατασκευαστής SmallInt(int) μετατρέπει τις τιμές int σε τιμές SmallInt.

Extern void calc(SmallInt); int i? // πρέπει να μετατρέψετε το i σε τιμή SmallInt // αυτό επιτυγχάνεται χρησιμοποιώντας το SmallInt(int) calc(i); Όταν καλείται το calc(i), το i μετατρέπεται σε τιμή SmallInt χρησιμοποιώντας τον κατασκευαστή SmallInt(int) που καλείται από τον μεταγλωττιστή για να δημιουργήσει ένα προσωρινό αντικείμενο του επιθυμητού τύπου. Ένα αντίγραφο αυτού του αντικειμένου μεταβιβάζεται στη συνέχεια στο calc() σαν να ήταν η κλήση της συνάρτησης της μορφής: // Ψευκωδικός σε C++ // δημιουργία ενός προσωρινού αντικειμένου τύπου SmallInt ( SmallInt temp = SmallInt(i); calc(temp); )

Οι σγουρές τιράντες σε αυτό το παράδειγμα υποδηλώνουν τη διάρκεια ζωής αυτού του αντικειμένου: καταστρέφεται όταν η συνάρτηση τερματίζεται.

Ο τύπος μιας παραμέτρου κατασκευαστή μπορεί να είναι ο τύπος κάποιας κλάσης:

Αριθμός κλάσης ( public: // δημιουργία μιας τιμής τύπου Number από μια τιμή τύπου SmallInt Number(const SmallInt &); // ... );

Σε αυτήν την περίπτωση, μια τιμή τύπου SmallInt μπορεί να χρησιμοποιηθεί όπου επιτρέπεται μια τιμή τύπου Number:

extern void func(Αριθμός); SmallInt si(87); int main() ( // καλώντας τον αριθμό (const SmallInt &) func(si); // ... )

Εάν ένας κατασκευαστής χρησιμοποιείται για την εκτέλεση μιας σιωπηρής μετατροπής, πρέπει ο τύπος της παραμέτρου του να ταιριάζει ακριβώς με τον τύπο της τιμής που πρόκειται να μετατραπεί; Για παράδειγμα, ο παρακάτω κώδικας θα καλούσε το SmallInt(int), που ορίζεται στην κλάση SmallInt, για να μεταφέρει το dobj σε έναν τύπο SmallInt;

Extern void calc(SmallInt); διπλό dobj? // καλείται το SmallInt(int); Ναι // το dobj μετατρέπεται από διπλό σε int // με τυπική μετατροπή calc(dobj);

Εάν είναι απαραίτητο, μια ακολουθία τυπικών μετατροπών εφαρμόζεται στο πραγματικό όρισμα πριν κληθεί ο κατασκευαστής να εκτελέσει τη μετατροπή που ορίζεται από το χρήστη. Κατά την κλήση της συνάρτησης calc(), χρησιμοποιείται η τυπική μετατροπή dobj από διπλό σε int. Στη συνέχεια, το SmallInt(int) καλείται να μεταφέρει το αποτέλεσμα στο SmallInt.

Ο μεταγλωττιστής χρησιμοποιεί σιωπηρά έναν κατασκευαστή με μία μόνο παράμετρο για να μετατρέψει τον τύπο του στον τύπο της κλάσης στην οποία ανήκει ο κατασκευαστής. Ωστόσο, μερικές φορές είναι πιο βολικό ο κατασκευαστής Number(const SmallInt&) να μπορεί να κληθεί μόνο για να προετοιμάσει ένα αντικείμενο τύπου Number με τιμή τύπου SmallInt και ποτέ να μην εκτελέσει σιωπηρές μετατροπές. Για να αποφύγουμε αυτή τη χρήση του κατασκευαστή, ας το δηλώσουμε ρητά:

Αριθμός κλάσης ( δημόσιος: // να μην χρησιμοποιείται ποτέ ρητός αριθμός (const SmallInt &); // ... );

Ο μεταγλωττιστής δεν χρησιμοποιεί ποτέ ρητούς κατασκευαστές για να εκτελέσει μετατροπές σιωπηρού τύπου:

extern void func(Αριθμός); SmallInt si(87); int main() ( // σφάλμα: δεν υπάρχει σιωπηρή μετατροπή από SmallInt σε Number func(si); // ... )

Ωστόσο, ένας τέτοιος κατασκευαστής μπορεί ακόμα να χρησιμοποιηθεί για μετατροπή τύπου, εάν ζητηθεί ρητά με τη μορφή τελεστή cast:

SmallInt si(87); int main() ( // σφάλμα: δεν υπάρχει σιωπηρή μετατροπή από SmallInt σε Number func(si); func(Number(si)); // correct: cast func(static_cast< Number >(σι)); // σωστό: cast )

15.10. Επιλογή μετασχηματισμού Α

Μια μετατροπή που ορίζεται από το χρήστη υλοποιείται ως μετατροπέας ή κατασκευαστής. Όπως αναφέρθηκε ήδη, μετά τη μετατροπή που εκτελείται από τον μετατροπέα, επιτρέπεται η χρήση της τυπικής μετατροπής για τη μετάδοση της επιστρεφόμενης τιμής στον τύπο προορισμού. Ένας μετασχηματισμός που εκτελείται από έναν κατασκευαστή μπορεί επίσης να προηγείται μιας τυπικής μετατροπής για τη μετάδοση του τύπου του ορίσματος στον τύπο της επίσημης παραμέτρου του κατασκευαστή.

Μια ακολουθία μετασχηματισμών που ορίζονται από το χρήστη είναι ένας συνδυασμός ορισμένο από τον χρήστηκαι την τυπική μετατροπή που απαιτείται για τη μετάδοση της τιμής στον τύπο προορισμού. Μια τέτοια ακολουθία μοιάζει με:

Ακολουθία τυπικών μετασχηματισμών ->

Μετασχηματισμός που ορίζεται από το χρήστη ->

Ακολουθία τυπικών μετασχηματισμών

όπου μια μετατροπή που ορίζεται από το χρήστη υλοποιείται από έναν μετατροπέα ή έναν κατασκευαστή.

Είναι πιθανό να υπάρχουν δύο διαφορετικές ακολουθίες μετατροπών που ορίζονται από το χρήστη για να μετατρέψουν την τιμή πηγής στον τύπο προορισμού και, στη συνέχεια, ο μεταγλωττιστής πρέπει να επιλέξει την καλύτερη από αυτές. Ας δούμε πώς γίνεται.

Επιτρέπεται ο ορισμός πολλών μετατροπέων σε μια κλάση. Για παράδειγμα, η κλάση Number έχει δύο: τον τελεστή int() και τον τελεστή float(), και οι δύο ικανοί να μετατρέψουν ένα αντικείμενο Number σε τιμή float. Φυσικά, μπορείτε να χρησιμοποιήσετε τον μετατροπέα Token::operator float() για άμεσο μετασχηματισμό. Αλλά το Token::operator int() λειτουργεί επίσης, επειδή το αποτέλεσμά του είναι τύπου int και επομένως μπορεί να μετατραπεί σε float χρησιμοποιώντας την τυπική μετατροπή. Είναι ένας μετασχηματισμός διφορούμενος εάν υπάρχουν πολλές τέτοιες ακολουθίες; Ή μήπως ένα από αυτά είναι καλύτερο από τα άλλα;

Αριθμός κλάσης ( public: operator float(); operator int(); // ... ); αριθμός αρ. floatff = num; // ποιος μετατροπέας; φλοτέρ()

Σε τέτοιες περιπτώσεις, η επιλογή της καλύτερης ακολουθίας μετασχηματισμών που ορίζονται από το χρήστη βασίζεται σε μια ανάλυση της ακολουθίας μετασχηματισμών που εφαρμόζεται μετά τον μετατροπέα. Στο προηγούμενο παράδειγμα, μπορούν να εφαρμοστούν οι ακόλουθες δύο ακολουθίες:

  1. τελεστής float() -> ακριβής αντιστοίχιση
  2. τελεστής int() -> τυπική μετατροπή

Όπως συζητήθηκε στην Ενότητα 9.3, μια ακριβής αντιστοίχιση είναι καλύτερη από έναν τυπικό μετασχηματισμό. Επομένως, η πρώτη ακολουθία είναι καλύτερη από τη δεύτερη, πράγμα που σημαίνει ότι έχει επιλεγεί ο μετατροπέας Token::operator float().

Μπορεί να συμβεί ότι δύο διαφορετικοί κατασκευαστές μπορούν να εφαρμοστούν για τη μετατροπή μιας τιμής στον τύπο προορισμού. Σε αυτήν την περίπτωση, αναλύεται η ακολουθία των τυπικών μετασχηματισμών που προηγούνται της κλήσης του κατασκευαστή:

Κατηγορία SmallInt ( public: SmallInt(int ival) : value(ival) ( ) SmallInt(double dval) : value(static_cast< int >(dwal)); ( ) ); εξωτερικός χειρισμός κενού (const SmallInt &); int main() ( double dobj; manip(dobj); // correct: SmallInt(double) )

Εδώ, η κλάση SmallInt ορίζει δύο κατασκευαστές, τους SmallInt(int) και SmallInt(double), οι οποίοι μπορούν να χρησιμοποιηθούν για την αλλαγή μιας διπλής τιμής σε αντικείμενο SmallInt: Το SmallInt(double) μετατρέπει ένα διπλό σε SmallInt απευθείας, ενώ το SmallInt(int) λειτουργεί με βάση το αποτέλεσμα μιας τυπικής μετατροπής διπλού σε εντ. Έτσι, υπάρχουν δύο ακολουθίες μετασχηματισμών που ορίζονται από τον χρήστη:

  1. ακριβής αντιστοίχιση -> SmallInt(διπλό)
  2. τυπική μετατροπή -> SmallInt(int)

Επειδή η ακριβής αντιστοίχιση είναι καλύτερη από μια τυπική μετατροπή, επιλέγεται ο κατασκευαστής SmallInt(double).

Δεν είναι πάντα δυνατό να αποφασίσουμε ποια σειρά είναι καλύτερη. Μπορεί να συμβεί να είναι όλοι εξίσου καλοί, οπότε λέμε ότι η μεταμόρφωση είναι διφορούμενη. Σε αυτήν την περίπτωση, ο μεταγλωττιστής δεν εφαρμόζει σιωπηρούς μετασχηματισμούς. Για παράδειγμα, εάν η κλάση Number έχει δύο μετατροπείς:

Αριθμός κλάσης ( public: operator float(); operator int(); // ... );

τότε δεν είναι δυνατή η σιωπηρή μετατροπή ενός αντικειμένου τύπου Number σε type long. Η ακόλουθη πρόταση προκαλεί ένα σφάλμα μεταγλώττισης επειδή η ακολουθία των μετατροπών που ορίζονται από το χρήστη είναι ασαφής:

// σφάλμα: τόσο η float() όσο και η int() μπορούν να χρησιμοποιηθούν long lval = num;

Για να μετατρέψετε το num σε τιμή τύπου long, ισχύουν δύο τέτοιες ακολουθίες:

  1. τελεστής float() -> τυπική μετατροπή
  2. τελεστής int() -> τυπική μετατροπή

Εφόσον και στις δύο περιπτώσεις η χρήση του μετατροπέα ακολουθείται από την εφαρμογή της τυπικής μετατροπής, και οι δύο ακολουθίες είναι εξίσου καλές και ο μεταγλωττιστής δεν μπορεί να επιλέξει καμία από αυτές.

Με τη χύτευση ρητού τύπου, ο προγραμματιστής μπορεί να καθορίσει την επιθυμητή αλλαγή:

// σωστό: ρητή cast long lval = static_cast (αριθμός);

Ως αποτέλεσμα αυτής της προδιαγραφής, επιλέγεται ο μετατροπέας Token::operator int(), ακολουθούμενος από την τυπική μετατροπή σε long.

Η ασάφεια στην επιλογή της ακολουθίας μετασχηματισμών μπορεί επίσης να προκύψει όταν δύο κλάσεις ορίζουν μετασχηματισμούς μεταξύ τους. Για παράδειγμα:

Κλάση SmallInt ( public: SmallInt(const Number &); // ... ); Αριθμός κλάσης ( public: operator SmallInt(); // ... ); extern void compute(SmallInt); εξωτερικός αριθμός αριθμός; υπολογισμός(αριθμός); // σφάλμα: δύο μετατροπές είναι δυνατές

Το όρισμα num μετατρέπεται σε SmallInt κατά δύο διαφορετικοί τρόποι: χρησιμοποιώντας τον κατασκευαστή SmallInt::SmallInt(const Number&) ή τον μετατροπέα Number::operator SmallInt(). Δεδομένου ότι και οι δύο αλλαγές είναι εξίσου καλές, η κλήση θεωρείται σφάλμα.

Για να επιλύσει την ασάφεια, ο προγραμματιστής μπορεί να καλέσει ρητά τον μετατροπέα κλάσης Αριθμών:

// correct: η ρητή κλήση αποσαφηνίζει το compute(num.operator SmallInt());

Ωστόσο, δεν θα πρέπει να χρησιμοποιούνται ρητά χυτά για την επίλυση ασαφειών, καθώς τόσο ο μετατροπέας όσο και ο κατασκευαστής λαμβάνονται υπόψη κατά την επιλογή μετατροπών κατάλληλων για χύτευση τύπου:

Υπολογισμός(SmallInt(αριθμός)); // σφάλμα: ακόμα διφορούμενο

Όπως μπορείτε να δείτε, η παρουσία ένας μεγάλος αριθμόςτέτοιοι μετατροπείς και κατασκευαστές δεν είναι ασφαλείς, άρα τους. πρέπει να χρησιμοποιείται με προσοχή. Μπορείτε να περιορίσετε τη χρήση κατασκευαστών κατά την εκτέλεση σιωπηρών μετατροπών (και επομένως να μειώσετε την πιθανότητα απροσδόκητων επιπτώσεων) καθιστώντας τα ρητά.

15.10.1. Επανεξέταση της ανάλυσης υπερφόρτωσης λειτουργίας

Το Κεφάλαιο 9 περιγράφει λεπτομερώς τον τρόπο επίλυσης μιας υπερφορτωμένης κλήσης λειτουργίας. Εάν τα πραγματικά ορίσματα όταν καλούνται είναι του τύπου μιας κλάσης, ενός δείκτη σε έναν τύπο κλάσης ή ενός δείκτη σε μέλη κλάσης, τότε περισσότερες συναρτήσεις συναγωνίζονται για πιθανούς υποψηφίους. Επομένως, η παρουσία τέτοιων ορισμάτων επηρεάζει το πρώτο βήμα της διαδικασίας επίλυσης υπερφόρτωσης - την επιλογή ενός συνόλου υποψήφιων συναρτήσεων.

Στο τρίτο βήμα αυτής της διαδικασίας, επιλέγεται η καλύτερη αντιστοίχιση. Σε αυτήν την περίπτωση, κατατάσσονται οι μετατροπές των τύπων των πραγματικών ορισμάτων στους τύπους τυπικών παραμέτρων της συνάρτησης. Εάν τα ορίσματα και οι παράμετροι είναι τύπου κλάσης, τότε το σύνολο των πιθανών μετατροπών θα πρέπει επίσης να περιλαμβάνει ακολουθίες μετατροπών που ορίζονται από τον χρήστη, υποβάλλοντάς τες επίσης σε κατάταξη.

Σε αυτήν την ενότητα, θα ρίξουμε μια πιο προσεκτική ματιά στον τρόπο με τον οποίο τα πραγματικά ορίσματα και οι τυπικές παράμετροι τύπου κλάσης επηρεάζουν την επιλογή των υποψήφιων συναρτήσεων και πώς οι ακολουθίες μετατροπών που καθορίζονται από τον χρήστη επηρεάζουν την επιλογή της καλύτερα καθιερωμένης συνάρτησης.

15.10.2. Λειτουργίες υποψηφίου

Μια υποψήφια συνάρτηση είναι μια συνάρτηση με το ίδιο όνομα με αυτήν που καλείται. Ας υποθέσουμε ότι έχουμε μια κλήση όπως αυτή:

SmallInt si(15); add(si, 566);

Η συνάρτηση υποψήφιος πρέπει να ονομάζεται προσθήκη. Ποιες από τις δηλώσεις add() λαμβάνονται υπόψη; Αυτά που είναι ορατά στο σημείο κλήσης.

Για παράδειγμα, και οι δύο συναρτήσεις add() που δηλώνονται στο καθολικό εύρος θα είναι υποψήφιες για την ακόλουθη κλήση:

const matrix& add(const matrix &, int); διπλή προσθήκη (διπλή, διπλή) int main() ( SmallInt si(15); add(si, 566); // ... )

Η εξέταση συναρτήσεων των οποίων οι δηλώσεις είναι ορατές στο σημείο κλήσης δεν περιορίζεται σε κλήσεις με ορίσματα τύπου κλάσης. Ωστόσο, για αυτούς, η αναζήτηση δηλώσεων πραγματοποιείται σε δύο ακόμη πεδία:

  • εάν το πραγματικό όρισμα είναι ένα αντικείμενο ενός τύπου κλάσης, ένας δείκτης ή μια αναφορά σε έναν τύπο κλάσης ή ένας δείκτης σε ένα μέλος κλάσης και αυτός ο τύπος δηλώνεται σε έναν χώρο ονομάτων που ορίζεται από το χρήστη, τότε συναρτήσεις που δηλώνονται σε αυτό το διάστημα και έχουν το ίδιο όνομα προστίθεται στο σύνολο συναρτήσεων υποψηφίου και καλείται:
namespace NS ( class SmallInt ( /* ... */); class String ( /* ... */); String add(const String &, const String &); ) int main() ( // si είναι του τύπος κλάσης SmallInt: // η κλάση δηλώνεται στον χώρο ονομάτων NS NS::SmallInt si(15); add(si, 566); // NS::add() είναι μια υποψήφια συνάρτηση που επιστρέφει 0;)

Το όρισμα si είναι τύπου SmallInt, δηλ. ο τύπος της κλάσης που δηλώνεται στον χώρο ονομάτων NS. Επομένως, το add(const String &, const String &) που δηλώνεται σε αυτόν τον χώρο ονομάτων προστίθεται στο σύνολο των υποψήφιων συναρτήσεων.

  • εάν το πραγματικό όρισμα είναι ένα αντικείμενο ενός τύπου κλάσης, ένας δείκτης ή μια αναφορά σε μια κλάση ή ένας δείκτης σε ένα μέλος κλάσης και αυτή η κλάση έχει φίλους που έχουν το ίδιο όνομα με την καλούμενη συνάρτηση, τότε προστίθενται στο σύνολο των υποψηφίων λειτουργιών:
  • χώρος ονομάτων NS ( class SmallInt ( φίλος SmallInt add(SmallInt, int) ( /* ... */ )); ) int main() ( NS::SmallInt si(15); add(si, 566); // συνάρτηση - φίλος add() - υποψήφιος επιστροφή 0;)

    Το όρισμα συνάρτησης si είναι τύπου SmallInt. Η συνάρτηση φίλου κλάσης SmallInt add(SmallInt, int) είναι μέλος του χώρου ονομάτων NS, αν και δεν δηλώνεται απευθείας σε αυτό το διάστημα. Μια κανονική αναζήτηση στο NS δεν θα βρει μια λειτουργία φίλου. Ωστόσο, όταν η adding() καλείται με ένα όρισμα τύπου κλάσης SmallInt, οι φίλοι της τάξης που δηλώνονται στη λίστα μελών της λαμβάνονται επίσης υπόψη και προστίθενται στο σύνολο υποψηφίων.

    Έτσι, εάν η λίστα των πραγματικών ορισμάτων συνάρτησης περιέχει ένα αντικείμενο, έναν δείκτη ή αναφορά σε μια κλάση και δείκτες σε μέλη κλάσης, τότε το σύνολο των υποψήφιων συναρτήσεων αποτελείται από το σύνολο των συναρτήσεων που είναι ορατές στο σημείο της επίκλησης ή που δηλώνονται στο τον ίδιο χώρο ονομάτων όπου ορίζεται, τύπος κλάσης ή δηλωμένοι φίλοι αυτής της κλάσης.

    Εξετάστε το ακόλουθο παράδειγμα:

    Χώρος ονομάτων NS ( κλάση SmallInt ( φίλος SmallInt add(SmallInt, int) ( /* ... */ )); class String ( /* ... */); String add(const String &, const String &); ) const matrix& add(const matrix &, int); διπλή προσθήκη (διπλή, διπλή) int main() ( // si είναι κατηγορίας τύπου SmallInt: // η κλάση δηλώνεται στον χώρο ονομάτων NS NS::SmallInt si(15); add(si, 566); // η συνάρτηση φίλος ονομάζεται return 0; )

    Εδώ οι υποψήφιοι είναι:

    • καθολικές λειτουργίες:
    const matrix& add(const matrix &, int) double add(double, double)
  • συνάρτηση από χώρο ονομάτων:
  • NS::add(const String &, const String &)
  • λειτουργία φίλου:
  • NS::add(SmallInt, int)

    Η ανάλυση υπερφόρτωσης επιλέγει τη συνάρτηση φίλου κλάσης SmallInt NS::add(SmallInt, int) ως την καλύτερη εφαρμογή: και τα δύο πραγματικά ορίσματα ταιριάζουν ακριβώς με τις δεδομένες τυπικές παραμέτρους.

    Φυσικά, η συνάρτηση που καλείται μπορεί να έχει πολλαπλά ορίσματα τύπου κλάσης, έναν δείκτη ή αναφορά σε μια κλάση ή έναν δείκτη σε ένα μέλος κλάσης. Επιτρέπονται διαφορετικοί τύποι κλάσεων για καθένα από αυτά τα ορίσματα. Η αναζήτηση για υποψήφιες συναρτήσεις για αυτές πραγματοποιείται στον χώρο ονομάτων όπου ορίζεται η κλάση και μεταξύ των συναρτήσεων φίλου της κλάσης. Επομένως, το προκύπτον σύνολο υποψηφίων για την κλήση μιας συνάρτησης με τέτοια ορίσματα περιέχει συναρτήσεις από διαφορετικούς χώρους ονομάτων και συναρτήσεις φίλων που δηλώνονται σε διαφορετικές κλάσεις.

    15.10.3. Υποψήφιες συναρτήσεις για να καλέσετε μια συνάρτηση στο πεδίο της τάξης

    Όταν καλείτε μια συνάρτηση της φόρμας

    εμφανίζεται στο εύρος μιας κλάσης (για παράδειγμα, μέσα σε μια συνάρτηση μέλους), τότε το πρώτο μέρος του συνόλου των υποψηφίων που περιγράφεται στην προηγούμενη υποενότητα (δηλαδή, το σύνολο που περιλαμβάνει δηλώσεις συναρτήσεων ορατές στο σημείο κλήσης) μπορεί να περιέχει περισσότερα από απλώς συναρτήσεις μέλους της τάξης. Η ανάλυση ονόματος χρησιμοποιείται για την κατασκευή ενός τέτοιου συνόλου. (Αυτό το θέμα συζητήθηκε λεπτομερώς στις ενότητες 13.9 - 13.12.)

    Εξετάστε ένα παράδειγμα:

    Χώρος ονομάτων NS ( struct myClass ( void k(int); static void k(char*); void mf(); ); int k(double); ); void h(char); void NS::myClass::mf() ( h("a"); // call global h(char) k(4); // call myClass::k(int) )

    Όπως σημειώνεται στην Ενότητα 13.11, οι προσδιοριστές NS::myClass:: αναζητούνται με αντίστροφη σειρά: πρώτα, η ορατή δήλωση για το όνομα που χρησιμοποιείται στον ορισμό της συνάρτησης μέλους mf() αναζητείται στην κλάση myClass και, στη συνέχεια, στο NS χώρο ονομάτων. Σκεφτείτε την πρώτη κλήση:

    Κατά την επίλυση του ονόματος h() στον ορισμό της συνάρτησης μέλους mf(), αναζητούνται πρώτα οι συναρτήσεις μέλους myClass. Εφόσον δεν υπάρχει συνάρτηση μέλους με αυτό το όνομα στο πεδίο αυτής της κλάσης, τότε η αναζήτηση συνεχίζεται στον χώρο ονομάτων NS. Ούτε η συνάρτηση h() είναι εκεί, οπότε περνάμε στο καθολικό πεδίο. Το αποτέλεσμα είναι η καθολική συνάρτηση h(char), η μόνη υποψήφια συνάρτηση που είναι ορατή στο σημείο της επίκλησης.

    Μόλις βρεθεί μια κατάλληλη διαφήμιση, η αναζήτηση σταματά. Επομένως, το σύνολο περιέχει μόνο εκείνες τις συναρτήσεις των οποίων οι δηλώσεις βρίσκονται σε πεδία όπου η ανάλυση ονόματος ήταν επιτυχής. Αυτό μπορεί να φανεί στο παράδειγμα της κατασκευής ενός συνόλου υποψηφίων για κλήση

    Αρχικά, η αναζήτηση πραγματοποιείται στο πλαίσιο της κλάσης myClass. Αυτό βρήκε δύο συναρτήσεις μέλους k(int) και k(char*). Εφόσον το υποψήφιο σύνολο περιέχει μόνο συναρτήσεις που δηλώνονται στο εύρος όπου επιτεύχθηκε η ανάλυση, ο χώρος ονομάτων NS δεν αναζητείται και η συνάρτηση k(double) δεν περιλαμβάνεται σε αυτό το σύνολο.

    Εάν η κλήση διαπιστωθεί ότι είναι διφορούμενη επειδή δεν υπάρχει η καλύτερη λειτουργία στο σετ, τότε ο μεταγλωττιστής εκδίδει ένα μήνυμα σφάλματος. Οι υποψήφιοι που ταιριάζουν καλύτερα με τα πραγματικά επιχειρήματα δεν αναζητούνται σε εσωκλειόμενα πεδία.

    15.10.4. Ακολουθίες κατάταξης μετασχηματισμών που καθορίζονται από το χρήστη

    Ένα όρισμα πραγματικής συνάρτησης μπορεί να μεταδοθεί σιωπηρά στον τύπο μιας επίσημης παραμέτρου χρησιμοποιώντας μια σειρά μετατροπών που ορίζονται από το χρήστη. Πώς επηρεάζει αυτό την ανάλυση υπερφόρτωσης; Για παράδειγμα, εάν υπάρξει μια επόμενη κλήση στο calc(), ποια συνάρτηση θα κληθεί;

    Κλάση SmallInt ( public: SmallInt(int); ); εξωτερικό κενό calc(διπλό); extern void calc(SmallInt); int ival? int main() ( calc(ival); // ποια calc() ονομάζεται; )

    Επιλέγεται η συνάρτηση της οποίας οι τυπικές παράμετροι ταιριάζουν καλύτερα με τους τύπους των πραγματικών ορισμάτων. Ονομάζεται η καλύτερη εφαρμογή ή η καλύτερη στάση. Για να επιλέξετε μια τέτοια συνάρτηση, κατατάσσονται οι σιωπηρές μετατροπές που εφαρμόζονται στα πραγματικά ορίσματα. Το καλύτερο επιζών είναι αυτό για το οποίο οι αλλαγές που εφαρμόζονται στα ορίσματα δεν είναι χειρότερες από οποιαδήποτε άλλη επιζούσα συνάρτηση και για τουλάχιστον ένα όρισμα είναι καλύτερες από όλες τις άλλες συναρτήσεις.

    Μια ακολουθία τυπικών μετατροπών είναι πάντα καλύτερη από μια ακολουθία μετατροπών που ορίζονται από τον χρήστη. Έτσι, κατά την κλήση της calc() από το παραπάνω παράδειγμα, και οι δύο συναρτήσεις calc() είναι καλά εδραιωμένες. calc(double) επιβίωσε επειδή υπάρχει μια τυπική μετατροπή από το πραγματικό όρισμα int στην επίσημη παράμετρο τύπου double και calc(SmallInt) επειδή υπάρχει μια μετατροπή που ορίζεται από το χρήστη από int σε SmallInt που χρησιμοποιεί τον κατασκευαστή SmallInt(int). Επομένως, η καλύτερη συνάρτηση όρθιας είναι το calc (διπλό).

    Πώς συγκρίνονται δύο ακολουθίες μετασχηματισμών που ορίζονται από το χρήστη; Εάν χρησιμοποιούν διαφορετικούς μετατροπείς ή διαφορετικούς κατασκευαστές, τότε και οι δύο τέτοιες ακολουθίες θεωρούνται εξίσου καλές:

    Αριθμός κλάσης ( public: operator SmallInt(); operator int(); // ... ); extern void calc(int); extern void calc(SmallInt); εξωτερικός αριθμός αριθμός; calc(αριθμός); // σφάλμα: αμφισημία

    Τόσο το calc(int) όσο και το calc(SmallInt) θα επιβιώσουν. το πρώτο είναι επειδή ο μετατροπέας Number::operator int() μετατρέπει ένα όρισμα πραγματικού τύπου Number σε παράμετρο τυπικού τύπου int και το δεύτερο επειδή ο μετατροπέας Number::operator SmallInt() μετατρέπει ένα όρισμα πραγματικού τύπου Number σε επίσημο τύπο SmallInt παράμετρος. Εφόσον οι ακολουθίες μετατροπών που ορίζονται από το χρήστη έχουν πάντα την ίδια κατάταξη, ο μεταγλωττιστής δεν μπορεί να αποφασίσει ποια είναι καλύτερη. Έτσι, αυτή η κλήση συνάρτησης είναι διφορούμενη και οδηγεί σε σφάλμα μεταγλώττισης.

    Υπάρχει ένας τρόπος να επιλυθεί η ασάφεια καθορίζοντας ρητά τη μετατροπή:

    // Η ρητή μετάδοση αποσαφηνίζει το calc(static_cast< int >(αριθμός));

    Ένα ρητό cast αναγκάζει τον μεταγλωττιστή να μετατρέψει το όρισμα num σε int χρησιμοποιώντας τον μετατροπέα Number::operator int(). Το πραγματικό όρισμα θα είναι τότε τύπου int, το οποίο ταιριάζει ακριβώς με τη συνάρτηση calc(int), η οποία επιλέγεται ως η καλύτερη.

    Ας υποθέσουμε ότι ο μετατροπέας Number::operator int() δεν έχει οριστεί στην κλάση Number. Θα υπάρξει τότε πρόκληση

    // only Number::operator SmallInt() ορίζεται calc(num); // ακόμα διφορούμενο;

    ακόμα διφορούμενο; Θυμηθείτε ότι το SmallInt διαθέτει επίσης έναν μετατροπέα που μπορεί να μετατρέψει μια τιμή SmallInt σε int.

    Κλάση SmallInt ( public: operator int(); // ... );

    Μπορούμε να υποθέσουμε ότι η συνάρτηση calc() καλείται μετατρέποντας πρώτα το πραγματικό όρισμα num από τον τύπο Number στον τύπο SmallInt χρησιμοποιώντας τον μετατροπέα Number::operator SmallInt() και μετά μεταφέροντας το αποτέλεσμα σε int χρησιμοποιώντας SmallInt::operator SmallInt() . Ωστόσο, δεν είναι. Θυμηθείτε ότι μια ακολουθία μετασχηματισμών που ορίζονται από το χρήστη μπορεί να περιλαμβάνει πολλούς τυπικούς μετασχηματισμούς, αλλά μόνο έναν προσαρμοσμένο. Εάν ο μετατροπέας Number::operator int() δεν έχει οριστεί, τότε η συνάρτηση calc(int) δεν θεωρείται σταθερή επειδή δεν υπάρχει σιωπηρή μετατροπή από τον τύπο του πραγματικού ορίσματος num στον τύπο της επίσημης παραμέτρου int.

    Επομένως, απουσία του μετατροπέα Number::operator int(), η μόνη συνάρτηση που απομένει θα είναι η calc(SmallInt), υπέρ της οποίας επιτρέπεται η κλήση.

    Εάν δύο ακολουθίες μετατροπών που ορίζονται από το χρήστη χρησιμοποιούν τον ίδιο μετατροπέα, τότε η επιλογή του καλύτερου εξαρτάται από τη σειρά των τυπικών μετατροπών που πραγματοποιούνται μετά την κλήση του:

    Κλάση SmallInt ( public: operator int(); // ... ); void manip(int); void manip(char); SmallInt si(68); main() ( manip(si); // call manip(int) )

    Και οι δύο manip(int) και manip(char) είναι καθιερωμένες συναρτήσεις. το πρώτο είναι επειδή ο μετατροπέας SmallInt::operator int() μετατρέπει το πραγματικό όρισμα του τύπου SmallInt στον τύπο της επίσημης παραμέτρου int και το δεύτερο επειδή ο ίδιος μετατροπέας μετατρέπει το SmallInt σε int, μετά το οποίο το αποτέλεσμα μετατρέπεται σε char χρησιμοποιώντας την τυπική μετατροπή. Οι ακολουθίες μετασχηματισμών που ορίζονται από το χρήστη μοιάζουν με αυτό:

    Manip(int) : operator int()->ακριβής αντιστοίχιση manip(int) : operator int()->τυπική μετατροπή

    Δεδομένου ότι ο ίδιος μετατροπέας χρησιμοποιείται και στις δύο ακολουθίες, η κατάταξη της ακολουθίας των τυπικών μετασχηματισμών αναλύεται για να προσδιοριστεί ο καλύτερος από αυτούς. Δεδομένου ότι μια ακριβής αντιστοίχιση είναι καλύτερη από μια μετατροπή, το manip(int) είναι η καλύτερα καθιερωμένη συνάρτηση.

    Τονίζουμε ότι ένα τέτοιο κριτήριο επιλογής γίνεται αποδεκτό μόνο όταν χρησιμοποιείται ο ίδιος μετατροπέας και στις δύο ακολουθίες μετασχηματισμών που ορίζονται από το χρήστη. Εδώ το παράδειγμά μας διαφέρει από εκείνα στο τέλος της Ενότητας 15.9, όπου δείξαμε πώς ο μεταγλωττιστής επιλέγει μια μετατροπή που καθορίζεται από το χρήστη από κάποια τιμή σε έναν δεδομένο τύπο στόχου: οι τύποι πηγής και στόχου διορθώθηκαν και ο μεταγλωττιστής έπρεπε να επιλέξει μεταξύ διαφορετικών μετατροπών που ορίζονται από τον χρήστη από τον έναν τύπο στον άλλο. Εδώ εξετάζουμε δύο διαφορετικές συναρτήσεις με διαφορετικούς τύπους τυπικών παραμέτρων και οι τύποι στόχου είναι διαφορετικοί. Αν για δύο ΔΙΑΦΟΡΕΤΙΚΟΙ ΤΥΠΟΙΟι παράμετροι απαιτούν διαφορετικές μετατροπές που καθορίζονται από το χρήστη, είναι δυνατή μόνο η προτίμηση του ενός τύπου από τον άλλο εάν χρησιμοποιείται ο ίδιος μετατροπέας και στις δύο ακολουθίες. Εάν δεν είναι, τότε οι τυπικές μετατροπές που ακολουθούν την εφαρμογή του μετατροπέα αξιολογούνται για την επιλογή του καλύτερου τύπου στόχου. Για παράδειγμα:

    Κλάση SmallInt ( public: operator int(); operator float(); // ... ); void compute(float); void compute(char); SmallInt si(68); main() ( υπολογισμός(si); // αμφισημία )

    Τόσο το compute(float) όσο και το compute(int) είναι καθιερωμένες συναρτήσεις. Το compute(float) είναι επειδή ο μετατροπέας SmallInt::operator float() μετατρέπει ένα όρισμα τύπου SmallInt σε τύπο παραμέτρου float και το compute(char) επειδή το SmallInt::operator int() μετατρέπει ένα όρισμα τύπου SmallInt σε τύπο int, μετά όπου το αποτέλεσμα χυτεύεται κανονικά στον τύπο char. Έτσι, υπάρχουν ακολουθίες:

    Υπολογισμός(float) : τελεστής float()->ακριβής αντιστοίχιση υπολογισμός(char) : τελεστής char()->τυπική μετατροπή

    Δεδομένου ότι χρησιμοποιούν διαφορετικούς μετατροπείς, είναι αδύνατο να προσδιοριστεί ποια συνάρτηση έχει επίσημες παραμέτρους που ταιριάζουν καλύτερα με την κλήση. Για να επιλέξετε το καλύτερο από τα δύο, δεν χρησιμοποιείται η κατάταξη της ακολουθίας τυπικών μετασχηματισμών. Η κλήση επισημαίνεται ως διφορούμενη από τον μεταγλωττιστή.

    Άσκηση 15.12

    Δεν υπάρχουν ορισμοί μετατροπέων στις κλάσεις C++ Standard Library και οι περισσότεροι κατασκευαστές που λαμβάνουν μία παράμετρο δηλώνονται ρητά. Ωστόσο, ορίζονται πολλοί υπερφορτωμένοι τελεστές. Γιατί πιστεύετε ότι πάρθηκε αυτή η απόφαση κατά τη διάρκεια του σχεδιασμού;

    Άσκηση 15.13

    Γιατί ο υπερφορτωμένος τελεστής εισόδου για την κλάση SmallInt που ορίζεται στην αρχή αυτής της ενότητας δεν υλοποιείται ως εξής:

    Istream& operator>>(istream &is, SmallInt &si) ( return (είναι >> is.value); )

    Άσκηση 15.14

    Δώστε πιθανές ακολουθίες μετατροπών που ορίζονται από το χρήστη για τις ακόλουθες αρχικοποιήσεις. Ποιο θα είναι το αποτέλεσμα κάθε αρχικοποίησης;

    Κλάση LongDouble ( operator double(); operator float(); ); εξωτερικό LongDouble ldObj; (α) int ex1 = ldObj; (β) float ex2 = ldObj;

    Άσκηση 15.15

    Ονομάστε τρία σύνολα υποψήφιων συναρτήσεων που λαμβάνονται υπόψη κατά την επίλυση μιας υπερφόρτωσης συνάρτησης όταν τουλάχιστον ένα από τα ορίσματά της είναι τύπου κλάσης.

    Άσκηση 15.16

    Ποια από τις συναρτήσεις calc() επιλέγεται ως η καλύτερη κατάσταση σε αυτήν την περίπτωση; Δείξτε την ακολουθία μετασχηματισμών που απαιτούνται για την κλήση κάθε συνάρτησης και εξηγήστε γιατί η μία είναι καλύτερη από την άλλη.

    Κλάση LongDouble( public: LongDouble(double); // ... ); extern void calc(int); extern void calc(LongDouble); διπλό dval? int main() ( calc(dval); // ποια συνάρτηση; )

    15.11. Ανάλυση υπερφόρτωσης και Λειτουργίες μέλους Α

    Οι συναρτήσεις μελών μπορούν επίσης να υπερφορτωθούν, σε αυτήν την περίπτωση, επίσης, εφαρμόζεται η διαδικασία επίλυσης υπερφόρτωσης για την επιλογή της καλύτερης που ισχύει. Αυτή η ανάλυση είναι πολύ παρόμοια με τη διαδικασία για κανονικές λειτουργίες και αποτελείται από τα ίδια τρία βήματα:

    1. Επιλογή υποψηφίων λειτουργιών.
    2. Επιλογή καθιερωμένων λειτουργιών.

    Ωστόσο, υπάρχουν μικρές διαφορές στους αλγόριθμους για τη δημιουργία ενός συνόλου υποψηφίων και την επιλογή σταθερών συναρτήσεων μέλους. Θα εξετάσουμε αυτές τις διαφορές σε αυτήν την ενότητα.

    15.11.1. Υπερφορτωμένες δηλώσεις συναρτήσεων μελών

    Οι συναρτήσεις μέλους τάξης μπορούν να υπερφορτωθούν:

    Κλάση myClass ( public: void f(double); char f(char, char); // υπερφορτώνει myClass::f(double) // ... );

    Όπως και με τις συναρτήσεις που δηλώνονται σε έναν χώρο ονομάτων, οι συναρτήσεις μέλους μπορούν να έχουν το ίδιο όνομα, με την προϋπόθεση ότι οι λίστες παραμέτρων τους είναι διαφορετικές είτε ως προς τον αριθμό των παραμέτρων είτε ως προς τους τύπους τους. Εάν οι δηλώσεις δύο συναρτήσεων μελών διαφέρουν μόνο στον τύπο επιστροφής, τότε η δεύτερη δήλωση θεωρείται σφάλμα μεταγλώττισης:

    Κλάση myClass ( public: void mf(); double mf(); // error: δεν μπορεί να υπερφορτωθεί // ... );

    Σε αντίθεση με τις συναρτήσεις στους χώρους ονομάτων, οι συναρτήσεις μέλους χρειάζεται να δηλωθούν μόνο μία φορά. Ακόμα κι αν οι λίστες τύπων και παραμέτρων επιστροφής δύο συναρτήσεων μελών είναι ίδιες, ο μεταγλωττιστής ερμηνεύει τη δεύτερη δήλωση ως μη έγκυρη εκ νέου δήλωση:

    Κλάση myClass ( public: void mf(); void mf(); // error: redeclaration // ... );

    Όλες οι υπερφορτωμένες λειτουργίες πρέπει να δηλώνονται στο ίδιο πεδίο. Επομένως, οι συναρτήσεις μέλους δεν υπερφορτώνουν ποτέ τις συναρτήσεις που δηλώνονται σε έναν χώρο ονομάτων. Επίσης, δεδομένου ότι κάθε κλάση έχει το δικό της πεδίο εφαρμογής, οι συναρτήσεις που είναι μέλη διαφορετικών κλάσεων δεν υπερφορτώνουν η μία την άλλη.

    Το σύνολο των υπερφορτωμένων συναρτήσεων μέλους μπορεί να περιέχει τόσο στατικές όσο και μη στατικές συναρτήσεις:

    Κλάση myClass ( public: void mcf(double); static void mcf(int*); // overloads myClass::mcf(double) // ... );

    Ποια από τις συναρτήσεις μέλους θα ονομαστεί - στατική ή μη - εξαρτάται από τα αποτελέσματα της ανάλυσης υπερφόρτωσης. Η διαδικασία επίλυσης σε μια κατάσταση όπου τόσο τα στατικά όσο και τα μη στατικά μέλη έχουν επιβιώσει συζητείται λεπτομερώς στην επόμενη ενότητα.

    15.11.2. Λειτουργίες υποψηφίου

    Εξετάστε δύο είδη κλήσεων συνάρτησης μέλους:

    Mc.mf(arg); pmc->mf(arg);

    όπου mc είναι μια έκφραση του τύπου myClass και pmc είναι μια έκφραση του τύπου "δείκτης στον τύπο myClass". Το σύνολο των υποψηφίων και για τις δύο κλήσεις αποτελείται από συναρτήσεις που βρίσκονται στο πεδίο του myClass κατά την αναζήτηση μιας δήλωσης mf().

    Ομοίως, για την κλήση μιας συνάρτησης της φόρμας

    MyClass::mf(arg);

    το σύνολο των υποψηφίων αποτελείται επίσης από συναρτήσεις που βρίσκονται στο πεδίο εφαρμογής της κλάσης myClass κατά την αναζήτηση της δήλωσης mf(). Για παράδειγμα:

    Κλάση myClass ( public: void mf(double); void mf(char, char = "\n"); static void mf(int*); // ... ); int main() ( myClass mc; int iobj; mc.mf(iobj); )

    Οι υποψήφιοι κλήσεις συνάρτησης στο main() είναι και οι τρεις συναρτήσεις μελών mf() που δηλώνονται στο myClass:

    Κενό mf(διπλό); void mf(char, char = "\n"); static void mf(int*);

    Εάν καμία συνάρτηση μέλους με το όνομα mf() δεν είχε δηλωθεί στο myClass, τότε το σύνολο των υποψηφίων θα ήταν κενό. (Στην πραγματικότητα, θα ληφθούν υπόψη και συναρτήσεις από βασικές κλάσεις. Θα συζητήσουμε πώς εμπίπτουν σε αυτό το σύνολο στην Ενότητα 19.3.) Εάν δεν υπάρχουν υποψήφιοι για κλήση συνάρτησης, ο μεταγλωττιστής εκδίδει ένα μήνυμα σφάλματος.

    15.11.3. Καθιερωμένες λειτουργίες

    Μια καλά εδραιωμένη συνάρτηση είναι μια συνάρτηση από ένα σύνολο υποψηφίων που μπορεί να κληθεί με τα δεδομένα πραγματικά ορίσματα. Για να επιβιώσει, πρέπει να υπάρχουν σιωπηρές μετατροπές μεταξύ των τύπων των πραγματικών ορισμάτων και των τυπικών παραμέτρων. Για παράδειγμα: class myClass ( public: void mf(double); void mf(char, char = "\n"); static void mf(int*); // ... ); int main() ( myClass mc; int iobj; mc.mf(iobj); // ποια συνάρτηση μέλους mf(); Διφορούμενη )

    Υπάρχουν δύο καλά καθιερωμένες συναρτήσεις σε αυτό το απόσπασμα για την κλήση της mf() από την main():

    Κενό mf(διπλό); void mf(char, char = "\n");

    • Το mf(double) επιβίωσε επειδή έχει μόνο μία παράμετρο και υπάρχει μια τυπική μετατροπή του ορίσματος int iobj σε διπλή παράμετρο.
    • Το mf(char,char) επέζησε επειδή υπάρχει μια προεπιλεγμένη τιμή για τη δεύτερη παράμετρο και υπάρχει μια τυπική μετατροπή του ορίσματος int iobj στον τύπο char της πρώτης επίσημης παραμέτρου.

    Κατά την επιλογή των καλύτερων από τις καθιερωμένες συναρτήσεις μετατροπής τύπου που εφαρμόζονται σε κάθε πραγματικό όρισμα, κατατάσσονται. Ο καλύτερος είναι αυτός για τον οποίο όλοι οι μετασχηματισμοί που χρησιμοποιούνται δεν είναι χειρότεροι από οποιαδήποτε άλλη καθιερωμένη συνάρτηση και για τουλάχιστον ένα όρισμα ένας τέτοιος μετασχηματισμός είναι καλύτερος από όλες τις άλλες συναρτήσεις.

    Στο προηγούμενο παράδειγμα, καθεμία από τις δύο καθιερωμένες συναρτήσεις χρησιμοποιεί μια τυπική μετατροπή για να μεταφέρει τον τύπο του πραγματικού ορίσματος στον τύπο της επίσημης παραμέτρου. Η κλήση θεωρείται διφορούμενη επειδή και οι δύο συναρτήσεις μελών την επιλύουν εξίσου καλά.

    Ανεξάρτητα από τον τύπο της κλήσης συνάρτησης, τόσο στατικά όσο και μη στατικά μέλη μπορούν να συμπεριληφθούν στο σύνολο των επιζώντων μελών:

    Κλάση myClass ( public: static void mf(int); char mf(char); ); int main() (char cobj; myClass::mf(cobj); // ποια συνάρτηση μέλους; )

    Εδώ, η συνάρτηση μέλους mf() καλείται με το όνομα της κλάσης και τον τελεστή ανάλυσης πεδίου myClass::mf(). Ωστόσο, δεν καθορίζεται ούτε ένα αντικείμενο (με τον τελεστή "κουκκίδα") ούτε ένας δείκτης προς το αντικείμενο (με τον τελεστή "βέλος"). Παρόλα αυτά, η συνάρτηση μη στατικού μέλους mf(char) εξακολουθεί να περιλαμβάνεται στο σωζόμενο σύνολο μαζί με το στατικό μέλος mf(int).

    Στη συνέχεια, η διαδικασία επίλυσης υπερφόρτωσης συνεχίζεται: με βάση μια κατάταξη των μετατροπών τύπων που εφαρμόζονται στα πραγματικά ορίσματα, για να επιλέξετε την καλύτερη στάσιμη συνάρτηση. Το όρισμα cobj τύπου char αντιστοιχεί ακριβώς στην επίσημη παράμετρο mf(char) και μπορεί να επεκταθεί στον τύπο της επίσημης παραμέτρου mf(int). Εφόσον η κατάταξη μιας ακριβούς αντιστοίχισης είναι υψηλότερη, επιλέγεται η συνάρτηση mf(char).

    Ωστόσο, αυτή η συνάρτηση μέλους δεν είναι στατική και επομένως καλείται μόνο μέσω ενός αντικειμένου ή ενός δείκτη σε ένα αντικείμενο της κλάσης myClass χρησιμοποιώντας ένα από τα accessors. Σε μια τέτοια περίπτωση, εάν το αντικείμενο δεν έχει καθοριστεί και, επομένως, η κλήση της συνάρτησης είναι αδύνατη (μόνο στην περίπτωσή μας), ο μεταγλωττιστής το θεωρεί σφάλμα.

    Ένα άλλο χαρακτηριστικό των συναρτήσεων μέλους που πρέπει να λαμβάνεται υπόψη κατά τον σχηματισμό ενός συνόλου καθιερωμένων συναρτήσεων είναι η παρουσία προσδιοριστών σταθερότητας ή πτητικών σε μη στατικά μέλη. (Συζητήθηκαν στην Ενότητα 13.3.) Πώς επηρεάζουν τη διαδικασία επίλυσης υπερφόρτωσης; Αφήστε την κλάση myClass να έχει τις ακόλουθες συναρτήσεις μέλους:

    Κλάση myClass ( public: static void mf(int*); void mf(double); void mf(int) const; // ... );

    Στη συνέχεια, τόσο η συνάρτηση στατικού μέλους mf(int*), η σταθερή συνάρτηση mf(int) και η μη σταθερή συνάρτηση mf(double) περιλαμβάνονται στο υποψήφιο σύνολο για την κλήση που φαίνεται παρακάτω. Ποιοι από αυτούς όμως θα συμπεριληφθούν στο σύνολο των survivors;

    int main() ( const myClass mc; double dobj; mc.mf(dobj); // ποια συνάρτηση μέλους mf(); )

    Καθώς εξετάζουμε τους μετασχηματισμούς που πρέπει να εφαρμοστούν στα πραγματικά ορίσματα, διαπιστώνουμε ότι οι συναρτήσεις mf(double) και mf(int) έχουν επιβιώσει. Ο τύπος double του πραγματικού ορίσματος dobj ταιριάζει ακριβώς με τον τύπο της επίσημης παραμέτρου mf(double) και μπορεί να μεταφερθεί στον τύπο της παραμέτρου mf(int) χρησιμοποιώντας τυπική μετατροπή.

    Εάν μια κλήση συνάρτησης μέλους χρησιμοποιεί τους τελεστές πρόσβασης με τελεία ή βέλος, τότε ο τύπος του αντικειμένου ή του δείκτη για τον οποίο καλείται η συνάρτηση λαμβάνεται υπόψη κατά την επιλογή συναρτήσεων στο σύνολο των στάσιμων.

    Το mc είναι ένα αντικείμενο const στο οποίο μπορούν να κληθούν μόνο μη στατικές συναρτήσεις μέλους const. Επομένως, η συνάρτηση μη σταθερού μέλους mf(double) εξαιρείται από το σύνολο των σωζόμενων και η μόνη συνάρτηση mf(int) παραμένει σε αυτήν, η οποία καλείται.

    Τι γίνεται αν ένα αντικείμενο const χρησιμοποιείται για την κλήση μιας συνάρτησης στατικού μέλους; Εξάλλου, για μια τέτοια συνάρτηση είναι αδύνατο να οριστεί ο προσδιοριστής const ή πτητικός, επομένως μπορεί να κληθεί μέσω ενός αντικειμένου const;

    Κλάση myClass ( public: static void mf(int); char mf(char); ); int main() ( const myClass mc; int iobj; mc.mf(iobj); // μπορεί να κληθεί μια συνάρτηση στατικού μέλους; )

    Οι στατικές συναρτήσεις μέλους είναι κοινές σε όλα τα αντικείμενα της ίδιας κλάσης. Μπορούν να έχουν απευθείας πρόσβαση μόνο στα στατικά μέλη της τάξης. Έτσι, τα μη στατικά μέλη του σταθερού αντικειμένου mc δεν είναι διαθέσιμα στο στατικό mf(int). Για το λόγο αυτό, επιτρέπεται η κλήση μιας συνάρτησης στατικού μέλους σε ένα αντικείμενο const χρησιμοποιώντας τους τελεστές κουκκίδας ή βέλους.

    Έτσι, οι στατικές συναρτήσεις μελών δεν εξαιρούνται από το σύνολο των σωζόμενων συναρτήσεων ακόμα και αν υπάρχουν σταθεροί ή πτητικές προσδιοριστές στο αντικείμενο στο οποίο καλούνται. Οι στατικές συναρτήσεις μέλους αντιμετωπίζονται ως αντίστοιχες σε οποιοδήποτε αντικείμενο ή δείκτη σε ένα αντικείμενο της κλάσης τους.

    Στο παραπάνω παράδειγμα, το mc είναι ένα αντικείμενο const, επομένως η συνάρτηση μέλους mf(char) εξαιρείται από το σύνολο των σωζόμενων. Αλλά η συνάρτηση μέλους mf(int) παραμένει σε αυτήν επειδή είναι στατική. Δεδομένου ότι αυτή είναι η μόνη σταθερή λειτουργία, αποδεικνύεται η καλύτερη.

    15.12. Ανάλυση υπερφόρτωσης και A Operators

    Οι κλάσεις μπορούν να δηλώσουν υπερφορτωμένους τελεστές και μετατροπείς. Ας υποθέσουμε ότι, κατά την αρχικοποίηση, συναντήθηκε ένας τελεστής προσθήκης:

    SomeClass sc; int iobj = sc + 3;

    Πώς αποφασίζει ο μεταγλωττιστής εάν θα καλέσει τον υπερφορτωμένο τελεστή στο SomeClass ή θα μετατρέψει τον τελεστή sc σε ενσωματωμένο τύπο και μετά θα χρησιμοποιήσει τον ενσωματωμένο τελεστή;

    Η απάντηση εξαρτάται από τους πολλούς υπερφορτωμένους τελεστές και μετατροπείς που ορίζονται στο SomeClass. Όταν επιλέγετε έναν χειριστή για να εκτελέσει μια προσθήκη, εφαρμόζεται η διαδικασία ανάλυσης υπερφόρτωσης συναρτήσεων. ΣΤΟ αυτός ο τομέαςθα περιγράψουμε πώς αυτή η διαδικασία σας επιτρέπει να επιλέξετε τον επιθυμητό τελεστή όταν οι τελεστές είναι αντικείμενα τύπου κλάσης.

    Η ανάλυση υπερφόρτωσης χρησιμοποιεί την ίδια διαδικασία τριών βημάτων που παρουσιάζεται στην Ενότητα 9.2:

    • Επιλογή υποψηφίων λειτουργιών.
    • Επιλογή καθιερωμένων λειτουργιών.
    • Η επιλογή των καλύτερων από τις καθιερωμένες λειτουργίες.
    • Ας δούμε αυτά τα βήματα με περισσότερες λεπτομέρειες.

      Η ανάλυση υπερφόρτωσης συνάρτησης δεν ισχύει εάν όλοι οι τελεστές είναι ενσωματωμένοι τύποι. Σε αυτήν την περίπτωση, ο ενσωματωμένος χειριστής είναι εγγυημένος ότι θα χρησιμοποιηθεί. (Η χρήση τελεστών με ενσωματωμένους τελεστές καλύπτεται στο Κεφάλαιο 4.) Για παράδειγμα:

    class SmallInt ( public: SmallInt(int); ); SmallInt operator+ (const SmallInt &, const SmallInt &); void func() ( int i1, i2; int i3 = i1 + i2; )

    Δεδομένου ότι οι τελεστές i1 και i2 είναι τύπου int αντί τύπου κλάσης, χρησιμοποιείται επιπλέον ο ενσωματωμένος τελεστής +. Ο υπερφορτωμένος τελεστής+(const SmallInt &, const SmallInt &) αγνοείται, αν και οι τελεστές μπορούν να μεταδοθούν στο SmallInt χρησιμοποιώντας μια μετατροπή που ορίζεται από το χρήστη με τη μορφή του κατασκευαστή SmallInt(int). Η διαδικασία επίλυσης υπερφόρτωσης που περιγράφεται παρακάτω δεν ισχύει σε αυτές τις περιπτώσεις.

    Επίσης, η ανάλυση υπερφόρτωσης για τελεστές χρησιμοποιείται μόνο όταν χρησιμοποιείται η σύνταξη τελεστή:

    Void func() ( SmallInt si(98); int iobj = 65; int res = si + iobj; // χρησιμοποιείται σύνταξη τελεστή)

    Αν αντ' αυτού χρησιμοποιήσουμε τη σύνταξη κλήσης συνάρτησης: int res = operator+(si, iobj); // σύνταξη κλήσης συνάρτησης

    τότε ισχύει η διαδικασία επίλυσης υπερφόρτωσης για συναρτήσεις στον χώρο ονομάτων (βλ. Ενότητα 15.10). Εάν χρησιμοποιείται η σύνταξη για την κλήση μιας συνάρτησης μέλους:

    // σύνταξη κλήσης συνάρτησης μέλους int res = si.operator+(iobj);

    τότε λειτουργεί η αντίστοιχη διαδικασία για τις λειτουργίες μελών (βλ. ενότητα 15.11).

    15.12.1. Λειτουργίες υποψηφίου χειριστή

    Μια συνάρτηση τελεστή είναι υποψήφια εάν έχει το ίδιο όνομα με την καλούμενη. Όταν χρησιμοποιείτε τον ακόλουθο τελεστή προσθήκης

    SmallInt si(98); intiobj = 65; int res = si + iobj;

    η συνάρτηση υποψήφιου χειριστή είναι operator+. Ποιες δηλώσεις operator+ λαμβάνονται υπόψη;

    Δυνητικά, στην περίπτωση χρήσης της σύνταξης τελεστή με τελεστές τύπου κλάσης, κατασκευάζονται πέντε σύνολα υποψηφίων. Οι τρεις πρώτες είναι οι ίδιες όπως κατά την κλήση κανονικών συναρτήσεων με ορίσματα τύπου κλάσης:

    • το σύνολο των τελεστών που είναι ορατοί στο σημείο κλήσης. Οι δηλώσεις συνάρτησης operator+() που είναι ορατές στο σημείο όπου χρησιμοποιείται ο τελεστής είναι υποψήφιες. Για παράδειγμα, ο τελεστής+() που δηλώνεται στο καθολικό εύρος είναι υποψήφιος εάν ο τελεστής+() χρησιμοποιείται μέσα στο main():
    SmallInt operator+ (const SmallInt &, const SmallInt &); int main() ( SmallInt si(98); int iobj = 65; int res = si + iobj; // ::operator+() είναι υποψήφια συνάρτηση)
  • το σύνολο των τελεστών που δηλώνονται στον χώρο ονομάτων στον οποίο ορίζεται ο τύπος του τελεστή. Εάν ο τελεστής είναι τύπου κλάσης και αυτός ο τύπος δηλώνεται σε έναν χώρο ονομάτων που ορίζεται από το χρήστη, τότε οι συναρτήσεις τελεστή που δηλώνονται στον ίδιο χώρο και έχουν το ίδιο όνομα με τον χρησιμοποιούμενο τελεστή θεωρούνται υποψήφιοι:
  • namespace NS ( class SmallInt ( /* ... */); SmallInt operator+ (const SmallInt&, double); ) int main() ( // si είναι τύπου SmallInt: // αυτή η κλάση δηλώνεται στον χώρο ονομάτων NS NS: :SmallInt si(15); // NS::operator+() - υποψήφια συνάρτηση int res = si + 566; επιστροφή 0; )

    Ο τελεστής si είναι του τύπου κλάσης SmallInt, που δηλώνεται στον χώρο ονομάτων NS. Επομένως, ο υπερφορτωμένος τελεστής+(const SmallInt, double) που δηλώνεται στον ίδιο χώρο προστίθεται στο υποψήφιο σύνολο.

  • σύνολο τελεστών που δηλώνονται φίλοι των κλάσεων στις οποίες ανήκουν οι τελεστές. Εάν ο τελεστής ανήκει σε έναν τύπο κλάσης και στον ορισμό αυτής της κλάσης υπάρχουν συναρτήσεις φίλου με το ίδιο όνομα στον εφαρμοσμένο τελεστή, τότε προστίθενται στο σύνολο των υποψηφίων:
  • χώρος ονομάτων NS ( κλάση SmallInt ( φίλος τελεστής SmallInt+(const SmallInt&, int) ( /* ... */ )); ) int main() ( NS::SmallInt si(15); // τελεστής συνάρτησης φίλου+() - υποψήφιος int res = si + 566; επιστροφή 0;)

    Ο τελεστής si είναι τύπου SmallInt. Η συνάρτηση τελεστή operator+(const SmallInt&, int), που είναι φίλος αυτής της κλάσης, είναι μέλος του χώρου ονομάτων NS, αν και δεν δηλώνεται απευθείας σε αυτό το διάστημα. Μια κανονική αναζήτηση στο NS δεν θα βρει αυτήν τη λειτουργία χειριστή. Ωστόσο, όταν χρησιμοποιείται ο operator+() με ένα όρισμα τύπου SmallInt, οι συναρτήσεις φίλου που δηλώνονται στο πεδίο εφαρμογής αυτής της κλάσης περιλαμβάνονται στην εξέταση και προστίθενται στο σύνολο των υποψηφίων. Αυτά τα τρία σύνολα συναρτήσεων υποψήφιων τελεστών σχηματίζονται με τον ίδιο τρόπο όπως για κανονικές κλήσεις συναρτήσεων με ορίσματα τύπου κλάσης. Ωστόσο, όταν χρησιμοποιείτε σύνταξη τελεστή, δημιουργούνται δύο ακόμη σύνολα:

    • το σύνολο των τελεστών μελών που δηλώνονται στην κλάση του αριστερού τελεστέου. Εάν ένας τέτοιος τελεστής του operator+() έχει τύπο κλάσης, τότε οι δηλώσεις τελεστή+() που είναι μέλη αυτής της κλάσης περιλαμβάνονται στο σύνολο των υποψήφιων συναρτήσεων:
    κλάση myFloat ( myFloat(double); ); class SmallInt ( public: SmallInt(int); SmallInt operator+ (const myFloat &); ); int main() ( SmallInt si(15); int res = si + 5.66; // υποψήφιο μέλος χειριστή+() )

    Ο τελεστής μέλους SmallInt::operator+(const myFloat &) που ορίζεται στο SmallInt περιλαμβάνεται σε ένα σύνολο υποψήφιων συναρτήσεων για την επίλυση της κλήσης προς τον operator+() στο main();

  • πολλοί ενσωματωμένοι χειριστές. Δεδομένων των τύπων που μπορούν να χρησιμοποιηθούν με τον ενσωματωμένο χειριστή+(), υποψήφιοι είναι επίσης:
  • int operator+(int, int); διπλός χειριστής+(διπλός, διπλός); Τ* χειριστής+(T*, I); Τ* χειριστής+(I, T*);

    Η πρώτη δήλωση αφορά τον ενσωματωμένο τελεστή για την προσθήκη δύο τιμών ακέραιων τύπων, η δεύτερη για τον τελεστή για την προσθήκη τιμών τύπων κινητής υποδιαστολής. Το τρίτο και το τέταρτο αντιστοιχούν στον ενσωματωμένο τελεστή προσθήκης τύπου δείκτη, ο οποίος χρησιμοποιείται για την προσθήκη ενός ακέραιου σε έναν δείκτη. Οι δύο τελευταίες δηλώσεις είναι συμβολικές και περιγράφουν μια ολόκληρη οικογένεια ενσωματωμένων τελεστών που μπορούν να επιλεγούν από τον μεταγλωττιστή ως υποψήφιοι κατά την επεξεργασία πράξεων προσθήκης.

    Οποιοδήποτε από τα τέσσερα πρώτα σετ μπορεί να είναι κενό. Για παράδειγμα, εάν δεν υπάρχει συνάρτηση με το όνομα operator+() μεταξύ των μελών της κλάσης SmallInt, τότε το τέταρτο σύνολο θα είναι κενό.

    Ολόκληρο το σύνολο των συναρτήσεων του υποψήφιου χειριστή είναι η ένωση των πέντε υποσυνόλων που περιγράφονται παραπάνω:

    Χώρος ονομάτων NS ( class myFloat ( myFloat(double); ); class SmallInt ( φίλος SmallInt operator+(const SmallInt &, int) ( /* ... */ ) public: SmallInt(int); operator int(); SmallInt operator+ ( const myFloat &); // ...); SmallInt Operator+ (const SmallInt &, double); ) int main() ( // type si - class SmallInt: // Αυτή η κλάση δηλώνεται στον χώρο ονομάτων NS NS::SmallInt si (15); int res = si + 5.66; // ποιος τελεστής+(); επιστρέφει 0;)

    Αυτά τα πέντε σύνολα περιλαμβάνουν επτά υποψήφιες συναρτήσεις τελεστή για το ρόλο του operator+() στο main():

      το πρώτο σετ είναι άδειο. Στο καθολικό εύρος, δηλαδή σε αυτό όπου το operator+() χρησιμοποιείται στη συνάρτηση main(), δεν υπάρχουν δηλώσεις του υπερφορτωμένου τελεστή operator+().
    • το δεύτερο σύνολο περιέχει τελεστές που δηλώνονται στον χώρο ονομάτων NS όπου ορίζεται η κλάση SmallInt. Υπάρχει ένας τελεστής σε αυτό το διάστημα: NS::SmallInt NS::operator+(const SmallInt &, double);
    • το τρίτο σύνολο περιέχει τελεστές που έχουν δηλωθεί ως φίλοι της κλάσης SmallInt. Αυτό περιλαμβάνει NS::SmallInt NS::operator+(const SmallInt &, int);
    • το τέταρτο σύνολο περιέχει τελεστές που έχουν δηλωθεί ως μέλη του SmallInt. Υπάρχει επίσης ένα: NS::SmallInt NS::SmallInt::operator+(const myFloat &);
    • το πέμπτο σύνολο περιέχει ενσωματωμένους δυαδικούς τελεστές:
    int operator+(int, int); διπλός χειριστής+(διπλός, διπλός); Τ* χειριστής+(T*, I); Τ* χειριστής+(I, T*);

    Ναι, η δημιουργία ενός συνόλου υποψηφίων για την ανάλυση ενός τελεστή που χρησιμοποιείται με σύνταξη τελεστή είναι κουραστική. Αλλά αφού κατασκευαστεί, οι διαρκείς συναρτήσεις και οι καλύτερες από αυτές βρίσκονται, όπως και πριν, αναλύοντας τους μετασχηματισμούς που εφαρμόζονται στους τελεστές των επιλεγμένων υποψηφίων.

    15.12.2. Καθιερωμένες λειτουργίες

    Το σύνολο των καθιερωμένων συναρτήσεων τελεστή σχηματίζεται από το σύνολο των υποψηφίων επιλέγοντας μόνο εκείνους τους τελεστές που μπορούν να κληθούν με τους δεδομένους τελεστές. Για παράδειγμα, ποιος από τους επτά υποψήφιους που βρέθηκαν παραπάνω θα είναι υποψήφιος; Ο τελεστής χρησιμοποιείται στο ακόλουθο πλαίσιο:

    NS::SmallInt si(15); si + 5,66;

    Ο αριστερός τελεστής είναι τύπου SmallInt και ο δεξιός τελεστής είναι διπλός.

    Ο πρώτος υποψήφιος είναι μια καθιερωμένη λειτουργία για αυτή τη χρήση operator+():

    Ο αριστερός τελεστής τύπου SmallInt ως αρχικοποιητής ταιριάζει ακριβώς με την επίσημη παράμετρο αναφοράς αυτού του υπερφορτωμένου τελεστή. Το σωστό, που είναι τύπου double, ταιριάζει ακριβώς και με τη δεύτερη τυπική παράμετρο.

    Θα ισχύει επίσης η ακόλουθη υποψήφια λειτουργία:

    NS::SmallInt NS::operator+(const SmallInt &, int);

    Ο αριστερός τελεστής si τύπου SmallInt ως αρχικοποιητής ταιριάζει ακριβώς με την επίσημη παράμετρο αναφοράς του υπερφορτωμένου τελεστή. Η δεξιά είναι τύπου int και μπορεί να μεταδοθεί στον τύπο της δεύτερης επίσημης παραμέτρου χρησιμοποιώντας την τυπική μετατροπή.

    Μια τρίτη υποψήφια συνάρτηση θα κρατήσει επίσης:

    NS::SmallInt NS::SmallInt::operator+(const myFloat &);

    Ο αριστερός τελεστής si είναι τύπου SmallInt, δηλ. ο τύπος της κλάσης της οποίας είναι μέλος ο υπερφορτωμένος χειριστής. Το σωστό είναι τύπου int και μεταδίδεται στον τύπο κλάσης myFloat χρησιμοποιώντας μια μετατροπή που ορίζεται από το χρήστη με τη μορφή του κατασκευαστή myFloat(double).

    Η τέταρτη και η πέμπτη καθιερωμένη λειτουργία είναι οι ενσωματωμένοι τελεστές:

    int operator+(int, int); διπλός χειριστής+(διπλός, διπλός);

    Η κλάση SmallInt περιέχει έναν μετατροπέα που μπορεί να μεταφέρει μια τιμή SmallInt σε ένα int. Αυτός ο μετατροπέας χρησιμοποιείται σε συνδυασμό με τον πρώτο ενσωματωμένο τελεστή για τη μετατροπή του αριστερού τελεστή σε int. Ο δεύτερος τελεστής τύπου double μετατρέπεται σε τύπο int χρησιμοποιώντας την τυπική μετατροπή. Όσον αφορά τον δεύτερο ενσωματωμένο τελεστή, ο μετατροπέας μετατρέπει τον αριστερό τελεστή από τον τύπο SmallInt στον τύπο int, μετά τον οποίο το αποτέλεσμα μετατρέπεται τυπικά σε διπλό. Ο δεύτερος τελεστής τύπου double ταιριάζει ακριβώς με τη δεύτερη παράμετρο.

    Η καλύτερη από αυτές τις πέντε διαρκείς συναρτήσεις είναι η πρώτη, operator+(), που δηλώνεται στον χώρο ονομάτων NS:

    NS::SmallInt NS::operator+(const SmallInt &, double);

    Και οι δύο τελεστές του ταιριάζουν ακριβώς με τις παραμέτρους.

    15.12.3. Ασάφεια

    Η παρουσία στην ίδια κατηγορία μετατροπέων που εκτελούν σιωπηρές μετατροπές σε ενσωματωμένους τύπους και υπερφορτωμένους τελεστές μπορεί να οδηγήσει σε ασάφεια κατά την επιλογή μεταξύ τους. Για παράδειγμα, υπάρχει ο ακόλουθος ορισμός κλάσης String με μια συνάρτηση σύγκρισης:

    Κλάση String ( // ... public: String(const char * = 0); bool operator== (const String &) const; // no operator operator== (const char *) );

    και αυτή η χρήση του τελεστή==:

    String flower ("τουλίπα"); void foo(const char *pf) ( // call overloaded operator String::operator==() if (flower == pf) cout<< pf <<" is a flower!\en"; // ... }

    Στη συνέχεια κατά τη σύγκριση

    Άνθος == πφ

    ο τελεστής ισότητας της κλάσης String ονομάζεται:

    Για να μετατρέψουμε τον σωστό τελεστή του pf από τον τύπο const char* στον τύπο String της παραμέτρου operator==(), εφαρμόζεται μια μετατροπή που ορίζεται από το χρήστη, η οποία καλεί τον κατασκευαστή:

    Συμβολοσειρά (const char *)

    Αν προσθέσουμε έναν μετατροπέα στο const char*, πληκτρολογήστε στον ορισμό της κλάσης String:

    Κλάση String ( // ... public: String(const char * = 0); bool operator== (const String &) const; operator const char*(); // new converter );

    τότε η εμφανιζόμενη χρήση του τελεστή==() γίνεται ασαφής:

    // Ο έλεγχος ισότητας δεν μεταγλωττίζεται πλέον! αν (λουλούδι == pf)

    Λόγω της προσθήκης του τελεστή μετατροπέα const char*() ενσωματωμένος τελεστής σύγκρισης

    θεωρείται επίσης σταθερή συνάρτηση. Με αυτό, το αριστερό λουλούδι τελεστών τύπου String μπορεί να μετατραπεί σε τύπο const char *.

    Υπάρχουν πλέον δύο καθιερωμένες συναρτήσεις τελεστή για τη χρήση του operator==() στο foo(). Η πρώτη

    Συμβολοσειρά::τελεστής==(const String &) const;

    απαιτεί μια καθορισμένη από το χρήστη μετατροπή του δεξιού τελεστή του pf από const char* σε συμβολοσειρά. Δεύτερος

    bool operator==(const char *, const char *)

    απαιτεί μια προσαρμοσμένη μετατροπή του αριστερού τελεστή του λουλουδιού από String σε const char*.

    Έτσι, η πρώτη καλά εδραιωμένη συνάρτηση είναι καλύτερη για τον αριστερό τελεστή και η δεύτερη είναι καλύτερη για τον δεξιό. Εφόσον δεν υπάρχει καλύτερη συνάρτηση, η κλήση επισημαίνεται ως διφορούμενη από τον μεταγλωττιστή.

    Όταν σχεδιάζετε μια διεπαφή κλάσης που περιλαμβάνει τη δήλωση υπερφορτωμένων τελεστών, κατασκευαστών και μετατροπέων, πρέπει να είστε πολύ προσεκτικοί. Οι μετατροπές που ορίζονται από το χρήστη εφαρμόζονται σιωπηρά από τον μεταγλωττιστή. Αυτό μπορεί να κάνει τους ενσωματωμένους τελεστές να είναι ανθεκτικοί στην ανάλυση υπερφόρτωσης για τελεστές με τελεστές τύπου κλάσης.

    Άσκηση 15.17

    Ονομάστε πέντε σύνολα υποψήφιων συναρτήσεων που λαμβάνονται υπόψη για την επίλυση υπερφόρτωσης τελεστών με τελεστές τύπου κλάσης.

    Άσκηση 15.18

    Ποιος τελεστής+() θα επιλεγεί ως ο τελεστής προσθήκης με την καλύτερη απόδοση στο main(); Καταχωρίστε όλες τις υποψήφιες συναρτήσεις, όλες τις καθιερωμένες συναρτήσεις και τις μετατροπές τύπων που θα εφαρμοστούν στα ορίσματα για κάθε καθιερωμένη συνάρτηση.

    Χώρος ονομάτων NS ( σύμπλεγμα κλάσεων ( σύμπλεγμα(διπλό); // ...); κλάση LongDouble (φίλος LongDouble operator+(LongDouble &, int) ( /* ... */ ) public: LongDouble(int); Operator double() ; LongDouble operator + (const complex &); // ... ); LongDouble operator

    Βασικά στοιχεία υπερφόρτωσης χειριστή

    Η C#, όπως κάθε γλώσσα προγραμματισμού, έχει ένα σύνολο από διακριτικά που χρησιμοποιούνται για την εκτέλεση βασικών λειτουργιών σε ενσωματωμένους τύπους. Για παράδειγμα, γνωρίζουμε ότι η πράξη + μπορεί να εφαρμοστεί σε δύο ακέραιους αριθμούς για να δώσει το άθροισμά τους:

    // Λειτουργία + με ακέραιους αριθμούς. int a = 100; int b = 240; int c = a + b; Το //s είναι τώρα 340

    Δεν υπάρχει τίποτα νέο εδώ, αλλά έχετε σκεφτεί ποτέ ότι η ίδια λειτουργία + μπορεί να εφαρμοστεί στους περισσότερους από τους ενσωματωμένους τύπους δεδομένων C#; Για παράδειγμα, σκεφτείτε αυτόν τον κώδικα:

    // Λειτουργία + με χορδές. string si = "Γεια"; string s2 = "world!"; συμβολοσειρά s3 = si + s2; // Το s3 περιέχει τώρα το "Hello world!"

    Ουσιαστικά, η λειτουργικότητα της λειτουργίας + βασίζεται μοναδικά στους τύπους δεδομένων που αντιπροσωπεύονται (συμβολοσειρές ή ακέραιοι αριθμοί σε αυτήν την περίπτωση). Όταν ο τελεστής + εφαρμόζεται σε αριθμητικούς τύπους, παίρνουμε το αριθμητικό άθροισμα των τελεστών. Ωστόσο, όταν η ίδια λειτουργία εφαρμόζεται σε τύπους συμβολοσειρών, προκύπτει συνένωση συμβολοσειρών.

    Η γλώσσα C# παρέχει τη δυνατότητα δημιουργίας ειδικών κλάσεων και δομών που επίσης ανταποκρίνονται μοναδικά στο ίδιο σύνολο βασικών διακριτικών (όπως ο τελεστής +). Λάβετε υπόψη ότι απολύτως κάθε ενσωματωμένος τελεστής C# δεν μπορεί να υπερφορτωθεί. Ο παρακάτω πίνακας περιγράφει τις δυνατότητες υπερφόρτωσης των κύριων λειτουργιών:

    Λειτουργία C# Δυνατότητα υπερφόρτωσης
    +, -, !, ++, --, αληθές, ψευδές Αυτό το σύνολο μοναδικών λειτουργιών μπορεί να υπερφορτωθεί
    +, -, *, /, %, &, |, ^, > Αυτές οι δυαδικές λειτουργίες μπορούν να υπερφορτωθούν
    ==, !=, <, >, <=, >= Αυτοί οι τελεστές σύγκρισης μπορεί να υπερφορτωθούν. Το C# απαιτεί συνεργατική υπερφόρτωση τελεστών "όμοιων" (δηλ.< и >, <= и >=, == και !=)
    Η λειτουργία δεν μπορεί να υπερφορτωθεί. Ωστόσο, οι δείκτες προσφέρουν παρόμοια λειτουργικότητα.
    () Ο χειριστής () δεν μπορεί να υπερφορτωθεί. Ωστόσο, η ίδια λειτουργικότητα παρέχεται από ειδικές μεθόδους μετατροπής
    +=, -=, *=, /=, %=, &=, |=, ^=, >= Οι τελεστές ανάθεσης συντομογραφίας δεν μπορούν να υπερφορτωθούν. Ωστόσο, τα λαμβάνετε αυτόματα υπερφορτώνοντας την κατάλληλη δυαδική λειτουργία

    Η υπερφόρτωση χειριστή σχετίζεται στενά με την υπερφόρτωση μεθόδου. Ο χειριστής είναι υπερφορτωμένος με τη λέξη-κλειδί χειριστήςΈνα που ορίζει μια μέθοδο τελεστή, η οποία με τη σειρά της ορίζει την ενέργεια του τελεστή σε σχέση με την κλάση του. Υπάρχουν δύο μορφές μεθόδων χειριστή (τελεστής): η μία για μοναδικούς τελεστές και η άλλη για δυαδικούς τελεστές. Ακολουθεί η γενική μορφή για κάθε παραλλαγή αυτών των μεθόδων:

    // Γενική μορφή υπερφόρτωσης ενός τελεστή. δημόσιος στατικός τελεστής return_type op(operand parameter_type) ( // λειτουργίες ) // Γενική μορφή υπερφόρτωσης δυαδικού τελεστή. δημόσιος στατικός τελεστής return_type op(param_type1 operand1, parameter_type2 operand2) ( // λειτουργίες )

    Εδώ, το op αντικαθίσταται από έναν υπερφορτωμένο τελεστή, όπως + ή /, και επιστροφή_τύπουυποδηλώνει τον συγκεκριμένο τύπο τιμής που επιστρέφεται από την καθορισμένη λειτουργία. Αυτή η τιμή μπορεί να είναι οποιουδήποτε τύπου, αλλά συχνά ορίζεται ότι είναι του ίδιου τύπου με την κλάση για την οποία ο τελεστής υπερφορτώνεται. Αυτή η συσχέτιση διευκολύνει τη χρήση υπερφορτωμένων τελεστών σε εκφράσεις. Για ενιαίους χειριστές όρος πράξηςδηλώνει τον μεταδιδόμενο τελεστή και για δυαδικούς τελεστές το ίδιο συμβολίζεται τελεστής 1και τελεστής 2. Λάβετε υπόψη ότι οι μέθοδοι χειριστή πρέπει να έχουν προσδιοριστές κοινού και στατικού τύπου.

    Υπερφόρτωση δυαδικών τελεστών

    Ας δούμε τη χρήση της υπερφόρτωσης δυαδικού τελεστή χρησιμοποιώντας το απλούστερο παράδειγμα:

    Χρήση του συστήματος. χρησιμοποιώντας System.Collections.Generic; χρησιμοποιώντας System.Linq; χρησιμοποιώντας System.Text; namespace ConsoleApplication1 ( κλάση MyArr ( // Συντεταγμένες ενός σημείου στον τρισδιάστατο χώρο δημόσιο int x, y, z; δημόσιο MyArr(int x = 0, int y = 0, int z = 0) ( this.x = x; αυτό. y = y; this.z = z; ) // Υπερφόρτωση δυαδικού τελεστή + δημόσιος στατικός τελεστής MyArr +(MyArr obj1, MyArr obj2) ( MyArr arr = νέο MyArr(); arr.x = obj1.x + obj2.x; arr. y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z; επιστροφή arr; ) // Υπερφόρτωση δυαδικού τελεστή - δημόσιος στατικός τελεστής MyArr -(MyArr obj1, MyArr obj2) ( MyArr arr = νέο MyArr (); arr.x = obj1.x - obj2.x; arr.y = obj1.y - obj2.y; arr.z = obj1.z - obj2.z; επιστροφή arr; ) ) κλάση Πρόγραμμα ( στατικό void Main (args συμβολοσειράς) ( MyArr Point1 = νέο MyArr(1, 12, -4); MyArr Point2 = νέο MyArr(0, -3, 18); Console.WriteLine("Συντεταγμένες πρώτου σημείου: " + Point1.x + " " + Point1.y + " " + Point1.z); Console.WriteLine("Συντεταγμένες δεύτερου σημείου: " + Point2.x + " " + Point2.y + " " + Point2.z + "\n"); MyArr Point3 = Point1 + Point2; Console.WriteLine("\nPoint1 + Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = Point1 - Point2; Console.WriteLine("\nPoint1 - Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Console.ReadLine(); ) ))

    Υπερφόρτωση Unary Operators

    Οι Unary τελεστές υπερφορτώνονται με τον ίδιο τρόπο όπως οι δυαδικοί. Η κύρια διαφορά είναι, φυσικά, ότι έχουν μόνο έναν τελεστή. Ας εκσυγχρονίσουμε το προηγούμενο παράδειγμα προσθέτοντας υπερφορτώσεις των τελεστών ++, --, -:

    Χρήση του συστήματος. χρησιμοποιώντας System.Collections.Generic; χρησιμοποιώντας System.Linq; χρησιμοποιώντας System.Text; namespace ConsoleApplication1 ( κλάση MyArr ( // Συντεταγμένες ενός σημείου στον τρισδιάστατο χώρο δημόσιο int x, y, z; δημόσιο MyArr(int x = 0, int y = 0, int z = 0) ( this.x = x; αυτό. y = y; this.z = z; ) // Υπερφόρτωση δυαδικού τελεστή + δημόσιος στατικός τελεστής MyArr +(MyArr obj1, MyArr obj2) ( MyArr arr = νέο MyArr(); arr.x = obj1.x + obj2.x; arr. y = obj1.y + obj2.y; arr.z = obj1.z + obj2.z; επιστροφή arr; ) // Υπερφόρτωση δυαδικού τελεστή - δημόσιος στατικός τελεστής MyArr -(MyArr obj1, MyArr obj2) ( MyArr arr = νέο MyArr (); arr.x = obj1.x - obj2.x; arr.y = obj1.y - obj2.y; arr.z = obj1.z - obj2.z; επιστροφή arr; ) // Υπερφόρτωση του unary τελεστής - δημόσιος στατικός τελεστής MyArr -(MyArr obj1) ( MyArr arr = νέο MyArr(); arr.x = -obj1.x; arr.y = -obj1.y; arr.z = -obj1.z; επιστροφή arr; ) // Υπερφόρτωση του unary τελεστή ++ δημόσιος στατικός τελεστής MyArr ++(MyArr obj1) ( obj1.x += 1; obj1.y += 1; obj1.z +=1; επιστροφή obj1; ) // Υπερφόρτωση του unary χειριστής -- δημ c στατικός τελεστής MyArr --(MyArr obj1) ( obj1.x -= 1; obj1.y -= 1; obj1.z -= 1; επιστροφή obj1; ) ) class Program ( static void Main(string args) ( MyArr Point1 = new MyArr(1, 12, -4); MyArr Point2 = new MyArr(0, -3, 18); Console.WriteLine("First point συντεταγμένες: " + Point1.x + " " + Point1.y + " " + Point1.z); Console.WriteLine("Συντεταγμένες του δεύτερου σημείου: " + Point2.x + " " + Point2.y + " " + Point2. z + "\n"); MyArr Point3 = Point1 + Point2; Console.WriteLine("\nPoint1 + Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = Point1 - Point2; Console.WriteLine("Point1 - Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point3 = -Point1; Console.WriteLine ("-Point1 = " + Point3.x + " " + Point3.y + " " + Point3.z); Point2++; Console.WriteLine("Point2++ = " + Point2.x + " " + Point2.y + " " + Point2.z); Point2--; Κονσόλα. WriteLine("Point2-- = " + Point2.x + " " + Point2.y + " " + Point2.z); Console.ReadLine(); ) ) )

    Πολλές γλώσσες προγραμματισμού χρησιμοποιούν τελεστές: τουλάχιστον, εκχωρήσεις (= , := ή παρόμοιους) και αριθμητικούς τελεστές (+ , - , * και /). Στις περισσότερες στατικά πληκτρολογημένες γλώσσες, αυτοί οι τελεστές συνδέονται με τύπους. Για παράδειγμα, στην Java, η προσθήκη με τον τελεστή + είναι δυνατή μόνο για ακέραιους αριθμούς, αριθμούς κινητής υποδιαστολής και συμβολοσειρές. Αν ορίσουμε τις δικές μας κλάσεις για μαθηματικά αντικείμενα, όπως πίνακες, μπορούμε να εφαρμόσουμε μια μέθοδο για την πρόσθεσή τους, αλλά μπορεί να καλείται μόνο με κάτι σαν αυτό: a = b.add(c) .

    Στην C++, δεν υπάρχει τέτοιος περιορισμός - μπορούμε να υπερφορτώσουμε σχεδόν οποιονδήποτε γνωστό τελεστή. Οι δυνατότητες είναι ατελείωτες: μπορείτε να επιλέξετε οποιονδήποτε συνδυασμό τύπων τελεστών, ο μόνος περιορισμός είναι ότι πρέπει να υπάρχει τουλάχιστον ένας τελεστής τύπου που ορίζεται από το χρήστη. Δηλαδή, ορίστε έναν νέο τελεστή σε ενσωματωμένους τύπους ή ξαναγράψτε έναν υπάρχοντα ειναι ΑΠΑΓΟΡΕΥΜΕΝΟ.

    Πότε πρέπει να υπερφορτώνετε τους χειριστές;

    Θυμηθείτε το κύριο πράγμα: υπερφόρτωση τελεστών εάν και μόνο εάν έχει νόημα. Αν δηλαδή το νόημα της υπερφόρτωσης είναι προφανές και δεν φέρει κρυφές εκπλήξεις. Οι υπερφορτωμένοι χειριστές θα πρέπει να ενεργούν το ίδιο με τις βασικές τους εκδόσεις. Φυσικά, οι εξαιρέσεις επιτρέπονται, αλλά μόνο σε περιπτώσεις που συνοδεύονται από κατανοητές εξηγήσεις. Ένα καλό παράδειγμα είναι οι χειριστές<< и >> τυπική βιβλιοθήκη iostream, η οποία σαφώς συμπεριφέρεται διαφορετικά από το κανονικό.

    Ακολουθούν καλά και κακά παραδείγματα υπερφόρτωσης χειριστή. Η παραπάνω προσθήκη μήτρας είναι μια ενδεικτική περίπτωση. Εδώ η υπερφόρτωση του τελεστή προσθήκης είναι διαισθητική και, αν εφαρμοστεί σωστά, είναι αυτονόητη:

    Πίνακας a, b; Πίνακας c = a + b;

    Ένα παράδειγμα κακής υπερφόρτωσης τελεστή προσθήκης θα ήταν η προσθήκη αντικειμένων δύο παίκτη σε ένα παιχνίδι. Τι εννοούσε ο δημιουργός της τάξης; Ποιο θα είναι το αποτέλεσμα; Δεν γνωρίζουμε τι κάνει η λειτουργία και επομένως είναι επικίνδυνη η χρήση αυτού του χειριστή.

    Πώς να υπερφορτώνετε τους χειριστές;

    Η υπερφόρτωση χειριστή είναι παρόμοια με την υπερφόρτωση συναρτήσεων με ειδικά ονόματα. Στην πραγματικότητα, όταν ο μεταγλωττιστής βλέπει μια έκφραση που περιέχει έναν τελεστή και έναν τύπο που ορίζεται από το χρήστη, αντικαθιστά αυτήν την έκφραση με μια κλήση στην κατάλληλη συνάρτηση του υπερφορτωμένου τελεστή. Τα περισσότερα από τα ονόματά τους ξεκινούν με τη λέξη-κλειδί τελεστής ακολουθούμενη από το όνομα τελεστή. Όταν η ονομασία δεν αποτελείται από ειδικούς χαρακτήρες, για παράδειγμα, στην περίπτωση τελεστή cast ή διαχείρισης μνήμης (νέος , διαγραφή κ.λπ.), η λέξη χειριστής και η ονομασία χειριστή πρέπει να διαχωρίζονται με ένα κενό (νέος τελεστής), Διαφορετικά ο χώρος μπορεί να αγνοηθεί (operator+ ).

    Οι περισσότεροι τελεστές μπορούν να υπερφορτωθούν τόσο με μεθόδους κλάσης όσο και απλές λειτουργίες, αλλά υπάρχουν μερικές εξαιρέσεις. Όταν ο υπερφορτωμένος τελεστής είναι μια μέθοδος κλάσης, ο τύπος του πρώτου τελεστή πρέπει να είναι αυτή η κλάση (πάντα *this) και ο δεύτερος πρέπει να δηλωθεί στη λίστα παραμέτρων. Επίσης, οι δηλώσεις μεθόδων δεν είναι στατικές, με εξαίρεση τις δηλώσεις διαχείρισης μνήμης.

    Κατά την υπερφόρτωση ενός τελεστή σε μια μέθοδο κλάσης, αποκτά πρόσβαση στα ιδιωτικά πεδία της κλάσης, αλλά η κρυφή μετατροπή του πρώτου ορίσματος δεν είναι διαθέσιμη. Επομένως, οι δυαδικές συναρτήσεις συνήθως υπερφορτώνονται ως ελεύθερες συναρτήσεις. Παράδειγμα:

    Κλάση Rational ( public: //Ο κατασκευαστής μπορεί να χρησιμοποιηθεί για σιωπηρή μετατροπή από int: Rational(int numerator, int παρονομαστής = 1); Rational operator+(Rational const& rhs) const; ); int main() ( Ορθολογική a, b, c; int i; a = b + c; //ok, δεν απαιτείται μετατροπή a = b + i; //ok, σιωπηρή μετατροπή του δεύτερου ορίσματος a = i + c; //ΣΦΑΛΜΑ: το πρώτο όρισμα δεν μπορεί να μετατραπεί σιωπηρά )

    Όταν οι unary τελεστές υπερφορτώνονται ως ελεύθερες συναρτήσεις, είναι διαθέσιμη σε αυτούς μια μετατροπή κρυφού ορίσματος, αλλά αυτή συνήθως δεν χρησιμοποιείται. Από την άλλη πλευρά, αυτή η ιδιότητα είναι απαραίτητη για δυαδικούς τελεστές. Η κύρια συμβουλή λοιπόν θα ήταν:

    Εφαρμόστε μοναδικούς τελεστές και δυαδικούς τελεστές όπως " Χ=” ως μέθοδοι κλάσης και άλλοι δυαδικοί τελεστές ως ελεύθερες συναρτήσεις.

    Ποιοι χειριστές μπορούν να υπερφορτωθούν;

    Μπορούμε να υπερφορτώσουμε σχεδόν οποιονδήποτε τελεστή C++, με την επιφύλαξη των ακόλουθων εξαιρέσεων και περιορισμών:

    • Δεν μπορείτε να ορίσετε έναν νέο τελεστή, όπως τελεστή**.
    • Οι ακόλουθοι τελεστές δεν μπορούν να υπερφορτωθούν:
      1. ?: (τριαδικός τελεστής);
      2. :: (πρόσβαση σε ένθετα ονόματα);
      3. . (πρόσβαση σε πεδία).
      4. .* (πρόσβαση πεδίου με δείκτη).
      5. τελεστές sizeof , typeid και cast.
    • Οι ακόλουθοι τελεστές μπορούν να υπερφορτωθούν μόνο ως μέθοδοι:
      1. = (ανάθεση);
      2. -> (πρόσβαση στα πεδία με δείκτη).
      3. () (κλήση συνάρτησης).
      4. (πρόσβαση με ευρετήριο).
      5. ->* (πρόσβαση δείκτη σε πεδίο με δείκτη).
      6. τελεστές μετατροπής και διαχείρισης μνήμης.
    • Ο αριθμός των τελεστών, η σειρά εκτέλεσης και η συσχέτιση των τελεστών καθορίζονται από την τυπική έκδοση.
    • Τουλάχιστον ένας τελεστής πρέπει να είναι τύπος που ορίζεται από το χρήστη. Το Typedef δεν μετράει.

    Στο επόμενο μέρος, θα γνωρίσετε υπερφορτωμένους τελεστές C++, ομαδικά και μεμονωμένα. Κάθε ενότητα χαρακτηρίζεται από σημασιολογία, δηλ. αναμενόμενη συμπεριφορά. Επιπλέον, θα παρουσιαστούν τυπικοί τρόποι δήλωσης και υλοποίησης τελεστών.