Kihagyás

Klónok, másolás

Másolás dinamikus memóriával

Azt már megszoktuk, ha egy egész számot lemásolunk, akkor megkapjuk a ,,tökéletes'' másolatát, azaz a másolat felveszi azt az értéket, amit az eredeti reprezentált.

1
2
3
4
5
int main() {
  int a = 6;
  int b;
  b = a;  // ekkor 'a' értéke lemásolódik, és 'b' az lesz, ami 'a' értéke volt
}

Ezt megtehetjük objektumokkal is (a string-nél már használtuk és működik). Mivel egy objektumot az egyes adattagok értékei határoznak meg, másoláskor ezeket az értékeket le kell másolnunk. "Egyszerű" esetben ez történik másoláskor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class A {
  int a;
public:
  A(int a = 6) : a(a) {}

  int get_a() const { return a; }
};

int main() {
  A a(4);
  A b;
  cout << b.get_a() << " -> ";
  b = a;
  cout << b.get_a() << endl;
}

A fenti példában nem mondtuk meg, hogyan történjen a másolás, ezért a default másolás futott le, mely minden adattagnak az értékét átmásolta. A kimenet emiatt:

6 -> 4

Ez azonban nem megfelelő akkor, ha az objektumban van dinamikusan foglalt memória. Ennek oka, hogy a dinamikusan foglalt memória nem része az objektumnak, csak az a pointer, ami a lefoglalt memóriaterületre mutat. Mivel minden adattag értéke másolásra kerül, a másolt objektum pointere is ugyanarra a területre fog mutatni (hiszen csak a mutatót másoljuk le). Ekkor mind a két objektum ugyanazt a memóriát használja. Ha az egyik módosít rajta, akkor a másiknál is látható ez a változás (hiszen ugyanazt memóriát használják). A másolást viszont azért használjuk, mert meg szeretnénk őrizni az eredeti értéket.

A Kurzus osztály dinamikus memóriával vizuálisan: operator+

Ha ezt default módon lemásolnánk: operator+

Látszódik, hogy a két külön objektum pointere ugyanarra a heap memória területen lévő elemre mutat. Ha ezt módosítjuk is: operator+ Módosítás után a kurzus2 hallgatói változtak, azonban a kurzus1 hallgatói is megváltoztak, hiszen ugyanaz a kettő.

A helyes másolás ebben az esetben azt jelentené, hogy minden értéket lemásolunk pontosan, de a dinamikusan foglalt memória esetében foglalunk új helyet és minden egyes értéket egyesével átmásolunk. Ekkor a pointer az új területre fog mutatni, ahol ugyan egyező értékek vannak, mégsem ugyan az: operator+

Módosítás esetében ekkor nem ütközünk az előző problémába: operator+ A módosított elem teljesen más memória területen van, így az eredeti értéket nem módosítjuk.

Másoló konstruktor (copy constructor)

Ahhoz, hogy dinamikus memóriakezelés esetében megfelelő másolást használjunk, meg kell mondanunk, hogy hogyan történjen a másolás. Ennek egy módja a másoló konstruktor. Copy constructor használatakor egy másik objektum alapján hozzuk létre az új objektumunkat.

Mivel a másoló konstruktor is konstruktor, ennek a neve is megegyezik az osztály nevével, de paraméterében egy ugyanolyan típusú objektumot vár. Mivel a lemásolt objektum nem változik és nem akarunk másolást végrehajtani a paraméter átadásakor, ezért a paramétert konstans referenciaként (const &) adjuk meg.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<iostream>
using namespace std;

class CopyCon {
  int value;
  int *heap_int;
public:
  CopyCon(int value = 1, int heap_value = 2) : value(value), heap_int(new int(heap_value)) {}

  //Copy konstruktor
  CopyCon(const CopyCon& cc) : value(cc.value), heap_int(new int(*cc.heap_int)) {}

