Kihagyás

8. gyakorlat

Ismétlés

Destruktor

A destruktor hasonló a már jól ismert konstruktorhoz, azonban ez az objektum életének végéhez kötődik. Míg a konstruktor az objektum létrejöttekor fut le, a destruktor akkor, amikor az objektum megszűnik létezni. Ez semmilyen extra hívást nem jelent, teljesen automatikus. Fontos, hogy nem összekeverendő a Java finalize metódusával!

Jelölése is mutatja, hogy mennyire hasonlít a konstruktorhoz, hiszen ez is az osztály nevével kell megegyezzen, csupán a tilde (~) jel előzi meg azt. A konstruktorokkal ellentétben a destruktornak nem lehetnek paraméterei, és csak egy darab lehet belőle egy osztályon belül.

Példa destruktorra és az automatikus lefutásra:

 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
#include<iostream>

using namespace std;

class Magic {
  int lucky_number;
public:
  Magic(int num) : lucky_number(num) {
  }

  //Destruktor:
  ~Magic() {
    cout << "Your lucky number was: " << lucky_number << endl;
  }

  int get_lucky_number() const {
    return lucky_number;
  }
};

int main() {
  Magic m(5);
  cout << m.get_lucky_number() << endl;
  cout << "Ez az utolso kiiratas" << endl;
}

A fenti példában látszólag az utolsó kiíratás után semmi sem szerepel, mégis a programot lefuttatva az utolsó kimenet a

Your lucky number was: 5

Ez azért történik, mert az m nevű objektum a main végén szűnik meg, így még a destruktorában lévő kódrészlet (kiíratás) lefut.

Az ilyen működés nagyon hasznos lehet, mivel gyakran használunk olyan objektumokat, melyek valamilyen erőforrást (dinamikus memória, hálózati kapcsolat, fájl, stb.) használnak. Ezeknek a felszabadításáról gondoskodnunk kell. C-ben jól ismert példa lehet egy fájl olvasása:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>

int main() {
  FILE *fptr;
  fptr = fopen("file","r");
  if (fptr == NULL) {
    printf("Error!");
    return 1;
  }
  //olvassunk adatokat a fájlból
  fclose(fptr);
  return 0;
}

Azt leszámítva, hogy a hibakezelés másként működik, fontos kiemelni, hogy a fájlt a használat végén fel kell szabadítanunk. Igaz, kisebb példánál nem jelentős, azonban ha az utolsó használat után még hosszú ideig futhat a program nem mindegy, hogy a fájlt felszabadítottuk vagy továbbra is foglaljuk erőforrásként. Ez a felelősség a programozóra hárul, hogy ne felejtse el meghívni az fclose metódust.

Ha egy fájlt kezelő osztályt képzelünk el C++-ban, akkor annak a destruktorában megtehetjük, hogy minden műveletet helyesen lezárunk a fájlon és azt felszabadítjuk. Ekkor a programozónak nem kell ezzel törődnie máshol, biztosan nem marad el, és csakis akkor történik meg, mikor a fájl már nem használható, hiszen az objektum megszűnésekor fut le a destruktor.

Dinamikus memóriakezelés

Dinamikus memóriakezelésére a heap memórián van lehetőségünk. Ez a stackkel szemben sokkal nagyobb méretű, igényelni kell a memóriát, lassabb és nem automatikus kezelésű. Ahhoz, hogy ezt a memória területet elérjük, pointer(ek)re van szükségünk. Memória foglalás során ennek a pointernek adunk értéket, mely a kapott (számunkra lefoglalt) memória kezdőcímére mutat. Ez a visszaadott érték void* típusú, mely minden pointer-típusra konvertálható.

C-ben ezt a malloc, calloc, realloc függvényekkel tehetjük meg. C++-ban ezek helyett használhatjuk a new operátort. Ez is void* típussal tér vissza sikeres memóriafoglalás esetén. Hiba esetén std::bad_alloc kivétel dobódik (a kivételekről később lesz szó, de a koncepció megegyezik a Javaban már látottakkal). Használata: T *var = new T;, ahol T egy típus.

Példa C++ dinamikus memória foglalásra, egy primitív típus esetén.

