• Nem Talált Eredményt

Mutatók, hivatkozások és a dinamikus memóriakezelésmemóriakezelés

Minden program legértékesebb erőforrása a számítógép memóriája, hisz a program kódja mellett a közvetlenül elérhető (on-line) adataink is itt tárolódnak. Az eddigi példáinkban a memóriakezelést teljes mértékben a fordítóra bíztuk.

A fordító – a tárolási osztály alapján – a globális (extern) adatokat egybegyűjtve helyezi el egy olyan területen, amely a program futása során végig törölhetjük azokat, ha már nincs szükség rájuk. Ezek a - dinamikus módon - kezelt változók eltérnek az eddig használt változóktól, hisz nincs nevük. Rájuk a címüket tároló változókkal, a mutatókkal (pointerekkel) hivatkozhatunk. A C++ nyelv operátorok sorával is segíti a dinamikus memóriakezelést *, &, new, delete.

I.13. ábra - C++ program

memóriahasználat

Az elmondottakon túlmenően nagyon sok területeken használunk mutatókat a C/C++ programokban:

függvények paraméterezése, láncolt adatstruktúrák kezelése stb. A pointerek kizárólagos alkalmazását a C++

egy sokkal biztonságosabb típussal, a hivatkozással (referenciával) igyekszik ellensúlyozni.

6.1. Mutatók (pointerek)

Minden változó definiálásakor a fordító lefoglalja a változó típusának megfelelő méretű területet a memóriában, és hozzárendeli a definícióban szereplő nevet. Az esetek többségében a változónevet használva adunk értéket a változónak, illetve kiolvassuk a tartalmát. Van amikor ez a megközelítés nem elegendő, és közvetlenül a változó memóriabeli címét kell használnunk (például a scanf() könyvtári függvény hívása).

A mutatók segítségével tárolhatjuk, illetve kezelhetjük a változók (memóriában tárolt adatok) és függvények címét. Egy mutató azonban nemcsak címet tárol, hanem azt az információt is hordozza, hogy az adott címtől kezdve hány bájtot, hogyan kell értelmezni. Ez utóbbi pedig nem más, mint a hivatkozott adat típusa, amit felhasználunk a pointer(változó) definíciójában.

6.1.1. Egyszeres indirektségű mutatók

Először ismerkedjünk meg a mutatók leggyakrabban használt, és egyben legegyszerűbb formájával, az egyszeres indirektségű pointerekkel, melyek általános definíciója:

típus *azonosító;

A csillag jelzi, hogy mutatót definiálunk, míg a csillag előtti típus a hivatkozott adat típusát jelöli. A mutató automatikus kezdőértékére a szokásos szabályok érvényesek: 0, ha a függvényeken kívül hozzuk létre, és definiálatlan, ha valamely függvényen belül. Biztonságos megoldás, ha a mutatót a létrehozása után mindig inicializáljuk - a legtöbb fejállományban megtalálható - NULL értékkel:

típus *azonosító = NULL;

Több, azonos típusú mutató létrehozásakor a csillagot minden egyes azonosító előtt meg kell adni:

típus *azonosító1, *azonosító2;

A mutatókkal egy sor művelet elvégezhető, azonban létezik három olyan operátor, melyeket kizárólag pointerekkel használunk:

*ptr A ptr által mutatott objektum elérése.

ptr->tag A ptr által mutatott struktúra adott tagjának elérése (8.

szakasz - Felhasználói típusok fejezet).

&balérték A balérték címének lekérdezése.

Érthetőbbé válik a bevezetőben vázolt két szint, ha példa segítségével szemléltetjük az elmondottakat. Hozzunk létre egy egész típusú változót!

int x = 2;

A változó definiálásakor a memóriában (például az 2004-es címen) létrejön egy (int típusú) terület, amelybe bemásolódik a kezdőérték:

Az

int *p;

definíció hatására szintén keletkezik egy változó (például az 2008-as címen), melynek típusa int*. A C++ nyelv szóhasználatával élve a p egy int* típusú változó, vagy a p egy int típusú mutató. Ez a mutató int típusú változók címének tárolására használható, melyet például a „címe” & művelet során szerezhetünk meg, például:

p = &x;

A művelet után az x név és a *p érték ugyanarra a memóriaterületre hivatkoznak. (A *p kifejezés a „p által mutatott” tárolót jelöli.)

Ennek következtében a

*p = x +10;

kifejezés feldolgozása során az x változó értéke 12-re módosul.

Alternatív mutatótípus is létrehozható a typedef kulcsszó felhasználásával:

int x = 2;

typedef int *tpi;

tpi p = &x;

Egyetlen változóra több mutatóval is hivatkozhatunk, és a változó értékét bármelyik felhasználásával módosíthatjuk:

int x = 2;

int * p, *pp;

p = &x;

pp = p;

*pp += 10; // x tartalma ismét 12 lesz

Ha egy mutatót eltérő típusú változó címével inicializálunk, fordítási hibát kapunk long y = 0;

char *p = &y; // hiba! ↯

Amennyiben nem tévedésről van szó, és valóban bájtonként szeretnénk elérni a long típusú adatot, a fordítót típus-átalakítással kérhetjük az értékadás elvégzésére:

long y = 0;

char *p = (char *)&y;

vagy

char *p = reinterpret_cast<char *>(&y);

Gyakori, súlyos programhiba, ha egy mutatót inicializálás nélkül kezdünk el használni. Ez általában a program futásának megszakadását eredményezi:

int *p;

*p = 1002; // ↯ ↯ ↯

6.1.2. Mutató-aritmetika

Mutató operandusokkal a már bemutatott (* és &) operátorokon túlmenően további műveleti jeleket is használhatunk. Ezeket a műveleteket összefoglaló néven pointer-aritmetikának nevezzük. Minden más művelet elvégzése definiálatlan eredményre vezet, így azokat javasolt elkerülni.

A megengedett pointer-aritmetikai műveleteket táblázatban foglaltuk össze, ahol a q és a p (nem void* típusú) mutatók, az n pedig egész (int vagy long):

Művelet Kifejezés Eredmény

két, azonos típusú mutató kivonható egymásból

q - p egész

a mutatóhoz egész szám hozzáadható

p + n, p++,++p, p += n, mutató

a mutatóból egész szám kivonható p – n, p--, --p, p -= n mutató

Művelet Kifejezés Eredmény

két mutató összehasonlítható p == q, p > q stb. bool (false vagy true)

Amikor hozzáadunk vagy kivonunk egy egészet egy pointerhez/ből, akkor a fordító automatikusan skálázza az egészet a mutató típusának megfelelően, így a tárolt cím nem n bájttal, hanem

n * sizeof(pointertípus)

bájttal módosul, vagyis a mutató n elemnyit „lép” a memóriában.

Ennek megfelelően a léptető operátorok az aritmetikai típusokon kívül a mutatókra is alkalmazhatók, ahol azonban nem 1 bájttal való elmozdulást, hanem a szomszédos elemre való léptetést jelentik.

A pointer léptetése a szomszédos elemre többféle módon is elvégezhető:

int *p, *q;

p = p + 1;

p += 1;

p++;

++p;

Az előző elemre való visszalépésre szintén több lehetőség közül választhatunk:

p = p - 1;

p -= 1;

p--;

--p;

A két mutató különbségénél szintén érvényesül a skálázás, így a két mutató között elhelyezkedő elemek számát kapjuk eredményül:

int h = p - q;

6.1.3. A void * típusú általános mutatók

A C++ nyelv típus nélküli (void típusú), általános mutatók használatát is lehetővé teszi, int x;

void * ptr = &x;

amelyek csak címet tárolnak, így sohasem jelölnek ki változót.

A C++ nyelv a mutatókkal kapcsolatban két implicit konverziót biztosít. Tetszőleges típusú mutató átalakítható általános (void) típusú mutatóvá, valamint a nulla (0) számértékkel minden mutató inicializálható. Ellenkező irányú konverzióhoz explicit típusátalakítást kell használnunk.

