Nem-OO nyelvi elemek, memóriakezelés

A mai óra célja a fontosabb nem-OO nyelvi elemek és a C-C++ memóriakezelési sajátosságainak átismétlése.

Felkészülés a gyakorlatra

1. Verem: mi a hiba?

Alább egy C-ben megírt verem adatszerkezet. A verem_init() függvény inicializálja az addig memóriaszemetet tartalmazó struktúrát. A verem_berak() függvény betesz egy számot a verembe. Ha kell, átméretezi a dinamikus memóriaterületet. A verem_free() egy feleslegessé vált verem dinamikus területét szabadítja fel.

A program memóriakezelési hibát tartalmaz. Hol? Hogyan lehet javítani?

struct Verem 
{
    double *adat;   /* dinamikus */
    int db;         /* értékes adatok száma */
    int kapacitas;  /* foglalt terület mérete (db <= kapacitas) */
};

int main() 
{
    Verem v1, v2;
    verem_init(&v1);
    verem_init(&v2);
    verem_berak(&v2, 5.1);
    v1 = v2;
    verem_free(&v1);
    verem_free(&v2);
}
Megoldás

A fenti kódban az elvi hiba a v1=v2 értékadásnál van. Itt ugyanis minden egyes adattag átmásolódik v1-be v2-ből; ez a darabnál például nem lenne gond, a pointernél viszont nagyon is az. Ugyanis innentől kezdve v1 és v2 pointere ugyanoda mutat, és a vermek nem működnek majd helyesen. Például az egyik verembe berakott adat a másikban is lehet, hogy meg fog jelenni. A v1.adat pointer ilyen módon történő felülírása memóriaszivárgást is okoz.

A struktúrák közötti értékadás maga egyébként működik, lefordul, már a sima C is ismeri! Nem az a hiba, hogy ilyen nincs, hanem amit csinál, az nem jó nekünk itt. (Gyakorlatilag a struktúra értékadásával megsértjük azt a szabályt, hogy nem nyúlunk a struktúra belsejébe - ugyanis kívülről nem tudhatjuk, hogy le szabad-e egyesével másolni az adattagjait. Ezt csak az tudja, aki a struktúrát kezelő függvényeket megírja.) Írni egy másolós függvényt, amelyik egyesével átmásolja a struktúra adattagjait, az ugyanúgy rossz, mintha egy sima értékadással lerendezzük, mert akkor a memóriaszivárgás és a rossz pointerek ugyanúgy meglesznek. Ezen kívül, az egymásra mutató pointerek mindenképpen rosszak, nem csak akkor, ha a két verem darabszáma vagy kapacitása épp eltérő.

dintomb

A memória felszabadítása lesz az, ahol a program lefagy; egészen pontosan v2 felszabadítása. v1.adat és v2.adat ugyanoda mutat, ezért a felszabadító függvény kétszer próbálja meg majd azt a memóriaterületet felszabadítani (free() vagy delete[], attól függ, hogyan vannak megírva).

Javítani úgy lehet, ha az értékadás helyett másoló függvényt írunk, és a v1=v2 értékadást egy verem_masol(&v1, &v2); sorra cseréljük.

void verem_masol(Verem *cel, const Verem *forras) 
{
    /* A cél veremből természetesen minden adat eltűnik, felülírjuk. */
    delete[] cel->adat;

    /* ez egyértelmű */
    cel->db = forras->db;
    /* igazából ez is; legegyszerűbb leutánozni a másik vermet, és
       akkor itt nem kell azzal foglalkozni, hogy mi a foglalási
       stratégia. a másik úgyis aszerint jött létre. */
    cel->kapacitas = forras->kapacitas;

    /* forrástól független memória foglalása */
    cel->adat = new double[cel->kapacitas];
    /* másolás */
    for (int i=0; i<cel->db; ++i)
        cel->adat[i]=forras->adat[i];
}

2. Verem: másolás vagy értékadás?

Miben különbözik az alábbi kódrészlet a fentitől? Miért hibás?

Verem v1;
verem_init(&v1);
Verem v3 = v1;

verem_free(&v1);
verem_free(&v3);
Megoldás

A hiba ugyanaz lesz, mint a fenti esetben: a v1.adat és a v3.adat ugyanarra a memóriaterületre mutat, így kétszer szabadítjuk fel. Az a különbség, hogy a hibás sorban v3 még nincs inicializálva, így a fenti verem_masol függvényt sem használhatjuk, mert az felszabadítaná a v3 dinamikus memóriaterületét, ami még nincs is.

Igazából ebben a sorban másolnunk kell a v1 tartalmát, de úgy, hogy a v3 tartalmát inicializálatlannak tekintjük. Az előző feladat verem_masol függvényéből ki kell vennünk a delete[]-et. A v1 = v2 stílusú értékadásra pedig érdemes új függvényt írni, ami először felszabadítja a régi verem tartalmát, aztán elvégzi a másolást.

void verem_masol(Verem *cel, const Verem *forras) 
{
    /* itt most nincs delete[] */

    cel->db = forras->db;
    cel->kapacitas = forras->kapacitas;
    cel->adat = new double[cel->kapacitas];
    for (int i=0; i<cel->db; ++i)
        cel->adat[i]=forras->adat[i];
}

void verem_ertekad(Verem *cel, const Verem *forras)
{
    verem_free(cel); // <- ez itt a lényeg!
    verem_masol(cel, forras);
}

Verem v3;
verem_init(&v3);
verem_ertekad(&v3, &v1);

3. Verem: új nyelvi elemek

Írjuk át az eddigi függvényeinket és a főprogramot a referencia és const használatára!

4. Verem: erőforráskezelés

Írjunk meg minden függvényt, ami szerepelt az eddigi kódjainkban! Mi a különbség a kapacitás és a méret között? Hogyan érdemes növelni a kapacitást? Mennyivel lesz ezáltal gyorsabb a verem_berak?

Megoldás

A kapacitás a lefoglalt elemek számát tárolja, a méret pedig az abból elhasznált elemek számát. Ha új elemet teszünk be, nem feltétlenül kell újrafoglalnunk a dinamikus tömböt, csak akkor kell, ha a kapacitás betelt. Ilyenkor az új memóriaterületet érdemes az eddigi valahányszorosának választani, nálunk az egyszerűség kedvéért legyen a kétszerese.

A C++ beépített típusai ennél okosabban csinálják, mert ismerik a memóriamenedzserek (malloc és new implementációk) sajátosságait, és az operációs rendszer virtuális memóriát kezelő trükkjeit. Így 2n-16 bájt, vagy 1.5n-16 bájt hatékonyabb lehet, és 4 kiB-nál nagyobbal nem érdemes növelni a kapacitást.

5. Verem: kívánságműsor

Mit szeretnénk látni a függvényhívások helyén, hogy lenne a legkényelmesebb leírni ezeket?

Megoldás
// Még nem tudunk ilyet csinálni, majd 3 hét múlva menni fog ez is.
void pelda()
{
    Verem v1, v2;           // ...automatikusan lefutna egy inicializálás
    verem_berak(v2, 5.1);
    v1 = v2;                // ...az értékadás tényleg értékadásként működjön
    Verem v3 = v1;          // ...lehessen rendesen másolással inicializálni

    // std::cout << v3 << std::endl;   // ...lehessen rendesen kiírni

    // ...és ne kelljen kiírni a felszabadítást, legyen automatikus.
}

Az eddigi tudásunk szerinti működő megoldás:

verem.cpp letöltése