1
2
3
4
5
6
7
8
#include<iostream>
using namespace std;

int main() {
  int *p = new int;
  cin >> *p;
  cout << "A " << p << " a címen lévő érték: " << *p << endl;
}

A példában a használt new operátor kivételt is dobhat, ha sikertelen a memória allokáció, pl. nincs elegendő memória. Ekkor a kivételt vagy elkapjuk, vagy hagyjuk tovább dobódni. Ha nem szeretnénk a kivételkezeléssel foglalkozni, akkor használhatjuk a new exception-mentes változatát. Ekkor a hibát nem kivétel dobással jelzi felénk a new, hanem a visszatérési érték egy üres pointer (null pointer) lesz (mint pl. a C-beli malloc). A new kivétel mentes változata: T *var = new (nothrow) T, ahol T egy típus.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include<iostream>
using namespace std;

int main() {
  int *p = new (nothrow) int;
  if (p == nullptr) { // vagy lehetne if (!p), ami null pointer esetén igazra értékelődne ki
    cerr << "Nem sikerült memóriát foglalni." << endl;
    return 1;
  }
  cin >> *p;
  cout << "A " << p << " a címen lévő érték: " << *p << endl;
}

A fenti példában nem kell foglalkoznunk az esetleges kivételek kezelésével, azonban ha nem vizsgáljuk meg a visszatérési értéket, null pointer dereferálásba futhatunk!

A new operátort használhatjuk egy-egy elem foglalására a heap-en, azonban legtöbbször több elemet szeretnénk lefoglalni. Ezt is megtehetjük a new[] operátorral. Használata: T *var = new T[N] , ahol N egy nemnegatív egész szám, T egy típus. Példa new[] operátorra:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include<iostream>
using namespace std;

int main() {
  int *array;
  try {
    array = new[1000000000];
  } catch (bad_alloc& error) {
    cerr << "Nem sikerult a memoria foglalas" << endl;
    return 1;  
  }
  cin >> array[0] >> array[1000000000-1];
  cout << array[0] << " " << array[1000000000-1] << endl;
}

A példában megpróbálunk memóriát foglalni 1000000000 méretű int tömbnek. Ha ennyit nem tud biztosítani a rendszer, bad_alloc kivételt kapunk. Ha sikerül, akkor használhatjuk indexelve mint egy szokásos tömböt. Természetesen a try-catch kiírása nem kötelező, de kivétel esetén nem tudjuk azt lekezelni. Ha nem szeretnénk kivételt kapni, itt is alkalmazható a nothrow változat.

A dinamikusan foglalt memória nem szabadul fel automatikusan, erről nekünk kell gondoskodnunk! Ha nem tesszük meg, memory-leak történhet. A lefoglalt memóriát a delete és a delete[] operátorral szabadíthatjuk fel. A delete a new által foglalt memóriát a delete[] a new[] által foglalt memóriát szabadítja fel. Ha megpróbálnánk new[] által foglalt tömböt a delete operátorral felszabadítani, a viselkedés definiálatlan (de általában lefut a program).

Példa memória felszabadításra:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include<iostream>

using namespace std;

int main() {
  int* array = new int[10];
  array[0] = 1;
  // Feltöltjük és használjuk a tömböt

  int* p = new int;
  // Beállítjuk a p-t és használjuk

  // Nincs szükségunk már a pointerekre, ezért a memóriát visszaszolgáltatjuk

  delete p; // new-val foglalt
  delete[] array; //new[]-bel foglalt (a méretet nem kell megadni)
}

Dinamikus memóriakezelés összefoglaló

Lefoglalt cella operátor hibakezelés felszabadítás
Egy cella new try-catch delete
Több cella new[] try-catch delete[]
Egy cella new (nothrow) visszatérési érték. if(pointer) delete
Több cella new (nothrow) visszatérési érték. if(pointer) delete[]

Osztályok dinamikus adattagjai

Sokszor a dinamikus memóriakezelés nem egy-egy függvényhez köthető, hanem az osztályok az általuk reprezentált entitások tárolásához is dinamikusan foglalnak memóriát. Ekkor a memória foglalást használhatjuk ugyan úgy, mintha csak lokális változónak adnánk értéket, akár inicializáló listában is.

