Nem illik mutogatni? –
pointerek kicsit másképp |
Ezt a cikket kezdőknek ajánlanám, pontosabban azoknak, akik most
ismerkednek a pointerekkel, és/vagy idegenkednek tőlük. Megpróbálom
emberközelibbé tenni a mutatókat, nagy ívben kerülve a túlzottan tudományos,
merev megközelítést. Távolról sem egy kimerítő, tanulmány, inkább egyfajta könnyed
bevezető a mutatók világába. |
Cím-cím-cimbora |
A pointer – avagy mutató -, mint neve is utal rá, valamire mutat; végül
is nem más, mint egy (memória)cím. A hétköznapi életre kivetítve, olyan, mint egy cím, melynek alapján meg
lehet találni valakit: Debug Endre 1183 Van tehát egy címünk, és az adott címen lakik Debug Endre. Persze azt is
mondhatjuk, hogy Debug Endre címe: 1183 Budapest XVIII.ker Elszif út 61. Most térjünk vissza a pointerekhez: itt is van egy címünk, ahol jó
esetben egy változó, függvény, objektum, vagy egy másik mutató(lásd: "Csillagokat
látok!" c. fejezet), stb. található. Megjegyzés: A cikk további részében a C nyelv oldaláról fogom megközelíteni a
mutatókat, és a példákat is C-ben fogom leírni. A pointereket deklarálni kell, mint bármilyen változót. Erre a C
szintaktika a következő: <típus>* A ptr nevű pointer "típus" típusú változók címeit
tartalmazza. Egy int-re mutató pointert pl. így deklarálhatunk: int* intPtr; Ez a mutató egyenlőre inicializálatlan, azaz olyan helyre mutat,
ahol csak memória-szemetet találhatunk. A pointernek át kell adnunk egy érvényes címet ahhoz, hogy használhassuk. Rögtön deklarálás után olyan, mint egy üres boríték (ezzel a hasonlattal
még találkozunk): ahhoz, hogy levelünk eljusson a címzetthez, fel kell
tüntetni a címet! Ha a már említett Debug Endrének szeretnénk levelet írni,
úgy az ő címével kell ellátni a borítékot. Ugyanígy, ha egy pointer-en keresztül akarunk hivatkozni egy változóra,
akkor a címét át kell adjuk a mutatónak: int* intPtr; // deklarálunk egy int típusú mutatót int x; // x egy
“sima” int intPtr = &x; // átadjuk x címét a mutatónknak A &x nem más, mint x címe. Mostmár intPtr x címét fogja tartalmazni. Rögtön le is
ellenőrizhetjük - egészítsük ki az előbbi kódot a következő
sorral: if( intPtr == &x ) { cout << “intPtr x-re mutat” << endl } Ha már a borítéknál tartunk, a hasonlat kicsit sántít, ugyanis a mutatót
bármikor más változó címére állíthatjuk. Az alábbi példában szemléltetem ezt: int* intPtr; int x; intPtr = &x;
// átadjuk x címét a mutatónknak int y; intPtr = &y;
// a mutató mostmár y, és nem x címét
tartalmazza! if( intPtr == &x ) //
nem teljesül, ugrás az else ágra { cout << “intPtr
x-re mutat” << endl; } else { cout
<< “intPtr nem x-re mutat!” <<
endl; // nem bizony! } |
Mutasd magad! |
Mivel a mutató a változó címét tárolja,
a mutatón keresztül a változó értékét is elérhetjük. Egyszerű: a címből kiindulva megtudhatjuk, hogy ki a tulajdonos
– C-ben persze sokkal gyorsabb, ugyanis elkerüljük a sorban állást a
"Földhivatal"-ban. Bár kezdőknél sok félreértéshez vezet, erre a célra is a "*" -got használjuk. Az
alábbi példában az intPtr címen tárolt értéket
íratjuk ki – ez a *intPtr: int* intPtr; //
létrehozzuk az int típusú mutatónkat int x = 5; // létrehozzuk
az x nevű int-et, rögtön értéket is kap intPtr = &x; //
átadjuk x címét a mutatónknak cout
<< “intPtr erteke” << *intPtr << endl; // itt egy ötös Vigyázni kell tehát a "*" jel
értelmezésével:
Mint mondtam, ha már van egy mutatónk egy változóra, a mutatón keresztül
elérhetjük, módosíthatjuk tetszés szerint. Tegyük fel, hogy találunk egy Debug Endre nevére szóló személyigazolványt
a bevásárló központban. Ezen persze fel van tüntetve a címe is. Kétféleképpen
járhatunk el (feltéve, hogy vissza akarjuk juttatni a szerencsétlennek a
papírjait):
int x; x = 5; cout << x << endl;
int x; x = 5; int* intPtr; intPtr = &x; // átadjuk a címét cout << *intPtr << endl;
// kiírjuk a címen található értéket Vajon mi lesz az eredménye a következő kódnak? int z = 0; intPtr = &z; *intPtr++; // növeljük az intPtr által mutatott
értéket cout << z << endl; // mi jelenik
meg? Ebben az esetben z értékét a mutatóján keresztül növeltük, és valóban -
meg is jelenik az "1"-gyes. |
Címzett ismeretlen |
Nem mindegy, minek a címét akarjuk átadni a mutatónknak: lényeges, hogy a
változó típusa megegyezzen a mutatóéval. Hogy érzékeltessem egy egyszerű példával: ha levelet szeretnénk írni
Debug Endre barátunkat, egyértelmű, hogy nem a telefonszámát írjuk a
borítékra a címe helyett – a levél valószínűleg visszajutna a feladóhoz
“címzett ismeretlen” pecséttel (feltéve, hogy a feladó címe helyett nem a
telefonszámunkat írtuk be). Aki nem hiszi, próbálja ki! Feladó: Return Árpád 1221 Címzett: Debug Endre 0680-255-32-16 Hát, ez nem az igazi… Gondolkozzunk most posta helyett C-ben. A következő sorok le sem
fordulnának (szerencsére): int* intPtr; char ch; intPtr = &ch; // fordítási hiba, char
típusú mutatót nem lehet int típusúra konvertálni Amennyiben egy mutatót szeretnénk a karakter változónkra, úgy char típusú mutatót kell létrehozunk: char *aCharPtr; aCharPtr = &ch: // ez már rendben van! Megjegyzés: A void típusú mutató jól alkalmazkodó fajta, bármit
"megeszik", annyi hátránya van, hogy, ha mégis rajta keresztül
szeretnénk elérni a változónkat, akkor elkerülhetetlen a cast-olás, azaz az
adatok átalakítása megfelelő típusra. A void típus – magyarul üres, semmis – olyan, mint a
Mikulás zsákja. Tehetünk bele bármit: csokit, kisvasutat, legót stb. A gondok
ott kezdődnek, mikor ki is akarunk venni belőle valamit: vigyázni
kell, nehogy a jó kislány kapja a virgácsot, és a vásott kölyök a Barbie-t. void* aVoidPtr; // íme a void pointerünk, egy igazi kaméleon char ch[] = "a";
// egy char típusú változó int b = 3; // egy int aVoidPtr = &ch; // a mutatónk megkapja a
karakter címét cout << *(char*)aVoidPtr <<
endl; // átalakítjuk char-ra aVoidPtr = &b; cout << *(int*)aVoidPtr <<
endl; //
átalakítjuk int-re A cast-olás szerepéről meggyőződhetnek Önök is: void* aVoidPtr; // a void pointerünk,
a mindenevő char ch[] = "a";
// egy char típusú változó aVoidPtr = &ch; cout << *(char*)aVoidPtr <<
endl; // rendben Amennyiben a cout-tal kezdődő sort erre cserélik le: cout << *(int*)aVoidPtr
<< endl; Furcsa érték jelenik meg a képernyőn, az ártatlan "a"
helyett! A mutató jó helyre mutat, csak rosszul mondjuk meg neki, mit, azaz
mennyit olvasson ki onnan… A köv. megközelítés legalább a megfelelő számú – azaz egy – bájtot ad vissza cout << *(short*)aVoidPtr
<< endl; Még mindig nem az igazi, hisz nem azt látjuk, hogy "a", hanem
azt, hogy 97. Ez az érték azonban nem más, mint az "a" ASCII kódja.
Győződjön meg róla: egy szövegszerkesztőben vagy DOS ablakban
az <Alt>-ot lenyomva üsse be a numerikus billentyűzeten a 97-et,
majd engedje fel az <Alt>-ot. Kompromisszumot kell kötnünk: mivel a mutató void típusú, mi kell megmondjuk, mire is mutat. A kényelemért így fizetünk... |
Csillagokat látok |
Mutatóra mutató mutatók – hát igen, ilyen is van: <típus>** Ez egy különleges eset, a mutató egy másik mutatót tartalmaz. Utóbbi
pedig a változóra mutat. Szemléltetem a dolgot Debug Endrével. Tudjuk, hogy a Bug-soft kft.-nél
dolgozik, a cég címe pedig: 1112 Elmegyünk az adott címre, ahol a portán érdeklődünk ismerősünk
felöl. Épp nincs benn, mert beteg, de hosszas könyörgésünkre a portástól
megszerezzük az otthoni címét: Debug Endre 1183 Nagyjából erről szól a pointer típusú pointer. Először is deklarálunk egy int típusú
mutatóra mutató mutatót (kicsit
redundánsra sikeredett, de kifejező): int** intPtrPtr; Ennek a mutatónak is könnyen megérthető a lelkivilága. Az *intPtrPtr visszaadja nekünk az int-re mutató pointert, a **intPtrPtr pedig magát
az értéket. Az előbbi hasonlattal élve: –
intPtrPtr – a
“Bug-soft” cég címe (1112 –
*intPtrPtr – a portástól
kicsikart cím (1183 Budapest XVIII.ker Elszif út 61) –
**intPtrPtr –Debug Endre
személyesen (majdnem) ugyanez C-ben: int x = 5; int** intPtrPtr = new(int*); // inicializálás,
memóriafoglalás *intPtrPtr = &x; // átadjuk a címet cout <<
**intPtrPtr; // kiírjuk az értéket (5) |
Minek nevezzelek? |
Mint láttuk, a & jelet egy változó
neve elé írva a változó memóriacímét kapjuk meg. Ezt átadva egy mutatónak,
azon keresztül is elérhetjük a változónkat. A & segítségével azonban ún. szinonimát, avagy
referenciát is deklarálhatunk egy változóra. A referencia és a változó címe
megegyezik! Bár más a neve, ugyanarról az objektumról van szó. Szinonimát így deklarálunk (feltétel: valtozoNev már deklarálva!): <típus>& A szinonimát kedves ismerősünkkel, Debug Endrével szemléltetném. Tegyük fel, hogy szerencsétlent kirúgják a munkahelyéről, és
bánatában bevonul a francia idegenlégióba… Mint tudjuk, ott új nevet kap
mindenki, és ezentúl Harry Pointer-nek hívják. Attól még az otthoni címe
ugyanaz, mint előtte (elvetettem azt az eshetőséget, hogy
előzőleg eladta): 1183 Remélem, átadtam a dolog lényegét, lássunk rögtön egy konkrét példát: int x; int& ref = x; // szinoníma
deklarálása x-re x = 5; cout << ref << endl // x értéke,
azaz 5 jelenik meg if ( &aRef == &x ) //
ellenőrzés { cout << “Címek megegyeznek” << endl; } else // ez az ág
biztos nem kerül végrehajtásra { cout << “Címek eltérnek” << endl; } |
Hulló csillag… |
Komoly félreértésekhez vezethet a következő deklarálás: int* x, y, z; Nem mind a három változó pointer – bár a deklarálás ezt sugallja! Mi is történik itt valójában? Kizárólag az <x> egy int típusú mutató, míg
<y> illetve <z> sima int. Kicsit átláthatóbb, ha így deklaráljuk a változóinkat: int *x, y, z; Ebben az esetben annyi a különbség, hogy a "*"-got a változó
nevéhez, nem pedig típusához ütköztettük. Ez csak részmegoldás, hisz sokan a típushoz szeretik ütköztetni a
"*"-got (pl. én is). (Szerk
megj: Én nem, mert egyszer fél napot szenvedtem egy ilyen elcsillagozás
miatt... J) Ajánlottabb a változók külön sorokban történő deklarálása –
szerintem különben sem elegáns több változót egy sorban deklarálni. Persze
senkire sem akarom ráerőltetni a jelölési szokásaimat, de legalább a
pointereket válasszuk el a többi változótól. A következő példaprogram (J) szemlélteti
a jó alternatívát: int* x; int y; int z; Átláthatóbb, rendezettebb, és ami a legfontosabb – rögtön látjuk, hogy x egy int típusú
mutató, y és z pedig “sima” int-ek. Sok mindenről lehetne még írni a mutatókkal kapcsolatban, de, mivel
ezt egy könnyed bevezetőnek szántam, maradjon könnyed bevezető. Egyenlőre ennyi, sok sikert kívánok Mindenkinek, és minél kevesebb
fordítási hibát! A Kód legyen veletek! |
Nyisztor Károly |