Ezért, ha értéket szeretnénk adni a ptr által megcímzett változónak, akkor felhasználói típuskonverzióval típust kell rendelnünk a cím mellé. A típus-átalakítást többféleképpen is elvégezhetjük:

int x;

void *ptr = &x;

*(int *)ptr = 1002;

typedef int * iptr;

*iptr(ptr) = 1002;

*static_cast<int *>(ptr) = 1002;

*reinterpret_cast<int *>(ptr) = 1002;

Mindegyik indirekt módon elvégzett értékadást követően, az x változó értéke 1002 lesz.

Megjegyezzük, hogy a mutatót visszaadó könyvtári függvények többsége void* típusú.

6.1.4. Többszörös indirektségű mutatók

A mutatókat többszörös indirektségű kapcsolatok esetén is használhatjuk. Ekkor a mutatók definíciójában több csillag (*) szerepel:

típus * *mutató;

A mutató előtt közvetlenül álló csillag azt jelöli, hogy pointert definiálunk, és ami ettől a csillagtól balra található, az a mutató típusa (típus *). Ehhez hasonlóan tetszőleges számú csillagot értelmezhetünk, azonban megnyugtatásul megjegyezzük, hogy a C++ nyelv szabványos könyvtárának elemei is legfeljebb kétszeres indirektségű mutatókat használnak.

Tekintsünk néhány definíciót, és mondjuk meg, hogy mi a létrehozott változó!

int x; x egy int típusú változó,

int *p; p egy int típusú mutató (amely int változóra mutathat),

int * *q; q egy int* típusú mutató (amely int* változóra, vagyis

egészre mutató pointerre mutathat).

Alternatív (typedef) típusnevekkel a fenti definíciók érthetőbb formában is felírhatók:

typedef int *iptr; // iptr - egészre mutató pointer típusa iptr p, *q;

vagy

typedef int *iptr; // iptr - egészre mutató pointer típusa

// iptr típusú változóra mutató pointer típusa typedef iptr *ipptr;

iptr p;

ipptr q;

A fenti definíciók megadása után az x = 2;

p = &x;

q = &p;

x = x + *p + **q;

utasítások végrehajtását követően az x változó értéke 6 lesz.

Megjegyezzük, hogy bonyolultabb pointeres kapcsolatok értelmezésében hasznos segítséget jelent a grafikus ábrázolás.

6.1.5. Konstans mutatók

A C++ fordító szigorúan ellenőrzi a const típusú konstansok felhasználását, például egy konstansra csak megfelelő mutatóval hivatkozhatunk:

const double pi = 3.141592565;

// double adatra mutató pointer

double *ppi = &pi; // hiba! ↯

Konstansra mutató pointer segítségével az értékadás már elvégezhető:

// double konstansra mutató pointer const double *pdc;

const double dc = 10.2;

double d = 2012;

pdc = &dc; // a pdc pointer a dc-re mutat

cout <<*pdc<<endl; // 10.2

pdc = &d; // a pdc pointert a d-re állítjuk

cout <<*pdc<<endl; // 2012

A pdc pointer felhasználásával a d változó értéke nem módosítható:

*pdc = 7.29; // hiba! ↯

Konstans értékű pointer, vagyis a mutató értéke nem változtatható meg:

int honap;

// int típusú adatra mutató konstans pointer int *const akthonap = &honap; //

Az akthonap pointer értéke nem változtatható meg, de a *akthonap módosítható!

*akthonap = 9;

cout<< honap << endl; // 9

akthonap = &honap; // hiba! ↯

Konstansra mutató konstans értékű pointer:

const int honap = 10;

const int ahonap = 8;

// int típusú konstansra mutató konstans pointer const int *const akthonap = &honap;

cout << *akthonap << endl; // 10

Sem a mutató, sem pedig a hivatkozott adat nem változtatható meg!

akthonap = &ahonap; // hiba! ↯

*akthonap = 12 // hiba! ↯

6.2. Hivatkozások (referenciák)

A hivatkozási (referencia) típus felhasználásával már létező változókra hivatkozhatunk, alternatív nevet definiálva. A definíció általános formája:

típus &azonosító = változó;

