Dinamikus memóriakezelés
Tartalom
- Emlékeztető – mutatók használata
- Emlékeztető – címaritmetika
- A
const
kulcsszó - Const kulcsszó általában
- Dinamikus memóriakezelés
- Statikus memóriakezelés: a három gondunk
- Dinamikus memóriakezelés: célok
- A
malloc()
és afree()
függvény malloc()
,free()
– a játékszabályokmalloc()
,free()
: példákmalloc()
,free()
: a foglalt terület tulajdonosacalloc()
,realloc()
malloc()
,free()
– kérdések- A DinamikusTömb
- A memóriakezelés áttekintése
- A három memóriaterület
- A globális memóriaterület
- A verem (stack)
sizeof(s)
ésstrlen(s)
- A kupac (heap)
- Hibalehetőség: érvénytelen pointerek
- Visszatérés dinamikusan foglalt területtel
- És ezek vajon helyesek-e?
- Több dimenziós tömbök
- Emlékeztető – több dimenziós tömbök
- Több dimenziós – átadás függvénynek
- Összetett deklarációk
- Sztringek tömbje – tömbök tömbje
- Sztringek tömbje – pointerek tömbje
- 2D dinamikus tömbök – módszerek
- 2D dinamikus tömb – melyiket válasszuk?
- Többszörös indirekció – mutató mutatóra
- Többszörös indirekció – 2D tömb
1 Emlékeztető – mutatók használata
Mutatók létrehozása, cím képzése, indirekció
int i, *pi = &i; i=2; *pi=2; char c, *pc = &c; putchar(c); putchar(*pc);
A pointer egy változó címét (helyét) tárolja a memóriában. Egy
adott változóra ez a cím a &
operátorral képezhető,
és a pointerben eltárolható. Így a pointer gyakorlatilag az adott
változóra hivatkozik, rajta keresztül elérjük a változót, aminek
a címét képeztük. Ha a pointert dereferáljuk a *
operátorral, akkor a változót kapjuk meg, amelynek értéke írható
és olvasható is a pointeren „keresztül”. Vagyis a dereferált
pointer is lehet balérték.
A pointer típusához hozzátartozik a mutatott változó típusa is, pl.
egészre mutató pointer típusa int*
, karakterre mutató
pointer típusa pedig char*
. Erre két dolog miatt van
szükség:
- Egyrészt így válik értelmessé a dereferálás, hiszen az csak akkor értelmes művelet, ha tudjuk, hogy milyen típusú adatot hivatkozunk a memóriában. A helye önmagában ehhez kevés.
- Tömbök esetén a típus ismerete ad lehetőséget a pointer aritmetikára: kiszámítható, hogy a következő elem hol van. Annyi bájttal van arrébb a tömb kezdetéhez képest, ahány bájtos egy elem.
Ezért működnek úgy a tömbök, hogy egyforma típusokat lehet csak tárolni
bennük. Nem lehet egy tömbben összevissza mindenféle, int
,
double
, struct Pont
stb. típusú
adat, hiszen ezek eltérő méretűek, és nem lehetne könnyen kiszámolni,
hogy melyik elem hol található a memóriában.
Cím szerinti paraméterátadás
void pluszegy(int *hova) { *hova += 1; }
int x=2; pluszegy(&x);
2 Emlékeztető – címaritmetika
int tomb[4], *pi; pi=tomb; // &tomb[0] pi[2]=4; // *(pi+2)=4 tomb[1]=2; // *(tomb+1)
A tömb elemei helyének kiszámítására azért van lehetőség, mert tudjuk, hogy azok egymás mellett helyezkednek el a memóriában. A fordító e célból pontosan így helyezi el őket a memóriában.
Akkor is ez történik, amikor az indexelő operátort használjuk; akár pointeren, akár egy tömbön magán. Kiszámolódik az adott elem címe, utána pedig az így keletkező pointer dereferálódik. C-ben így működik a tömbök paraméterként átadása is, ahogyan az a következő forráskódokon látható.
void tomb_kiir(int *tomb, int meret) { int i; for (i=0; i<meret; ++i) printf("%d ", tomb[i]); }
int tomb[10]; tomb_kiir(tomb, 10);
Akár változót adunk át cím szerint, akár egy tömböt a függvénynek, mindkét
esetben egy pointer adódik át. Önálló változó esetén ezt a pointert a *
operátorral dereferáljuk a függvényben, és így elérjük a változót.
Tömbök esetén elvileg egy címszámítás és egy dereferencia kell (*(tomb+i)
),
de ezt külön kiírni felesleges, hiszen erre való az indexelő operátor.
Az itt bemutatott működés kellemetlen tulajdonsága az, hogy akár tömböt adunk át, akár önálló változóra mutató pointert, mindkét esetben a paraméter típusa mutató. Ezért ha látunk egy ilyen függvénydeklarációt:
void fuggveny(int*);
akkor ránézésre nem tudjuk megmondani, hogy ez a függvény egy darab integert vár cím szerint, vagy egy tömböt. Tömb esetén ezért a definiálatlan méretű tömbparamétert szokták használni néha:
void fuggveny(int[]);
De ez tökéletesen ugyanazt jelenti a fordító számára, mint az előbbi. Vagyis ilyenkor is egy pointer adódik csak át, és ilyenkor sem másolódik le a tömb, hanem az eredetit látjuk! Ezért ez a forma meg ilyen szempontból lehet kicsit megtévesztő.
3 A const
kulcsszó
/* a világ utolsó C bugja */ while (1) { status=GetRadarInfo( ); if (status=1) LaunchMissiles( ); }
Nem mindig szeretnénk, ha a cím szerint átadott változót meg tudná változtatni a függvény!
Például egy kereső függvénytől nem várjuk, hogy megváltozzon a hívás során a tömb értéke, és ha ilyet hibázunk, arra a fordító sem figyelmeztet. Előfordulhat, hogy egy nagy struktúrát cím szerint adunk át egy függvénynek – nem azért, hogy meg tudjuk változtatni, hanem csak mert nem szeretnénk feleslegesen lemásolni, mert az nagyon lassítaná a programunkat.
A const
kulcsszóval tudjuk jelezni egy adatnál, hogy
azt nem szabad megváltoztatni.
void kiir(int const *tomb, int meret); int tomb[100] = { … … … }; kiir(tomb, 100); // int* → int const*
struct Rajz { … … … }; void rajzol(Rajz const *r); Rajz r; rajzol(&r); // Rajz * → Rajz const *
int const *
– ez azt jelenti, hogy ez a mutató
konstans integerekre mutat. Vagyis hogy meg lehet hivatkozni
a mutatott értékeket (a tömb elemeit), és ki is lehet olvasni azokat,
de változtatni nem lehet rajtuk. Vagyis pl. egy értékadást nem fog
engedni a fordító, hanem fordítási hibával visszadobja a programot.
Az ilyen mutatókat kétféleképpen szokták írni: int const *
és const int *
. Mind a kettő ugyanazt jelenti: a pointer
által mutatott integereket konstansnak tekintjük. Az utóbbi időben
elterjedtebb az int const
forma, mert az logikusabb,
ha mindig azután írjuk, hogy const
, ami konstans. (Pl.
int * const
azt jelenti, hogy az integereket lehet
változtatni, de a pointert nem lehet máshova állítani: *p=2
szabad, p++
viszont nem. Erről a második
félévben lesz szó részletesebben.)
A függvényhívás helyén a tömb kezdőcíme elvileg int *
típusú. Ezt a fordító automatikusan konvertálja int const *
-gá;
ilyenkor persze igazából semmi nem történik, ez csak egy jelzés, hogy
a függvényen belül már tilos a változtatás. Egy adott típusról
ugyanolyan típusú konstanssá automatikus a konverzió. A másik irányba
viszont értelemszerűen tiltott: ha egy konstans pointer sima pointerré
alakulhatna vissza automatikusan, akkor az egésznek semmi értelme
nem lenne.
A konstansoknak egyébként kiváló „öndokumentáló” szerepük is van. Ha olvassuk a kódot, és látjuk valamiről, hogy konstans, akkor hamarabb megértjük a program működését – hiszen egyből látjuk, mi a konstans, és mi a változó.
4 Const kulcsszó általában
A karbantarthatóság egyik alapfeltétele: az olvashatóság.
int x, y; for (y=0; y<480; ++y) for (x=0; x<640; ++x) putpixel(x, y, 133, 224, 89);
„mágikus számok” –
kezelhetetlenek!
int const KepMag=480; int const KepSzel=KepMag/3*4; int const piros=133, zold=224, kek=89;
int x, y; for (y=0; y<KepMag; ++y) for (x=0; x<KepSzel; ++x) putpixel(x, y, piros, zold, kek);
konstansok
Ha a programunkat egy másik felbontásra szeretnénk átállítani, elegendő mindössze a két konstans értékét felülírni és a következő fordításnál már minden algoritmus az új értékekkel fog dolgozni.
Ha a konstansok helyett számokat használtunk volna, akkor minden előfordulásnál felül kéne írni őket. Ez fáradságos munka, és hibalehetőségeket is rejt magában: elég egy helyen elfelejteni az átírást, és egy nehezen megtalálható hibát vittünk a programba.
A konstansok „furcsa állatok”. Ezek tulajdonképp konstans
változók: az oxymoron mindkét fontos tulajdonságukat kifejezi.
Konstansok, mert a létrehozás után már nem lehet őket megváltoztatni.
Változók, ugyanúgy van helyük a memóriában, értéket kaptak a
létrehozásuk pillanatában – és nem lehet velük tömb méretét megadni!
Az utóbbi ok miatt konstans egészeket gyakran az enum
kulcsszóval
hozunk létre, bár az eredendően nem erre való.
6 Statikus memóriakezelés: a három gondunk
1. A program írásakor nem tudjuk előre, mennyi memória kell.
int tomb[1000], i, n; printf("Hány szám? "); scanf("%d", &n); for (i=0; i<n; ++i) tomb[i] = …
- Pl. a felhasználó mondja meg, mennyi adat lesz.
- Felülbecsüljük? Túl sok memóriát foglalunk!
- Alulbecsüljük? Nem működik a programunk!
char *beolvas(void) { char s[200]; fgets(s, 200, stdin); return s; }
2. Nem tudjuk kontrollálni az élettartamot.
- Globális változó: „örökké” létezik
- Függvény lokális változója: csak a függvényen belül létezik
3. A hatalmas tömb, amit szeretnénk, nem fér a verembe.
Az élettartam problémája: például egy globális változó akkor is létezik,
ha éppen nincs rá szükség, és ilyenkor feleslegesen foglalja a memóriát.
A lokális változó meg megszűnik, ezért ott nem tudunk eredményt létrehozni
a hívó számára – hacsak nem másoljuk le. A második példa, a beolvas()
kifejzetten hibás:
a függvényből visszatérve megszűnik az s[]
tömb, ezért az arra
mutató pointer érvénytelenné válik.
7 Dinamikus memóriakezelés: célok
Mi dönthetjük el:
- Mennyi memóriát foglalunk
- Mikor foglalunk memóriát, és mikor szabadítjuk fel
Vagyis a kezünkbe kerül minden.
![]()
Mindennek van ára…
A fentiekért cserébe a saját felelősségünk
- a foglalás, és
- a felszabadítás is.
Ha elfelejtjük megtenni, a programunk folyton növekszik, és előbb-utóbb le kell állítani…
- Ez vagy a felhasználónak kellemetlen,
- Vagy nekünk, ha ezt az operációs rendszer kénytelen megtenni.
Gondoljunk bele, a dolog néha kényelmetlen, ugyanakkor nagyon hasznos tud lenni. Ha egy adott memóriaterületre csak egy bizonyos ideig van szükség, akkor lefoglalhatjuk csak akkor, amikor először kell, és felszabadítatjuk, amikor már nem kell. Így a programunk kevesebb memóriát foglal.
Másrészt pedig nem kell pazarlóan felülbecsülnünk a méretet, és nem kell attól sem félni, hogy alulbecsüljük azt a program tervezése során. Amikor már tudjuk, mennyi memória kell, csak akkor foglalunk. Például két sztring összefűzve: megnézzük a két összefűzendő sztring hosszát, és az alapján tudjuk, hogy az összefűzött számára mennyit kell foglalnunk. Vagy megnyitunk egy fájlt, megnézzük, milyen hosszú, lefoglalunk annyi memóriát, és beolvassuk az egészet.
8 A malloc()
és a free()
függvény
void *malloc(size_t méret)
Lefoglal egy bájtban megadott méretű memóriaterületet (malloc: memory allocation).
- Visszaad egy pointert a legfoglalt területre
- Azért
void*
, mert nem ismeri a típusunkat - Ha nem sikerül, akkor
NULL
pointert ad - A terület inicializálatlan (memóriaszemét)
void free(void *ptr)
Felszabadít egy memóriaterületet, amit a malloc()
foglalt.
- Ugyanazt a pointert kell adni neki, amit a
malloc()
adott
9 malloc()
, free()
– a játékszabályok
1. A lefoglalt memóriaterületet fel kell szabadítani.
- Ahány
malloc()
hívás, annyifree()
hívás kell történjen!
2. A malloc()
által adott pointer szemünk fénye!
- Ha elveszítjük, nem tudjuk majd azt a területet visszaadni.
3. Allokálatlan memóriaterület nem használható!
char *szoveg; szoveg[2]='x';
Ha nem foglaltuk le, akkor még nem.
free(szoveg); szoveg[2]='x';
Ha felszabadítjuk, akkor már nem.
10 malloc()
, free()
: példák
Nem tudjuk, mekkora tömb kell
Feladat: írjunk programot, amiben a felhasználó bárhány számot tud adni; ezeket mind tároljuk el!
#include <stdlib.h> double *tomb; // ptr a majdan lefoglalt területre int i, n; printf("Hány számot? "); scanf("%d", &n); tomb=(double*) malloc(n*sizeof(double)); // foglalás if (tomb==NULL) { printf("Nem sikerült memóriát foglalni!\n"); return 1; } tomb[3]=12; /* tesszük a dolgunkat */ // ez egy tömb! free(tomb); // felszabadítás
A lefoglalt területre egy void*
mutatót kapunk.
Ez azért mutat void
, vagyis ismeretlen típusra,
mivel a malloc()
nem tudja, milyen típusúak
a lefoglalt területen tárolt adataink. (Nem is tudhatja, hiszen
a malloc()
a nyelvbe van beépítve, vagyis nem lehet
felkészítve az összes általunk létrehozott típusra, pl. a
saját magunk által definiált felépítésű struktúrákra.)
Ezt a void*
típusú pointert átalakítjuk a saját
típusunkra mutató pointerré (type cast), pl. jelen esetben
double*
típusúvá, és így tároljuk el a
változónkban.
Mivel a malloc()
semmit nem tud a lefoglalt
területünkről, a méretét nekünk kell kiszámolni, és bájtokban
megadni. Itt egy tömböt foglalunk, ezért a legegyszerűbben
ezt egy szorzással tehetjük meg. A sizeof
operátor, ahogy a fájlkezelésnél is szerepelt, egy adott típus
méretét bájtban megadja; pl. sizeof(double)
az annyi lesz, ahány bájtos egy double
szám.
Ha ezt megszorozzuk a tömb elemszámával, akkor éppen a kérdéses
méretet kapjuk. A lefoglalt terület inicializálatlan, vagyis memóriaszemetet
tartalmaz.
Innentől kezdve a pointeren keresztül el tudjuk érni a memóriaterületet. Kényelmes itt nagyon, hogy a pointereken is használható az indexelő operátor, hiszen a használat közben nem is kell foglalkozni vele, hogy a tömb statikusan vagy dinamikusan lett lefoglalva; ugyanúgy működik az indexelő operátor, ugyanúgy átadható függvénynek (hiszen a tömbös függvények eddig is pointert vártak) és így tovább. Túlindexelni természetesen továbbra sem szabad.
Ha végeztünk, és már nincsen szükség az adatokra, akkor visszaadjuk
a lefoglalt területet. Ehhez átadjuk a free()
utasításnak
azt a pointert, amit a malloc()
-tól kaptunk (ezzel
azonosítja, hogy melyik területről van szó). Elvileg ugyan a free()
egy void*
típusú pointert vár, de ilyenkor a típus
konverzióját nem kell elvégezni, mert a valami*
→void*
konverzió automatikus. Miután felszabadítottuk a memóriaterületet, már
nem szabad hivatkozni azt! Hiszen azt egy másik malloc()
hívás másra használhatja, vagy bármi más történhet vele. Ha így teszünk,
az nagyon súlyos hiba!
A C nyelv újabb, de nem minden fordító által elfogadhatóan
támogatott verziójában, a C99-ben lehetőség van arra, hogy
malloc()
hívás nélkül hozzunk létre olyan tömböt,
amelynek mérete futási időben derül ki. Ilyen tömb természetesen csak
függvény lokális változója lehet, és bár a mérete nem fix, de az
élettartama igen, hiszen a függvényből visszatérve meg fog
szűnni.Vagyis a fentit C99-ben akár így is írhatjuk (természetesen a
free()
hívást is elhagyva):
printf("Hány számot? "); scanf("%d", &n); double tomb[n]; …
A változóval megadott méretű tömböket a különböző programozási nyelvek
vagy támogatják, vagy nem. A Pascal és a C89 nem támogatja, a C99 és a C++
pedig igen. Algoritmusok tervezésekor figyelembe kell vennünk, hogy egy
meg nem adott méretű tömböt esetleg dinamikus memóriakezeléssel tudunk
csak majd megvalósítani a választott programozási nyelven. Figyelembe kell
vennünk azt is, hogy a verem mérete általában jóval kisebb, mint a
dinamikusan, malloc()
-kal foglalható memória mérete. Így ezt
csak kisebb tömbökre használhatjuk.
Kontrollálnunk kell az élettartamot is
Feladat: írjunk függvényt, amely összefűz két, paraméterként kapott sztringet! A függvény visszatérési értéke legyen az összefűzött sztring!
char *s; s=osszefuz("alma", "fa"); ...
Ebben dinamikus memóriakezelést kell használnunk. Nem csak azért, mert nem tudjuk előre, hogy mekkora lesz a tömb, hanem azért is, mert a függvényből visszatérve a lefoglalt tömbnek meg kell maradnia. Az semmiképpen nem lehet a függvény lokális változója!
/* visszatér egy sztringgel, ami s1 és s2 összefűzve. * a hívónak fel kell szabadítania a kapott sztringet! */ char *osszefuz(char const *s1, char const *s2) { int mennyi; char *res; mennyi = strlen(s1)+strlen(s2)+1; res = (char*) malloc(mennyi*sizeof(char)); if (res == NULL) /* ha nem sikerült a malloc() */ return NULL; strcpy(res, s1); strcat(res, s2); return res; }
Látható, hogy így meg tudjuk oldani az élettartam problémáját is.
A függvényből visszatérve ugyanis az új sztringet tároló memóriaterület
nem szabadul fel automatikusan, hanem a hívó döntheti el, hogy
mikor nincsen már a továbbiakban szüksége arra. Vissza
ez a függvény nem a sztringet adja, hanem csak egy pointert a lefoglalt
memóriaterületre. A lokális char* res
változó
ugyan megszűnik, de azt a visszaadáskor lemásoljuk! (Csak a pointert!
Nem az egész tömböt!)
Amikor visszatérünk, a létrehozott tömb így nem szűnik meg! A pointer ugyan igen, de azt visszaadjuk, és a hívónak azt el kell tárolnia magának.
11 malloc()
, free()
: a foglalt terület tulajdonosa
A hívó dolga a felszabadítás: ezt a tényt dokumentálni kell.
/* visszatér egy sztringgel, ami s1 és s2 összefűzve. * a hívónak fel kell szabadítania a kapott sztringet! */ char *osszefuz(char const *s1, char const *s2);
char *s; s=osszefuz("alma", "fa"); printf("Ezt írtad be: %s\n", s); free(s); // !
A függvény visszatér egy pointerrel a lefoglalt területre. Valakinek azt fel is kell szabadítania – a hívó felel érte, hogy fel legyen szabadítva.
Fontos figyelni arra, hogy fel is szabadítsuk a memóriaterületet,
ha már nincsen rá szükség. Mivel az osszefuz()
függvény ezt nem teheti meg
(hiszen épp az a feladata, hogy foglalja le a sztringet, és ne
szabadítsa fel), ez csakis a hívó feladata és felelőssége lehet.
Emiatt a visszakapott pointert el kell menteni egy változóba,
hiszen ha elfelejtjük, akkor semmi mód nem lesz már arra, hogy
felszabadítsuk azt a memóriaterületet. Természetesen az
s=osszefuz(………)
értékadás ilyenkor nem sztring másolás,
hanem csak egy pointer értékadás. Hiszen s
típusa
pointer, a visszaadott érték is pointer, és ezt másoljuk az értékadással.
Ha a változó élettartama átível a függvényeken, bízzuk rá valakire! Rendeljük valakihez a felelősséget!
Hogy biztosítsuk a program tervezése során, hogy a területet végül fel is
fogja valaki szabadítani, annak egy szemléletes módja az, ha valakihez
hozzárendeljük a felelősséget. Például a int* fuggveny(void)
függvény dokumentációjában leírjuk azt, hogy a visszaadott pointer egy
olyan memóriaterületre mutat, ahol az eredmény van; és hogy azt később
a hívó dolga lesz felszabadítani, ha már nincsen rá szüksége. Ezzel
azt jelöljük ki, hogy ki annak az erőforrásnak a tulajdonosa.
Maga a malloc()
–free()
függvénypáros
is ilyen! A malloc()
lefoglal egy területet, és az a program
tulajdonába kerül. Felelőssé válik is ezáltal azért, hogy később a free()
függvényt meghívja.
12 calloc()
, realloc()
void *calloc(size_t darabszám, size_t egyelem)
- Lefoglal egy tömböt és kitölti 0 bájtokkal
- Az első paraméter egy elem mérete
- A második a tömb elemszáma
- Pl.
int *tomb=(int*) calloc(100, sizeof(int));
- Vigyázat! Ez nem biztos, hogy jó nekünk! Pl. a
NULL
pointer lehet, hogy nem csupa 0 bájtokból áll.
void *realloc(void *ptr, size_t méret)
- A
ptr
-rel megadott memóriaterületet újrafoglalja - Az új mérete a második paraméter
- A meglévő adatokat másolja
- Ha nagyobb lett, a többi memóriaszemét, ha kisebb, a vége elveszik
13 malloc()
, free()
– kérdések
- Hogyan lehet lekérdezni, hogy hány elemű tömböt foglaltunk?
- Sehogyan, nekünk kell tudni.
- Hogyan lehet ellenőrizni, hogy egy adott terület le van-e foglalva?
- Sehogyan, nekünk kell tudni.
Ezek nagyon fontosak. Különösen nem szabad egy dinamikus tömbön a
sizeof
operátort használni – hiszen az nem a tömb méretét, hanem a pointer méretét fogja megadni!
- Szabad nulla méretű területet foglalni?
- Szabad. Kapunk egy NULL vagy egy nem NULL pointert.*
-
Szabad
free(NULL)
hívást csinálni? - Szabad.*
* A legutóbbiak nem voltak mindig így. A C szabvány 1990-es verziója (C90) előtt
gyakran volt az, hogy régebbi fordítók által generált programok lefagytak egy
free(NULL)
hívás hatására. Ma azonban már a szabvány előírja, hogy
kezeljék ezt az esetet. Ennek ellenére előfordul manapság is az, hogy ez
problémát jelent egyes fordítók és C könyvtárak esetén.
Azt nem írja elő, hogy a nulla méretű terület foglalásakor mi kell történjen. Vagy
kapunk egy nem NULL pointert, amelyet azonban dereferálni nem szabad (hiszen ott
nulla méretű adat van), vagy egy NULL pointert (akkor meg azért nem). A szabvány
ezt a fordítókra bízza. Ellenben ha a pointer nem NULL, akkor fel is kell szabadítanunk
azt (a nulla méretétől függetlenül), úgyhogy emiatt kényelmes az, hogy a free()
elvileg hívható NULL
paraméterrel is.
14 A DinamikusTömb
Tartsuk nyilván, mekkora a legfoglalt terület.
A méret és a pointer összetartozó adat – rakjuk struktúrába! Hiszen éppen erre való a struktúra: tartsuk egy helyen, ami összetartozik.
struct DinTomb { double *adat; int meret; };
DinTomb dt; dintomb_foglal(&dt, 100); // foglal dt.adat[34]=19; // használ dintomb_kiir(&dt); dintomb_felszabadit(&dt); // felszabadít
Amikor létrehozzuk a dt
nevű struktúrát, akkor a
tömbnek természetesen nem lesz memória lefoglalva, mert a
struktúrában csak egy pointer és egy integer van. Ezért a foglaláshoz
írunk egy függvényt: ennek feladata a memóriaterület lefoglalása, és
az adatok beírása a struktúrába. (Mivel módosítja a struktúrát,
ezért arra mutató pointert kell átvegyen.)
Ezután már használhatjuk a tömböt. A struktúra előnye itt is látszik: egy ilyen tömböt egyetlen paraméterrel át tudunk adni. Nem kell megadnunk a pointert és a méretet is, hanem elég csak a struktúrát megmutatni, hiszen abban benne van minden információ.
int dintomb_foglal(DinTomb *dt, int meret) { dt->adat=(double*) malloc(meret*sizeof(double)); if (dt->adat==NULL) return 0; // nem sikerült dt->meret=meret; return 1; // sikerült } void dintomb_kiir(DinTomb const *dt) { int i; for (i=0; i<dt->meret; ++i) printf("%f ", dt->adat[i]); printf("\n"); } void dintomb_felszabadit(DinTomb *dt) { free(dt->adat); }
A kiíró függvény ugyan nem kellene pointerével átvegye a struktúrát
(mert úgysem akarja megváltoztatni), de egyszerűbb így megoldani.
Nem kell majd fejben tartani, hogy melyik függvény várja értékként
és melyik címként a paramétert, hanem mindegyik címként, így mindegyiknél
ki kell írni a &
operátort. Ez azoknak kényelmes, akik
használják a függvényt.
16 A három memóriaterület
A programunk változói három memóriaterületen vannak.
- Globális változók memóriaterülete,
- Verem (stack): lokális változók memóriaterülete,
- Kupac (heap, free store): dinamikusan foglalt memóriaterület.
A definíció helyétől függ, hogy egy változó hol jön létre.
A pointerek bármelyik memóriaterület változóira tudnak mutatni.
Persze, hogy ott jön létre, ahol definiáljuk. A fordító csak követi azt, amit beírtunk. Mivel az egyes helyeken megadott változók élettartama eltérő, ezért a definíció az élettartamot is meghatározza!
Természetesen a fentiek nem azt jelentik, hogy a számítógépben valójában is többféle memória van. Csak annyit, hogy ugyanannak a memóriának egyes részeit máshogyan kezeljük, és más célból teszünk oda változókat.
A három memóriaterület mérete változik is. A globális
adatterületen mindig ugyanannyi adat van. A veremben lévő adatok
mennyisége függvényhívásokkor nő, visszatéréskor pedig csökken. (Így
aztán pl. a main()
változói mindig ott vannak a
„legalján”.) A kupac mérete is változik, a lefoglalt-felszabadított
területek nagyságától függően.
17 A globális memóriaterület
Itt vannak a globális változók. Ugyancsak itt vannak a névtelen sztring konstansok is.
int x=7; char sz[15]="szöveg "; char *p; int main() { p=sz; printf("Helló világ"); return 0; }
A globális változók a program futása alatt mindvégig léteznek.
Globális változónak számítanak a memóriakezelés szempontjából a
függvények static
kulcsszóval jelzett változói is. Azok is a globális memóriaterületen helyezkednek
el, és léteznek akkor is, miután visszatérünk a függvényből.
18 A verem (stack)
A verem legtetején jönnek létre a lokális változók, és szűnnek meg, ha visszatérünk a függvényekből.
void fv() { char sz[10]="HELLÓ"; char *p="helló"; // ! } int main() { int x=7; fv(); }
A veremben mindig a legfelső adatokat látjuk – emiatt lehetséges a rekurzió.
Hoppá! A char* p
pointer által mutatott sztring
a globális memóriaterületre került. Miért? Azért, mert a fordító
csinálja, amit mondtunk. A p
nevű lokális változó
típusa char*
, azaz a vermen létrehozott egy pointert.
A sztring konstansok globális memóriaterületre kerülnek, ezért
az a kisbetűs „helló” sztring oda került. Ezzel szemben
az sz
változó típusa char[15]
, vagyis egy
karaktertömb. Mivel így definiáltuk, az egész nagybetűs
„HELLÓ” a verembe került. A p
pointernél azt mondtuk, hogy
legyen egy pointer, ami arra a sztringre mutat. Az sz[]
tömbnél pedig azt, hogy legyen egy tömb, aminek a tartalma az.
Emiatt egyébként a két tömb mérete eltérő! A névtelen tömb, amelyik
a globális területen van (attól még, hogy van egy pointer, amelyik
mutat rá, névtelen!), egy hat karakterből álló tömb, hiszen öt
plusz egy lezáró nulla. Az sz[]
tömb ezzel szemben
tíz karakterből áll, mert akkorának definiáltuk.
A veremben a fehér színű terület tartozik a main()
függvényhez, a szürke színű pedig az fv()
függvényhez.
Ha az fv()
függvény visszatér, a szürke színű rész
változói eltűnnek. Így a veremben lévő adatok mennyisége
folyamatosan változik a függvényhívások és visszatérések során.
Az „örökké” létező globális változókkal szemben ezek csak addig
léteznek, amíg a függvény belsejében vagyunk. Ugyanazon függvény
újbóli meghívása esetén újra létrejönnek, újra inicializálódnak
(ha van megadva kezdeti érték) – tulajdonképpen azok már másik
változók.
19 sizeof(s)
és strlen(s)
definíció | sizeof(str) | strlen(str) |
---|---|---|
char str[50]="hello" | 50 | 5 |
char str[20]="hello" | 20 | 5 |
char str[]="hello" | 6 | 5 |
char *str="hello" | gépfüggő! | 5 |
int sztring_hossz(char *s) { ... } printf("%d", sztring_hossz("hello"));
Ezért nézzük a sztring méretét
az strlen()
függvénnyel! A sizeof
a tömb vagy a pointer méretét adja.
Az utolsó sorban a pointer mérete látszik – ez géptípusonként más.
20 A kupac (heap)
Innen foglalódik le az a memóriaterület,
amit a malloc()
-tól kapunk.
int main() { int *t; char *p; t=(int*) malloc(100*sizeof(int)); t[0]=3; t[1]=6; p=(char*) malloc(20*sizeof(char)); strcpy(p, "helló"); …
21 Hibalehetőség: érvénytelen pointerek
Érvénytelen pointerek (dangling pointer): olyan pointerek, amelyek már megszűnt változóra mutatnak.
Mutató felszabadított memóriaterületre:
int *ptr=(int*) malloc(100*sizeof(int)); ptr[2]=12; free(ptr); printf("%d", ptr[2]); // hibás!!!
A felszabadítás után a ptr
érvénytelen
pointerré válik. Ilyenek lesznek a programjainkban sokszor,
nincsen velük baj; az a baj, ha rajtuk keresztül a már megszűnt
változót megpróbáljuk elérni. Erre ugyanaz vonatkozik, mint
a lentire: a nagy baj az, hogy könnyen előfordulhat, teszteléskor
nem vesszük észre a hibát. Mert ha az a memóriaterület még nem íródott
felül, akkor kiolvasva azt az adatot kapjuk, amit látni szeretnénk.
Mutató lokális változóra – visszatéréskor megszűnik a változó!!!
int *fv(void) { int i=7; return &i; // hibás!!! } int *ptr=fv();
Ezt sem biztos, hogy észrevesszük tesztelésnél. Az adott memóriaterületen később még lehet, hogy ott lesz az érték, amire számítunk, és akkor látszólag a program helyes. Némely fordítók szerencsére néha felfedezik ezt a hibát. Pl. a gcc kimenete a fenti példára:
asd.c:4:4: warning: function returns address of local variableUgyanezért hibás a következő
beolvas()
függvény is. A tömb a veremben van, és megszűnik,
ha visszatérünk a függvényből, hiába olvastunk be akármit a
billentyűzetről:
char *beolvas(void) { char s[200]; fgets(s, 200, stdin); return s; // hibás!!! }
Ezek nagyon súlyos programozási hibák.
22 Visszatérés dinamikusan foglalt területtel
int *beolvas_sokszam(int mennyi) { int *uj; uj=(int*) malloc(mennyi*sizeof(int)); … return uj; } int main() { int *s; s=beolvas_sokszam(5); … free(s); // !
Így lehet egyszerűen megoldani az előző dián említett problémát. Az a célunk, hogy ne tűnjön el a tömb, ezért átvesszük a memória kezelését a fordítótól. (Saját felelősségre! Mert a felszabadítás is a mi dolgunk.)
23 És ezek vajon helyesek-e?
char *hello(void) { return "hello"; } printf("%s", hello());
Helyes. A sztring globális memóriaterületen van, a függvényből visszatérés után is létezni fog. Szabad hivatkozni arra a memóriaterületre!
int *hello(void) { static int tomb[5] = { 9, 4, 5, 6, 1 }; return tomb; } printf("%d", *hello());
Helyes. A statikus változó megmarad a függvényből visszatérés után, vagyis az élettartama szempontjából majdnem úgy viselkedik, mintha globális változó lenne.
int *hello(void) { int *tomb = (int*) malloc(10*sizeof(int)); tomb[0] = 12; return tomb; } printf("%d", *hello());
Helytelen! Bár megmarad, és szabad hivatkozni,
de felszabadítani ki fogja?! Ha nincs elmentve az a pointer, ami hivatkozik
a memóriaterületre, akkor nem tudjuk odaadni a free()
-nek,
és semmi módja nincsen a felszabadításnak! Sőt a függvény minden meghívásakor
újabb memóriaterület lesz lefoglalva. Előbb-utóbb a programunk sok száz
megabájt memóriát fog magánál tartani!
25 Emlékeztető – több dimenziós tömbök
Több dimenziós tömb
int tomb[3][4]; // magas, széles tomb[1][2]=9; // y, x
széles*magas
darab integer!
A fordító leképezi egydimenziós tömbre, amiben az elemek sorfolytonosan helyezkednek el.
Így akárhány dimenziós lehet. A tömb mérete szélesség*magasság
integernyi.
Leképezés: tomb1d[y*szélesség+x]
.
Egy sor a tömbből
int osszeg_1d(int *tomb, int meret); int tomb[3][4]={ {…}, {…}, {…} }; // 3 sor, 4 oszlop printf("%d", osszeg_1d(tomb[1], 4)); // 1. sor: 1D tömb, 4 elemű
A 2D tömb egyik sora: az egy 1D tömb!
26 Több dimenziós – átadás függvénynek
int osszeg_2d(int tomb[3][4]) { // 3×4-es tömb int sum, x, y; sum=0; for (y=0; y<3; ++y) // fix szélesség és magasság for (x=0; x<4; ++x) sum+=tomb[y][x]; return sum; }
int osszeg_2d(int tomb[][4], int magassag); int osszeg_2d(int (*tomb)[4], int magassag); // mindkettő jó
Nem lehet a tömb akármilyen széles! Az y*szélesség+x
leképezés
miatt a fordítónak a típusból ismernie kell a szélességet.
int osszeg_2d(int tomb[][], int szeles, int magas); // ez ROSSZ
Akármelyik formában adjuk át a tömböt, a verembe a
függvény számára mindig csak egy pointer kerül! Még a legelső esetben is,
ahol mindkét dimenziót kiírjuk. (Annak az esetnek az a furcsasága, hogy a
3-mal a fordító nem is foglalkozik. Annyira nem, hogy akármilyen magas, 4
elem széles int
tömbbel hívható az a függvény!)
27 Összetett deklarációk
„A deklaráció a használatra hasonlít!” (declaration resembles use)
A C a változók, függvények és függvényparaméterek deklarációjakor ugyanazokat
a precedenciaszabályokat érvényesíti, amelyeket a programkód elemzésekor is használ.
Ebből az következik, hogy a deklarációnál pont ugyanúgy kell megadnunk a tömböket,
pointereket, mint ahogyan azokat használjuk. Ezért kell egy tömböt int t[10]
alakban megadni: mivel ha a tömb neve mögé tesszük az indexelő operátort, akkor
kapjuk meg a benne lévő egész számot; és ezért kell a pointert int *p
alakban
megadni, mivel a pointer neve elé tett *
operátorral tudjuk dereferálni azt,
kapva az egész számot.
int *valami[4] | deklaráció |
---|---|
*valami[4] | egy egész |
valami[4] | egészre mutató pointer |
valami | 4 elemű, egészre pointerekből álló tömb |
Az első táblázat int *valami[4]
deklarációja azt jelenti, hogy a
valami
nevű változót *valami[i]
formában használva egy
int
típushoz jutunk (ahol i valamilyen index). Mivel az indexelő operátor []
precedenciája magasabb, mint az indirekció *
operátoráé, ez azt jelenti, hogy
az indexelés vonatkozik a változóra, a *
pedig már az így kapott dologra.
Vagyis ha a valami
-t megindexeljük, akkor kapunk egy dereferálható
értéket (egy pointert), amit dereferálva pedig egy egész számhoz jutunk. A valami
így egy tömb kell legyen, ami int*
mutatókat tartalmaz: pointerekből álló tömb.
int (*valami)[4] | deklaráció |
---|---|
(*valami)[4] | egy egész |
*valami | 4 elemű tömb egészekből |
valami | pointer 4 elemű egész tömb(ök)re |
A zárójelezéssel tudjuk a precedenciaszabályok „ellenében”
módosítani azt, hogy mely operátorok mely operandusokra vonatkoznak. A fenti deklaráció
azt jelenti, hogy ezt a valami
-t úgy kell használni, hogy előbb dereferáljuk,
utána pedig indexeljük, és így kapunk majd egész számot. Szóval ez a valami
egy pointer kell legyen egy olyan helyre a memóriában, ahol indexelhető dolgok (tömbök)
vannak, amiket indexelve egészeket kapunk. Ez egy pointer egy négy elemű tömbre,
vagy esetleg négy elemű tömbökre (első négy elemű tömb, második négy elemű tömb
és így tovább.) Ezért a int (*tomb)[4])
a függvény paramétere az előző
dián: a két dimenziós tömb 4 hosszúságú sorairól van szó.
Összetett deklarációk létrehozásához és értelmezéséhez jól használható a cdecl nevű program, amely elérhető online verzióban is http://cdecl.org/ helyen.
28 Sztringek tömbje – tömbök tömbje
char sztringek[3][12] = { "hello vilag", "almafa", "hexdump!" };
12 karakteres tömbökből álló tömb.
A memória bájtjai a sztringek[]
tömbnél:
0000 68 65 6c 6c 6f 20 76 69 6c 61 67 00 hello vilag. 000c 61 6c 6d 61 66 61 00 00 00 00 00 00 almafa...... 0018 68 65 78 64 75 6d 70 21 00 00 00 00 hexdump!....
Összetett deklarációt alkotunk akkor is, amikor sztringek tömbjét hozzuk
létre. Ha egy sztring 12 bájtból áll: char sztring[12]
,
akkor a sztringek tömbje valami olyasmi kell legyen, amit ha megindexelünk, akkor
egy 12 bájtos karaktertömböt kapunk: char sztringek[3][12]
.
Természetesen az első indexeléssel a sztringet választjuk ki ebből, egy második
indexelés adná a karaktert; ezért a deklarációban az első méret a sztringek száma,
a második pedig a sztringek mérete.
A típust lépésenként is megadhatjuk a typedef
kulcsszó használatával:
typedef char Sztring[12]; typedef Sztring SztringTomb[3]; SztringTomb sztringek;
29 Sztringek tömbje – pointerek tömbje
char *sztringek[3] = { "hello vilag", "almafa", "hexdump!" };
Pointerekből álló tömb. A sztringek maguk (a tartalom) globális memóriaterületre kerül, és független a pointereket tároló tömbtől!
A memória bájtjai a sztringek[]
tömbnél:
0000 22 00 00 3c "..< 0004 2e 00 00 3c ...< 0008 35 00 00 3c 5..<
Itt egy olyan típust alkottunk, aminek a használata
nagyon hasonló az előzőéhez; megindexelve egy pointert kapunk, ami akár
egy nullával lezárt karaktertömbre is mutathat. Ebben az esetben azonban
nem egy kétdimenziós tömbről van szó, amely a memóriában sorfolytonosan
helyezkedik el, hanem egy olyan tömbről, amely pointereket tartalmaz. Ha
megnézzük, mit találunk a memóriában a sztringek
változó
helyén, akkor ott a szövegeket nem fogjuk megtalálni, csak három
darab pointert! (A sztringek ilyenkor globális területre kerülnek.)
30 2D dinamikus tömbök – módszerek
Nézzük meg, hogyan lehet két dimenziós, dinamikus tömböket létrehozni, amelyeknek a szélessége és a magassága is tetszőlegesen megválasztható! (Ugyanezek a gondolatmenetek általánosíthatóak bármennyi dimenzióra.)
0. módszer: 1D leképezés
Mi magunk végezzük el a leképezést. Ezzel egyszerűen kikerüljük a problémát. Megold mindent, de sajnos nehézkes használni. (Mindenhol a szorzós képletet kell beírni az indexbe.)
double *szamok; szamok=(double*) malloc(szelesseg*magassag*sizeof(double)); szamok[y*szelesseg+x]=3.14; // szamok[y][x] helyett :( fv(szamok, szelesseg, magassag); // fv(double*, int, int) free(szamok);
1. módszer: soronkénti foglalás
Minden sort külön foglalunk le. Az egyes sorokhoz tartozó pointereket egy pointertömbbe
tesszük, amelyet szintén dinamikusan foglalunk (hogy a magasság is tetszőleges legyen). Ha így
teszünk, a lefoglalt tömböt a megszokott módon használhatjuk, két egymás utáni indexeléssel, pl.
szamok[2][3]
. Az első indexelés a pointerek tömbjéből (a rajzon a függőleges)
kiválaszt egy pointer, a második indexelés pedig már az ezáltal mutatott tömbön történik (a
rajzon valamelyik vízszintes).
Természetesen a pointerek tömbjét kell előbb foglalni, utána a sorokat; mert az előbbibe
tesszük be a sorok pointereit. A felszabadításnál ugyanez visszafelé történik: amíg a sorokat
szabadítjuk fel, addig szükségünk van a pointerekre, ezért a pointerek tömbje lesz az,
legutoljára szabadítunk fel. A módszer hátránya az, hogy sok malloc()
hívás kell
hozzá, ami lassabb, mintha csak egy vagy kettő lenne.
double **szamok; /* foglalás */ szamok=(double**) malloc(magassag*sizeof(double*)); for (y=0; y<magassag; ++y) szamok[y]=(double*) malloc(szelesseg*sizeof(double)); /* használat */ szamok[y][x]=3.14; fv(szamok, szelesseg, magassag); // fv(double**, int, int) /* felszabadítás */ for (y=0; y<magassag; ++y) // ahány malloc, annyi free free(szamok[y]); free(szamok); // ezt legutoljára
2. módszer: leképezés és pointertömb
Az öszvér. Az egész tömböt (a „kilapított”, sorfolytonos leképezést) egy
malloc()
hívással foglaljuk, mint a nulladik módszernél. Ezen kívül fogunk
egy másik tömböt, amely pointerekből áll, mint az első módszernél. Ezek a pointerek a kilapított tömb belsejébe
mutatnak, mégpedig oda, ahol a két dimenziós tömb leképezett sorainak elejei vannak. Így
ha megindexeljük a pointerekből álló tömböt (amely a középső a rajzon), egy pointert kapunk,
amely a kilapított tömb belsejébe mutat; azt megindexelve megkapjuk a keresett elemet.
Ez a módszer gyorsabb foglalást eredményez, mint az előző, mert mindössze
két darab malloc()
hívásra van hozzá szükség. A foglalás első lépése a pointertömb
foglalása, második lépése az elemek tömbjének foglalása, a harmadik lépése pedig a kilapított
tömb belsejébe mutató pointerek kiszámítása egy ciklusban.
double **szamok; /* foglalás */ szamok=(double**) malloc(magassag*sizeof(double*)); szamok[0]=(double*) malloc(szelesseg*magassag*sizeof(double)); for (y=1; y<magassag; ++y) szamok[y]=szamok[0]+y*szelesseg; /* használat */ szamok[y][x]=3.14; fv(szamok, szelesseg, magassag); // fv(double**, int, int) /* felszabadítás */ free(szamok[0]); // ahány malloc, annyi free free(szamok);
31 2D dinamikus tömb – melyiket válasszuk?
Táblás játék: állandó a szélesség, így ez egyszerűbb, gyorsabb.
Sztringek tömbje: a méret eltérő lehet, és külön újrafoglalhatóak!
32 Többszörös indirekció – mutató mutatóra
A cím szerinti paraméterátadás működése:
void novel(VALAMI *ptr) { // megnöveli az átvett változót (*ptr)++; }
A VALAMI
típus akár egy mutató is lehet:
void novel(char **ptr) { (*ptr)++; } char *szoveg="hello"; novel(&szoveg); printf("%s", szoveg); // „ello”
Pointerre mutató pointer – a 2D dinamikus tömböknél már használtuk.
33 Többszörös indirekció – 2D tömb
Írjunk függvényt, amely cím szerint átvett paraméterben adja vissza a lefoglalt 2D dinamikus tömbre mutató pointert!
double **tomb; foglal(&tomb, 10, 20); // double** változó címe: double***
A tomb
változó itt egy pointer. De attól még az csak egy sima
változó, amelynek címe is van a memóriában. Ha a típusa double**
,
akkor a címének típusa double***
– és ez lesz az őt cím szerint
átvevő függvény paraméterének típusa.
void foglal(double ***ptomb, int szeles, int magas) { double **uj; int y; uj=(double**) malloc(magas*sizeof(double*)); for (y=0; y<magas; ++y) uj[y]=(double*) malloc(szeles*sizeof(double)); *ptomb = uj; // *ptomb ← double** }
A ptomb
változó egy pointer a fenti double**
típusú
változóra. Vagyis ha dereferáljuk: *ptomb
, akkor hivatkozni tudunk
arra a változóra, amelyet megkaptunk cím szerint. Ide másoljuk be a foglalt
pointertömb kezdőcímét, vagyis az uj
pointer értékét.