9 Mutató Ha profi akarsz lenni a C nyelv használatában, nagyon tiszta és világos fogalmak szükségeltetnek a mutatókról. Sajnos ez a téma sok "újonc" fejében csak egy nagy sötét foltként jelenik meg, főleg azoknál, akik más nyelvekből (pl. Pascal, BASIC) érkeztek. Nekik szól ez a cikk. Hogy a lehető leghasznosabb legyen, fontosnak tartom, hogy a kódok minél több környezetben működjenek. Ezért megpróbáltam ragaszkodni az ANSI szabványhoz, hogy bármilyen ANSI C fordítóval le lehessen őket fordítni. Arra is ügyeltem, hogy a kód mindig jól elkülönüljön a szöveg többi részétől. Így egyszerűen csak bemásolod egy szerkesztőprogramba, elmented sima szövegként (ASCII) és már fordítható is. Javaslom az olvasónak, hogy tegyen így, ez segíteni fogja a cikk megértését.
9.1
Mi is az a mutató?
Azon dolgok egyike, amik nehéznek bizonyulnak a kezdők számára a C-ben. Azt tapasztaltam, hogy a mutatókkal kapcsolatos problémák a változók és kezelésük hiányos ismeretéből ered, ezért először ezekkel fogunk foglalkozni. A változó a programban egy olyan valami, aminek neve van, egy olyan érték, ami változhat. A fordító (compiler) és a szerkesztő (linker) ezt úgy kezeli, hogy hozzárendel egy blokkot a memóriából, ami ezt az értéket tárolja. Ennek a blokknak a mérete attól függ, hogy milyen határok között változhat a változó. Például, egy 32 bites PC-n az egész típus mérete 4 byte. A régebbi, 16 bites gépeken 2 byte volt. A C-ben egy változótípusnak, mint pl. ez egész, nem kell minden géptípuson ugyanolyan méretűnek lenni. Sőt mi több, nem is csak egyféle egész létezik a nyelvben. Lehet egész (int), hosszú egész (long), vagy rövid egész (short) is, ezeket majdnem minden C kódban megtalálhatod. A továbbiakban feltételezzük, hogy 32 bites rendszeren 4 byteos egészeket használunk. Ha meg akarod tudni, hogy az egyes típusok milyen méretűek a te rendszereden, a következő program megmutatja: #include int main() { printf("short merete %dn", sizeof(short)); printf("int merete %dn", sizeof(int)); printf("long merete %dn", sizeof(long)); } Amikor egy változót deklarálunk, akkor egyszerre két dologról is tájékoztatjuk a fordítót: a változó nevéről és típusáról. Például deklarálunk egy k nevű egészet: int k; Amikor a fordító az 'int'-hez ér, akkor szépen lefoglal 4 byte memóriát, hogy az egészünk értékét tárolni tudja. És ezen felül, beállítja a szimbólumtáblát (symbol table). Beleteszi a k szimbólumot, és mellé fölírja, hogy honnan kezdődik a memóriában az a 4 byte, amit lefoglalt. Így amikor később azt írjuk, hogy: 32
k=2;
Amikor a futás ide ér, akkor a k számára fenntartott helyre a 2-es szám kerül. A C-ben a k egészre úgy hivatkozunk, hogy "objektum". Bizonyos értelemben, a k-hoz két érték is tartozik. Az egyik az egész szám, amit benne tárolunk (a fenti példában ez a 2), a másik pedig a memóriahely "értéke", tehát k címe. Több szövegben ezekre úgy hivatkoznak (ebben a sorrendben), mint rvalue (right value, jobbérték) és lvalue (left value, balérték). Néhány nyelvben balérték csak a "=" hozzárendelő operátor bal oldalán engedélyezett (tehát mint az a cím, ahova a jobb oldalon található kifejezés értéke kerül). A jobbérték pedig az, ami a jobb oldalon áll, a példánkban a 2. A jobbértékeket tilos az egyenlőségjel bal oldalára írni, ezért ez helytelen: 2=k;
Azonban a balérték fenti definíciója egy kissé módosult a C-ben. [K&R II, 197.old] (ld. a fejezet végén) szerint: "Az objektum egy névvel rendelkező tárolási hely, a balérték pedig egy objektumra hivatkozó kifejezés." Jelenleg a fenti definíció számunkra bőven elegendő, később majd jobban belemegyünk a részletekbe. Rendben, akkor most tekintsük a következőt: int j, k; k = 2; j = 7; <-- első sor k = j; <-- második sor A fenti kódrészletben az első sorban a fordító j-t úgy értelmezi, mint j címét (tehát a balértékét), és olyan kódot készít, hogy a 7-est arra a címre helyezze. Azonban a második sorban, a j már a jobbértékét jelenti (mivel az "=" jobb oldalán van). Tehát most j az az érték, amit a j memóriacímén tárolunk, ebben az esetben 7. Így végül a k balértéke által mutatott helyre a 7 kerül. Az összes eddigi példában 4 byteos egészeket használunk, így amikor a másolásra kerül a sor, mindig 4 byteot másolunk. Ha 2 byteos egészeket használtunk volna, akkor 2 byteot is másoltunk volna. Most mondjuk, valamilyen okból egy olyan változóra van szükségünk, ami arra van kitalálva, hogy egy balértéket tároljunk benne (egy memóriacímet). Hogy egy ilyen változónak mennyi hely szükséges, az rendszerfüggő. Régebbi gépeken, amiknek összesen 64KB memóriája volt, egy memóriacím tárolására elég volt 2 byte. Az ennél több memóriával rendelkező gépeken nagyobb blokk szükséges. A ténylegesen szükséges érték nem is fontos, amíg van rá mód, hogy tájékoztassunk a fordítót, hogy most egy címet akarunk majd tárolni.
33
Egy ilyen változót hívnak mutató változónak (pointer variable). Hogy miért pont így, az a későbbiekben remélhetőleg egy kissé tisztábbá fog válni. A C-ben úgy definiálhatunk mutatót, hogy a változó neve elé egy csillagot (*) teszünk. A C-ben a mutatóknak típust is adunk, ami ebben az esetben, azt az adattípust jelenti, amilyen típusú változónak a címét akarjuk a mutatónkban tárolni. Példaként tekintsük a következő változódeklarációt: int *ptr;
Itt a változónk neve ptr (mint ahogy az előbbiekben a k volt a neve az egész változónak). A '*' azt jelzi a fordítónak, hogy itt egy mutatóról van szó, tehát annyi helyet foglaljon le a memóriában, amennyivel egy mutatót tárolni lehet. Az int arra utal, hogy egy egész címét fogjuk majd tárolni. Tehát ez egy "egészre mutató" mutató. Vegyük észre, hogy amikor az int k;-t leírtuk, a k-nak nem adtunk értéket. Ha ez minden függvényen kívül történt, akkor egy szabványos ANSI fordító a 0 értéket kell hogy adja neki. Hasonlóképpen, a ptr-nek sem adtunk, tehát, ha minden függvényen kívül deklaráltuk, akkor a fordító biztosítja, hogy garantáltan ne mutasson egyetlen érvényes C objektumra vagy függvényre. Egy ilyen mutatót "nulla-mutatónak" (null pointer) nevezünk. Azonban egy nulla-mutató tulajdonképpen képe a memóriában (bitminta, bit pattern) valószínűleg nem lesz éppen 0, ez megint csak a használt rendszertől függ. Hogy a forráskód hordozható maradjon, a nulla-mutató jelölésére egy makrót használunk. Ennek a makrónak a neve NULL. Így ha egy mutatónak ezt adjuk értékül, pl. a ptr=NULL; utasítással, akkor a mutatónk garantáltan nulla-mutatóvá fog válni. Hasonlóan, amikor egy egész értéket vizsgálunk, akkor az if(k==0) kifejezést használjuk, pointerek esetén ez az if(ptr==NULL) lesz. De térjünk vissza a vadiúj ptr-ünk használatára. Tegyük fel most, hogy a ptr-ben a k címét akarjuk használni. Ehhez az & címképző operátort használjuk. ptr=&k; Az & operátor annyit csinál, hogy visszaadja k balértékét (címét), bár k az "=" jobb oldalán áll, utána ezt az értéket másoljuk ptr-be. Most azt mondjuk, hogy ptr "k-ra mutat". Ennek a fordítottja a hivatkozás-fordító (dereferencing) operátor, a csillag, használata a következő: *ptr=7; Ez a 7-et arra a címre másolja, amit a ptr-ben tároltunk. Így, ha ptr k-ra mutat (k címét tartalmazza), a fenti utasítás eredményeképpen k értéke 7 lesz. Amikor a '*' operátort használjuk, akkor arra az értékre hivatkozunk, amire ptr mutat, és nem ptr értékére. Hasonlóképpen írhatjuk: printf("%d\n",*ptr);
34
hogy kiírjuk a képernyőre annak az egésznek az értékét, amire ptr mutat. Hogy lássuk hogy is működik az egész egyben, nézzük át és futtassuk a következő példaprogramot. #include int j, k; int *ptr; int main(void) { j = 1; k = 2; ptr = &k; printf("n"); printf("j erteke %d es a %p cimen van tarolvan", j, (void *)&j); printf("k erteke %d es a %p cimen van tarolvan", k, (void *)&k); printf("ptr erteke %p es a %p cimen van tarolvan", ptr, (void *)&ptr); printf("Annak az egesznek az erteke, amire ptr mutat: %dn", *ptr); return 0; } Megjegyzés: még nem beszéltünk a C nyelv azon elemeiről, amik a (void *) kifejezés használatát megindokolják. Egyelőre rakd bele a kódba, az okokat majd később magyarázzuk meg. Összefoglalva A változót a típus és a név megadásával deklaráljuk. (pl.: int k;) Mutatót szintén típus és név megadásával deklarálunk (pl.: int *ptr;), a fordítónak a csillag jelzi, hogy mutatóról van szó, a típus pedig ezt mondja meg, hogy milyen típusra fog a mutató mutatni (ebben az esetben egész). A már deklarált változó címét az & címképzõ operátorral érhetjük el (pl.: &k). Egy mutatót "megfordíthatunk" (dereference), tehát hivatkozhatunk arra az értékre, amire a mutató mutat. Ezt a * operátorral érhetjük el (pl.: *ptr). Egy változó balértéke a címének az értéke, tehát hogy a memóriában hol tárolódik. A jobbérték pedig a változóban, a balérték által mutatott címen tárolt érték.
35
10 Struktúrák A struktúra egy vagy több, esetleg különböző típusú változó együttese, amelyet a kényelmes kezelhetőség céljából önálló névvel látunk el. Néhány nyelvben az így értelmezett struktúrát rekordnak nevezik (pl. a Pascal rekordja hasonló tulajdonságú adatfajta). A struktúra bevezetése segíti az összetett adathalmazok szervezését, ami különösen nagy programok esetén előnyös, mivel lehetővé teszi, hogy az egymással kapcsolatban lévő változók egy csoportját egyetlen egységként kezeljük, szemben az egyedi adatkezeléssel. A struktúrára az egyik hagyományos példa a bérszámfejtési lista: ez az alkalmazottakat attribútumok halmazával (név, lakcím, társadalombiztosítási szám, bér stb.) írja le. Ezen attribútumok némelyike maga is lehet struktúra, pl. a név is több részből áll, csakúgy mint a cím vagy a bér. A másik, C nyelvre jellemzőbb példát a számítógépes grafika adja: a pont egy koordinátapárral írható le, a négyzet egy pontpárral adható meg stb. A struktúrákat érintő, az ANSI szabványból adódó legfontosabb változás, hogy a szabvány értelmezi a struktúrák értékadását. A struktúrák átmásolhatók egymásba, értékül adhatók más struktúráknak, átadhatók függvénynek és a függvények visszatérési értékei is lehetnek. Ezt évek óta a legtöbb fordítóprogram támogatja, de ezeket a tulajdonságokat most pontosan definiáljuk. A szabvány lehetővé teszi az automatikus tárolási osztályú struktúrák és tömbök inicializálását, amivel szintén ebben a fejezetben foglalkozunk.
10.1 Alapfogalmak Hozzunk létre néhány struktúrát, amelyek a grafikus ábrázoláshoz használhatók. Az alapobjektum a pont, amely egy x és egy y koordinátával adható meg. Tételezzük fel, hogy a koordináták egész számok. A két komponens (koordináta) egy struktúrában helyezhető el a struct pont { int x; int y; }; deklarációval. A struktúra deklarációját a struct kulcsszó vezeti be, amelyet kapcsos zárójelek között a deklarációk listája követ. A struct kulcsszót opcionálisan egy név, az ún. struktúracímke követheti (mint a példánkban a pont). Ez a címke vagy név azonosítja a struktúrát és a későbbiekben egy rövidítésként használható a kapcsos zárójelek közötti deklarációs lista helyett. A struktúrában felsorolt változóneveket a struktúra tagjainak nevezzük. Egy struktúra címkéje (neve), ill. egy tagjának a neve és egy közönséges (tehát nem struktúratag) változó neve lehet azonos, mivel a programkörnyezet alapján egyértelműen megkülönböztethetők. Továbbá ugyanaz a tagnév előfordulhat különböző struktúrákban, bár célszerű azonos neveket csak egymással szoros kapcsolatban lévő adatokhoz használni. Egy struct deklaráció egy típust is definiál. A jobb oldali, záró kapcsos zárójel után következhet a változók listája, hasonlóan az alapadattípusok megadásához. Így pl. a struct {...} x, y, z;
36
szintaktikailag analóg az int x, y, z; deklarációval, mivel mindkét szerkezet a megadott típusú változóként deklarálja x, y és z változót és helyet foglal számukra a tárolóban. Az olyan struktúradeklaráció, amelyet nem követ a változók listája, nem foglal helyet a tárolóban, csak a struktúra alakját írja le. Ha a struktúra címkézett volt, akkor a címke a későbbi definíciókban a struktúra konkrét előfordulása helyett használható. Például felhasználva a pont korábbi deklarációját a struct pont pt; definíció egy pt változót definiál, ami a struct pont-nak megfelelő típusú struktúra. Egy struktúra úgy inicializálható, hogy a definíciót az egyes tagok kezdeti értékének listája követi. A kezdeti értékeknek állandó kifejezéseknek kell lenni. Például: struct pont maxpt = { 320, 200 }; Egy automatikus struktúra értékadással vagy egy megfelelő típusú struktúrát visszaadó függvény hívásával is inicializálható. Egy kifejezésben az adott struktúra egy tagjára úgy hivatkozhatunk, hogy struktúra-név.tag A pont struktúratag operátor összekapcsolja a struktúra és a tag nevét. A pt pont koordinátáit pl. úgy írathatjuk ki, hogy printf("%d, %d", pt.x, pt.y); A pt pont origótól mért távolsága: double dist, sqrt(double); dist = sqrt((double)pt.x * pt.x + (double)pt.y * pt.y); A struktúrák egymásba ágyazhatók.
10.2 Struktúrák és függvények A struktúrák esetén megengedett művelet a struktúra másolása vagy értékadása, ill. a struktúra címéhez való hozzáférés az & operátorral és a struktúra tagjaihoz való hozzáférés. Ezek a műveletek a struktúrát egy egységként kezelik, és a másolás vagy értékadás magában foglalja a struktúrák függvényargumentumkénti átadását, ill. a struktúra típusú függvényvisszatérés lehetőségét is. Struktúrák egy egységként nem hasonlíthatók össze. A struktúrák állandó értékek listájával inicializálhatók, és automatikus tárolási osztályú struktúrák kezdeti értéke értékadással is beállítható. A struktúrák tulajdonságainak vizsgálatához írjunk néhány függvényt, amelyek pontokkal és téglalapokkal manipulálnak. A feladat megoldásának három lehetséges módja van: a függvénynek átadjuk az egyes komponenseket, a teljes struktúrát vagy annak mutatóját. Mindegyik módszernek van előnye és hátránya. Az első függvény legyen a makepoint, amelyet két egész értékkel hívunk és visszatér egy pont struktúrával.
37
/* makepoint: egy pont struktúrát csinál az x és y komponensekből */ struct pont makepoint (int x, int y) { struct pont temp; temp.x = x; temp.y = y; return temp; } Vegyük észre, hogy nincs konfliktus abból, hogy az argumentum és a struktúratag neve megegyezik: az összefüggés kiértékelésekor újra felhasználja a rendszer a nevet. Ha nagy struktúrát kell átadnunk egy függvénynek, akkor sokkal hatékonyabb, ha a struktúra mutatóját adjuk át és nem pedig a teljes struktúrát másoljuk át. Egy struktúrához tartozó mutató éppen olyan, mint egy közönséges változó mutatója. A struktúra mutatójának deklarációja: struct pont *pp; Ez egy struct pont típusú struktúrát kijelölő mutatót hoz létre. Ha pp egy pont struktúrát címez, akkor *pp maga a struktúra, és (*pp).x, ill. (*pp).y pedig a struktúra tagjai. A pp értékét felhasználva pl. azt írhatjuk, hogy struct pont kezdet, *pp; pp = &kezdet; printf("kezdet: (%d, %d)\n", (*pp).x, (*pp).y); A zárójelre a (*pp).x kifejezésben szükség van, mert a . struktúratag operátor precedenciája nagyobb, mint a * operátoré. A *pp.x kifejezés azt jelentené, mint a *(pp.x), ami viszont szintaktikailag hibás, mivel jelen esetben x nem mutató. A struktúrák mutatóit gyakran használjuk egy új, rövidített jelölési formában. Ha p egy struktúra mutatója, akkor a p-> struktúratag kifejezés közvetlenül a struktúra megadott tagját címzi. A -> operátor a mínusz jel és a nagyobb jel egymás után írásával állítható elő. Ezt felhasználva az előző példa printf függvényét úgy is írhatjuk, hogy printf("kezdet: (%d, %d)\n", pp->x, pp->y); A . és a -> operátorok balról jobbra hajtódnak végre, ezért a struct tegla r, *rp = &r; deklaráció esetén az r.pt1.x; rp->pt1.x; (r.pt1).x; (rp->pt1).x; kifejezések egymással egyenértékűek.
38