Ugrás a tartalomhoz

memóriakezelés

A Wikiszótárból, a nyitott szótárból

Kiejtés

  • IPA: [ ˈmɛmoːrijɒkɛzɛleːʃ]

Főnév

memóriakezelés

  1. (informatika) A C++ nyelvben kiemelten fontos a memóriakezelés és az, hogy egy függvény hogyan kapja meg a változókat (paramétereket). Ebben a részletes magyarázatban áttekintjük a különböző paraméterátadási módokat és a memória használatát C++-ban, kódrészletekkel illusztrálva. Az alábbi témákra térünk ki:
  1. Érték szerinti paraméterátadás (Pass by Value)
  2. Mutató szerinti paraméterátadás (Pass by Pointer)
  3. Referencia szerinti paraméterátadás (Pass by Reference)
  4. Mutatók és referenciák használata
  5. Stack és Heap memória különbsége
  6. Dinamikus memóriafoglalás és felszabadítás (new, delete, unique_ptr, shared_ptr)
  7. Példaprogramok és kódrészletek (a fenti technikák bemutatása)

Minden fejezetben rövid magyarázatot és kódpéldát találsz, hogy könnyebben megértsd a fogalmakat.

1. Érték szerinti paraméterátadás (Pass by Value)

Hogyan működik: Érték szerinti paraméterátadáskor a függvény egy másolatot kap a változóról, nem pedig magát az eredeti változót. Amikor meghívunk egy függvényt egy változóval, annak értéke átmásolódik a függvény paraméterébe (C++ Passing By Reference vs Pointer: A Quick Guide). Így a függvényen belül szabadon módosíthatjuk a paramétert, de ez nincs hatással a függvényen kívüli eredeti változóra (C++ Passing By Reference vs Pointer: A Quick Guide). A paraméter másolata a függvény veremmemóriájában (stack) jön létre, és a függvény visszatérésekor automatikusan felszabadul.

Előnyök: - Biztonságos módosítás: A függvény nem tudja megváltoztatni a hívó által átadott eredeti változót, így elkerülhetők a nem szándékos mellékhatások. - Egyszerű használat: A hívás szintaxisa egyszerű (fuggveny(valtozo)), és nem kell mutatókkal vagy referenciákkal foglalkozni. - Kis méretű típusoknál hatékony: Alapvető típusok (pl. int, char) vagy kis méretű struktúrák esetén a másolás költsége elhanyagolható, így egyszerű átadni őket érték szerint.

Hátrányok: - Másolás költsége: Nagyobb méretű objektumok (pl. nagy struct vagy class példányok) esetén a másolat elkészítése sok erőforrást használhat. Ez teljesítménycsökkenést okozhat, különösen ha a függvényt sokszor hívjuk meg. - Nem módosítható az eredeti: Mivel a függvény csak egy másolatot kap, nem tudjuk a függvényen belül módosítani az eredeti változó értékét. Ha a függvénynek az lenne a célja, hogy eredményt adjon vissza a paraméteren keresztül (pl. egy szám növelése), akkor érték szerinti átadásnál ez nem lehetséges közvetlenül. - Memóriahasználat: Minden híváskor létrejön egy új példány a paraméterből a veremben. Sok nagy objektum érték szerinti átadása megnövelheti a memóriafogyasztást.

Példakód – érték szerinti átadás:

#include <iostream>
using namespace std;

void addTen(int value) {
    value += 10; 
    cout << "Függvényben (value): " << value << endl;
}

int main() {
    int a = 5;
    cout << "Hívás előtt a: " << a << endl;
    addTen(a);  // érték szerint adjuk át 'a' értékét
    cout << "Hívás után a: " << a << endl;
    return 0;
}

Magyarázat: A fenti kódban az addTen függvény érték szerint veszi át a paraméterét. A main függvényben lévő a változó értéke 5, ezt átadjuk a függvénynek. A függvényben a value paraméter egy másolat, aminek az értékét megnöveljük 10-zel (így a függvényen belül 15 lesz). A függvény kilépése után azonban a main-beli a változó értéke változatlanul 5 marad, mert az addTen csak a másolatot módosította. A kimenet típikusan ez lesz:

Hívás előtt a: 5  
Függvényben (value): 15  
Hívás után a: 5  

Látható, hogy a függvényben hiába lett 15 a paraméter, a főprogramban a továbbra is 5 maradt (az eredeti nem változott).

Megjegyzés: Ha nagy méretű objektumot kell átadnunk a függvénynek, de nem akarjuk, hogy módosuljon, érdemes lehet const referencia szerint átadni (lásd később), hogy elkerüljük a másolás költségét, de továbbra se módosíthassa a függvény az eredetit.

2. Mutató szerinti paraméterátadás (Pass by Pointer)

Hogyan működik: Mutató (pointer) szerinti átadáskor a függvény egy memóriacímet kap paraméterként. A pointer egy változó, ami egy másik változó memóriacímet tárolja. Ha a hívó fél egy változó címét (& operátorral) adja át, a függvény egy pointeren keresztül érheti el az eredeti változót. Így a pointer dereferálásával (* operátor) a függvény közvetlenül módosíthatja a hívó változójának értékét.

A C++ függvények valójában mindig érték szerint kapják meg a paramétereiket. Mutató átadásnál is a pointer értéke (ami egy memóriacím) kerül másolásra. A különbség az, hogy ez a memóriacím a hívó eredeti változójára mutat, így indirekt módon a függvény eléri és módosítja azt. Ez a technika hasonló ahhoz, ahogyan C nyelvben referenciahatást érünk el pointerekkel.

Mikor érdemes használni: - Ha azt szeretnénk, hogy a függvény módosítsa a változót, és megengedett az is, hogy ne adjunk meg tényleges változót (pl. nullptr értéket is lehessen küldeni). Pointer esetén ugyanis lehetőség van “null pointer” (NULL/nullptr) átadására, jelezve, hogy épp nincs érvényes objektum. Referenciánál ilyen nincs, ott mindig létező objektumra kell hivatkozni (C++ Passing By Reference vs Pointer: A Quick Guide). - Ha a függvény dinamikusan foglal memóriát, és egy pointeren keresztül adja vissza az eredményt a hívónak. Például egy függvény új objektumot hoz létre a heap-en és egy pointert állít be (lásd később a dinamikus memóriakezelést). - Ha több értéket szeretnénk visszaadni egy függvényből kimenő paraméterként. C-ben tipikusan pointereket használnak kimenő paraméterekhez. C++-ban erre jobb a referencia, de pointerrel is megoldható. - Olyan esetekben, amikor a függvény opcionális paraméterként kezel valamit: ha nincs értelme az adott hívásban módosítani, átadhatunk nullptr-t, és ezt a függvény lekezelheti. - C kompatibilitás: Ha C stílusú függvényeket hívunk C++ kódból, gyakran pointereket kell használni (mert a C nem ismer referencia típust).

