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.

A sorozat további részei

Hozzáfűznél valamit?

Dobj egy emailt a blog kukac deadlime pont hu címre.

Feliratkoznál?

Az RSS feed-et ajánljuk, ha kedveled a régi jó dolgokat.