10. fejezet - Osztályok


A programozási módszertan fejlődésével a procedurális szemléletet elkezdte leváltani az objektum orientált paradigma. Ennek előnye, hogy közelebb áll az emberi gondolkozáshoz: az entitásokat hierarchiákba lehet szervezni, és az enkapszuláció révén egy entitás minden tulajdonsága (adatok és műveletek) logikailag egy helyen van. Ezen kívül az adatrejtés elvével bizonyos tulajdonságokat elrejthetünk a külvilág elől.



Egy osztály egy minta valamilyen objektum konstrukciójához. Definiálja az objektum tulajdonságait (tagváltozók) és amit csinálni tud (metódusok). Konvencionálisan az osztályokat egy header fájlban definiáljuk, a metódusok implementációit pedig egy hasonló nevű .cpp fájlban valósítjuk meg. Nézzük például hogyan nézne ki egy alma:

CODE
// alma.h #ifndef ALMA_H #define ALMA_H class Alma { public: int szin; int meret; }; #endif
CODE
// alma.cpp #include "alma.h" // egyelőre semmi

Hasonlóan a függvényes fejezetben látottakhoz, osztályok esetében is a header fájl a konvencionális ellenőrzéssel kezdődik.
Az almának van színe és mérete; a public kulcsszó azt jelenti, hogy ez kívülről látható (és publikus változó esetében változtatható) tulajdonság. Az osztályt a következőképpen példányosítjuk:

CODE
Alma a1; Alma a2; a1.szin = 1; // 1 <=> piros (érett) a1.meret = 10; a2.szin = 0; // 0 <=> zöld (éretlen) a2.meret = 7; // és az éretlen alma kisebb is

Ebben a kódban két Alma típusú objektumot hoztam létre; a fordító az osztálydefinícióból tudja, hogy mennyi memóriát kell foglalni. A kívülről látható tulajdonságokra és metódusokra a . (pont) operátorral lehet hivatkozni.


Elég unalmas dolog minden tagváltozót egyesével inicializálni, sőt ha esetleg elfelejtenéd akkor kapsz egy elfajult almát. Ezenkívül az almák nagyjából ugyanakkorák, tehát elég lenne csak azt megadni, hogy érett-e vagy nem. Minden osztálynak van két speciális metódusa, a konstruktor és a destruktor. Mindkettő neve megegyezik az osztályéval, de a destruktor előtt van egy ~ jel. Ha nem adtuk meg, akkor a fordító generál egy default konstruktort és destruktort (például a fenti kódban). Visszatérő értékük nem lehet.

A konstruktor mindig akkor fut le, amikor az objektum létrejön. Például arra lehet használni, hogy kezdőértéket adj a tagváltozóknak. Lehetnek paraméterei is. Fontos: ha deklaráltál az osztálynak konstruktort, akkor a fordító nem fogja legenerálni a default konstruktort, tehát ilyen esetben túl kell terhelni.

A destruktor az objektum megsemmisülése előtt fut le, itt lehet felszabadítani az objektum által használt dinamikus memóriaterületeket. Az automatikus tagváltozók maguktól szabadulnak fel, sőt ha ez egy objektum, akkor annak a destruktora is automatikusan meghívódik. Ha az osztálynak van dinamikusan létrehozott tagváltozója, akkor azt a programozónak kell felszabadítani a destruktorban, a delete operátorral.

Írjuk át az alma osztályt úgy, hogy csak a színt kelljen megadni.

CODE
// alma.h #ifndef ALMA_H #define ALMA_H class Alma { public: int szin; int meret; Alma(int sz); // konstruktor ~Alma(); // destruktor }; #endif
CODE
// alma.cpp #include "alma.h" Alma::Alma(int sz) { szin = sz; // érett v éretlen meret = (sz == 1 ? 10 : 7); } Alma::~Alma() { // nincs mit felszabadítani }

Ha az osztályon kívül implementálsz egy metódust, akkor ki kell tenni az osztály nevét és a scope operátort. Fontos: header fájlban ne implementáld a metódusokat, hacsaknem az osztálydeklarációban, mert linkelési hibát kapsz (ugyanazért, mint a függvényes fejezetben).

Mivel megadtam konstruktort (és van paramétere) egy almát úgy deklarálhatok csak, hogy megadom a színét:

CODE
//Alma a0; // fordítási hiba Alma a1(1); Alma a2(0); std::cout << "Az elso alma merete: " << a1.meret << "\n"; std::cout << "A masodik alma merete: " << a2.meret << "\n"; // output: 10 7

Nagyon fontos, hogy a destruktort soha ne hívd meg közvetlenül, mert nem tudod hogyan van implementálva! A konstruktor és a destruktor csak két kitüntetett metódus, nem ezek foglalják és szabadítják fel a memóriát! Még fontosabb, hogy konstruktorok nem hívhatják egymást!


