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 Budapest XVIII.ker Elszif út 61

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>* nev;

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:

  • deklarálásnál azt mutatja, hogy egy mutatót hoztunk létre (ilyen egy adott mutatónál csak egyszer lesz)
    int* intPtr;  // deklaráltunk egy int pointert
  • deklarálás után a mutató által referált értéket adja vissza
    cout << *intPtr  // írjuk ki az intPtr címen található értéket

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):

  1. bemondatjuk a hangosbemondóban, hogy Debug Endre jelentkezzen az Információ-nál, és mikor jelentkezik, átadjuk neki - tehát a neve alapján. Kódoljuk ezt le:

int x;

x = 5;

cout << x << endl;

  1. postai úton elküldjük neki az iratot – a címe alapján. Ennek C-megfelelője:

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 Budapest Dummy Void út 11                             

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>** nev;

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 Budapest, XI.ker Kivétel utca 7

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 Budapest XVIII.ker Elszif út 61

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 Budapest, XI.ker Kivétel utca 7)

         *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>& nev = valtozoNev;

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 Budapest XVIII.ker Elszif út 61

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