2016. augusztus 26., péntek

Overloading

A C++ nyelvben például a + és - operátoroknak több független funkciója van: aritmetikai műveleteket végez egész számokkal, valós számokkal és mutatókkal. Hogy hogyan végezi a műveletet, az a kontextustól függ, azaz hogy milyen adattípussal van használva. Minden adattípusnál másképp dolgozik, azaz másképp van definiálva a művelet. Ezek a definíciók vagy átdefiniálások úgymond túlterhelik az operátort (overloading). Saját adattípussal is túl lehet terhelni az operátorokat, például osztállyal vagy struktúrával létrehozott típussal. A túlterhelő függvény egy szimpla függvény, csakhogy elébe van írva az operator kulcsszó, amit a túlterhelni kívánt operátor követ:



Az operátorfüggvény általában tagfüggvény vagy friend függvény lehet. A (), [] és -> operátorokat csakis nem statikus tagfüggvényekkel lehet túlterhelni, a new és delete operátorokat pedig statikus tagfüggvényekkel. Hogy milyen függvényt válasszunk a túlterhelésre, az az operátortól függ.
- Amikor az operátorfüggvény tagfüggvény, akkor a baloldali vagy az egyetlen operandus az osztályának az objektuma vagy az arra hivatkozó referencia kell legyen. Ha a feladat úgy követlei, hogy a baloldali operandus egy másik osztály objektuma legyen, akkor az operátorfüggvény nem lehet tagfüggvény. Ha nem tagfüggvény, de közvetlenül el kell érje az adott osztály privát vagy protected tagjait, akkor friend típusnak kell deklarálni. Ha viszont az adott osztálynak van getter és setter tagfüggvénye a privát tagváltozók módosításához, akkor nem kötelező a friend típus használata.
- Egy példa erre a << és a >> operátor, melyek baloldalán be- vagy kimeneti stream-ek vannak. A << operátornak egy ostream& típusú operandusa kell legyen bal oldalt, mint például a cout, ezért az operátorfüggvénye nem lehet tagfüggvény. Ugyanez igaz a >> operátorra is.
- Egy másik ok, hogy az operátor ne legyen tagfüggvény, a kommutativitás. Például egy long int típusú number objektum és egy HugeInteger típusú hugenumber objektum (amely a számítógép korlátainál nagyobb egészeket képes összeadni) közötti összeadó művelet kommutatív kell maradjon. A hugenumber+number művelethez az operator+ függvény maradhat tagfüggvény, de hogy a number+hugenumber is működjön, az operátorfüggvényt az osztályon kívül kell megírni.

A << és a >> operátor túlterhelése
      A PhoneNumber osztály a következő példából egy olyan adattípus, amely egy 4+6 számjegyből álló telefonszámot határoz meg:

#include<iostream>
using std::cout;
using std::cin;
using std::ostream;
using std::istream;
#include<iomanip>
using std::setw;

class PhoneNumber
{
     friend ostream&operator<<(ostream&, const PhoneNumber&);
     friend istream&operator>>(istream&, PhoneNumber&);

     private:
           char areaCode[5];//4 prefix és '\0'
           char number[7];  //6 szam és '\0'
};

ostream&operator<<(ostream& out, const PhoneNumber& num)
{
     out << num.areaCode <<"/"<< num.number;
     return out; // fűzér mód
}

istream&operator>>(istream& in, PhoneNumber& num)
{
     in >> setw(5) >> num.areaCode;
     in.ignore(1); //figyelmen kívül hagyja a következő karaktert (/)
     in >> setw(7) >> num.number;
     return in;    // fűzér mód
}

int main()
{
     PhoneNumber phone;
    
     cout <<"Add meg a telefonszámot 0711/123456 formátumban: ";
    
     cin >> phone;
     // a cin meghívja az operator>> függvényt a (cin, phone) paraméterrel
    
     cout <<"A beírt telefonszám: "<< phone;
     // a cout meghívja az operator<< függvényt a (cout, phone) paraméterrel

     return 0;
}

A kimenet:
Add meg a telefonszámot 0711/123456 formátumban: 0366/456231
A beírt telefonszám: 0366/456231