Van egy kis baj az almával: a színét és a méretét bárki meg tudja változtatni valami érvénytelen értékre (például kék), és különben is ezek az alma privát tulajdonságai amit látunk ugyan, de nem kéne tudnunk beleszólni. A megoldás az, hogy a színt és méretet private -ként deklaráljuk, de az olvasáshoz biztosítunk metódusokat (ezeket más nyelvekben property-nek nevezik).

CODE
// alma.h #ifndef ALMA_H #define ALMA_H class Alma { private: int szin; int meret; public: // inline: a forditó a hívás helyén kifejtheti inline int GetMeret() const { return meret; } // const: nem változtatja meg az objektumot inline int GetSzin() const { return szin; } Alma(int sz); // konstruktor ~Alma(); // destruktor }; #endif
CODE
// alma.cpp #include "alma.h" Alma::Alma(int sz) { szin = sz % 2; // piros v zöld meret = (szin == 1 ? 10 : 7); } Alma::~Alma() { // nincs mit felszabadítani }

Privátnak deklaráltam a változókat, így nem lehet hivatkozni rájuk kívülről. Az értéküket viszont el lehet kérni a publikus metódusok segítségével. Ezenkívül lekezeltem azt az esetet is, amikor valaki a konstruktorral akarna kék almát csinálni.

CODE
Alma a1(1); Alma a2(4); // kék alma (itt most zöld lesz) //a1.meret = 20; // fordítási hiba: 'meret' privát std::cout << "Az elso alma merete: " << a1.GetMeret() << "\n"; std::cout << "A masodik alma merete: " << a2.GetMeret() << "\n"; // output: 10 7

Természetesen metódusokat is lehet priváttá tenni (akár a konstruktort is, majd látni fogjuk, hogy mikor jó az). Létezik egy harmadik láthatósági szint is, a protected, ez majd öröklődésnél lesz fontos, de most ugyanúgy viselkedne mint a private.


A módszer teljesen hasonló mint az eddigi dinamikus memóriakezeléses példák. Egy almát a következőképpen hozunk létre dinamikusan:

CODE
Alma* a = new Alma(1); std::cout << "Az alma merete: " << (*a1).GetMeret() << "\n"; std::cout << "Az alma merete: " << a1->GetMeret() << "\n"; delete a; // output: 10 10

Pointerek esetében a tagokra hivatkozhatsz a -> (nyíl) operátorral is, ez kellemesebb mint a (*). A létrehozás és megsemmisítés itt is a new és delete operátorokkal történik.


C++ -ban a legtöbb operátort (de az aritmetikai operátorokat mindenképpen) felül lehet definiálni. Például írjuk meg a komplex számok osztályát az összeadás és szorzás műveletével:

CODE
// komplex.h #ifndef KOMPLEX_H #define KOMPLEX_H class Komplex { public: float _re; float _im; Komplex(); Komplex(float re, float im); Komplex operator +(const Komplex& other); Komplex operator *(const Komplex& other); }; #endif
CODE
// komplex.cpp #include "komplex.h" Komplex::Komplex() : _re(0), _im(0) // inicializáló lista { } Komplex::Komplex(float re, float im) : _re(re), _im(im) // inicializáló lista { } Komplex Komplex::operator +(const Komplex& other) { return Komplex(_re + other._re, _im + other._im); } Komplex Komplex::operator *(const Komplex& other) { return Komplex( _re * other._re - _im * other._im, _im * other._re + _re * other._im); }

Megjelent egy új dolog: az incializáló lista, itt a tagváltozók konstruktorait lehet meghívni. A sorrend nem mindegy, érdekes hibákat lehet kapni ha összevissza hivogatod a konstruktorokat; érdemes az ilyen változókat az osztály elejére tenni egy kupacba.

Az operátorok teljesen szokványos metódusok, kivéve hogy a nevük az operator kulcsszóból és egy jelből áll. Csak létező operátort lehet felüldefiniálni, tehát például × operátort nem lehet csinálni. Előnyük, hogy meghívhatóak infix módon is, azaz két változó közé írva az operátor jelét:

CODE
Komplex a(2, 3); Komplex b(-5, 8); Komplex c(a + b), d; std::cout << "a + b == (" << c._re << ", " << c._im << ")\n"; d = a * b; // ugyanaz mint a.operator *(b) std::cout << "a * b == (" << d._re << ", " << d._im << ")\n";

Felül lehet defininálni az operator new és operator delete operátorokat is, sőt konverziós operátorokat is, például (típus)változó. Ezekkel most nem foglalkozok. Ami érdekesebb, hogy az operátort nem feltétlenül kell az osztályon belül deklarálni, lehet úgy is mint egy sima függvényt. Például írjuk meg a kivonás műveletét:

