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ő.
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];
}
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);
Í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.
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: