• Nem Talált Eredményt

Adatok és műveletek egybeépítése

III. fejezet - Objektum-orientált programozás C++ nyelven

2. Osztályok és objektumok

2.1. A struktúráktól az osztályokig

2.1.2. Adatok és műveletek egybeépítése

Az class és a struct osztályok deklarációját a C++ programban bárhol elhelyezhetjük, ahol deklaráció szerepelhet, azonban a modul szintű (fájl szintű) megadás felel meg leginkább a modern programtervezési módszereknek.

2.1. A struktúráktól az osztályokig

Ebben a fejezetben a már meglévő (struct típus) ismereteinkre építve lépésről-lépésre jutunk el az objektumok alkalmazásáig. Az alfejezetekben felvetődő problémákat először hagyományos módon oldjuk meg, majd pedig rátérünk az objektum-orientált gondolkodást követő megoldásra.

2.1.1. Egy kis ismétlés

Valamely feladat megoldásához szükséges adatokat már eddigi ismereteinkkel is össze tudjuk fogni a struktúra típus alkalmazásával, amennyiben az adatok tárolására a struktúra tagjait használjuk:

struct Alkalmazott{

int torzsszam;

string nev;

float fizetes;

};

Sőt műveleteket is definiálhatunk függvények formájában, amelyek argumentumként megkapják a struktúra típusú változót:

void BertEmel(Alkalmazott& a, float szazalek) { a.ber *= (1 + szazalek/100);

}

A struktúra tagjait a pont, illetve a nyíl operátorok segítségével érhetjük el, attól függően, hogy a fordítóra bízzuk a változó létrehozását, vagy pedig magunk gondoskodunk a területfoglalásról:

int main() {

Alkalmazott mernok;

mernok.torzsszam = 1234;

mernok.nev = "Okos Antal";

mernok.ber = 2e5;

BertEmel(mernok,12);

cout << mernok.ber << endl;

Alkalmazott *pKonyvelo = new Alkalmazott;

pKonyvelo->torzsszam = 1235;

pKonyvelo->nev = "Gazdag Reka";

pKonyvelo->ber = 3e5;

BertEmel(*pKonyvelo,10);

cout << pKonyvelo->ber << endl;

delete pKonyvelo;

}

Természetesen a fent bemutatott módon is lehet strukturált felépítésű, hatékony programokat fejleszteni, azonban ebben a fejezetben mi tovább megyünk.

2.1.2. Adatok és műveletek egybeépítése

Első lépésként - a bezárás elvének (encapulation) megfelelően - az adatokat és a rajtuk elvégzendő műveleteket egyetlen programegysége foglaljuk, azonban ezt a programegységet már, bár struktúra osztálynak nevezzük.

struct Alkalmazott { (objektumot), hiszen alapértelmezés szerint az objektumon végez műveletet. Az Alkalmazott típusú objektumok használatát bemutató main() függvény is valamelyest módosult, hiszen most már a változóhoz tartozó

Alkalmazott *pKonyvelo = new Alkalmazott;

pKonyvelo->torzsszam = 1235;

pKonyvelo->nev = "Gazdag Reka";

pKonyvelo->ber = 3e5;

pKonyvelo->BertEmel(10);

cout << pKonyvelo->ber << endl;

delete pKonyvelo;

}

2.1.3. Adatrejtés

Az osztály típusú változók (objektumok) adattagjainak közvetlen elérése ellentmond az adatrejtés elvének.

Objektum-orientált megoldásoknál kívánatos, hogy az osztály adattagjait ne lehessen közvetlenül elérni az objektumon kívülről. A struct típus alaphelyzetben teljes elérhetőséget biztosít a tagjaihoz, míg a class típus teljesen elzárja a tagjait a külvilág elől, ami sokkal inkább megfelel az objektum-orientált elveknek. Felhívjuk a figyelmet arra, hogy az osztályelemek elérhetőségét a private, protected és public kulcsszavak segítségével magunk is szabályozhatjuk.

A public tagok bárhonnan elérhetők a programon belül, ahonnan maga az objektum elérhető. Ezzel szemben a private tagokhoz csak az osztály saját tagfüggvényeiből férhetünk hozzá. (A protected elérést a 3. szakasz - Öröklés (származtatás) tárgyalt öröklés során alkalmazzuk.)

Az osztályon belül tetszőleges számú tagcsoportot kialakíthatunk az elérési kulcsszavak (private, protected, public) alkalmazásával, és a csoportok sorrendjére sincs semmilyen megkötés.