Az operator>> függvény a stream-ből (adatfolyamból) olvas ki, melynek első paramétere egy istream referencia, a második pedig egy PhoneNumber referencia. Az első paraméter lehetőséget ad, hogy hozzáférjünk a cin-el beolvasott adatokhoz, a második pedig, hogy betegyük őket a privát tagváltozókba. Amikor azt írjuk, hogy cin >> phone , akkor valójában az operator>>(cin,phone) függvény hívódik meg. A függvény változatlanul vissza is téríti a kapott információt, így lehetőség van füzérszerűen használni az operátort, például: cin >> phone1 >> phone2;Az operator<< függvénynél is ugyanez a helyzet, a cout<<phone meghívásánál az operator<<(cout,phone) hívódik meg. Ez a két operátorfüggvény friend típusú kell legyen, mert a PhoneNumberobjektum mindig jobboldali operandusként szerepel. Hogy tagfüggvények legyenek az osztály objektuma baloldali operandusként kell szerepeljen.

Az unáris operátorok túlterhelése
      Az unáris (egy operandusú) operátor túlterhelő függvényét tagfüggvényként is meg lehet írni, egy paraméterrel vagy paraméter nélkül. Ha paramétere van, akkor az az osztály objektuma vagy az osztály objektumára hivatkozó referencia kell legyen. Legyen egy String nevű osztály, melyben átdefiniáljuk a ! operátort, hogy leellenőrizze, hogy az objektum üres-e vagy sem. Ha például s a String osztály objektuma, akkor a !s kifejezés az s.operator!() függvényt fogja meghívni. Az operátorfüggvényt az osztályon belül deklaráljuk:

class String
{
     public:
           booloperator!() const;
     ...
};

Ha osztályon kívül deklaráljuk, akkor argumentumként meg kell adni az osztály objektumát vagy annak referenciáját.

class String
{
     friend booloperator!(const String&);
     ...
};

Ebben az esetben a !s kifejezés az operator!(s) függvényt fogja meghívni. Ha választhatunk, akkor kerüljük a friend függvények használatát.

A bináris operátorok túlterhelése
      A bináris (két operandusú) operátor túlterhelő függvényét tagfüggvényként is meg lehet írni, egy vagy két paraméterrel, melyből az egyik az osztály objektuma vagy annak referenciája. Legyen x és y a String osztály objektuma és legyen a += operátor ezen két objektum összekapcsoló operátora. Ha tagfüggvénynek írjuk az operátorfüggvényt, akkor az y+=z kifejezés az y.operator+=(z) függvényt fogja meghívni.

class String
{
     public:
           const String&operator+=(const String&);
     ...
};

Ha friend-ként deklaráljuk, akkor bináris operátorként terheljük túl és az y+=z kifejezés az operator+=(y,z) függvényt fogja meghívni.

class String
{
     friendconst String&operator+=(String&, const String&);
     ...
};

Típusátalakítás
      A fordító sokféle típusátalakítást ismer, ám a programozó által definiált típusokat nem tudja átalakítani. Ezért a programozó dolga, hogy meghatározza miként lehet az ő típusait más típusokká alakítani. Ezt legegyszerűbb az osztály konstruktorában megvalósítani, ahol más osztályok objektumait konvertálhatjuk a jelenlegi osztály típusára. A String osztály konstruktora például átalakíthatja a char* értékeket String típusú objektummá. Erre a célra ideális a cast operátor, amelyet csakis tagfüggvényben alkalmazhatunk. A függvény prototípusa:

A::operator char*() const;

Ez egy cast túlterhelő függvényt (operátorfüggvényt) deklarál, melyekkel ideiglenes char* típusú objektumok kreálhatók az A típusú objektumból. Nem kell meghatározni a visszatérített típust, hisz az az objektum átkonvertált típusa lesz. Amikor a fordító találkozik a (char*) s kifejezéssel, meghívja az s.operator char*() tagfüggvényt.
      A cast és a konverziós operátorok egyik hasznos tulajdonsága, hogy valahányszor szükség van rájuk, a fordító meghívja őket hogy ideiglenes objektumokat hozzon létre. Például ha az s objektum a String osztályból olyan helyen jelenik meg, ahol valójában char* kéne legyen, mint például a cout<<s kifejezésben, akkor a fordító meghívja a túlterhelt cast operátort, hogy átalakítsa az objektumot char* típusba. Ha létezik ez az operátorfüggvény a String osztályban, akkor nincs is már szükség a << operátor túlterhelésére a String objektumok kiírásához.

A String osztály
      A String osztály a C++ standard könyvtárának része. Ettől függetlenül, a következő példában egy saját String osztály van megvalósítva, amely bemutatja az operátorok túlterhelését.

string1.h
#ifndef STRING1_H
#define STRING1_H
#include <iostream>
using std::ostream;
using std::istream;

