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ó. Az objektum-orientáltság egy újabb „szint” ebben a fejlődésben, ez az uralkodó programozási irányzat. 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 osztályok
objektum-orientáltság (class)
írását
jelenti.
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.
string s; //a string az osztály, az s az objektum s="Almafa"; //a string osztályban fölül van írva (értelmezve van) az é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. class tort { private: //ugyanúgy lehetne használni, mint a struct mezőit, ha nem lenne private, így nem matathatunk hozzájuk int szamlalo; unsigned int nevezo; //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. class tort { private: int szamlalo; unsigned int nevezo; public: //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
int main() { 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<
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 class tort { private: int szamlalo; unsigned int nevezo; public: //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<
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. class tort { private: int szamlalo; unsigned int nevezo; public: 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<<" / "<
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. class tort { private: int szamlalo; unsigned int nevezo; public:
4
tort(int sz=0, unsigned int n=1); void Kiir(); private: //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<<" / "<
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. class tort { private: int szamlalo; unsigned int nevezo; public: 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<<" / "<
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
class tort { private: int szamlalo; unsigned int nevezo; public: 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
objektummal
hivatkozást
dolgozunk,
nem
adunk egy
át.
Így
az
másolatával.
eredeti 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<<"/"<
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. class tort { private: int szamlalo; unsigned int nevezo; public: 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<
8
tort t(1,2); //mostmár nem kell a kiir cout<
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. class tort { private: int szamlalo; unsigned int nevezo; public: tort(int sz=0, int n=1); friend ostream & operator<<(ostream & ki, tort & t); };
9
ostream& operator<<(ostream &ki, tort& t){ ki<
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. } int main() { 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<
(val==2)
cout<<"Nem
hozhattam
letre
negativ nevezo"; } }
vagy másképp a főprogram: int main() { 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
a
tortet!
} cout<
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