Programozás módszertan p. Programozás módszertan Operátorok túlterhelése a C++ nyelvben Pere László (pipas@linux.pte.hu) PÉCSI TUDOMÁNYEGYETEM TERMÉSZETTUDOMÁNYI KAR INFORMATIKA TANSZÉK
Bevezetés Programozás módszertan p.
Programozás módszertan p. Operátorok túlterhelése A programozás során gyakran használunk operátorokat a programban elvégzendő feladatok tömör, olvasható kifejezésére. Az operátorok a beépített típusok kezelésére kitűnően használhatók, a programozó által létrehozott típusok, osztályok kezelésére azonban természetesen nem alkalmasak változatlan formájukban. Megoldást az operátorok túlterhelése (operator overloading) jelentheti. A túlterhelés segítségével a programozó meghatározhatja, hogy mi történjék az általa létrehozott típusokkal az egyes operátorok hatására.
Programozás módszertan p. Operátorok túlterhelése Az operátorok túlterhelése során megváltoztathatjuk az egyes operátorok jelentését, de: az operátor túlterhelés segítségével nem hozhatunk létre új operátorokat, nem változtathatjuk meg az operátorok aritását, nem változtathatjuk meg az operátorok precedenciáját. Nyilvánvaló, hogy ezek a megkötések korlátot szabnak a programozó dühöngő alkotói vágyának és megakadályozzák a nyelv felismerhetetlenségig fajuló torzítását.
Programozás módszertan p. Túlterhelhető operátorok A következő operátorok túlterhelhetők: + - * / % ^ & ~! = < > += -= *= /= %= ^= &= = << >> >>= <<= ==!= <= >= && ++ -- ->*, -> [] () new new[] delete delete[] A = (értékadó), & (címképző) és a, (kiválasztó) operátorok túlterhelés nélkül is érvényesek.
Programozás módszertan p. Nem túlterhelhető operátorok A következő operátorok nem túlterhelhetők: ::..*? : sizeof typeid Ezeknek az operátoroknak a túlterhelés nemkívánatos mellékhatásokkal járna, ezért nem lehet őket túlterhelni.
Programozás módszertan p. Előre meghatározott jelentés Bizonyos operátorok szokásos jelentései között összefüggés van. A a++ például általában megegyezik az a+=1 jelentésével, ami ugyanaz, mint a=a+1. Az ilyen hasonlóságokat a fordító túlterhelt operátorok esetében nem követi, azaz, ha ilyen tulajdonságokat kívánunk használni, magunknak kell megvalósítani őket.
A túlterhelés menete Programozás módszertan p.
Programozás módszertan p. A függvény neve A túlterhelés során az operátort megvalósító utasításokat függvényként adjuk meg. A függvény nevében az operator kulcsszót az operátor követi. Az operátor argumentumai a függvény argumentumai és visszatérési értéke, amelyek beépített típusok, objektumok (kis méret) vagy referenciák (nagy méret) lehetnek. Int &operator=(const int i);
Programozás módszertan p. 1 Unáris operátorok Az unáris operátorok megvalósíthatók: Paraméter nélküli nem statikus tagfüggvényként: Int &Int::operator++(void) {... return *this; } Egyparaméter nem tag függvényként: Int &operator++(int &i) {... return i; }
Programozás módszertan p. 1 Unáris operátorok A ++ és -- alapértelmezett esetben prefix, postfix változatot egy ál-argumentummal készíthetünk, amelynek típusa int. public: Int(void); Int(const Int &i); Int &operator=(const int i); Int &operator++(void); // Prefix Int &operator++(int); // Postfix
Programozás módszertan p. 1 Bináris operátorok A bináris operátorok megvalósíthatók: Egyparaméterű nem statikus tagfüggvényként: Int &Int::operator+=(Int a){ ival += a.ival; return *this; } Kétparaméterű nem tag függvényként: Int operator+(int &a, Int &b) { Int s = a; return s += b; }
Programozás módszertan p. 1 Tag vs. nem tag Az olyan operátorok, amelyek első paraméterként alaptípust fogadnak, nem lehetnek tagfüggvények, hiszen a taggfüggvények első, rejtett argumentumként csak objektum adható meg. Az a + 2 értelmezhető a.operator+(2) formában, mert az első paraméter nem alaptípus, így lehet a osztályának tagfüggvénye. A 2 + a viszont nem értelmezhető 2.operator+(a), formában, mert a 2 típusa int, ami nem osztály, így nem lehetnek tagfüggvényei.
Programozás módszertan p. 1 Tag vs. nem tag Előnyös, ha minél kevesebb függvény fér hozzá az objektum rejtett (private) adattagjaihoz, azaz, ha minél kevesebb tagfüggvényt készítünk a operátorok túlterhelése közben. Ezt úgy érhetjük el, ha csak azokat az operátorokat valósítjuk meg tagfüggvényként, amelyek módosítják első paraméterüket (például ++, =, +=). Azokat az operátorokat, amelyek csak új értéket állítanak elő, tagfüggvényekre visszavezetve nem tag függvényként valósíthatjuk meg.
Programozás módszertan p. 1 Tag vs. nem tag A következő részlet ezen meggondolás alapján készült: Int &Int::operator+=(Int a){ } ival += a.ival; return *this; Int &operator+(int &a, Int &b) { Int s = a; return s += b; }
Típuskonverzió Programozás módszertan p. 1
Programozás módszertan p. 1 A kezdeti értékadás Fel kell hívnunk a figyelmet, hogy a kezdeti értékadás nem egyezik meg az értékadással. A különbség különösen akkor szembeszökő, ha az operátorokat túlterheljük, mert a kezdeti értékadásnál nem használjuk a túlterhelt értékadó operátort. Az Int x = 10; tehát nem azonos az Int x; x = 10; kifejezésekkel.
Programozás módszertan p. 1 Egyparaméterű konstruktor Az egyparaméterű konstruktor konverziót jelent a paraméter típusáról a konstruktor típusára. Ezt kezdeti értékadás során is, és kifejezéseken belül is automatikusan használhatjuk, azaz a konstruktor nevét ilyen esetben nem kell kiírni. Hasonlóképpen használhatók a többparaméterű konstruktorok is, de azok nevét mindig ki kell írni.
Programozás módszertan p. 1 Egyparaméterű konstruktor inline Int::Int(int i) { initialized = true; ival = i; }... Int x = 10; Int z; z = x + 1;...
Programozás módszertan p. 2 Explicit konstruktorok Ha az egyparaméterű konstruktorra szükségünk van, de nem akarjuk automatikus típuskonverzióra használni, az explicit kulcsszót használhatjuk: class Int { public:... Int(void); explicit Int(int i); Az ilyen konstruktorok csak úgy használhatók típuskonverzióra, hogy kimondottan előírjuk a hívásukat: a = b + Int(1);
Programozás módszertan p. 2 Többparaméterű konstruktor A többparaméterű konstruktor nevét mindig ki kell írni: complex x = complex(2, 2); y = x + complex(4, 3); Viszont a kezdeti értékadást ilyen esetben a másoló konstrukor hívása nélkül is el lehet végezni: complex x(2, 2);
Programozás módszertan p. 2 Vegyes módú aritmetika Ha az egyparaméterű konstruktor hívását el akarjuk kerülni mert például a konstruktor hívása nem olcsó, akkor használhatjuk a vegyes módú aritmetika módszerét is. A módszer lényege, hogy a túlterhelt operátorváltozatokból a fordító az argumentumok alapján azt választja ki, amelyikre szükség van, nem az argumentumot konvertálja a függvényparaméterre. E módszer hátránya, hogy fárasztó a sokféle függvény elkészítése, közben hibákat véthetünk.
Programozás módszertan p. 2 Vegyes módú aritmetika complex operator+(complex a, complex b) { complex r = a; return r += b; // b complex } complex operator+(complex a, double b) { complex r = a; return r += b; // b double } complex operator+(double a, complex b) { complex r = b; return r += a; // a double }
Programozás módszertan p. 2 Konverziós operátorok A konstruktorral elvégzett típuskonverzió nem alkalmas beépített alaptípusra való konverzióra, mert az alaptípusoknak nincsen konstruktora, régebben létrehozott osztályokra való konverzióra, ha a régebbi osztály nem módosítható. Ilyen esetekben konverziós operátorokat használhatunk.
Programozás módszertan p. 2 Konverziós operátorok Ha T és X egy-egy típus neve, akkor a X::operator T() függvény határozza meg az X típusról a T típusra való konverziót. A konverziós operátor visszatérési értékének típusát nem adhatjuk meg, ilyen értelemben a konverziós operátor hasonlít a konstruktorokra. A konverziós operátorok hívása automatikusan történik. Ha túl sok konverziós operátort készítünk, a kifejezések többértelművé válhatnak!
Programozás módszertan p. 2 Konverziós operátor A következő függvény az Int típusról konvertál int típusra: Int::operator int const() { } return ival;
A friend Programozás módszertan p. 2
Programozás módszertan p. 2 A friend Amikor egy tagfüggvényt hozunk létre, három dolgot jelzünk: 1. A függvény hozzáférhet a privát tagokhoz. 2. A függvény az osztály hatókörébe tartozik. 3. A függvényt az osztály egy objektumára kell meghívni. A static esetében csak az első kettő, a friend esetében pedig csak az első érvényes.
Programozás módszertan p. 2 Szükségszerűség A friend módosító használata szükséges, ha olyan operátort valósítunk meg, amelynek operandusai és/vagy eredménye különbözők, ezért szükséges, hogy több osztály privát tagjaihoz is hozzáférjen a megvalósító függvény. Az ilyen esetekben nem tudjuk elkerülni a friend módosító használatát.
Programozás módszertan p. 3 A friend függvény (nem tag) class Matrix; class Vector {... friend Vector operator*(const Matrix&, const Vector&); }; class Matrix {... friend Vector operator*(const Matrix&, const Vector&); };
Programozás módszertan p. 3 A friend függvény (nem tag) Vector operator*(const Matrix&, const Vector&) {... }
Programozás módszertan p. 3 A friend függvény (tag) A friend függvény lehet egy másik osztály tagfüggvénye: class List_iterator { };... int *next(); class List {... friend int *List_iterator::next(); };
Programozás módszertan p. 3 A friend osztály Ha egy osztály minden tagfüggvénye friend, egyszerűsített jelölést is használhatunk: class List { };... friend class List_iterator;
Nagyméretű objektumok Programozás módszertan p. 3
Programozás módszertan p. 3 Argumentumok Az operátotok túlterhelésére használt függvények argumentumai lehetnek objektumok, de ez nagyméretű objektumok esetében nem szerencsés. Mutatókat nem használhatunk argumentumként, mert a mutatókra alkalmazott operátorok nem terhelhetők túl. A referenciák argumentumként használva lehetővé teszik a nagyméretű objektumok kezelését anélkül, hogy lemásolnánk őket.
Programozás módszertan p. 3 Visszatérési érték Az operandusok értékét megváltoztató operátorok (például ++, --) a megkapott referenciát módosítás után referenciaként visszaadhatják. Azon operátorok, amelyek új értéket hoznak létre (például +, -), referenciát visszaadva lokális változójuk referenciáját adnák vissza, ami tiltott művelet. Mivel az operátot egy kifejezésen belül többször is szerepelhet, a visszaadott érték nem lehet lokális statikus változó referenciája sem. A legegyszerűbb megoldás a visszatérési érték másolása.
Indexelés Programozás módszertan p. 3
Programozás módszertan p. 3 Indexelés Az indexelést a [] operátor túlterhelésével befolyásolhatjuk. Az operator[] függvénynek tagfüggvénynek kell lennie, paramétere azonban tetszőleges típusú lehet, így készíthetünk akár asszociatív tömböt is. Az operator[] rejtett operátora természetesen maga az objektum, amelyet tömbként idexelni akarunk.
Programozás módszertan p. 3 Indexelés Figyeljük meg, hogy az indexelés során az előzőekben elmondottakkal ellentétben referenciát, balértéket kapunk: class Assoc { public: Assoc(); const double &operator[](const string &); double &operator[](string &); }...
Függvényhívás Programozás módszertan p. 4
Programozás módszertan p. 4 A függvényhívás A függvényhívó operátor túlterhelése olyan osztályok esetében hasznos, amelyeknek csak egy műveletük van. Ez triviális, hiszen ha a egy objektum, akkor a a() a függvényhívás, ami csak egyféle lehet. A függvényhívó operátor túlterhelése az operator() nevű függvény megvalósításával történhet.
Az indirekció Programozás módszertan p. 4
Programozás módszertan p. 4 Az indirekció A -> az indirekció operátora egyparaméterű posztfix operátorként terhelhető túl. Az argumentum a -> bal oldala, visszatérési értékének mutatónak kell lennie, amire az indirekció vonatkozni fog. A -> túlterhelésével smart pointer készíthető, amellyel a hivatkozás esetén mindig csinálni akarunk valamit.
Programozás módszertan p. 4 Példa A következő példa bemutatja, hogyan készíthetünk pointert, ami a hivatkozáskor beolvassa az adatokat a háttértárról: Rec *Rec_ptr::operator->() { if (in_core_address == 0) in_core_address = read_disk(id); } return in_core_address;
Programozás módszertan p. 4 Kötelező irodalom [1] BJARNE STROUSTRUP: A C++ Programozási Nyelv, Kiskapu Kiadó Budapest, ISBN: 963 9301 18 3, 343 393. o.