class String
{
     friend ostream& operator<<(ostream&, const String&);
     friend istream& operator>>(istream&, String&);

     public:
          
           String(const char* = ""); //átalakító konstruktor: char* -> objektum
           String(const String&);    //másoló konstruktor
           ~String();
          
           const String& operator=(const String&);      //érték adás
           const String& operator+=(const String&);     //összekapcsolás
          
           bool operator!() const;                      //üres-e ?
           bool operator==(const String&) const;        //s1==s2 ?
           bool operator<(const String&) const;         //s1<s2 ?
           bool operator!=(const String& right) const   //s1!=s2 ?
           {
                return !( *this == right );
           }
           bool operator>(const String& right) const    // s1>s2 ?
           {
                return right < *this;
           }
           bool operator<=(const String& right) const   // s1<=s2 ?
           {
                return !( right < *this );
           }
           bool operator>=(const String& right) const   // s1>=s2 ?
           {
                return !( *this < right );
           }
               
           char& operator[](int);               //indexelés
           const char& operator[](int) const;   //indexelés
           String operator()(int, int);         //substringet térít vissza
           int getLength() const;               //a string hossza
          
     private:
          
           int length;                     //string hossz
           char* sPtr;                     //mutató a string elejére
           void setString(const char*);    //segítő függvény: beállítja az sPtr mutatót
};
#endif

string1.cpp
#include <iostream>
using std::cout;
using std::endl;
#include <iomanip>
using std::setw;
#include <cstring>
#include <cassert>
#include"string1.h"

//Átlakító konstruktor: char*->objektum
String::String(const char* s) : length(strlen(s))
{
     cout << "Átalakító konstruktor: " << s << '\n';
     setString(s); //beállítja az sPtr mutatót az s elejére
}

//Másoló konstruktor
String::String(const String& copy) : length(copy.length)
{
     cout << "Másoló konstruktor: " << copy.sPtr << '\n';
     setString(copy.sPtr);
}

//Destruktor
String::~String()
{
     cout << "Destruktor: " << sPtr << '\n';
     delete[] sPtr; //memória felszabadítás
}

//Az = operátor túlterhelése: stringek másolása
const String& String::operator=(const String& right)
{
     cout << "Az = operátor meghívódott\n";
     if(&right != this)   //ha nem saját magával tettük egyenlővé
     {
           delete[] sPtr;  //a régi string törlése (ha netán volt tartalma)
           length = right.length;//az új string hossza
           setString(right.sPtr);//az új string mutatója
     }
     else
           cout << "Nem tehető saját magával egyenlővé\n";
     return *this; //fűzér mód
}

//A += operátor túlterhelése: stringek összekapcsolása
const String& String::operator+=(const String& right)
{
     char* tempPtr = sPtr;      //elmenti a bal oldali stringet
     length += right.length;    //új hossz
     sPtr = new char[length+1]; //új memóriaterület lefoglalása
     assert(sPtr!=0);           //abort, ha nem tudta lefoglalni
     strcpy(sPtr, tempPtr);     //új string = régi string
     strcat(sPtr, right.sPtr);  // + az új string
     delete[] tempPtr;          //a régi string türlése
     return *this;              //fűzér mód
}

//Üres a string?
bool String::operator!() const
{
     return length==0;
}
    
//Egyenlő a jobb oldali stringgel?
bool String::operator==(const String& right) const
{
     return strcmp(sPtr, right.sPtr)==0;
}

//Stringek összehasonlítása
bool String::operator<(const String& right) const
{
     return strcmp(sPtr, right.sPtr)<0;
}
    
//Visszatéríti a string[i] karakter referenciáját mint lvalue
char& String::operator[](int i)
{
     assert(i>=0 && i<length); //abort, ha az index a korlátokon kívül van
     return sPtr[i];           // lvalue
}

//Visszatéríti a string[i] karakter referenciáját mint rvalue
const char& String::operator[](int i) const
{
     assert(i>=0 && i<length); //abort, ha az index a korlátokon kívül van
     return sPtr[i];           // rvalue
}

//Substring: indextől indulva subLength hosszúsággal
String String::operator()(int index, int subLength)
{
     assert(index>=0 && index<length && subLength>=0); // abort ha nem teljesül

     int len;                        // a substring hossza
     if(subLength==0)
           len = length-index;
     else
           len = subLength;
          
     char* tempPtr = new char[len+1]; //a substring memóriahelye+'\0'
     assert(tempPtr!=0);             //abort, ha nem sikerül lefoglalni
     strncpy(tempPtr, &sPtr[index], len);//bemásolja a substringet a memóriába
     tempPtr[len]='\0';              //string vége karakter
    
     String tempString(tempPtr);     //a substring is String típusú objektum
     delete[] tempPtr;               //felszabadítja a substring mutató cimét
     return tempString;
}