  int get_heap_int() const {
    return *heap_int;
  }

  void set_heap_int(int a) {
    *heap_int = a;
  }
};

int main() {
  CopyCon a(3);
  a.set_heap_int(8);

  CopyCon b(a);
  cout << b.get_heap_int() << endl;
}

A fenti példában a másoló konstruktor kap egy objektumot, és annak az értékeit lemásolja. A dinamikusan foglalt elemek esetében foglal memóriát majd a memóriában tárolt értéket is átmásolja. Ha a new[] operátorral tömböt foglaltunk volna, akkor minden egyes elemet át kellene másolni, pl. egy for-ciklussal.

Az objektumok hasonlóan viselkednek a primitív típusokhoz, így kérdéses lehet, hogy a másolás elvégezhetjük-e ugyan azzal a szintaxissal mint pl. egy egésznél:

1
2
int a = 5;
int b = a;

Erre van mód:

1
2
CopyCon a(7);
CopyCon b = a;  // Ez is a "b(a)"" hívást jelenti, csak olvashatóbb formában

Természetesen az 'explicit' kulcsszó a copy constructorra is hatással van!

Assignment operátor

A másoló konstruktor lehetőséget adott arra, hogy egy másik objektum mintájára hozzuk létre az új objektumot. Azonban szükség lehet arra is, hogy egy létező objektumnak szeretnénk értékül adni egy másik objektumot, azaz később adunk értéket egy már létező objektumnak:

1
2
3
4
5
6
int b = 6;
int c = 8;
c++; b++;   // használjuk a b-t vagy a c-t
/* ... */   // majd c-nek új értéket akarjuk adni
            // de nem hozhatjuk létre újra az int c = b; utasítással, mert az fordítási hibához vezetne (hiszen c már létezik)
c = b;      // új értéket adhatunk c-nek, de ilyenkor nem hívódik meg a copy konstruktor

Objektumok esetében is van lehetőségünk megmondani, hogy hogyan történjen meg az értékadás. Ehhez az operator=-t kell felüldefiniálni. Mivel egy másik objektum alapján szeretnénk beállítani az értékeket:

  • ugyanúgy másolás mint a copy konstruktor
  • dinamikus memória használatra figyelni kell
  • azonos eredményt kell kapjunk mintha copy constructorral másoltunk volna
  • de előtte "törölni kell" azt az objektumot, amit éppen aktuálisan már reprezentál
  • a kiinduló/másolandó objektumot ebben az esetben sem akarjuk módosítani, tehát a paramétert konstans referenciaként (const &) fogadjuk ebben az esetben is
  • az értékadás után egy objektumot kapunk, így a visszatérési érték egy referencia az adott objektumra

Példa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class AssignOp {
  int *ip;
 public:
  AssignOp(int n) : ip(new int) {
    *ip = n;
  }

  AsignOp(const AssignOp& ao) : ip(new int) {
    *ip = *(ao.ip);
  }

  //Assignment operatátor, avagy értékadó operátor
  AssignOp& operator=(const AssignOp& ao) { // "Jó" a működése, de valami még hiányzik!
    delete ip; // létező dinamikus adattag törlése (hiszen elveszítjük rá a pointert, a memóriaszivárgásra pedig figyelünk)
    ip = new int;
    *ip = *(ao.ip);

    return *this;
  }
};

Az assignment operátor használatával így egy másik AssignOp értékét állítjuk be az adott objektumnak. A foglalt memóriát felszabadítjuk. Ebben az esetben nincsen eltérés, hiszen egy elem mindig egy lesz, azonban ha ez egy változó tömb lenne, a méret változhat, nem biztos, hogy elfér az új érték az eddigi memóriában. Foglalunk új memóriát majd a megfelelő értéket is átmásoljuk. Ennek a hibája akkor jön elő, ha a következő kifejezést szeretnénk leírni:

1
2
AssignOp a(5);
a = a;

