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