O co chodzi z tymi operatorami?
W języku C++ dostępne są różne typy zmiennych przechowujących wartości liczbowe (np. int, double). Jednak byłoby trochę bezsensowne, gdyby nie można było z tymi danymi nic zrobić. Właśnie, dlatego powstały operatory umożliwiające operacje na zmiennych np.
int x, y;
x = 2;
y = 3;
int z = x + y;
Jednak, gdy tworzymy własną klasę posiadającą wiele wartości, to w chwili, gdy będziemy próbowali je dodać w sposób, jak powyżej kompilator zgłupieje i nie będzie wiedział, co zrobić. Można oczywiście zrobić metodę:
void MojaKlasa::dodaj( MojaKlasa x );
Rozwiąże ona nasz problem, jednak użycie jej jest mocno niewygodne. Ponadto, gdy trzeba będzie użyć kilku działań to kod zacznie wyglądać strasznie nieczytelnie, a szukanie jakichkolwiek błędów będzie koszmarem. Na szczęście w C++ istnieje możliwość przeciążenia operatorów takich jak ‘+’, ‘-’, ‘+=’, ‘==’ i innych, tak żeby działał bezpośrednio z naszą klasą.
Operatory w klasie
Na potrzeby tego artykułu utwórzmy klasę ‘Vector’, która jak sama nazwa wskazuje będzie reprezentować wektor na płaszczyźnie kartezjańskiej. Załączymy do niej od razu konstruktory.
class Vector
{
public:
double x;
double y;
Vector()
{
this->x = 0;
this->y = 0;
}
Vector( double x, double y )
{
this->x = x;
this->y = y;
}
}
Ogólna zasada pisania operatorów w klasie wygląda następująco:
zwracany_typ operator@(typ_prawego_operandu &nazwa_prawego_operandu);
gdzie ‘@’ to operator, który chcemy przeciążyć. Pierwszym argumentem przy przeciążaniu w klasie zawsze jest zmienna ‘this’, która jest przedstawicielem tej klasy. Taka mała oszczędność czasu. Drugi (opcjonalny) parametr jest przekazywany jako wskaźnik w argumencie funkcji.
Pierwszym operatorem, jaki przeciążymy będzie negacja(‘!’), która zwróci wektor przeciwny. Dopiszmy do ciała naszej klasy:
Vector operator !()
{
return Vector( - this->x, - this->y );
}
Zgodnie ze schematem – pierwszym argumentem jest ‘Vector this’, drugiego argumentu nie ma, więc funkcja nie ma parametrów, a zwracany jest nowa zmienna ‘Vector’.
Kolejną rzeczą, jaką zrobimy jest porównywanie (‘==’). Osoby, które programują w C/C++ wiedzą, że zwraca on wartość logiczną ‘true’, jeśli operandy są takie same lub ‘false’ w innym przypadku. Aby zadziałało to w naszej przykładowej klasie musimy dodać no niej następującą metodę:
bool operator ==( const Vector & v )
{
if(( this->x == v.x ) &&( this->y == v.y ) )
return true;
else
return false;
}
Porównywanie, jak pewnie większość wie jest dwuargumentowe. Zatem zgodnie z powyższym schematem – wartością zwracaną jest ‘bool’, pierwszym operandem ‘Vector this’, a drugim przekazany przy wywołaniu argument ‘Vector &v’.
Przejdźmy teraz do następnego operatora, czyli do ‘+’. Dopiszmy do klasy poniższy kod:
Vector operator +( const Vector & v )
{
return Vector( this->x + v.x, this->y + v.y );
}
Jak w poprzednim przypadku pierwszym argumentem jest domyślnie nasza klasa, drugim podany w funkcji argument. Zwracaną wartością jest nowa zmienna, w której jak można się domyślić x i y, są sumą swoich odpowiedników z operandów.
Skoro mamy zwykłe dodawanie to, może czas, żeby zrobić obsługę operatora ‘+=’. Po raz kolejny do klasy załączamy kod:
Vector & operator +=( const Vector & v )
{
this->x += v.x;
this->y += v.y;
return * this;
}
Od razu widać, że to nie jest takie proste jak poprzednia wersja. Problem wynika z tego, że w tym przypadku nie jest zwracana nowa znienna, tylko modyfikowany jest pierwszy argument, a zwracany jest wskaźnik na niego. Takie rozwiązanie należy stosować zawsze, gdy pierwszy operand jest modyfikowany.
W analogiczny sposób możemy zrobić przeciążenie operatora, gdy drugi operand jest czymś innym niż nasza klasa:
Vector operator *( const double & d )
{
Vector ret;
ret.x = this->x * d;
ret.y = this->y * d;
return ret;
}
Vector & operator *=( const double & d )
{
this->x *= d;
this->y *= d;
return * this;
}
Operatory poza klasą
Ostatni przykład pozwala nam wykonać następującą instrukcję:
v = v * 0.5;
Gdzie zmienna ‘v’ jest przedstawicielem naszej klasy. Jednak definicje operacji nie są przemienne i gdy będziemy chcieli skompilować
v = 0.5 * v;
otrzymamy błąd. Niestety klasa double nie istnieje, a nawet gdyby istniała to i tak nie moglibyśmy jej edytować. Dlatego istnieje możliwość przeciążenia operatorów również poza klasą. Schemat, którego należy się trzymać wygląda następująco:
zwracany_typ operator@(typ_lewego_operandu &nazwa_lewego_operandu, typ_lewego_operandu &nazwa_lewego_operandu)
Wygląda to bardzo podobnie do przeciążania w klasie, jednak z pewną różnicą. Jest nią konieczność definiowania funkcji poza klasą, a co za tym idzie – trzeba przekazać do funkcji wskaźniki na oba operandy. Definicja funkcji która przy mnożeniu jako lewostronny operand przyjmuje ‘double’, a dopiero jako prawostronny ‘Vector’ wygląda w następujący sposób:
Vector operator *( const double & d, const Vector & v )
{
return Vector( v.x * d, v.y * d );
}
Teraz, żeby nie odnieść mylnego wrażenia, że operatory odpowiadają tylko za działania arytmetyczne, zróbmy coś innego. Programiści javy mają do swojej dyspozycji funkcję standardową toString(), która jest wykorzystywana do automatycznego konwertowania naszej zmiennej na napis. W C++ taka funkcja nie istnieje. Jednak istnieje możliwość przeciążenia operatora ‘<<’ z lewostronnym operandem typu ‘str::ostream’ i zwracaną wartością w takim samym formacie. Da to w rezultacie taki sam efekt. Przykład takiego zastosowania wygląda następująco:
std::ostream & operator <<( std::ostream & s, const Vector & v )
{
return s << '<' << v.x << ',' << v.y << '>';
}
Poniżej zamieszczona jest tabelka z możliwymi do przeciążenia operatorami i schematami jak to zrobić.
operator |
w klasie |
poza klasą |
+ |
_zwracany_typ_ operator +( const _typ_ & ); |
_zwracany_typ_ operator +( const _typ1_ &, const _typ2_ & ); |
- |
_zwracany_typ_ operator -( const _typ_ & ); |
_zwracany_typ_ operator -( const _typ1_ &, const _typ2_ & ); |
* |
_zwracany_typ_ operator *( const _typ_ & ); |
_zwracany_typ_ operator *( const _typ1_ &, const _typ2_ & ); |
/ |
_zwracany_typ_ operator /( const _typ_ & ); |
_zwracany_typ_ operator /( const _typ1_ &, const _typ2_ & ); |
% |
_zwracany_typ_ operator %( const _typ_ & ); |
_zwracany_typ_ operator %( const _typ1_ &, const _typ2_ & ); |
^ |
_zwracany_typ_ operator ^( const _typ_ & ); |
_zwracany_typ_ operator ^( const _typ1_ &, const _typ2_ & ); |
& |
_zwracany_typ_ operator &( const _typ_ & ); |
_zwracany_typ_ operator &( const _typ1_ &, const _typ2_ & ); |
| |
_zwracany_typ_ operator |( const _typ_ & ); |
_zwracany_typ_ operator |( const _typ1_ &, const _typ2_ & ); |
~ |
_zwracany_typ_ operator ~(); |
_zwracany_typ_ operator ~( const _typ_ & ); |
! |
_zwracany_typ_ operator !(); |
_zwracany_typ_ operator !( const _typ_ & ); |
= |
_zwracany_typ_ & operator =( const _typ_ & ); |
_zwracany_typ_ & operator =( _zwracany_typ_ &, const _typ_ & ); |
< |
_zwracany_typ_ operator <( const _typ_ & ); |
_zwracany_typ_ operator <( const _typ1_ &, const _typ2_ & ); |
> |
_zwracany_typ_ operator >( const _typ_ & ); |
_zwracany_typ_ operator >( const _typ1_ &, const _typ2_ & ); |
+= |
_zwracany_typ_ & operator +=( const _typ_ & ); |
_zwracany_typ_ & operator +=( _zwracany_typ_ &, const _typ_ & ); |
-= |
_zwracany_typ_ & operator -=( const _typ_ & ); |
_zwracany_typ_ & operator -=( _zwracany_typ_ &, const _typ_ & ); |
*= |
_zwracany_typ_ & operator *=( const _typ_ & ); |
_zwracany_typ_ & operator *=( _zwracany_typ_ &, const _typ_ & ); |
/= |
_zwracany_typ_ & operator /=( const _typ_ & ); |
_zwracany_typ_ & operator /=( _zwracany_typ_ &, const _typ_ & ); |
%= |
_zwracany_typ_ & operator %=( const _typ_ & ); |
_zwracany_typ_ & operator %=( _zwracany_typ_ &, const _typ_ & ); |
^= |
_zwracany_typ_ & operator ^=( const _typ_ & ); |
_zwracany_typ_ & operator ^=( _zwracany_typ_ &, const _typ_ & ); |
&= |
_zwracany_typ_ & operator &=( const _typ_ & ); |
_zwracany_typ_ & operator &=( _zwracany_typ_ &, const _typ_ & ); |
*= |
_zwracany_typ_ & operator |=( const _typ_ & ); |
_zwracany_typ_ & operator |=( _zwracany_typ_ &, const _typ_ & ); |
<< |
_zwracany_typ_ & operator <<( const _typ_ & ); |
_zwracany_typ_ & operator <<( const _zwracany_typ_ &, const _typ_ & ); |
>> |
_zwracany_typ_ & operator >>( const _typ_ & ); |
_zwracany_typ_ & operator >>( const _zwracany_typ_ &, const _typ_ & ); |
<<= |
_zwracany_typ_ & operator <<=( const _typ_ & ); |
_zwracany_typ_ & operator <<=( _zwracany_typ_ &, const _typ_ & ); |
>>= |
_zwracany_typ_ & operator >>=( const _typ_ & ); |
_zwracany_typ_ & operator >>=( _zwracany_typ_ &, const _typ_ & ); |
== |
_zwracany_typ_ operator ==( const _typ_ & ); |
_zwracany_typ_ operator ==( const _typ1_ &, const _typ2_ & ); |
!= |
_zwracany_typ_ operator !=( const _typ_ & ); |
_zwracany_typ_ operator !=( const _typ1_ &, const _typ2_ & ); |
<= |
_zwracany_typ_ operator <=( const _typ_ & ); |
_zwracany_typ_ operator <=( const _typ1_ &, const _typ2_ & ); |
>= |
_zwracany_typ_ operator >=( const _typ_ & ); |
_zwracany_typ_ operator >=( const _typ1_ &, const _typ2_ & ); |
&& |
_zwracany_typ_ operator &&( const _typ_ & ); |
_zwracany_typ_ operator &&( const _typ1_ &, const _typ2_ & ); |
|| |
_zwracany_typ_ operator ||( const _typ_ & ); |
_zwracany_typ_ operator ||( const _typ1_ &, const _typ2_ & ); |
++ |
_zwracany_typ_ & operator ++(); |
_zwracany_typ_ & operator !=( _typ_ & ); |
-- |
_zwracany_typ_ & operator --(); |
_zwracany_typ_ & operator --( _typ_ & ); |
->* |
_zwracany_typ_ operator->*( const _typ_ & ); |
_zwracany_typ_ operator->*( const _typ1_ &, const _typ2_ & ); |
-> |
_zwracany_typ_ operator->( const _typ_ & ); |
_zwracany_typ_ operator->( const _typ1_ &, const _typ2_ & ); |
Uwagi końcowe
Operatory są rozróżniane po operatorze i typach operandów. Oznacza to, że niemożliwe jest napisanie 2 operatorów różniących się tylko zwracanym typem.
Operatory takie jak
==,
!=,
<,
>,
<=,
>= zazwyczaj zwracają wartość logiczną, czyli typ bool, jednak możliwe jest przeciążenie w taki sposób, żeby działały w inny sposób.
Operatory
+,
-,
itp. zazwyczaj nie modyfikują operandów, jednak nic nie stoi na przeszkodzie, żeby napisać przeciążenie w taki sposób, żeby działały jak
+=,
-=,
itp. Konieczne jest wtedy usunięcie słowa kluczowego
const z definicji funkcji. Operatory +=, -=, *=, itp. powinny mieć zwracany typ identyczny, jak lewy operand.