PO wyk09 v2

background image

Zaawansowane funkcje

Przeciążanie funkcji wewnętrznych

Przykład

Funkcja nie pobierająca parametrów wywołuje funkcję z
parametrami przekazując do niej wartości przechowywane w
zmiennych klasy. Zawsze starajcie się unikać powielania kodu w
dwóch funkcjach. Powtarzanie takich samych instrukcji w dwóch
(lub więcej) miejscach utrudnia ich modyfikację i często prowadzi
do "rozsynchronizowania" programu.

Kompilator, na podstawie liczby parametrów, decyduje, którą funkcję ma

wykonać.

background image

Wykorzystywanie wartości domyślnych

Przykład

Podobnie jak w przypadku zwykłych funkcji, w przypadku metod klasy można
określić domyślne wartości parametrów, które mają być przyjmowane w
momencie pominięcia któregoś z argumentów.

background image

Wartości domyślne czy przeciążanie funkcji ?

Przeciążanie funkcji stosuje się gdy:

 nie ma żadnej sensownej wartości domyślnej,

 wykorzystuje się różne algorytmy w zależności od liczby parametrów,

 trzeba obsługiwać różne typy w liście parametrów.

Konstruktor domyślny

Jeżeli nie zadeklaruje się jawnie konstruktora klasy to przy każdym tworzeniu
obiektu klasy wywoływany jest konstruktor domyślny, nie pobierający
argumentów i nie wykonujący żadnych operacji.

Konstruktor dostarczany przez kompilator nazywany jest domyślnym, ale
konwencja mówi, że każdy konstruktor nie pobierający argumentów jest
domyślny. Pojawia się tutaj pewne zagmatwanie, zazwyczaj z kontekstu
wynika, o który konstruktor chodzi.

Przeciążanie konstruktorów

Destruktorów przeciążać nie można. Każdy destruktor, z definicji,
tworzony jest tak samo: nazwa klasy poprzedzona znakiem tyldy (
).

Destruktor nie pobiera żadnych parametrów.

Konstruktory, podobnie jak wszystkie inne metody klasy, mogą być
przeciążane. Np. można stworzyć klasę Prostokat posiadającą dwa
konstruktory: pierwszy, pobierający dwa parametry określające wymiary,
drugi, bez parametrów, tworzący prostokąt o wymiarach domyślnych.
Kompilator wybierze konstruktor na podstawie typu i liczby parametrów, tak
jak w przypadku zwykłych funkcji.

background image

Inicjalizacja obiektów

Dotychczas, wartości wewnętrznych zmiennych klasy były ustalane wewnątrz
treści konstruktorów. Jednakże, każdy konstruktor powinien składać się z
dwóch części, inicjalizującej i treści.
Większość zmiennych można zainicjować w dowolnej z tych części, albo
poprzez inicjalizację w części inicjalizującej, albo przez przypisanie wartości w
treści konstruktora.

Po nawiasie zamykającym listę parametrów konstruktora stawiamy
dwukropek. Następnie wpisujemy nazwę zmiennej i w nawiasach podaje się
wyrażenie, którego wartość ma być nadana tej zmiennej. Inicjalizację
zmiennych oddziela się przecinkami.

KOT():

// nazwa konstruktora i

parametry
nJegoWiek (5), // lista inicjalizacji
nJegoWaga (8)
{ }

//treść konstruktora

Przykład:

UWAGA: Referencje i stałe muszą być inicjalizowane, nie można im
przypisywać wartości. W celu nadania im wartości trzeba
wykorzystać metodę podaną powyżej

.

background image

Konstruktor kopiujący

Podobnie jak w przypadku zwykłego konstruktora i destruktora, kompilator
dostarcza również domyślny konstruktor kopiujący. Konstruktor kopiujący jest
wywoływany w momencie tworzenia kopii obiektu danej klasy.
Kiedy obiekt jest przekazywany przez wartość, jako parametr lub ewentualnie
wartość zwracana, to jest wykonywana chwilowa, robocza kopia tego obiektu.
Jeśli jest to obiekt klasy zdefiniowanej przez użytkownika, to jest wywoływany
konstruktor kopiujący tej klasy.
Konstruktor kopiujący ma tylko jeden parametr: referencje do obiektu tej
samej klasy. Dobrze jest deklarować ten parametr jako const, gdyż
konstruktor kopiujący nie ma prawa zmieniać zawartości obiektu.

