Przeciążanie funkcji
Transkrypt
Przeciążanie funkcji
Programowanie w języku C++ Część siódma Operatory przeciążone Autor Roman Simiński Kontakt [email protected] www.us.edu.pl/~siminski Niniejsze opracowanie zawiera skrót treści wykładu, lektura tych materiałów nie zastąpi uważnego w nim uczestnictwa. Opracowanie to jest chronione prawem autorskim. Wykorzystywanie jakiegokolwiek fragmentu w celach innych niż nauka własna jest nielegalne. Dystrybuowanie tego opracowania lub jakiejkolwiek jego części oraz wykorzystywanie zarobkowe bez zgody autora jest zabronione. Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie, co to takiego? Przeciążanie funkcji Przeciążanie funkcji (ang. function overloading) — tworzenie większej liczby funkcji o takiej samej nazwie. Nazwa funkcji może być zatem użyta wielokrotnie do realizacji różnych czynności. Jest więc „przeciążona” dodatkowymi „obowiązkami”. Kompilator zadba o dobranie właściwej wersji funkcji przeciążonej w zależności od kontekstu jej wywołania. Przykład funkcji przeciążonej int add( int a, int b ) { return a + b; } // 1-sza wersja funkcji przeciążonej add double add( double a, double b ) { return a + b; } // 2-ga wersja funkcji przeciążonej add cout << endl << "Dodawanie int :" << add( 1, 1 ); cout << endl << "Dodawanie double :" << add( 1.0, 1.0 ); Copyright © Roman Simiński Strona : 2 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie, co to takiego? Przeciążanie funkcji — rygory Przeciążanie funkcji jest możliwe, jeżeli listy parametrów funkcji przeciążonych różnią sie od siebie: typami parametrów, liczbą parametrów, jednocześnie typami i liczbą parametrów. Funkcje o tych samych nazwach i listach parametrów, różniące się tylko typem rezultatu, nie są przez kompilator rozróżniane i powodują wystąpienie błędu kompilacji. Copyright © Roman Simiński Strona : 3 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie, co to takiego? Dlaczego przeciążanie nie uwzględnia rezultatu funkcji? int addAndPrint( int a, int b ) { cout << a + b; return a + b; } void addAndPrint( int a, int b ) { cout << a + b; } int result = addAndPrint( 10, 20 ); // OK addAndPrint( 10, 20 ); Copyright © Roman Simiński Strona : 4 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, szczegóły implementacyjne Przeciążanie funkcji — delikatne problemy Załóżmy, że dane są następujące funkcje: void printData( long num ) { cout << "Liczba long: " << num << endl; } void printData( float num ) { cout << "Liczba float: " << num << endl; } void printData( double num ) { cout << "Liczba double: " << num << endl; } Te wywołania są poprawne: long ln = 200; float fn = 200; double dn = 200; printData( ln ); printData( fn ); printData( dn ); Copyright © Roman Simiński Strona : 5 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, szczegóły implementacyjne Przeciążanie funkcji — delikatne problemy To wywołanie jest niepoprawne: printData( 200 ); To poprawne, leczy być może zaskakujące: printData( 3.14 ); Natomiast, gdyby istniały tylko dwie, następujące wersje funkcji printData: void printData( long num ) { cout << "Liczba long: " << num << endl; } void printData( float num ) { cout << "Liczba float: " << num << endl; } printData( 3.14 ); Copyright © Roman Simiński Strona : 6 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, szczegóły implementacyjne Przeciążanie funkcji — zasady definiowania funkcji 1. Sygnatury i typ rezultatu są jednakowe, zakłada się deklarację tej samej funkcji, różnice w nazwach parametrów nie są znaczące: void printData( long num ); void printData( long number ); 2.Sygnatury są jednakowe, typy rezultatów są różne, kompilator potraktuje drugą deklarację jako niepoprawną redeklarację tej samej funkcji i zgłosi błąd: void printData( long num ); long printData( long num ); Uwaga — przy doborze wersji funkcji przeciążonej typ rezultatu nie jest brany pod uwagę! 3.Sygnatury obu funkcji różnią są typami parametrów lub ich liczbą, mamy dwie wersje funkcji przeciążonej: int printData( int num ); long printData( long num ); Nazwa zdefiniowana przez typedef nie oznacza nowego typu: typedef int Integer; int printData( int num ); Integer printData( Integer num ); Copyright © Roman Simiński Strona : 7 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, szczegóły implementacyjne Przeciążanie funkcji — rozróżnianie wersji Poszczególne wersje funkcji przeciążonej różni sygnatura — nazwa funkcji i lista parametrów. Określenie, która wersja funkcji przeciążonej ma być wywołana w danym kontekście nazywa się rozróżnianiem wersji lub rozpoznawaniem wywołania (ang. function call resolution). Najważniejszym elementem rozpoznawania wywołania jest dopasowywanie parametrów (ang. argument matching). Polega on na porównaniu parametrów aktualnych wywołania z parametrami formalnymi funkcji. Proces dopasowania parametrów może skończyć się jednym z wariantów: udane dopasowanie, dopasowanie nie jest możliwe, dopasowanie jest niejednoznaczne. Copyright © Roman Simiński Strona : 8 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, szczegóły implementacyjne Przeciążanie funkcji — rodzaje dopasowania void printData( long num ); void printData( float num ); void printData( double num ); Udane dopasowanie printData( 10L ); printData( 10.5f ); printData( 10.5 ); Dopasowanie nie jest możliwe printData( "10" ); Dopasowanie jest niejednoznaczne printData( 10 ); Copyright © Roman Simiński Strona : 9 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, szczegóły implementacyjne Przeciążanie funkcji — jak uzyskać ścisłe dopasowanie? Istnieją cztery sposoby uzyskania ścisłego dopasowania, stosowane w następującej kolejności: ścisła zgodność, zgodność dzięki awansowaniu (ang. through promotion), zgodność dzięki standardowym przekształceniom typów, zgodność dzięki przekształceniom typów definiowanym przez programistę. Copyright © Roman Simiński Strona : 10 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, szczegóły implementacyjne Ścisła zgodność Typy parametru aktualnego i formalnego są identyczne: void printData( int ); void printData( char * ); printData( 0 ); printData( "Napis" ); Wersja zgodna z printData( int ) Wersja zgodna z printData( char * ) // Można stosować jawne rzutowanie typów printData( ( char * )0 ); Wersja zgodna z printData( char * ) Copyright © Roman Simiński Strona : 11 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, szczegóły implementacyjne Zgodność dzięki awansowaniu Jeżeli nie występuje ścisła zgodność, to przed następną próbą dopasowania, typ parametru aktualnego jest „poszerzany” wg. następujących zasad : Parametr typu char, unsigned char lub short awansują do typu int. Jeżeli rozmiary typu int i short są równe, parametr unsigned short awansuje do unsigned int, w przeciwnym wypadku do int. Parametr typu float awansuje do typu double. Parametr typu wyliczeniowego awansuje do typu int. void printData( int ); void printData( short ); void printData( long ); printData( 'a' ); printData( 3.14 ); Wersja zgodna z printData(int), 'a' awansuje do typu int. Brak możliwości awansu – odwołanie niejednoznaczne. void printData( char ); void printData( int ); void printData( unsigned int ); unsigned char uc; printData( uc ); Copyright © Roman Simiński Wersja zgodna z printData( int ), uc awansuje do int. Strona : 12 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, szczegóły implementacyjne Zgodność dzięki standardowym przekształceniom typów Jeżeli nie występuje ścisła zgodność, nawet po zastosowaniu awansowania parametru aktualnego, przed następną próbą dopasowania zastosowane zostaną standardowe przekształcenia typów wg. następujących zasad : Każdy argument aktualny typu liczbowego będzie zgodny z dowolnym innym typem liczbowym parametru formalnego. Każdy argument aktualny typu wyliczeniowego będzie zgodny z dowolnym typem liczbowym parametru formalnego. Literał 0 będzie zgodny z parametrem formalnym typu wskaźnikowego jak i typu liczbowego. Wskaźnik do dowolnego typu będzie zgodny z parametrem formalnym typu void *. void printData( int ); void printData( float); printData( 3.14 ); Dwa możliwe przekształcenia, odwołanie niejednoznaczne. void printData( int ); printData( 3.14 ); Copyright © Roman Simiński Wersja printData( int ), standardowe przekształcenie, nieco dziwne być może... . Strona : 13 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, szczegóły implementacyjne Zgodność dzięki przekształceniom typów definiowanym przez programistę Jeżeli żaden z poprzednich sposobów nie doprowadził do dopasowania parametrów to dokonuje się zdefiniowanych przez programistę przekształceń typów — o ile takie istnieją. Definiowanie przekształceń typów dotyczy klas, dla nich można definiować operatory zwane operatorami przekształcenia typu. Pozwalają one na zdefiniowanie sposobu konwersji obiektu pewnej klasy na obiekt zadanego typu. Szczegółowe omówienie operatorów przekształcenia typów nastąpi później. Copyright © Roman Simiński Strona : 14 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, szczegóły implementacyjne Od klasy do typu wbudowanego z operatorem konwersji typu: class Integer64 { public: operator int(); . . . }; Operator przekształcenia obiektu klasy Integer64 do typu int. Integer64 i; void printData( int ); void printData( float ); printData( i ); Copyright © Roman Simiński Wersja fun( int ), wg. przekształcenia w klasie Integer64. Strona : 15 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, szczegóły implementacyjne Zgodność dzięki przekształceniom typów definiowanym przez programistę, cd. ... Kompilator będzie w trakcie dopasowywania parametrów będzie używał konwersji konstruktorowych. Polega ona na niejawnym utworzeniu obiektu tymczasowego i zainicjowaniu go odpowiednim konstruktorem. Jeżeli w jakieś klasie C istnieje konstruktor posiadający pojedynczy parametr typu T, to kompilator użyje tego konstruktora zawsze, gdy konieczna jest konwersja obiektu typu T na obiekt klasy C. Copyright © Roman Simiński Strona : 16 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, szczegóły implementacyjne Od typu wbudowanego do klasy, z wykorzystaniem konstruktora: class Integer64 { public: Integer64(); Integer64( int . . . }; Wersja printData( Integer64 & ) i ); void printData( Integer64 & num ); printData( 1 ); Utworzenie niejawnego obiektu tymczasowego i zainicjowanie go konstruktorem Integer64( int ): printData( Integer64( 1 ) ) Działa to również — w sposób analogiczny — w przypadku konwersji pomiędzy dwoma klasami. Copyright © Roman Simiński Strona : 17 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, szczegóły implementacyjne Wywołania z wieloma parametrami Następuje próba dopasowania każdego argumentu wg. zasad podanych wcześniej. Stosowany jest algorytm koniunkcji (ang. intersection rule). Wybiera się wersje o co najmniej takiej samej zgodności każdego z parametrów oraz typuje się tą wersję, która posiada najpełniejsze dopasowanie dla przynajmniej jednego parametru. Odwołanie uznaje się za niejednoznaczne, jeżeli: żadna wersja nie posiada parametru najbardziej zgodnego, więcej niż jedna wersja zawiera najpełniej zgodny parametr. void fun( int, int ); void fun( char *, int ); fun( 0, 'a' ); void fun( int, int ); void fun( long, long ); int i, j; fun( i, j ); void fun( int, int ); void fun( double, double ); fun( 'a', 3.14f ); Copyright © Roman Simiński Wersja fun( int, int ), pełna zgodność dla 0 z int oraz równa zgodność 'a' w obu wersjach. Nie istnieje najpełniejsze dopasowanie Dwie funkcje posiadają parametr najbardziej zgodny. Strona : 18 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, szczegóły implementacyjne Wywołania z parametrami domyślnymi Dla funkcji z parametrami domyślnymi stosuje się zwykłe zasady dopasowania, zgodność może dotyczyć zarówno wersji z bez parametrów domyślnych jak i wersji z parametrami. void printData( int ); void printData( long, int = 0 ); printData( 0, 0 ); Wersja zgodna z printData( long, int ) printData( 0 ); Wersja zgodna z printData( int ) printData( 10L ); Wersja zgodna z printData( long, int ) printData( 3.14 ); Wywołanie niejednoznaczne Copyright © Roman Simiński Strona : 19 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, podsumowanie Przeciążanie funkcji — krótkie podsumowanie Dzięki możliwości przeciążania funkcji: programista może stosować jednakowe, spójne nazwy funkcji mimo różnic w typach i liczbie parametrów, poszczególne wersje funkcji mogą być prostsze i łatwiejsze do napisania, być może niektóre wersje funkcji będą wyraźnie szybsze. int sqrt( int num ); float sqrt( float num ); double sqrt( double num ); Dobór odpowiedniej wersji funkcji przeciążonej: odbywa się na etapie kompilacji i jest pamiętany w kodzie wynikowym, nie wpływa na efektywność wykonania programu, jest prosty i oczywisty w przypadkach jednoznacznych, może powodować błędy kompilacji kub nieprzewidziane zachowanie kodu w przypadku gdy parametry wywołania nie pasują do parametrów formalnych funkcji. Copyright © Roman Simiński Strona : 20 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, podsumowanie Przeciążanie funkcji — uwagi na zakończenie Z funkcjami przeciążonymi wiąże się jeszcze kilka istotnych zagadnień: Przeciążone funkcje składowe klas, Przeciążone funkcje składowe klas w hierarchii klas, Przeciążanie funkcji a zasięg identyfikatorów. Przeciążone funkcje składowe kontra funkcje wirtualne, czyli jak jest różnica pomiędzy przeciążaniem a redefinicją. Przeciążone funkcje kontra funkcje wzorcowe, czyli kiedy przeciążać funkcje a kiedy definiować ją z wykorzystaniem wzorców (szablonów). Mówiąc o przeciążaniu funkcji autorzy książek często używają pojęcia polimorfizmu funkcji. Jednak pojęcie polimorfizmu występuje głównie w kontekście obiektów wykorzystujących metody wirtualne. W obu tych kontekstach rozumienie polimorfizmu może być nieco inne. Wiele interesujących informacji na funkcji przeciążonych zawiera książka: S.B. Lippman, J. Lajoie, Podstawy języka C++,WNT, 2003. oraz jej poprzednie wersje. Copyright © Roman Simiński Strona : 21 Podstawy i języki programowania Język C++ Operatory przeciążone Overloading ― przeciążanie funkcji, podsumowanie Przeciążanie funkcji — czy bez tego można się obejść? Zamiast przeciążania funkcji można zdefiniować jej różne wersje i „ręcznie” zaopatrzyć je w odpowiedni przedrostek lub przyrostek: void printDataInt ( int num ); void printDataLong ( long num ); void printDataDouble( double num ); Albo napisać to jak w „starym”, „dobrym” C: enum DataTypes { INT, LONG, DOUBLE }; printData( INT, &in ); printData( LONG, &ln ); printData( DOUBLE, &dn ); void printData( int dataType, void * data ) { switch( dataType ) { case INT : cout << "Liczba int: "; cout << *( ( int * )data ) << endl; break; case LONG : cout << "Liczba long: "; cout << *( ( long * )data ) << endl; break; case DOUBLE : cout << "Liczba double: "; cout << *( ( double * )data ) << endl; break; } } Copyright © Roman Simiński Strona : 22 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — koncepcja Problem Należy napisać program realizujący obliczenia dla układów prądu sinusoidalnie zmiennego. Do realizacji tych obliczeń wykorzystuje się liczby zespolone — do tego celu należy zbudować klasę reprezentującą liczbę zespoloną, klasę Complex . Podbudowa teoretyczna W świecie liczb rzeczywistych zakłada się nieistnienie pierwiastka z liczby nieujemnej. W XVI wieku wprowadzono pojęcie jednostki urojonej i o własności: i2 = –1. Można z tego wysnuć wniosek, że , w istocie jednak niepoprawny, dlatego że dalej nie pierwiastkuje sie liczb ujemnych.. Za liczbę zespoloną z przyjmuje się zatem liczbę w następującej postaci: z = a + bi, gdzie i2 = –1 Liczbę (rzeczywistą) a nazywamy częścią rzeczywistą, zaś liczbę b częścią urojoną liczby zespolonej z. Podstawowe operacje na liczbach zespolonych w tej postaci: (a+bi)+(c+di)=(a+c)+(b+d)i (a+bi)-(c+di)=(a-c)+(b-d)i (bc - ad) a+ bi (ac + bd) = i + 2 2 2 2 (c + b ) (c + b ) c + di (a+bi)·(c+di)=(ac-bd)+(bc+ad)i. Copyright © Roman Simiński Strona : 23 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — koncepcja Klasa Complex — pierwsza wersja class Complex { public: // Konstruktor – ogólny dzięki parametrom domyślnym staje sie też // domyślnym, konstruktor kopiujący nie jest konieczny. Complex( double re = 0, double im = 0 ); // Akcesory double getReal() const; double getImag() const; void setReal( double newVal ); void setImag( double newVal ); private: double real; double imag; }; Copyright © Roman Simiński // Część rzeczywista // Część urojona Strona : 24 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — koncepcja Klasa Complex — pierwsza wersja, implementacja funkcji składowych Complex::Complex( double re, double im ) : real( re ), imag( im ) { } double Complex::getReal() const { return real; } double Complex::getImag() const { return imag; } void Complex::setReal( double newVal ) { real = newVal; } void Complex::setImag( double newVal ) { imag = newVal; } Copyright © Roman Simiński Strona : 25 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — koncepcja Klasa Complex — jak ją wykorzystać? Complex z1; // Konstruktor: z1.Complex(); parametr domyślny! Complex z2( 2 ); // Konstruktor: z2.Complex( 2 ); parametr domyślny! complex z3( 1, 1 ); // Konstruktor: z2.Complex( 1, 1 ); // Dodawanie liczb zespolonych – wersja siermiężna z1.setReal( z2.getReal() + z3.getReal() ); z1.setImag( z2.getImag() + z3.getImag() ); Czy nie można tak: // Działania na liczbach zespolonych – wersja poprawiona z1 = z2 + z3; . . . z1 = z2 * z3; . . . Można! Tylko skąd kompilator ma wiedzieć, jak dodawać, mnożyć, dzielić czy odejmować liczby zespolone? Należy go tego nauczyć! Polega to na związaniu z klasą Complex zestawu operatorów, realizujących określone działania zgodnie z intencjami programisty. Copyright © Roman Simiński Strona : 26 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — koncepcja Operatory są w naturalny sposób przeciażone Koncepcja przeciążania w przypadku operatorów jest oczywista i całkiem naturalna. Operatory „potrafią” wykonywać określone działania dla danych różnych typów; int ia, ib, ic; . . . ic = ia + ib; float fa, fb, fc; . . . fc = fa + fb; Również w innych językach operatory są w naturalny sposób „przeciążone”, niejednokrotnie nie tylko dla typów liczbowych (język Pascal, impl. Borland): Var SA, SB, SC : String; . . . SA := 'Operatory są '; SB := 'przeciążone robotą'; SC := SA + SB; WriteLn( SC ); Copyright © Roman Simiński Strona : 27 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — koncepcja Jak zmusić operatory języka C++ do działania zgodnie z rachunkiem liczb zespolonych? Koncepcja przeciążania operatorów w języku C++ nie jest nowa. Nowością jest potraktowanie użycia operatora jako wywołania specjalnej funkcji, zwanej funkcją operatorową. Programista może napisać dla większości operatorów specjalne funkcje, określające sposób działania danego operatora dla argumentów, z których przynajmniej jeden jest obiektem. Generalne zasady wykorzystania funkcji operatorowych Nie wolno zmieniać znaczenia operatora określonego dla wbudowanych typów danych. Nie wolno budować nowych operatorów. Przynajmniej jeden argument funkcji operatorowej musi być obiektem jakiejś klasy. Nie wolno zmieniać zdefiniowanych pierwotnie reguł pierwszeństwa operatorów. Musi być zachowana liczba argumentów operatora. Copyright © Roman Simiński Strona : 28 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — koncepcja Operatory jako funkcje, użycie operatora jako wywołanie funkcji Weźmy pod lupę operator przypisania = Complex z1; Complex z2( 1, 2 ); z1 = z2; Czy takie zastosowanie zgodne jest ustalonymi zasadami? Tak, bowiem: oba argumenty są obiektami klasy; Jest to istniejący operator z dozwolonego zestawu, nie następuje zmiana reguł pierwszeństwa i liczby argumentów. Definiujemy funkcje operatorową dla operatora = klasy Complex class Complex { public: . . . void operator = ( Complex & z ); . . . }; Copyright © Roman Simiński Strona : 29 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — koncepcja Operatory jako funkcje, użycie operatora jako wywołanie funkcji, cd. ... Implementacja funkcji operatorowej void Complex::operator = ( Complex & z ) { real = z.real; // Lub setReal( z.getReal() ); imag = z.imag; // Lub setImag( z.getImag() ); } Jak to działa? Complex z1; Complex z2( 1, 2 ); z1 = z2; Wywołanie: z1.operator=( z2 ); z1 real: 1 imag: 2 Copyright © Roman Simiński void Complex::operator = ( Complex & z ) { real = z.real; imag = z.imag; } z2 real: 1 imag: 2 Strona : 30 Język C++ Podstawy i języki programowania Operatory przeciążone Przeciążanie operatorów — operator przypisania = Czy nasz operator przypisania jest dobry i w tym przypadku? Complex z1; Complex z2; complex z3( 1, 2 ); z2.operator = ( z3 ) Wywołanie: z1.operator = ( z1 = z2 = z3; ? ); Complex & Complex::operator = ( Complex & z ) { real = z.real; this — w obrębie definicji każdej funkcji imag = z.imag; składowej klasy występuje wskaźnik this. Wskazuje on zawsze na obiekt, dla którego została wywołana dana funkcja składowa. return *this; } z1 real: 1 imag: 2 z2 real: 1 imag: 2 z3 real: 1 imag: 2 z1.operator = ( z2.operator = ( z3 ) ); Copyright © Roman Simiński Strona : 31 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — operator przypisania = Operator przypisania, analizy ciąg dalszy A co gdy napiszemy tak? z1 = z1; Zabezpieczenie przed przypisaniem do samego siebie Complex & Complex::operator = ( const Complex & z ) { if( &z != this ) { Przy okazji, umieszczenie const pozwala na: real = z.real; imag = z.imag; const Complex a( 0, 0 ); } Complex b; return *this; } b = a; Operator przypisania kontra konstruktor kopiujący Complex z1( 1, 2 ); Complex z2 = z1; Konstruktor kopiujący z2 = z1; Operator przypisania Copyright © Roman Simiński Strona : 32 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — operator przypisania = Czy musimy przeciążać operator = i definiować konstruktor kopiujący? Jeżeli operator przypisania nie jest jawnie zdefiniowany dla danej klasy, a jest potrzebny kompilatorowi, generuje on domyślny operator przypisania, realizujący kopiowanie pole-po-polu. Jeżeli konstruktor kopiujący nie jest jawnie zdefiniowany dla danej klasy, a jest potrzebny kompilatorowi, realizuje on inicjalizację obiektu wykorzystując kopiowanie pole-po-polu. No to w końcu ― używać, nie używać? Stosowanie konstruktora kopiującego i przeciążonego operatora przypisania jest dobrą, programistyczną praktyką. Dzięki jawnym definicjom programista ma kontrolę nad kopiowaniem wartości, występujących w wielu, czasem zaskakujących sytuacjach. Naprawdę mało klas jest tak prostych, że nie potrzebują konstruktora kopiującego ani przeciążonego operatora przypisania. Operator przypisania jest nierozerwalnie związany z klasą i może być definiowany jedynie jako funkcja składowa klasy. Copyright © Roman Simiński Strona : 33 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — operator przypisania = Klasa Complex po poprawkach class Complex { public: // Konstruktory Complex( double re = 0, double im = 0 ); Complex( const Complex & z ); // Akcesory double getReal() const; double getImag() const; void setReal( double newVal ); void setImag( double newVal ); // Operator przypisania Complex & operator = ( const Complex & z ); private: double real; double imag; }; Copyright © Roman Simiński // Część rzeczywista // Część urojona Strona : 34 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — operator przypisania = Klasa Complex po poprawkach, cd. ... // Konstruktory Complex::Complex( double re, double im ) : real( re ), imag( im ) {} Complex::Complex( const Complex & z ) : real( z.real ), imag( z.imag ) {} // Akcesory double Complex::getReal() const { return real; } double Complex::getImag() const { return imag; } void Complex::setReal( double newVal ) { real = newVal; } void Complex::setImag( double newVal ) { imag = newVal; } // Operator przypisania Complex & Complex::operator = ( const Complex & z ) { if( &z != this ) { real = z.real; imag = z.imag; } return *this; } Copyright © Roman Simiński Strona : 35 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — operator dodawania + Chcemy dodawać liczby zespolone Complex z1; Complex z2( 2 ); complex z3( 1, 1 ); z1 = z2 + z3; Rozszerzamy klasę Complex o przeciążony operator dodawania class Complex { public: . . . Complex operator + ( const Complex & z ); . . . }; Implementacja, wersja 1-sza Complex Complex::operator + ( const Complex & z ) { Complex tmp; // Obiekt tymczasowy do przechowania sumy tmp.real = real + z.real; tmp.imag = imag + z.imag; return tmp; } Copyright © Roman Simiński Strona : 36 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — operator dodawania + Jak to działa? Complex z1; Complex z2( 2 ); complex z3( 1, 1 ); Wywołanie: z2.operator + ( z3 ); z2 + z3; Razem z operatorem przypisania z1 = z2 + z3; Wywołanie: z1.operator = ( z2.operator + ( z3 ) ); Implementacja, wersja 2-ga Complex Complex::operator + ( const Complex & z ) { return Complex( real + z.real, imag + z.imag ); } Dlaczego rezultat operatora + nie jest referencją? W C i C++ rezultatem funkcji nie powinien być wskaźnik ani referencja do zmiennych automatycznych funkcji. Te lokowane są zwykle na stosie i po zakończeniu działania funkcji przestają istnieć. Copyright © Roman Simiński Strona : 37 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — operator odejmowania Chcemy odejmować liczby zespolone Complex z1; Complex z2( 2 ); complex z3( 1, 1 ); z1 = z2 - z3; Rozszerzamy klasę Complex o przeciążony operator odejmowania class Complex { public: . . . Complex operator - ( const Complex & z ); . . . }; Implementacja operatora Complex Complex::operator - ( const Complex & z ) { return Complex( real - z.real, imag - z.imag ); } Copyright © Roman Simiński Strona : 38 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — operator Chcemy zmienić znak liczby zespolonej Complex z1; Complex z2( 2, 2 ); z2 = -z1 Rozszerzamy klasę Complex o przeciążony operator zmiany znaki („unarny minus”) class Complex { public: . . . Complex operator - ( const Complex & z ); Complex operator - (); . . . }; Implementacja jednoargumentowego operatora Complex Complex::operator - () { return Complex( - real, - imag ); } Jak to działa? z2 = -z1; z1 = z2 - z3; Copyright © Roman Simiński Wywołanie: z2.operator = ( z1.operator - () ); Wywołanie: z1.operator = ( z2.operator - ( z3 ) ); Strona : 39 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — funkcja składowa czy zwykła? Niby jest świetnie, ale... Complex z; double d; z + d; Wywołanie: z.operator + ( Complex( d ) ); d + z; Wywołanie: ??? Funkcja operatorowa nie musi być (z pewnymi wyjątkami) zdefiniowana jako funkcja składowa a jako zwykła funkcja, posiadająca przynajmniej jeden argument będący obiektem. Dla jednoargumentowego operatora # oraz obiektu O: #O Dla dwuoargumentowego operatora # oraz obiektów X i Y: X # Y oznacza wywołanie funkcji składowej O.operator#(), zwykłej operator#( O ). oznacza wywołanie funkcji składowej X.operator#( Y ), zwykłej operator#( X, Y ). Copyright © Roman Simiński Strona : 40 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — funkcja składowa czy zwykła? Implementacja operatorów w postaci zwykłych funkcji Complex operator + ( double num, const Complex & z ) { return Complex( num + z.getReal(), z.getImag() ); } Nieskładowa funkcja operatorowa w akcji d + z1; Wywołanie: operator + ( d, z1 ); Z operatorem przypisania z2 = d + z1; Copyright © Roman Simiński Wywołanie: z2.operator = ( operator + ( d, z1 ) ); Strona : 41 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — funkcja składowa czy zwykła? Nieskładowa funkcja operatorowa ma utrudniony dostęp do pól Complex operator + ( double num, const Complex & z ) { return Complex( num + z.getReal(), z.getImag() ); } Aby funkcja nieskładowa miała wygodniejszy dostęp do pól klasy, może się z nią zaprzyjaźnić. Taka funkcja może mieć bezpośredni dostęp do prywatnych pól klasy. Nieskładowa, zaprzyjaźniona funkcja operatorowa class Complex { . . . friend Complex operator + ( double num, const Complex & z ); . . . }; Complex operator + ( double num, const Complex & z ) { return Complex( num + z.real, z.imag ); } Copyright © Roman Simiński Strona : 42 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — funkcja składowa czy zwykła? Nieskładowa funkcje operatorowe na każdą okazję class Complex { . . . friend Complex operator + ( double num, const Complex & z ); friend Complex operator + ( const Complex & z, double num ); friend Complex operator + ( const Complex & z1, const Complex & z2 ); . . . }; Complex operator + ( double num, const Complex & z ) { return Complex( num + z.real, z.imag ); } Complex operator + ( const Complex & z, double num ) { return Complex( num + z.real, z.imag ); } Complex operator + ( const Complex & z1, const Complex & z2 ) { return Complex( z1.real + z2.real, z1.imag + z2.imag ); } Copyright © Roman Simiński Strona : 43 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — funkcja składowa czy zwykła? Wykorzystanie funkcji nieskładowych z2 = 2 + z3; z2.operator = ( operator + ( double, Complex ) ); z4 = z3 + 2; z4.operator = ( operator + ( Complex, double ) ); z1 = z2 + z3; z1.operator = ( operator + ( Complex, Complex ) ); A gdyby tak pozostawić tylko jedną funkcję nieskładową? class Complex { . . . friend Complex operator + ( const Complex & z1, const Complex & z2 ); . . . }; z2 = 2 + z3; z2.operator=( operator+( Complex(double), Complex ) ); z4 = z3 + 2; z4.operator=( operator+( Complex, Complex(double) ) ); z1 = 2 + 3; z1.operator=(operator+(Complex(double),Complex(double))); Copyright © Roman Simiński Strona : 44 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — przeciążanie operatorów ++ i —― Problem — dwie wersje operatorów ++ i —― z1++; ++z1; --z2; z2--; Do wersji 3.0 języka C++ nie istniało rozróżnienie pomiędzy operatorami w wersji przedrostkowej i przyrostkowej. Teraz wersja przyrostkowa obsługiwana jest przez funkcję operatorową, definiowaną z parametrem typu int. Nie odgrywa on żadnego znaczenia praktycznego, jest istotny ze względu na syntaktykę języka. Rozszerzamy klasę Complex o przeciążone operatory ++ i —― class Complex { public: . . . Complex & operator ++ Complex operator ++ ( . . . Complex & operator -Complex operator -- ( . . . }; Copyright © Roman Simiński (); int ); // Postać przedrostkowa // Postać przyrostkowa (); int ); // Postać przedrostkowa // Postać przyrostkowa Strona : 45 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — przeciążanie operatorów ++ i —― Implementacja operatorów przedrostkowych Complex & Complex::operator ++ () { ++real; Zwiększ część rzeczywistą i urojoną o 1 ++imag; return *this; } „Oddaj” zmodyfikowany obiekt Complex & Complex::operator -- () { --real; Zmniejsz część rzeczywistą i urojoną o 1 --imag; return *this; } „Oddaj” zmodyfikowany obiekt Wywołanie operatorów przedrostkowych z1 = ++z2; Wywołanie: z1.operator = ( z2.operator ++ () ); z1 = --z2; Wywołanie: z1.operator = ( z2.operator -- () ); Copyright © Roman Simiński Strona : 46 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — przeciążanie operatorów ++ i —― Implementacja operatorów przyrostkowych Complex Complex::operator ++ ( int ) { Zapamiętaj w copy wartość obiektu przed inkrementacją Complex copy; copy = *this; ++real; ++imag; return copy; } Zwróć kopię wartości obiektu z przed inkrementacji Complex Complex::operator -- ( int ) { Complex copy( *this ); Tu nieco krócej — konstruktor kopiujący --real; --imag; return copy; } Wywołanie operatorów przyrostkowych z1 = z2++; Wywołanie: z1.operator = ( z2.operator ++ (0) ); z1 = z2--; Wywołanie: z1.operator = ( z2.operator -- (0) ); Copyright © Roman Simiński Strona : 47 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — wirtualna tablica FileArray Problem Zdefiniować klasę FileArray, przechowującą znaki w pliku, pozwalającą na wykonywanie operacji zbliżonych do tych, które wolno wykonać na „zwykłej” tablicy. FileArray tab( "temp.dat" ); if( tab.ready() ) { for( int i = 0; i < 26; i++ ) tab[ i ] = 'A' + i; for( int i = 0; i < tab.size(); i++ ) cout << tab[ i ]; } Copyright © Roman Simiński Strona : 48 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — wirtualna tablica FileArray Definicja klasy class FileArray { public: FileArray( char * fileName ); ~FileArray(); bool ready() const; Te funkcje operatorowe są tu najważniejsze FileArray & operator = ( char c ); FileArray & operator [] ( long ); operator char(); long size() const; private: FILE * file; void operator &() {}; }; Copyright © Roman Simiński Nie pozwól pobrać adresu tej tablicy! Strona : 49 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — wirtualna tablica FileArray Definicja funkcji składowych FileArray::FileArray( char * fileName ) : file( 0 ) { file = fopen( fileName, "w+b" ); } FileArray::~FileArray() { if( file ) fclose( file ); } bool FileArray::ready() const { return ( file != 0 ); } long FileArray::size() const { long currPos = ftell( file ); fseek( file, 0, SEEK_END ); long len = ftell( file ); fseek( file, currPos, SEEK_SET ); return len; } Copyright © Roman Simiński Strona : 50 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — wirtualna tablica FileArray Definicja funkcji składowych FileArray & FileArray::operator = ( char c ) { if( file ) { fputc( c, file ); fflush( file ); } return *this; } FileArray & FileArray::operator [] ( long i ) { if( file ) fseek( file, i, SEEK_SET ); return *this; } FileArray::operator char() { if( file ) fgetc( file ); } Copyright © Roman Simiński Strona : 51 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — wirtualna tablica FileArray Definicja funkcji składowych FileArray tab( "temp.dat" ); if( tab.ready() ) { cout << "Zapisywanie" << endl; for( int i = 0; i < 26; i++ ) { tab[ i ] = 'A' + i; cout << char( 'A' + i ); } tab.operator[](i).operator=( 'A' + i ); cout << endl << "Rozmiar tablicy: " << tab.size() << endl; cout << "Odczytywanie:" << endl; for( int i = 0; i < tab.size(); i++ ) cout << tab[ i ]; } Copyright © Roman Simiński tab.operator[]( i ).operator char(); Strona : 52 Podstawy i języki programowania Język C++ Operatory przeciążone Przeciążanie operatorów — podsumowanie Operatory, które można przeciążać + - * / % ~ & | ^ ! , = < > <= >= ++ -- << >> == != && || += -= /= %= ^= &= |= *= <<= >>= [] () -> ->* new delete Operatory, których przeciążać nie wolno :: .* . ?: Czego nie wolno a co trzeba Nie wolno zmieniać znaczenia operatora określonego dla wbudowanych typów danych. Nie wolno budować nowych operatorów. Przynajmniej jeden argument funkcji operatorowej musi być obiektem jakiejś klasy. Nie wolno zmieniać zdefiniowanych pierwotnie reguł pierwszeństwa operatorów. Musi być zachowana liczba argumentów operatora. Copyright © Roman Simiński Strona : 53