OOP alapok Az objektum-orientált programozás (röviden OOP) a természetes gondolkodást, cselekvést közelítő programozási mód, amely a programozási nyelvek tervezésének természetes fejlődése következtében alakult ki. Egy OOP nyelvet három fontos dolog jellemez. Az egységbezárás (encapsulation) azt takarja, hogy az adatstruktúrákat és az adott struktúrájú adatokat kezelő függvényeket kombináljuk; azokat egy egységként kezeljük, és elzárjuk őket a külvilág elől. Az így kapott egységeket objektumoknak nevezzük. Az objektumoknak megfelelő tárolási egységek típusát a C++-ban osztálynak (class) nevezzük. Az öröklés (inheritance) azt jelenti, hogy adott, meglévő osztályokból levezetett újabb osztályok öröklik a definiálásukhoz használt alaposztályok már létező adatstruktúráit és függvényeit. Ugyanakkor újabb tulajdonságokat is definiálhatnak, vagy régieket újraértelmezhetnek. Így egy osztályhierarchiához jutunk.
OOP alapok A többrétűség (polymorphism) alatt azt értjük, hogy egy adott tevékenység (metódus) azonosítója közös lehet egy adott osztályhierarchián belül, ugyanakkor a hierarchia minden egyes osztályában a tevékenységet végrehajtó függvény megvalósítása az adott osztályra nézve specifikus lehet. Az ún. virtuális függvények lehetővé teszik, hogy egy adott metódus konkrét végrehajtási módja csak a program futása során derüljön ki. Ugyancsak a többrétűség fogalomkörébe tartozik az ún. overloading, aminek egy sajátságos esete a C nyelv standard operátorainak átdefiniálása (operator overloading). A C++ egyik fontos tulajdonsága, hogy lehetőségünk van az adataink és az őket manipuláló programkód összeforrasztására, egy egységbe zárására, egy osztályba foglalására. (Ez az ún. encapsulation.)
Például tegyük fel, hogy létrehozunk egy verem adatszerkezetet. (most az egyszerűség kedvéért valósítsuk meg tömbbel).
OOP alapok A hagyományos C-ben az a szokásos megoldás, hogy az adatstruktúráinkat és a hozzájuk tartozó függvényeket egy önálló, külön fordítható forrásmodulban helyezzük el. (akarmi.h) Ez a megoldás már elég elegáns, de az adatok és az őket manipuláló függvények között még nincs explicit összerendeltség, továbbá más programozók egy másik modulból direkt módon is hozzáférhetnek az adatainkhoz, anélkül, hogy az adatok kezelésére szolgáló függvényeinket használnák. Ilyen esetekben az alap-adatstruktúra megváltozása fatális hibát okozhat egy nagyobb project-ben. A példánkban maradva van egy VEREM nevű tömb VMUT nevű változó, egy BETESZ, egy KIVESZ függvény, esetleg egy URES_E függvény. A programozó azonban közvetlenül is manipulálhatja a VEREM tömböt. Adhat neki értéket, stb. Pl. lehet a programban olyan, hogy VEREM[i]=8; Ha valaki közben dinamikus veremre tér át, akkor nincs is VEREM tömb. Minden közvetlen hivatkozás hibaüzenetet eredményez.
OOP alapok A C++-ban speciális tárolási egységfajták, az osztályok szolgálnak arra, hogy adatokat és a hozzájuk rendelt függvényeket egy egységként, egy objektumként kezelhessünk. Az osztályok alaptípusa a C++-ban a class. A C++-ban egy osztály típusú tárolási egység (class) függvényeket (ún. függvénymezőket angolul member functions) kombinál adatokkal (adatmezőkkel - angolul data members), és az így létrejött kombinációt elrejtjük, elzárjuk a külvilág elől. Ezt értjük az egységbezárás alatt. Egy class-deklaráció hasonló a jól ismert struktúradeklarációhoz: class osztálynév { … }; Pl.: class verem {
...
};
Most már használható mint akármelyik típus. Deklarálhatsz ilyen változót (objektumot). verem v1, v2
OOP alapok Öröklés Az objektumok örökölhetnek tulajdonságokat a szülő objektumtól. Nézzünk egy példát. Létre akarunk hozni valami iskolai programot. A képernyőn rendezett kiírást szeretnénk (a Név mindig az 5. pozíción van, a Születési hely a 35.-en, stb. Létrehozunk egy személy osztályt amiben a pozíció és az odamenő függvény van. class szemely { string nev, szulhely, stb; void megjelenit() } Vannak tanárok és tanulók, ezekkel az adatokkal mindenki rendelkezik, de még további tulajdonságokkal is. A tanároknak van szakjuk, stb, a tanulóknak vannak tantárgyaik, gondviselőjük, stb
OOP alapok Akkor most létrehozhatunk olyan objektumokat, amelyek öröklik a meglevő tulajdonságokat: class tanar : szemely { itt jöhetnek a további jellemzők }; class tanulo : szemely { itt a tanulo további jellemzői }; Az öröklés alakja: class osztálynév : szülő osztály neve1, szülő osztály neve2 { }; A többszörös öröklés során a gyermek osztály örökli az összes szülő minden tulajdonságát. Vigyázat!!! A többszörös öröklést nem támogatja minden c++ fordító, a többszörös öröklés csak az AT&T 2.0-ás verziójú C++ fordítójával kompatibilis C++ rendszerekben lehetséges.
OOP alapok A többrétűség (vagy sokalakúság, sokoldalúság) a C++-ban azt jelenti, hogy egy adott őstípusból származtatott további típusok természetesen öröklik az őstípus minden mezőjét, így a függvénymezőket is. De az evolúció során a tulajdonságok egyre módosulnak, azaz például egy öröklött függvénymező neve ugyan nem változik egy leszármazottban, de esetleg már egy kicsit (vagy éppen nagyon) másképp viselkedik. Példánkban a szemely osztály tartalmaz megjelenítés függvényt, ami kiírja az adatokat. A tanar osztály örökli a megjelenítés függvényt, de most nem ott és nem azokat az adatokat jeleníti meg. Ezt a C++-ban a legflexibilisebb módon az ún. virtuális függvények (virtual functions) teszik lehetővé. A virtuális függvények biztosítják, hogy egy adott osztály-hierarchiában (származási fán) egy adott függvény különböző verziói létezhessenek úgy, hogy csak a kész program futása során derül, hogy ezek közül éppen melyiket kell végrehajtásra meghívni.
OOP alapok Ezt a mechanizmust, azaz a hívó és a hívott függvény futási idő alatt történő összerendelését késői összerendelésnek (late binding) nevezzük. Tanultuk már, hogy ugyan azzal a névvel lehet függvényeket létrehozni. A paraméterlistából dönti el a fordító, hogy melyik változatot hívja meg. Ez még fordítási időben történik. Ennek továbbfejlesztése a futási időben történő összerendelés. Az előzőekben láthattuk, hogy egy osztálynak nemcsak adatmezői, hanem függvénymezői (function members) is lehetnek. Hogyan hozhatunk létre függvénymezőket?
OOP alapok vagy az osztály-definíción belül definiáljuk a függvényt (ún. implicit inline függvényként), vagy az osztálydefiníción belül csak deklaráljuk, és azután valahol később definiáljuk. A két módszer különböző szintaxissal rendelkezik. Lássunk egy példát az első lehetőségre: class verem { int vmut, verem[100]; void betesz (int mit) {verem[vmut]=mit; vmut ++;}; }; A másik lehetőség a függvény prototípussal egyezik meg. class verem { int vmut, verem[100]; void betesz (int mit); }; void verem::betesz(int mit) {verem[vmut]=mit; vmut ++;};
OOP alapok Figyeljük meg, hogyan használtuk a :: hatáskört definiáló operátort (scope resolution operator). A verem osztályazonosító szükséges ahhoz, hogy a fordítóprogram tudja, melyik osztályhoz is tartozik a betesz függvénydefiníciója, valamint tudja azt is, hogy mi az érvényességi tartománya. Ez tehát azt jelenti, hogy a verem::betesz függvény (a hagyományos globális változókon kívül) csak a verem osztályhoz tartozó objektumok mezőihez férhet hozzá. Persze létezhet több, más osztályhoz tartozó betesz függvény is. Az osztály-deklaráción belüli függvény-definíció esetében természetesen nem volt szükség az érvényességi tartományt definiáló verem:: előtagra, hiszen a betesz függvény hovatartozása abban az esetben teljesen egyértelmű volt. A hovatartozás definiálásán túlmenően a verem::-nak más szerepe is van. Hatása kiterjed az őt követő függvénydefinícióra oly módon, hogy a betesz függvényen belül a vmut azonosítójú változóra való hivatkozás a verem osztály vmut azonosítójú adatmezőjére vonatkozik, valamint a betesz függvény a verem osztály hatáskörébe kerül.
OOP alapok Elképzelhető, hogy egy származtatott típusban lokálisan deklarálunk egy függvénymezőt, melynek azonosítója és paraméterlistája is megegyezik egy őstípusban definiált függvénymező azonosítójával és paraméterlistájával, ugyanakkor a definíció során fel szeretnénk használni az őstípusbeli változatot. Mivel a későbbi definícióban lévő függvénymező a saját hatáskörében elfedi a korábbi definíciót, a származtatott típusban csak az érvényességi tartományt definiáló operátor segítségével használhatjuk az őstípusban definiált, "elfedett" függvénymezőt. class szemely { string nev, szulido; void megjelen(int sor);}; class tanar : szemely { string dszam, szak1,szak2; void megjelen(int sor);}; void tanar::megjelen(void) { szemely::megjelen(sor); gotoxy(40,sor) cout <
OOP alapok A függvénymezők egy adott típusú adathalmazon végrehajtandó műveleteket jelentenek. Amikor tehát a betesz vagy megjelen függvényt meghívjuk, tudatnunk kell vele azt is, hogy most éppen melyik definiált verem, tanar, szemely típusú objektum-példány (object instance) függvényére van szükségünk. A megoldás a hagyományos C struktúra-mezőkhöz való hozzáférés szintaktikájának kiterjesztése. Az általános szintaxis a következő: objektumnév.függvénymező-név( argumentumlista) Pl. tanar tan1, *tanptr, tanarok[100]; tan1.megjelenit(5); tanarok[23].megjelenit(1); DE!!!
tanptr->megjelen(3);
OOP alapok A this nevű, implicit mutató Egy függvénymezőben direkt módon is hivatkozhatunk arra az objektumra, amelyiknek a függvénymezejét aktivizáltuk. Például a class valami { int m; public: int read_m(void) { return m; } }; valami egyik, masik; deklaráció esetén, az egyik.read_m( ), illetve a masik.read_m( ) függvényhívások esetén az egyik.m, illetve masik.m értéket adja vissza. A függvényhívás során úgy derül ki, hogy melyik objektum adatmezőit kell használni, hogy minden függvénymező számára implicit módon deklarálásra kerül egy this nevű pointer. Ha tehát a read_m függvény egy valami típusú osztály függvénymezője, akkor a read_m-en belül this egy valami* típusú pointer, ilyen módon az első read_m hivatkozásnál this az egyik változóra, míg a második hivatkozás alkalmával a masik-ra mutat. A this mutató explicit módon is megjelenhet a függvénymezők definíciója során: class valami {int m; public: int read_m(void) { return this->m; } };
OOP alapok Konstruktorok és destruktorok
Két speciális, előredefiniált függvénymező-fajta létezik, amelyek kulcsszerepet játszanak a C++ban. Ezek a konstruktorok ( constructors) és a destruktorok (destructors). Általános probléma a hagyományos nyelveknél az inicializálás. Mielőtt használnánk egy adatstruktúrát, gondoskodnunk kell arról, hogy megfelelő tárterületet biztosítsunk az adatstruktúra számára és megfelelő kezdeti értékkel rendelkezzen az adott típusú változó. Pl.: a verem osztálynál a vmut értékét kezdetben nullára kell állítani. A megoldás a konstruktor használata. A konstruktor mindig lefut, ha egy osztálypéldányt létrehozunk. Akkor is ha nem írtuk meg!!! Ekkor egy alapértelmezett konstruktor hajtódik végre.
OOP alapok class verem {int vmut, verem[100]; void betesz(int mit); verem(void) {vmut=0}; }; A konstruktort ugyanúgy deklarálhatjuk, mint minden egyéb függvénymezőt. A konstruktor neve és az osztály neve ugyanaz, mind a kettő verem. Innen tudja a fordítóprogram, hogy az adott függvénymező a konstruktor. Figyeljük meg azt is, hogy mint minden függvénynek, a konstruktornak is lehetnek paraméterei (bár most éppen nincs), a konstruktor törzse pedig egy szabályos függvénytörzs. Az egyetlen, de lényeges különbség, hogy egy konstruktornak nem lehet típusa, még void sem! (Így értéket sem adhat vissza.) Az overloading a konstruktorok esetében is alkalmazható, így egy osztálynak több konstruktora is lehet, és mindig az aktuális paraméterlista alapján dől el, hogy melyik változatot kell aktivizálni. Ha nem definiálunk mi magunk egy konstruktort, akkor a C++ generál egyet, amelynek nincsenek argumentumai.
OOP alapok Lehetséges azonban a következő: class szemely {string nev, szulhely; szemely(string benev="", string beszulhely="") { nev=benev; szulhely=beszulhely;} } Ekkor a szemely ember("Kis Péter"); deklaráció hatására létrejön az ember változó, ami személy osztályú és az ember.nev Kis Péter lesz, az ember.szulhely pedig üres. Általában egy C++ trükk, hogy az általunk definiált konstruktor argumentumaihoz egy alapértelmezést rendelhetünk, mint ahogy azt a szemely::szemely függvény esetében is tettük. Egy speciális konstruktor az ún. másoló konstruktor ( copy constructor). Ha s egy osztály, akkor a hozzá tartozó másoló konstruktor alakja s::s(s&).
OOP alapok Destruktorok definiálása Ahogy konstruktorokat definiálhatunk, ugyanúgy definálhatunk saját magunk destruktorokat is. A statikus objektumok számára a C++ futtató rendszer a main meghívása előtt foglal tárterületet és a main befejezése után felszabadítja azt. Az auto objektumok esetében a tárterület felszabadítás akkor történik meg, amikor az adott változó érvényét veszti (tehát az objektum definíciót tartalmazó blokk végén). Az általunk definiált destruktorokat akkor aktivizálja a C++ futtató rendszer, amikor egy objektumot meg kell szüntetni. class verem {int vmut, verem[100]; void betesz(int mit); verem(void) {vmut=0}; ~verem(void) {cout<<"MEGSZUNT A VEREM";}; };
OOP alapok Mező hozzáférés hozzáférési mód : mezőlista ahol a hozzáférési mód a public, private, vagy protected kulcsszavak egyike lehet. Egy mezőhozzáférést módosító kulcsszó hatása egy következő hasonló kulcsszóig, vagy az őt tartalmazó blokkzáró kapcsos zárójelig tart. Pl.: class valami { public: int a,b; bool c; private: float d; public: valami{cout<<"LETREJOTT";}; ~valami{cout<<"MEGSZUNT";}; }
OOP alapok Általános elvként tekinthetjük azt, hogy az adatmezők privátak vagy védettek és a csak belső műveleteket végrehajtó függvénymezők szintén privátak, míg a külvilággal való kapcsolattartást publikus függvénymezők biztosítják. Ezért kell a class használata, hiszen a class alapértelmezés szerinti mezőhozzáférési szintjei pontosan a fenti kívánalmaknak felelnek meg. A private-ként deklarált mezőket csak az ugyanabban az osztályban deklarált függvénymezők érhetik el. A protected mezőket az ugyanabban az osztályban, és az adott osztályból származtatott további osztályokban definiált függvénymezők érhetik el. A public mezők korlátozás nélkül elérhetők mindenhonnan, ahonnan az adott osztály is elérhető.
OOP alapok Mezőhozzáférés és öröklés
A mezőhozzáférés és az öröklés egymással szoros kapcsolatban állnak. Ennek áttekintéséhez a tanar típusnak a szemely-ből levezetett változatát használjuk úgy, hogy már a mezőhozzáférést is szabályozzuk. class szemely { protected: string nev, szulido; public:void megjelen(int sor);}; class public tanar : szemely { protected: string dszam, szak1,szak2; public: void megjelen(int sor);}; Ez azt eredményezi, hogy a tanar osztály változtatás nélkül örökli a szemely osztaly mezőit és a hozzáférési szinteket is.
OOP alapok Egy származtatott típust általában a következõképpen deklarálhatunk: class D : hozzáférés-módosító B { ... }; ahol D a származtatott osztály típusazonosítója, B pedig az alap osztály típusazonosítója. A hozzáférés-módosító vagy public, vagy private lehet; használata opcionális. A class esetén a hozzáférés-módosító alapértelmezés szerint private. Egy származtatott osztály mezőihez való hozzáférést csak szigorítani lehet az alaptípus egyes mezőinek hozzáférési szintjeihez képest. Ha a hozzáférés-módosító a private, akkor az összes protected és public mező is private lesz. Ha a hozzáférés-módosító a public, akkor a származtatott típus változás nélkül örökli az alaptípus mezőhozzáférési szintjeit. Egy származtatott típus az alaptípus minden mezőjét örökli, de azok közül csak a public és a protected mezőket használhatja korlátozás nélkül. Az alaptípus private mezői a származtatott típusban direkt módon nem érhetők el. Használni kell a rájuk vonatkozó függvényeket…