• Nie Znaleziono Wyników

Przecianie operatorw

N/A
N/A
Protected

Academic year: 2021

Share "Przecianie operatorw"

Copied!
53
0
0

Pełen tekst

(1)

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

(2)

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 );

(3)

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.

(4)

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; }

(5)

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 );

(6)

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 );

(7)

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 );

(8)

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,

(9)

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

(10)

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,

(11)

Ś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 )

(12)

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.

(13)

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.

(14)

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.

(15)

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

(16)

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.

(17)

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 ):

(18)

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.

(19)

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

(20)

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.

(21)

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.

(22)

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 );

(23)

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

(24)

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ęś

(25)

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; }

(26)

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.

(27)

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 );

(28)

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.

(29)

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;

(30)

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 2

z2

(31)

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 2

z1

real: imag: 1 2

z2

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 ) );

(32)

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;

(33)

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.

(34)

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ęś

(35)

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; }

(36)

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;

}

(37)

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

(38)

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 ); }

(39)

-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 - () );

(40)

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 ),

(41)

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 ) );

(42)

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ą

(43)

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 ); }

(44)

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 ); . . .

(45)

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--;

(46)

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

(47)

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

(48)

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 ];

(49)

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!

(50)

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;

(51)

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 ); }

(52)

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 );

(53)

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.

Cytaty

Powiązane dokumenty

Szkic ujawnił doskonałą orientację Bargielskiej w tym temacie, przy okazji zaś okazał się niemałą pomocą w wyjaśnieniu niektórych tajemniczych wąt- ków jej wierszy – do

Dokonaj symulacji jednej trajektorii procesu Wienera {W t } na odcinku [0,1000] w taki sposób, »e kolejne obserwacje pojawiaj¡ si¦ w równych odst¦pach czasu co 0.5. Zaznacz te

That when the people of any one of said rebel States shall have formed a constitution of government in conformity with the Constitution of the United States in all respects, framed

Zakończony etap badań i obliczeń, opisany we wcześniejszym rozdziale, jednoznacznie pozwolił ustalić rangę ważności para- metrów pompy wirowo-śmigłowej. Zmienna zastępcza Z

Próbny egzamin ósmoklasisty powinien być przeprowadzany wyłącznie w celu informacyjnym (tj. danie uczniom kolejnej szansy pracy z arkuszem egzaminacyjnym w czasie przeznaczonym

Въздухът, изсмукван от аспиратора, не трябва да се отвежда през тръба, която се използва за отвеждане на дим или за уреди, захранвани с газ или

elektryczne szyby przednie elektryczne szyby tylne elektrycznie sterowana klapa bagażnika elektrycznie ustawiane fotele elektryczny starter kierownica wielofunkcyjna

W artykule podano niektóre realizacje wszechprze- pustowej sekcji drugiego rzędu przy zastosowaniu ogniwa filtru środkowoprzepustowego.. Wybrano te realizacje filtrów