Az & jelzi, hogy referenciát definiálunk, míg az & előtti típus a hivatkozott adat típusát jelöli, melynek egyeznie kell a kezdőértékként megadott változó típusával. Több azonos típusú hivatkozás készítésekor az & jelet minden referencia előtt meg kell adnunk:

típus &azonosító1 = változó1, &azonosító2 = változó2 ;

A referencia definiálásakor a balértékkel történő inicializálás kötelező. Példaként készítsünk hivatkozást az int típusú x változóra!

int x = 2;

int &r = x;

Ellentétben a mutatókkal, a referencia tárolására általában nem jön létre külön változó. A fordító egyszerűen második névként egy új nevet ad az x változónak (r).

Ennek következtében az alábbi kifejezés kiértékelése után 12 lesz az x változó értéke:

r = x +10;

Míg a mutatók értéke, ezáltal a hivatkozott tároló bármikor megváltoztatható, az r referencia a változóhoz kötött.

int x = 2, y = 4;

int &r = x;

r = y; // normál értékadás cout << x << endl; // 4

Ha egy referenciát konstans értékkel, vagy eltérő típusú változóval inicializálunk fordítási hibát kapunk. Még az sem segít, ha a második esetben típus-átalakítást használunk. Az ilyen eseteket csak akkor kezeli a fordító, ha ún. konstans (csak olvasható) referenciát készítünk. Ekkor a fordító először létrehozza a hivatkozás típusával megegyező típusú tárolót, majd pedig inicializálja az egyenlőségjel után szereplő jobbérték kifejezés értékével.

const char &lf = '\n';

unsigned int b = 2004;

const int &r = b;

b = 2012;

cout << r << endl; // 2004

Szinonim referenciatípus szintén létrehozható a typedef kulcsszó segítségével:

typedef int &rint;

int x = 2;

rint r = x;

Mutatóhoz referenciát más típusú változókhoz hasonlóan készíthetünk:

int n = 10;

int *p = &n; // p mutató az n változóra int* &rp = p; // rp referencia a p mutatóra

*rp = 4;

cout << n << endl; // 4

Ugyanez a typedef segítségével:

typedef int *tpi; // egészre mutató pointer típusa typedef tpi &rtpi; // referencia egészre mutató pointerre

int n = 10;

tpi p = &n;

rtpi rp = p;

Felhívjuk a figyelmet arra, hogy referenciához referenciát, illetve mutatót nem definiálhatunk.

int n = 10;

int &r = n;

int& *pr = &r; // hiba! ↯ int& &rr =r; // hiba! ↯ int *p = &r;

// a p mutató az r referencián keresztül az n-re mutat

Referenciát bitmezőkhöz (8. szakasz - Felhasználói típusok) sem készíthetünk, sőt referencia elemeket tartalmazó tömböt (7. szakasz - Tömbök és sztringek) sem hozhatunk létre.

A referencia típus igazi jelentőségét függvények készítésekor fogjuk megtapasztalni.

6.3. Dinamikus memóriakezelés

Általában nem azért használunk mutatókat, hogy más változókra mutassunk velük, bár ez sem elvetendő (lásd paraméterátadás). A mutatók a kulcs a C++ nyelv egyik fontos lehetőségéhez, az ún. dinamikus memóriakezeléshez. Ennek segítségével a program futásához szükséges tárolóterületeket mi foglaljuk le, amikor szükségesek, és mi szabadítjuk fel, amikor már nem kellenek.

A szabad memória (heap) dinamikus kezelése alapvető részét képezi minden programnak. A C nyelv könyvtári függvényeket biztosít a szükséges memóriafoglalási (malloc(),...) illetve felszabadítási (free()) műveletekhez. A C++ nyelvben a new és delete operátorok nyelvdefiníció szintjén helyettesítik a fenti könyvtári függvényeket (bár szükség esetén azok is elérhetők).

A dinamikus memóriakezelés az alábbi három lépést foglalja magában:

• egy szabad memóriablokk foglalása, a foglalás sikerességének ellenőrzésével,

• a terület elérése mutató segítségével,

• a lefoglalt memória felszabadítása.