Ekkor *this és az ao is azonos objektum. Az első lépés, hogy az adott objektum (*this) által használt memóriát (erőforrást) felszabadítjuk, ami az ao-hoz tartozó memória is, hiszen azonos a kettő. Utána foglalunk új helyet, ez még helyes. Mikor az értéket akarjuk beállítani, akkor az előbb felszabadított memóriából szeretnénk olvasni. Ennek a kivédésére meg kell vizsgálnunk, hogy a két objektum azonos-e, tehát a memória címük ugyanaz-e. Assignment operátor helyesen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
AssignOp& operator=(const AssignOp& ao) {
  if ( this == &ao )  // this egy pointer az objektumra &ao a kapott objektum címe
    return *this;     // a = a esetében önmagát kell kapjuk
                        // speciális esetben ennél több is kellhet

  delete array;
  array = new int;
  *array = *(ao.array);
  return *this;
}

Mivel a copy constructor és az assignment operátor ilyen szoros kapcsolatban állnak, egyszerre kell megvalósítani (vagy letiltani, azaz = delete) azokat.

postfix ++ operátor

A postfix és prefix operator++-ról már volt szó. Működésükben a visszaadott érték esetében van különbség. Postfix esetben a "régi" értéket le kell másolni. Mivel másolatot használunk, dinamikus memóriakezeléskor a postfix ++ operátorhoz kell a copy constructor és az assignment operator.

Példa postfix ++ operátorra:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class PostFixOp {
  int* p; // dinamikusan foglalt memória pointere
public:
  // konstruktor
  // copy-constructor
  // assignment operator
  // destruktor

  // prefix ++, ++k
  PostFixOp& operator++() {
    // A megadott növelési művelet
      return *this;
  }

  // postfix ++, k++
  PostFixOp operator++(int) {
      PostFixOp copied_to_return = *this; // copy-constructor lefut

      operator++(); // megcsináljuk az objektumon az adott növelést
                    // ha van rá fgv, ne írjuk meg újra

      return copied_to_return;  // a régi állapotot visszaadjuk
                                // de közben az objektum megváltozott
  }
};

Objektumok klónozása

Az eddigi másolásokban, értékadásokban a közös az volt, hogy mindig tudtuk, hogy pontosan mit, milyen típusú elemet szeretnénk másolni. De mi van akkor, ha a másolás során is szükség lenne arra, hogy azt "polimorfikusan" oldjuk meg? Nyilván ez nem lehetséges, hiszen maguk a konstruktorok nem lehetnek virtuálisak, hiszen nem létezik az objektum, amelyen keresztül meg szeretnénk hívni azt. A megoldás ilyenkor egy olyan metódus biztosítása, ami konkrétan a másolásért felel, de ami virtuálissá tehető, és ilyen esetben ezt alkalmazhatjuk. A Java nyelv mintájára ennek a metódusnak a neve lehet clone, amely feladata, hogy az aktuális objektumról egy másolatot készítsen és adjon vissza, és amely a virtual kulcsszóval polimorfikussá is tehető.

Példaként tekintsük az Egyetem osztályt, amely szeretné a nyilvántartásában jelen levő alkalmazottakat (illetve a nyilvántartás másolatát) átadni a GazdaságiOsztaly-nak. Ehhez el kell készítenie az Alkalmazottakat tároló vectornak a másolatát. (Biztonsági okokból nem szeretnék, hogy a saját rendszerükbe a GazdaságiOsztaly bele tudjon nyúlni....). A Kutatok speciális alkalmazottak, az ő másolásuk nem menne csupán az Alkalmazott osztály copy konsrtuktorával. Hogy mindezt kipróbáljuk, a következő kódban implementáltuk a gazdasági osztályt, amely az alkalmazottakLekerese függvénnyel hozzáfér az egyetem alkalmazottaihoz. (Ahhoz, hogy ezt megtehesse, hiszen az Egyetem osztály ezen adattagja private, az Egyetem osztályon belül ezt engedélyezni lehet azzal, hogy ezen függvényt friend-ként felveszi az Egyetem osztály.)

