Operatory przeciążone
Część siódma
Niniejsze opracowanie zawiera skrót treści wykładu, lektura tych materiałów nie zastąpi uważnego w nim uczestnictwa. Roman Simiński
siminski@us.edu.pl
www.us.edu.pl/~siminski
Autor Kontakt
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.
int add( int a, int b ) // 1-sza wersja funkcji przeci onej addąż
{
return a + b; }
double add( double a, double b ) // 2-ga wersja funkcji przeci onej addąż
{
return a + b; }
cout << endl << "Dodawanie int :" << add( 1, 1 ); cout << endl << "Dodawanie double :" << add( 1.0, 1.0 );
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.
int addAndPrint( int a, int b ) {
cout << a + b; return a + b; }
Dlaczego przeciążanie nie uwzględnia rezultatu funkcji?
addAndPrint( 10, 20 );
void addAndPrint( int a, int b ) {
cout << a + b; }
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 );
Przeciążanie funkcji — delikatne problemy To wywołanie jest niepoprawne:
printData( 200 );
printData( 3.14 );
To poprawne, leczy być może zaskakujące:
void printData( long num ) {
cout << "Liczba long: " << num << endl; }
void printData( float num ) {
cout << "Liczba float: " << num << endl; }
printData( 3.14 );
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 );
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,
Przeciążanie funkcji — rodzaje dopasowania
Udane dopasowanie
void printData( long num ); void printData( float num ); void printData( double num );
printData( 10L ); printData( 10.5f ); printData( 10.5 );
Dopasowanie nie jest możliwe
printData( "10" );
Dopasowanie jest niejednoznaczne
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,
Ścisła zgodność
Typy parametru aktualnego i formalnego są identyczne:
void printData( int ); void printData( char * ); printData( 0 );
printData( "Napis" );
// Mo na stosować jawne rzutowanie typówż
printData( ( char * )0 );
Wersja zgodna z printData( char * ) Wersja zgodna z printData( int )
Zgodność dzięki awansowaniu
void printData( int ); void printData( short ); void printData( long ); printData( 'a' );
printData( 3.14 );
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( char ); void printData( int );
void printData( unsigned int ); unsigned char uc;
printData( uc );
Wersja zgodna z printData(int), 'a' awansuje do typu int. Brak możliwości awansu – odwołanie niejednoznaczne.
Zgodność dzięki standardowym przekształceniom typów
void printData( int ); void printData( float); printData( 3.14 );
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 ); printData( 3.14 );
Wersja printData( int ), standardowe przekształcenie, nieco dziwne być może... . Dwa możliwe przekształcenia, odwołanie niejednoznaczne.
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.
Od klasy do typu wbudowanego z operatorem konwersji typu: class Integer64 { public: operator int(); . . . }; Integer64 i;
void printData( int ); void printData( float ); printData( i );
Operator przekształcenia obiektu klasy Integer64 do typu int.
Wersja fun( int ), wg. przekształcenia w klasie
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.
Od typu wbudowanego do klasy, z wykorzystaniem konstruktora: class Integer64 { public: Integer64(); Integer64( int i ); . . . };
void printData( Integer64 & num ); printData( 1 );
Działa to również — w sposób analogiczny — w przypadku konwersji pomiędzy dwoma klasami.
Wersja
printData( Integer64 & )
Utworzenie niejawnego obiektu tymczasowego i zainicjowanie go konstruktorem Integer64( int ):
Wywołania z wieloma parametrami
void fun( int, int ); void fun( char *, int ); fun( 0, 'a' );
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.
Wersja fun( int, int ), pełna zgodność dla 0 z int oraz równa zgodność 'a' w obu
wersjach.
void fun( int, int ); void fun( long, long ); int i, j;
fun( i, j );
Nie istnieje najpełniejsze dopasowanie
void fun( int, int );
void fun( double, double ); fun( 'a', 3.14f );
Dwie funkcje posiadają parametr najbardziej zgodny.
Wywołania z parametrami domyślnymi
void printData( int );
void printData( long, int = 0 ); printData( 0, 0 );
printData( 0 ); printData( 10L ); printData( 3.14 );
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.
Wersja zgodna z printData( long, int ) Wersja zgodna z printData( int )
Wersja zgodna z printData( long, int ) Wywołanie niejednoznaczne
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.
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.
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 );
enum DataTypes { INT, LONG, DOUBLE };
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;
} }
printData( INT, &in ); printData( LONG, &ln ); printData( DOUBLE, &dn );
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 (a+bi)·(c+di)=(ac-bd)+(bc+ad)i. ( + )ac bd + a bi + c di ( + )c2 b2 ( + )c2 b2 ( - bc ad) = + i
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; // Cz ć rzeczywistaęś
double imag; // Cz ć urojonaęś
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; }
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.
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 );
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.
Operatory jako funkcje, użycie operatora jako wywołanie funkcji Weźmy pod lupę operator przypisania =
oba argumenty są obiektami klasy;
Jest to istniejący operator z dozwolonego zestawu, nie następuje zmiana reguł pierwszeństwa i liczby argumentów.
Czy takie zastosowanie zgodne jest ustalonymi zasadami? Tak, bowiem:
class Complex {
public: . . .
void operator = ( Complex & z ); . . .
};
Definiujemy funkcje operatorową dla operatora = klasy Complex
Complex z1;
Complex z2( 1, 2 ); z1 = z2;
Operatory jako funkcje, użycie operatora jako wywołanie funkcji, cd. ... Implementacja funkcji operatorowej
Jak to działa?
void Complex::operator = ( Complex & z ) { real = z.real; imag = z.imag; } Complex z1; Complex z2( 1, 2 );
void Complex::operator = ( Complex & z ) {
real = z.real; // Lub setReal( z.getReal() ); imag = z.imag; // Lub setImag( z.getImag() ); } Wywołanie: z1.operator=( z2 ); z1 = z2; real: imag: 1 2
z1
real: imag: 1 2z2
Czy nasz operator przypisania jest dobry i w tym przypadku? Complex z1; Complex z2; complex z3( 1, 2 ); Wywołanie: z1.operator = (
?
); z1 = z2 = z3; real: imag: 1 2z1
real: imag: 1 2z2
z2.operator = ( z3 )Complex & Complex::operator = ( Complex & z ) {
real = z.real; imag = z.imag; return *this;
}
this — w obrębie definicji każdej funkcji
składowej klasy występuje wskaźnik this. Wskazuje on zawsze na obiekt, dla którego została wywołana dana funkcja składowa.
real: imag: 1 2
z3
z1.operator = ( z2.operator = ( z3 ) );Operator przypisania, analizy ciąg dalszy
Complex & Complex::operator = ( const Complex & z )
{
if( &z != this ) { real = z.real; imag = z.imag; } return *this; }
A co gdy napiszemy tak?
z1 = z1;
Zabezpieczenie przed przypisaniem do samego siebie
Operator przypisania kontra konstruktor kopiujący
Complex z1( 1, 2 ); Complex z2 = z1; z2 = z1;
Konstruktor kopiujący Operator przypisania
Przy okazji, umieszczenie const pozwala na:
const Complex a( 0, 0 ); Complex b;
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.
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.
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.
Operator przypisania jest nierozerwalnie związany z klasą i może być definiowany jedynie jako funkcja składowa klasy.
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; // Cz ć rzeczywistaęś
double imag; // Cz ć urojonaęś
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; }
Chcemy dodawać liczby zespolone Complex z1; Complex z2( 2 ); complex z3( 1, 1 ); z1 = z2 + z3; class Complex { public: . . .
Complex operator + ( const Complex & z );
. . . };
Rozszerzamy klasę Complex o przeciążony operator dodawania
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;
}
Jak to działa? Complex z1; Complex z2( 2 ); complex z3( 1, 1 ); z2 + z3; z1 = z2 + z3;
Razem z operatorem przypisania
Complex Complex::operator + ( const Complex & z ) {
return Complex( real + z.real, imag + z.imag );
}
Implementacja, wersja 2-ga
Wywołanie: z2.operator + ( z3 );
Wywołanie: z1.operator = ( z2.operator + ( z3 ) );
Dlaczego rezultat operatora + nie jest referencją?
W C i C++ rezultatem funkcji nie powinien być wskaźnik ani referencja do
zmien-nych automatyczzmien-nych funkcji. Te lokowane są zwykle na stosie i po zakończeniu
Chcemy odejmować liczby zespolone Complex z1; Complex z2( 2 ); complex z3( 1, 1 ); z1 = z2 - z3; class Complex { public: . . .
Complex operator - ( const Complex & z );
. . . };
Rozszerzamy klasę Complex o przeciążony operator odejmowania
Complex Complex::operator - ( const Complex & z ) {
return Complex( real - z.real, imag - z.imag ); }
-Chcemy zmienić znak liczby zespolonej Complex z1; Complex z2( 2, 2 ); z2 = -z1 class Complex { public: . . .
Complex operator - ( const Complex & z );
Complex operator - ();
. . . };
Rozszerzamy klasę Complex o przeciążony operator zmiany znaki („unarny minus”)
Complex Complex::operator - () {
return Complex( - real, - imag ); }
Implementacja jednoargumentowego operatora
-Jak to działa?
z2 = -z1;
z1 = z2 - z3;
Wywołanie: z2.operator = ( z1.operator - () );
Niby jest świetnie, ale...
Complex z; double d; z + d; d + z;
Wywołanie: z.operator + ( Complex( d ) ); 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
oznacza wywołanie funkcji
składowej O.operator#(),
zwykłej operator#( O ).
Dla dwuoargumentowego operatora # oraz obiektów X i Y:
X # Y
oznacza wywołanie funkcji
składowej X.operator#( Y ),
d + z1;
Implementacja operatorów w postaci zwykłych funkcji
Complex operator + ( double num, const Complex & z ) {
return Complex( num + z.getReal(), z.getImag() ); }
Wywołanie: operator + ( d, z1 );
Nieskładowa funkcja operatorowa w akcji
z2 = d + z1; Wywołanie: z2.operator = ( operator + ( d, z1 ) );
class Complex {
. . .
friend Complex operator + ( double num, const Complex & z );
. . . };
Complex operator + ( double num, const Complex & z ) {
return Complex( num + z.real, z.imag );
}
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() );
}
Nieskładowa, zaprzyjaźniona funkcja operatorowa
Aby funkcja nieskładowa miała wygodniejszy dostęp do pól klasy, może się z nią
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 ); }
Wykorzystanie funkcji nieskładowych
z2 = 2 + z3; z4 = z3 + 2; z1 = z2 + z3;
z2.operator = ( operator + ( double, Complex ) ); z4.operator = ( operator + ( Complex, double ) ); z1.operator = ( operator + ( Complex, Complex ) );
A gdyby tak pozostawić tylko jedną funkcję nieskładową?
z2 = 2 + z3; z4 = z3 + 2; z1 = 2 + 3;
z2.operator=( operator+( Complex(double), Complex ) ); z4.operator=( operator+( Complex, Complex(double) ) ); z1.operator=(operator+(Complex(double),Complex(double)));
class Complex {
. . .
friend Complex operator + ( const Complex & z1, const Complex & z2 ); . . .
Problem — dwie wersje operatorów ++ i —―
z1++; ++z1;
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.
class Complex {
public: . . .
Complex & operator ++ (); // Postać przedrostkowa
Complex operator ++ ( int ); // Postać przyrostkowa
. . .
Complex & operator -- (); // Postać przedrostkowa
Complex operator -- ( int ); // Postać przyrostkowa
. . . };
Rozszerzamy klasę Complex o przeciążone operatory ++ i —―
--z2; z2--;
Implementacja operatorów przedrostkowych
Complex & Complex::operator ++ () {
++real; ++imag;
return *this; }
Complex & Complex::operator -- () {
--real; --imag;
return *this; }
Zwiększ część rzeczywistą i urojoną o 1
z1 = ++z2; z1 = --z2;
Wywołanie: z1.operator = ( z2.operator ++ () );
Wywołanie operatorów przedrostkowych
Zmniejsz część rzeczywistą i urojoną o 1 „Oddaj” zmodyfikowany obiekt
„Oddaj” zmodyfikowany obiekt
Implementacja operatorów przyrostkowych
Complex Complex::operator ++ ( int ) { Complex copy; copy = *this; ++real; ++imag; return copy; }
Complex Complex::operator -- ( int ) {
Complex copy( *this ); --real;
--imag;
return copy;
}
Zapamiętaj w copy wartość obiektu przed inkrementacją
z1 = z2++; z1 = z2--;
Wywołanie: z1.operator = ( z2.operator ++ (0) );
Wywołanie operatorów przyrostkowych
Zwróć kopię wartości obiektu z przed inkrementacji
Wywołanie: z1.operator = ( z2.operator -- (0) ); Tu nieco krócej — konstruktor kopiujący
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 ];
Definicja klasy
class FileArray {
public:
FileArray( char * fileName ); ~FileArray();
bool ready() const;
FileArray & operator = ( char c ); FileArray & operator [] ( long ); operator char();
long size() const; private:
FILE * file;
void operator &() {};
}; Nie pozwól pobrać adresu tej tablicy!
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;
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 ); }
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 ); }
cout << endl << "Rozmiar tablicy: " << tab.size() << endl; cout << "Odczytywanie:" << endl;
for( int i = 0; i < tab.size(); i++ ) cout << tab[ i ];
}
tab.operator[](i).operator=( 'A' + i );
Operatory, które można przeciążać + - * / % ~ & | ^ ! , = < > <= >= ++ -- << >> == != && || += -= /= %= ^= &= |= *= <<= >>= [] () -> ->* new delete Operatory, których przeciążać nie wolno
:: .* . ?:
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.