Objektum-orientált C++ I.
Bevezetés az objektum-orientált C++ világába.
Egy kis fejtörés után arra az elhatározásra jutottam, hogy az elkerülhetetlennek vélt C++ tudás-felfrissítést egy bejegyzés-sorozat keretében ejtem meg, így legalább nem csak nekem lesz hasznom a dologból.
Feltételezem, hogy aki ez iránt a téma iránt érdeklődik, már rendelkezik némi C++-os illetve C-s tapasztalattal, tehát az alapvető szintaxis ismertetésétől eltekintenék. Hogy rögtön a közepébe vágjunk, első körben a struct
nevezetű nyelvi szerkezet felé terelném a szót: a dolog már a C nyelvből is ismerős lehet, a különbség ott kezdődik, hogy immár nem csak különböző típusú változókat gyűjthetünk vele egy kupacba, hanem a hozzájuk tartozó függvényeket is. Igazából én az egyetlen értelmét a C-vel kapcsolatos visszafelé-kompatibilitásban látom ennek a dolognak, mivel egy az egyben ugyanarra a dologra használható mint a class
azzal a csöppnyi különbséggel, hogy a struct
tagfügvényei és változói alapértelmezésben public
, míg a class
esetében private
részében vannak.
Osztályok
Talán tisztázzuk is rögtön ezeket a public
, private
dolgokat. A public
részben elhelyezett függvényeket (illetve változókat) az osztály példányosítása után lehet használni, míg a private
részben lévőket csak az osztály maga tudja felhasználni. Ezek a kis cimkék nagyon hasznosak tudnak lenni, így egy objektumhoz létre tudunk hozni egy nyilvános (public
) felhasználói felületet, aminek ismeretével használni lehet majd az osztályt. Arra gondoltam, hogy a komplex számokat megvalósító osztály elég jó bevezető lehet, aztán majd a későbbiekben jobban elrugaszkodunk a valóságtól.
class Complex
{
int re, im;
public:
Complex(const int& R, const int& I)
{
re = R;
im = I;
}
};
Ezzel így, ebben a formában semmit sem értünk el, mert hát egy struct
is képes lett volna erre annak idején C-ben, a konstruktort leszámítva (az osztály nevével megegyező nevű rémséget (vagy gyakrabban rémségeket) nevezzük konstruktornak, amivel például a kezdeti értékadást végezhetjük el, mint esetünkben is). Aki nem szeret ilyen sokat írni, annak ajánlom a Complex(int R, int I) : re(R), im(I) {}
- kicsit haxorosabb - formájú konstruktort, amivel ugyanezt a hatást érhetjük el. Itt az ideje, hogy valami izgalmasat is kezdjünk, a kezünkbe adott fene nagy hatalommal. Írjunk operátorokat!
Operátorok
class Complex
{
int re, im;
public:
Complex(const int& R, const int& I) : re(R), im(I) {}
Complex(const int& R) : re(R), im(0) {}
// Összeadás
Complex operator+(const Complex& num)
{
return Complex(re+num.re, im+num.im);
}
// Értékadás
Complex& operator=(const Complex& num)
{
re = num.re;
im = num.im;
return *this;
}
Complex& operator=(const int& num)
{
re = num;
im = 0;
return *this;
}
};
Mint az szépen látszik, a konstruktort és az egyenlőség operátort túlterheltem, hogy a következő megadások is működjenek:
Complex c1 = 3;
Complex c2(6, 3);
c1 = 5;
c1 = c2;
Az első sorban a második konstruktor futott le, mivel ez nem egy egyszerű értékadás (a Complex c1(3);
hosszabb formája). A második sorban (egyértelműen) az első konstruktor futott le, a harmadik illetve negyedik sorokban értelemszerűen a megfelelő értékadó operátor függvények. Most, hogy már ilyen rengeteg kódunk van, itt az ideje egy kicsit egyszerűsíteni a dolgokon. Az egyszerűsítés a második konstruktornak köszönhető, mivel ezzel egy int-Complex típuskonverziót is definiáltunk. Tehát az értékadás operátor túlterhelése ilyen módon feleslegessé vált.
Érdemes lehet még pár szót fecsérelni arra, hogy az értékadó operátor miért tér vissza egy Complex
referenciával, és mi a fene az a *this
. Az első kérdésre egy kicsit a dolgok mélyére kell nézni. Mi is történik egy c1 = c2 = 5;
sor hatására? Hosszabban valahogy így írhatnánk fel a dolgot: c1.operator=(c2.operator=(Complex(5))
. Így már világosan látszik, hogy ha nem térne vissza azzal az értékkel az értékadás, mint amit az objektum felvett, akkor bizony a második értékadás értelmetlen lenne. Ez nem feltétlenül baj, de engem úgy neveltek, hogy ha egy új típust/objektumot ír az ember, akkor az ott használt operátorok viselkedésükben próbáljanak minél jobban hasonlítani az operátor eredeti viselkedéséhez.
A második válasz már lényegesen egyszerűbb, a this
változó egy, az objketum éppen használt példányára mutató pointer, amit minden - nem static
- tagfüggvényben használhatunk, tehát a *this
maga az objektum amivel éppen dolgozunk.
Bevezetésnek, kedvcsinálónak talán ennyi elég is lesz. A következő részben kiegészítjük a Complex
osztályunkat pár új operátorral és egy új konstruktorral is (copy konstruktor), majd gyengéden súroljuk a std
névtér streamjeit is, csak hogy tényleg ne tűnjön olyan egyszerűnek az élet.