C++ - leszel a barátom?

 

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.

 

 

Öröklődés

 

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!

 

 

Varázsszavak: abrakadabra, public, protected, private

 

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!

 

Nyisztor Károly - nyisztor.karoly@evosoft.hu