Mind az Alkalmazott, mind a Kutato osztály rendelkezik copy konstruktorral. Amennyiben a GazdasagiOsztaly ezekkel próbálná meg az alkalmazottak másolását (alkalmazottak.push_back(new Alkalmazott(*a));), Kutatok esetében is csak azok Alkalmazott részét másolná át. (Ezt kipróbálhatjuk, ha a másolatoknak meghívjuk a megbeszelesreJar függvényét.) Ezzel szemben a clone metódus hívása a megfelelő módon teszi meg a másolást.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
#include <iostream>
#include <vector>;
#include <algorithm>
using namespace std;
class Alkalmazott  {
public:
  const string& nev;
  Alkalmazott(const std::string& nev) : nev(nev){}

  virtual ~Alkalmazott() {}

  Alkalmazott(const Alkalmazott& a) : nev(a.nev){}
  virtual void megbeszelesreJar() const {
    cout << "Alkalmazott megbeszelesre ment." << endl;
  }

  virtual Alkalmazott* clone() const{
    return new Alkalmazott(*this);
  }
};

class Kutato : public Alkalmazott {
  unsigned munkak;
  int* munkassag = nullptr;
public:
  void megbeszelesreJar() const override { // hiszen Alkalmazott
    std::cout << "A kutatoi munka igen sok eszmecseret igenyel mas kutatokkal." << std::endl;
  }

  Kutato(const std::string & nev, unsigned munkak) : Alkalmazott(nev), munkak(munkak), munkassag(new int[munkak]) {}
  Kutato(const Kutato& k) : Alkalmazott(k.nev) {
    if (k.munkassag != nullptr) {
      munkak = k.munkak;
      munkassag = new int[k.munkak];
      for(int i=0;i<munkak;i++) {
        munkassag[i]=k.munkassag[i];
      }
    }
  }

  virtual Kutato* clone() const{
    return new Kutato(*this);
  }

  ~Kutato() {
        delete[] munkassag; // new[] -> delete[]
    }
};

class Egyetem;

class GazdasagiOsztaly {
  vector<Alkalmazott*> alkalmazottak;
public:
  void alkalmazottakLekerese(const Egyetem& egyetem);
  void megbeszeles() const {
    for(const auto& alk : alkalmazottak)
      alk->megbeszelesreJar();
  }
};

class Egyetem {
  vector<Alkalmazott*> alkalmazottak;
public:
  Egyetem() = default;

  ~Egyetem() {
    for(int i=0; i<alkalmazottak.size();i++)
      delete alkalmazottak[i];
  }

  Egyetem& operator+(Alkalmazott* alkalmazott) {
    alkalmazottak.push_back(alkalmazott);
    return *this;
  }

  void megbeszeles() const {
    for(const auto& alk : alkalmazottak)
      alk->megbeszelesreJar();
  }

  friend void GazdasagiOsztaly::alkalmazottakLekerese(const Egyetem& egyetem);
};

void GazdasagiOsztaly::alkalmazottakLekerese(const Egyetem& egyetem) {
  for(const auto& a : egyetem.alkalmazottak) {
    //alkalmazottak.push_back(new Alkalmazott(*a));  //NEM JO, mert itt "csak" Alkalmazott lesz a Kutato is
    alkalmazottak.push_back(a->clone());
  }
}

int main() {
  Egyetem e;
  Alkalmazott *a = new Alkalmazott("Bela");
  Kutato *k = new Kutato("Tudos", 10);
  e+k+a;

  GazdasagiOsztaly go;
  go.alkalmazottakLekerese(e);
  go.megbeszeles();
}

Utolsó frissítés: 2024-11-21
Létrehozva: 2018-11-23