Előnyök: - Eredeti módosítása: A pointer segítségével a függvény tudja módosítani a hívó változóját (mivel arra mutat). - Opcionális paraméter lehet: A pointer beállítható null értékre (nullptr), így jelezhető a függvény számára, hogy “nincs adat”. Referencia esetén erre nincs lehetőség, mindenféleképp valós objektumot kell átadni (C++ Passing By Reference vs Pointer: A Quick Guide). - Több eredmény visszaadása: Mutatókon (esetleg több mutató paraméteren) keresztül a függvény több kimenő értéket is adhat a hívónak. - Rugalmasság: A pointer átadásával a függvényen belül létrehozhatunk új objektumot is a mutatón (dinamikus foglalás), vagy átiranyíthatjuk másik objektumra (bár ehhez pointer pointerének átadása kell). A pointerek alacsony szintű hozzáférést adnak a memóriához, ezért rugalmasak.

Hátrányok: - Null és érvénytelen pointerek: Ügyelni kell rá, hogy a pointer ne legyen nullptr, mielőtt dereferáljuk. Ha egy pointer nem mutat érvényes memóriahelyre és úgy használjuk (*pointer), az futásidejű hibához (szegmens hiba) vezethet. A referencia ezzel szemben mindig érvényes objektumra utal (nem lehet null), így biztonságosabb ilyen szempontból (C++ Passing By Reference vs Pointer: A Quick Guide). - Bonyolultabb szintaxis: A pointer használata kissé összetettebb – a híváskor & jelet kell tenni, a függvényben pedig *-ot használunk a változó eléréséhez. Ez könnyen vezethet hibához, ha elfelejtjük a megfelelő operátort. Például egy gyakori hiba, hogy valaki dereferálás (*) nélkül próbálja használni a pointert, vagy fordítva. - Memória kezelése: Ha a pointer dinamikus memóriára mutat, a függvénynek és a hívónak egyértelműen meg kell állapodnia abban, hogy ki szabadítja fel azt a memóriát. A pointerek használata felelősséggel jár: figyelni kell a memóriaszivárgásra (memory leak) és a lógó pointerekre (dangling pointer). Lógó pointer keletkezhet például, ha a pointer által mutatott memóriát már felszabadítottuk, de a pointert még használjuk. - Kevesebb védelem: A pointer bárhová mutathat, akár olyan memóriaterületre is, ami nem a várt típusú adat. Ez nagyon rugalmas, de ha rossz címre mutat, a program összeomolhat vagy hibásan működhet. A referencia ezzel szemben típussafe és mindig egy létező objektumra utal (kivéve extrém eseteket, pl. referencia visszaadása lokális változóra, ami kerülendő).

Példakód – pointer szerinti átadás:

#include <iostream>
using namespace std;

void incrementPointer(int *ptr) {
    if (ptr != nullptr) {            // Ellenőrizzük, hogy nem null-pointer
        *ptr = *ptr + 1;             // Növeljük az eredeti változó értékét 1-gyel
    }
}

int main() {
    int b = 10;
    incrementPointer(&b);            // Átadjuk b címét a függvénynek
    cout << "b értéke: " << b << endl;    // b értéke módosult
    
    incrementPointer(nullptr);       // Nem csinál semmit (pointer null), biztonságos hívni
    return 0;
}

Magyarázat: Az incrementPointer függvény egy int *ptr mutatót vár. A main-ben létrehozunk egy b integer változót 10-es értékkel. Meghívjuk incrementPointer(&b)-t, vagyis átadjuk b címét. A függvényben a ptr a b címét tartalmazza. A *ptr = *ptr + 1; sorral a pointer által mutatott értéket növeljük meg 1-gyel. Mivel ptr b-re mutat, ez a művelet valójában b értékét növeli 10-ről 11-re. A függvény visszatérése után a main-ben lévő b valóban 11 lesz, amit ki is írunk. A második hívásban incrementPointer(nullptr) esetén a függvény paramétere NULL pointer, így a függvényben a if (ptr != nullptr) feltétel megakadályozza a dereferálást (nem hajt végre semmit). Ez demonstrálja, hogy pointer átadásnál oda kell figyelni a nullptr esetre is.

A kimenet ez lesz:

b értéke: 11

Az első hívás után b értéke 11 lett, mert pointeren keresztül átadtuk és módosítottuk. (A nullptr-os hívásnak nincs látható hatása.)

Figyelem: Ha incrementPointer-ben elhagytuk volna a nullptr ellenőrzést és egy nullptr-t dereferálunk, a program valószínűleg futás közben hibával leáll. Mindig ellenőrizzük vagy kerüljük el a NULL pointer dereferálást! Hasonlóképp, soha ne használjunk olyan pointert, ami által mutatott memóriát már felszabadítottuk (lógó pointer probléma).

3. Referencia szerinti paraméterátadás (Pass by Reference)

Hogyan működik: A referencia egy C++-ban bevezetett fogalom, amely egy másik változó “álneve” (alias). Referencia szerinti paraméterátadáskor a függvény paramétere egy referencia (pl. int& x). A hívó fél normál módon adja át a változót (nem kell & jel a hívásnál), a fordító automatikusan úgy kezeli, mintha egy pointert adott volna át a háttérben. A referencia valójában belül egy rejtett pointerként is felfogható, de a használata sokkal kényelmesebb: a függvényben a referencia paramétert úgy használhatjuk, mintha az eredeti változó lenne, nem kell dereferálni. Minden művelet, amit a referencián végzünk, az eredeti változón hajtódik végre.

Például:

void f(int &ref) {
    ref = 42;  // ref valójában a hívó által átadott változóra utal
}
...
int a = 0;
f(a);
// f meghívása után a értéke 42 lesz, mert a ref alias-ként az a-t módosította.

A referencia mindig egy létező változóra/objektumra kell hogy mutasson, nem lehet üres vagy null. Emiatt referenciát csak inicializáláskor lehet beállítani, utólag nem változtathatjuk meg, hogy mire hivatkozik.

