====== Κατασκευαστές Αντιγραφείς ====== Στην ενότητα των συναρτήσεων είδαμε [[cpp:functions#κλήση_με_τιμή_και_κλήση_με_αναφορά|την κλήση με τιμή και κλήση με αναφορά]] προκειμένου να περάσουμε παραμέτρους σε μία συνάρτηση. Κατά **την κλήση με τιμή** όταν η παράμετρος είναι ένα αντικείμενο, ένα αντίγραφο του αντικειμένου θα πρέπει να δημιουργηθεί στο //stack// της συνάρτησης που καλείται. Προκειμένου να γίνει αυτό η C++ ορίζει την έννοια του κατασκευαστή αντιγραφέα (//copy constructor//), ο οποίος δημιουργεί ένα αντικείμενο που είναι ακριβές αντίγραφο ενός άλλου αντικειμένου του ίδου τύπου. Δείτε το παρακάτω παράδειγμα της μεθόδου //printArea// η οποία λαμβάνει ως παράμετρο ένα αντικείμενο της κλάσης //Rectangle//. #include #include #include using namespace std; class Rectangle { private: int width, height; public: Rectangle(int w, int h); Rectangle(int s); Rectangle(); void setWidth(int w); void setHeight(int h); int getWidth(); int getHeight(); }; #include "Rectangle.hpp" Rectangle::Rectangle(int w, int h) { cout << "Calling 2 args constructor" << endl; width = w; height = h; } Rectangle::Rectangle(int s) { cout << "Calling 1 args constructor" << endl; width = s; height = s; } Rectangle::Rectangle() { cout << "Calling default constructor" << endl; width = height = 0; } void Rectangle::setWidth(int w) { width = w; } void Rectangle::setHeight(int h) { height = h; } int Rectangle::getWidth() { return width; } int Rectangle::getHeight() { return height; } #include using namespace std; #include "Rectangle.hpp" void printArea(const Rectangle r) { cout << " area: " << r.getArea() << endl; } int main() { Rectangle rect(5,6); printArea(rect); } Παρακάτω δίνεται το σχηματικό διάγραμμα της στοίβας της διεργασίας πριν και κατά τη διάρκεια της κλήσης της μεθόδου //printArea//. Κατά την κλήση της συνάρτησης //printArea//, διακρίνεται η αντιγραφή του αντικειμένου //rect// στη μεταβλητή //r// που ανήκει στη στοίβα της //printArea//. {{ :cpp:cppstackcopyconstructor.png |}} Το ερώτημα είναι με ποιό τρόπο γίνεται η δημιουργία του αντιγράφου του αρχικού αντικειμένου στο //stack//. Μπορείτε να δηλώσετε τον δικό σας κατασκευαστή αντιγραφέα (λεπτομέρειες πιο κάτω..) ή να αφήσετε τον //compiler// να δηλώσει τον //default//. Ο //default// κατασκευαστής αντιγραφέας δημιουργεί το νέο αντικείμενο από το παλιό αντιγράφοντας τα πεδία ένα προς ένα. Εάν αυτός ο τρόπος σας ικανοποιεί, τότε δεν χρειάζεται να κάνετε κάτι περισσότερο. Εάν όμως η παραπάνω μεθοδολογία δεν είναι ικανοποιητική, είστε αναγκασμένοι να ορίσετε τον δικό σας κατασκευαστή αντιγραφέα. ===== Ορισμός ενός κατασκευαστή αντιγραφέα ===== Ένας κατασκευαστής αντιγραφέας για την παραπάνω κλάση //Rectangle// θα μπορούσε να είναι ο εξής: Rectangle::Rectangle(Rectangle &r) { width = r.width; height = r.height; } ή ισοδύναμα ο παρακάτω Rectangle::Rectangle(const Rectangle &r) { width = r.width; height = r.height; } Και οι δύο παραπάνω κατασκευαστές είναι ισοδύναμοι και δηλώνουν ένα κατασκευαστή αντιγραφέα. Η μεταβλητή //r// μπορεί να δηλωθεί ως //const// (όπως στη δεύτερη περίπτωση), διότι κατά την εκτέλεση του κατασκευαστή αντιγραφέα το αντικείμενο //r// δεν μεταβάλλεται. Συνολικά, η κλάση //Rectangle// διαμορφώνεται ως εξής: #include #include #include using namespace std; class Rectangle { private: int width, height; public: Rectangle(int w, int h); Rectangle(int s); Rectangle(); Rectangle(const Rectangle& r); void setWidth(int w); void setHeight(int h); int getWidth(); int getHeight(); }; #include "Rectangle.hpp" Rectangle::Rectangle(int w, int h) { cout << "Calling 2 args constructor" << endl; width = w; height = h; } Rectangle::Rectangle(int s) { cout << "Calling 1 args constructor" << endl; width = s; height = s; } Rectangle::Rectangle() { cout << "Calling default constructor" << endl; width = height = 0; } Rectangle::Rectangle(const Rectangle& r) { cout << "Calling copy constructor" << endl; width = r.width; height = r.height; } void Rectangle::setWidth(int w) { width = w; } void Rectangle::setHeight(int h) { height = h; } int Rectangle::getWidth() { return width; } int Rectangle::getHeight() { return height; } Εάν δεν ορίσετε ένα δικό σας κατασκευαστή αντιγραφέα ο //compiler// δημιουργεί τον //default copy constructor//. Ο //default// αντιγράφει τα περιεχόμενα του αντικειμένου που δίνεται ως όρισμα στο νέο αντικείμενο πεδίο προς πεδίο. ==== Άλλη περίπτωση κλήσης κατασκευαστή αντιγραφέα ==== Μία άλλη περίπτωση κατά την οποία θα κληθεί o κατασκευαστής αντιγραφέας είναι η παρακάτω. #include "Rectangle.hpp" int main() { Rectangle r1(5,6); Rectangle r2 = r1; } Εδώ η δήλωση της μεταβλητής r2 συμπίπτει με την αρχικοποίηση του αντικειμένου. Σε αυτή την περίπτωση καλείται ο κατασκευαστής αντιγραφέας με όρισμα το //r1//. Το παραπάνω είναι λειτουργικά ισοδύναμο με το εξής: #include "Rectangle.hpp" int main() { Rectangle r1(5,6); Rectangle r2; r2 = r1; } Για τον μεταγλωττιστή όμως οι δύο κώδικες είναι διαφορετικοί. Στην πρώτη περίπτωση καλείται ο κατασκευαστής αντιγραφέας (//copy constructor//), ενώ στη δεύτερη περίπτωση καλείται ο //default// κατασκευαστής και στη συνέχεια γίνεται ανάθεση των τιμών των πεδίων του //r1// στα πεδία του //r2// (πεδίο προς πεδίο). **Σημείωση:** Κάποιες νεότερες εκδόσεις του μεταγλωττιστή εισάγουν την κλήση του κατασκευαστή αντιγραφέα και σε αυτή την περίπτωση με σκοπό τη βελτίωση της επίδοσης. ===== Πιο σύνθετες περιπτώσεις ===== ==== 1η Περίπτωση - Αρχικοποίηση ενός αντικειμένου με δυναμικά δεσμευμένα πεδία από άλλο αντικείμενο ==== Στις περιπτώσεις που υπάρχουν πεδία που είναι δείκτες και δείχνουν σε άλλα αντικείμενα (στατικά ή δυναμικά δεσμευμένα) εάν δεν ορίσετε τον δικό σας κατασκευαστή αντιγραφέα, ο //default// αντιγράφει τις διευθύνσεις αυτές πεδίο προς πεδίο, όπως θα αντιγράφονταν οποιοδήποτε άλλο πεδίο. Αυτό πρακτικά σημαίνει ότι δύο ή περισσότερα αντικείμενα δείχνουν σε μία κοινή περιοχή μνήμης μέσω των αντίστοιχων πεδίων τους. Το παραπάνω μπορεί να προκαλέσει δυσλειτουργίες, καθώς η μεταβολή του περιεχομένου της κοινής μνήμης επηρεάζει το σύνολο των αντικειμένων που το μοιράζονται. Ενδεικτικό είναι το παρακάτω παράδειγμα. Στο παρακάτω παράδειγμα ορίζουμε την κλάση //Point// η οποία αντιπροσωπεύει ένα σημείο στο δισδιάστατο χώρο. #include using namespace std; class Point { int x, y; public: Point(int vx,int vy) { x = vx; y = vy; cout << "Point regular constructor!\n"; } Point(const Point &p) { x = p.x; y = p.y; cout << "Point copy constructor!\n"; } Point() { cout << "Point default constructor!\n"; } ~Point() { cout << "xP Point destructor!\n"; } void setX(int vx) { x = vx; } void setY(int vy) { y = vy; } int getX() const { return x; } int getY() const { return y; } }; Η κλάση //Rectangle// που ακολουθεί ορίζει το πεδίο //origin// που είναι δείκτης σε ένα αντικείμενο τύπου //Point//. Η δημιουργία ενός αντικειμένου τύπου //Rectangle// συνεπάγεται τη δυναμική δέσμευση μνήμης για το αντικείμενο τύπου //Point// που αυτή περιέχει. Δείτε το παράδειγμα που ακολουθεί. #include #include #include using namespace std; #include "Point.hpp" class Rectangle { private: int width, height; Point *origin; public: Rectangle(int w, int h, Point p); Rectangle(int s, Point p); Rectangle(); ~Rectangle(); void setWidth(int w); void setHeight(int h); int getWidth(); int getHeight(); void setOrigin(Point *p); Point *getOrigin() const; }; #include "Rectangle.hpp" Rectangle::Rectangle(int w, int h, Point p) { width = w; height = h; origin = new (nothrow) Point( p.getX(), p.getY() ); if(origin == NULL) { cerr << "Memory allocation failure!\n"; exit(-1); } cout << "Calling 2 args constructor" << endl; } Rectangle::Rectangle(int s, Point p) : Rectangle(s,s,p) { cout << "Calling 1 args constructor" << endl; } Rectangle::Rectangle() : Rectangle(0,Point()) { cout << "Calling 0 args constructor" << endl; } Rectangle::~Rectangle() { delete origin; } void Rectangle::setWidth(int w) { width = w; } void Rectangle::setHeight(int h) { height = h; } int Rectangle::getWidth() { return width; } int Rectangle::getHeight() { return height; } void Rectangle::setOrigin(Point *p) { origin = p; } Point *Rectangle::getOrigin() const { return origin; } #include "Rectangle.hpp" int main() { Point p{5,5}; Rectangle r1{5,6,p}; Rectangle r2 = r1; } Ο παραπάνω κώδικας δεν περιέχει κατασκευαστή αντιγραφέα για την κλάση Rectangle. Ο //default// που δημιουργεί ο //compiler//, αντιγράφει στο αντικείμενο //r2// τα πεδία του αντικειμένου //r1// πεδίο προς πεδίο. Αυτό σημαίνει ότι τα αντικείμενα //r1// και //r2// μοιράζονται το ίδιο αντικείμενο τύπου //Point//. Ισχύουν επομένως τα εξής: * Εάν μεταβληθούν οι συντεταγμένες του //Point// από το αντικείμενο //r1//, η μεταβολή θα ισχύει και για το αντικείμενο //r2//. * Κατά την έξοδο από τη συνάρτηση main, το αντικείμενο //r1// θα καταστραφεί ελευθερώνοντας τη δεσμευμένη μνήμη για το πεδίο του //origin//. Η προσπάθεια καταστροφής του αντικειμένου //r2// θα οδηγήσει σε σφάλμα διότι θα προσπαθήσει να ελευθερώσει μία περιοχή μνήμης που έχει ήδη ελευθερωθεί κατά την καταστροφή του //r1//. Το σφάλμα που εκτυπώνεται όταν το πρόγραμμα εκτελεστεί είναι το εξής: free(): double free detected in tcache 2 Aborted (core dumped) Για να αποφύγετε την παραπάνω συμπεριφορά θα πρέπει να ορίσετε τον δικό σας κατασκευαστή αντιγραφέα που κάνει τα εξής: - Δημιουργεί ένα νέο αντικείμενο τύπου //Point//. - Αντιγράφει τα περιεχόμενα του παλιού αντικειμένου στο νέο. Ο προτεινόμενος κατασκευαστής αντιγραφέας δίνεται παρακάτω: Rectangle::Rectangle(const Rectangle &r) { width = r.width; height = r.height; if(r.origin == nullptr) origin = nullptr; else { origin = new (nothrow) Point(r.getOrigin()->getX(), r.getOrigin()->getY()); if(origin == NULL) { cerr << "Memory allocation failure!"; exit(-1); } } } ==== 2η Περίπτωση - Κλήση συνάρτησης με παράμετρο αντικείμενο που περιέχει δυναμικά δεσμευμένα πεδία ==== #include "Rectangle.hpp" int moveOrigin(Rectangle r, int dx, int dy) { Point *p = r.getOrigin(); p->setX(p->getX() + dx); p->setY(p->getY() + dy); } int main() { Point p{5,5}; Rectangle r1{5,6,p}; moveOrigin(r1, 1,-1); } Η κλήση της συνάρτησης moveOrigin θα απαιτήσει τη δημιουργία ενός αντιγράφου του r1 στο stack για τις ανάγκες της εκτέλεσης της συγκεκριμένης συνάρτησης. Εφόσον δεν έχουμε δημιουργήσει κατασκευαστή αντιγραφέα, τα πεδία του r1 θα αντιγραφούν ένα προς ένα στην τοπική μεταβληή r. Αυτό θα έχει ως συνέπεια το πεδίο origin των αντίκειμένων r εντός της συνάρτησης moveOrigin και r1 της main να δείχνουν σε μία κοινή περιοχή δυναμικά δεσμευμένης μνήμης. Κατά την έξοδο από τη συνάρτηση η κοινή αυτή περιοχή θα καταστραφεί, έτσι ώστε το αντικείμενο r1 στη main να δείχνει σε μία περιοχή που έχει ήδη αποδεσμευτεί. Η έξοδος από τη main θα σηματοδοτήσει την καταστροφή του r1 μέσω του καταστροφέα της κλάσης. Όταν ο καταστροφέας θα επιχειρήσει να αποδεσμεύσει μνήμη που έχει ήδη αποδεσμευτεί θα λάβετε ένα μήνυμα λάθους όμοιο με το μήνυμα της προηγούμενης περίπτωσης. free(): double free detected in tcache 2 Aborted (core dumped) Η δημιουργία του κατασκευαστή αντιγραφέα που προτάθηκε παραπάνω λύνει και αυτό το πρόβλημα.