Informatika2-2015/Eloadas 12 C-5 Memoria kezeles

A MathWikiből
(Változatok közti eltérés)
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 az is kell egyébként, hogy a C-ben minden típusnak fix a mérete. Ezért le tudok foglalni 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, hogy a tömbök is fix méretűek ellenben a python-os listával ellentétben, és ilyen kevés fajta típus van.)
+
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ó.

Call stack layout.png

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.

Személyes eszközök