C++/Przeciążanie operatorów
Z Wikibooks, biblioteki wolnych podręczników.
Przeładowanie (przeciążanie) operatorów polega na nadaniu im nowych funkcji.
Spis treści |
[edytuj] Użycie
Nie możemy przeładowywać operatorów dla typów wbudowanych (int, char, float).
Przeciążanie polega na zdefiniowaniu tzw. funkcji operatorowej. Identyfikatorem funkcji operatorowej jest zawsze słowo kluczowe operator, bezpośrednio po którym następuje symbol operatora
typ_zwracany operator@ (argumenty)
{
// operacje
}
np.: operator+, operator-, operator<< itd. Co najmniej jeden argument tej funkcji musi być obiektem danej klasy.
[edytuj] Operatory które można przeciążać
+ - * / % & | ^ << << ~ && || ! == != < <= > >= += -= *= /= %= &= |= ^= <<= >>= ++ -- = -> ->* () [] new delete ,
Trzeba zapamiętać że można przeciążać TYLKO te operatory. Próba przeciążania innych (tudzież tworzenia nowych jak np @) skończy się błędem kompilacji.
[edytuj] Przykład zastosowania
Mamy daną klasę Student
class Student
{
int nr_indeksu;
float srednia_ocen;
public:
Student(int nr=0, float sr=0) {nr_indeksu=nr; srednia_ocen=sr;} // konstruktor
};
i chcemy przeładować operator wyjścia <<
Robimy to w następujący sposób:
class Student
{
int nr_indeksu;
float srednia_ocen;
public:
Student(int nr=0, float sr=0) {nr_indeksu=nr; srednia_ocen=sr;} // konstruktor
friend ostream & operator<< (ostream &wyjscie, const Student &s);
};
ostream & operator<< (ostream &wyjscie, const Student &s)
{
wyjscie << "Nr indeksu : " <<s.nr_indeksu << endl << "Srednia ocen : " <<s.srednia_ocen<<endl;
return wyjscie;
}
Aby zobaczyć, jak to działa, wystarczy że funkcja main() będzie miała następującą postać:
int main()
{
Student st, stu(10,5);
cout << st; // wypisze nr indexu = 0, srednia ocen=0,
// poniewaz st to wartosci konstruktora domyślnego :)
cout << stu; // wypisze nr indexu = 10, srednia ocen=5
return 0;
}
W powyższym przykładzie wprowadzone zostało także nowe pojęcie - zaprzyjaźnianie (friend). Należy uzyć go wtedy, kiedy chcemy używać typów wprowadzonych przez inną bibliotekę. (tu przez iostream).
Ale weźmy przykład nieco prostszy. Chcemy sprawdzić czy to jest ten sam student - przeciążamy operator:
class Student
{
//...
public:
int operator==(Student & s, Student &q) {return s.nr_indeksu==q.nr_indeksu}; //to jest przeciez zle, w ciele klasy przeladowanie jest jedno argumentowe!
int operator==(Student & s, int &q) {return s.nr_indeksu==q}; //jw
};
I niby wszystko jest pięknie, ale tu zacznają się schody... My, jako twórcy klasy wiemy, że porównanie dotyczy się tylko i wyłącznie numeru indeksu. Przy różnych średnich i tych samych indeksach dostaniemy wynik pozytywny. A nuż niech ktoś sobie ubzdura, że == odnosi się do wszystkich składowych...
Dalsze zamieszanie wprowadzą kolejne zaproponowane przeze mnie operatory.
class Student
{
//...
public:
int operator< (Student & s, Student &q) {return s.srednia < q.srednia};
int operator< (Student & s, int &q) {return s.srednia < q};
// itd dla kolejnych operatorów.
};
Samo w sobie nie jest groźne. Dopiero przy konfrontacji z poprzednim operatorem zaczyna wprowadzać zamieszanie. Wszystko jest dobrze kiedy pamiętamy, jakie operacje dane operatory wykonują. Ale pamiętajmy: pamięć ludzka jest ulotna i ktoś inny (albo my) może spędzić kilka dni zanim dojdzie do tego, dlaczego to działa nie tak jak powinno.
Ale powyższy przykład wygląda naprawdę blado w porównaniu z tym:
class Student
{
//...
public:
int operator+ (Student & s, Student &q) {return (s.srednia + q.srednia +11) };
int operator+ (Student & s, int &q) {return (s.srednia - q / 30) };
};
Jak widzicie operator + wcale nas nie zmusza do wykonywania operacji dodawania. Możemy równie dobrze wewnątrz odejmować. A przy odejmowaniu porównywać. Lecz takie postępowanie nie jest intuicyjne. Takie postępowanie jest dozwolone jedynie w przypadkach, kiedy startujecie w konkursie na najbardziej nieczytelny kod.
Ale dość już straszenia. Teraz należy pokazać jak prawidłowo przeciążać operatory jednoargumentowe (++, --) oraz jak prawidłowo zwracać obiekt np. przy dodawaniu.
Oprócz operatorów arytmetycznych oraz działających na strumieniach można przeciążać również operatory logiczne.
[edytuj] Operatory "bool" i "!"
W języku C++ jest również możliwość przeciążania operatorów bool i !. Dzięki temu możemy w instrukcji warunkowej używać nazwy obiektu do testowania, czy spełnia on jakieś określone kryteria. Poniżej znajduje się prosty przykład, który to ilustruje:
#include <iostream> using namespace std; class TablicaInt { public: TablicaInt(int el) : Tab(new int[el]), L_elementow(el) {} operator bool() const {return (L_elementow != 0);} bool operator!() const {return (L_elementow == 0);} private: int * Tab; int L_elementow; }; int main() { int n = 5; TablicaInt tab(n); if(tab) cout << "Tablica nie jest pusta." << endl; if(!tab) cout << "Tablica jest pusta." << endl; TablicaInt tab2(0); if(tab2) cout << "Tablica nie jest pusta." << endl; if(!tab2) cout << "Tablica jest pusta." << endl; return 0; }
W efekcie na ekranie otrzymamy dwa wpisy:
Tablica nie jest pusta. Tablica jest pusta.
W pierwszym przypadku tablica zawierała niezerową liczbę elementów i prawdę zwrócił operator bool. W drugim natomiast liczba elementów wynosiła zero i prawdę zwrócił operator !.
[edytuj] Operator "[]"
W niektórych przypadkach bardzo przydatny jest operator indeksu []. Można go przeciążyć oczywiście w dowolny sposób, ale chyba najbardziej intuicyjne jest przypisanie mu funkcji dostępu do konkretnego elementu np. w tablicy.
Posługując się przykładem klasy TablicaInt możemy dopisać do niej następujące dwa operatory:
int & operator[](int el) {return Tab[el];} const int & operator[](int el) const {return Tab[el];}
oraz testową funkcję main
int main() { int n = 5; TablicaInt tab(n); for(int i = 0; i < n; ++i) { tab[i] = i; cout << tab[i] << endl; } return 0; }
W ogólności te operatory nie muszą wykonywać dokładnie tych samych czynności. Można sobie wyobrazić przykład w którym operator do zapisu zapisuje coś do tablicy, a w przypadku braku miejsca alokuje dodatkową pamięć. Operator stały nie będzie posiadał takiej funkcjonalności ponieważ nie może zmieniać obiektu na rzecz którego został wywołany.
[edytuj] Operator "()"
Ten operator służy do tworzenia tzw. funktorów czyli klas które naśladują funkcje:
class Foo { public: int operator() (int a, int b) { return (a+b); } };
Nie jest to może najmądrzejszy przykład gdyż jest dostępny do przeciążania operator "+" ale oddaje zasadę działania. Trzeba zaznaczyć że ten operator może zwracać dowolną wartość oraz przyjmować dowolną liczbę parametrów dowolnego typu. Niestety musi być zadeklarownay jako niestatyczna metoda klasy (gdyż inne operatory które mogą być statyczne zwracają obiekt (&Foo operator...), choć możliwość udawania przez klasę funkcji z pewnością to wynagrodzi.