A fenti példánknál maradva, a korlátozott elérés miatt szükséges további tagfüggvényeket megadnunk, amelyekkel ellenőrzött módon beállíthatjuk (set), illetve lekérdezhetjük (get) az adattagok értékét. A beállító függvényekben a szükséges ellenőrzéseket is elvégezhetjük, így csak érvényes adat fog megjelenni az Alkalmazott típusú objektumokban. A lekérdező függvényeket általában konstansként adjuk meg, ami azt jelöli, hogy nem módosítjuk az adattagok értékét a tagfüggvényből. Konstans tagfüggvényben a függvény feje és törzse közé helyezzük a const foglalt szót. Példánkban a GetBer() konstans tagfüggvény.

class Alkalmazott{

nev = n;

mernok.SetAdatok(1234, "Okos Antal", 2e5);

mernok.BertEmel(12);

cout << mernok.GetBer() << endl;

Alkalmazott *pKonyvelo = new Alkalmazott;

pKonyvelo->SetAdatok(1235, "Gazdag Reka", 3e5);

pKonyvelo->BertEmel(10);

cout << pKonyvelo->GetBer() << endl;

delete pKonyvelo;

}

Megjegyezzük, hogy a konstans tagfüggvényekből is megváltoztathatunk adattagokat, amennyiben azokat a mutable (változékony) kulcsszóval deklaráljuk, például:

mutable float ber;

Az ilyen megoldásokat azonban igen ritkán alkalmazzuk.

Megjegyezzük, ha egy osztály minden adattagja nyilvános elérésű, akkor az objektum inicializálására a struktúráknál bemutatott megoldást is használhatjuk, például:

Alkalmazott portas = {1122, "Biztos Janos", 1e5};

Mivel a későbbiek folyamán a fenti forma használhatóságát további megkötések korlátozzák (nem lehet származtatott osztály, nem lehetnek virtuális tagfüggvényei), ajánlott az inicializálást az osztályok speciális tagfüggvényeivel, az ún. konstruktorokkal elvégezni.

2.1.4. Konstruktorok

Az osztályokat használó programokban egyik leggyakoribb művelet az objektumok létrehozása. Az objektumok egy részét mi hozzuk részre statikus vagy dinamikus helyfoglalással (lásd fent), azonban vannak olyan esetek is, amikor a fordítóprogram készít ún. ideiglenes objektumpéldányokat. Hogyan gondoskodhatunk a megszülető objektumok adattagjainak kezdőértékkel való (automatikus) ellátásáról? A választ a konstruktornak nevezett tagfüggvények bevezetésével találjuk meg.

}

dolgozo.SetNev("Kiss Pista");

Alkalmazott mernok(1234, "Okos Antal", 2e5);

mernok.BertEmel(12);

cout << mernok.GetBer() << endl;

Alkalmazott fomernok = mernok;

// vagy: Alkalmazott fomernok(mernok);

fomernok.BertEmel(50);

cout << fomernok.GetBer() << endl;

Alkalmazott *pDolgozo = new Alkalmazott;

pDolgozo->SetNev("Kiss Pista");

delete pDolgozo;

Alkalmazott *pKonyvelo;

pKonyvelo = new Alkalmazott(1235, "Gazdag Reka", 3e5);

pKonyvelo->BertEmel(10);

cout << pKonyvelo->GetBer() << endl;

delete pKonyvelo;

Alkalmazott *pFomernok=new Alkalmazott(mernok);

pFomernok->BertEmel(50);

cout << pFomernok->GetBer() << endl;

delete pFomernok;

}

A fenti példában – a függvénynevek túlterhelését alkalmazva - készítettünk egy paraméter nélküli, egy paraméteres és egy másoló konstruktort. Látható, hogy a konstruktor olyan tagfüggvény, amelynek neve megegyezik az osztály nevével, és nincs visszatérési típusa. Az osztály konstruktorát a fordító minden olyan esetben automatikusan meghívja, amikor az adott osztály objektuma létrejön. A konstruktor nem rendelkezik visszatérési értékkel, de különben ugyanúgy viselkedik, mint bármely más tagfüggvény. A konstruktor átdefiniálásával (túlterhelésével) többféleképpen is inicializálhatjuk az objektumokat.

A konstruktor nem foglal tárterületet a létrejövő objektum számára, feladata a már lefoglalt adatterület inicializálása. Ha azonban az objektum valamilyen mutatót tartalmaz, akkor a konstruktorból kell gondoskodnunk a mutató által kijelölt terület lefoglalásáról.

Egy osztály alapértelmezés szerint két konstruktorral rendelkezik: a paraméter nélküli (default) és a másoló konstruktorral. Ha valamilyen saját konstruktort készítünk, akkor a paraméter nélküli alapértelmezett (default) konstruktor nem érhető el, így azt is definiálnunk kell. Saját másoló konstruktort általában akkor használunk, ha valamilyen dinamikus tárterület tartozik az osztály példányaihoz.

