4. hét: Objektumok memóriakezelése

Gera Dóra · 2019.02.27.

Heti kiegészítő feladatok

Felkészülés

1. Verem osztály

A mai feladat egy ehhez hasonló Verem osztályt írni, ami tetszőlegesen sok egész számot tud tárolni. Két fő művelete van: egy elemet bele lehet rakni, és mindig a legutolsót lehet elérni (LIFO)

A belső adatszerkezetre – a konzultáción elhangzottól eltérően – most egyszeresen láncolt listát fogunk használni, és elrejtjük a dinamikus memóriakezelést az objektum belsejében. Rendelkezésre áll a Prog1-ből ismerős lista típus:

struct ListaElem {
    int adat;
    ListaElem *kov;
};

Hozd létre ennek segítségével a Verem osztályt! Milyen adattagja(i) lesz(nek)? Írd meg a default konstruktorát (paraméter nélküli), ami egy üres listát hoz létre!

Tipp

Elég egyetlen ListaElem* típusú adattag, amit a default konstruktor NULL-ra állít.

Megoldás
class Verem {
    ListaElem* eleje;
public:
    Verem(): eleje(NULL) {}
};

2. Tagfüggvények

Írd meg a következő tagfüggvényeket:

  • berak: a paraméterében kapott számot berakja a verembe a listába való befűzéssel
  • kivesz: visszatér a legutoljára belerakott inttel, és kiveszi a veremből, azaz kifűzi a listából
  • meret: visszaadja, hogy milyen nagy a verem (hány elem van a listában)
  • ures: megmondja, hogy üres-e a verem

Ha az új adatot mindig a lista elejére teszed, akkor nem kell mindig végigiterálni az egész listán, és kitörölni is a legelső elemet kell. A memóriafoglalásnál és a felszabadításnál használj new és delete hívásokat!

3. Kiírás

Írj egy olyan globális operátort, amivel a Verem kiíratható lesz egy std::cout-ra!

Verem v1;
// v1 feltöltése intekkel
std::cout << v1 << std::endl; 
Tipp

A globális függvény nem láthatja az osztály privát adattagjait, de hívhatja publikus tagfüggvényeit. Az osztálynak legyen egy kiir(std::ostream&) függvénye, amit az operator<< meghívhat.

Megoldás
void Verem::kiir(std::ostream &os) const {
    ListaElem *p;
    for (p = eleje; p != NULL; p = p->kov)
        os << p->adat << " ";
}

std::ostream& operator<<(std::ostream &os, Verem const& v) {
    v.kiir(os);
    return os;
}

4. Destruktor

Mivel az új számok berakásakor dinamikusan foglalunk memóriát, ezért ezt valamikor fel is kell szabadítani. Írd meg a destruktort, hogy ne legyen memóriaszivárgás!

Megoldás
Verem::~Verem() {
    while (eleje != NULL) {
        ListaElem *tmp = eleje->kov;
        delete eleje;
        eleje = tmp;
    }
}

5. Másoló konstruktor

Mit csinálna a következő sor? Helyesen működik?

Verem v1;
// v1 feltöltése intekkel
Verem v2 = v1; 

A fordító által generált másoló konstruktor most csak az egyetlen pointer adattagunkat másolná le, és a két Verem objektum ugyanazokra a listaelemekre mutatna:

copy1

Nekünk viszont arra van szükségünk, hogy minden veremnek saját listája legyen:

copy2

Ehhez le kell másolni az egész listát. Írd meg a másoló konstruktort! Segítségképp egy előre megírt listamásoló függvény C-ben:

ListaElem *lista_masol(ListaElem *eleje)
{
    ListaElem *ujeleje = NULL;
    ListaElem *ujvege = NULL;
    for (ListaElem *iter = eleje; iter != NULL; iter = iter->kov)
    {
        ListaElem *uj = (ListaElem*)malloc(sizeof(ListaElem));
        uj->adat = iter->adat;
        uj->kov = NULL;
        if (ujvege != NULL)
            ujvege ->kov = uj;
        ujvege = uj;
        if (ujeleje == NULL)
            ujeleje = uj;
    }
    return ujeleje;
}

A memóriafoglalást írd át C++-osra!

Megoldás
Verem::Verem(Verem const& masik) {
    eleje = lista_masol(masik.eleje);
}

6. Értékadó operátor

Ha nekünk kell megírni egy osztálynak a másoló konstruktorát és destruktorát, akkor valószínűleg az értékadó operátor is kelleni fog.

Írd meg az értékadó operátort! Mi a függvény visszatérési értéke? Legyen láncolható, és figyelj az önértékadásra is!

Megoldás
Verem& Verem::operator=(Verem const& masik) {
    if (this != &masik) {
        while (eleje != NULL) {
            ListaElem *tmp = eleje->kov;
            delete eleje;
            eleje = tmp;
        }
        eleje = lista_masol(masik.eleje);
    }
    return *this;
}

7. Kódduplikáció

Itt az alábbi kódrészlet:

ListaElem *uj = new ListaElem();
uj->adat = iter->adat;
uj->kov = NULL;

Meg lehet-e valósítani, hogy egy sorban történjen a memóriafoglalás és az új objektum adattagjainak inicializálása? Mivel kell kiegészíteni a ListaElem osztályt?

Tipp

A new ListaElem() utasítás meghívja a ListaElem default konstruktorát, amit a fordító írt nekünk. Írj egy két paraméteres konstruktort a ListaElem-nek, majd javítsd ki a new hívásokat a kódodban!

Megoldás
struct ListaElem {
    int adat;
    ListaElem *kov;

    ListaElem(int adat, ListaElem *kov): adat(adat), kov(kov) {}
};

ListaElem *uj = new ListaElem(iter->adat, NULL);

Nézd át a másoló konstruktor, destruktor és az értékadó operátor tartalmát! Ha van benne kódduplikáció, írj egy lista_masol és egy lista_felszabadit függvényt, hogy megszüntesd!

Megoldás
void lista_felszabadit(ListaElem* eleje) {
    while (eleje != NULL) {
        ListaElem *tmp = eleje->kov;
        delete eleje;
        eleje = tmp;
    }
}

Verem::~Verem() {
    lista_felszabadit(eleje);
}

Verem& Verem::operator=(Verem const& masik) {
    if (this != &masik) {
        lista_felszabadit(eleje);
        eleje = lista_masol(masik.eleje);
    }
    return *this;
}
Tipp

A két függvényt csak a Verem osztály tagfüggvényei használják, ezért felesleges hogy globálisak legyenek, tedd ezeket a Verem private, static függvényeivé!

A ListaElem most egy olyan osztály, amit nem akarunk a Verem nélkül használni, és emiatt nem kell láthatónak lennie a főprogramból. Elég, ha az osztály adattagjait és függvényeit csak a Verem ismeri, ezért az egész típus definícióját beemelhetjük a Verem osztályon belülre. Legyen a ListaElem osztály privát, és figyelj oda a sorrendre: nem vehetsz fel egy ListaElem* típusú adattagot előbb, mint az osztály deklarációja.

Amikor egy osztályon belül egy másik osztályt definiálunk, azt nested class-nak hívjuk, azaz belső osztálynak, erről még lesz szó a félév során, de előre is olvashatsz a témában a 6. fejezetben.

Megoldás