Przykład

KOT ( const KOT &kotek ) ;

Domyślny (dostarczany przez kompilator) konstruktor kopiujący, wykonuje
kopię każdej zmiennej wewnętrznej obiektu źródłowego i umieszcza w nowym
obiekcie. Jest to bardzo "płytkie" kopiowanie i o ile w przypadku zwykłych
zmiennych będzie działać prawidłowo, to w przypadku np. wskaźników
całkowicie zawiedzie, gdyż skopiowane zostaną adresy zawarte we
wskaźnikach.

Konstruktor kopiujący z klasy KOT
pobiera

stałą

referencję

do

istniejącego obiektu klasy KOT.
Zadaniem tego konstruktora jest
wykonanie w pamięci kopii obiektu
kotek.

background image

Płytkie kopiowanie polega na skopiowaniu wartości zmiennych obiektu
źródłowego do obiektu tworzonego. Jeżeli w obiekcie występują wskaźniki to
w efekcie końcowym otrzymamy dwa obiekty, w których te wskaźniki
wskazują na tę samą pamięć. Wskaźników nie należy kopiować bezpośrednio.
Należy wykonać kopie wartości przechowywanych pod adresami przez nie
wskazywanymi do nowego obszaru pamięci.

Jeżeli klasa KOT będzie zawierać zmienną nJegoWiek wskazującą na wartość
typu int na stercie, to konstruktor kopiujący skopiuje wartość tej zmiennej
(czyli adres w niej zawarty) do zmiennej nJegoWiek obiektu tworzonego. Oba
wskaźniki będą wskazywać na ten sam obszar pamięci.

Oryginalny

KOT

nJegoWie
k

nowy

KOT

nJegoWie
k

5

Sterta

Takie rozwiązanie spowoduje katastrofę, gdy jeden z obiektów zostanie
usunięty z pamięci. Zostanie wtedy wywołany destruktor klasy KOT, który
zwolni zarezerwowaną na stercie pamięć.

background image

Załóżmy, że z pamięci zostanie usunięty oryginalny obiekt KOT. Destruktor
zwolni zarezerwowaną pamięć. Jednak kopia nadal będzie wskazywać na ten
obszar. Jeżeli będzie się próbowało dostać do tej pamięci to program
przestanie działać...

Oryginalny

KOT

nJegoWie
k

nowy

KOT

nJegoWie
k

5

Sterta

Rozwiązaniem tego problemu jest napisanie własnego konstruktora
kopiującego i rezerwacja pamięci we własnym zakresie. Jeżeli pamięć
zostanie zarezerwowana, to wartości z oryginalnego obiektu (w szczególności
te wskazywane przez wskaźniki) mogą zostać do niej skopiowane. Takie
kopiowanie nazywamy głębokim.

background image

Przykład

Deklarowane są dwie zmienne wewnętrzne, każda jako
wskaźnik do wartości typu int. Normalnie przechowywanie w
klasie

zmiennych

typu

int

pod

wskaźnikami

jest

niestosowane.

Tutaj

wykorzystałem

to

tylko

do

zademonstrowania zarządzania danymi na stercie.

Domyślny konstruktor, rezerwuje dla dwóch zmiennych
typu int pamięć na stercie i przypisuje im odpowiednie
wartości.

Konstruktor kopiujący. Zwróćmy uwagę na
parametr rhs. Jest to często spotykana
nazwa parametru konstruktora kopiującego
(z ang. right-hand side - znajdujący się po
prawej stronie).

background image

Zasada działania konstruktora
kopiującego:

Rezerwowana jest pamięć na
stercie.

Przepisywane tam są wartości z
istniejącego obiektu klasy KOT

Parametr rhs to obiekt klasy KOT przekazany
do konstruktora kopiującego jako stała
(const)

referencja

.

Wykorzystujemy

wewnętrzne funkcje rhs.PobierzWiek () i
rhs.PobierzWage () do odczytania wartości
zmiennych

wewnętrznych

nJegoWiek

i

nJegoWaga i przepisania ich do tworzonego
obiektu.