A paraméter nélküli és a paraméteres konstruktort gyakran összevonjuk az alapértelmezés szerinti argumentumok bevezetésével:

nev = n;

A konstruktorokból az osztály tagjait kétféleképpen is elláthatjuk kezdőértékkel. A hagyományosnak tekinthető megoldást, a konstruktor törzsén belüli értékadást már jól ismerjük. Emellett a C++ nyelv lehetővé teszi az ún.

taginicializáló lista alkalmazását. Az inicializáló listát közvetlenül a konstruktor feje után kettősponttal elválasztva adjuk meg. A vesszővel tagolt lista elemei az osztály adattagjai, melyeket zárójelben követnek a kezdőértékek. A taginicializáló lista bevezetésével a fenti példák konstruktorai üressé válnak:

class Alkalmazott{

Alkalmazott(int tsz=0, string n="", float b=0) : torzsszam(tsz), nev(n), ber(b) { }

Alkalmazott(const Alkalmazott & a)

: torzsszam(a.torzsszam), nev(a.nev), ber(a.ber) { }

}

Szükséges megjegyeznünk, hogy a konstruktor hívásakor az inicializáló lista feldolgozása után következik a konstruktor törzsének végrehajtása.

2.1.4.2. Az objektumok explicit inicializálása

Egyparaméteres konstruktorok esetén a fordító – szükség esetén - implicit típus-átalakítást használ a megfelelő konstruktor kiválasztásához. Az explicit kulcsszó konstruktor előtti megadásával megakadályozhatjuk az ilyen konverziók alkalmazását a konstruktorhívás során.

Az a objektum létrehozásakor az explicit konstruktor hívódik meg, míg a b objektum esetén a float paraméterű.

Az explicit szó elhagyásával mindkét esetben az első konstruktor aktiválódik.

2.1.5. Destruktor

Gyakran előfordul, hogy egy objektum létrehozása során erőforrásokat (memória, állomány stb.) foglalunk le, amelyeket az objektum megszűnésekor fel kell szabadítanunk. Ellenkező esetben ezek az erőforrások elvesznek a programunk számára.

A C++ nyelv biztosít egy speciális tagfüggvényt - a destruktort - amelyben gondoskodhatunk a lefoglalt erőforrások felszabadításáról. A destruktor nevét hullám karakterrel (~) egybeépített osztálynévként kell megadni. A destruktor, a konstruktorhoz hasonlóan nem rendelkezik visszatérési típussal.

Az alábbi példában egy 12-elemű, dinamikus helyfoglalású tömböt hozunk létre a konstruktorokban, az alkalmazottak havi munkaidejének tárolására. A tömb számára lefoglalt memóriát a destruktorban szabadítjuk fel.

Alkalmazott(const Alkalmazott & a) { torzsszam = a.torzsszam;

void SetMunkaora(int honap, int oraszam) { if (honap >= 1 && honap <=12) {

Alkalmazott mernok(1234, "Okos Antal", 2e5);

mernok.BertEmel(12);

mernok.SetMunkaora(3,192);

cout << mernok.GetBer() << endl;

Alkalmazott *pKonyvelo;

pKonyvelo = new Alkalmazott(1235, "Gazdag Reka", 3e5);

pKonyvelo->BertEmel(10);

pKonyvelo->SetMunkaora(1,160);

pKonyvelo->SetMunkaora(12,140);

cout << pKonyvelo->GetBer() << endl;

delete pKonyvelo;

}

A lefordított program minden olyan esetben meghívja az osztály destruktorát, amikor az objektum érvényessége megszűnik. Kivételt képeznek a new operátorral dinamikusan létrehozott objektumok, melyek esetén a

destruktort csak a delete operátor segítségével aktivizálhatjuk. Fontos megjegyeznünk, hogy a destruktor nem magát az objektumot szűnteti meg, hanem automatikusan elvégez néhány általunk megadott „takarítási”

műveletet.

A példaprogram futtatásakor az alábbi szöveg jelenik meg:

224000 330000

Gazdag Reka torolve Okos Antal torolve

Ebből láthatjuk, hogy először a *pKonyvelo objektum destruktora hívódik meg a delete operátor használatakor.

Ezt követően a main() függvény törzsét záró kapcsos zárójel elérésekor automatikusan aktiválódik a mernok objektum destruktora.

Amennyiben nem adunk meg destruktort, a fordítóprogram automatikusan egy üres destruktorral látja el az osztályunkat.