Előnyök: - Eredeti módosítása egyszerűen: A referenciát a függvényben ugyanúgy használjuk, mint egy normál változót, de valójában a hívó eredeti változóját módosítjuk. Nincs szükség dereferálásra (*) és nem kell a hívásnál sem & operátort használni. Ez tiszta és olvasható szintaxist ad. - Nincs másolási költség: Referencia átadáskor nem készül másolat az átadott értékről, így nagy objektumok esetén is hatékony (ugyanúgy, mint a pointer, elkerüli a másolást) (C++ Passing By Reference vs Pointer: A Quick Guide). A függvény a paraméteren keresztül közvetlenül az eredeti objektummal dolgozik. - Biztonságosabb, mint a pointer: A referencia mindig egy létező objektumra utal (a nyelvi szabályok szerint nem lehet null értékű). Így a függvényben nem kell null-ellenőrzéssel foglalkozni, és kevésbé valószínű, hogy lógó referenciát kapunk (ha betartjuk, hogy ne adjunk vissza referenciát lokális változóra). Mivel a referencia nem “mutathat mellé”, a használata valamivel biztonságosabb, mint a nyers pointereké (C++ Passing By Reference vs Pointer: A Quick Guide). - Konstans referencia: Lehetőség van const referenciát is átadni, amely garantálja, hogy a függvény nem módosítja a paramétert, de másolás nélkül tud olvasni belőle. Ez különösen hasznos nagy objektumoknál input paraméterként (pl. átadunk egy nagy std::string-et const std::string& str formában, így nem másoljuk, de a függvény nem tudja megváltoztatni az értékét).

Hátrányok / veszélyek: - Eredeti módosul: Habár ez előny is lehet, de azt is jelenti, hogy a függvény hívásának mellékhatása van a hívó változóra. Ha a programozó nem figyel oda, a referencia szerinti átadású függvény megváltoztathatja a bemenő adatot, ami esetleg nem várt viselkedés. (Ez igaz pointerre is, de ott a hívásnál látszik a &, ami figyelmeztethet.) - Nem lehet null/reference hiány: Nem jelezhető könnyen, hogy “nincs adat”. Mindenképp egy létező objektumot át kell adnunk. Ha opcionális paramétert akarunk, pointert vagy std::optional-t kell használni, mert referencia nem lehet üres. - Referenciák rejtettsége: A hívás helyén a kód nem különbözteti meg a pass by value vs pass by reference hívást – mindkettő f(valtozo) formájú. Csak a függvény deklarációjából derül ki, hogy valtozo értéke változhat. Emiatt a kód olvasójának tudnia kell, hogy az adott függvény mit vár. (Egyes konvenciók pl. függvénynévben vagy dokumentációban jelzik, ha valami referencia által módosul.) - Élettartam problémák: Bár paraméterátadásnál ez ritkább, de általánosan a referenciákkal vigyázni kell, hogy mindig hosszabb élettartamú objektumra referáljanak, mint ameddig a referenciát használjuk. Például hibás, ha egy függvény lokális változójának referenciáját visszaadjuk visszatérési értékként – az a referencia a függvény kilépése után érvénytelen lesz (dangling reference). Ugyanígy osztály adattagjaként se tároljunk referenciát olyan objektumra, amely esetleg hamarabb megszűnik.

Példakód – referencia szerinti átadás:

#include <iostream>
using namespace std;

void incrementRef(int &ref) {
    ref = ref + 1;   // Közvetlenül a hívó változóját növeli
}

int main() {
    int c = 10;
    incrementRef(c);      // Érték helyett referenciaként adódik át c
    cout << "c értéke: " << c << endl;
    return 0;
}

Magyarázat: Az incrementRef függvény referencia szerint vár egy integer paramétert. A main-ben a c változót adjuk át. A függvényben a ref referencia ugyanarra a memóriaterületre utal, mint a c. A ref = ref + 1; utasítás így c értékét növeli 1-gyel. A függvényből visszatérve a c új értéke 11 lesz, amit kiírunk. A kimenet:

c értéke: 11

Látható, hogy a referencia használatával elértük, hogy a függvény módosítsa a main-beli változót anélkül, hogy visszatérési értéket kellett volna használnia, vagy globális változóhoz nyúlt volna.

Összehasonlítás pointer vs referencia: A pointer és referencia szerinti átadás hasonló célt szolgál (mindkettővel elérhetjük az eredeti változó módosítását a függvényben). A fő különbségek: - A pointer null lehet, a referencia nem (ezért a referencia biztonságosabb ilyen szempontból). - Referenciát kényelmesebb használni (nem kell * és & mindenhol), kevésbé hajlamos hibára. - A pointer rugalmasságosabb: át tudjuk irányítani (pl. ptr++ a következő elemre mutat), míg a referencia mindig ugyanarra utal. - Általában C++-ban, ha nem kell null értéket kezelni, előnyben részesítik a referencia szerinti átadást, mert tisztább és kevésbé hajlamos hibára (C++ Passing By Reference vs Pointer: A Quick Guide) (C++ Passing By Reference vs Pointer: A Quick Guide). A pointert pedig inkább akkor, ha muszáj (pl. opcionális paraméter vagy C API hívása).

Konstans referencia példa:

void printLength(const string &text) {
    // Csak olvassa a text-et, nem módosítja
    cout << "Hossz: " << text.size() << endl;
}

string s = "Hello world!";
printLength(s);  // nagy string érték másolása nélkül adódik át

A fenti printLength függvény nem akarja módosítani a szöveget, csak kiolvassa annak hosszát. Ha érték szerint kapná a string-et, minden híváskor másolni kellene akár egy nagy szöveget is. Ehelyett const string&-ként vesszük át: nem jön létre másolat (tehát hatékony), és a const miatt garantált, hogy a függvény nem változtatja meg a szöveget. Ez a minta gyakori C++-ban a csak olvasási célú paramétereknél, különösen nagyobb objektumok esetén.

4. Mutatók és referenciák használata különböző helyzetekben

Ebben a részben általánosabb példákat és használati mintákat mutatunk be a pointerek (mutatók) és referenciák kapcsán, túl a függvényparaméter átadáson.

Mutató deklarálása és használata: Egy pointer változó típusát a csillag (*) jelöli. Például int *p; egy integerre mutató pointert deklarál. Ahhoz, hogy használni tudjuk, be kell állítanunk egy érvényes címre. Gyakran egy másik változó címével inicializáljuk:

int x = 25;
int *p = &x;      // p értéke x címére állítva
cout << *p << endl;  // kiírja az x értékét (25)

Itt a p pointer az x memóriacímét tárolja. A *p kifejezés dereferálja a pointert, vagyis eléri azt az értéket, amire p mutat – jelen esetben x-et. Ha a dereferált értéket módosítjuk, az x fog változni:

*p = 30;         // az x értékét 30-ra állítja
cout << x << endl;  // kiírja 30-at

Tehát a pointerrel gyakorlatilag tudjuk olvasni és írni azt a változót, amire mutat. A pointer átirányítható másik változóra is:

int y = 100;
p = &y;          // mostantól p már y-ra mutat
cout << *p << endl;  // kiírja 100-at (y értékét)