Kiedy jest wywoływany konstruktor kopiujący, to istniejący obiekt klasy
KOT jest przekazywany do niego jako parametr. Do zmiennych
tworzonego obiektu można się odwoływać bezpośrednio, ale do
odczytania zmiennych obiektu rhs trzeba wykorzystać funkcje dostępu.

background image

Ilustracja głębokiego kopiowania

Wartości z istniejącego obiektu są kopiowane do pamięci zarezerwowanej dla
nowego obiektu.

Oryginalny

KOT

nJegoWie
k

nowy

KOT

nJegoWie
k

5

Sterta

5

background image

Tworzony jest obiekt o nazwie
oFilemon. Wypisywany jest jego wiek.

Tworzony jest nowy obiekt oPikus z
wykorzystaniem

konstruktora

kopiującego i obiektu oFilemon.
Gdyby oFilemon był przekazany przez
wartość do funkcji, to również
zostałby

wywołany

konstruktor

kopiujący.

Wiek oFilemon ulega zmianie na 7.
Ponownie wypisujemy wiek obu
kotów. oFilemon ma 7 lat, a oPikus
nadal 6.

Dowodzi to, że obiekty

znajdują się w oddzielnych
obszarach pamięci.

Podczas usuwania obiektów KOT z pamięci automatycznie są wywoływane
destruktory klasy. Za pomocą delete kasowane są obydwa wskaźniki,
nJegoWiek i nJegoWaga, zwalniana jest zajmowana przez nie pamięć. Dla
bezpieczeństwa, wskaźnikom jest nadawana wartość NULL.

Faktycznie, oPikus ma tyle samo
lat co oFilemon (a nie domyślną
wartość 5).

background image

Przeciążanie operatorów

C++ posiada wiele wbudowanych typów danych, takie jak int, float, char
itp. Każdy z nich posiada pewną liczbę określonych operatorów takich jak
dodawanie (+), mnożenie (*) itd. C++ pozwala na dodawanie tych
operatorów do własnych klas.

Przykład

Jak widać, stworzyliśmy całkowicie
bezużyteczną klasę. Jej jedyna
zmienna wewnętrzna jest typu int.
Domyślny konstruktor, nadaje tej
zmiennej wartość 0.
W przeciwieństwie do zwykłej,
zmiennej typu int, obiekt klasy
Licznik nie może zostać ani
zwiększony, ani zmniejszony, nie
można mu nadać żadnej wartości
ani

nic

do

niego

dodać.

Skomplikowane

jest

również

wypisywanie jego wartości.

background image

Funkcja inkrementująca

Przeciążenie operatora przywraca część funkcjonalności utraconej podczas
definiowania własnej klasy takiej jak Licznik.

Przykład

Implementacja operatora ++, została
zmieniona tak, że zwraca aktualny obiekt
za pomocą pośredniego odwołania do
this
. Dzięki temu możemy wykonać
operację przypisania

Zauważ, że wartość zwracana
jest referencją do obiektu klasy
Licznik,

przez

co

unikamy

zbędnego

tworzenia

kopii

obiektu. Jest ona zadeklarowana
jako const, ponieważ wartość
nie powinna być zmieniona
przez funkcje wykorzystującą
ten obiekt.

background image

Przeciążanie operatora przyrostkowego

Zanim zaczniemy przeciążać operatory przyrostkowe i przedrostkowe
musimy poznać różnicę miedzy nimi. W skrócie: operator przedrostkowy
najpierw inkrementuje zmienną, a potem zwraca jej wartość, operator
przyrostkowy odwrotnie, najpierw zwraca wartość, a potem inkrementuje
zmienną.

O ile operator przyrostkowy może zwiększyć wartość i zwrócić obiekt przez
wartość to operator przedrostkowy musi zwrócić wartość przed
inkrementacją. W tym celu trzeba stworzyć pomocniczy obiekt,
przechowujący początkową wartość obiektu przed inkrementacją. Zwrócić
należy ten chwilowy obiekt. Spójrzmy na to wszystko z innej strony. Jeśli
napisze się

a = x++;

