Operatory
Klasy definiowane przez użytkownika muszą być co najmniej tak samo dobrymi typami jak typy wbudowane. Oznacza to, że:
muszą dać się efektywnie zaimplementować,
muszą dać się wygodnie używać.
To drugie wymaga, by twórca klasy mógł definiować operatory.
Definiowanie operatorów wymaga szczególnej ostrożności.
Ostrzeżenie
Operatory definiujemy przede wszystkim po to, by móc czytelnie i wygodnie zapisywać programy. Jednak bardzo łatwo można nadużyć tego narzędzia (np. definiując
operację + na macierzach jako odejmowanie macierzy). Dlatego projektując operatory (symbole z którymi są bardzo silnie związane pewne intuicyjne znaczenia),
trzeba zachować szczególną rozwagę.
Większość operatorów języka C++ można przeciążać, tzn. definiować ich znaczenie w sposób odpowiedni dla własnych klas.
Przeciążanie operatora polega na zdefiniowaniu metody (prawie zawsze może to też być funkcja) o nazwie składającej się ze słowa operator i nazwy operatora (np. operator=).
Poniższe operatory można przeciążać:
+, -, *, /, \item\lstC
<, >, >=, <=, ==, !=,@
=, +=, -=, *=, /=, \item\lstC
++, –,!
, , ->*, ->,
(), [],
new, delete.
Dla poniższych operatorów można przeciążać zarówno ich postać jedno- jak i dwuargumentową:
+, -, *, &
.
Tych operatorów nie można przeciążać:
., .*, ::, ?:, sizeof
(ani symboli preprocesora #
i ##
)
Operatory new
i delete
mają specyficzne znaczenie i nie odnoszą się do nich przedstawione tu reguły.
Tak zdefiniowane metody (funkcje) można wywoływać zarówno w notacji operatorowej:
a = b + c;
jak i funkcyjnej (tej postaci praktycznie się nie stosuje):
a.operator=(b.operator+(c));
Definiując operator nie można zmieniać jego priorytetu, łączności ani liczby argumentów. Można natomiast dowolnie (p. nast. punkt) ustalać ich typy, jak również typ wyniku.
Jeśli definiujemy operator jako funkcję, to musi ona mieć co najmniej jeden argument będący klasą bądź referencją do klasy. Powód: chcemy, żeby 1+3
zawsze znaczyło 4, a nie np. -2.
Operatory =, (), [] i ->
można deklarować jedynie jako (niestatyczne) metody.
Metody operatorów dziedziczą się (poza wyjątkiem kopiującego operatora przypisania, który jest bardziej złożonym przypadkiem).
Nie ma obowiązku zachowywania równoważności operatorów występujących w przypadku typów podstawowych (np. ++a
nie musi być tym samym co a+=1
).
Operator przeciążony nie może mieć argumentów domyślnych.
Operator jednoargumentowy (przedrostkowy) @
można zadeklarować jako:
(niestatyczną) metodę składową bez argumentów:typ operator@()
i wówczas @a jest interpretowane jako:a.operator@()
funkcję przyjmującą jeden argument:typ1 operator@(typ2)
i wówczas @a jest interpretowane jako:operator@(a)
.
Jeśli zadeklarowano obie postacie, to do określenia z której z nich skorzystać używa się standardowego mechanizmu dopasowywania argumentów.
Operatorów ++
oraz –
można używać zarówno w postaci przedrostkowej jak i przyrostkowej.
W celu rozróżnienia definicji przedrostkowego i przyrostkowego ++
(–
) wprowadza się dla operatorów przyrostkowych dodatkowy
parametr typu int (jego wartością w momencie wywołania będzie liczba 0).
class X{ public: X operator++(); // przedrostkowy ++a X operator++(int); // przyrostkowy a++ }; // Uwaga: ze względu na znaczenie tych operatorów // pierwszy z nich raczej definiuje się jako: // X& operator++(); int main(){ X a; ++a; // to samo co: a.operator++(); a++; // to samo co: a.operator++(0); }
Operator dwuargumentowy @
można zadeklarować jako:
(niestatyczną) metodę składową z jednym argumentem:typ1 operator@(typ2)
i wówczas a @ b
jest interpretowane jako:a.operator@(b)
funkcję przyjmującą dwa argumenty:typ1 operator@(typ2, typ3)
i wówczas a @ b
jest interpretowane jako:operator@(a, b)
.
Jeśli zadeklarowano obie postacie, to do określenia z której z nich skorzystać używa się standardowego mechanizmu dopasowywania argumentów.
Najlepiej jako metodę.
Nie zawsze można:
gdy operator dotyczy dwa klas,
gdy istotne jest równe traktowanie obu argumentów operatora.
Bardziej naturalne jest definiowanie operatorów jako metod, gdyż operator jest częścią definicji klasy, zatem także tekstowo powinien znajdować się w tej definicji. Są jednak sytuacje wymuszające odstępstwa od tej reguły:
Czasem operator pobiera argumenty będące obiektami dwu różnych klas, wówczas nie widać, w której z tych klas miałby być zdefiniowany (ze względów składniowych musiałby być zdefiniowany w klasie, z której pochodzi pierwszy argument). Co więcej czasami definiując taki operator mamy możliwość modyfikowania tylko jednej z tych klas, i może to akurat być klasa drugiego argumentu operatora (np. operator<<).
Czasami zamiast definiować wszystkie możliwe kombinacje typów argumentów operatora, definiujemy tylko jedną jego postać i odpowiednie konwersje.
Oto przykład:
class Zespolona{ // public: Zespolona(double); // Konstruktor ale i konwersja Zespolona operator+(const Zespolona&); };
Przy przedstawionych deklaracjach można napisać:
Zespolona z1, z2; z1 = z2 + 1; // Niejawne użycie konwersji
ale nie można napisać:
z1 = 1 + z2;
co jest bardzo nienaturalne.
Gdybyśmy zdefiniowali operator +
jako funkcję, nie było by tego problemu.
Kopiujący operator przypisania jest czymś innym niż konstruktor kopiujący!
O ile nie zostanie zdefiniowany przez użytkownika, to będzie zdefiniowany przez kompilator, jako przypisanie składowa po składowej (więc nie musi to być przypisywanie bajt po bajcie). Język C++ nie definiuje kolejności tych przypisań.
Zwykle typ wyniku definiuje się jako X&
, gdzie X
jest nazwą klasy, dla której definiujemy operator=
.
Uwaga na przypisania x = x
, dla nich operator=
też musi działać poprawnie!
Jeśli uważamy, że dla definiowanej klasy operator=
nie ma sensu, to nie wystarczy go nie definiować
(bo zostanie wygenerowany automatycznie). Musimy zabronić jego stosowania. Można to zrobić na dwa sposoby:
zdefiniować jego treść jako wypisanie komunikatu i przerwanie działają programu (kiepskie, bo zadziała dopiero w czasie wykonywania programu),
zdefiniować go (jako pusty) w części private (to jest dobre rozwiązanie, bo teraz już w czasie kompilacji otrzymamy komunikaty o próbie użycia tego operatora poza tą klasą).
Automatycznie definiowany kopiujący operator przypisania w podklasie wywołuje operator przypisania z nadklasy, jeśli samemu definiujemy ten operator, to musimy sami o to zadbać.
Można zdefiniować także inne (niż kopiujący) operatory przypisania.
Wywołanie:
wyrażenie_proste( lista_wyrażeń )uważa się za operator dwuargumentowy z wyrażeniem prostym jako pierwszym argumentem i, być może pustą, listą wyrażeń jako drugim. Zatem wywołanie:
x(arg1, arg2, arg3)interpretuje się jako:
x.operator()(arg1, arg2, arg3)
Wyrażenie:
wyrażenie_proste [ wyrażenie ]interpretuje się jako operator dwuargumentowy. Zatem wyrażenie:
x[y]interpretuje się jako:
x.operator[](y)
Wyrażenie:
wyrażenie_proste -> wyrażenie_prosteuważa się za operator jednoargumentowy. Wyrażenie:
x -> minterpretuje się jako:
(x.operator->())->mZatem
operator->()
musi dawać wskaźnik do klasy, obiekt klasy albo referencję do klasy.
W dwu ostatnich przypadkach, ta klasa musi mieć zdefiniowany operator ->
(w końcu musimy uzyskać coś co będzie wskaźnikiem).
W C++ możemy specyfikować konwersje typów na dwa sposoby:
do definiowanej klasy z innego typu (konstruktory),
z definiowanej klasy do innego typu (operatory konwersji).
Oba te rodzaje konwersji nazywa się konwersjami zdefiniowanymi przez użytkownika.
Są one używane niejawnie wraz z konwersjami standardowymi.
Konwersje zdefiniowane przez użytkownika stosuje się jedynie wtedy, gdy są jednoznaczne.
Przy liczeniu jednej wartości kompilator może użyć niejawnie co najwyżej jednej konwersji zdefiniowanej przez użytkownika.
Na przykład:
class X { /* ... */ X(int); }; class Y { /* ... */ Y(X); }; Y a = 1; // Niepoprawne, bo Y(X(1)) zawiera już dwie konwersje użytkownika
Uwaga: ponieważ kompilator stosuje konwersje niejawnie trzeba być bardzo ostrożnym przy ich definiowaniu. Definiujmy je dopiero wtedy, gdy uznamy to za absolutnie konieczne i naturalne
Są metodami o nazwie: operator nazwa_typu
nie deklarujemy typu wyniku, bo musi być dokładnie taki sam jak w nazwie operatora,
taka metoda musi instrukcją return przekazywać obiekt odpowiedniego typu.
Jeśli zostaną zdefiniowane, będą używane przez kompilator w momencie wywoływania operacji new
i delete
do (odpowiednio)
przydzielania i zwalniania pamięci. Opis zastosowania tych metod nie mieści się w ramach tego wykładu.
Operatory <<
i >>
służą (m.in.) do wczytywania i wypisywania obiektów definiowanej klasy. Zostaną omówione wraz ze strumieniami.
Treść automatycznie generowana z plików źródłowych LaTeXa za pomocą oprogramowania wykorzystującego LaTeXML.
strona główna | webmaster | o portalu | pomoc
© Wydział Matematyki, Informatyki i Mechaniki UW, 2009-2010. Niniejsze materiały są udostępnione bezpłatnie na licencji Creative Commons Uznanie autorstwa-Użycie niekomercyjne-Bez utworów zależnych 3.0 Polska.
Projekt współfinansowany przez Unię Europejską w ramach Europejskiego Funduszu Społecznego.
Projekt współfinansowany przez Ministerstwo Nauki i Szkolnictwa Wyższego i przez Uniwersytet Warszawski.