Referenciák használata változókhoz: Referenciát nem csak paraméterként lehet deklarálni, hanem bármilyen blokkban alias-ként bevezethetünk egy másik változóhoz. Szintaxis: int &alias = eredeti;. Ezután az alias név ugyanarra a memóriára hivatkozik, mint eredeti.

int a = 5;
int &r = a;    // r egy referencia, ami a-ra utal
r = 10;        // r-en keresztül megváltoztatjuk a értékét
cout << a << endl;  // kiírja 10-et

Itt az r referencia úgy működik, mintha két neve lenne ugyanannak a változónak. Bármilyen művelet r-en valójában a-t érinti. Referenciát csak deklarációkor lehet inicializálni, később nem tudjuk máshova “átrendelni”. A fenti példában tehát r mindig a-ra fog hivatkozni.

Pointer aritmetika és tömbök: A pointerekkel lehet ún. pointer-aritmetikát végezni. Ha egy tömb első elemére mutató pointerhez hozzáadunk 1-et, akkor a következő elem címére fog mutatni. Például:

int arr[3] = {10, 20, 30};
int *ptr = arr;       // az arr tömb első elemére mutat (ekvivalens: &arr[0])
cout << *ptr << endl;      // 10
ptr++;                // léptetés a következő elemre
cout << *ptr << endl;      // 20
ptr += 1;             // még egy léptetés (most a harmadik elemre mutat)
cout << *ptr << endl;      // 30

A pointereket így tömbök bejárására is használhatjuk (bár C++-ban erre inkább iterátorokat vagy indexeket használunk, a pointer aritmetika alapja az iterátor működésének is). Figyelem: Csak arra figyeljünk, hogy a pointer ne lépje túl a tömb határait, mert az meghatározatlan viselkedéshez vezet.

Referenciák tömbök és objektumok esetén: Referenciákat általában nem használunk tömbök elemeinek léptetésére, mivel egy referencia fixen egy konkrét elemre mutat. Viszont gyakori minta a range-based for ciklusban referencia használata:

int arr[3] = {1, 2, 3};
for (int &elem : arr) {
    elem *= 2;   // minden elem értékét duplázzuk
}

Itt a for ciklusban az elem egy referencia a tömb soron következő elemére, így közvetlenül módosítjuk a tömb elemeit. Ez egyszerűbb, mint egy indexet használni és arr[i]-t írni, és hatékony is (nem történik másolat az elemről).

Mutató vs referencia összefoglalva:

  • Egy pointer önálló változó, amely tartalmaz egy memóriacímet. Értékét változtatva más címre mutathatunk vele, pointerek lehetnek null értékűek, és pointerekkel műveleteket végezhetünk (inkrementálás, stb.). Kezelni kell az életciklusukat (pl. dinamikus memória esetén felszabadítás).
  • A referencia nem önálló memóriahely, hanem csupán egy másik változó alternatív neve. A deklarációkor kell megadni, hogy mire utal, és utána nem változik. Nem lehet null (hivatalosan a C++ szabályok szerint), és nincs plusz műveleti lehetőség (nincs “referencia aritmetika”). A referenciák használata korlátozottabb, de éppen emiatt biztonságosabb és a kód olvashatóbb lehet.

Példa – pointer és referencia ugyanazon változóhoz:

int n = 5;
int *p = &n;    // pointer n-re mutat
int &r = n;     // referencia n-re
*p = 6;         // pointeren keresztül módosítjuk n-t
cout << r << endl;   // referencia segítségével olvassuk n-t (6)
r = 7;          // referencián keresztül módosítjuk n-t
cout << *p << endl;  // pointeren keresztül olvassuk n-t (7)

Mind a pointer p, mind a referencia r ugyanarra az n változóra utal. Bármelyiken keresztül írjuk az értéket, a másikon keresztül olvasva az a változás látszik. A fenti példa kimenete:

6  
7  

Mutatja, hogy először pointerrel írtunk (n lett 6), majd referencián át olvastuk; utána referencián át írtunk (n lett 7), és pointerrel olvastuk ki.

Összefoglaló tanácsok: - Függvényparaméter: Ha egy függvénynek át kell adnod egy nagyobb objektumot módosítás nélkül, használj const referenciát. Ha módosítani kell az eredetit, használj referenciát (és dokumentáld, hogy a paraméter kimenő is). Ha lehetséges, hogy nincs is érték (opcionális), vagy a referencia nem megfelelő (pl. C API), akkor használj pointert, és kezeld a null értéket. - Dinamika vs statikus: Mutatókat használsz dinamikus memóriafoglaláskor (erről a következő pontban részletesen), referenciát pedig tipikusan nem, mert referencia nem tud új memóriára mutatni, csak ami már van. Dinamikus memória kezeléséhez pointerek vagy okos pointerek kellenek. - Olvashatóság: Referenciát ott használj, ahol egyértelmű, hogy ugyanarról a dologról beszélünk (pl. paraméterek, visszatérési érték, ciklusok). Pointert akkor, ha tényleg szükséges a mutató tulajdonság (null, aritmetika, stb.). Ha pointert használsz valahol, az jelzi, hogy az adott változóval alacsony szintű műveletek, esetleg dinamikus memória van kapcsolatban.

5. Stack és Heap memória különbsége

A C++ program futása során két fő memória területet használ a dinamikus adatokhoz: stack (verem) és heap (kupac) memória. Ezek működése és felhasználási esetei eltérőek:

(CS 225 | Stack and Heap Memory) A verem (stack) és a kupac (heap) memóriaterületek egymáshoz viszonyított helyzete egy folyamat címtartományában. A stack általában a magasabb címtartomány felől növekszik lefelé, míg a heap a szabad memória alacsonyabb címeitől növekszik felfelé.

  • Stack (verem memória): A stack egy speciális memóriaterület, amit a program automatikusan használ a függvényhívásokhoz és a lokális változókhoz. Amikor egy függvény meghívódik, a számára fenntartott stack területen létrejön egy veremkeret (stack frame), benne a függvény lokális változóival és paramétereivel. A függvény visszatérésekor ez a keret automatikusan felszabadul. A stack memória mérete viszonylag korlátozott (néhány MB tipikusan), és LIFO (Last-In First-Out) módon használható: mindig a legutóbb betett adatok szabadulnak fel először. A stacken lévő változók élettartama addig tart, amíg a blokk/függvény fut; utána automatikusan törlődnek. Fontos jellemzői:
    • Nagyon gyors hozzáférés (a stack használata egyszerű címeltolás művelet, tipikusan egy CPU regiszter mutat a stack tetejére).
    • Automatikus felszabadítás: a programozónak nem kell törölnie a stacken lévő változókat, a scope végeztével maguktól megszűnnek.
    • Korlátozott méret: Ha túl sok adatot teszünk a stackre (pl. óriási tömböt lokális változóként, vagy túl mély rekurziót használunk), stack overflow hibát kapunk. A stack memória sosem töredezik, mivel mindig csak a tetejére rakjuk vagy vesszük le az adatokat.
    • Használat: Lokális változók, függvényparaméterek, visszatérési címek tárolása történik itt (C++ ban mi a különbség a Stack és a Heap memória közt?). Például egy int x = 5; lokális változó deklarációja a stacken foglal helyet.
  • Heap (kupac memória): A heap egy másik memóriaterület, amit a program dinamikus memóriafoglalásra használ. A heap memóriát nem kezeli automatikusan a rendszer stack módjára, hanem a programozónak kell kérni (foglalni) és felszabadítani. C++-ban a new operátorral (vagy C-ben a malloc-kal) tudunk a heap-en memóriát foglalni, és a delete (C-ben free) hívással felszabadítani. A heap szabadon elérhető memória, ahonnan különböző méretű blokkokat kérhetünk. A heap-en lévő adatoknak nincs automatikus élettartamuk – addig maradnak lefoglalva, amíg expliciten fel nem szabadítjuk vagy a program be nem fejeződik. Jellemzői:
    • Nagy és rugalmas: A heap tipikusan a program számára elérhető memória nagy részét kiteszi. Ide foglalhatunk nagyon nagy adatstruktúrákat is (akár több száz MB-ot, ha a rendszer engedi), míg a stack erősen korlátozott.
    • Kézi kezelés: A programozó felelőssége a foglalás és felszabadítás. Ha elfelejtjük felszabadítani, memóriaszivárgás keletkezik (a lefoglalt memória nem lesz többé elérhető, de nem is használható újra). Fontos, hogy amit a heap-en lefoglalsz memóriát, azt neked kell felszabadítani – C++-ban new-hez párosítva delete hívással (tömbök esetén delete[]), különben a memória nem szabadul fel (C++ ban mi a különbség a Stack és a Heap memória közt?) (C++ ban mi a különbség a Stack és a Heap memória közt?).
    • Szabad címzés, töredezés: A heap nem strukturált LIFO módon. Bármikor foglalhatunk és felszabadíthatunk memóriát tetszőleges sorrendben. Emiatt idővel a heap töredezetté válhat: a felszabadított blokkok miatt lyukak keletkeznek a memóriatérben. A memóriakezelő rendszer feladata kezelni ezt (például újrafűzheti a szomszédos szabad blokkokat). A stack ezzel szemben mindig összefüggő terület.
    • Lassabb hozzáférés: A dinamikus memóriafoglalás művelete (new vagy malloc) lassabb, mint a stack allokáció, mert a rendszernek meg kell találnia egy megfelelő méretű szabad memóriaterületet a heap-ben. Az oda való írás/olvasás is picit lassabb lehet a cache-hatások miatt, de ez tipikusan kevésbé jelentős, mint a foglalás felszabadítás költsége.
    • Használat: Olyan esetekben használjuk, amikor rugalmasan kell kezelni a memória méretét vagy élettartamát. Például:
      • Nagy adatszerkezetek (pl. nagy vektorok, mátrixok) amik nem férnének el a stacken.
      • Dinamikus struktúrák, mint linkelt listák, fák, ahol futásidőben alkotjuk a csomópontokat.
      • Olyan objektumok, amelyeknek a függvény visszatérte után is élniük kell. Ha egy függvény new-val hoz létre egy objektumot és pointert ad vissza, az objektum a heap-en él és a függvény után is megmarad (a pointert használva érhető el).
      • Abban az esetben is, ha nagyon sok adatot kezelünk, a stack kicsi lehet, ilyenkor a heap a megoldás a tárolásra (C++ ban mi a különbség a Stack és a Heap memória közt?) (C++ ban mi a különbség a Stack és a Heap memória közt?).

Egyszerű példa – stack vs heap:

int main() {
    // Stack allokáció:
    int x = 5;         // 'x' a veremben jön létre, értéke 5
    
    // Heap allokáció:
    int *p = new int;  // foglalás a heap-en egy int méretű helyre
    *p = 10;           // a lefoglalt helyre érték írása
    
    cout << "x: " << x << endl;
    cout << "*p: " << *p << endl;
    
    delete p;          // felszabadítjuk a heap-en foglalt memóriát
    return 0;
}

Magyarázat: A x nevű változó a stacken jön létre automatikusan a main elején. A p egy pointer, ami szintén a stacken van (lokális változó), de new int hatására a heap-en foglaltunk egy integernek helyet, és ennek címét eltároltuk p-ben. Tehát:

  • x (stacken) = 5.
  • A p pointer (stacken) mutat egy heap-en lévő integerre, aminek az értékét 10-re állítjuk.

A cout kiírja: x: 5 és *p: 10. A program vége előtt meghívjuk a delete p; utasítást, ami felszabadítja a heap-en foglalt integer memóriáját. (Ezt mindenképp meg kell tenni, mielőtt a pointer kimegy a hatókörből, különben a 10 értékű integer lefoglalt memóriája ott marad felszabadítatlanul, memóriaszivárgást okozva.)

A main végén a stack is “kiürül”: a lokális változók (x és p) megszűnnek automatikusan. A p pointer egyébként addigra már nem is mutat érvényes helyre, mert töröltük amit mutatott – egy jó szokás ilyenkor a p = nullptr; beállítás törlés után, hogy jelezzük, már nem használható.

Összefoglalva:

  • A stack gyors, automatikus, de korlátozott kapacitású; főleg a függvényeken belüli rövid élettartamú adatokra használjuk.
  • A heap nagy és rugalmas, de manuálisan kell kezelni (foglalni/felszabadítani) és valamivel lassabb; hosszabb élettartamú vagy dinamikus méretű adatokra használjuk.

6. Dinamikus memóriafoglalás és felszabadítás

A C++-ban a dinamikus memóriafoglalást a new kulcsszóval végezhetjük, ami a heap-ről kér memóriát egy adott típus számára, és visszaadja annak pointerét. A lefoglalt memória felszabadítására a delete kulcsszó szolgál. Emellett a modern C++ ajánlja az okos pointerek használatát (std::unique_ptr, std::shared_ptr, stb.) a manuális new/delete helyett, mert ezek automatikusan felszabadítják a memóriát, és nehezebb velük hibázni. Nézzük sorra:

