Visual C++ osztály készítése, adattagok, és metódusok, láthatóság, konstruktor, destruktor. Objektum létrehozása, használata, öröklés. Az osztály egy olyan típus leíró struktúra, amely tartalmaz adattagokat és metódusokat, amelyek segítségével az adattagokon műveleteket végezhetünk. Az osztályt, mint típust új objektumok definiálására használjuk, hogy azzal további műveleteket végezzünk. Emellett maximálisan támogatja az öröklődést, a polimorfizmust(virtuális függvények) és az egységbezárást, amik az objektum orientált programozás alapkövei. Példa: A példában a Rajzol, VonalRajzol egy elképzelt függvény, KisNyul is lehetne a helyén. Szemléltetésre szolgál. class CTeglalap { int Bal; int Felso; int Jobb; int Also; void Rajzol(void) { VonalRajzol(Bal,Felso,Jobb,Felso); VonalRajzol(Jobb,Felso,Jobb,Also); VonalRajzol(Jobb,Also,Bal,Also); VonalRajzol(Bal,Also,Bal,Felso); } }; A Bal, Felso, Jobb, Also változókat adattagoknak, a Rajzol metódust pedig tagfüggvénynek nevezzük. Egy osztálydefiníció egy új típust hoz létre,. Az osztály használatához létre kell hozni a típushoz tartozó objektumot. Ezt nevezzük az osztály példányának. Az osztály objektumát a beépített típusokba tartozó változókhoz hasonlóan vehetjük fel. Másik megoldással, ha dinamikusan akarjuk létrehozni és törölni, akkor a new és delete operátorokkal tehetjük ezt meg. Példa: CTeglalap Tegla; Ez a CTeglalap egy példányát(objektumát) hozza létre. Ez automatikusan memóriát foglal le az objektum(példány) részére, és már lehet is vele műveletet végezni. A példány megsemmisülése automatikusan fog történni. CTeglalap *Tegla = new CTeglalap; Ez a CTeglalap egy példányát(objektumát) dinamikusan hozza létre. Mi saját magunk memóriát foglalunk le az objektum(példány) részére, és már lehet is vele műveletet végezni. A példány megsemmisülése mi általunk vezérelten fog történni,NEM LESZ AUTOMATIKUS a megsemmisülés. Az objektum mindaddig foglalt marad, amíg mi azt explicit módon(kívülről) fel nem szabadítjuk a delete operátor segítségével. delete Tegla; Egy osztályból tetszőleges példányt(ameddig a memória bírja) hozhatunk létre. A tagokat a . vagy -> operátorral érhetjük el a C struktúrák elemeihez hasonlóan. Az osztály tagjainak elérhetőségét a protected, public és private hozzáférés módos1tókkal állíthatjuk be. Alapértelmezésként minden osztályon belüli tag, ha külön nem adjuk meg private-nak minősül. Az egységbezárás elvének megfelelően a hozzáférés-módosítókkal védjük meg az osztály felhasználójától az osztály belső felhasználásra szánt adatait. Írhatunk persze olyan tagfüggvényeket, amelyek a felhasználó által beadott adatok alapján módosíthatják a védett adattagokat. Másik előnye, hogy a belső adatstruktúrák korlátozottsága miatt az osztály szerzője megváltoztathatja az adatszerkezetek felépítését, anélkül, hogy az osztályt használó programrészen változtatni kellene. A lényeg az, hogy a publikus tagfüggvények hívási módja legyen változatlan.
Példa: Jelenlegi definíciójával az osztályunk le fog fordulni szépen, csak éppen a példányával nem fogunk tudni dolgozni, mivel a CTeglalap minden adattagja és tagfüggvénye automatikusan private-ként jött létre. class CTeglalap { public: int Bal; int Felso; int Jobb; int Also; void Rajzol(void) { VonalRajzol(Bal,Felso,Jobb,Felso); VonalRajzol(Jobb,Felso,Jobb,Also); VonalRajzol(Jobb,Also,Bal,Also); VonalRajzol(Bal,Also,Bal,Felso); } };
1
Így már hozzáférhetünk minden adattaghoz és függvényhez: CTeglalap Tegla; Tegla.Bal = 5; Tegla.Felso = 10; Tegla.Jobb = 100; Tegla.Also = 150; Tegla.Draw(); A gond az, hogy az egységbezárás(encapsulation) elve nem érvényesül. Ezen alapelv szerint az osztály belső mechanizmusainál használt adatstruktúrákat az osztály használójának nem kellene látnia, mert neki csak a rajzolás a fontos, hogy mi van mögötte az a felhasználónak nem számít, csak rajzoljon a függvény egy téglalapot és kész. Azonban most mindenhez hozzáfér és ez nem szerencsés. Megoldás: class CTeglalap { private: int Bal; int Felso; int Jobb; int Also; public: void Rajzol(void) { VonalRajzol(Bal,Felso,Jobb,Felso); VonalRajzol(Jobb,Felso,Jobb,Also); VonalRajzol(Jobb,Also,Bal,Also); VonalRajzol(Bal,Also,Bal,Felso); } }; Itt a private kulcsszóra nem is lenne szükségünk, mivel automatikusan private lenne, de jobban olvasható a kód. Itt azonban felmerül egy másik gond, nevezeten, mivel a példányban(objektum) nem tudunk hozzáférni a belső adattagokhoz, valami értelmetlent fogunk rajzolni vagy lehet, hogy a program szabálytalan műveletet fog végrehajtani. A teendő egy publikus függvény megírása, amely beállítja a belső adattagokat a Rajzol függvény használatára. public: void Rajzol(void) { VonalRajzol(Bal,Felso,Jobb,Felso); VonalRajzol(Jobb,Felso,Jobb,Also); VonalRajzol(Jobb,Also,Bal,Also); VonalRajzol(Bal,Also,Bal,Felso); }
void SetKoordinatak(int a, int b, int c, int d) { Bal = a; Felso = b; Jobb = c; Also = d; } Itt a private részben akár meg is változtathatnánk az adattagokat: private: int Bal; int Felso; int Szelesseg; int Magassag; Csak a SetKoordinatak és a Rajzol függvényt kellene átírni egy kicsit, hogy jól számoljanak, de a függvény típus paraméterezése maradna olyan, mint amilyen eddig volt. A konstruktor olyan különleges tagfüggvény, amely a példány létrehozásakor automatikusan meghívódik. Általában az adattagok inicializálására, és az osztály használatára való felkészítéséhez szükséges feladatok elvégzésére használjuk. A konstruktor akárhány paramétert kaphat, az alapértelmezett konstruktornak nincsenek paraméterei. Ha nem készítünk konstruktorokat egy osztályhoz, akkor a fordító generál egyet alapértelmezettként, de ez nem inicializálja az adattagokat. Általában mindig saját konstruktort írunk. A túlterhelt konstruktorok. Használatuk igen gyakori. Használatuk oka pedig az, hogy több módszert nyújtanak az új példány inicializálására. Fontos, hogy mindig legyen egy üres, alapértelmezett konstruktor. A konstruktornak nem lehet visszatérési értéke.
2
Példa: class CTeglalap { private: int Bal; int Felso; int Jobb; int Also; public: //konstruktor és túlterhelés CTeglalap() { } CTeglalap(int a, int b, int c, int d) { SetKoordinatak( a, b, c, d); } //desktruktor ~CTeglalap() { } void Rajzol(void) { VonalRajzol(Bal,Felso,Jobb,Felso); VonalRajzol(Jobb,Felso,Jobb,Also); VonalRajzol(Jobb,Also,Bal,Also); VonalRajzol(Bal,Also,Bal,Felso); } void SetKoordinatak(int a, int b, int c, int d) { Bal = a; Felso = b; Jobb = c; Also = d; } }; Látjuk, hogy a konstruktor public-nak vettük fel, hogy példányosítható legyen. CTeglalap Tegla; Tegla.SetKoordinatak(25,25,100,100); Tegla.Rajzol(); vagy, ha mi allokálunk memóriát CTeglalap *Tegla = new CTeglalap; Tegla->SetKoordinatak(25,25,100,100); Tegla->Rajzol(); Túlterhelt létrehozás. CTeglalap Tegla(25,25,100,100); vagy CTeglalap *Tegla = new Cteglalap(25,25,100,100); Láthattuk, hogy nem a malloc függvényt hívjuk meg C++-ban a példány dinamikus létrehozására, hanem mindig a new operátort, mert az meghívja a létrehozott példány konstruktorát. A destruktor olyan különleges tagfüggvény, amely az osztály példány megsemmisülésekor kerül meghívásra. Felhasználható memória felszabadításra vagy egyéb a megsemmisüléshez szükséges feladatokra is. Destruktort nem lehet túlterhelni. A destruktor neve megegyezik az osztály nevével, de van előtte egy ~ jel. A destruktornak nem lehet visszatérési értéke és nem lehetnek paraméterei sem. A destruktor bármilyen szükséges tevékenységet elvégezhet a memóriaterületek felszabadítására mielőtt az objektum megsemmisül. Tagfüggvényeket létrehozhatunk az osztályon belül vagy osztályon belül csak deklaráljuk és kívül írjuk meg(valami.cpp,valami.h). Az osztályon belül definiált tagfüggvények automatikusan inline tagfüggvényeknek számítanak. A kívül megírt függvények csak akkor lesznek inline függvények, ha azokat az inline kulcsszóval deklaráljuk. Tagfüggvényeken belül a this C++ kulcsszó tartalmazza a függvényhívásnál hivatkozott objektum címét vagyis azt az objektumot, aminek a tagfüggvényét meghívtuk. Ha egy adattagot static kulcsszóval deklarálunk, akkor annak csak egyetlen példánya lesz, az osztályból létrehozott objektumok számától függetlenül A statikus adattagokat az osztály nevével és a hatókör feloldó operátorral érhetjük el, meghatározott példányhivatkozás nélkül. A static kulcsszóval deklarált tagfüggvényeket az osztály nevével és a hatókör feloldó operátorral hívhatjuk, objektumhivatkozás nélkül. Az ilyen függvények közvetlenül csak a saját osztályába tartozó statikus adattagokat és tagfüggvényeket érhetnek el. Általában egy osztály minden példányának megvan a saját másolata az osztályhoz tartozó adattagokból. Amennyiben egy adattagot static kulcsszóval deklarálunk, akkor ebből az adattagból csak egyetlen példány fog létrejönni a memóriában, függetlenül a létrehozott osztály példányainak a számától. Alapszabály és a példában is megfigyelhető, hogy egy osztály statikusnak felvett tagjait az osztályon kívül is deklarálnunk és inicializálnunk kell, ugyanúgy, ahogy azt egy általános globális adattaggal tennénk. Mivel a statikus adattagok definíciója az osztályon kívül kap helyet, egyúttal az osztály nevét is meg kell adni a hatókör-feloldó operátorral(CTest::). Mivel a statikus adattag az osztályon kívül létezik, ezért elérhetjük a példányoktól függetlenül, pusztán az osztály neve és a hatókör-feloldó operátor segítségével. Ahogy a példa is jól mutatja, deklarálhatunk statikus függvényt is. A statikus függvények tulajdonságai a következők: Az osztályon kívüli kód úgy hívhatja meg a függvényt, hogy az osztály nevét használja a hatókör-feloldó operátorral, anélkül, hogy egy példányra hivatkozna. Még nem is kell léteznie a példánynak.
3
A statikus tagfüggvények közvetlenül csak az osztály statikus adattagjaira és statikus függvényeire hivatkozhatnak. Mivel konkrét példányhivatkozás nélkül is hívhatóak, ezért a statikus tagfüggvényeknél nincs this mutató, ami az objektum(példány) címét tartalmazná. A nem statikus adattagok pedig csak a példány létrehozásakor jönnek létre. A statikus adattagokat és tagfüggvényeket az adott osztályra általánosan érvényes adatok kezelésére szoktuk használni. Példa: #include
#include <string> using namespace std; class CTest { private: static int iCount; public: CTest() { ++iCount; } ~CTest() { --iCount; } static int GetCount() { return iCount; } }; int CTest::iCount = 0; void main() { cout << CTest::GetCount() << " objektum letezik\n"; CTest Test1; CTest *pTest2 = new CTest; cout << CTest::GetCount() << " objektum letezik\n"; delete pTest2; cout << CTest::GetCount() << " objektum letezik\n"; getchar(); } A program futásának az eredménye: 0 objektum letezik 2 objektum letezik 1 objektum letezik Az öröklés fogalma:
olyan alapvető programozási technika, amely lehetővé teszi, hogy a már meglévő osztályainkból újakat tudunk származtatni, valamint az egymással kapcsolatban álló osztályokat hierarchiába tudjuk rendezni.
Egy meglévő osztályból újat származtathatunk azáltal, hogy a meglévő osztály nevét feltüntetjük az új osztály deklarációjában. A már meglévő osztályt ősosztálynak, az új osztályt származtatott osztálynak nevezzük. A származtatott osztály örökli az ősosztály minden tagját, a származtatott osztályt új tagokkal láthatjuk el (változók, függvények). A származtatott osztály konstruktora explicit módon inicializálhatja az ősosztályát azáltal, hogy paramétereket ad át az ősosztály konstruktorának. Ha a származtatott osztály konstruktora expliciten nem inicializálja az ősosztály konstruktorát, akkor a fordító automatikusan az ősosztály konstruktorát hívja meg.
4
Példa: #include #include <string> using namespace std; //Õsosztály class CEmberek { private: int iGondolat; public: CEmberek() { } CEmberek(int iparam) { iGondolat = iparam; } ~CEmberek() { } void Gondolkodik() { iGondolat = 1; } }; //Származtatott osztály class CProgramozo : public CEmberek { private: int iSorokSzama; void SorInit(int iSzam) { iSorokSzama = iSzam; } public: CProgramozo() { } CProgramozo(int iparam) : CEmberek(iparam) { SorInit(iparam); } ~CProgramozo() { } void SokSortIszik() { iSorokSzama++; } int GetSorokSzama() { return iSorokSzama; } }; void main() { CProgramozo Geza(25); cout << "Geza " << Geza.GetSorokSzama() << " sort iszik\n"; Geza.SokSortIszik(); cout << "Geza " << Geza.GetSorokSzama() << " sort iszik"; getchar(); } Látható, hogy a két osztály nagyon hasonló, a programozó is az emberek halmazába tartozik. Tehát a programozó rendelkezik az emberekre jellemző tulajdonságokkal, ezen felül iszik sört. A : public CEmberek kifejezés hatására a CProgramozo a CEmberek osztályból származó osztály lesz. Ezért örökli a CEmberek osztály adattagjait és tagfüggvényeit. Ha a CProgramozo osztályt teljesen üresen deklaráltuk volna, akkor is rendelkezne az ősosztálya adattagjaival és metódusaival(tagfüggvényeivel). A „class CProgramozo : public CEmberek” public kulcsszavának hatására az ősosztály minden nyilvános tagja nyilvános marad a származtatott osztályban is. Nagyon ritka az az eset, amikor nem így származtatunk, csak speciális esetekben fordul elő. A CProgramozo osztály void SokSortIszik() taggfüggvénye már a CProgramozo osztály egyedi jellemző függvénye. A konstruktorok és destruktorok meghívásának a sorrendje: Egy származtatott osztály példányosításakor a fordító a következő sorrendben hívja meg a konstruktorokat: 1. Ősosztály konstruktora. 2. Származtatott osztály tagobjektumainak (azon adattagjainak, amelyek objektumok(példányosított osztályok)) konstruktorai. Ezeket a konstruktorokat olyan sorrendben hívja meg, amilyen sorrendben azok az adott osztályban definiálva lettek. 3. Az osztály saját konstruktora. A destruktorok – amennyiben definiálva vannak – pontosan az ellenkező sorrendben futnak le.
5
Így, amikor egy adott konstruktor hajtódik végre, tudni lehet, hogy az ősosztály és a tagobjektumok már inicializálásra kerültek, és így biztonságosan használhatóak. Hasonlóan, amikor egy adott destruktor fut le, tudjuk, hogy sem az ősosztály sem bármelyik tagobjektum nem lett még megsemmisítve, és így azok még mindig használhatóak. Ha az ősosztály egy adattagja védett, azaz deklarációjában szerepel a protected hozzáférés-módosító, akkor a tag elérhető az osztályból származtatott osztályban, de a program más függvényei által nem. Az int iGondolat változót nem tudjuk elérni az származtatott osztályból, vannak esetek, amikor erre szükség van. Ezt a következőképpen tehetjük meg. Példa: class CEmberek { protected: int iGondolat; public: CEmberek() { } CEmberek(int iparam) { iGondolat = iparam; } ~CEmberek() { } void Gondolkodik() { iGondolat = 1; } }; Mivel az iGondolat változó protected lett, így már a CProgramozo származtatott osztályból is elérhető lesz. Egy származtatott osztály szolgálhat egy másik osztály őseként. Ilyen módon az egymással kapcsolatban álló osztályok többszintű hierarchiája valósítható meg. Az öröklődés segítségével lehetőség nyílik egy osztály korábban megírt adatstruktúráinak és kódjának újrafelhasználására. Ezáltal könnyebben kezelhető a program és lehetőség van a program által kezelt objektumok kapcsolatrendszerének modellezésére. #include #include <string> using namespace std; //Õsosztály, a CApolo közvetett(indirekt) őse class CEmberek { protected: int iGondolat; public: CEmberek() { } CEmberek(int iparam) { iGondolat = iparam; } ~CEmberek() { } void Gondolkodik() { iGondolat = 1; } }; //Származtatott osztály, CApolo közvetlen(direkt) őse class CProgramozo : public CEmberek { private: int iSorokSzama; void SorInit(int iSzam) { iSorokSzama = iSzam; } public: CProgramozo() { } CProgramozo(int iparam) : CEmberek(iparam) { SorInit(iparam); }
6
~CProgramozo() { } void SokSortIszik() { iSorokSzama++; } int GetSorokSzama() { return iSorokSzama; } }; //Származtatott osztály class CApolo : public CProgramozo { private: int iSorokSzama; void SorInit(int iSzam) { iSorokSzama = iSzam; } public: CApolo() { } CApolo(int iparam) : CProgramozo(iparam) { SorInit(iparam); } ~CApolo() { } void SokSortIszik() { iSorokSzama++; } int GetSorokSzama() { return iSorokSzama; } }; void main() { CProgramozo Geza(25); cout << "Geza " << Geza.GetSorokSzama() << " sort iszik\n"; Geza.SokSortIszik(); cout << "Geza " << Geza.GetSorokSzama() << " sort iszik"; getchar(); } Az int iGondolat bármelyik CEmberek-ből származtatott osztályból elérhető, de nem érhető el közvetlenül az osztályhierarchián kívüli kódrészben. A származtatott osztályok hierarchiájában minden osztály adott tagfüggvényének saját változatával rendelkezhet. Ha ez a függvény virtuális is egyben, akkor ennek meghívásakor mindig az aktuális objektum típusának megfelelő változat fog lefutni, még akkor, ha a hívás egy ősosztályra mutató mutatón keresztül történik. A virtuális függvények a polimorfizmus is támogatják, azaz egyetlen utasítás segítségével több, művelet futhat le, és hogy éppen melyik, az a szóban forgó objektum típusától függ.
különféle
A virtuális függvények segítségével egyszerű, általános célú rutinok írhatóak, amelyek különböző, de egymással kapcsolatban álló objektumok széles választékát tudják kezelni. A virtuális függvények segítségével továbbá lehetőség van az ősosztály viselkedésének módosítására anélkül, hogy a forráskódon módosítanánk. Amikor a származtatunk, akkor a kód megkettőzése és a lehetséges egyéb redundanciák elkerülése cél. Példa: #include #include <string> using namespace std; //Õsosztály class CEmberek { protected: int iGondolat; public: CEmberek() { }
7
CEmberek(int iparam) { iGondolat = iparam; } ~CEmberek() { } virtual void Gondolkodik() { iGondolat = 1; } }; //Származtatott osztály class CProgramozo : public CEmberek { private: int iSorokSzama; void SorInit(int iSzam) { iSorokSzama = iSzam; } public: CProgramozo() { } CProgramozo(int iparam) : CEmberek(iparam) { SorInit(iparam); } ~CProgramozo() { } void SokSortIszik() { iSorokSzama++; } int GetSorokSzama() { return iSorokSzama; } virtual void Gondolkodik() { iGondolat = 10; iSorokSzama += iGondolat; } }; //Származtatott osztály class CApolo : public CProgramozo { private: int iSorokSzama; void SorInit(int iSzam) { iSorokSzama = iSzam; } public: CApolo() { } CApolo(int iparam) : CProgramozo(iparam) { SorInit(iparam); } ~CApolo() { } void SokSortIszik() { iSorokSzama++; } int GetSorokSzama() { return iSorokSzama; } virtual void Gondolkodik() { iGondolat = 100; iSorokSzama += iGondolat; } };
8
void main() { CProgramozo Geza(25); CApolo Anett(25); cout << "Geza " << Geza.GetSorokSzama() << " sort iszik\n"; Geza.SokSortIszik(); cout << "Geza " << Geza.GetSorokSzama() << " sort iszik\n"; Geza.Gondolkodik(); cout << "Geza " << Geza.GetSorokSzama() << " -ra gondolt.\n"; Anett.Gondolkodik(); cout << "Geza " << Anett.GetSorokSzama() << " -re gondolt.\n"; getchar(); } A program futásának az eredménye: Geza 25 sort iszik. Geza 26 sort iszik. Geza 36 -ra gondolt. Anett 125 -re gondolt. A virtuális függvények az objektum orientált programozás egy fontos vonását támogatják, a polimorfizmust(többalakúság). Akkor beszélünk többalakúságról, ha egyetlen utasítással többfajta különböző művelet hajtható végre, és az, hogy éppen melyik, attól függ, hogy éppen melyik objektumról van szó. Fenti példa jól mutatja ki hogyan gondolkodik és a gondolkodás milyen hatással van a megivott sörök számára. Az öröklés előnyei: Az öröklés segítségével újra felhasználhatóvá válnak a már megírt kódok és a már megtervezett adatstruktúrák. Ezáltal elkerülhető az adat és kód megkettőződése. A programokat könnyebben lehet karbantartani, mivel egy adott feladatot megvalósító kód és adatok általában a programban egyetlen, könnyen elérhető osztály definíciójában szerepelnek ahelyett, hogy szétszórva helyezkednének el a forráskódban. Számos, valós világból származó kapcsolatot lehet hatékonyan modellezni a C++ - ban definiált osztályhierarchiával. Például az emberek osztályából származtatott programozónak is szüksége lehet ápolóra, aki szintén programozó, hogy hatékony és gyors segítséget tudjon nyújtani a programozónak.
9