i x było równe 5, to po wykonaniu tej instrukcji a będzie równe 5 i x będzie
równe 6. Dzieje się tak dlatego, że zwracamy wartość x i przypisujemy ją do
a. Jeśli x jest obiektem, to jego operator przyrostkowy musi przechować
oryginalną wartość x w obiekcie pomocniczym, zwiększyć x i zwrócić obiekt
pomocniczy.

Należy jednak pamiętać, że zwracając obiekt pomocniczy nie można
go zwrócić przez referencję gdyż jest to obiekt lokalny. Koniecznie
trzeba go zwrócić przez wartość.

background image

Przykład

Zauważmy,

że

deklaracja

operatora

przedrostkowego nie zawiera parametru w
przeciwieństwie do deklaracji operatora
przyrostkowego. Parametr w deklaracji
operatora

przyrostkowego

jest

jedynie

informacją dla kompilatora i nie jest nigdy
wykorzystywany

background image

Operator +

Operator inkrementacji jest operatorem unarnym co oznacza, że operuje
na tylko jednym obiekcie. Operator dodawania (+) jest operatorem
binarnym, działającym na dwóch obiektach. Jak przeciążyć operator
dodawania w klasie Licznik?

Celem jest możliwość zadeklarowania dwóch obiektów klasy Licznik, a
następnie dodania ich tak, jak w przykładzie:

Licznik oJeden, oDwa, oTrzy;
oTrzy = oJeden + oDwa;

Można by napisać funkcję Dodaj (), przekazać do niej obiekt Licznik, dodać
wartości i zwrócić obiekt klasy Licznik jako wynik.

background image

Przykład

Funkcja Dodaj() jako parametr pobiera stałą
referencję do obiektu klasy Licznik, który
będzie dodany do aktualnego obiektu.
Funkcja Dodaj () zwraca obiekt, który będzie
można przypisać do innego obiektu.

background image

Przeciążanie operatora +

W poprzednim programie zdefiniowaliśmy funkcję Dodaj (). Ona działa, lecz
jej użycie jest raczej nienaturalne. Lepszym, bardziej intuicyjnym
rozwiązaniem jest przeciążenie operatora dodawania (+)
Przykład

Niby niewielka zmiana, a program wygląda dużo lepiej i czytelniej.

oTrzy = oJeden + oDwa;

Łatwiej jest napisać:

oTrzy = oJeden.Dodaj (oDwa) ;

ni
ż

background image

Ograniczenia przy przeciążaniu operatorów

Przede wszystkim nie jest możliwe przeciążanie operatorów wbudowanych
typów C++. Nie można zmienić kolejności wykonywania działań i arności
działań (liczby argumentów). Nie można tworzyć nowych operatorów. Próba
stworzenia np. operatora ** dla podnoszenia do potęgi nie powiedzie się.

Co i kiedy przeciążać ?

Przeciążanie operatorów jest często nadużywane przez początkujących
programistów. Próbują oni tworzyć nowe, ciekawe zastosowania dla prostych
operatorów, lecz nieodzownie prowadzi to do zbędnej komplikacji programu i
nieporozumień.
Pewnie, że można przeciążyć operator + tak, aby służył do odejmowania i
zmusić operator * do dodawania, jednak nikt tego nie robi. Większe
niebezpieczeństwo kryje się w pozornie poprawnym w zamierzeniach
przeciążeniu np. operatora + do łączenia znaków w łańcuchy lub / do
dzielenia łańcuchów. Można rozważyć takie rozwiązania ale z rozwagą.
Pamiętajcie, że przeciążanie operatorów ma zwiększać przejrzystość
kodu i łatwość korzystania z niego.


Document Outline


Wyszukiwarka

Podobne podstrony:
PO wyk11 v2
PO wyk10 v2
PO wyk07 v1
Rehabilitacja po endoprotezoplastyce stawu biodrowego
Systemy walutowe po II wojnie światowej
HTZ po 65 roku życia
Zaburzenia wodno elektrolitowe po przedawkowaniu alkoholu
Organy po TL 2
Metoda z wyboru usprawniania pacjentów po udarach mózgu
03Operacje bankowe po rednicz ce 1
Piramida zdrowia po niemiecku
przewoz drogowy po nowelizacji adr
Opieka nad pacjentem po znieczuleniu i operacji
Dzień po dniu

więcej podobnych podstron