6.3.1. Szabad memória foglalása és elérése

A dinamikus memóriakezelés első lépése egy szükséges méretű tárterület lefoglalása a szabad memóriából (heap). Erre a célra a new operátor áll rendelkezésünkre. A new operátor az operandusában megadott típusnak megfelelő méretű területet foglal a szabad memóriában, és a terület elejére mutató pointert ad eredményül.

Szükség esetén kezdőértéket is megadhatunk a típust követő zárójelben.

mutató = newtípus;

mutató = newtípus(kezdőérték);

A new segítségével nemcsak egyetlen elemnek, hanem több egymás után elhelyezkedő elemnek is helyet foglalhatunk a memóriában. Ez így létrejövő adatstruktúrát dinamikus tömbnek nevezzük (7. szakasz - Tömbök és sztringek).

mutató = newtípus[elemszám];

Nézzünk néhány példát a new műveletre!

int main() { int *p1;

double *p2;

p1 = new int(2);

p2 = new double;

}

Az definíció hatására a veremben létrejönnek a p1 és p2 mutatóváltozók. Az értékadásokat követően a halomterületen megszületik két dinamikus változó, melyek címe megjelenik a megfelelő mutatókban (I.14. ábra - Dinamikus memóriafoglalás).

I.14. ábra - Dinamikus memóriafoglalás

A memóriafoglalásnál, főleg amikor nagyméretű dinamikus tömb számára kívánunk tárterületet foglalni, előfordulhat, hogy nem áll rendelkezésünkre elegendő, összefüggő, szabad memória. A C++ futtatórendszer ezt a helyzetet a bad_alloc kivétel (exception fejállomány) létrehozásával jelzi. Így a kivételkezelés eszköztárát használva programunkat biztonságossá tehetjük.

#include <iostream>

#include <exception>

using namespace std;

int main() { long * padat;

// Memóriafoglalás try {

padat = new long;

}

catch (bad_alloc) { // Sikertelen foglalás

cerr << "\nNincs eleg memoria!" << endl;

return -1; // Kilépünk a programból }

// ...

// A lefoglalt memória felszabadítása delete padat;

return 0;

}

Amennyiben nem kívánunk élni a kivételkezelés adta lehetőségekkel, a new operátor után meg kell adnunk a nothrow memóriafoglalót (new fejállomány). Ennek hatására a new operátor sikertelen tárfoglalás esetén a kivétel helyett 0 értékkel tér vissza.

#include <iostream>

#include <new>

using namespace std;

int main() { long * padat;

// Memóriafoglalás

padat = new (nothrow)long;

if (0 == padat) {

// Sikertelen foglalás

cerr << "\nNincs eleg memoria!" << endl;

return -1; // Kilépünk a programból }

// ...

// A lefoglalt memória felszabadítása delete padat;

return 0;

}

Az alfejezet végén felhívjuk a figyelmet a new operátor egy további lehetőségére. A new után közvetlenül zárójelben megadott mutató is állhat, melynek hatására az operátor a mutató címével tér vissza (vagyis nem foglal memóriát):

int *p=new int(10);

int *q=new(p) int(2);

cout <<*p << endl; // 2

A fenti példákban a q pointer a p által mutatott területre hivatkozik. A mutatók eltérő típusúak is lehetnek:

long a = 0x20042012;

short *p = new(&a) short;

cout << hex <<*p << endl; // 2012

6.3.2. A lefoglalt memória felszabadítása

A new operátorral lefoglalt memóriablokkot a delete operátorral szabadíthatjuk fel:

delete mutató;

delete[] mutató;

A művelet első formáját egyetlen dinamikus változó felszabadítására, míg a második alakot dinamikus tömbök esetén használjuk.

A delete művelet 0 értékű mutatók esetén is helyesen működik. Minden más, nem new-val előállított érték esetén a delete működése megjósolhatatlan.

A tárterület felszabadításával az előző rész bevezető példája teljessé tehető:

int main() { int *p1;

double *p2;

p1 = new int(2);

p2 = new double;

delete p1;

delete p2;

p1 = 0;

p2 = 0;

}