Metódus: függvények és eljárások összefoglaló neve. Memóriakezelés, dinamikus memóriakezelés Nézzük végig, a C++ memóriakezelését. Alapvetően három fő memóriaterületet különböztetünk meg. Végrehajtási verem (execution stack): elemei az aktivációs rekordok, az éppen futó metódusok találhatóak meg benne, legelső a main metódus, utána következik egy a main függvényben meghívott metódus. A program metódushívások láncolata. Az aktivációs rekordban találhatóak egy függvény paraméterei, lokális változói is. A statikus tömbök is a veremben kapnak helyet. Az itt található változók, paraméterek számára fordítási időben megtörténik a helyfoglalás. Statikus objektumok is az aktivációs rekordban kapnak helyet. Az éppen futó metódusra egy stack pointer mutat, metódusból való visszalépés esetén mindig ez a pointer ugrik az aktuális metódusra. Az aktivációs rekordokban található változókat szokás automatikus változóknak is hívni, hiszen automatikus létrejönnek, mikor a vezérlés eléri az őket tartalmazó blokkot és automatikusan megszűnnek, mikor elhagyjuk az őket tartalmazó blokkot. Dinamikus memória (heap): dinamikus tömbök, objektumok kapnak itt helyet. A heap-en futás időben foglalunk memóriát (allokálunk) new kulcsszó segítségével. A program futás közben az operációs rendszertől kér memóriát a dinamikus területen, az operációs rendszer pedig biztosítja a szükséges helyet, amennyiben még van szabad hely, egyéb esetben hibát kapunk. C++ esetén egy pointert kapunk, ami arra a memória területre mutat, ahol a dinamikus memória területen foglalt hely található, ha nem sikerül a helyfoglalás, akkor null pointert kapunk. A dinamikus memórián foglalt helyek és az ott tárolt értékek nem automatikusak, felszabadításukról gondoskodni kell (deallokáció). Bizonyos nyelvek, pl. Java, beépített mechanizmussal rendelkeznek, melyek detektálják és felszabadítják a használaton kívüli (elérhetetlen) objektumokat. Java-ban ez a garbage collector, pl.: mark-and-sweep garbage collector. C++ esetén a dinamikusan foglalt memória felszabadításáról a programozónak kell gondoskodni, delete kulszó. Ha nem gondoskodunk a dinamikusan foglalt memória felszabadításáról, akkor memória szivárgás (memory leak) történik, azaz a program befejezése után is foglalt marad az adott memóriaterület és elérhetetlenné válik. Statikus terület: speciális rész, a globális változók, statikus adattagok számára. 1
#include <iostream> int main() { int* p = new int(5); std::cout << p << : << *p << std::endl; *p = 3; std::cout << p << : << *p << std::endl; delete p; return 0; Dinamikus tömb Olyan tömb, aminek a mérete futás idejű adat, a heap-en kap helyet. A megfelelő helyen gondoskodnunk kell a tömb felszabadításáról is. Dinamikus tömb segítségével valósítható meg pl. a vektor adatszerkezet is. #include <iostream> int main() { int n; std::cin >> n; int* t = new int[n]; //új n elemű tömb létrehozása a heap-en, futás időben. delete[] t; //t tömb felszabadítása return 0; Karakter konverzió Feladat: szeretnénk karaktereket átkonvertálni más karakterekké, pl. valami egyszerű titkosítás. Van egy 256 elemű karakter tömbünk, benne mindenféle karakter, azt szeretnénk, hogy a konvertálandó karakter kódjának megfelelő indexű elem legyen a konvertálás eredménye. Három különféle megoldást nézünk, a szempontok: gyorsaság, kevés memóriahasználat, karbantarthatóság. 2
char conv (char ch) { static const char cnv[] = { z, y, *, x, w,. a return cnv[ch&0xff]; A függvényben egy lokális statikus konstans tömböt használunk, ez azt jelenti, hogy a tömb létrejön, amikor a vezérlés eléri a függvényt és végig a memóriában is marad, nekem minden hívás során létrehozni. A karakterek is számkánt vannak kezelve, ezért ch segítségével indexelhetjük a tömböt. A probléma azonban az, hogy ha 255-nél nagyobb ch értéke, akkor kiindexelünk a 256 elemű tömbből. A ch&0xff ch&255 kifejezés garantálja, hogy az index biztosan a [0, 255] intervallumba esik. A kifejezést célszerű hexadecimális formában írni, hiszen jobban kifejezi a célt. A & bitenkénti és műveletet jelent, nézzünk erre egy példát: 1 0 1 0 0 1 1 1 0 1 0 0 1 1 1 1 1 1 1 1 0 0 1 0 0 1 1 1 0 1 Az első 8 biten tartalmilag visszakapjuk az eredeti számot, felsőbb helyi értékeken csupa 0 bit marad, ezért ha az eredeti szám sem lehet nagyobb 255-nél, mivel a felsőbb helyi értékeken található 1-es bitek az és művelet miatt 0-ák lesznek. A függvényben használ 256 elemű tömböt pl. script segítségével is fel lehet tölteni. A megoldás hátránya a relatív nagy memória fogyasztás. A kevés memóriahasználatot szem előtt tartva: cahr conv(char ch) { switch(ch) { case A : return t ; break; case G : return x ; break; case m return a ; break; default: return c ; Ebben az esetben hatékonyabb a switch szerkezet használata, mint else-if-eket használni. Ha diszjunkt case ágakat szeretnénk, akkor szükséges a break használata, de itt mivel minden case-ág return kulcsszóval zárul elhagyható lenne. 3
Karbantarthatóság A karbantarthatóságot szem előtt tartva készíthetünk két tömböt, az egyik a konvertálandó karaktereket tartalmazza, a másik az eredményeket. Ennél a megoldásnál hiba lehet, ha a két tömb mérete nem egyezik meg, csak futás időben derülne ki. Tovább javítva ezt a megoldást, használjunk egy tömböt, amiben saját típusú elemeket tárolunk. struct Pair { ; char from; char to; Itt az lehet probléma, hogy ha nem adtuk meg vagy a from vagy a to értékét. Ezt a hibát úgy védhetjük ki, ha létrehozunk egy olyan függvényt, ami biztosítja hogy csak akkor jön létre a Pair típusú objektum, ha mindkét paramétert megadtuk. Ez a függvény lesz a konstruktor, konstruktort struktúrához is írhatunk. struct Pair { ; char from; char to; Pair(char from, char to) { this->from = from; //this a saját objektumra mutató pointer, rajta keresztül tud az objektum a saját helyéről this->to = to; C++-ban a struktúrából is objektum fog képződni, jelen esetünkben, statikus formában a fordító hozza létre és a végrehajtási veremben lesz tárolva. A class és a struct között az alapértelmezett különbség az, hogy ha nem adunk meg a láthatóságra vonatkozó információkat, akkor a struct tagjai alapértelmezetten publikusak, míg a class-é privátok. A fő különbség inkább a használat céljában van. Az osztályok az OOP alapkövei, logikailag összetartozó adatokat és rajtuk értelmezett műveleteket fognak össze. A struktúrák inkább csomagoló szerepet látnak el, összefoghatnak összetartozó dolgokat, adatokat, pl. ha sok mindent kell átadni függvény paraméterként, stb. Pair p; //fordítási hiba lenne, mivel nem rendelkezik paraméter nélküli konstruktorral. Pair p( a, b ); //fordítási időben fog létrejönni a veremben. 4
Referenciák Alapvetően álnevet jelent. A fogalom nagyon hasonlít a pointerekre, a működésük a háttérben pointerekkel van megoldva. A referencia a C nyelvben nem ismert fogalom. A fő különbségek Pointer (C, C++), null pointer változhat, hogy hova mutat nem kötelező inicializálni Referencia (C++) nincs null referencia, mindenképpen hivatkoznia kell valamire mindig ugyanannak az álneve kötelező inicializálni pointer aritmetika - int x = 2; int y = 5; int* p = &x; int& r = x; //innentől r és x elválaszthatatlanok, r x álneve r = 4; *p = 5; p = &y; //OK r = y; //r nem y álneve lesz Az fenti kódrészletben az x változóra tulajdonképpen három néven hivatkozhatunk, értékét p pointer és r referencia segítségével is megváltoztathatjuk. p azonban mutathat máshova is, de r már nem. int& z; //fordítási hiba 5
Függvény paraméterek Érték szerinti paraméter átadás void f(int a, int b) { a = b; int x = 1; int y = 2; f(x, y); std::cout << x <<, << y << std::endl; Mit ír ki a fenti kódrészlet? 1, 2 lesz az eredmény, hiába adtuk értékül az eljárásban a-nak b- t az eredeti változókra ez nem lesz hatással. Az érték szerinti paraméterátadás lényege, hogy a függvény formális paraméterei (jelen esetben a és b) és a függvény aktivációs rekordjában kapnak helyet, tulajdonképpen lokális változók lesznek. Mikor meghívjuk a függvényt, akkor az aktuális paraméterek értékei (itt x, y) belemásolódnak ezekbe a lokális változókba és az ő értékük cserélődik meg. A csere x és y változókra tehát nem lesz hatással. 6
Referencia szerinti void f(int& a, int& b) { a = b; int x = 1; int y = 2; f(x, y); Itt x és y értéke valóban fel fog cserélődni. A referencia szerint paraméterátadás azt jelenti, hogy az f függvény a és b formális paraméterei tulajdonképpen a függvény hívásakor átadott aktuális paraméterek álnevei lesznek. Durván fogalmazva a függvény a változók memória címét használja. Az érték szerinti paraméterátadás hátránya, hogy feleslegesen sok memóriát foglalunk, pl. ha egy nagyméretű objektumot, vagy vektort adunk át akkor az is le fog másolódni. A referencia szerinti paraméterátadás esetén ez a probléma nem áll fenn, mivel ott ugyanarra a változóra hivatkozunk álnéven. De mi van akkor, ha biztosítani szeretnénk, hogy a függvény ne változtathassa meg az eredeti változók értékét, mégis el szeretnénk kerülni a felesleges másolási költségeket? 7
Konstans referencia szerinti void f(const int& a, const int& b) { a = b; //fordítási hiba Ebben az esetben is referencia szerinti hivatkozás történik, de a függvény nem változtathatja meg a paramétereket, ezt a fordító ellenőrzi. Mi a helyzet a tömbökkel? Korábban már láttuk, hogy a tömbök első elemre mutató pointerként adódnák át egy függvénynek, tehát semmi esetben sem másolódnak le, de a pointer is érték szerint adódik át. Nézzünk néhány példát: void g(int& r) { void h(const int& r) { int x = 4; g(x); //OK g(2); //fordítási hiba, számkonstansra nem tudunk referenciát állítani g(x + 3); //fordítási hiba //x + 3 egy temporális érték lesz, nem x értéke változik meg, ezért erre sem tudunk referenciát állítani g(x++); //fordítási hiba, hiszen itt csak x értékét olvassuk ki és adjuk át, temporális érték g(++x); //OK, itt csak megnöveljük x értékét egy-el mielőtt átadjuk h(x); //OK h(x + 4) //OK, konstans referenciával mutathatunk a temporális értékre h(5); //OK h(x++); //OK h(++x); //OK 8
Függvény pointer, függvény referencia Előfordulhat, hogy egy függvénybe valamilyen tevékenységet szeretnénk bejuttatni, egyszóval egy mási függvényt, de nem fixen meghívni, hanem paraméterként adjuk át. Pl.: van egy összegző függvényünk, de azt szeretnénk, hogy tudjunk négyzetszámokat vagy reciprokokat is összegezni és ehhez ne kelljen minden újra megírni, csak a legszükségesebbeket. double sum(double (*a) (int), int n) { double sum = 0; for (int i = 1; i <= n; ++i) { sum += a(i); return sum; double sqr(int x) { return x * x; sum(sqr, 5); Függvény referencia double sum(double a(int), int n) { A függvények is a memóriában kapnak helyet, pointerekkel, referenciákkal hivatkozhatunk rájuk. C++-os inline függvények esetén ez nem működik, de inline függvénynek is lehet függvény paramétere. Függvény pointer esetén megadjuk a függvény visszatérési értékének típusát, a nevét, amivel az adott függvényben hivatkozunk és, paraméter(einek) típusát, formális paraméter nevet nem adunk. C, C++ esetén is működik. A függvény referencia csak C++ esetén működik. 9
Bináris keresőfa Készítsünk egy egyszerűsített bináris keresőfát. A segítségével írjuk ki növekvő sorrendben a standard input-ról érkező számokat. struct Node { ; Node* left; Node* right; int val; Node(const int& val) { left = right = 0; this->val = val; void print(const Node* n) { if (n) { print(n->left); std::cout << n->val << ; print(n->right); void insert(node*& n, int v) { if (n) { else { insert(v <= n->val? n->left : n->right, v); n = new Node(v); 10
void dealloc(node* n) { if (n) { int main() { dealloc(n->left); dealloc(n->right); delete n; Node* root = 0; int i; while (std::cin >> i) { insert(root, i); print(root); dealloc(root); return 0; Az egyszerű bináris keresőfában rendezetten tárolódnak az értékek, minden csúcsra igaz, hogy a nála kisebb értékű elemek tőle balra, a nála nagyobbak tőle jobbra helyezkednek el. A kiíráshoz rekurzív inorder fabejárást alkalmazunk, így növekvő sorrendben kapjuk az eredményt. Az inorder bejárás lényege, hogy minden csúcs esetén először a bal részfát járjuk be, aztán feldolgozzuk az aktuális csúcsot (itt kiírjuk az értékét), végül a jobb részfát dolgozzuk fel. 11
Ha az insert metódusnál lehagyjuk a referenciát és csak sima pointer a paraméter, akkor nem lesz hiba, de a fa nem fog felépülni, hiszen érték szerint adtuk át a pointert, a függvényben belül ugyan megváltozik az értéke, de ez nem lesz hatással az eredeti változóra. Mivel a csúcsokat dinamikusan hoztuk létre, gondoskodnunk kell a felszabadításukról is. Ezt a dealloc eljárás biztosítja postorder fabejárás formájában. A postorder bejárás során először feldolgozzuk egy csúcs bal- és jobboldali részfáit végül pedig az aktuális csúcsot. A felszabadítás során csak így járhatunk el, hiszen ha felszabadítanánk az aktuális csúcsot, mielőtt elrendeztük volna a gyerekeit, akkor elveszítjük az elérést a gyerekeire. 12