new és delete használata

  • new operátor: Memóriát foglal a heap-en a megadott típusból és visszaad egy mutatót rá. Pl. int *p = new int; lefoglal egy sizeof(int) bájt méretű memóriaterületet a heap-en, és visszaadja annak címét, amit eltárolunk p-ben. A new operátor hívhatja a megfelelő konstruktorokat is, ha objektumot hozunk létre. Lehet inicializálni is:

    int *p1 = new int(42);       // egy int a heap-en, értéke 42
    string *ps = new string("Hello"); // string objektum heap-en, "Hello" értékkel
    

    Tömböt is foglalhatunk:

    int *arr = new int[10];  // 10 elemű int tömb a heap-en, elemei nincsenek inicializálva (random érték vagy 0, típustól függően)
    

    (Alapértelmezés szerint a beépített típusok tömbje nincs nullázva, ha value-inicializálást akarunk: new int[10]() zárójelekkel.)

  • delete operátor: A new-val lefoglalt memóriát szabadítja fel. Ha egy darab objektumot foglaltunk, egyszerű delete pointer; formában kell hívni. Ha tömböt foglaltunk, akkor delete[] pointer; formát kell használni. Nagyon fontos, hogy minden new-hoz tartozzon megfelelő delete, különben a memória felszabadítatlan marad (memóriaszivárgás). Illetve soha ne hívjunk delete-et olyan memóriára, amit nem new-val foglaltunk, és egy pointert csak egyszer szabad delete-elni. Az utóbbi megszegése dupla felszabadítást eredményez, ami szintén hibás működéshez vezet.

Példák new/delete-re:

// 1. Egyszerű foglalás és törlés
int *pi = new int(100);   // egy int foglalása, értéke 100
cout << *pi << endl;      // kiírja: 100
delete pi;                // felszabadítjuk az int-et
pi = nullptr;             // pointer nullára állítása a biztonság kedvéért

// 2. Tömb foglalása és használata
double *pd = new double[5];    // tömb foglalása 5 double számára
for (int i = 0; i < 5; ++i) {
    pd[i] = 0.1 * i;           // feltöltjük értékekkel 0.0, 0.1, 0.2, 0.3, 0.4
}
cout << pd[3] << endl;         // kiírja: 0.3
delete[] pd;                   // felszabadítjuk a tömböt
pd = nullptr;

A fenti kódban látható, hogy az 1. esetben egy integer foglalását oldottuk meg, majd töröltük. A 2. esetben 5 elemű double tömböt foglaltunk, használtuk, majd delete[]-zel töröltük. Mindkét esetben a pointert a törlés után nullra állítottuk – ez nem kötelező, de jó gyakorlat, mert jelzi, hogy a pointer már nem mutat érvényes adatra.

Tipikus hibák, amiket el kell kerülni: - Memory leak (szivárgás): Elfelejtjük a delete-et hívni. Ekkor a program lefutása után az operációs rendszer visszaadja a memóriát, de a program futása közben a szivárgás problémákat okozhat (felesleges memóriafogyasztás, ami extrém esetben kimerítheti a memóriát). - Dangling pointer: Törlés (delete) után tovább használunk egy pointert. A törlés után a pointer még ugyanazt a memóriacímet tartalmazza, de azon a címen már nem a korábbi objektum van (lehet, hogy semmi, vagy később más foglalás kerülhet oda). Ha ilyenkor dereferálunk, az hibás. Ezért szokás nullptr-ra állítani a pointert törlés után – a nullptr dereferálását a program könnyebben észleli (általában azonnal crash), míg a véletlenül érvényesnek tűnő címet nehezebb. - Double delete: Ugyanarra a pointerre kétszer meghívni a delete-et. Az első törli az objektumot, a második már egy érvénytelen területet próbál törölni, ami undefined behavior (általában összeomlik a program). - Helytelen delete[] vs delete: Ha tömböt foglalunk new[]-zal, akkor is sima delete-et hívni undefined behavior. Mindig használjuk a megfelelő szintaxist (delete[] tömbökhöz). Fordítva, ha nem tömböt foglaltunk, akkor nem szabad delete[]-et hívni rá.

Okos pointerek: std::unique_ptr és std::shared_ptr