CODE
Komplex operator -(const Komplex& c1, const Komplex& c2) { return Komplex(c1._re - c2._re, c1._im - c2._im); } // main()-be: d = a - b; // ugyanaz mint ::operator -(a, b) std::cout << "a - b == (" << d._re << ", " << d._im << ")\n";

Ha pointeresen hozod létre az objektumot, akkor ki kell írni a teljes metódusnevet (a->operator +(b)) vagy a (*a) + b módszert használni.


Egy változót vagy metódust osztályszintűvé lehet tenni a static kulcsszóval. Ilyenkor is az osztályban kell deklarálni, de ekkor a definíció nem itt fog történni, hanem a .cpp fájlban.

CODE
// komplex.h #ifndef KOMPLEX_H #define KOMPLEX_H class Komplex { public: static Komplex i; // ... }; #endif
CODE
// komplex.cpp #include "komplex.h" Komplex Komplex::i(0, 1);

A metódusok esete teljesen hasonló, de a .cpp fájlban már nem kell kiírni a static kulcsszót.
Használatkor az osztály nevével kell minősiteni a statikus változót.

CODE
d = a + Komplex::i;

Statikus osztálynak hívják az olyan osztályt, aminek csak statikus változói és metódusai vannak. C++ -ban nyelvi szinten nincs ilyen, de az osztály konstruktorát priváttá téve lehet szimulálni. Innentől tehát ha statikus osztályról beszélek, akkor a konstruktorát privátnak tekintem. Az ilyen osztályt nem lehet példányosítani és a tagváltozói a program végéig léteznek (mint a rendes statikus változók esetében).

Fontos: statikus metódusban nem statikus tagváltozóhoz és metódushoz nem lehet hozzáférni!


Mért nem lehet statikus metódusból elérni nem statikus tagokat? Hát például mert ebben az esetben nincs is objektum. Egy kicsit mélyedjünk bele a metódusok lelki világába. Amikor leírom ezt:

CODE
class Valami { public: int n; void foo(int i); }; void Valami::foo(int i) { n = i; } // main()-be: Valami v; v.foo(5);

akkor a fordító valami ilyesmit generál belőle:

CODE
void Valami_foo_int(Valami* this, int i) { this->n = i; } // main()-ben: Valami v; Valami_foo_int(&v, 5);

Ez a this pointer láthatatlanul mindig átadódik a metódusnak, így amikor egy tagváltozóra hivatkozok, valójában a this->tagváltozó -ra történik hivatkozás; kivéve a statikus metódusokat, ott ugyanis nincs this pointer (hiszen nincs objektum). Magát a this pointert lehet is használni, például névelfedéskor (egy tagváltozót elfed a metódus lokális változója).


C-ben a strukturák a rekord adatszerkezetet valósítják meg, tehát csak tagváltozóik lehetnek, metódusaik nem (láthatóság meg abszolút nem, minden publikus). C++ -ban a struct-ot lehet ugyanúgy használni mint a class-t, de van néhány különbség. Osztályoknál ha nem írod ki láthatóságot, akkor az alapértelmezett a private. Strukturáknál a public. Hasonlóan osztályoknál a default öröklődés szintén private, strukturáknál public (következő fejezet).

Előfordulhat olyan eset, amikor két osztály kereszthivatkozik egymásra. Ez olyan probléma, mint hogy a tojás volt-e előbb vagy a tyúk, tehát megoldhatatlannak tűnik. Szerencsére osztályokat lehet előre deklarálni, ezzel jelezve a fordítónak, hogy az az azonosító egy osztályt jelöl, de csak később lesz definiálva. Megjegyzendő, hogy ez csak akkor működik, ha az említett hivatkozás referencia vagy pointer (hiszen annak ismert a mérete, 4 bájt).

CODE
class A; class B { //A a; // így nem jó A* a; // így jó }; class A { B b; };

Hasonlóan lehet használni a forward deklarációt az #include hivatkozások csökkentésére. A C++ standard library néhány osztályának forward deklarációja megtalálható az <iosfwd> headerben.


Néhány alapvető dolgot mondtam el osztályokkal kapcsolatban. A következő fejezetben egy nagyon fontos dologról lesz szó, az öröklődésröl és a hozzá kapcsolódó dinamikus kötésről.


Höfö:

  • Írj egy 2D-s vektor osztályt! Írd meg az összeadás (+), kivonás (-), skalárral való szorzás (*) és a skaláris szorzás (dot) műveleteket!

Irodalomjegyzék

http://en.cppreference.com/w/cpp
http://www.cplusplus.com/
http://www.parashift.com/c++-faq-lite/ (ezt különösen ajánlott elolvasni)
http://aszt.inf.elte.hu/~gsd/halado_cpp/ (ezt is)

back to homepage

Valid HTML 4.01 Transitional Valid CSS!