//tagfüggvény, amely megadja a length tagváltozó értékét
int String::getLength() const
{
     return length;
}

//segédfüggvény: beállítja az sPtr mutatót a string elejére
void String::setString(const char* string2)
{
     sPtr = new char[length+1]; //memória lefoglalás
     assert(sPtr!=0);           //abort, ha nem sikerül lefoglalni
     strcpy(sPtr, string2);
}

//Objektumot kiíró operátor
ostream& operator<<(ostream& output, const String& s)
{
     output << s.sPtr;
     return output;
}

//Objektumot beolvasó operátor
istream& operator>>(istream& input, String& s)
{
     char temp[100];  //100 karakterláncot fogad be (a '\0'-val együtt)
     input >> setw(100)>>temp; // csak az első 99 karaktert veszi figyelembe
     s = temp;       //az s objektum megkapja a beolvasott értéket
     return input;   //lehetővé teszi a fűzérszerű alkalmazást
}

- A program első két friend függvénye ugyanúgy van megvalósítva, mint az előző program PhoneNumber osztályában. Az egyik kiírja, a másik beolvassa az objektumot. Minden függvény, ami nem változtat az eredeti objektumon, az konstansnak van deklarálva.
- A konstruktort lehet típus átalakítónak tekinteni, mert egy paramétere van (ami éppen egy karakterlánc), és abból kreál objektumot. Ez azt jelenti, hogy minden tagváltozóját inicializálja abból az egy paraméterből. A length tagváltozó már a fejlécben megkapja a karakterlánc hosszát, a setString() segédfüggvény pedig beállítja az sPtr mutatót a karakterlánc elejére.
- Ez után egy újabb konstruktor következik. Az osztálynak lehet több konstruktora és mindig az hívódik meg amelyikre éppen szükség van. Az átalakítóra mindig szükség van valahányszor char* típussal dolgozunk, viszont szükség van egy másoló konstruktorra is, például amikor egy objektumot egy másik alapján hozunk létre. Ez gyakorlatilag ugyanolyan mint az átalakító konstruktor, csak karakterlánc helyett egy létező objektum referenciáját kapja meg.
- A destruktor dolga, hogy felszabadítsa a dinamikusan lefoglalt memóriát.
- Az értékadó (=) operátor az objektumok másolását végzi. Ugyanez a szerepe túlterhelés nélkül is, azonban itt az átdefiniált változat hívódik meg. Az operátor egy objektum paramétert kap a jobb oldalára, amibe bemásolja a bal oldali objektum összes értékét. Végül pedig visszatéríti az eredeti objektumot, hogy füzérszerű másolást is lehessen végezni.
- Az összekapcsoló (+=) operátorfüggvény a jobb oldalára kapott objektumot hozzáadja a bal oldali objektumhoz. Ez abból áll, hogy kiveszi a két objektumból a stringeket (az első karakterre mutató mutatókat), majd az strcat paranccsal összefűzi őket. Az objektum hosszait pedig összeadja, és az új értékekkel felülírja az eredeti (bal oldali) objektumot.
- A String objektum ürességét a !  operátor ellenőrzi. Ez 1-et térít vissza, ha a length tagváltozó nulla.
- A String objektumok összehasonlíthatók egymással:  ==, !=, <, >, <=, >=. Erre jó a strcmp parancs, mely binárisan hasonlítja össze két bemenetének karaktereit. Nullát térít vissza, ha minden karakter talál, negatívat, ha a bal oldal rövidebb mint a jobb, és pozitívat ellenkező esetben. Elegendő volt az == és a < operátoroknál használni ezt és a cstring header fájl miatt a string1.cpp-ben került sor a definíciókra. A többi összehasonlító operátor a string1.cpp fájlban van definiálva az == és a < túlterhelt operátorok segítségével.
- A string adott karakterére a [] operátorfüggvény hivatkozik. Ez leellenőrzi a keresett index hitelességét, majd visszatéríti az sPtr[] értékét. Ez lehet lvalue (amikor az index egy memóriacímmel rendelkező változó, például s[i]) és rvalue (amikor az indexnek nincs pontos memóriacíme, például s[25] vagy s[i+7]).
- A ()operátor egy substring-et vesz ki a stringből, például az s1(0,14) kiírja a nulladik pozíciótól vett első 14 karaktert. A függvény leellenőrzi a paramétereket, majd beállítja a substring hosszát. Ha a kért substring nulla hosszúságú, akkor kiírja az indextől a string végéig. A substringnek dinamikusan van lefoglalva a memória a tempPtr mutató révén, amibe bemásoljuk az eredeti stringből az index pozíciótól számítva a kért hosszúságú karakterláncot. Az erre megfelelő parancs a strncpy. Ezután String típusú objektumot készítünk a karakterláncból (amit a konstruktor végez el), majd visszatérítjük az objektumot.