A C++11 óta rendelkezésünkre állnak az RAII (Resource Acquisition Is Initialization) elvet megvalósító okos pointer osztályok, amelyek megkönnyítik és biztonságosabbá teszik a dinamikus memóriakezelést. A két legfontosabb okos pointer a <memory> header-ben található std::unique_ptr és std::shared_ptr. (Létezik még std::weak_ptr is a shared_ptr-rel együtt használva, és korábban std::auto_ptr, de az utóbbi mára elavult.)

  • std::unique_ptr<T>: Egyedi tulajdonlású pointer egy T típusú erőforrásra. A unique_ptr azt garantálja, hogy adott erőforráshoz (pl. egy heap-en lévő objektumhoz) csak egy unique_ptr tartozik, azaz nem lehet több példányban lemásolni (copy konstruktor = delete). Mozgatni (move) lehet, ekkor az átadja a tulajdonjogot. A unique_ptr automatikusan felszabadítja az erőforrást, amikor maga a pointer objektum megszűnik (pl. kimegy a hatókörből). Ez azt jelenti, hogy ha unique_ptr-t használunk, nem kell kézzel delete-et hívni – determinisztikusan, a destruktorában törli a mutatott objektumot. Használata:

    #include <memory>
    ...
    std::unique_ptr<int> up1(new int(5));       // foglal egy int-et, értéke 5, up1 owns it
    cout << *up1 << endl;                       // kiírja 5
    // Preferált módon használjuk a make_unique segédfüggvényt C++14 óta:
    auto up2 = std::make_unique<string>("Hello");  // string objektum "Hello" értékkel
    cout << up2->size() << endl;                // kiírja a string hosszát (5)
    // Nem kell delete, automatikus felszabadulás a scope végén.
    

    A unique_ptr olyan, mint egy normál pointer, dereferálható (*up1), arrow operátorral taghívás is megy (up2->size()). Nem másolható, tehát auto up3 = up1; nem fordul le. Ha át akarjuk adni másnak a tulajdont, std::move-val kell:

    auto up3 = std::move(up1);
    // Most up3 birtokolja az int-et, up1 üres (nullptr lett az erőforrás pointere).
    

    Tipikusan unique_ptr-t használunk:

    • Dinamikus objektumok tárolására osztályok adattagjaként (hogy maguktól felszabaduljanak a példány megszűnésekor).
    • Konténerekben (pl. vector<std::unique_ptr<T>>), így a konténer elem törlődésekor automatikusan törlődik a mutatott objektum is.
    • Olyan függvény visszatérési értékeként, amelyik dinamikus objektumot hoz létre. Így a hívó felelőssége egyértelmű: kap egy unique_ptr-t és tudja, hogy ő az egyedi tulajdonos, nem felejtődik el a delete (ha nem használja, eldobja, akkor is törlődik).
  • std::shared_ptr<T>: Megosztott tulajdonlású pointer T erőforrásra. A shared_ptr egy pointer, amely mögöttesen tart egy referenciaszámlálót. Többször is lehet másolni, és minden másolat azonos erőforrásra mutat. A referenciaszámláló azt tartja nyilván, hány shared_ptr utal ugyanarra az objektumra. Amikor egy shared_ptr példányt megszüntetünk (pl. kimegy a hatókörből), csökkenti a count-ot. Ha a count 0 lesz, akkor törli a heap-en lévő objektumot. Így éri el a shared_ptr, hogy több tulajdonos esetén is pontosan egyszer töröljön: az utolsó kikapcsolja a villanyt. Használata:

    #include <memory>
    ...
    auto sp1 = std::make_shared<int>(10);   // létrehoz egy int-et értéke 10, sp1 mutat rá
    std::shared_ptr<int> sp2 = sp1;         // másolat, most már ketten mutatják ugyanazt az int-et
    cout << *sp1 << ", count = " << sp1.use_count() << endl;  // kiírja: 10, count = 2
    sp2.reset();                           // sp2 megszűnik (reset), csökkenti a count-ot
    cout << "count = " << sp1.use_count() << endl;            // kiírja: count = 1
    // A main vége után sp1 is megszűnik, utolsóként felszabadítja az int-et.
    

    A make_shared használata ajánlott, mert egy lépésben allokálja a kontroll blokkal együtt az objektumot. A use_count() metódus lekérdezi a referenciaszámlálót (csak debuggolásra használjuk, program logikát ne építsünk rá). A reset() megszünteti a pointer kapcsolatát (így csökkenti a count-ot, és ha ezzel 0-ra ment, törli is az objektumot). Lehet reset-nek paramétert adni, hogy másik erőforrásra mutasson, de jellemzőbb a sima megszüntetés.

    A shared_ptr akkor hasznos, ha:

    • Ugyanazt az erőforrást több helyen is kell használni, és nem egyértelmű, ki felel a törléséért. Pl. graf struktúráknál, ahol több csúcs is mutathat ugyanarra a objektumra.
    • Erőforrásokat szeretnénk aszinkron módon megosztani (pl. több szál is dolgozik ugyanazzal az objektummal). (Bár megjegyzendő, a shared_ptr alapértelmezetten szálbiztos a count kezelésére).
    • Egyszerűsít, hogy nem kell egy tulajdonos objektum élettartamához kötni az erőforrás élettartamát – a shared_ptr önállóan kezeli.

    Óvatosan a körkörös hivatkozással: Ha két objektum shared_ptr-rel hivatkozik egymásra, akkor ciklus alakul ki, és a referenciaszámláló sosem megy le nullára (pl. A tart egy shared_ptr-t B-re, B tart egyet A-ra). Emiatt a memória nem szabadul fel, még ha kívülről már senki nem hivatkozik rájuk. Az ilyen helyzeteket el kell kerülni, vagy std::weak_ptr használatával megtörni a ciklust (a weak_ptr olyan, mint a shared_ptr gyenge mása: nem növeli a count-ot, csak megfigyeli az objektumot).

Mikor melyiket használjuk? A legtöbb esetben a unique_ptr elegendő és hatékonyabb. Csak akkor használjunk shared_ptr-t, ha valóban több tulajdonosra van szükségünk. Általános irányelv: “Ha okos pointer kell, használd unique_ptr-t, és csak akkor válts shared_ptr-re, ha kifejezetten megosztott tulajdonlás szükséges.” (c++ - Why/when should I use std::unique/shared_ptr (std::vector<>) over just std::vector<>? - Stack Overflow). A shared_ptr kis mértékű memória- és teljesítmény overhead-del jár (a referenciakövetés miatt), ezért feleslegesen ne használjuk.

Példakód – unique_ptr és shared_ptr:

#include <memory>
#include <iostream>
using namespace std;

struct Node {
    int value;
    Node(int v) : value(v) {
        cout << "Node(" << value << ") létrehozva\n";
    }
    ~Node() {
        cout << "Node(" << value << ") törölve\n";
    }
};

int main() {
    // unique_ptr példa
    unique_ptr<Node> ptr1 = make_unique<Node>(1);
    // unique_ptr<Node> ptr2 = ptr1;  // Fordítási hiba! Nem másolható.
    unique_ptr<Node> ptr2 = std::move(ptr1);   // Átadjuk a tulajdonjogot ptr1 -> ptr2
    cout << "ptr1 " << (ptr1 ? "nem null" : "null") << endl;
    cout << "ptr2 értéke: " << ptr2->value << endl;
    // ptr2 automatikusan törli a Node(1)-et a main végekor

    // shared_ptr példa
    auto sp1 = make_shared<Node>(2);
    {
        shared_ptr<Node> sp2 = sp1;
        cout << "sp1 use_count: " << sp1.use_count() << endl;  // 2 tulajdonos (sp1, sp2)
    }
    // Itt sp2 kiment a blokkból, egyedül sp1 maradt -> Node(2) még él
    cout << "sp1 use_count: " << sp1.use_count() << endl;      // 1 tulajdonos
    // A main vége után sp1 törlődik, ekkor törli a Node(2)-t is.

    return 0;
}

Magyarázat: A fenti programban definiáltunk egy Node struktúrát, ami létrehozáskor és törléskor kiír valamit (így követhetjük, mikor szabadul fel).

  • Először a unique_ptr példában ptr1 kap egy új Node(1) objektumot. Majd ptr2 = std::move(ptr1) átveszi tőle. Ekkor ptr1 üressé válik (nem mutat semmire), ptr2 pedig egyedüliként birtokolja a Node(1)-et. A main végén ptr2 megsemmisül, ekkor automatikusan meghívódik a delete a Node-ra (láthatjuk a kimenetben a destruktor üzenetét).
  • A shared_ptr példában sp1 hoz létre egy Node(2)-t. Bekerül egy blokkba, ahol készítünk egy sp2 shared_ptr-t, ami lemásolja sp1-et (így ketten mutatják Node(2)-t). A blokk végén sp2 megszűnik, így egyedül sp1 marad tulajdonos. A program végén sp1 is megszűnik, ekkor törli a Node(2)-t. A kimenet jelzi a létrehozásokat és törléseket, pl.:
Node(1) létrehozva  
ptr1 null  
ptr2 értéke: 1  
Node(2) létrehozva  
sp1 use_count: 2  
sp1 use_count: 1  
Node(1) törölve  
Node(2) törölve  

Látható, hogy a Node(1) törlése a main végén történt (unique_ptr által), míg a Node(2) törlése szintén a program végén (shared_ptr utolsó példánya által).

