Operator overloading
operátor kiterjesztés¶
Az eddig ismert alap típusokra léteznek eleve definiált műveletek, operátorok. Egy egész típusú változó operátora lehet pl. összeadás (+), kivonás (-), értékadás (=), inkrementálás (++). Ezeknek a jól megszokott hatásuk van a változó értékére. Az általunk megvalósított osztályokra azonban alapból nem működnek ezek az operátorok. A C++ azonban lehetőséget kínál rá, hogy saját típusra is értelmezzük az operátorokat, csak meg kell mondanunk, hogy mi történjen pl. összeadás esetén. Ezt operátor kiterjesztésnek, vagy angolosan operator overloading hívjuk.
Vegyük alapul, hogyan működik pl. az összeadás operátor az egész számokra! Mit is jelent, ha leírjuk az a+b
kifejezést:
1 |
|
Fontos észrevenni, hogy az a
és b
értéke nem változik meg, és a kifejezés eredményének tárolására egy új ,,objektum'' jön létre (3+2 esetén 5, ami szintén egy egész), tehát van visszatérési értéke. Egész számok esetén ez egy egész szám lesz, tehát ha kiegészítjük a fentit a visszatérési értékkel, akkor a következő módon fog kinézni:
1 |
|
A saját osztályunkra nézve hasonlóan néz ki, csak meg kell határoznunk, hogy mit mivel szeretnénk összeadni és ennek mi lesz az eredménye. Ha vesszük a Kurzus
osztályt és hozzáadunk egy Hallgato
osztályt, akkor egy bővített kurzust kapunk, ami szintén egy Kurzus
.
Tehát a két paraméter: Kurzus
, Hallgato
(ami valójában konstans referencia lesz). A visszatérési érték típusa Kurzus
.
1 |
|
Mivel az operátor viselkedése szorosan kötődik a Kurzushoz (így fogalmaztuk meg) OOP szerint az osztályon végzett műveletet rakjuk be az osztályba magába! Ezt a metódust megírhatjuk mint a Kurzus
osztály tagfüggvénye. Ha tagfüggvény, akkor az első paraméter (Kurzus
) adott is, hiszen ha meghívjuk egy objektum operator+
függvényét, az első paraméter maga az objektum, így a paraméterlistából azt el is hagyhatjuk.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Ez hozzáadja a Kurzus
-hoz a kapott hallgatót, ha még elfér. Hasonló szemantikája van, mint az egész számoknál.
A használata a következő:
1 2 3 4 5 6 7 8 |
|
Az egészeknél ha egy változóhoz hozzá akarunk adni egy értéket, az összeadást a következőképpen kell használnunk:
1 |
|
1 |
|
operator+=
-t definiáljuk felül.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Ez a megvalósítás meglehetősen hasonló, azonban fontos kiemelni néhány jelentős különbséget! Az első, hogy a visszatérési érték nem Kurzus
, hanem Kurzus&
és nem hozunk létre egy újabb Kurzus
típusú objektumot. Ennek oka a következő:
Mikor az operator+
-t használtuk, akkor az operandusok változatlanok maradtak, és létrejött egy új objektum, mely az összeget tárolta el és teljesen független a két operandustól. Ezt az alábbi ábra vizualizálja, amin két Kurzus
objektumot összeadva létrejön egy harmadik Kurzus
:
Ha ezt k1 = k1 + k2;
formában akarjuk használni, akkor az eredeti változó objektuma felülíródik az eredmény objektummal. Ezt a kövezkező két ábra szemlélteti.
Ilyen esetekben jobb az operator+=
használata. Ennél már magában az operátorban benne van az értékadás, tehát az eredeti objektum módosul. Ezért is kell referenciával visszatérni, hiszen ugyanarról az objektumról beszélhetünk.
A hallgató hozzáadása is hasonlóan működik:
Fontos, hogy az eredeti Kurzus
módosult, a Hallgato
bekerült a kurzusra, de nem módosult. Ezért lehetett a paraméterben const &
.
Mivel az operátorokat függvényeknek vettük, ezért a meghatározásoknál nemcsak az operátor neve, hanem a paraméter típusa is meghatározza. A Kurzus
osztályban lehet két operator+=
, az egyik Hallgato
, a másik Kurzus
paraméterrel. Az így kiegészített Kurzus
osztály:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
A += operátor hasonlóan működik Kurzus
-ra, mint Hallgató
esetén:
Természetesen, a kurzushoz adott paraméter most sem módosul, így az lehet const &
.
indexer operátor¶
Az indexer operátor különleges operátornak tekinthető. Szemantikáját általában tömbökhöz kötjük, elemek eléréséhez (de tetszőleges értelmezést adhatunk neki). Ehhez az operator[](int)
metódust kell kiterjeszteni. Ha egy tömbben el akarunk érni egy elemet, azzal több dolgot is tehetünk. Egyszerűen lekérdezhetjük (módosítás nélkül):
1 |
|
Vagy az értéket módosíthatjuk:
1 |
|
Mivel az első esetben csak az értékre vagyunk kíváncsiak, nem kell tudnunk módosítani azt. A második esetben már a visszaadott értéket szeretnénk módosítani, vagyis igazából az int_array
objektumban lévő 2. indexen lévő elemet szeretnénk felülírni.
Azt már vettük, hogy ha egy függvény módosíthatja vagy sem az objektumot azt a const
kulcsszóval jelezhetjük. Az első esetben nem módosíthatjuk tehát használjuk a const
kulcsszót, a második esetben pedig nem.
Természetesen nem elég a metódust konstanssá tenni, hiszen ha a visszatérési értékben kiszivárog egy "kiskapu", akkor az objektumot elronthatjuk. Ennek kivédése érdekében a visszatérési értéket is máshogy kell megadnunk. Erről már szó volt a const
-ról szóló részben. Az indexer operátorok kiterjesztése a Kurzus
osztályra (annak éri meg, hiszen annak van tömbhöz köthető jellege):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Az első esetben egy referenciát adunk vissza, tehát ahova visszaadjuk, ott elérik az adott elemet pl. felül is írhatják az értékét. Mivel ezzel módosulhat a Kurzus
, ez a függvény nem is const
. A második esetben szintén referenciát adunk vissza, azonban ez most const &
tehát nem írható felül az értéke. Ez biztosítja, hogy akármit is teszünk, a Kurzus
nem fog megváltozni, így ez a függvény lehet const
.
++ / -- operátorok¶
A ++a
, a++
, --a
és a--
kifejezések mindenki számára ismerősek. A különbség a prefix és a postfix inkrementálás (dekrementálás) közt annyi pl. intekre nézve, hogy a prefix verzióban egy kifejezés kiértékelésekor már a megnövelt (csökkentett) érték van használva, míg postfix esetben a kifejezés kiértékeléskor még a nem megnövelt (csökkentett) értéket használjuk fel.
Mivel ezek is operátorok, ezeket is felül lehet definiálni. Azonban a pre és post verziók közt leírásban nem sok különbséget találunk, így valahogyan meg kell különböztetni azokat. Az operator kiterjesztése során a post verzióhoz egy plusz (nem használt) paramétert adunk, csupán a megkülönböztetés miatt.
Működésük miatt a visszatérési értékük sem teljesen egyezik meg. Nézzünk egy példát rá!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Hasznos a kettőt együtt megvalósítani, és ekkor az egyiket a másik felhasználásával, hiszen ekkor hiba javításakor csak egy helyen kell módosítanunk.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
További operátor felüldefiniálás¶
Ahogy azt előzőekben láttuk, az operátor felüldefiniálás csak egy függvény / metódus megvalósítását jelenti. Ha egy osztálynak metódusként írunk meg egy operátort, akkor tudjuk, hogy az operátor első paraméterét elhagyhatjuk, hiszen az maga az objektum lesz. Természetesen, ha unáris (vagyis csak egy paramétert váró) operátorról van szó, akkor nem kell megadni semmit, hiszen csak az objektumra van szükség. Ilyen operátort pl. az operator++
, operator--
, operator!
stb.
operator!¶
A felkijáltójel használata gyakran a negálást jelenti. Ez egy paramétert vár, magát az objektumot. A Kurzus példánál maradva nézhetjük, hogy bizonyos kurzusok jóváhagyásosak. Ezt egy bool értékkel le tudjuk tárolni, azonban ha meg akarjuk változtatni, akkor akár egy negálással is megtehetjük. Erre nézzünk példát!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
Konverziós operátorok¶
Sokszor konvertáljuk az adatokat és előfordulhat, hogy ezt egy általunk készített osztályra is meg kell tennünk. Ekkor írhatunk egy eljárást, ami mindig megcsinálja a konverziót, de kényelmesebb, ha meg tudjuk mondani, hogyan kell egy-egy adott konverziót megtenni.
Ha a Kurzus objektumunkat int
té szeretnénk konvertálni, pl. a létszámmal legyen egyenlő a konverzió eredménye, akkor ezt megtehetjük egy konverziós operátorral.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
A fenti példán láthatjuk, hogy a kiíratásban unsigned típusra konvertáltuk a kurzusunkat. Ezt a Kurzusban definiált konverziós operátorral tettük meg. Fontos, hogy konverziós operátor esetében nem kell kiírni a visszatérési típust! Nem kell, hiszen a konverziós operátor neve egyértelűen meghatározza a visszatérési típust. Amire szeretnénk konvertálni, az lesz az eredmény. Eltérő típus esetében fordítási hibát kapunk.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Ebben a példában láthatjuk, hogy a konverziót nem kell explicit módon kiírnunk, automatikusan megtörténik. Ez azonban problémához is vezethet, hiszen lehet, hogy nem akartunk konvertálni, vagy éppen másik típusra akartuk volna. Erre megoldást az jelentene, ha a konverzió csakis az első módszerrel történhetne meg, vagyis csak akkor, ha kiírjuk (explicit módon). Ennek a megoldása, ha a konverziót ellátjuk az explicit taggal. Ekkor az automatikus konverziót letiltjuk, kötelezően ki kell írni a kívánt konverziót.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
A konverzió másik iránya¶
Eddig azt néztük meg, hogyan lehet egy már létező objektumból konvertálással egy értéket előállítani. A másik irány, hogy egy értékből hogyan tudunk adott típusú objektumot előállítani. Erre példát a stringeknél láthattunk:
1 2 3 4 5 6 7 8 |
|
Ezt a saját osztályunkra is megtehetjük. Mivel érték alapján kell létrehoznunk objektumot, a konstruktorhoz kapcsolódó részeket kell vizsgálnunk!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Kimenet
5
0
5
Látszik, hogy az unsigned értékkel tudjuk a k3 objektumot inicializálni, hiszen létezik hozzá konstruktor. Terészetesen előfordulhat, hogy ezt az autmatikus konverziót (konstruktor hívást) szeretnénk elkerülni. Ekkor a konstruktornak is megadhatjuk, hogy explicit lehessen csak meghívni.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Létrehozva: 2019-09-23