Objektum-orientált programozás A programozás történetének során folyamatosan alakultak ki szabályok, amelyek célja a programkód átláthatósága, hibalehetőségek kizárása, alkalmazások közös fejlesztésének megkönnyítése és a már megírt kódrészletek újra felhasználása. Ilyen meggondolásból először strukturálttá tették a programkódot: az alaputasításokat kizárólag szekvenciálisan sorolhatjuk (lineárisan egymásután), elágazásokba (if, switch) és ismétlésekbe foglalhatjuk. Azelőtt lehetséges volt ugrálni a programsorok között. A fejlődés másik lépése az alfeladatokra való lebontás, alprogramok, modulok használata. Az alprogramokra bontott programot, már többen tudják fejleszteni és könnyebben javítható, hiszen a hiba egyszerűbben lokalizálható. irányzat. Az objektum-orientáltság egy újabb szint ebben a fejlődésben, ez az uralkodó programozási Az objektum-orientált programozás lehetővé teszi az adatok és a hozzá tartozó műveletek összezárását. Az objektum tervezője és fejlesztője kontrollálhatja az adatokhoz való hozzáférést. Az objektumok megértését elősegítheti a már használt C++ objektumok működésének elemzése. Ilyen objektumok (osztályok) a string, a queue és a stack. Az objektum-orientáltság string s; //a string az osztály, az s az objektum osztályok (class) írását jelenti. s="almafa"; //a string osztályban fölül van írva (értelmezve van) az Objektumnak az osztály egy példányát nevezzük. Pl: a string osztályt használhatjuk, ha karakterláncot szeretnénk tárolni. értékadás operátor, ezért használhatjuk az s esetén cout<<s; //a string osztályban fölül van írva a << operátor is for (int i=0;i<s.length();i++) s[i]=s[i]+1; //a string osztályban fölül van írva a [] operátor is, ezért indexelhetjük cout<<s; http://www.cplusplus.com/reference/string/string/ Az objektumokkal kapcsolatos fogalmakat a tört adatszerkezet (osztály) fokozatos felépítésével vegyük sorra. A megírt tört osztályt a stringhez hasonlóan, bármilyen törttel kapcsolatos feladat megoldása során használhatjuk. 1
A tört osztály 1. Adattagok - az osztályoknak (hasonlóan a struktúrához) adattagjaik vannak, amelyek lehetnek kifele látható (módosítható, lekérdezhető) vagy éppen láthatatlanok. Azért, hogy megvalósuljon az adatok védettsége nem tanácsos láthatóvá tenni őket. A tagokhoz csatolhatjuk a következő kulcsszavakat: a private (rejtett) ez az alapértelmezett érték. Az adattagot csak a metódusok és a friend alprogramok módosíthatják. a public (nyilvános) ez nem javasolt, mert bárki módosíthatja az értéküket. a protected (védett) hasonló a private-hoz, de az öröklődés során előállított leszármazott osztályokban is módosíthatóak. //ugyanúgy lehetne használni, mint a struct mezőit, ha nem lenne private, így nem matathatunk hozzájuk //előjel nélküli egész szám elég a számlálónak az előjel ; 2. Metódusok (setter, getter) alprogramok, amelyek az adattagokra vonatkoznak műveletek, amelyeket velük elvégezhetünk. Gyakori metódus típusok az adattagok értékeit lekérő és módosító (setter, getter) metódusok. Lehetővé teszik az ellenőrzött hozzáférést az adattagokhoz. //ezek nyilvános metódusok //a Get-esek visszatérítik egy-egy mező értékét int GetSzamlalo() { return szamlalo; unsigned int GetNevezo() { return nevezo; //a Set-eseknél ellenőrizhetjük, hogy a nevező ne legyen 0 void SetSzamlalo(int sz) { szamlalo=sz; void SetNevezo(unsigned int n) { if (n!=0) nevezo=n; ; 2
tort t; //a t egy tort objektum t.setnevezo(5); //a nevező és a számláló kissé bonyolult beállítása t.setszamlalo(4); cout<<t.getszamlalo()<<"/"<<t.getnevezo(); //a kiírás 3. Metódusok (konstruktorok) speciális metódus(ok), amelyek automatikusan meghívódnak az objektumok létrehozásakor: nevük az osztály nevével kell azonos legyen több is lehet, amennyiben más-más paraméterezéssel rendelkeznek visszatérített értéket nem kell megadni void-ot sem Ha a tort-höz adunk konstruktort, a Set-es alprogramokat esetleg módosításra maradhatna //konstruktor mivel a paramétereknek kezdőértéket adtam meghívható több módon is ha nincs változó a paraméter helyén, az alapértelmezett értéket adja át tort(int sz=0, unsigned int n=1) { szamlalo=sz; if (n!=0) nevezo=n; else nevezo=1; int GetSzamlalo() {return szamlalo; unsigned int GetNevezo(){return nevezo; ; int main(){ tort t, t1(5), t2(5, 4); //a t esetén nincs paraméter, ezért 0/1 lesz cout<<t.getszamlalo()<<"/"<<t.getnevezo()<<endl; //a t1 esetén csak az első paraméter kap más értéket, a második marad 1: 5/1 cout<<t1.getszamlalo()<<"/"<<t1.getnevezo()<<endl; //a t2-re kiírt érték: 5/4 cout<<t2.getszamlalo()<<"/"<<t2.getnevezo(); 3
4. Metódusok (az osztályon kívül) a gyakorlatban nem az osztályon belül szoktuk kifejteni a metódusok kódját. Az osztályon kívüli alprogramokat normális alprogramként kezeli a környezet, az inline függvényeket behelyettesíti a kódba, nem javasolt túl sok kódot így hagyni. Az alprogramok használatának épp az lenne az egyik lényege, hogy ne ismétlődjenek a kódok. tort(int sz=0, unsigned int n=1); //ha csak a kiírásra használnánk a get-es metódusokat, akár egy kiír-t is írhatunk. void Kiir(); ; //metódusok kifejtése az osztályon kívül tort::tort(int sz, unsigned int n) { szamlalo=sz; if (n!=0) nevezo=n; else nevezo=1; void tort::kiir(){ cout<<szamlalo<<" / "<<nevezo; ; tort t, t1(5), t2(5, 4); //az előző kiírást helyettesíthetjük a kiír metódussal t.kiir(); cout<<endl; t1.kiir(); cout<<endl; t2.kiir(); cout<<endl; 5. Metódusok (private) nem minden metódus nyilvános. Például, ha egyszerűsíteni szeretném időnként a törtet, ezt nem szükséges az osztályom felhasználójának is megengedjem. Egyszerűsíthetek a tört létrehozásakor, illetve ha valamilyen műveletet végzek vele. 4
tort(int sz=0, unsigned int n=1); void Kiir(); //csak az osztályon belül használható metódus void Egyszerusit(); ; tort::tort(int sz, unsigned int n) { szamlalo=sz; if (n!=0) nevezo=n; else nevezo=1; Egyszerusit(); //egyszerűsíthetem létrehozáskor void tort::kiir(){ cout<<szamlalo<<" / "<<nevezo; ; void tort::egyszerusit() { int a, b, r; a=szamlalo; b=nevezo; while (b){ r=a%b; a=b; b=r; szamlalo/=a; nevezo/=a; tort t2(5, 40); t2.kiir();cout<<endl; 6. Metódusok (túlterhelés) - ugyanolyan névvel több metódust is írhatunk. A feladatuk azonos, csak más kell legyen a paraméterezés. tort(int sz=0, unsigned int n=1); //a Kiir alprogram két változata következik, az újabb egy karakterláncot is kap, amit szintén kiír. void Kiir(); void Kiir(string sz); ; 5
tort::tort(int sz, unsigned int n) { szamlalo=sz; if (n!=0) nevezo=n; else nevezo=1; void tort::kiir(){ cout<<szamlalo<<" / "<<nevezo; ; void tort::kiir(string sz){ cout<<sz<<" "<<szamlalo<<" / "<<nevezo; ; tort t, t1(5, 40); t.kiir("elso tort");cout<<endl; t1.kiir();cout<<endl; 7. Metódusok ( kettős operátor túlterhelés) - a törtekhez értelmezhetjük a klasszikus C++ operátorok bármelyikét, azaz megadhatjuk, hogy mit értünk az alatt, hogy tört + tört vagy éppen tört * tört. Az értékadást az ilyen típusú objektumokra megy automatikusan a fenti alprogramok esetén ki lehet próbálni. Dinamikusan tárolt elemek esetén lehet gond és szükséges az értékadás felülírása. int main(){ tort t, t1(5, 40); t=t1; //a t tort 0/1, a t1 5/40 az értékadás következtében a t is 5/40 kell legyen t1.setszamlalo(11); //hogy leteszteljük, hogy a címét erőltette a t1 a t-re, vagy a mezők értékei lettek átmásolva lecseréltem a t1 számlálóját. Külön változnak, tehát nem közös a címük, a művelet sikerült. t.kiir("elso tort");cout<<endl; t1.kiir();cout<<endl; Az összeadás viszont már nem működik, tehát ha használni akarjuk, meg kell írni. Többféleképpen megoldható ez is. Kiegészíthető egyszerűsítéssel, a rend kedvéért és természetesen egyéb műveleteket is érdemes megírni. A példák tehát: 6
tort(int sz=0, unsigned int n=1); //ha egy tört után teszünk + jelet ez a metódus hívódik meg paraméter lesz, amit hozzá akarunk adni ez a t. Eredményként ennek a két törtnek az összege kerül visszatérítésre. tort operator+(tort& t); void Kiir(); ; tort tort::operator+(tort& t){ //a paraméterre hivatkozást adunk át. Így az eredeti objektummal dolgozunk, nem egy másolatával. Működik referencia nélkül is. tort eredm; //az eredm egy ideiglenes objektum, amely mezőinek értéke az értékadás révén az eredmény törtbe másolódnak. eredm.szamlalo=t.nevezo*szamlalo+t.szamlalo*nevezo; eredm.nevezo=nevezo*t.nevezo; return eredm; tort::tort(int sz, unsigned int n) { szamlalo=sz; if (n!=0) nevezo=n; else nevezo=1; void tort::kiir(){ cout<<szamlalo<<"/"<<nevezo; ; tort t, t2(1,2), t1(3, 2); t=t1+t2; t.kiir();cout<<" = ";t1.kiir();cout<<" + ";t2.kiir(); Igazán szép az lenne, ha a fejléc így nézne ki: const tort operator+(const tort& t) const 7
A const a konstansra értékét nem változtató adatra utal. Az eredmény const-ja miatt a t1+t2=t formájú utasításra hibát ad a környezet, mert az eredményként létrehozott ideiglenes törtet nem engedi változtatni, ezért nem is szerepelhet értékadás bal oldalán. A paraméter const-ja biztosítja, hogy bár a törtet, amit hozzáadunk az aktuális objektumhoz hivatkozással adtuk át, nem fogjuk az alprogramban véletlenül sem megváltoztatni. A harmadik const az aktuális objektumra vonatkozik, amelyet szintén nem indokolt változtatni. 8. Metódusok (friend függvény) - a kiírás alprogram helyettesíthető a << operátorral, nem is beszélve, hogy elegánsabb is. A << operátor végül nem a törtre, hanem valamilyen stream objektumra, ezért hiába akarjuk törtre fölülírni. Viszont használhatunk friend függvényt. Ezeket az alprogramokat az objektumon belül értelmezzük, de nem alkalmazhatjuk rájuk a private, protected kulcsszavakat. Az osztály barát alprogramja hozzáférhet az adott osztály egyébként rejtett mezőihez. tort(int sz=0, unsigned int n=1); //a friend kulcsszó jelzi a barátalprogramot //a törtet amit a << jel után teszünk paraméter lesz //azért kell visszatérített értéket adni, hogy láncolni lehessen a kiírást friend ostream & operator<<(ostream & ki, tort & t); ; //ide már nem kell friend ostream& operator<<(ostream &ki, tort& t){ //ki az ostreamre a hivatkozás képernyőre és állományra is mehet. ki<<t.szamlalo<<"/"<<t.nevezo; return ki; tort::tort(int sz, unsigned int n) { szamlalo=sz; if (n!=0) nevezo=n; else nevezo=1; 8
tort t(1,2); //mostmár nem kell a kiir cout<<t; 9. Metódusok (destruktor) Destruktor a konstruktor párja, akkor hívódik meg, amikor az objektum felszámolódik. A törthöz erre nincs szükség, a dinamikusan lefoglalt helyeket kell az objektum felszámolása esetén szabályosan felszabadítani. Ha megírnánk valahogy ilyen formája kellene legyen: ~tort(){cout<<"felszamolva!!!!"; 10. Hibakezelés (Hibák dobálása ) Programjaink futtatása közben felmerülhetnek olyan problémák, amelyek meghiúsítják az alprogram/program helyes végrehajtását, de amelyekre valamilyen formában reagálni kell. Az alprogram return-nal vagy paraméterként visszatéríthet számértékeket, amelyek alapján az alprogramot meghívó programozó eldöntheti mit kezd a hibával. Pl: a string osztálynak van egy find nevű metódusa, amely visszatéríti, hogy egy másik string hol van az alapstringen belül. Persze előfordulhat, hogy nincs meg benne. Ebben az esetben a visszatérített érték a string hosszának a lehető legnagyobb értéke. Ebből lehet kisütni, hogy nincs eredmény. string s="almafa"; if (s.find("bu")>s.length()) cout<<"nincs benne."; else cout<<"benne van."; Egy másik lehetőség a hiba feldobása, amelyet gyakran használnak objektum-orientált környezetben. Például a törtek esetén, ha 0 nevezőt adnak meg, lehet önkényesen helyettesíteni más értékkel, mint ahogy a fenti példákban vagy pedig jelezhetünk hibát. Ilyenkor az osztály leírásában nyilván meg kell adni, hogy miként reagál a konstruktor erre az esetre. Baj lehet akkor is, ha állományból olvasunk és nem létezik stb. Ez esetben nem igazán lehet a programot folytatni. tort(int sz=0, int n=1); friend ostream & operator<<(ostream & ki, tort & t); ; 9
ostream& operator<<(ostream &ki, tort& t){ ki<<t.szamlalo<<"/"<<t.nevezo; return ki; tort::tort(int sz, int n) { szamlalo=sz; if (n>0) nevezo=n; else if (n==0) throw(1); else throw(2); //átírtam a konstruktort, hogy ha 0 a nevező dobjon egy egyest, ha negatív (átírtam az n-et int-re) dob egy kettest. Lehet hibaüzenetet is. try{ tort t(1,-1); //ezek után, ha értelmezni akarok rossz törtet leáll a program, ennek elkerülésére jön a try catch... cout<<t; //a try-ba írom a problémás utasítást catch (const int val){ //a catch-be azt, hogy mit kezdjen, ha hiba lép fel if (val==1) cout<<"nincs tort! 0 a nevezo!"; if (val==2) cout<<"nem hozhattam letre a tortet! negativ nevezo"; vagy másképp a főprogram: int x, y; tort t; cin>>x>>y; try{ tort t1(x,y); t=t1; catch (const int val){ if (val==1) {tort t1(x,1); t=t1; if (val==2) {tort t1(x,-y); t=t1; 10
cout<<t; Gyakorlatok: 1. Írd meg kiegészítve a tort osztályt (alapműveletek, egyszerűsítés), majd írj egy programot, amely felhasználva az osztályt a következő opciókat kínál fel: a) művelet két törttel: összeadás kivonás szorzás osztás b) művelet egy törteket tartalmazó állományra: összeadás kiírás képernyőre 2. Írj egy nagyszámokat leíró osztályt. Nagyszám, ami már nem tárolható egy számként, hanem szükséges számjegyenként tárolni (Pl: tömbben). Írj programot, amely az osztályt felhasználva megvalósít egy nagyszámokkal foglalkozó számológépet. Bibliográfia: Emanuela Cerchez, Marinel Serban: Programarea în limbajul C/C++, vol. 4 11