Az okos pointerek használatával elkerültük a delete explicit hívását. Ez nemcsak kevesebb kód, de biztonságosabb is: nehezebb elfelejteni törölni (mert automatikus), és nem lehet duplán törölni (mert a pointer önmaga csak egyszer törli). Mindig preferáljuk az okos pointereket a raw new/delete helyett, hacsak nincs különösebb okunk raw pointert használni. Sok esetben pedig még okos pointer sem kell, próbáljuk meg lokális változóként vagy konténerben tárolni az objektumot, így a stack-en vagy a konténer által kezelve szintén automatikusan felszabadul.

7. Példaprogramok és kódrészletek

Az alábbiakban egy átfogó példaprogramot mutatunk, amely felhasználja a különböző paraméterátadási módokat és a dinamikus memória kezelésének technikáit. A program három függvényt definiál a számok felcserélésére (swap) érték szerint, pointerrel és referenciával, így összehasonlítható az eredmény. Emellett bemutatja a stack vs heap viselkedést egy lokális és egy dinamikusan foglalt változóval, majd használ unique_ptr-t és shared_ptr-t is.

#include <iostream>
#include <memory>
using namespace std;

// Swap függvények különböző paraméterátadással
void swapByValue(int a, int b) {      // Érték szerint (másolatok)
    int temp = a;
    a = b;
    b = temp;
}

void swapByPointer(int *a, int *b) {  // Pointerek (címek) átadása
    if (a == nullptr || b == nullptr) return;
    int temp = *a;
    *a = *b;
    *b = temp;
}

void swapByReference(int &a, int &b) { // Referenciák átadása
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 1, y = 2;
    cout << "Kezdetben: x=" << x << ", y=" << y << endl;
    swapByValue(x, y);
    cout << "swapByValue után: x=" << x << ", y=" << y << "  (nincs változás)\n";
    swapByPointer(&x, &y);
    cout << "swapByPointer után: x=" << x << ", y=" << y << "  (felcserélve)\n";
    // Visszacseréljük értéküket eredetire referencia segítségével
    swapByReference(x, y);
    cout << "swapByReference után: x=" << x << ", y=" << y << "  (ismét eredeti)\n\n";

    // Stack vs Heap példa
    int stackVar = 42;
    int *heapVar = new int(42);
    cout << "Stack változó értéke: " << stackVar << endl;
    cout << "Heap változó értéke: " << *heapVar << endl;
    // Címek összehasonlítása (nem garantált, de jellemzően a stack címe nagyobb mint a heap címe)
    cout << "Stack változó címe: " << &stackVar << endl;
    cout << "Heap változó címe: " << heapVar << endl;
    delete heapVar; // felszabadítás
    heapVar = nullptr;
    cout << endl;

    // unique_ptr és shared_ptr példa
    auto ptr = make_unique<int>(100);
    cout << "*ptr (unique_ptr): " << *ptr << endl;
    {
        auto sh1 = make_shared<int>(200);
        auto sh2 = sh1; // megosztott ownership
        cout << "*sh1: " << *sh1 << ", *sh2: " << *sh2 
             << " (use_count=" << sh1.use_count() << ")\n";
    } // sh2 scope vége, 200-as érték még nem törlődik, mert sh1 még él
    // Itt sh1 már out of scope lenne, de mivel make_shared-et blokkban használtuk, 
    // nem látjuk sh1-et. Tegyük globálissá vagy kiiratáshoz külön blokkba.
    return 0;
}

Magyarázat a példaprogramhoz:

  • A swapByValue(int a, int b) függvény érték szerint kapja a paramétereket, így nem cseréli fel tartósan x és y értékét – a főprogramban nem lesz változás a hívás után.
  • A swapByPointer(int *a, int *b) pointereken keresztül kapja meg x és y címét, így dereferálva (*a, *b) felcseréli az eredeti változók értékét. A főprogramban a hívás után x és y értékei valóban felcserélődnek.
  • A swapByReference(int &a, int &b) referenciákkal éri el x-et és y-t, és felcseréli. Ez is hatással van a főprogrambeli változókra. (Ebben az esetben ugyanazt csinálja mint a pointeres verzió, csak szintaktikailag egyszerűbben.)
  • A főprogram kiírja a kezdeti értékeket, majd mindegyik swap után az eredményt. Érték szerint nem változnak, pointerrel és referenciával igen.

Kimenetrészlet (első fele) tipikusan:

Kezdetben: x=1, y=2  
swapByValue után: x=1, y=2  (nincs változás)  
swapByPointer után: x=2, y=1  (felcserélve)  
swapByReference után: x=1, y=2  (ismét eredeti)  
  • A stack vs heap résznél létrehozunk egy stackVar int-et a stacken és egy heapVar int-et a heap-en (értékük mindkettőnek 42). Kiírjuk az értékeiket és a címeiket. Általában a stack címei magasabb memória-tartományban vannak, a heap alacsonyabban (ez platformfüggő, de a beillesztett ábra szerint is a stack “felül” van, a heap alatta). Ezt nem garantálja semmi, de gyakran megfigyelhető különbség. A lényeg, hogy külön memóriaterületről vannak. A heapVar törlése után beállítjuk nullra a pointert.
  • Végül a unique_ptr/shared_ptr résznél:
    • Készítünk egy unique_ptr<int>-et, ami 100-ra inicializált int-re mutat. Kiírjuk az értékét.
    • Majd egy belső blokkban egy shared_ptr<int>-et hozunk létre 200 értékkel (sh1), és készítünk egy második shared_ptr<int>-et (sh2), ami ugyanarra mutat (tehát másoljuk a pointert). Kiírjuk mindkettő értékét (nyilván ugyanaz, 200) és a use_count értéket, ami 2 lesz. A blokk végén sh2 megsemmisül, így a 200-as értékű int még nem törlődik, mert sh1 még élt a blokkban (itt a példa kód végén sh1 is blokkban van, így a main végére az is megszűnik – ott törlődik végleg az int).

A program második felének kimenete például így alakulhat:

Stack változó értéke: 42  
Heap változó értéke: 42  
Stack változó címe: 0x7ffeefbff5bc  
Heap változó címe: 0x558a260c8e70  

*ptr (unique_ptr): 100  
*sh1: 200, *sh2: 200 (use_count=2)  

(Látható, hogy a stack változó címe magasabb memóriacím, mint a heap-é, de ez környezetfüggő.)

A program befejeződésével minden erőforrás szabályosan felszabadul: - A swap függvények lokális változói a stackről automatikusan törlődtek a hívások után. - A heap-ről foglalt heapVar-t manuálisan töröltük. - A unique_ptr automatikusan törölte a 100-as értékű intet. - A shared_ptr utolsó példánya automatikusan törölte a 200-as értékű intet.

Ezzel a példával látható, hogyan használjuk helyesen a különböző technikákat a memóriakezelésben.


Fordítások