Informatika2-2015/Eloadas 12 C-5 Memoria kezeles
a (Informatika2-2015/Eloadas 11 C-5 Memoria kezeles lapot átneveztem Informatika2-2015/Eloadas 12 C-5 Memoria kezeles névre) |
|||
29. sor: | 29. sor: | ||
Mivel a python egy olyan nyelv, ami automatikus memóriakezelést tartalmaz, ezért ott a felhasználónak nem kell törődnie ezzel a problémával, a python interpreter biztosít minket hogy minden amire már nincs szükség az idővel megsemmisül, és a soha nem fog a program túl sok fölösleges memóriát használni. Majd az előadás későbbi részében visszatérünk rá, hogy ezt pontosan hogyan csinálja. | Mivel a python egy olyan nyelv, ami automatikus memóriakezelést tartalmaz, ezért ott a felhasználónak nem kell törődnie ezzel a problémával, a python interpreter biztosít minket hogy minden amire már nincs szükség az idővel megsemmisül, és a soha nem fog a program túl sok fölösleges memóriát használni. Majd az előadás későbbi részében visszatérünk rá, hogy ezt pontosan hogyan csinálja. | ||
− | Azonban a példa mutatja, hogy a program számára a memória is egy ''erőforrás'', amit kezelni kell. Amikor létrehozok egy objektumot vagy bármilyen változót, akkor annak '''lefoglalok/allokálok''' valamennyi memóriát. Ezt a részt a memóriában '''fel kell szabadítani''' mielőtt lefoglalhatom valamilyen más célra. | + | Azonban a példa mutatja, hogy a program számára a memória is egy ''erőforrás'', amit kezelni kell. Amikor létrehozok egy objektumot vagy bármilyen változót, akkor annak '''lefoglalok/allokálok''' valamennyi memóriát. Ezt a részt a memóriában '''fel kell szabadítani''' mielőtt lefoglalhatom valamilyen más célra. A pythonban ez az egész folyamat automatikus, nem kell törődni vele nagyon, azonban a C-ben ennél kicsit bonyolultabb a helyzet. |
== C memóriakezelés == | == C memóriakezelés == | ||
45. sor: | 45. sor: | ||
[[Fájl:Call stack layout.png]] | [[Fájl:Call stack layout.png]] | ||
− | Ahhoz hogy ez így megtehető legyen, | + | Ahhoz hogy ez így megtehető legyen, az is kell egyébként, hogy a C-ben minden típusnak fix a mérete. Ezért, miután lefoglaltam egy memóriablokkot a DrawSquare-nek, a tetejére tehetek egy másik memóriablokkot, és nem kell aggódnom amiatt, hogy hirtelen nagyobb hely fog kelleni valamelyik változónak a DrawSquare-ben. (Ezért van az is, hogy a tömbök is fix méretűek ellenben a python-os listával, és ilyen kevés fajta típus van.) |
+ | |||
+ | És így az is látható, hogy mi történik akkor, ha használunk egy mutatót, ami olyan változóra mutat, ami már megsemmisült, aminek már vége a blokkjának: ha kezdődött egy másik blokk azóta, akkor az könnyen lehet hogy ugyanazt a helyet elfoglalja a memóriában amire mutat a mutató, és akkor valami olyasmit módosítunk a mutatón keresztül amit nem akarnánk. | ||
+ | |||
+ | === C kupac === | ||
+ | |||
+ | Azonban vannak olyan adatok, amiknek az élettartama nem ilyen szépen struktúrált. Egy korábbi példát nézve, lehet hogy egy függvény egy tömbben akar visszaadni adatokat, de a tömb méretéről csak azt tudjuk a program írásakor, hogy valahol 2 és 100000 között lesz. Az ilyen függvénynél nem jó mindig egy 100000-es tömböt használni, főleg ha a függvény sokszor lefuthat a program futása folyamán. Használható az ilyen esetekben a manuális memóriakezelés. | ||
+ | |||
+ | A C-ben a memóriának a másik fontos része, a ''verem''en kívül, a '''kupac'''. A kupacból a programozó tud kérni memóriát egy speciális C függvénnyel, viszont az így elkért memóriát szintén a programozónak kell felszabadítania, egy másik függvénnyel. A memória kérésére használt speciális függvény a [http://en.cppreference.com/w/c/memory/malloc malloc()], a felszabadításra használt függvény a [http://en.cppreference.com/w/c/memory/free free()]. Mielőtt jobban kifejtem a működésüket, nézzünk először egy egyszerű példát: | ||
+ | |||
+ | <c> | ||
+ | int i; | ||
+ | int *tomb = malloc(10 * sizeof(int)); | ||
+ | |||
+ | tomb[0] = 0; | ||
+ | for(i = 1; i < 10; ++i) { | ||
+ | tomb[i] = tomb[i-1] + i; | ||
+ | } | ||
+ | |||
+ | free(tomb); | ||
+ | tomb = NULL; | ||
+ | </c> | ||
+ | |||
+ | Tehát amit itt látunk, hogy a ''malloc()''-kal kérek egy 10 méretű tömbnyi memóriát, majd azt a memóriát használom 10 méretű tömbként, aztán felszabadítom a memóriát. A ''malloc()'' függvény paramétere az, hogy mennyi memóriát kérek, byte-ban mérve. Van egy spciális operátor a C-ben, a [http://en.cppreference.com/w/c/language/sizeof sizeof()], ami arra való, hogy megmondja, hogy egy alaptípus hány byte-ot foglal el. Tehát, a fenti példában, az ''int'' tipikusan modern rendszereken 4 byte méretű (ami 32 bit, ezért van hogy -2^31 és 2^31 közötti számokat tud tárolni), 40 byte memóriát foglalunk le a ''malloc()''-kal. (De azért jó a ''sizeof()''-ot használni, és nem csak odaírni hogy 40, mert ki tudja hogy valamikor használni akarják-e a programunkat nem tipikus rendszeren. Plusz, akkor nem kell megjegyeznünk hogy az ''int'' 4 byte-os.) | ||
+ | |||
+ | Aztán, miután tudom hogy már nem akarom többé használni ezt a tömböt, felszabadítom a számára lefoglalt memóriát a ''free()''-vel. Jó szokás a memória felszabadítása után törölni (''NULL''-ra állítani) a mutatót, hogy nehogy próbáljuk a felszabadítás után is használni, hiszen addigra már lehet hogy valamilyen más célra van lefoglalva. | ||
+ | |||
+ | Tehát az eddigieket összefoglalva, a C-ben a ''malloc()'' és a ''free()'' használható manuális memóriakezelésre, ami azt jelenti hogy a memória lefoglalását és felszabadítását a programozó irányítja, nem hagyja a programnyelvre. | ||
+ | |||
+ | === python fájlkezelés hasonlat === | ||
+ | |||
+ | A python fájlkezelésnél tanultuk, hogy a legtöbb esetben ajánlott a fájl megnyitását a ''with'' kulcsszóval egybekötni, és akkor a fájl le lesz zárva a ''with'' blokk végén: | ||
+ | |||
+ | <python> | ||
+ | with open("adatok.txt", "w") as fajl: | ||
+ | muszer = Muszer() | ||
+ | |||
+ | fajl.write(muszer.azonosito + "\n") | ||
+ | </python> | ||
+ | |||
+ | De bizonyos esetekben a fájlt nem csak egy blokkon belül akarom használni, hanem pl. vissza akarok térni vele egy függvényből: | ||
+ | |||
+ | <python> | ||
+ | def iedik_fajl(n): | ||
+ | fajlnev = "adatok_{}.txt" % n | ||
+ | fajl = open(fajlnev, "w") | ||
+ | return fajl | ||
+ | </python> | ||
+ | |||
+ | Ekkor ennek a függvény felhasználójának a felelőssége hogy bezárja a fájlt, mikor már nem használja többet, különben nem garantált a jó működés. Ehhez hasonló a ''verem'' és a ''kupac'' különbsége a C memóriakezelésnél: | ||
+ | * A legtöbb esetben megfelelő hogy egy blokkon belül érhető el csak a változó, ekkor egyszerűen definiálok egy változót, és az a veremben lesz tárolva. | ||
+ | * Bizonyos esetekben jobb ha nem vagyok a blokkokhoz kötve így, ekkor manuálisan is megoldhatom a memória kezelését a ''malloc()''-kal és ''free()''-vel. | ||
+ | |||
+ | A hasonlóságok nem véletlenek, a fájlok is úgy tekinthetőek mint egy erőforrás, amit a programoknak kezelniük kell, csak az még nehezebben automatizálható úgy hogy mindig jól működjön, és kevesebb problémát tud okozni a rossz kezelés, ezért annak a manuális kezelését a python-ban is engedik. | ||
+ | |||
+ | == C memóriakezelés példák == | ||
+ | |||
+ | Nézzünk néhány példát a memóriakezelésre. | ||
+ | |||
+ | === Tetszőleges méretű tömbbel visszatérés === | ||
+ | |||
+ | Korábban néztük, hogy amikor egy függvény egy tömbbel akar visszatérni, akkor egy lehetséges megoldás, hogy paraméterként megkapja a tömböt amibe beírja az adatokat. Egy másik lehetséges megoldás, hogy a függvény manuálisan lefoglal megfelelő mennyiségű memóriát a tömbnek. Ekkor a függvény visszatér a mutatóval erre a tömbre, és a függvény meghívójának felelőssége azt a memóriát felszabadítani: | ||
+ | |||
+ | <c> | ||
+ | // A visszateresi erteket fel kell szabaditani! | ||
+ | int *beolvasott_range(int *n) { | ||
+ | int i; | ||
+ | scanf("%d", n); | ||
+ | int *tomb = malloc(*n * sizeof(int)); | ||
+ | for(i = 0; i < *n; ++i) { | ||
+ | tomb[i] = i; | ||
+ | } | ||
+ | return tomb; | ||
+ | } | ||
+ | |||
+ | int main() | ||
+ | { | ||
+ | int N, i; | ||
+ | int *mostani_range; | ||
+ | mostani_range = beolvasott_range(&N); | ||
+ | |||
+ | for(i = 0; i < N; ++i) { | ||
+ | printf("%d ", mostani_range[i]); | ||
+ | } | ||
+ | |||
+ | free(mostani_range); | ||
+ | return 0; | ||
+ | } | ||
+ | </c> | ||
+ | |||
+ | Itt láthatjuk, hogy a függvény a tömb leendő méretét a parancssorról olvassa be, tehát a függvény hívója nem tudhatta előre, hogy mekkora tömböt adjon oda feltöltésre. Az ilyen függvényeknél a dokumentáció mindig tartalmazza, hogy fel kell szabadítani a visszatérési értéket. | ||
+ | |||
+ | === Változó méretű tömb === | ||
+ | |||
+ | Ha egy tömbnek akarjuk tudni változtatni a méretét, az lényegében elérhető, ha teszünk köré egy megfelelő struktúrát, megfelelő függvényekkel: | ||
+ | |||
+ | <c> | ||
+ | struct ValtozoTomb { | ||
+ | int meret; | ||
+ | int *adat; | ||
+ | }; | ||
+ | |||
+ | void ValtozoTomb_atmeretez(struct ValtozoTomb* tomb, int uj_meret) { | ||
+ | if(uj_meret == 0) { | ||
+ | (*tomb).meret = 0; | ||
+ | free((*tomb).adat); | ||
+ | (*tomb).adat = NULL; | ||
+ | } else if((*tomb).meret != uj_meret){ | ||
+ | int *uj_adat = malloc(uj_meret * sizeof(int)); | ||
+ | int i; | ||
+ | // Atmasoljuk amit kell | ||
+ | if(uj_meret < (*tomb).meret) { | ||
+ | for(i = 0; i < uj_meret; ++i) { | ||
+ | uj_adat[i] = (*tomb).adat[i]; | ||
+ | } | ||
+ | } else { | ||
+ | for(i = 0; i < (*tomb).meret; ++i) { | ||
+ | uj_adat[i] = (*tomb).adat[i]; | ||
+ | } | ||
+ | for(i = (*tomb).meret; i < uj_meret; ++i) { | ||
+ | uj_adat[i] = 0; | ||
+ | } | ||
+ | } | ||
+ | //Felszabaditjuk a regi memoriat: | ||
+ | free((*tomb).adat); | ||
+ | //Beallitjuk az uj ertekeket | ||
+ | (*tomb).meret = uj_meret; | ||
+ | (*tomb).adat = uj_adat; | ||
+ | } | ||
+ | } | ||
+ | </c> | ||
+ | |||
+ | Ezt a változtatható méretű tömböt pl. így lehet használni: | ||
+ | |||
+ | <c> | ||
+ | int main() { | ||
+ | struct ValtozoTomb teszt_tomb = {0, NULL}; | ||
+ | |||
+ | ValtozoTomb_atmeretez(&teszt_tomb, 10); | ||
+ | teszt_tomb.adat[0] = 20; | ||
+ | |||
+ | ValtozoTomb_atmeretez(&teszt_tomb, 2); | ||
+ | printf("%d", teszt_tomb.adat[0]); | ||
+ | |||
+ | // Ez az adatok felszabaditasa: | ||
+ | ValtozoTomb_atmeretez(&teszt_tomb, 0); | ||
+ | |||
+ | return 0; | ||
+ | } | ||
+ | </c> | ||
+ | |||
+ | Figyeltem a kód megírása közben, hogy amikor átméretezem, akkor a régi adatot mindig felszabadítsam. Ennél a tömbnél a helyes használat átméretezni 0-ra amikor befejeztük a használatát, mert az felszabadítja a belső adattárolót. Az, hogy a ValtozoTomb struktúra megsemmisül a blokk végén, az nem jelenti alapból azt hogy a benne levő adattároló is felszabadul, csak ha direkt felszabadítjuk így. | ||
+ | |||
+ | === Láncolt lista? === | ||
+ | |||
+ | == Automatikus vs. manuális memóriakezelés == | ||
+ | |||
+ | === Hogy működik az automatikus memóriakezelés? === | ||
+ | |||
+ | A python interpreter ugyan felszabadítja idővel a fölösleges memóriát, de tényleg csak idővel, így a program folyamatosan több memóriát használ mint feltétlenül szükséges. (Plusz, eleve az automatikus memóriakezelés megvalósításához plusz memóriára van szükség az interpreterben, mielőtt a mi programunk akár elindult volna.) Aztán időnként lefut az interpreterben az a folyamat, amit '''szemét gyűjtés'''nek hívnak, amikor kidob mindent ami már nem kell, azonban ez a folyamat bizonytalan hogy mennyi ideig tart, és kb. bármikor lefuthat, így ha a programunknak azonnal kell tudnia reagálni, akkor megengedhetetlen hogy közben lefusson egy szemétgyűjtés, python-ban viszont ezt nem lehet megakadályozni. | ||
+ | |||
+ | === Összehasonlítás === | ||
+ | |||
+ | Mint láthatjuk a manuális memóriakezelés nem egyszerű, mindjárt nézünk néhány tipikus hibát is majd, amit könnyű vele elkövetni. Sokkal nehezebb használni, de mit nyerünk vele, miért használják mégis még mindig a C-t mint programozási nyelv? A válasz, mint a C-nél általában, az, hogy hatékonyságot. | ||
+ | |||
+ | Ezek mind olyan problémák, amik a legtöbb programozó számára nem számítanak. Ellenben a C problémáival, amivel mindenki szembesül aki használni próbálja. Ezért is mondtam az első C előadáson, hogy valószínűleg nem fog nektek a C kelleni a jövőben, csak a nagyon erőforrásigényes és/vagy sebesség igényes alkalmazásokhoz használják. De azért néhány tanulság levonható a most tanultakból, ami automatikus memóriakezelés esetén is érvényes. |
A lap 2015. április 29., 11:27-kori változata
Tartalomjegyzék |
Memória kezelés
A programozási nyelveknek alapvetően két fajta hozzáállása lehet a memória kezeléséhez, automatikus vagy manuális. A ma is elterjedt programozási nyelvek közül csak a C és a C++ az ami még manuális memóriakezelést használ. A mai előadáson megnézzük hogy ez pontosan mit jelent. Ez azért is tanulságos lesz, hogy jobban megértsük hogy működnek azok a nyelvek amik automatikus memória kezelést használnak.
Mit is értünk memória kezelés alatt
Először nézzünk egy python példát:
# Ez egy lista objektum: lista1 = [1, 2, 3] # Ez az objektum valahol letezik a memoriaban, # es a "lista1" valtozo hivatkozik ra. # Ha ezt csinalom akkor mar ket valtozo hivatkozik ra: lista2 = lista1 # Ha ezt, akkor megint csak egy: lista1 = [] # Ha ezt csinalom akkor mar egy valtozo sem hivatkozik ra: lista2 = [] # Akkor vajon letezik meg az a lista? # Elerni mar nem tudom egyik valtozon keresztul se.
A válasz a kód végén feltett kérdésre az, hogy nem tudjuk. Mivel nem elérhető az a lista sehogyan sem, ezért a program helyes futása szempontjából mindegy hogy még létezik-e az a lista, hiszen már soha nem tudjuk használni semmire. Azonban abból a szempontból fontos hogy létezik-e, hogy ha még létezik, akkor a memóriának az a része ahol ez a lista található, nem használható másra.
Mivel a python egy olyan nyelv, ami automatikus memóriakezelést tartalmaz, ezért ott a felhasználónak nem kell törődnie ezzel a problémával, a python interpreter biztosít minket hogy minden amire már nincs szükség az idővel megsemmisül, és a soha nem fog a program túl sok fölösleges memóriát használni. Majd az előadás későbbi részében visszatérünk rá, hogy ezt pontosan hogyan csinálja.
Azonban a példa mutatja, hogy a program számára a memória is egy erőforrás, amit kezelni kell. Amikor létrehozok egy objektumot vagy bármilyen változót, akkor annak lefoglalok/allokálok valamennyi memóriát. Ezt a részt a memóriában fel kell szabadítani mielőtt lefoglalhatom valamilyen más célra. A pythonban ez az egész folyamat automatikus, nem kell törődni vele nagyon, azonban a C-ben ennél kicsit bonyolultabb a helyzet.
C memóriakezelés
C verem
A C memóriakezelésének egy részével már találkoztunk. Amikor két előadással ezelőtt arról beszéltem, hogy mi egy változó élettartalma, akkor az egyfajta memóriakezelés. Mindig amikor elkezdődik egy blokk a kódban, akkor a C program lefoglalja a szükséges memóriát az abban levő változóknak, és amikor vége a blokknak, akkor felszabadítja ezt a memóriát.
Mivel a blokkok szigorúan tartalmazzák egymást (nem lehet részleges átfedés két blokk között), ezért ha egy blokk éppen fut amig egy másik blokk kezdődik, akkor a másik blokknak lesz előbb vége. Ez azt jelenti, hogy ha lefoglaltam már az éppen futó X blokkhoz X memóriát, és most lefoglalok Y blokkhoz Y memóriát, akkor biztos az Y-t kell előbb felszabadítanom mint az X-et.
Emiatt ezeket a memóriablokkokat tárolhatom egymás "tetején", az X tetejére tehetem az Y-t, hisz az Y-t biztos előbb kell eldobnom mint az X-et. A memóriának azt a részét, ahol a C program a blokkok memóriáját így, egymás tetején, tárolja, úgy hívják hogy verem (angolul stack).
Itt látható egy ábra az angol wikipédiáról, hogy hogy néz ki ez a verem. Itt a DrawSquare függvény meghívta a DrawLine függvényt, és amíg a DrawLine függvény fut, addig a DrawLine függvény paraméterei és belső (lokális) változói le vannak tárolva a verem tetején. Mivel a DrawSquare biztosan nem tud véget érni amig a DrawLine még fut, ezért nem baj hogy lejjebb van a veremben és még nem felszabadítható.
Ahhoz hogy ez így megtehető legyen, az is kell egyébként, hogy a C-ben minden típusnak fix a mérete. Ezért, miután lefoglaltam egy memóriablokkot a DrawSquare-nek, a tetejére tehetek egy másik memóriablokkot, és nem kell aggódnom amiatt, hogy hirtelen nagyobb hely fog kelleni valamelyik változónak a DrawSquare-ben. (Ezért van az is, hogy a tömbök is fix méretűek ellenben a python-os listával, és ilyen kevés fajta típus van.)
És így az is látható, hogy mi történik akkor, ha használunk egy mutatót, ami olyan változóra mutat, ami már megsemmisült, aminek már vége a blokkjának: ha kezdődött egy másik blokk azóta, akkor az könnyen lehet hogy ugyanazt a helyet elfoglalja a memóriában amire mutat a mutató, és akkor valami olyasmit módosítunk a mutatón keresztül amit nem akarnánk.
C kupac
Azonban vannak olyan adatok, amiknek az élettartama nem ilyen szépen struktúrált. Egy korábbi példát nézve, lehet hogy egy függvény egy tömbben akar visszaadni adatokat, de a tömb méretéről csak azt tudjuk a program írásakor, hogy valahol 2 és 100000 között lesz. Az ilyen függvénynél nem jó mindig egy 100000-es tömböt használni, főleg ha a függvény sokszor lefuthat a program futása folyamán. Használható az ilyen esetekben a manuális memóriakezelés.
A C-ben a memóriának a másik fontos része, a veremen kívül, a kupac. A kupacból a programozó tud kérni memóriát egy speciális C függvénnyel, viszont az így elkért memóriát szintén a programozónak kell felszabadítania, egy másik függvénnyel. A memória kérésére használt speciális függvény a malloc(), a felszabadításra használt függvény a free(). Mielőtt jobban kifejtem a működésüket, nézzünk először egy egyszerű példát:
int i; int *tomb = malloc(10 * sizeof(int)); tomb[0] = 0; for(i = 1; i < 10; ++i) { tomb[i] = tomb[i-1] + i; } free(tomb); tomb = NULL;
Tehát amit itt látunk, hogy a malloc()-kal kérek egy 10 méretű tömbnyi memóriát, majd azt a memóriát használom 10 méretű tömbként, aztán felszabadítom a memóriát. A malloc() függvény paramétere az, hogy mennyi memóriát kérek, byte-ban mérve. Van egy spciális operátor a C-ben, a sizeof(), ami arra való, hogy megmondja, hogy egy alaptípus hány byte-ot foglal el. Tehát, a fenti példában, az int tipikusan modern rendszereken 4 byte méretű (ami 32 bit, ezért van hogy -2^31 és 2^31 közötti számokat tud tárolni), 40 byte memóriát foglalunk le a malloc()-kal. (De azért jó a sizeof()-ot használni, és nem csak odaírni hogy 40, mert ki tudja hogy valamikor használni akarják-e a programunkat nem tipikus rendszeren. Plusz, akkor nem kell megjegyeznünk hogy az int 4 byte-os.)
Aztán, miután tudom hogy már nem akarom többé használni ezt a tömböt, felszabadítom a számára lefoglalt memóriát a free()-vel. Jó szokás a memória felszabadítása után törölni (NULL-ra állítani) a mutatót, hogy nehogy próbáljuk a felszabadítás után is használni, hiszen addigra már lehet hogy valamilyen más célra van lefoglalva.
Tehát az eddigieket összefoglalva, a C-ben a malloc() és a free() használható manuális memóriakezelésre, ami azt jelenti hogy a memória lefoglalását és felszabadítását a programozó irányítja, nem hagyja a programnyelvre.
python fájlkezelés hasonlat
A python fájlkezelésnél tanultuk, hogy a legtöbb esetben ajánlott a fájl megnyitását a with kulcsszóval egybekötni, és akkor a fájl le lesz zárva a with blokk végén:
with open("adatok.txt", "w") as fajl: muszer = Muszer() fajl.write(muszer.azonosito + "\n")
De bizonyos esetekben a fájlt nem csak egy blokkon belül akarom használni, hanem pl. vissza akarok térni vele egy függvényből:
def iedik_fajl(n): fajlnev = "adatok_{}.txt" % n fajl = open(fajlnev, "w") return fajl
Ekkor ennek a függvény felhasználójának a felelőssége hogy bezárja a fájlt, mikor már nem használja többet, különben nem garantált a jó működés. Ehhez hasonló a verem és a kupac különbsége a C memóriakezelésnél:
- A legtöbb esetben megfelelő hogy egy blokkon belül érhető el csak a változó, ekkor egyszerűen definiálok egy változót, és az a veremben lesz tárolva.
- Bizonyos esetekben jobb ha nem vagyok a blokkokhoz kötve így, ekkor manuálisan is megoldhatom a memória kezelését a malloc()-kal és free()-vel.
A hasonlóságok nem véletlenek, a fájlok is úgy tekinthetőek mint egy erőforrás, amit a programoknak kezelniük kell, csak az még nehezebben automatizálható úgy hogy mindig jól működjön, és kevesebb problémát tud okozni a rossz kezelés, ezért annak a manuális kezelését a python-ban is engedik.
C memóriakezelés példák
Nézzünk néhány példát a memóriakezelésre.
Tetszőleges méretű tömbbel visszatérés
Korábban néztük, hogy amikor egy függvény egy tömbbel akar visszatérni, akkor egy lehetséges megoldás, hogy paraméterként megkapja a tömböt amibe beírja az adatokat. Egy másik lehetséges megoldás, hogy a függvény manuálisan lefoglal megfelelő mennyiségű memóriát a tömbnek. Ekkor a függvény visszatér a mutatóval erre a tömbre, és a függvény meghívójának felelőssége azt a memóriát felszabadítani:
// A visszateresi erteket fel kell szabaditani! int *beolvasott_range(int *n) { int i; scanf("%d", n); int *tomb = malloc(*n * sizeof(int)); for(i = 0; i < *n; ++i) { tomb[i] = i; } return tomb; } int main() { int N, i; int *mostani_range; mostani_range = beolvasott_range(&N); for(i = 0; i < N; ++i) { printf("%d ", mostani_range[i]); } free(mostani_range); return 0; }
Itt láthatjuk, hogy a függvény a tömb leendő méretét a parancssorról olvassa be, tehát a függvény hívója nem tudhatta előre, hogy mekkora tömböt adjon oda feltöltésre. Az ilyen függvényeknél a dokumentáció mindig tartalmazza, hogy fel kell szabadítani a visszatérési értéket.
Változó méretű tömb
Ha egy tömbnek akarjuk tudni változtatni a méretét, az lényegében elérhető, ha teszünk köré egy megfelelő struktúrát, megfelelő függvényekkel:
struct ValtozoTomb { int meret; int *adat; }; void ValtozoTomb_atmeretez(struct ValtozoTomb* tomb, int uj_meret) { if(uj_meret == 0) { (*tomb).meret = 0; free((*tomb).adat); (*tomb).adat = NULL; } else if((*tomb).meret != uj_meret){ int *uj_adat = malloc(uj_meret * sizeof(int)); int i; // Atmasoljuk amit kell if(uj_meret < (*tomb).meret) { for(i = 0; i < uj_meret; ++i) { uj_adat[i] = (*tomb).adat[i]; } } else { for(i = 0; i < (*tomb).meret; ++i) { uj_adat[i] = (*tomb).adat[i]; } for(i = (*tomb).meret; i < uj_meret; ++i) { uj_adat[i] = 0; } } //Felszabaditjuk a regi memoriat: free((*tomb).adat); //Beallitjuk az uj ertekeket (*tomb).meret = uj_meret; (*tomb).adat = uj_adat; } }
Ezt a változtatható méretű tömböt pl. így lehet használni:
int main() { struct ValtozoTomb teszt_tomb = {0, NULL}; ValtozoTomb_atmeretez(&teszt_tomb, 10); teszt_tomb.adat[0] = 20; ValtozoTomb_atmeretez(&teszt_tomb, 2); printf("%d", teszt_tomb.adat[0]); // Ez az adatok felszabaditasa: ValtozoTomb_atmeretez(&teszt_tomb, 0); return 0; }
Figyeltem a kód megírása közben, hogy amikor átméretezem, akkor a régi adatot mindig felszabadítsam. Ennél a tömbnél a helyes használat átméretezni 0-ra amikor befejeztük a használatát, mert az felszabadítja a belső adattárolót. Az, hogy a ValtozoTomb struktúra megsemmisül a blokk végén, az nem jelenti alapból azt hogy a benne levő adattároló is felszabadul, csak ha direkt felszabadítjuk így.
Láncolt lista?
Automatikus vs. manuális memóriakezelés
Hogy működik az automatikus memóriakezelés?
A python interpreter ugyan felszabadítja idővel a fölösleges memóriát, de tényleg csak idővel, így a program folyamatosan több memóriát használ mint feltétlenül szükséges. (Plusz, eleve az automatikus memóriakezelés megvalósításához plusz memóriára van szükség az interpreterben, mielőtt a mi programunk akár elindult volna.) Aztán időnként lefut az interpreterben az a folyamat, amit szemét gyűjtésnek hívnak, amikor kidob mindent ami már nem kell, azonban ez a folyamat bizonytalan hogy mennyi ideig tart, és kb. bármikor lefuthat, így ha a programunknak azonnal kell tudnia reagálni, akkor megengedhetetlen hogy közben lefusson egy szemétgyűjtés, python-ban viszont ezt nem lehet megakadályozni.
Összehasonlítás
Mint láthatjuk a manuális memóriakezelés nem egyszerű, mindjárt nézünk néhány tipikus hibát is majd, amit könnyű vele elkövetni. Sokkal nehezebb használni, de mit nyerünk vele, miért használják mégis még mindig a C-t mint programozási nyelv? A válasz, mint a C-nél általában, az, hogy hatékonyságot.
Ezek mind olyan problémák, amik a legtöbb programozó számára nem számítanak. Ellenben a C problémáival, amivel mindenki szembesül aki használni próbálja. Ezért is mondtam az első C előadáson, hogy valószínűleg nem fog nektek a C kelleni a jövőben, csak a nagyon erőforrásigényes és/vagy sebesség igényes alkalmazásokhoz használják. De azért néhány tanulság levonható a most tanultakból, ami automatikus memóriakezelés esetén is érvényes.