User Tools

Site Tools


cpp:copy_constructors

This is an old revision of the document!


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

Στην ενότητα των συναρτήσεων είδαμε την κλήση με τιμή και κλήση με αναφορά προκειμένου να περάσουμε παραμέτρους σε μία συνάρτηση. Κατά την κλήση με τιμή όταν η παράμετρος είναι ένα αντικείμενο, ένα αντίγραφο του αντικειμένου θα πρέπει να δημιουργηθεί στο 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::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(Rectangle& r);
    void setWidth(int w);
    void setHeight(int h);
    int getWidth();
    int getHeight();
};
 
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(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 αντιγράφει τα περιεχόμενα του αντικειμένου που δίνεται ως όρισμα στο νέο αντικείμενο πεδίο προς πεδίο.

Άλλη περίπτωση κλήση Copy Constructor

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

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

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

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

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

Μια πιο σύνθετη περίπτωση

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

Στο παρακάτω παράδειγμα ορίζουμε την κλάση 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() { return x; }
    int getY() { 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();
    Rectangle(Rectangle &r);
    void setWidth(int w);
    void setHeight(int h);
    int getWidth();
    int getHeight();
    void setOrigin(Point *p);
    Point *getOrigin();
};
 
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() { return origin; }
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{10,5};
  Rectangle r1{5,6,p};  
  Rectangle r2 = r1;
 
  moveOrigin(r1, 1,-1);
}

Ο παραπάνω κώδικας αντιγράφει στο αντικείμενο r2 τα πεδία του αντικειμένου r1 πεδίο προς πεδίο. Αυτό σημαίνει ότι τα αντικείμενα r1 και r2 μοιράζονται το ίδιο αντικείμενο τύπου Point. Ισχύουν επομένως τα εξής:

  • Εάν μεταβληθούν οι συντεταγμένες του Point από το αντικείμενο r1, η μεταβολή θα ισχύει και για το αντικείμενο r2.
  • Κατά την έξοδο από τη συνάρτηση main, το αντικείμενο r1 θα καταστραφεί ελευθερώνοντας τη δεσμευμένη μνήμη για το πεδίο του origin. Η προσπάθεια καταστρροφής του αντικειμένου r2 θα οδηγήσει σε σφάλμα διότι θα προσπαθήσει να ελευθερώσει μία περιοχή μνήμης που έχει ήδη ελευθερωθεί κατά την καταστροφή του r1. Το σφάλμα που εκτυπώνεται όταν το πρόγραμμα εκτελεστεί είναι το εξής:
*** Error in `./a.out': double free or corruption (fasttop): 0x00000000006d6010 ***
Ακυρώθηκε (core dumped)

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

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

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

Rectangle::Rectangle(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);
    }
  }
}
cpp/copy_constructors.1586877879.txt.gz · Last modified: 2020/04/14 14:24 (external edit)