Az előző anyagban a Kurzus osztálynak volt egy Hallgato tömbje, mely fix méretű volt. Ebben az esetben nem tudtuk volna valóban növelni a kurzus létszámát, erre megoldás a dinamikus memóriakezelés. Kurzus osztály dinamikus Hallgato tömbbel:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Kurzus {
  string nev, kod;
  unsigned felvette = 0;
  // Hallgato hallgatok[10]; Ez volt korábban, de ennek fix a mérete
  Hallgato* hallgatok;
public:
  Kurzus(const string& nev, const string& kod) :
          nev(nev),
          kod(kod),
          hallgatok(new Hallgato[10]) // dinamikus memória foglalás 10 Hallgato-nak -> hallgatok = new Hallgato[10];
  {
  }
};

Destruktor ismét, avagy hogyan kezeljük a lefoglalt memóriát

Mivel a dinamikus memória nem szabadul fel automatikusan, nekünk kell ezt megtennünk. Azonban ezt objektumok esetében nehéz megmondanunk, hogy mikor tegyük meg. Bármikor kellhet az objektumnak az adott memória terület. Írhatnánk egy metódust, melyet meghívva felszabadítjuk a memóriát, azonban ekkor nincsen garancia, hogy utána nem használják az objektumot vagy nem felejti el a programozó.

A most bevezetett destruktor azonban pont egy olyan metódus, mely lefutása csakis az objektum életének végén megy végbe és utána biztosan nem használhatjuk az objektumot. A destruktorban történő memória felszabadítás megfelelő megoldás. Ezt olyan szinten vehetjük megoldásnak, hogy ha dinamikusan foglalunk memóriát az objektumunkban, akkor azt legkésőbb a destruktorban fel kell szabadítani (kivétel, ha egy másik objektum "felel" azért). Minden new-hoz köthető egy delete.

Példa destruktorban történő felszabadításra:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Kurzus {
  string nev, kod;
  unsigned felvette = 0;
  // Hallgato hallgatok[10]; Ez volt korábban, de ennek fix a mérete
  Hallgato* hallgatok;
public:
  Kurzus(const string& nev, const string& kod) :
          nev(nev),
          kod(kod),
          hallgatok(new Hallgato[10])  //dinamikus memória foglalás 10 Hallgato-nak -> hallgatok = new Hallgato[10];
  {
  }

  ~Kurzus() {
      delete[] hallgatok;  //a lefoglalt eroforras felszabaditasa. delete[] mivel new[] volt.
  }
};

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
29
30
#include<iostream>
using namespace std;

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

  CopyCon(const CopyCon& cc) : value(cc.value), heap_int(new int) {
    *heap_int = *(cc.heap_int); // az érték másolása új az új memória területre
  }

  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 úja 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
class AssignOp {
  int *ip;
 public:
  AssignOp(int n) : ip(new int) {
    *ip = n;
  }

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

  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. Lásd Kurzus teljes példa). 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
11
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) azokat.

Dinamikusan foglalt memóriával rendelkező osztályra példa a Kurzus, ahol a konstruktor, a másoló konstruktor (copy-constructor), értékadó operátor (assingment operator) és a destruktor is meg van valósítva. Sőt, az operátorok is helyesen kezelik a változásokat (azaz amikor a tömb méretét változtatni kell).

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
  }
};

Kurzus dinamikus memóriakezelés teljes példa

Az eddigi ismeretek alapján a dinamikus memóriakezeléshez köthető elemek a Kurzus osztályon a következőképp használatosak:

 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
class Kurzus {
  string nev, kod;
  unsigned felvette = 0, kapacitas;
  Hallgato *hallgatok;
 public:
  // dinamikusan foglal memoriat a kurzus hallgatoinak tarolasahoz
  Kurzus(const string& nev, const string& kod, unsigned kapacitas) :
    nev(nev), kod(kod), kapacitas(kapacitas), hallgatok(new Hallgato[kapacitas]) {
  }

  // destruktor, a dinamikusan foglalt memoriat szabaditja fel
  ~Kurzus() {
    delete[] hallgatok;
  }

