User Tools

Site Tools


cpp:copy_constructors

Κατασκευαστές Αντιγραφείς

Στην ενότητα των συναρτήσεων είδαμε την κλήση με τιμή και κλήση με αναφορά προκειμένου να περάσουμε παραμέτρους σε μία συνάρτηση. Κατά την κλήση με τιμή όταν η παράμετρος είναι ένα αντικείμενο, ένα αντίγραφο του αντικειμένου θα πρέπει να δημιουργηθεί στο stack της συνάρτησης που καλείται. Προκειμένου να γίνει αυτό η C++ ορίζει την έννοια του κατασκευαστή αντιγραφέα (copy constructor), ο οποίος δημιουργεί ένα αντικείμενο που είναι ακριβές αντίγραφο ενός άλλου αντικειμένου του ίδου τύπου.

Δείτε το παρακάτω παράδειγμα της μεθόδου printArea η οποία λαμβάνει ως παράμετρο ένα αντικείμενο της κλάσης Rectangle.

Rectangle.hpp
#include <iostream>
#include <cstdlib>
#include <ctime>
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();
};
Rectangle.cpp
#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; }
foo.cpp
#include <iostream>
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.

Το ερώτημα είναι με ποιό τρόπο γίνεται η δημιουργία του αντιγράφου του αρχικού αντικειμένου στο 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 διαμορφώνεται ως εξής:

Rectangle.hpp
#include <iostream>
#include <cstdlib>
#include <ctime>
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();
};
Rectangle.cpp
#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 κατασκευαστής αντιγραφέας είναι η παρακάτω.

CopyRectangle.cpp
#include "Rectangle.hpp"
 
int main() {
  Rectangle r1(5,6);
  Rectangle r2 = r1;
}

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

Το παραπάνω είναι λειτουργικά ισοδύναμο με το εξής:

CopyRectangle.cpp
#include "Rectangle.hpp"
 
int main() {
  Rectangle r1(5,6);
  Rectangle r2;
  r2 = r1;
}

Για τον μεταγλωττιστή όμως οι δύο κώδικες είναι διαφορετικοί. Στην πρώτη περίπτωση καλείται ο κατασκευαστής αντιγραφέας (copy constructor), ενώ στη δεύτερη περίπτωση καλείται ο default κατασκευαστής και στη συνέχεια γίνεται ανάθεση των τιμών των πεδίων του r1 στα πεδία του r2 (πεδίο προς πεδίο).

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

Πιο σύνθετες περιπτώσεις

1η Περίπτωση - Αρχικοποίηση ενός αντικειμένου με δυναμικά δεσμευμένα πεδία από άλλο αντικείμενο

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

Στο παρακάτω παράδειγμα ορίζουμε την κλάση Point η οποία αντιπροσωπεύει ένα σημείο στο δισδιάστατο χώρο.

Point.hpp
#include <iostream>
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 που αυτή περιέχει. Δείτε το παράδειγμα που ακολουθεί.

Rectangle.hpp
#include <iostream>
#include <cstdlib>
#include <ctime>
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;
};
Rectangle.cpp
#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; }
CopyRectangle.cpp
#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)

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

  1. Δημιουργεί ένα νέο αντικείμενο τύπου Point.
  2. Αντιγράφει τα περιεχόμενα του παλιού αντικειμένου στο νέο.

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

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η Περίπτωση - Κλήση συνάρτησης με παράμετρο αντικείμενο που περιέχει δυναμικά δεσμευμένα πεδία

MoveOrigin.cpp
#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)

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

cpp/copy_constructors.txt · Last modified: 2022/05/12 19:41 by gthanos