Podstawy Programowania Obiektowego. Spotkanie 07 Dr inż. Dariusz JĘDRZEJCZYK
Tematyka wykładu Pojęcie funkcji operatorowej Definicja funkcji operatorowej w klasie Definicja funkcji operatorowej poza klasą Argumentowość operatorów Operator przypisania = Operator wypisywania << Przykłady do samodzielnego wykonania 2
Definicja i trochę teorii Przeciążanie operatorów (ang. operator overloading, tłumaczone też jako przeładowanie operatorów) to rodzaj polimorfizmu występującego w niektórych językach programowania, polegający na tym, że operator może mieć różne implementacje w zależności od typów użytych argumentów (operandów). Mówiąc najkrócej w tej części zajmiemy się tym, jak sprawić by znaczki takie jak: +, - itd. pracowały dla nas i robiły to, co my im każemy. Sam znaczek + nie dodaje dwóch liczb. Kiedy on występuje w tekście programu, kompilator wywołuje specjalną funkcję, która zajmuje się dodawaniem liczb. Nazwijmy tę funkcję operatorem dodawania. Możesz sam napisać swoją funkcję operator dodawania, która będzie wywoływana wtedy, gdy koło znaczka + pojawią się argumenty wybranego przez Ciebie typu. Po prostu po raz kolejny zostanie przeładowany operator. 3
Definicja i trochę teorii Projektując klasę możemy zadbać o to, by pewne operacje na obiektach tej klasy wykonywane były na zasadzie przeładowania operatorów. Przeładowanie operatora dokonuje się definiując swoją własną funkcję która: Nazywa się operator@ - gdzie @ oznacza symbol operatora, o którego przeładowanie nam chodzi (np.: +, -, *, &, itd.) Jako co najmniej jeden z argumentów, przyjmuje obiekt danej klasy. (Musi być obiekt, nie może być wskaźnik do obiektu.) 4
Definicja i trochę teorii Definicja takiego operator wygląda tak: typ_zwracany operator@(argumenty) { // ciało funkcji } Do przeładowania mamy do dyspozycji bardzo wiele operatorów - wybieramy z nich te, które rzeczywiście będą nam potrzebne. Nie ma sensu przeładowywać wszystkich. 5
Definicja i trochę teorii Oto lista operatorów, które mogą być przeładowane: + - ~ * / % ^ &! = %= < > += -= *= /= ^= &=!= = << >> >>= <<= == <= > >= && ++ --, ->* - new delete () [] Natomiast nie mogą być przeładowane operatory:..* ::?: 6
Definicja i trochę teorii Kilka uwag dotyczących przeładowania operatorów: Przeładowywać można te operatory i tylko te podane na wcześniejszym slajdzie. Przeładowanie może nadać operatorowi dowolne znacznie, nie ma też ograniczeń co do wartości zwracanej przez operator (wyjątkiem są operatory new i delete) Nie można jednak zmieniać priorytetu wykonywania tych operatorów. Nie można też zmieniać argumentowości operatorów, czyli tego, czy operator jest jedno-, czy dwuargumentowy. Nie można zmienić łączności operatorów czyli tego, czy operator łączy się z argumentem z lewej, czy z prawej strony. 7
Definicja i trochę teorii Jeśli funkcja operatorowa jest zdefiniowana jako zwykła (globalna) funkcja, to przyjmuje tyle argumentów na ilu pracuje operator. Przynajmniej jeden z tych argumentów musi być typu zdefiniowanego przez użytkownika. Nie ma znaczenia który. Argumenty nie mogą być domniemane. jest niczym więcej jak tylko efektownym sposobem ułatwiającym notację wyrażeń, w których występują obiekty danych klas. Przeładowanie operatorów nie daje niczego takiego, co nie było możliwe do tej pory. To tylko notacja się upraszcza. 8
Funkcja operatorowa jako funkcja składowa Funkcja operatorowa może być zwykłą, globalną funkcją, a może być także funkcją składową klasy. Dana funkcja operatorowa może być albo funkcją globalną albo niestatyczną funkcją składową klasy, dla której pracuje. Jeśli operator definiujemy jako funkcję składową, to ma ona zawsze o jeden argument mniej niż ta sama funkcja napisana w postaci funkcji globalnej. Ten brakujący argument w wypadku funkcji składowej spowodowany jest oczywiście istnieniem wskaźnika this. 9
Funkcja operatorowa przyjacielem klasy? Jeśli funkcja operatorowa ma pracować tylko na publicznych składnikach klasy, to może być ona zwykłą funkcją składową, bez żadnych specjalnych uprawnień. Jeśli chcemy, by operator mógł pracować także na niepublicznych składnikach klasy wówczas klasa musi zadeklarować tę funkcję operatorową jako zaprzyjaźnioną. Przyjaźń nie jest obowiązkowa. To przecież oczywiste jeśli operator ma np.: zagwizdać na cześć obiektu danej klasy, to do tego nie jest mu potrzebny dostęp do jej prywatnych składników. 10
Operatory predefiniowane Jest kilka operatorów, których znaczenie jest tak oczywiste, że zostają one automatycznie generowane dla każdej klasy. Te operatory to: = - przypisanie [podstawienie] do obiektu danej klasy, & - (jednoargumentowy) pobranie adresu obiektu danej klasy,, (przecinek) - znaczenie takie jak dla typów wbudowanych, new, delete - kreacja i likwidacja obiektów w zapasie pamięci. 11
Argumentowość operatorów Operatory działają albo na jednym argumencie (jak np.: negacja), albo na dwóch argumentach (jak np.: dzielenie). Poza jednym wyjątkiem nie ma operatorów, które pracują na więcej niż dwóch argumentach. Jednym jedynym wyjątkiem, kiedy do operatora można przesłać większą liczbę argumentów jest operator( ). 12
Operatory jednoargumentowe Operatory te występują zwykle jako przedrostek, czyli stoją przed obiektem danej klasy. Oto przykłady: int i; - i! i ++i ~i &i Mogą być też jednoargumentowe operatory przyrostkowe. Stoją one za obiektem. i -- i ++ 13
Operatory jednoargumentowe Jeśli mamy daną klasę, to operatory jednoargumentowe typu przedrostkowego pracują na obiektach klasy K można zdefiniować jako: nieskładową (zwykłą) funkcję wywołaną z jednym argumentem (obiektem tej klasy K): typ_zwracany operator@(k){ } // ciało funkcji funkcję składową klasy K wywoływaną beż żadnych argumentów: 14
Operatory dwuargumentowe Operatory dwuargumentowe możemy także przeładowywać na dwa sposoby: jako funkcję składową niestatyczną wywoływaną z jednym argumentem: x.operator@(y) albo jako funkcję globalną (czyli zwykłą, nieskładową), wywoływaną z dwoma argumentami: operator@(x,y) Taka funkcja operatorowa zostaje automatycznie wywołana, gdy obok znaczka danego operatora znajdą się dwa argumenty określonego przez nas typu: x @ y 15
Przeładowanie operatora dwuargumentowego Załóżmy, że chcemy przeładować operator mnożenia *. Dla odmiany weźmy klasę reprezentującą trójwymiarowy wektor. Trzeba się zastanowić co chcemy, by ten operator* z obiektem klasy wektorek dla nas robił. Niech, służy do mnożenia wektora przez liczbę rzeczywistą. Jeśli jakiś wektor mnożymy przez 2, to wszystkie jego współrzędne mają zostać podwojone.
Przeładowanie operatora dwuargumentowego #include <iostream.h> //*************************************************** *** class wektorek { public : float x, y, z ; // --- konstruktor wektorek(float xp = 0, float yp = 0, float zp = 0 ) : x(xp), y(yp), z(zp) { /* cialo puste */ } ; } ; //... inne funkcje skladowe
Przeładowanie operatora dwuargumentowego wektorek operator*(wektorek kopia, float liczba ){ wektorek rezultat ; } rezultat.x = kopia.x * liczba ; rezultat.y = kopia.y * liczba ; rezultat.z = kopia.z * liczba ; return rezultat ; //*************************************************** *** void pokaz(wektorek www) ; // deklaracja //*************************************************** *** 18
Przeładowanie operatora dwuargumentowego void main(){ wektorek a(1,1,1), b(-15, -100, +1), c ; } c = a * 6.66 ; pokaz(c) ; c = b * -1.0 ; pokaz(c) ; //*************************************************** *** void pokaz(wektorek www){ } cout << " \n\n\nx = " << www.x << " y = " << www.y << " z = " << www.z << endl ; 19
Przemienność Dzięki przeładowaniu, możemy tworzyć wyrażenia: wektora = wektorb * 11.1; wektora = 11.1 * wektorb; Natomiast odwrócenie kolejności czynników iloczynu nie wchodzi w grę, czyli zapis zostanie przez kompilator odrzucony jako błędny. Funkcja operatorowa, która jest funkcją składową klasy wymaga, aby obiekt stojący po lewej stronie (znaczka) operatora był obiektem jej klasy. Operator, który jest zwykłą funkcją globalną -nie ma tego ograniczenia. 20
Operator przypisania = Dwuargumentowy operator przypisania klasa & klasa::operator=(klasa &) służy do przypisania jednemu obiektowi klasy klasa treści drugiego obiektu tej klasy. Jeśli nie zdefiniujemy sobie tego operatora kompilator automatycznie wygeneruje swoją wersję tego operatora polegającą na tym, że przypisanie odbędzie się metodą składnik po składniku. W rezultacie takiego przypisania będziemy mieli dwa obiekty o bliźniaczo identycznej treści. 21
Operator przypisania = W operatorze przypisania można wyraźnie rozróżnić dwie części, w których: obiekt likwidujemy. kreujemy obiekt jeszcze raz. Jedyna sytuacją, gdy na widok znaczka = rusza do pracy konstruktor kopiujący jest wystąpienie tego znaku w linijce definicji obiektu. Znaczek ten wtedy oznacza inicjalizacji, a nie przypisanie. Inicjalizacja zajmuje się konstruktor kopiujący, a przypisaniem operator przypisania. 22
Przeładowanie operatora przypisania Załóżmy, że mamy do czynienia z klasą wizytowka. Jeśli zamierzamy zbierać dane o tysiącach ludzi należy to zrobić w ten sposób, żeby rezerwować w pamięci tyle miejsca ile jest nam potrzebne do pomieszczenia konkretnego nazwiska oraz imienia. 23
Przeładowanie operatora przypisania #include <iostream.h> #include <string.h> //*************************************************** *** class wizytowka { public : char *nazw ; char *imie ; wizytowka(char *n, char *im) ; wizytowka(const wizytowka &wzor) ; // konstruktor kopiujacy ~wizytowka(); void pisz(char *) ; przypisania // operator // konstruktor // destruktor wizytowka & operator=(const wizytowka &wzor) ; 24
Przeładowanie operatora przypisania wizytowka::wizytowka(char*n, char *im){ nazw = new char[strlen(n) + 1] ; imie = new char[strlen(im) + 1] ; strcpy(nazw, n); strcpy(imie, im); cout << "Pracuje konstruktor zwykly" << endl ; } //*************************************************** *** wizytowka::wizytowka(const wizytowka &wzor){ nazw = new char[strlen(wzor.nazw) + 1] ; imie = new char[strlen(wzor.imie) + 1] ; strcpy(nazw, wzor.nazw); strcpy(imie, wzor.imie); cout << "Pracuje konstruktor kopiujacy " << endl ; } 25
Przeładowanie operatora przypisania wizytowka::~wizytowka(){ delete nazw ; delete imie ; } //*************************************************** *** void wizytowka::pisz(char *txt){ cout << " " << txt << ": Mamy goscia, jest to " << imie << " " << nazw << endl ; } 26
Przeładowanie operatora przypisania wizytowka & wizytowka::operator=(const wizytowka &wzor){ // -- czesc "destruktorowa" ------ } delete nazw ; delete imie ; // -- czesc "konstruktorowa (konst. kopiujacy)"- nazw = new char[strlen(wzor.nazw) + 1] ; imie = new char[strlen(wzor.imie) + 1] ; strcpy(imie, wzor.imie); strcpy(nazw, wzor.nazw); cout << "Pracuje operator= (przypisania)\n" ; return *this ; 27
Przeładowanie operatora przypisania void main(){ } cout << "Definicje 'veneziano', i 'salzburger' \n" ; wizytowka veneziano("vivaldi", "Antonio"), salzburger("mozart", "Wolfgang A."); cout << "Definicja 'nowy' : \n" ; wizytowka nowy = veneziano ; cout << "Oto tresc w obiektach\n" ; veneziano.pisz("ven1"); salzburger.pisz("sal1"); nowy.pisz("now1"); cout << "Zabawy z przypisywaniem ---\n" ; nowy = salzburger ; nowy.pisz("now2"); nowy = veneziano ; nowy.pisz("now3"); nowy = salzburger = veneziano ; nowy.pisz("now4"); salzburger.pisz("sal4"); veneziano.pisz("ven4"); // konstruktor kopiujacy 28
Operator = nie generowany automatycznie Jeśli klasa ma składnik const, to operator nie będzie wygenerowany. Jest to oczywiste, bo skoro w klasie jest jakiś składnik ustawiony jako const to znaczy, że dopuszczamy jego inicjalizację, ale potem nie wolno już go zmieniać, czyli nic do niego przypisywać. Podobnie w wypadku obecności składnika będącego referencją. Jak wiemy, referencję tylko się inicjalizuje, a potem już przepadło nie można rozmyślić się i przerzucić przezwisko na inny obiekt. Jeśli klasa ma składnik będący obiektem innej klasy i w tej innej klasie operator przypisania określony jest jako private to wówczas nie będzie generowany operator przypisania dla klasy go zawierającej. Analogicznie jest w przypadku, gdy klasa ma klasę podstawową, w której operator= jest typu private. 29
Operatory postinkrementacji i postdekrementacji Wyróżniamy dwa różne operatory++: Gdy symbol ++ stoi przed nazwą obiektu to kompilator wybiera wersję jednoargumentową tego operatora (pre inkrementacja). Gdy symbol ++ stoi za nazwą obiektu kompilator wywołuje wersję dwuargumentową (post inkrementacja). Ważne jest tu sformułowanie: dwa różne. Bowiem ciało jednej i drugiej wersji może być całkowicie odmienne. To co dotyczy operatora postinkrementacji, dotyczy analogicznie operatora postdekrementacji. 30
Praktyczne rady dotyczące przeładowania Jako, że mechanizm przeładowania jest możliwością, z której można skorzystać lub nie. Chodzi o to, żeby jeśli już zdecydujemy się na przeładowanie jednego lub kilku operatorów żeby robić to mądrze, posługując się jakąś logiką. Oto kilka rad: Nie ma sensu przeładowywać wszystkich operatorów dla danej klasy. Może się bowiem okazać, że wykonałeś kawał dobrej, solidnej, nikomu nie potrzebnej roboty. Nie staraj się przeładowywać na siłę. Jeśli nazwa funkcji składowej lepiej opisuje działanie tej funkcji, niż robi to wygląd operatora, to lepiej pozostać przy funkcji. Przeładowanie powinno służyć uproszczeniu czytania, a nie produkcji łamigłówek. Cała wspaniałość przeładowania polega na zbliżeniu zapisu operacji na klasach, do prostoty zapisu operacji na typach wbudowanych. 31
Praktyczne rady dotyczące przeładowania Jeśli przeładowałeś operator+ oraz operator= to nie sądź, że tym samym masz automatycznie operator+= albo operator++. Są to zupełnie inne funkcje operatorowe i jeśli chcesz się nimi posługiwać wobec obiektów danej klasy, to musisz je także przeładować. Przed przystąpieniem do pracy trzeba się zastanowić, jak taka klasa wygląda z zewnątrz - to znaczy jakie wykonuje się operację na obiektach danej klasy. Kiedy już to jest jasne, można się zastanowi, które z tych funkcji wygodniej byłoby przeprowadzać za pomocą operatorów. Mimo całej dowolności treści przeładowanych funkcji operatorowych - staraj się zachować logikę pewnych zależności między operatorami. 32
Praktyczne rady dotyczące przeładowania Jeśli operator jest nieszkodliwy dla typu wbudowanego - to znaczy nie zmienia wartości zmiennej, na której pracuje, to staraj się, by jego odpowiednik dla klasy również niczego nie zmieniał wewnątrz obiektu. Wartość zwracana przez operator jest bardzo ważna. Dzięki temu, że operator nie tylko wykonuje działanie, ale też zwraca rezultat możliwe są wyrażenia kaskadowe a + b + c + d gdzie najpierw odbywa się jedno dodawanie, potem jego rezultat staje się składnikiem drugiego dodawania i kolejny rezultat staje się składnikiem trzeciego. 33
Operator jako funkcja składowa - czy globalna Skoro ten sam operator można zdefiniować jako funkcję składową albo funkcję globalną, to nasuwa się pytanie: jak lepiej zrobić? Jednoznacznej odpowiedzi nie ma. Zależy to od tego, czego oczekujemy od operatora. Ogólnie można powiedzieć, że: Jeśli operator zmienia w jakiś sposób obiekt, na którym pracuje, to powinien być zdefiniowany jako funkcja składowa jego klasy. Operatory te wtedy zwracają l wartości (np.: operatory =, ++, --). Jeśli natomiast operator sięga po obiekt po to, by pracować z nim bez modyfikowania go to wówczas raczej stosuje się operator w postaci funkcji globalnej (np.: operatory -, +, /). 34
Operator jako funkcja składowa - czy globalna Jeśli operator ma dopuszczać, by po jego lewej stronie stał typ wbudowany, to nie może być funkcją składową. Musi być globalną. Jeśli używamy operatora, który zdefiniowany jest jako funkcja składowa, wówczas w stosunku do jego pierwszego argumentu nie może zajść żadna niejawna konwersja (Mowa o tym argumencie, który zostaje przesłany za pomocą wskaźnika this). 35
Tajemnica operator << Właściwie już od początku posługujemy się zapisem, w którym występuje operator<<: cout<< Pierwszy program ; Dwuargumentowy operator<< jest, w stosunku do typu wbudowanego int, operatorem powodującym przesunięcie bitów o żądaną liczbę pozycji. Pytanie: Jak to więc jest możliwe, że taki zapis: int m = 2; cout<< m; powoduje wyprowadzenie na ekran liczby zapisanej w zmiennej typu int? 36
Tajemnica operator << Odpowiedź: Mamy tu do czynienia z najzwyklejszym przeładowaniem operatora<<. Zapis ten jest inaczej rozumiany tak: cout.operator<< (m); cout jest egzemplarzem obiektu klasy, która nazywa się ostream. To dla tej klasy dokonano przeładowania operatora. Przeładowanie jest możliwe, gdy jednym z argumentów jest obiekt typu zdefiniowanego przez użytkownika. Takim typem zdefiniowanym - choć bibliotecznym - jest właśnie klasa ostream. 37
Tajemnica operator << Jest kilka wersji przeładowania operatora<< na okoliczność pracy z różnymi typami argumentów np.: float, char*. Jeśli mamy zdefiniowaną własną klasę np.: wektor, to użycie operatora<< w ten sposób: wektor w; cout<< w; Jest niepoprawne. Aby można było wykorzystać taki zapis należy po raz kolejny przeładować operator<<. Jest on dwuargumentowy, więc argumenty będą typu ostream i wektor. 38
Tajemnica operator << 01. #include <iostream.h> 02. class wektor 03. { 04. public: 05. float x, y, z ; 06. wektor(float a=0, float b=0, float c=0):x(a) 07. { 08. y = b; 09. z = c; 10. } 11. }; 39
Tajemnica operator << 01. ostream & operator<<(ostream & ekran, wektor & w) 02. { 03. ekran << "wspolrzedne wektora : "; 04. ekran << "( ; 05. ekran << w.x; 06. ekran << ", " << w.y << ", " << w.z << ")"; 07. return ekran; 08. } 40
Tajemnica operator << 01. void main(void) 02. { 03. wektor w(1,2,3),v,k(-10, -20, 100); 04. cout << w; 05. cout << "\nwektor v ->" << v<< endl; 06. cout << "Wywolanie jawne \n ; 07. operator<<( cout, w); 08. } 41
Koniec Dziękuję za uwagę i zapraszam na 15 minut przerwy. W dalszej części ćwiczenia do samodzielnego wykonania. 42
Ćwiczenia do wykonania Zadanie 01 Zdefiniuj klasę LiczbaZespolona oraz stosowne funkcje, konstruktory oraz funkcje operatorowe, ponadto uzupełnij klasę LiczbaZespolona tak, aby możliwe stało się wykonywanie następującego kodu: LiczbaZespolona z1, z2(1,2), z3(z2); cout<< z1; cout<< z2; cout<< z3; int a; z1 = z2 + z3; cout<< z1; cout<< z2; cout<< z3; z1 = z1 - z3; cout<< z1; cout<< z2; cout<< z3; z3 = z1 * z2; cout<< z1; cout<< z2; cout<< z3; z1 = z2.liczbaprzeciwna(); cout<< z1; cout<< z2; z3 = liczbasprzezona(z1); cout<< z1; cout<< z3; a =!z2; cout<< a; cout<< z1; cout<< z2; cout<< z3; //modul z liczby zespolonej z2 43
Ćwiczenia do wykonania UWAGA: Dla liczby zespolonej z1(a,b) -> z1 = a + bi oraz z2(c,d) -> z2 = c + di gdzie: a,c -> to części rzeczywiste, b,d -> części urojone, operacje arytmetyczne przedstawiają się następująco: - dodawanie: z1 + z2 = (a+c) + (b+d)i, - odejmowanie: z1- z2 = (a-c) + (b-d)i, - mnożenie: z1*z2 = (ac-bd)+ (ad+ bc)i, - moduł z z1: z1 = pierwiastek(a^2 + b^2), - liczba przeciwna do liczby zespolonej z1: -z1 = -a - bi, - liczbą sprzężoną z liczbą z1: z1* = a - bi. 44