Nyugodtan mondhatjuk, hogy C++ == D :)
A C++ igencsak különbözik a C-től. Elsősorban a két plussz miatt,
másrészt meg a C++ egy igazi objektum-orientált nyelv. Ugyanakkor használható
úgy is, hogy nem aknázzuk ki a benne rejlő plussz lehetőségeket, de akkor nincs
is igazán szükségünk C++-ra, megteszi a jó öreg C is.
Ahhoz, hogy az objektum-orientáltság előnyeit kiaknázhassuk, meg kell
ismerkedni néhány új elemmel, melyektől egy öreg-motoros C programozót nem
biztos hogy elönt a boldogság. Az én első reakcióm is (sőt, bevallom a második
is) hasonló volt: minek túlbonyolítani a kódot felesleges
osztálydeklarációkkal, objektumlétrehozásokkal, virtuális függvényekkel, stb.?
Idővel rá kellett jönnöm, hogy ez a megközelítés nagyban megkönnyíti a
programozók munkáját, hisz a valós világot próbálja lemodellezni valamilyen –
kezdetleges - szinten.
Az öröklődés az objektum orientáltság alappillére, és egyben sok félreértés
okozója.
Idővel, ha jobban megismerjük az öröklődési mechanizmusokat, jól
megtervezett, könnyen karbantartható és bővíthető projecteket hozhatunk létre:
ehhez elengedhetetlen, hogy az osztályhierarchiánk logikusan legyen felépítve,
az objektum orientáltság összes előnyét kihasználva.
A C++ egy vérbeli OO nyelv: mindennel szolgál, mi szem-szájnak ingere -
legyen szó adatrejtésről (data hiding), egymásba ágyazásról (encapsulation),
vagy többalakúságról (polymorphism).
Ebben a cikkben leginkább az öröklődés megértéséhez feltétlenül szükséges
három kulcsszót - public, protected és private - próbálom
bemutatni, példákon keresztül érzékeltetni.
Alapfeltétel az öröklődéshez, hogy legyen kitől örökölni: az ősről illetve
szülőről van szó, C++ terminológiával az alap-osztályról (base class).
Nem csak osztály (class) de a struct is lehet ős, illetve szülő.
Az ősből lehet származtatni gyermek- (child) avagy származtatott
(derived) osztályokat, és utóbbiakból további osztályokat.
Az ún. osztályhierarchiánk egy fa struktúrát eredményez(het), ahol egy szülőnek
több gyermeke lehet, és a gyermekeknek is lehetnek leszármaz(tat)ottaik (többszörös
öröklődés), és így tovább.
Ez a hierarchia leginkább egy olyan családfára emlékeztet, ahol kizárólag
az egyenes ági leszármazottak szerepelnek, azaz a szülők és a gyerekek.
Tipp: MS VisualC++-ban megtekinthetjük egy osztály
ősosztályait illetve leszármazottait, ha a “Workspace->ClassView”-ban
jobb egérgombbal rákattintunk az adott osztályra, majd a “Derived Classes”
illetve “Base Classes” menüt választjuk. Részletes listát kapunk az osztály
függvény- és adattagjairól is, elérés szerint (public, protected illetve
private) csoportosítva.
Persze előfordulhat az is, hogy az ősosztályből nem kell származtatni,
teljességgel kielégíti minden igényünket.
Egy származtatott osztályt így deklarálunk:
class GyermekOsztály
: [elérhetőség] ŐsOsztály
{
// tagfüggvények és tagváltozók
deklarálása
}
Az elérhetőség – et (access
specifier) nem kötelező megadni, ebben az esetben az alapértelmezett
elérhetőség érték lép érvénybe:
-
osztályok esetében private,
-
struct-ok esetében pedig public lesz.
Ha már vannak osztályaink, ahhoz, hogy az
egészet felhasználhassuk a programunkban, az adott osztályt példányosítani
kell, azaz objektumokat kell belőle létrehozni.
Objektumokat ugyanúgy deklarálunk, mint
bármilyen más változót (az osztály neve a változó típusa):
GyermekOsztaly
gyermekObj;
vagy
GyermekOsztály*
gyermekObjPtr = new GyermekOsztály;
Figyelem: ha mutatóként
hozzuk létre az objektumot, természetesen gondoskodnunk kell az általa
lefoglalt memória felszabadításárol is, ha már nincs szükségünk az adott
objektumra:
delete
gyermekObjPtr;
Az objetumon keresztül a public tagokat a “.”
illetve pointerek esetében a “->” operátoron keresztül érhetjük el:
gyermekObj.meret
= 100; gyermekObjPtr->meret
= 100;
int i =
gyermekObj.getValue(10); int i
= gyermekObjPtr->getValue(10);
Figyelem:
Amikor létrehozzuk az
osztály egy példányát, kizárólag a public tagok érhetők el – legyenek ezek
adattagok vagy függvények.
A elérhetőség megadásával
háromféle “szűrőt” alkalmazhatunk:
1.) Public
– a nagylelkű
class
GyermekOsztaly : public OsOsztaly
Az OsOsztaly public és
protected tagjai ugyancsak public illetve protected tagjai lesznek a
származtatott osztálynak is. Az így származtatott osztály példányából elérhetők
az ős public tagjai.
2.) Protected
– a szigorú
class
GyermekOsztaly : protected OsOsztaly
Az OsOsztaly public és
protected tagjai protected elérésűek lesznek
a származtatott osztályban. Ezért a gyermekosztály példányából nem érhetők
el az ős public tagjai, public tagokat kell definiáljunk a gyermekOsztályban
ahhoz, hogy az objektumot létrehozásán kívül bármi másra használhassuk a
kódban.
3.) Private – a fukar
class
GyermekOsztaly : private OsOsztaly
Az OsOsztaly public és protected tagjai private
elérésűek lesznek a származtatott osztályban. Természetesen ebben az esetben is
igaz, hogy a gyermekosztályból létrehozott objektumból nem érhetők el az ős
public tagjai, saját public tagokat kell definiálni.
Figyelem:
A
gyermekosztály semmilyen esetben sem érheti el a szülőosztály private tagjait!
Most kizárólag az utolsó hárommal szeretnék foglalkozni, az “abrakadabra”
amúgy is túl van tárgyalva, arról nem is beszélve, hogy nem sok hasznát vesszük
a programozásban (bár néha elkelne egy kis varázslat, főleg project-zárások előtt…).
A public a nyitott kapu, ha egy osztály adattagja vagy függvénye public-ként
lett deklarálva, akkor mindenki számára publikus, azaz elérhető. Szép és jó ez
a nyitottság, ugyanakkor nem árt vele csínján bánni. Egy olyan class, ahol
minden public, finoman szólva is gyanús, és általában rossz tervezésre
utal. Valószínű, hogy lesz néhány olyan függvényünk, adatunk, amit nem kell a
“nyilvánosság” elé tárni.
Főleg az adattagok esetében nem túl célszerű, ha mindenki szabadon
módosítgathatja azokat. Sokkal elegánsabb, ha az adataink private- vagy
protected– ként deklaráljuk, és public függvényeken keresztül lehet őket
lekérdezni illetve módosítani.
Valami ilyesmire gondoltam:
class BaseClass
{
// adat-tagok
protected:
bool myBusyFlag;
// függvények
public:
// constructor & destructor
BaseClass();
~BaseClass();
bool getBusyFlag() { return myBusyFlag; }
void setBusyFlag( bool flag_in = true ) { myBusyFlag =
flag_in; }
}
Ezekután a myBusyFlag-et az osztály objektumaiból nem lehet direkt módon
elérni, csakis a függvényein keresztül:
1.)
a következő
részletnél hibát dob a fordító, mivel a myBusyFlag protected tag, tehát így nem
lehet elérni
BaseClass aClass;
aClass.myBusyFlag = true; // hiba!
2.)
a public
függvényeken keresztül már gond nélkül elérhető a private adat
BaseClass aClass;
aClass.setBusyFlag();
bool aFlag;
aFlag = aClass.getBusyFlag;
A protected kulcsszó sok kezdő számára
okozhat fejfájást. Nos, a titka abban áll, hogy igazából csak az öröklődésben
játszik szerepet, a protected tagok nem érhetők el az osztály példányaiban.
Öröklődésnél viszont kifejezetten hasznos, mivel
így átörökíthetünk olyan adattagokat, függvényeket a gyermekosztályokba,
melyeket nem akarunk az osztály példányában direkt módon elérhetővé tenni.
Tehát nem kell az ősben public-nak deklarálni a
tagokat, mégis át lehet őket örökíteni egy gyerekosztályba, és ráadásul még
rejtve is marad – ergo teljesül az adatrejtés avagy data hiding követelménye
is.
Megpróbálom ezt is egy konkrét példán keresztül
érzékeltetni. Legyen az ősosztály az emlősök osztálya.
class Emlos
{
// adattagok
public:
…
protected:
…
private:
…
// függvények
public:
// constructor
& destructor
Emlos();
~Emlos();
taplalkozik();
szaporodik();
protected:
jar();
ugral;
uszik();
repul();
…
private:
}
Miért lettek protected függvények a jar(), ugral(),
uszik(), repul() ? Ha belegondolunk, ebből komoly bonyodalmak
lehettek volna.
Ha ugyanezeket a függvényeket public-nak
deklaráltuk volna az ősosztályban (“Emlos”) az “Ámbráscet” osztálya public
öröklődés mellett rendelkezne a repülés képességével, ami beláthatatlan
következményekkel járt volna az élet alakulására a Földön…
Megjegyzés: Természetesen megtehetjük azt is, hogy
az “Emlos” osztályban egyáltalán nem deklaráljuk illetve definiáljuk ezeket a
függvényeket, hanem minden egyes állatfaj esetében az adott fajra jellemző
mozgásformát.
Ez viszont óriási többletmunkával járna, a kód nagyságrendekkel nagyobb lenne,
hisz minden egyes emlős állatfaj esetében deklarálni és definiálni kellene a
megfelelő függvényeket.
A “repul()” és “uszik()” esetében szerencsések lennénk,hisz viszonylag kevés
emlősre jellemző, de a “jar()” már szerepelne néhány százezerszer – és ezzel a
körülményes megoldással egyben az objektum-orientáltságnak is beintenénk egy jó
nagyot.
Szerencsére a Nagy Tervező értett a dolgához, így
a cetek maradtak a vízben:
class Ambrascet : public Emlos
{
…
public:
Ambrascet();
~Ambrascet();
mozog() { uszik(); }
}
Az ámbráscetek az “Emlos” osztályból öröklött
uszik() fgv. meghívásával valósítják meg a mozgást.
Mint említettem, az uszik() függvény protected,
tehát nem érhető el sem az ős, sem pedig a származtatott osztály példányából:
Ambrascet
cetObj; // létrehozzuk a cet egy példányát
ambrascetObj.uszik();
// hiba!
A következő módszer helyes:
Ambrascet
cetObj; // létrehozzuk a cet egy példányát
ambrascetObj.mozog();
A mozog public függvény az Ambrascet osztályban,
és meghívja az “Emlos”-beli protected uszik() függvényt.
A private tagot kizárólag az őt deklaráló
osztály érheti el, és senki más.
Öröklődéssel nem adható át, és az osztályból
létrehozott objektumból sem érhető el.
class Denever :
public Emlos
{
//adattagok
public:
protected:
private:
static
int deneverekSzama; // példányok száma
…
//függvények
public:
Denever();
{
kisDeneverSzuletett();
}
~Denever();
{
deneverElpusztult();
}
mozog()
{ repul(); }
int
hanyDeneverunkVan() { return deneverekSzama; };
private:
int
kisDeneverSzuletett() { return deneverekSzama++; } // J
int
deneverElpusztult() { return deneverekSzama--; } // L
}
Az fenti peldában a denever-populáció számát
követhetjük nyomon a statikus számláló alapján, mely az osztály konstruktorában
lesz növelve, és a destruktorban csökkentve. Nem kívánatos, hogy ezt az értéket
bármi más módon változtathassuk, ezért lettek a “deneverekSzama”
nevű változó és az őt módosító függvények private-ként deklarálva.
A konstruktor akkor hívódik meg, amikor
létrehozzuk az osztály egy példányát, a destruktor pedig akkor, amikor a példány
– avagy objektum – megszűnik. A “deneverekSzama” static- ként lett deklarálva,
ezzel biztosítva hogy a Denever osztály összes példánya (objektuma) ezt az egy
változót használja ( és nem jön létre minden példány esetében egy új
“deneverekSzama” változó ). A hanyDeneverunkVan() public függvényen keresztül
kapjuk vissza a számláló értékét.
{
Denever
repuloEgerke;
cout <<
repuloEgerke.hanyDeneverunkVan() << /r/n; // 1
{
Denever kisVampir;
cout << kisVampir.hanyDeneverunkVan() << /r/n; // 2
} // kilépünk a
hatókörből, kisVampir megszűnik :(
cout <<
repuloEgerke.hanyDeneverunkVan() << /r/n; // 3
}
Az eredmény a következő lesz:
1
2
1
Mi is történt?
1.) Amikor
repuloEgerke-t letrehozzuk, meghívódik a Denever osztály konstruktora,
mely növeli a „deneverekSzama“ változó értékét – így az első lekérdezés
után megjelenik az 1 a kérpernyőn.
2.) Új
kapcsoszárójel új hatókört jelent, ebben létrehozzuk kisVampirt; újból
meghívódik a Denever osztály konstruktora, növeli a „deneverekSzama“
változó értékét – a második lekérdezés eredménye 2. Ne felejtsuk el, hogy a
“deneverekSzama” static mivolta miatt csak egyetlen példányban létezik. Ezt
látja a kisVampir illetve repuloEgerke nevű objektum is.
3.) Bezárjuk a
kapcsoszárójelet, ezáltal a hatókör megszűnik és a kisVampir-nak is vége – meghívódik
a Denever osztály destruktora, mely csökkenti a „deneverekSzama“
változó értékét – és az utolsó lekérdezés után már csak egy denevérünk –
bocsánat, Denever objektumunk létezik.
Számos fogalom vár még tisztázásra, de az itt
bemutatottak megértése mindenképp szükséges ahhoz, hogy megértsük az öröklődés
működését a C++-ban.
A Kód legyen Veletek!