Pointerek a C nyelvben
Ez a Mű a Creative Commons Nevezd meg! - Ne add el! - Ne változtasd! 4.0 Nemzetközi Licenc feltételeinek megfelelően felhasználható.
Tartalom
- Bevezetés
- Mik azok a mutatók?
- Cím szerinti paraméterátadás
- A tömbök és mutatók kapcsolata
- Mutató struktúrára
- Gyakori hibák
Bevezetés
Az alábbi cikkben igyekszem minél érthetőbben elmagyarázni mindazt, amit a C programozási nyelvben a pointerekről, azaz a muatókról tudni kell. Feltételezem, hogy az olvasó tisztában van a következő dolgokkal:
- A primitív adattípusok a C nyelvben
- Struktúrák
- Standard kimenetre írás, standard bemenetről olvasás
- Függvények, paraméterátadás
Mik azok a mutatók?
Először nézzük meg, mi is az a memória. Nem kell bonyolult dologra gondolni, képzeljünk el egy nagyon hosszú szalagot, amin több milliárd kis cella van. Minden cellába 8 bitet írhatunk, azaz 1 bájtot. Egy ilyen cella, azaz bájt elegendő, hogy ott eltároljunk egy char típusú változót. Kettő, egymás melletti bájttal egy short-ot, néggyel egy intet, és így tovább.
Minden egyes bájtnak van egy sorszáma, pontosabban címe. A legelső címe 0, majd egyesével nőnek a címek. 32 bites rendszerben ez $2^{32}$, azaz 4294967296 különböző cím, tehát 0-tól 4294967295-ig terjednek a címek. 64 bites rendszernél pedig $2^{64} = 18446744073709551616$ címünk lehet. Ez azért elég kell legyen egy ideig . A változók a C-ben pedig nem mások, mint amolyan szimbólumok, amelyek a memória egy meghatározott részére hivatkoznak. Ha alacsonyabb szintű nyelvet használunk, mondjuk assembly-t, akkor ott nincsenek változók, ott már a memória címeket kell kezelnünk, ami valljuk be, elég kényelmetlen, és sok hibára ad lehetőséget. A C, és más magasabb szintű nyelvek megalkotói ezért is vezették be a változókat: Nem kell már közvetlenül a memóriával foglalkozni, elég int, double és más változókban gondolkodni. Tehát, ha például létrehozunk egy int típusú változót, akkor elég, ha majd a fordítóprogram foglalkozik azzal a problémával, hogy a kész program azt hova helyezze el a memóriába, mi csak kényelmesen leírjuk a változó nevét, ebből már a fordító tudja, hogy melyik memóriadarabkára kell gondolnia.
1 2 3 4 5 6 7 8 |
int main() { /* * Az a változó most 4 bájtot foglal el a * memóriában. */ int a = 2; return 0; } |
Tegyük fel, hogy valamiért (erre majd később kitérek) szükségünk van arra, hogy eltároljuk egy változó címét. Erre természetesen egy másik változót kell használnunk, nade mi legyen annak a típusa? Mivel a címek 0-tól indulnak, és 1-esével nőnek, valamilyen előjel nélküli egészre lesz szükségünk. Érdemes az unsigned long-ot használni, mivel 32 bites környezetben általában ez 32 bites, 64 bites esetben pedig 64 bites. Azonban a C megalkotói ennél jobbat találtak ki. Az olyan változókat, amelyek egy memóriacímet tárolnak, nevezzük mutatóknak, azaz pointereknek. A pointer típusa attól függ, hogy milyen típusú változó címét tároljuk benne. Az alábbi példában egy int típusú változó címét tároljuk el a pa változóban:
1 2 3 4 5 |
int main() { int a = 2; int * pa = &a; return 0; } |
Itt a 3. sorban hoztuk létre a mutató változót. Mivel az a változó címét szeretnénk eltárolni, megnézzük, hogy annak mi a típusa, ez most int. Ezért a pa típusa pedig int * lesz, azaz a mutatott változó típusát leírjuk, és hozzáadunk egy *-ot. Később, ha ránéznk a pa-ra, látjuk, hogy a típusa int *, ebből tudjuk, hogy ő egy int-re hivatkozik. Az értékadás jobb oldalán azt látjuk, hogy &a. Az & ez esetben az un. címképző operátor. Ezt az operátort egy változó elé írva megkapjuk annak a változónak a címét. Pontosabban, a & <változó> kifejezés a változó címét adja vissza, a kifejezés eredményének típusa pedig <változó típus> * lesz.
A fenti program tehát 2 változót tartalmaz, és értelemszerűen mindkettőnek van helye a memóriában, azaz a pa-nak is. Tehát akkor az ő memóriacímét is lekérdezhetjük, illetve eltárolhatjuk egy harmadik változóban. A kérdés csak az, hogy ennek a változónak mi lesz a típusa? Ugyanazt a szabályt kell alkalmazni, mint mikor a pa-t deklaráltuk: Megnézzük, hogy a mutatott változó (most pa) típusa micsoda, ez most int *. Ezt a típust ellátjuk egy további *-al:
1 2 3 4 5 6 |
int main() { int a = 2; int * pa = &a; int ** ppa = &pa; return 0; } |
A fenti ábrán láthatjuk a megoldást, mégpedig a 4. sorban. A pa címét szintén a & operátorral kérdezzük le. Tehát most van 3 változónk: A ppa értéke a pa változó címe. A pa változó értéke pedig az a változó címe. Az a változó értéke pedig 2, amit nem értelmezünk címként.
Ha csak a pa változót használhatjuk, akkor hogyan tudjuk lekérdezni az a értékét? A pa értéke ugye az a címe, ez esetben az un. indirekció operátorra van szükségünk:
1 2 3 4 5 6 7 8 9 10 |
#include <stdio.h> int main() { int a = 2; int * pa = &a; int ** ppa = &pa; printf("a erteke: %d\n", *pa); printf("a erteke: %d\n", **ppa); return 0; } |
A 7. sorban a pa mutatón keresztül kérdezzük le az a értékét, mégpedig a * indirekció operátorral. Tehát a *pa kifejezés jelentése: Annak a változónak az értéke, amire a pa mutat. Hasonlóan a **ppa kifejezést is értelmezhetjük: Az átláthatóság kedvéért zárójelezzük ezt a kifejezést: *(*ppa). Tehát először a *ppa kifejezést értékeljük ki, ennek jelentése: annak a változónak az értéke, amire a ppa mutat. Ez pedig a pa változó. Erre alkalmazzuk végül a zárójelen kívüli *-ot, ennek a jelentése pedig: az a változó, amire a pa mutat, vagyis az a.
Cím szerinti paraméterátadás
Jogos a kérdés, hogy mire is jó mindez? A helyzet az, hogy a C nyelvben a mutatókat elég nehéz megkerülni, erre az első példa a cím szerinti paraméterátadás. Feltételezem, hogy az olvasó már írt egyszerűbb függvényeket C-ben, amelyeknek paramétert is adott át:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <stdio.h> void foo(int b) { b++; printf("1-el novelt ertek: %d\n", b); } int main() { int a = 1; foo(a); foo(a); foo(a); printf("a erteke: %d\n", a); return 0; } |
Ebben a példában a foo függvény b nevű belső változója mindig a main-ben lévő a változó értékét veszi fel. Hiába változtatjuk b-t a foo-n belül, annak semmilyen hatása nem lesz az a-ra, mivel ez két különböző változó, amelyek a memória két különböző pontján találhatóak. Emiatt a program kimenete a következő lesz:
1-el novelt ertek: 2 1-el novelt ertek: 2 1-el novelt ertek: 2 a erteke: 1
Mit tehetünk, ha az a szándékunk, hogy a foo függvényen belül az eredeti, main-ben lévő a-t szeretnénk módosítani? Erre használhatjuk a mutatókat. Úgy kell módosítani a foo paraméterezését, hogy most ne egy int-et, hanem egy int*-ot kapjon, azaz egy olyan értéket, ami egy változó címe lesz:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <stdio.h> void foo(int * b) { (*b)++; printf("1-el novelt ertek: %d\n", *b); } int main() { int a = 1; foo(&a); foo(&a); foo(&a); printf("a erteke: %d\n", a); return 0; } |
A főprogramban most a foo függvényt úgy kell paraméterezni, hogy nem a értékét, hanem annak címét adjuk át, ezért használjuk a & címképző operátort (10-12. sorok). A foo formális paramétere pedig int * b, azaz egy mutató. A 4. sorban lévő kifejezés jelentése: Először kiértékeljük a *b kifejezést, azaz arra a változóra hivatkozunk, amire b mutat, ebben a példában ez most a main-ben található a. Tehát a (*b)++ jelentése: Növeljük meg azt a változót 1-el, amire a b mutat.
A scanf függvény is így működik: Ha valamilyen primitív változóba (int, double, float, stb) olvasunk be, akkor ezért kell a & címképző operátort alkalmazni. A scanf-nek szüksége van annak a változónak a memóriacímére, amelybe majd a beolvasott értéket el kell mentenie.
A tömbök és mutatók kapcsolata
Tegyük fel, hogy van egy 10 elemű int tömbünk, és szeretnénk lekérdezni a 0. elemének címét. Ezt két módon is megtehetjük:
1 2 3 4 5 6 7 |
int main() { int tomb[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int * mutato1 = &tomb[0]; int * mutato2 = tomb; return 0; } |
A 4. sorban az a megoldás szerepel, amit az eddigi tudásunk alajpán használnánk: Hivatkozunk a tömb 0. elemére, majd annak címét lekérdezzük a címképző operátorral. Viszont az. 5. sorban lévő megoldás is pontosan ezt eredményezi. Ha egy tömb nevét önmagában leírjuk, akkor az a kifejezés a tömb 0. elemének címét adja vissza. Tehát a fenti példában a mutato1 és mutato2 változók értékei megegyeznek, mindketten a tömb 0. elemére hivatkoznak.
A mutatókat arra is használjuk, hogy egész értékekkel eltoljuk őket. A legegyszerűbb példa erre a mutató 1-el való növelése:
1 2 3 4 5 6 7 8 9 10 11 |
#include <stdio.h> int main() { int tomb[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int * mutato = tomb; printf("A mutatott ertek: %d\n", *mutato); mutato++; printf("A mutatott ertek: %d\n", *mutato); return 0; } |
A kód kimenete a következő:
A mutatott ertek: 1 A mutatott ertek: 2
A tömb elemei most 4 bájtosak, tehát a tömb 1. eleme a 0. elem után 4 bájttal következik a memóriában. Viszont a mutatónkat 1-el növeltük. Ez első ránézésre furcsának tűnhet, de valójában az történik, hogy valójában nem 1 bájttal léptetjük a címet, hanem 4*1-el, azaz az egység mindig a mutatott változó mérete. Emiatt nem kell aggódnunk amiatt, hogy olyan mutatót hozunk létre, amivel egy tömb két szomszédos eleme között átfedést tudunk csinálni. Hasonlóan működik a -- operátor is, illetve tetszőleges egészt is hozzáadhatunk a mutatóhoz:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <stdio.h> int main() { int tomb[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int * mutato = tomb; printf("A mutatott ertek: %d\n", *mutato); mutato++; printf("A mutatott ertek: %d\n", *mutato); mutato += 3; printf("A mutatott ertek: %d\n", *mutato); mutato--; printf("A mutatott ertek: %d\n", *mutato); return 0; } |
Kimenet:
A mutatott ertek: 1 A mutatott ertek: 2 A mutatott ertek: 5 A mutatott ertek: 4
Ha a mutatót kedvünkre tologathatjuk, megtehetjük-e ezt magával a tömbbel? Hiszen eddig úgy tűnik, a tomb kifejezés a fenti kódban mutatóként is működik. Nézzük az alábbi kódot:
1 2 3 4 5 6 7 8 9 |
#include <stdio.h> int main() { int tomb[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; tomb++; printf("A tomb 0. eleme: %d\n", tomb[0]); return 0; } |
Ha ezt megpróbáljuk lefordítani, a következő hibaüzenetet kapjuk:
pelda10.c: In function ‘main’: pelda10.c:6:6: error: lvalue required as increment operand tomb++; ^
A probléma a tomb++ kifejezés: Növelni csak változót tudunk, maga a tomb pedig nem az, csupán egy szimbólum, ami egy tömböt reprezentál. Viszont az alábbi kód működik, hiszen itt a tomb+1 kifejezésnek nincsen olyan mellékhatása, hogy megváltozik a tomb mint "változó", hanem kapunk egy memóriacímet, ami a tömb 1. elemére hivatkozik:
1 2 3 4 5 6 7 8 9 |
#include <stdio.h> int main() { int tomb[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int * mutato = tomb + 1; printf("A mutatott ertek: %d\n", *mutato); return 0; } |
Kimenet:
A mutatott ertek: 2
Ebből következik, hogy a tömb elemeire így is hivatkozhatunk:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <stdio.h> int main() { int tomb[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int * mutato = tomb; printf("A 3. elem: %d\n", tomb[3]); printf("A 3. elem: %d\n", *(tomb + 3)); printf("A 3. elem: %d\n", mutato[3]); printf("A 3. elem: %d\n", *(mutato + 3)); return 0; } |
A kimenet minden kiírásnál ugyanaz:
A 3. elem: 4 A 3. elem: 4 A 3. elem: 4 A 3. elem: 4
Vagyis a tömb szimbólumon ugyanúgy alkalmazhatjuk a fent bemutatott pointer aritmetikát, mint a mutatókon, feltéve, hogy nem a ++, illetve -- operátorokat alkalmazzuk. Továbbá a mutatókat használhatjuk tömb szintaktikával is. Röviden összefoglalva tehát: Ha van egy tömböm, akkor a klasszikus tomb[i] indexelős kifejezéssel teljesen azonos kifejezés a következő: *(tomb + i), illetve tömb helyett ezt mutatókkal is eljátszhatjuk.
Nézzük az alábbi példát:
1 2 3 4 5 6 7 8 9 10 11 |
#include <stdio.h> int main() { int tomb[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int * mutato = tomb; mutato++; printf("A mutatott elem: %d\n", mutato[2]); return 0; } |
Itt a mutatót először beállítjuk, hogy mutasson a tömb 0. elemére (6. sor). A következő sorban 1-el eltoljuk a mutatót, tehát a tömb 1. elemére hivatkozik. A 8. sorban a mutato[2] kifejezés azzal egyenrangú, hogy *(mutato + 2), tehát még 2-vel eltoljuk, azaz összesen 3-al toltuk el a kezdeti memóriacímünket, azaz a 3-as indexű tömb elemre fogunk hivatkozni, ennélfogva a kimenet:
A mutatott elem: 4
Mutató struktúrára
A fenti példákban csupán primitív adattípusokra alkalmaztunk mutatókat, most nézzük meg mire kell figyelni, ha struktúráról, vagy unionról van szó. Nézzük az alábbi példát:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <stdio.h> typedef struct MetanHajtasuAutistaHarciHorcsog { int zetor; int beszorult; } MetanHajtasuAutistaHarciHorcsog; int main() { MetanHajtasuAutistaHarciHorcsog bela; bela.zetor = 100; bela.beszorult = 1; MetanHajtasuAutistaHarciHorcsog * mutato = &bela; mutato->zetor = 200; return 0; } |
Ebben a kódban található egy (minden bizonnyal valószínű) struktúra, a MetanHajtasuAutistaHarciHorcsog. Ebből csináltunk a 9. sorban egy változót, bela néven, ennek a címét pedig eltároltuk a mutato nevű mutató változóban. A fentiek alapján már tudjuk, hogy a mutato típusa MetanHajtasuAutistaHarciHorcsog * lesz. Ami viszont eddig ismeretlen volt számunkra az az, hogy hogyan hivatkozunk a mutatón keresztül a struktúra elemeire. Annyi a különbség, hogy a . operátor helyett most a -> operátort kell alkalmazni.
Gyakori hibák
A mutatók használatakor legyünk rendkívül körültekintőek, ugyanis ha olyan címet tárolunk el egy pointerben, ami nem a mi programunkhoz tartozik, akkor az végzetes hibát eredményezhet. Jó, senki nem fog belehalni (feltéve, ha nem a NASA-nak, vagy egy atomerőműnek fejlesztünk), ami általában előfordulhat ilyenkor az az, hogy elszáll a programunk. Ugyanis a modern operációs rendszerek nem engedik meg a felhasználó programjainak, hogy a memória akármelyik részéhez hozzáférjenek. Az érvénytelen hivatkozást az operációs rendszer érzékeli, majd a kérdéses programot leállítja. A legjobb példa erre az un. nullpointer használata:
1 2 3 4 5 6 7 |
#include <stdio.h> int main() { int * a = 0; printf("A mutatott ertek: %d\n", *a); return 0; } |
Ha egy sorban több pointert szeretnénk deklarálni, akkor tegyük ki mindegyik neve elé a *-ot. Az alábbi kód ezért hibás, itt az a változó int *, míg a b csak int:
1 2 3 4 |
int main() { int * a, b; return 0; } |
Irodalomjegyzék
- Brian W. Kernighan, Dennis M. Rithcie, A C programozási nyelv - Az ANSI szerint szabványosított változat
- Richard G. Beauchamp, Pointer (Comprehensive Owner's Guide)
A cikkben szereplő kódok letölthetőek innen: link.