  // masolo konstruktor
  Kurzus(const Kurzus& k) :
    nev(k.nev), kod(k.kod), felvette(k.felvette),
    kapacitas(k.kapacitas), hallgatok(new Hallgato[kapacitas])
  {
    // nemcsak a tarolot kell lemasolni, annak elemeit is
    for (unsigned i = 0; i < felvette; i++) // az egyes ertekek atmasolasa egyenkent.
      hallgatok[i] = k.hallgatok[i];
  }

  // ertekado operator
  Kurzus& operator=(const Kurzus& k) {
    if (this == &k)   // Kurzus k("pelda", "123");
      return *this;   // k = k; miatt kell!!!

    delete[] hallgatok;
    nev = k.nev;
    kod = k.kod;
    felvette = k.felvette;
    kapacitas = k.kapacitas;
    hallgatok = new Hallgato[kapacitas];
    for (unsigned i = 0; i < felvette; i++)
      hallgatok[i] = k.hallgatok[i];
    return *this;
  }

  // ++k (pre inc.)
  Kurzus& operator++() {
    ++kapacitas;
    // uj memoria foglalas, hiszen tobb memoriara van szuksegunk
    Hallgato* temp = new Hallgato[kapacitas];
    for (unsigned i = 0; i < felvette; i++)
      temp[i] = hallgatok[i];
    delete[] hallgatok;
    hallgatok = temp;
    return *this;
  }

  // k++ (post inc.)
  Kurzus operator++(int) {
    Kurzus ret = *this;
    operator++();
    return ret;
  }
};

Feladatok

  1. Gyakorló feladatsor

    • Írj egy Virag osztály! A virág egyetlen attribútuma a színe, ezt tárolja egy sztring. Írj gettert és settert is! Írj paraméteres konstruktort az osztályhoz, mely kiírja a standard outputra, hogy "X szinu virag letrehozva".

    • Írd meg a következő metódusokat is a kettőspont után szereplő kiíratásokkal:

      • destruktor: "X szinu virag torolve"
      • copy konstruktor: "X szinu virag masolva copy konstruktorral"
      • assignment operator: "X szinu virag masolva ertekadas operatorral"
    • Példányosíts le egy virágot a main-ben és figyeld meg a kiírtásokat!

      • Hozz létre egy új virágot copy konstruktorral.
      • Írj felül egy virágot az assignment (értékadás) operátor segítségével.
      • Írj egy függvényt, ami érték szerinti paraméterként vár egy virágot.
      • Írd át ezt a függvényt úgy, hogy referenciát várjon paraméterül.
      • Írj egy metódust, ami érték szerinti Virággal tér vissza. Nézd meg, történik-e másolás. (Magyarázat)
      Részmegoldás
    • Írj egy Kert osztályt, ami tetszőleges számú virágot tartalmaz egy dinamikus tömbben. Írj neki egy konstruktort, ami egy unsigned értéket, a leendő virágok számát várja.

      • Példányosítsd le a kertet a main-ben és nézd meg a standard outputot!
      • Valósítsd meg a destruktort, hogy a hiba javítva legyen!
      • Valósítsd meg a += operátort, ami egy virágot vár paraméterül és a Kert első szabad helyéhez adja. Ha nincs már hely a tömbben ne csináljon semmit.
      • Valósítsd meg a (nem konstans) indexer operátort az i-edik virág lekérdezésére. Ha az index rossz legyen a 0-ás tömbelem visszaadva.
      • Készíts egy void kiir() metódust, amely sorban egymás után kiírja a kertben aktuálisan szereplő virágok színét.
    • Próbáld ki a kert generált copy konstruktorát, assignment operátorát!

      • Változtasd meg az egyik virág színét a lemásolt kertben. Milyen hatása lesz ennek az eredeti kertre? Hívd meg mindkettő kiir metódusát!
      • Írj egy függvényt, ami érték szerinti kert paramétert vár. Próbáld ki, mi történik, ha ez meg van hívva.
      • Valósítsd meg helyesen (deep copy-val) a kert copy konstruktorát és értékadás operátorát!
      Részmegoldás
    Teljes megoldás

Utolsó frissítés: 2022-09-15 09:40:31