test_string1.cpp
#include <iostream>
using std::cout;
using std::endl;
#include"string1.h"

int main()
{
     String s1("happy"), s2(" birthday"), s3;

     //relációs operátorok tesztelése
     cout << "s1 = \"" << s1 << "\"; s2 = \"" << s2 << "\"; s3 = \"" << s3 << '\"'
          << "\ns2 == s1: " << (s2==s1 ? "true" : "false")
          << "\ns2 != s1: " << (s2!=s1 ? "true" : "false")
          << "\ns2 > s1: "  << (s2>s1 ?  "true" : "false")
          << "\ns2 < s1: "  << (s2<s1 ?  "true" : "false")
          << "\ns2 >= s1: " << (s2>=s1 ? "true" : "false")
          << "\ns2 <= s1: " << (s2<=s1 ? "true" : "false");

     //a ! operátor tesztelése
     if(!s3)
     {
           cout << "\n\nAz s3 üres; Legyen s3 = s1;\n";
           s3 = s1; //az = operátor tesztelése
           cout << "s3 = \"" << s3 << "\"";
     }
    
     //az összekapcsoló operátor tesztelése
     s1 += s2;
     cout << "\n\ns1 += s2 => s1 = " << s1 << endl;

     //az átalkító konstruktor tesztelése
     s1 += " to you";
     cout << "s1 += \" to you\" => s1 = " << s1  << "\n\n";

     //a substring operátor tesztelése
     cout << "s1(0,11): " << s1(0,11) << "\n"; //0. indextől 11 karakter hosszú
     cout << "s1(11,0): " << s1(11,0) << "\n"; //11. indextől a string végéig
     cout << endl;

     //a másoló konstruktor tesztelése
     String* s4Ptr = new String(s1);
     cout << "*s4Ptr = " << *s4Ptr << "\n\n";

     //az = operátor tesztelése saját magával (kondício)
     cout << "*s4Ptr = *s4Ptr kifejezes \n";
     *s4Ptr = *s4Ptr;
     cout << endl;
    
     //a destruktor tesztelése
     delete s4Ptr;

     //az index operátor tesztelése
     s1[0] = 'H';
     s1[6] = 'B';
     cout << "\ns1 az s1[0] = 'H' és s1[6] = 'B' után: " << s1 << "\n\n";

     //helytelen index tesztelése
     cout << "s1[30]='d' kifejezés " << endl;
     s1[30] = 'd';     //Assertion failed!

     system("pause"); // hogy ne záródjon be a konzol ablaka
     return 0;
}

A kimenet:
Átalakító konstruktor: happy
Átalakító konstruktor:  birthday
Átalakító konstruktor:
s1 = "happy"; s2 = " birthday"; s3 = ""
s2 == s1: false
s2 != s1: true
s2 > s1: false
s2 < s1: true
s2 >= s1: false
s2 <= s1: true

Az s3 üres; Legyen s3 = s1;
Az = operátor meghívódott
s3 = "happy"

s1 += s2 => s1 = happy birthday
Átalakító konstruktor:  to you
Destruktor:  to you
s1 += " to you" => s1 = happy birthday to you

Átalakító konstruktor: happy birth
s1(0,11): happy birth
Destruktor: happy birth
Átalakító konstruktor: day to you
s1(11,0): day to you
Destruktor: day to you

Másoló konstruktor: happy birthday to you
*s4Ptr = happy birthday to you

*s4Ptr = *s4Ptr kifejezés
Az = operátor meghívódott
Nem tehető saját magával egyenlővé

Destruktor: happy birthday to you

s1 az s1[0] = 'H' es s1[6] = 'B' után: Happy Birthday to you

s1[30]='d' kifejezés
Assertion failed!

Nincsenek megjegyzések:

Megjegyzés küldése