C memóriakezelés Ez a kis segédanyag az adatszerkezetek órán használt eszközök megértését hivatott elősegíteni. A teljesség igénye nélkül kerül bemutatásra a mutató típus és a dinamikus memóriakezelés. A mutató típusú változó A változóknak van név, érték, attribútum (pl: típus, const, ) és cím komponensük. A mutató típusú változók memóriacímek tárolására szolgálnak. Speciális elem a NULL, mely azt jelzi, hogy a mutató nem tartalmaz érvényes címet, nem mutat semmire. Mutató típusú változót egy típus és a változó neve elé írt csillag karakterrel hozhatjuk létre. <típus> *<név> [=kezdőérték] [,*<név> [=kezdőérték]] ; A mutató típusú változóban tárolt címet a megadott típusnak megfelelően fogja felhasználni a rendszer. Ez dönt például arról, hogy hány bájtról olvassunk ki/vagy hány bájtra írjunk adatot, a címet felhasználva és persze arról is, hogy az ott talált bitsorozatot hogyan értelmezzük. Nézzünk pár példát, hogy könnyebben megérthessük, hogyan kell a fentieket érteni: 1. int x = 300; //reprezentációja: 00000000 00000000 00000001 00101100 2. int *p = &x; //A * a típusmegadás része -> p egy mutató 3. char *p2 = &x; //A * a típusmegadás része -> p2 egy mutató 4. 5. printf("%d ", x); // kiírja a 300-as értéket 6. printf("%p ", &x); // kiírja x címét 7. 8. printf("%d ", *p); // kiírja a 300-as értéket (p címen lévő értéket) 9. printf("%p ", p); //kiírja p értékét, vagyis x címét 10. 11. printf("%d ", *p2); //kiírja 44-es értéket 12. printf("%p ", p2); //kiírja x címét Az x egy egész típusú 300-as kezdőértékkel rendelkező változó, p egy egész típusú változóra mutató, mutató típusú változó (röviden int típusú mutató). Kezdőértéket is adtunk neki a & (címe operátor, prefix) segítségével, mely egy változó neve elé írva annak címét adja vissza. Az prefix * jel az & inverz operátora, úgynevezett indirekciós operátor. Ez egy mutató elé írva, visszaadja, a mutató által hivatkozott címen lévő értéket. (A változó létrehozásakor csak a mutató típus jelzésére szolgál a csillag.) A p címen lévő érték feldolgozásakor a p címtől kezdődő 4 bájton (egészen pontosan sizeof(int) bájton) lévő bitsorozatot előjeles egészként fogjuk értelmezni. A p2 viszont karakter típusú mutató, ezért hiába adtuk meg egész típusú változó címét, p2-n keresztül feldolgozva a címen lévő bitsorozat 1 bájtos előjeles egészként lesz értelmezve. Ezért van az, hogy a 11. sor 44-es értéket ír ki a 300-as helyett, hiszen az elért 1 bájt tartalma a 00101100, ami egészként értelmezve pont 44.
Megj.: Ha a mutatóra hivatkozunk, annak akarunk értéket adni, akkor * nélkül hivatkozunk rá. A deklarációban a típusmegadás miatt kell csak a *. int x, *p; p = &x; //p innentől x-re mutat. (x címét p-be másoltuk) Mégis mire jó ez? A változók fizikai tárolása A tárolás szempontjából a programunk által használt memóriát több részre fogjuk osztani. Az egyik rész, a statikus memóriaterület. Erre a részre kerülnek a konstansok, melyek értéke az egész program futása során állandó. Ezen kívül van egy dinamikus memóriaterület. Ezen a részen foglalhatunk le területet a változók számára. Ami most még érdekes számunkra, a hívási láncot tartalmazó verem. Ez azért fontos, mert minden meghívott függvénynél (a főprogram az első meghívott) a verembe kerül egy elem, mely az adott függvény futtatásához szükséges információkat tartalmazza. Ebbe a verembe kerülnek tárolásra a függvények lokális változói is. Ha a függvény lefutott, akkor a verem legfelső eleme törlődik, vagyis megszűnnek a lokális változók. Feladat 1.: Cseréljünk fel két változó értékét egy függvény segítségével! Először lássuk azt a kódot, ami nem oldja meg a feladatot: void csereldfel(int a, int b){ int tmp = a; a = b; b = tmp; int x = 2, y = 4; printf( eredeti: %d %d\n, x, y); /*2 4 jelenik meg*/ csereldfel( x, y ); printf( felcserelt: %d %d\n, x, y); /*2 4 jelenik meg */ A C-ben kizárólag érték szerinti paraméterátadás van, tehát az x és y változók értéke kerül át a a csereldfel függvény a és b paraméterébe, és annak változtatása az eredeti példányokra nincs hatással. A csereldfel függvényt ebben a formájában akár két konstanssal is meghívhatnánk, hiszen úgyse azok értékét próbálná felcserélni. Ahhoz, hogy elérjük célunkat, meg kell oldani, hogy a csereldfel függvény ismerje az x és y változóink címét. A címek ismeretében, már tudná módosítani a változók értékét. Címszerinti paraméterátadás hiányában ezt úgy tudjuk megoldani, hogy x és y címét, mint értékeket adjuk át. Ehhez alakítsuk át úgy a csereldfel függvényt, hogy cím típusú értékeket tudjon fogadni, és a híváskor x és y értéke helyett a cím operátor segítségével azok címeit adjuk át. Ennyivel sajnos nem ússzuk meg, hiszen a csereldfel függvény belső részében is gondoskodnunk kell
arról, hogy ne az a és b értéke (az x és y változók címei) legyenek felcserélve a függvényen belül, hanem az a és b címen lévő értékek (vagyis x és y változó tartalma). Ehhez a függvényen belül az indirekciós operátort kell használnunk az a és b paraméterek előfordulásainál. void csereldfel(int *a, int *b){ /*memóriacím fogadására felkészítve*/ int tmp = *a; /* az a címen lévő érték kerül tmp-be*/ *a = *b; /*a b címen lévő érték kerül az a-ban lévő címre*/ *b = tmp; /*a b-ben lévő címre kerül a tmp-ben lévő érték*/ int x = 2, y = 4; printf( eredeti: %d %d\n, x, y); /*2 4 jelenik meg*/ csereldfel( &x, &y ); /*x és y címét adjuk át, mint értékeket*/ printf( felcserelt: %d %d\n, x, y); /*2 4 jelenik meg */ Feladat 2.: Készítsünk függvényt, mely létrehoz és visszaad egy 10 elemű egész típusú tömböt és feltölti 1 és 10 között lévő számokkal. Lássuk az alábbi kódot, mely nem oldja meg a feladatot! int[] ujtomb(){ int tomb[10], i; for(i = 0; i<10; ++i) tomb[i] = i; return tomb; /*ajjaj, mi lesz itt?*/ int[] t = ujtomb(); printf( %d, t[3]); Eddig a változók memóriabeli tárolásával nem kellett foglalkoznunk. A változók definiálásánál megadott típus szerint automatikusan lefoglalódott a memóriában a hely a változó számára, ahol a változó értéke tárolásra került. Nos, most éppen ez az eddig hasznos segítség okozza számunkra a problémát, hiszen a tomb az ujtomb függvény lokális változója, ezért a rendszer nem csak lefoglalja számára a helyet de a függvény befejeztével gondosan fel is szabadítja azt, így hiába kapjuk meg a tömb címét, a mainben már nem sok mindent tudunk kezdeni vele. A dinamikus memóriakezelés használatával orvosolhatjuk a problémát. Ismerkedjünk meg néhány függvénnyel: void *malloc(int size); A malloc függvény a paraméterben megadott bájtnyi memóriablokkot foglal le. Egy void* típusú mutatót ad vissza, mely a lefoglalt terület elejére mutat. Sikertelen memóriafoglalás esetén NULL értéket ad vissza.
void *calloc(int, int); A calloc függvény első paramétere egy elemszám, a második paramétere egy elem mérete. Az elemszámnak és típusnak megfelelő memóriablokkot foglal le és egy void* típusú mutatót ad vissza, mely a lefoglalt terület elejére mutat. Sikertelen memóriafoglalás esetén NULL értéket ad vissza. void free(void *ptr); A dinamikusan lefoglalt terület felszabadítására szolgál. Egy mutatót vár, ami a felszabadítandó memóriablokk elejére mutat. (NULL érték átadása nem okoz hibát.) Példa: //10 int típusú elemnek megfelelő hely int *t = (int*) malloc(10*sizeof(int)); //12 bájtnyi terület lefoglalása és int típusra konvertálása int *t2 = (int *) malloc(12); //5 karakternek megfelelő hely (5 bájt lefoglalása) char *s = (char *) malloc(5*sizeof(char)); //10 valós típusú elem számára szükséges hely lefoglalása, kinullázása double *t3 = (double*)calloc(10, sizeof(double)); //a 10 int típusú elem számára foglalt hely felszabadítása free(t); Láthatjuk, hogy a malloc és calloc függvény is void* típussal tér vissza, melyet konvertáltunk, amit a számunkra megfelelő típusra konvertálhatunk. A tanultak ismeretében alakítsuk át a fenti újelem függvényt, hogy megfelelően működjön! int* ujtomb(){ int i, *tomb = (int *)malloc(10*sizeof(int)); for(i = 0; i<10; ++i) tomb[i] = i; //amint látjuk a mutatóknál is használható az index return tomb; int* t = ujtomb(); printf( %d, t[3]); free(t); //ha már nincs szükség a tömbre, a helyet felszabadítjuk
Mutató aritmetika A mutatók növelése, csökkentése mindig függ a mutató típusától. Ha egy mutatót léptetünk, automatikusan annyival nő vagy csökken az értéke (annyi bájttal arrébb lévő címre mutat), ahány bájt kell a típus reprezentálására. Pl.: Legyen char *p1, és egy int * p2 mutatónk, akkor ++p1 hatására p1 eggyel nő (egy bájttal arrébb lévő címre mutat), a ++p2 hatására a p2 néggyel (egészen pontosan sizeof(int)- tel) nő (ennyi bájttal odébb mutat). int *p, *q; int t[10]; p = t; //A tömb neve a tömb címét adja meg, ezért nem kell & Mutatókhoz hozzáadhatunk konstansokat, kivonhatunk belőlük konstansokat. Futtassuk képzeletben a következő kódot! *(p+1); p[1]; p[-3]; ++p; *p; *p-1; *(p-1); *p[-1]; //a p+1 címen lévő érték, ami t[1] //a p+1 címen lévő érték, ami szintén t[1] //hibás, mivel t tömb negatív indexű elemére hivatkozik //p egy elemmel arrébb lép! //t[1] elem (az előző sor miatt) //t[1]-1 //t[0] //t[0] Azonos tömbre mutató mutatókat kivonhatunk egymásból. Ez egy egész számot fog adni eredményül! Lássunk egy példát a használatára! char s[] = Gyűlölöm a mutatókat! ; char *p; int index = -1; // ha nincs a betű, akkor -1 lesz az index //keressük meg az első a betű helyét! for(p = t; *p; ++p) if(*p == a ) break; if(*p){ //A stringnél a \0 az tényleges nulla index = p-s; //Ahol megtaláltuk az a -t mínusz a tömb kezdőcíme. (Egyéb műveletek is végezhetők, de nem mindennek van értelme.) Feladatok: Hozzunk létre egy x valós változót, és egy mutatót, mely a deklarációtól kezdve rá mutat!
Hozzunk létre egy x valós változót, és egy mutatót, mely a deklarációt követő sor után fog rá mutatni! Hozzunk létre két olyan mutatót, mely ugyan arra a memóriacímre hivatkozik! Foglaljunk le helyet 5 elemű karakterekből álló tömb számára! Foglaljunk le helyet 15 hosszúságú szöveg tárolására! Foglaljunk le helyet 10 elemű egészek számára, úgy, hogy az elemeknek nulla legyen a kezdőértéke! Foglaljunk le helyet 15 hosszúságú szöveg tárolására! Számoljuk össze kétféle módon, hogy hány b betű áll a tömbben! (Indexel, és mutató léptetéssel!) A program végén szabadítsuk fel a lefoglalt memóriát! Írjunk függvényt, mely paraméterként átadott n darab int típusú elem számára foglal helyet, és visszatér a tömb kezdőcímével! Írjunk függvényt, mely paraméterként átadott n darab, szintén paraméterként adott m méretű elem számára foglal helyet, és visszatér a tömb kezdőcímével! Hozzunk létre olyan 5 elemű tömböt, mely int típusú mutatókat tartalmaz! Hozzunk létre dinamikusan olyan 5 elemű tömböt, mely int típusú mutatókat tartalmaz! Hozzunk létre dinamikusan egy NxM es mátrixot! Töltsük fel az előbbi mátrixot, majd összegezzük a főátlóban lévő elemeket! Számoljuk ki az előbbi mátrix oszlopösszegeinek átlagát! Hozzunk létre egy sztring típusú változót, és egy p mutatót. Index használata nélkül döntsük el, hogy az adott sztring tartalmaz-e két azonos karaktert egymás mellett!