Programowanie obiektowe - Instytut Fizyki Uniwersytetu

Transkrypt

Programowanie obiektowe - Instytut Fizyki Uniwersytetu
Szybki wstęp do OOP
na przykładzie C++
(wersja 0.1)
Marcin Kośmider
Instytut Fizyki
Uniwersytet Zielonogórski
[email protected]
8 grudzień 2010
1
SPIS TREŚCI
SPIS TREŚCI
Spis treści
1 Wstęp
3
2 Klasy
4
2.1
Wykorzystanie gotowych klas - klasa string . . . . . . . . . . . . . . . . .
5
2.2
Wykorzystanie gotowych klas - operacje wejścia/wyjścia . . . . . . . . . . 10
2.3
Tworzenie własnych klas i hermetyzacja danych . . . . . . . . . . . . . . . 12
2.4
Pliki nagłówkowe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.5
Pamięć dynamiczna - konstruktor kopiujący i destruktor . . . . . . . . . . 23
3 Dziedziczenie
29
3.1
Konstruktor, destruktor i wywołanie metod klasy bazowej . . . . . . . . . 32
3.2
Dziedziczenie i enkapsulacja . . . . . . . . . . . . . . . . . . . . . . . . . . 35
4 Polimorfizm
38
5 Abstrakcja
42
2
Materiał dystrybuowany bezpłatnie
1
1
WSTĘP
Wstęp
Współczesny świat, w tym także współczesna fizyka, nie może się obejść bez metod i
technik komputerowych. Dobra znajomość tych technik, jak i szerokiej gamy narzędzi,
pozwalają w sposób wygodny i sprawny rozwiązywać i optymalizować problemy oraz
zadania napotykane w toku studiów, życiu codziennym i przyszłej pracy zawodowej.
Program kierunku fizyka w Instytucie Fizyki Uniwersytetu Zielonogórskiego jest dostosowany do tego wymogu, oczywiście w różnym stopniu w zależności od wybranej specjalności. Mimo sporych różnic programowych w zakresie przedmiotów komputerowych
(np. między specjalnością fizyka komputerowa a ekofizyka czy fizyka teoretyczna) część
przedmiotów jest wspólna. Jednym z pierwszych wspólnych przedmiotów jest przedmiot
“Podstawy programowania” prowadzony w II semestrze pierwszego roku studiów.
Program tego przedmiotu zakłada naukę programowania od zupełnych podstaw - od
pojęcia kodu źródłowego, kompilacji, aż po elementy związane z pojęciem klasy. Taki
dobór treści jest spowodowany słabym przygotowaniem uczniów z zakresu programowania na poziomie szkoły średniej. W większości przypadków programowanie realizowane
było w PASCALU, a jeżeli nawet był to język C++, to nie zawierał on żadnych elementów programowania obiektowego. Dlatego też, mimo iż kurs podstaw programowania
trwa cały semestr, to typowa tematyka zawiązana z programowaniem obiektowym najczęściej jest pomijana w trakcie jego trwania (choć zależy to od aktualnego poziomu
grup laboratoryjnych i tempa zdobywania umiejętności). Z drugiej strony programowanie obiektowe stanowi współczesny standard programowania i praktycznie niemożliwe
jest omawianie współczesnych bibliotek, języków programowania czy frameworków w
oderwaniu od pojęć związanych z programowaniem obiektowym i bez zrozumienia jego
podstawowych mechanizmów (jak np. omawiać analizę i prezentację danych w SciPy
czy mówić o tworzeniu GUI bez zrozumienia programowania obiektowego). Studenci
specjalności fizyka komputerowa są w tym wypadku w znacznie lepszej sytuacji ponieważ w ich programie nauczania programowanie obiektowe jest osobnym, kolejnym po
podstawach programowania przedmiotem.
Celem niniejszego opracowania jest szybkie i “łagodne” wprowadzenie do pojęć związanych z programowaniem obiektowym na przykładzie języka C++ tak, aby po jego
lekturze móc zacząć programować obiektowo na poziomie podstawowym. Opracowanie
przeznaczone jest dla każdego kto zna podstawowe elementy języka C/C++ takie jak
deklaracje zmiennych, pętle, warunki, funkcje, tablice czy dynamiczną rezerwację pamięci. Nie jest to materiał z zakresu informatyki, bo unika abstrakcyjnego i formalnego
języka oraz teoretycznego podejścia całościowego do problematyki OOP, nie ma tutaj
zagadnień związanych z UML, z wzorcami projektowymi, z szablonami czy wreszcie nie
jest to opis języka C++ i jego bibliotek.
Osoby zainteresowane poszerzeniem swojej wiedzy w tym zakresie bez problemu znajdą
stosowną literaturę i materiały w internecie.
3
Materiał dystrybuowany bezpłatnie
2
2
KLASY
Klasy
Klasa to jedno z fundamentalnych pojęć w programowaniu obiektowym. Zanim jednak wyjaśnimy czym jest klasa, zastanówmy się jak postrzegamy rzeczywistość i jaki
jest związek naszego postrzegania z programowaniem obiektowym. Obserwując swoje
otoczenie, niejako podświadome widzimy i charakteryzujemy występujące w nim przedmioty. Każdy z otaczających nas przedmiotów jest w jakiś sposób charakterystyczny ma swój kształt, kolor, rozmiar, ma swoje funkcje czy zachowania. Co więcej, bardzo
często w danym przedmiocie jesteśmy w stanie wyróżnić inne przedmioty składowe. Te
przedmioty składowe także mają swoje własne cechy i zachowania. Jako przykład rozważmy żarówkę. Żarówka, np. ta, która właśnie świeci się w lampce na biurku, ma
swój kolor, ma swój kształt, ma konkretną, określoną średnicę gwintu, ma określoną
moc i określone napięcie z jakim może pracować. Co więcej, ma ona również dwa stany
- włączona, wyłączona oraz charakterystyczne zachowania - świecenie i grzanie się. Inny
przykład z naszego otoczenia - pilot od telewizora. Ma on również swój kształt, swój numer seryjny, swój kolor, oraz szereg funkcji - włącz/wyłącz, głośniej/ciszej, przełączanie
kanałów itp.
Wszystkie przedmioty które nas otaczają nazywamy obiektami. Generalizując możemy
powiedzieć, że obiekt to coś konkretnego, coś na co patrzymy, coś co ma swoje unikatowe
cechy, coś co może się jakoś zachowywać (np. włącz/wyłącz). Obiektem będzie żarówka,
pilot od telewizora, samochód zaparkowany na parkingu przed oknem. Jednak analizując
dalej obiekty z naszego otoczenia, możemy wyciągnąć kolejne wnioski. Pewne obiekty
są bardzo podobne (prawie identyczne), inne zaś są zupełnie od siebie różne. Każda
żarówka ma zespół takich samych wielkości i zachowań, które ją opisują. Każda żarówka
ma gwint, każda ma jakiś kolor, każda świeci (poza zepsutą). Każdy pilot od telewizora
ma możliwości włączania i wyłączania odbiornika, zmiany kanałów, regulacji głośności,
każdy ma jakiś kolor i jest “przypisany” do jakiegoś modelu lub marki telewizora. Każdy
obiekt więc należy do jakiegoś typu, który jednoznacznie go odróżnia od obiektów innego
typu. Każda żarówka jest do innych żarówek podobna (np. ma określoną moc), ale
zupełnie nie jest podobna do samochodu czy pilota od telewizora. Obiekty różnych
typów są czymś zupełnie innym. Co więcej, taki typ w przeciwieństwie do obiektów
jest czymś zupełnie abstrakcyjnym, jest pojęciem, a nie konkretną rzeczą. Wykonując
polecenie “podaj mi żarówkę” podajemy konkretną żarówkę, konkretny obiekt (ma kolor,
moc etc.) Nie możemy komuś podać żarówki jako abstrakcyjnego typu. Tak samo nie
możemy powiedzieć w salonie samochodowym “kupuję samochód“. Możemy co najwyżej
powiedzieć, że chcemy kupić ten konkretny samochód, albo chcemy samochód o pewnych,
konkretnych parametrach. Otaczające nas przedmioty są obiektami określonych typów.
Właśnie taki typ w programowaniu nazywamy klasą. Klasa jest pewnym szablonem
opisującym cechy obiektów i ich zachowania. Jednak klasa mówi tylko jakie są to cechy i
zachowania, nie precyzując przy tym wartości tych cech. Klasa żarówka będzie zawierać
cechę - moc, ale nie będzie miała wartości tej mocy. Wartość będą miały już konkretne
obiekty tej klasy - żarówki. Samochód “mały fiat” jako klasa (czyli typ) będzie miał
4
Materiał dystrybuowany bezpłatnie
2.1
Wykorzystanie gotowych klas - klasa string
2
KLASY
pole kolor, ale to konkretny “mały fiat” jest czerwony. Oczywiście pewne cechy mogą
mieć wartość taką samą dla każdego obiektu klasy (np. pojemność silnika “malucha”)
i wtedy są cechami samej klasy (tzw. pola statyczne klasy). Zespół zachowań jest
natomiast wspólny dla każdego obiektu klasy. Każda żarówka świeci, każdy samochód
skręca w lewo, każdy pies szczeka.
Przekładając obserwacje z realnego świata na programowanie powiemy, że klasa jest
pewnym typem, szablonem określającym jakie pola obiekty tej klasy mają oraz określającym zachowania obiektów tej klasy. Pola to po prostu zmienne klasy, a zachowania
to funkcje, które w odróżnieniu od zwykłych funkcji (nie zawierających się w klasie)
nazywamy metodami tej klasy. A więc klasa to nie tylko nowy typ danych, ale to typ
danych wraz z możliwymi zachowaniami tych danych.
Podkreślmy po raz kolejny - klasa to szablon, klasa o danej nazwie jest jedna, klasa
jest pewnym przepisem, natomiast obiekt to konkretna realizacja tej klasy ( mówimy
że obiekt to instancja klasy), obiektów danej klasy może być wiele, ale klasa będzie
zawsze jedna. Klasa to “mały fiat”, a obiekty tej klasy to “mały fiat” sąsiada, czerwony
”mały fiat“ za oknem, klasa to student, ale student Jan Kowalski to już obiekt tej
klasy bo ma konkretne cechy (czyli przykładowe pola tej klasy takie jak imię, nazwisko,
kierunek studiów, grupa mają konkretne wartości, które odróżniają obiekty klasy student
od siebie).
2.1
Wykorzystanie gotowych klas - klasa string
Zanim stworzymy swoją pierwszą klasę zobaczmy jak korzystać z gotowych klas dostępnych w bibliotekach języka. Jako przykład wykorzystamy klasę string. Aby móc
korzystać z klasy string należy dołączyć plik nagłówkowy.
Listing 1:
1 #i n c l u d e < s t r i n g >
2 u s i n g namespace s t d ;
3
4 i n t main ( )
5 {
6 s t r i n g name ;
7 }
Obiekty klasy deklarujemy tak samo samo jak zmienne zwykłych typów. Podajemy
nazwę klasy (jako typ), a później nazwę zmiennej reprezentującej obiekt tej klasy. W
powyższym przykładzie utworzyliśmy obiekt klasy string, obiekt o nazwie name. Jednak
w tym wypadku stworzenie obiektu jest czymś znacznie więcej niż zwykłym zdeklarowaniem zmiennej. W momencie, kiedy tworzymy obiekt jakiejś klasy to zawsze odbywa
się to poprzez wywołanie specjalnej metody zawartej w klasie (metody czyli funkcji).
Metoda nazywa się konstruktorem i wywoływana jest ZAWSZE jako pierwsza w momencie tworzenia klasy. Po co jest konstruktor ? Otóż, czasami w momencie tworzenia
5
Materiał dystrybuowany bezpłatnie
2.1
Wykorzystanie gotowych klas - klasa string
2
KLASY
obiektu danej klasy, chcemy dokładnie określić jak dany obiekt ma być tworzony - np.
jaką mają mieć wartość początkową jego atrybuty (pola klasy), czy ma być stworzony
jako kopia innego obiekty tej klasy, czy może ma odbyć się dynamiczna rezerwacja pamięci, otwarcie pliku i wiele innych czynności. Jednym słowem, konstruktor odpowiada
za skonstruowanie obiektu. Bardzo często klasa dostarcza kilku różnych konstruktorów
odpowiadających możliwościom tworzenia obiektów na podstawie różnych danych początkowych. W powyższym programie w momencie tworzenia obiektu wywoływany jest
konstruktor bezparametrowy, który tworzy pusty obiekt klasy string.
Konstruktory klasy string zostały zebrane w tabeli 1.
Konstruktor
string ( )
s t r i n g ( const s t r i n g & s )
s t r i n g ( c o n s t s t r i n g & s , s i z e t pos , s i z e t n )
s t r i n g ( const char ∗t , s i z e t n )
s t r i n g ( const char ∗ t )
s t r i n g ( s i z e t n , char c )
Opis
Konstruktor bezparametrowy
tworzący pusty obiekt klasy
string
Nowy obiekt klasy string jest
tworzony na podstawie już istniejącego obiektu s
Nowy obiekt klasy string
jest tworzony na podstawie
podstringu już istniejącego
obiektu s. Podstring zaczyna
się na pozycji pos i zawiera n
elementów
Nowy obiekt klasy string jest
tworzony na podstawie n
pierwszych elementów tablicy
znaków t
Nowy obiekt klasy string jest
tworzony na podstawie tablicy znaków t
Nowy obiekt klasy string zawiera n powtórzeń znaku c
Tablica 1: Konstruktory klasy string
Przykłady tworzenia obiektów klasy string:
Listing 2:
1 #i n c l u d e <i o s t r e a m >
2 #i n c l u d e < s t r i n g >
3
4 u s i n g namespace s t d ;
5
6 i n t main ( )
7 {
6
Materiał dystrybuowany bezpłatnie
2.1
Wykorzystanie gotowych klas - klasa string
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
KLASY
s t r i n g s 1 ; // p u s t y s t r i n g
s t r i n g s 2 ( ” j a k i s n a p i s ” ) ; // t o samo co s 2=” j a k i s n a p i s ”
c h a r t [ ] = ” c−s t r i n g ” ; // t a b l i c a znakow z konczacym NULL
// tzw c−s t r i n g
s t r i n g s3 ( t ) ;
// o b i e k t u t w o r z o n y na p o d s t a w i e t a b l i c y znakow
s t r i n g s 4 ( s2 , 6 , 5 ) ;
s t r i n g s5 (10 , ’ a ’ ) ;
cout
cout
cout
cout
<<” s 2
<<” s 3
<<” s 4
<<” s 5
=
=
=
=
”<<s2<<e n d l
”<<s3<<e n d l
”<<s4<<e n d l
”<<s5<<e n d l
;
;
;
;
return 0;
}
wynik programu to:
s2
s3
s4
s5
=
=
=
=
j a k i s napis
c−s t r i n g
napis
aaaaaaaaaa
Jak już zostało to podkreślone, klasa to nie tylko same dane, ale to dane wraz z metodami, które na tych danych pracują. Metody są wspólne dla wszystkich obiektów
danej klasy, jednak wywołuje się je zawsze (z wyjątkiem tzw. metod statycznych) na
rzecz konkretnych obiektów. Jest to dość oczywiste - metody to funkcje pracujące na
konkretnych danych, a to właśnie konkretne dane są tym, co rozróżnia obiekty tej samej
klasy. Jeden człowiek ma na imię Jan a inny Karol, a więc metoda ”podajImie“ musi
wyraźnie wskazywać kto ma to imię podać, a więc musi być wywołana dla konkretnego
obiektu. Jak to zrealizować technicznie ( a właściwie jak to zapisać) zależy od tego czy
pracujemy na obiekcie czy na wskaźniku do obiektu. W pierwszym przypadku odwołanie
do metody lub atrybutu obiektu odbywa się za pomocą znaku ”.“:
string s (” napis ” );
c o u t << s . s i z e ()<< e n d l ;
// o d w o l a n i e do metody s i z e na r z e c z o b i e k t u s
w drugim przypadku poprzez dwa znaki ”->“:
string s (” napis ” );
s t r i n g ∗ s 1=&s ;
c o u t << s1−>s i z e ()<< e n d l ;
// o d w o l a n i e do metody s i z e
// na r z e c z o b i e k t u s 1 ( p o p r z e z w s k a z n i k )
Programowanie z wykorzystaniem gotowych klas to przede wszystkim paca z dokumentacją dotyczącą konkretnych bibliotek i klas. Celem tego opracowania nie jest przegląd
wszystkich standardowych klas wraz z ich metodami, dlatego ograniczymy się do zastosowani kilku metod z klasy string na konkretnym przykładzie. Z całością standardowej dokumentacji zapoznać się można np. pod adresem http://www.cplusplus.com/reference.
7
Materiał dystrybuowany bezpłatnie
2.1
Wykorzystanie gotowych klas - klasa string
2
KLASY
Napiszmy dla przykładu program, który pobierze z klawiatury napis, wypisze informację
o jego długości, następnie wypisze każdą jego literę w nowej linii, a na zakończenie
utworzy nowy obiekt klasy string zawierający podany napis, ale zapisany od końca.
Dodatkowo program sprawdzi czy napis jest palindromem (w tym wypadku porównanie
będzie uwzględniać zarówno wielkość liter jaki i tzw. białe znaki!!!!).
Listing 3:
1 #i n c l u d e <i o s t r e a m >
2 #i n c l u d e <a l g o r i t h m >
3 #i n c l u d e < s t r i n g >
4
5 u s i n g namespace s t d ;
6
7 i n t main ( )
8 {
9
10
s t r i n g s1 , s 2 ;
11
12
c o u t <<” P o d a j j a k i s n a p i s ”<<e n d l ;
13
// czytamy l i n i e
14
g e t l i n e ( cin , s1 ) ;
15
c o u t <<” P o d a l e s n a p i s o d l u g o s c i ”<<s 1 . s i z e ()<< e n d l ;
16
c o u t <<” Z a w i e r a on z n a k i : ”<<e n d l ;
17
f o r ( i n t i =0; i <s 1 . s i z e ( ) ; i ++)
18
c o u t <<s 1 . a t ( i )<< e n d l ;
19
20
// k o p i a o b i e k t u
21
s 2=s 1 ;
22
// o d w r o c e n i e
23
r e v e r s e ( s 2 . b e g i n ( ) , s 2 . end ( ) ) ;
24
c o u t <<” N a p i s od konca t o : ” ;
25
c o u t <<s2<<e n d l ;
26
// sprawdzamy p a l i n d r o m :
27
i f ( s 2==s 1 )
28
c o u t <<” N a p i s j e s t p a l i n d r o m e m ”<<e n d l ;
29
else
30
c o u t <<” N a p i s n i e j e s t p a l i n d r o m e m ”<<e n d l ;
31
return 0;
32 }
W linii 2 dołączamy do naszego programu bibliotekę z algorytmami (część biblioteki
STL), a to po to, aby wykorzystać zawartą tam funkcję reverse (uwaga funkcję a nie
metodę !!!!!) odwracającą zawartość stringa (w ogólności każdego kontenera). W naszym
programie w linii 10 tworzymy dwa puste obiekty klasy string - obiekt o nazwie s1 i obiekt
o nazwie s2. Następnie w linii 14 pobieramy dane z klawiatury i zapisujemy je w obiekcie
s1. Dane odczytujemy za pomocą funkcji getline po to, aby odczytać wszystkie dane
wprowadzone przez użytkownika w danej linii. Zakładamy, że użytkownik poproszony o
wprowadzenie napisu może chcieć wpisać jakieś zdanie zawierające białe znaki (spacja,
tabulator itp). Gdybyśmy skorzystali z formatowanych operacji wejścia/wyjścia
c i n >>s 1 ;
8
Materiał dystrybuowany bezpłatnie
2.1
Wykorzystanie gotowych klas - klasa string
2
KLASY
to wtedy wszystko co zostałoby wprowadzone po znaku spacji zostałoby zignorowane.
Funkcja getline nie interpretuje znaków za wyjątkiem znaku końca linii, a więc wczyta
wszystkie znaki wprowadzone aż do naciśnięcia klawisza Enter. W linii 17 i 18 wykorzystujemy dwie metody z klasy string - size() i at() wywołując je na rzecz obiektu s1.
Pierwsza z nich zwraca długość stringa (a więc mamy pewność, że pętla for przebiegnie
po każdej literze napisu), druga zwraca znak znajdując się na pozycji wskazanej przez
parametr wywołania metody at(). Od linii 21 zaczyna się druga część naszego programu.
Najpierw kopiujemy obiekt s1 jako obiekt s2 wykorzystując przy tym operator przypisania odpowiednio przedefiniowany w klasie string. Następnie korzystając z funkcji revers
odwracamy napis zawarty w obiekcie s2. Ważne jest to, że reverse jest funkcją a nie
metodą - nie jest ona wywoływana na rzecz żadnego obiektu. Przyjmuje ona dwa parametry - tzw. iteratory początku i końca zakresu kontenera który chcemy odwrócić.
Metoda begin() wywołana na rzecz obiektu s2 zwraca iterator (analog do wskaźnika) do
początku stringu s2, a metoda end() do jego końca. Na zakończenie wystarczy sprawdzić czy podany napis jest palindromem. Twórcy klasy string zadbali w tym wypadku o
stosowne przedefiniowanie operatora porównania (==), a więc możemy go użyć zgodnie
z jego przeznaczeniem. Wynik programu jest następujący:
Podaj j a k i s n a p i s
j a k i s napis
P o d a l e s n a p i s o d l u g o s c i 11
Z a w i e r a on z n a k i :
j
a
k
i
s
n
a
p
i
s
N a p i s od konca t o : s i p a n s i k a j
Napis n i e j e s t palindromem
a w przypadku palindromu ”kobyla ma maly bok“ (uwaga na spacje !!!!)
Podaj j a k i s n a p i s
k o b y l a m amaly bok
P o d a l e s n a p i s o d l u g o s c i 15
Z a w i e r a on z n a k i :
k
o
b
y
l
a
m
a
9
Materiał dystrybuowany bezpłatnie
2.2
Wykorzystanie gotowych klas - operacje wejścia/wyjścia
2
KLASY
m
a
l
y
b
o
k
N a p i s od konca t o : ko bylam amal ybok
Napis j e s t palindromem
2.2
Wykorzystanie gotowych klas - operacje wejścia/wyjścia
Jako kolejny przykład wykorzystania gotowych klas i ich obiektów omówmy zagadnienia związane z operacjami wejścia/wyjścia na przykładzie operacji plikowych. Aby móc
korzystać z operacji plikowych dołączamy plik nagłówkowy <fstream >. Jak zwykle
pracę z klasą zaczynamy od zapoznania się z możliwymi konstruktorami. W tym wypadku nie mamy wielkiego wyboru. Obiekty klasy fstream możemy tworzyć albo tylko
poprzez ich deklarację (czyli za pośrednictwem pustego konstruktora), albo podając w
momencie tworzenia obiektu nazwę pliku (w postaci c-stringu !!!!) i tryb dostępu do
pliku (dokładny opis w dokumentacji). Załóżmy, że chcemy odczytać plik o podanej
nazwie (wprowadzonej przez użytkownika) i wyświetlić kilka informacji o tym pliku.
Przykładowy plik tekstowy - dane.txt
To j e s t l i n i a p i e r w s z a
To j e s t l i n i a d r u g a
trzecia
i koniec pliku .
i nasz program
Listing 4:
1 #i n c l u d e <i o s t r e a m >
2 #i n c l u d e <f s t r e a m >
3 #i n c l u d e < s t r i n g >
4
5 u s i n g namespace s t d ;
6
7 i n t main ( )
8 {
9
s t r i n g nazwa ;
10
11
c o u t <<” P o d a j nazwe p l i k u ”<<e n d l ;
12
c i n >>nazwa ;
13
14
f s t r e a m p l i k ( nazwa . c s t r ( ) , f s t r e a m : : i n ) ;
15
16
// j e s l i o b i e k t n i e z o s t a l u t w o r z o n y
10
Materiał dystrybuowany bezpłatnie
2.2
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Wykorzystanie gotowych klas - operacje wejścia/wyjścia
i f ( plik . f a i l ())
{
c o u t <<” B l a d z w i a z a n y z o t w a r c i e m p l i k u
return 1;
}
2
KLASY
! ! ! ! ”<<e n d l ;
// p l i k o t w a r t y a w i e c czytamy dane l i n i a po l i n i i
char b u f f [ 1 0 2 4 ] ;
w h i l e ( ! p l i k . e o f ( ) && p l i k . good ( ) )
{
p l i k . g e t l i n e ( buff ,1024);
c o u t <<” Wczytano l i n i e z a w i e r a j a c a ”<< p l i k . g c o u n t ( )
<<” znakow ”<<e n d l ;
c o u t <<” T r e s c w c z y t a n e j l i n i i t o : ”<<b u f f <<e n d l <<e n d l ;
}
plik . close ();
return 0;
}
wynik:
P o d a j nazwe p l i k u
dane . t x t
Wczytano l i n i e z a w i e r a j a c a 23 znakow
To j e s t l i n i a p i e r w s z a
Wczytano l i n i e z a w i e r a j a c a 20 znakow
To j e s t l i n i a d r u g a
Wczytano l i n i e z a w i e r a j a c a 8 znakow
trzecia
Wczytano l i n i e z a w i e r a j a c a 15 znakow
i koniec pliku .
Do linii 14 właściwie nie pojawia się nic nowego. W linii 14 definiujemy obiekt o nazwie plik, który jest obiektem klasy fstream. W tym wypadku używamy konstruktora,
który wymaga podania nazwy pliku w postaci wskaźnika do tablicy znaków zakończonej
znakiem NULL (czyli po prostu c-stringa). W naszym programie nazwa pliku została
pobrana z klawiatury i zapisana w obiekcie klasy string. Jednak klasa ta definiuje metodę
c str(), która wywołana na rzecz obiektu klasy string zwraca jego elementy w postaci
c-stringa. Drugi parametr konstruktora określa typ dostępu do pliku - w tym wypadku
fstream::in co oznacza, że plik zostanie otwarty do odczytu. Kolejnym niezbędnym elementem jest sprawdzenie czy udało się otworzyć plik zgodnie z tym co zostało przesłane
w konstruktorze (linia 17). Metoda fail() (wywołana na rzecz obiektu plik w tym wypadku) zwraca czy bit błędu jest ustawiony czy nie. Jeżeli wszystko poszło pomyślnie
można rozpocząć pracę z plikiem. Aby właściwie przeczytać dane z pliku tekstowego
(bez pomijania np. białych znaków) użyjemy nieformatowanych operacji wejścia/wyj11
Materiał dystrybuowany bezpłatnie
2.3
Tworzenie własnych klas i hermetyzacja danych
2
KLASY
ścia. Metoda getline() czyta z pliku jedną linię (jako linię znaków) i przeczytane dane
umieszcza w podanej tablicy znaków. Drugi parametr tej metody to maksymalny rozmiar bufora (tablicy w której dane będą zapisywane). Czytanie z pliku linia po linii
trwać będzie tak długo, dopóki nie dotrzemy do końca pliku (metoda eof() zwraca true
jeśli napotkano koniec pliku) lub nie wystąpił jakiś błąd (metoda good() zwraca wartość
true jeśli nie wystąpił błąd w operacjach wejścia/wyjścia). Jako dodatkową informację
wyświetlamy na ekranie ilość przeczytanych elementów (linia 29).Metoda gcount zwraca
ilość przeczytanych bajtów w trakcie ostatniej wykonanej operacji wejścia/wyjścia. W
naszym wypadku operacje to czytanie całej linii z pliku, a więc gcount zwraca ilość
znaków w linii, a dokładniej o jeden więcej ze względu na znak nowej linii.
2.3
Tworzenie własnych klas i hermetyzacja danych
Programowanie polega na rozwiązywaniu realnych problemów (postrzeganych obiektowo), a więc wcześniej czy później każdy spotka się z koniecznością stworzenia własnych
klas stosownych do rozwiązania zadanego problemu. Deklaracja klasy zawiera deklarację
jej pól oraz metod i jest informacją dla kompilatora jak postępować z obiektami danej
klasy. Aby stworzyć własną klasę używamy słówka kluczowego class.
c l a s s ComplexNumber
{
// t u t a j j e s t c i a l o k l a s y
};
Bardzo ważne jest to, że zamykający nawias } zakończony jest znakiem ”;“ !!!! Przy
okazji omawiania tworzenia własnych klas należy wspomnieć o standardach związanych
z nazewnictwem. Wprawdzie standardy te nie stanowią formalnego wymogu języka
programowania, ale niewątpliwie ułatwiają programowanie i wpływają na jakość kodu
źródłowego. Oczywiście standardów jest kilka, jednak na potrzeby tego opracowania
przyjmiemy następujące reguły:
• Nazwy klas piszemy z dużej litery. Jeżeli nazwa składa się z kilku wyrazów, to
każdy z nich piszemy dużą literą np. Numbers, ComplexNumbers, Person, DowodOsobisty.
• Nazwy zmiennych piszemy od małej litery. Jeżeli nazwa składa się z kliku wyrazów,
to każdy z nich piszemy od dużej litery (za wyjątkiem pierwszego) np: suma, kolor,
mainWindow.
• Nazwy metod podlegają tym samym regułom co nazwy zmiennych.
• Stałe piszemy dużymi literami
12
Materiał dystrybuowany bezpłatnie
2.3
Tworzenie własnych klas i hermetyzacja danych
2
KLASY
Powyższy standard nosi nazwę ”camel“ i jest jednym z najpopularniejszych (np. język
Java, framework QT). Jak łatwo zauważyć standardu tego nie stosuje np. biblioteka
STL (klasa string, vector etc.)
W przypadku tworzenia własnych klas należy bardzo wyraźnie odróżnić deklarację klasy
od jej definicji. Deklaracja mówi czym klasa jest - jakie ma pola i jakie metody jednak
nie zawiera konkretnej implementacji tych metod. W języku C++ wszystkie deklaracje
klas powinny się znajdować w plikach nagłówkowych ( o rozszerzeniu .h), a ich definicje
w plikach o rozszerzeniu cpp (znowu wspomniana zasada jest pewną konwencją, choć
czasami jest wymogiem formalnym - np. QT). Na razie jednak dla prostoty nie będziemy
dokonywać takiego rozbicia.
Kontynuujmy tworzenie klasy ComplexNumber reprezentującej liczby zespolone. Po
pierwsze musimy się zastanowić jakie pola klasa będzie miała. W tym przypadku sytuacja jest oczywista - dwa pola typu double reprezentujące część rzeczywistą i urojoną
liczby zespolonej. Drugi etap projektowania klasy to wyróżnienie konstruktorów, a więc
sposobów w jaki obiekty klasy mogą być tworzone. Konstruktor to po prostu metoda
o nazwie takiej samej jak nazwa klasy, metoda która nic nie zwraca. Jeżeli sami nie
napiszemy żadnego konstruktora, to domyślnie tworzone są dwa - konstruktor bezparametrowy i konstruktor kopiujący. Pierwszy nie robi nic, drugi ma za zdanie utworzyć
obiekt danej klasy na podstawie przesłanego obiektu tej samej klasy,a więc po prostu
ma wykonać kopię obiektu. Zagadnienie konstruktora kopiującego zostanie omówione w
jednym z kolejnych podrozdziałów.
Listing 5:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
c l a s s ComplexNumber
{
double r e , im ;
// k o n s t r u k t o r y
// b e z p a r a m e t r o w y u s t a w i a j a c y w a r t o s c i r e i im na 0
ComplexNumber ( )
{
re =0.0;
im = 0 . 0 ;
}
// k o n s t r u k t o r z dwoma p a r a m e t r a m i − im i r e
ComplexNumber ( double r e , double im )
{
t h i s −>r e=r e ;
t h i s −>im=im ;
}
};
W drugi konstruktorze tworzone są dwie zmienne lokalne re i im. Nazwy tych zmiennych są dokładnie takie same jak nazwy pól klasy, czyli je przekrywają. Dlatego chcąc
ustawić wartość pola klasy musimy dokładnie określić, że chodzi nam o zmienne klasy
13
Materiał dystrybuowany bezpłatnie
2.3
Tworzenie własnych klas i hermetyzacja danych
2
KLASY
a nie lokalne zmienne tej metody, a więc musimy użyć wskaźnika this. Wskaźnik ten
wskazuje nam na ten konkretny obiekt na którym właśnie pracujemy. Oczywiście można
by unikać przekrywania nazw i zmienne deklarowane w metodzie nazwać np. a i b, jednak wtedy czytelność kodu dramatycznie się zmniejsza - sama deklaracja metody nie
daje nam żadnej informacji co ta metoda będzie robić. Wykorzystajmy naszą klasę (w
tym wypadku listing przedstawia cały plik o nazwie zespolone.cpp).
Listing 6:
1 #i n c l u d e <i o s t r e a m >
2 u s i n g namespace s t d ;
3
4
5 c l a s s ComplexNumber
6 {
7
double r e , im ;
8
9
// k o n s t r u k t o r y
10
// b e z p a r a m e t r o w y u s t a w i a j a c y w a r t o s c i r e i im na 0
11
ComplexNumber ( )
12
{
13
re =0.0;
14
im = 0 . 0 ;
15
}
16
17
// k o n s t r u k t o r z dwoma p a r a m e t r a m i − im i r e
18
ComplexNumber ( double r e , double im )
19
{
20
t h i s −>r e=r e ;
21
t h i s −>im=im ;
22
}
23 } ;
24
25 i n t main ( )
26 {
27
ComplexNumber z1 ( 1 0 , 5 ) ;
28
return 0;
29 }
Próba kompilacji przebiegnie niepomyślnie, kompilator zwróci następujące błędy:
g++ z e s p o l o n e . cpp
z e s p o l o n e . cpp : I n f u n c t i o n i n t main ( ) :
z e s p o l o n e . cpp : 1 8 : e r r o r : ComplexNumber : : ComplexNumber ( d o u b l e , d o u b l e )
is private
z e s p o l o n e . cpp : 2 7 : e r r o r : w i t h i n t h i s c o n t e x t
W linii 27 próbujemy tworzyć obiekt za pomocą konstruktora przyjmującego dwa parametry. Oczywiście taki konstruktor jest w naszej klasie (linia 18), jednak kompilator
zgłosił błąd polegający na próbie dostępu do prywatnych składowych klasy. W języku
C++ wszystkie składniki domyślnie są prywatne.
14
Materiał dystrybuowany bezpłatnie
2.3
Tworzenie własnych klas i hermetyzacja danych
2
KLASY
W tym momencie spotykamy drugie fundamentalne pojęcie związane z programowaniem obiektowym - hermetyzacja danych lub inaczej enkapsulacja. Znowu i tym
razem najprościej będzie odwołać się do obserwacji przedmiotów (obiektów) w naszym
otoczeniu. Weźmy dla przykładu telewizor. Telewizor jest jakimś obiektem posiadającym mnóstwo atrybutów (oporniki, procesory, tranzystory, kineskop, ....) oraz szereg
zachowań (włącz, wyłącz, głośniej, ciszej, .....). Jednak my jako użytkownicy nie mamy
dostępu bezpośredniego do wszystkich elementów telewizora. Mimo, iż praca telewizora polega na zmianie stanu jego atrybutów (np. przyłożenie odpowiedniego napięcia
do poszczególnych elementów) to my jako użytkownicy tej zmiany dokonujemy poprzez
przygotowany dla nas interfejs (np. pilot), nie mamy dostępu do elementów składowych
i nie manipulujemy nimi (nie przełączamy ”kabelków”). Możemy więc powiedzieć, że
elementy wewnętrzne telewizora są jego składowymi prywatnymi. Wszystkie elementy
prywatne są niedostępna spoza danej klasy - a więc elementy wewnątrz telewizora wpływają na swoje działanie bezpośrednio, ale my znajdując się na zewnątrz zmiany te wprowadzamy tylko za pośrednictwem publicznych (czyli ogólnodostępnych) metod. Ogólna
reguła dobrego programowania mówi, że wszystkie pola powinny być prywatne, a dostęp
do nich powinien być realizowany za pośrednictwem publicznych metod.
Idea tego podejścia jest znacznie głębsza niż zwykła dbałość o to, aby użytkownik czegoś nie zepsuł. Enkaspulacja zapewnia separację konkretnej realizacji kodu od interfejsu
(czyli mówiąc ogólnie metody pracy z klasą). Programista projektujący klasę może dostarczyć metodę string getFirstName() i dla użytkownika tej klasy nie ma znaczenia jak
zmienna firstName jest reprezentowana w klasie. Może się zdarzyć tak, że na początku
to będzie zwykła zmienna typu string, ale z biegiem czasu metoda ta zwróci wynik wyszukiwania w rozproszonej bazie danych. Dzięki enkaspsulacji, program bazujący na
takiej klasie nie ulegnie zmianie bo zmieni się tylko prywatna część klasy, ale interfejs
pozostał ten sam. Można to porównać z samochodem. Nie ma znaczenia jaki silnik jest
”pod maską“ sposób zmiany biegów, hamowania, przyspieszana i skręcania jest taki sam
- działanie silnika jest ”prywatne“, a obsługa samochodu to publiczny interfejs.
W języku C++ mamy trzy modyfikatory dostępu do danych - public, private i protected. Pierwszy oznacza, że wszystkie pola i metody znajdujące się za jego deklaracją
będą publiczne (a więc dostępne spoza klasy), drugi oznacza, że będą prywatne (a więc
dostępne tylko z poziomu tej klasy) i to jest domyślny tryb dostępu, trzeci tryb dostępu
ma znaczenie w dziedziczeniu i zostanie omówiony później. Zmieńmy w takim razie
naszą klasę, tak aby konstruktory były metodami publicznymi.
Listing 7:
1
2
3
4
5
6
7
8
c l a s s ComplexNumber
{
double r e , im ;
public :
// k o n s t r u k t o r y
// b e z p a r a m e t r o w y u s t a w i a j a c y w a r t o s c i r e i im na 0
ComplexNumber ( )
15
Materiał dystrybuowany bezpłatnie
2.3
9
10
11
12
13
14
15
16
17
18
19
20
Tworzenie własnych klas i hermetyzacja danych
2
KLASY
{
re =0.0;
im = 0 . 0 ;
}
// k o n s t r u k t o r z dwoma p a r a m e t r a m i − im i r e
ComplexNumber ( double r e , double im )
{
t h i s −>r e=r e ;
t h i s −>im=im ;
}
};
Wszystkie metody i ewentualne pola począwszy od linii 5 traktowane będą jako publiczne, chyba, że zmienimy dostęp na private. Aby móc przetestować naszą klasę
powinniśmy mieć dostęp do informacji o części rzeczywistej i urojonej liczby zespolonej.
W obecnej postaci pola te są prywatne i nie możemy się do nich odwołać. Dlatego dodamy do klasy metody dostępowe set (ustawiające pola) i get (pobierające wartości tych
pól). Metody takie nazywa się muttator i accesor.
Listing 8:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
c l a s s ComplexNumber
{
double r e , im ;
public :
// k o n s t r u k t o r y
// b e z p a r a m e t r o w y u s t a w i a j a c y w a r t o s c i r e i im na 0
ComplexNumber ( )
{
re =0.0;
im = 0 . 0 ;
}
// k o n s t r u k t o r z dwoma p a r a m e t r a m i − im i r e
ComplexNumber ( double r e , double im )
{
t h i s −>r e=r e ;
t h i s −>im=im ;
}
// metody d o s t e p o w e do p o l
v o i d s e t R e ( double r e )
{
t h i s −>r e=r e ;
}
v o i d s e t I m ( double im )
{
16
Materiał dystrybuowany bezpłatnie
2.3
31
32
33
34
35
36
37
38
39
40
Tworzenie własnych klas i hermetyzacja danych
2
KLASY
t h i s −>r e=im ;
}
double g e t R e ( )
{ return re ;}
double g e t I m ( )
{ r e t u r n im ; }
};
Następnym elementem tworzenia klasy jest dodawanie do niej kolejnych funkcjonalności
- obliczenie modułu liczby zespolonej, operacje matematyczne na liczbach czy operacje
porównujące dwie liczby. W języku C++ mamy możliwość przedefiniowania operatorów
tak, aby praca z obiektami tworzonych klas była jak najbardziej wygodna i intuicyjna.
Problem ten jednak wymaga szerszego omówienia i w tym opracowaniu nie zostanie
omówiony. Naszą klasę uzupełnimy o dwie metody - metodę obliczającą moduł liczby
zespolonej i metodę zwracającą sumę dwóch liczb zespolonych (ale bez przedefiniowania
operatora dodawania).
Moduł liczby zespolonej to po prostu
|z| =
p
re2 + im2 .
Jednak, aby móc skorzystać z pierwiastkowania musimy do naszego programu dołączyć
plik nagłówkowy <cmath >. W przypadku dodawania sprawa jest oczywista:
(a + ib) + (c + id) = (a + c) + i(b + d)
jednak do rozstrzygnięcia pozostaje inna kwestia - jak zapisać wynik dodawania. Możliwości są co najmniej dwie - albo modyfikujemy liczbę zespoloną (w sensie obiektu klasy
ComplexNumber) do której dodajemy inną liczbę, albo w wyniku operacji otrzymujemy
nową liczbę zespoloną (nowy obiekt klasy ComplexNumber). Oczywiste jest, że jedynym
rozsądnym rozwiązaniem jest opcja druga.
Listing 9:
1 #i n c l u d e <i o s t r e a m >
2 #i n c l u d e <cmath>
3
4 u s i n g namespace s t d ;
5
6
7 c l a s s ComplexNumber
8 {
9
double r e , im ;
10
11
public :
12
// k o n s t r u k t o r y
13
// b e z p a r a m e t r o w y u s t a w i a j a c y w a r t o s c i r e i im na 0
17
Materiał dystrybuowany bezpłatnie
2.3
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
Tworzenie własnych klas i hermetyzacja danych
2
KLASY
ComplexNumber ( )
{
re =0.0;
im = 0 . 0 ;
}
// k o n s t r u k t o r z dwoma p a r a m e t r a m i − im i r e
ComplexNumber ( double r e , double im )
{
t h i s −>r e=r e ;
t h i s −>im=im ;
}
// metody d o s t e p o w e do p o l
v o i d s e t R e ( double r e )
{
t h i s −>r e=r e ;
}
v o i d s e t I m ( double im )
{
t h i s −>r e=im ;
}
double g e t R e ( )
{ return re ;}
double g e t I m ( )
{ r e t u r n im ; }
// metdoy a b s i add
double a b s ( )
{ r e t u r n s q r t ( r e ∗ r e + im ∗ im ) ; }
ComplexNumber add ( ComplexNumber &w)
{
r e t u r n ComplexNumber (w . g e t R e ()+ r e , w . g e t I m ()+ im ) ;
}
};
i n t main ( )
{
ComplexNumber
z1 ( 1 , 1 ) , z2 ( 4 , 2 ) , z3 ;
z3=z1 . add ( z2 ) ;
c o u t <<”Nowa l i c z b a z e s p o l o n a : ”<<e n d l ;
c o u t <<” r e = ”<<z3 . g e t R e ()<< e n d l ;
c o u t <<” im = ”<<z3 . g e t I m ()<< e n d l ;
c o u t <<” Modul l i c z b y z e s p o l o n e j z3 = ”<<z3 . a b s ()<< e n d l ;
18
Materiał dystrybuowany bezpłatnie
2.4
67
68
Pliki nagłówkowe
2
KLASY
return 0;
}
Prezentowany powyżej sposób zapisu klasy polegający na pomieszaniu deklaracji z definicjami jest bardzo złym nawykiem. Po pierwsze, kod takiej klasy jest nieczytelny.
Użytkownik klasy chciałbym mieć szybki dostęp do informacji na temat dostępnych metod i pól bez konieczności ”przedzierania“ się przez kod źródłowy. Po drugie, dobrze
napisana klasa będzie wykorzystywana wielokrotnie, a więc umieszczenie deklaracji, definicji i przykładowego wykorzystania w jednym pliku powoduje brak takiej możliwości.
Po trzecie klasy często wykorzystują inne pliki nagłówkowe (przykładowo w naszym
przypadku cmath). To definicja klasy powinna określać co ma być dołączone aby klasa
działała. Użytkownik nie musi, co więcej najczęściej nie chce, wiedzieć co dołącza klasa.
Użytkownik chce dołączyć bibliotekę z daną klasą i móc z niej korzystać. I po czwarte
przy powyższym zapisie niemożliwe jest używanie zmiennych i metod statycznych ponieważ muszą one być definiowane poza obszarem deklaracji klasy.
2.4
Pliki nagłówkowe
Dla przykładu rozpatrzmy klasę Employee reprezentującą pracownika. Załóżmy dla
prostoty, że każdy pracownik ma tylko trzy atrybuty go opisujące - imię, nazwisko i
stanowisko pracy. Każde z tych pól jest po prostu ciągiem znaków (stringiem). Zgodnie
z tym co powiedzieliśmy wcześniej, pola te będą polami prywatnymi, a dostęp do nich
realizować będą publiczne metody.
Zacznijmy od deklaracji klasy z wyróżnieniem interfejsu. Deklarację zapisujemy w pliku
nagłówkowym (z rozszerzeniem .h), najczęściej o nazwie takiej, jak nazwa klasy, która
jest w nim deklarowana. W naszym przypadku plik employee.h wyglądałby następująco:
Listing 10:
1 // e m p l o y e e . h
2
3 #i f n d e f EMPLOYEE H
4 #d e f i n e EMPLOYEE H
5
6 #i n c l u d e < s t r i n g >
7
8
9 c l a s s Employee
10 {
11
// p u b l i c i n t e r f a c e
12
public :
13
Employee ( ) { } ;
14
Employee ( s t d : : s t r i n g f i r s t N a m e , s t d : : s t r i n g secondName ,
15
std : : s t r i n g position ) ;
16
Employee ( s t d : : s t r i n g f i r s t N a m e , s t d : : s t r i n g secondName ) ;
17
19
Materiał dystrybuowany bezpłatnie
2.4
Pliki nagłówkowe
2
KLASY
18
std : : s t r i n g getFirstName ( ) ;
19
s t d : : s t r i n g getSecondName ( ) ;
20
std : : s t r i n g getPosition ( ) ;
21
22
void s e t P o s i t i o n ( std : : s t r i n g p o s i t i o n ) ;
23
24
// p r i v a t e d a t a
25 p r i v a t e :
26
s t d : : s t r i n g f i r s t N a m e , secondName , p o s i t i o n ;
27
28 } ;
29 #e n d i f
Plik nagłówkowy wymaga pewnego omówienia. Po pierwsze, musimy zabezpieczyć się
przed sytuacją, że nasz plik będzie dołączany wielokrotnie do programu. Taka sytuacja spowodowałaby ponowną deklarację klasy o tej samej nazwie, a więc kompilator
zgłosiłby błąd. Standardowym rozwiązaniem jest ”obłożenie“ deklaracji klasy przez dyrektywy pre-procesora (linia 3,4 i 28). Rozwiązanie sprowadza się do sprawdzenia czy
została zdefiniowana zmienna o nazwie EMPLOYEE H. Jeżeli nie, to jest ona definiowana i dołączana jest deklaracja klasy, jeżeli natomiast zmienna była zdeklarowana, to
cały kod pomiędzy #ifndef i #endif zostanie pominięty. Drugi problem to używanie
using namespace w plikach nagłówkowych. Sprawa jest prosta - nie używać nigdy,
ponieważ może to prowadzić do wielu, bardzo trudnych do wyłapania błędów (patrz
google: namespace pollution). Plik nagłówkowy nie zawiera konkretnego kodu metod
(za wyjątkiem pustego konstruktora) a jedynie deklaruje jakie metody w klasie są, jaki
jest do nich dostęp oraz jakie pola klasa posiada. Dość często przyjętym standardem
jest wypisywanie najpierw publicznych metod i pól (choć dyskusyjne jest czy takie być
powinny), a później dopiero prywatnych składowych klasy. Powód jest bardzo prosty informacja jak używać obiektów klasy jest dostępna od razu bez konieczności przewijania
kolejnych ekranów z kodem.
Następnym krokiem jest zaimplementowanie wypisanych metod w pliku z kodem (np. o
rozszerzeniu cpp). W naszym przypadku plik nazywa się employee.cpp.
Listing 11:
1 // e m p l o y e e . cpp
2 #i n c l u d e ” e m p l o y e e . h ”
3
4 using std : : s t r i n g ;
5
6 Employee : : Employee ( s t r i n g f i r s t N a m e , s t r i n g secondName , s t r i n g p o s i t i o n )
7 {
8
t h i s −>f i r s t N a m e=f i r s t N a m e ;
9
t h i s −>secondName=secondName ;
10
t h i s −>p o s i t i o n=p o s i t i o n ;
11 }
12
13 Employee : : Employee ( s t r i n g f i r s t N a m e , s t r i n g secondName )
14 {
20
Materiał dystrybuowany bezpłatnie
2.4
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Pliki nagłówkowe
2
KLASY
t h i s −>f i r s t N a m e=f i r s t N a m e ;
t h i s −>secondName=secondName ;
p o s i t i o n=” b r a k p r z y d z i a l u ” ;
}
s t r i n g Employee : : g e t F i r s t N a m e ( )
{
return firstName ;
}
s t r i n g Employee : : getSecondName ( )
{
r e t u r n secondName ;
}
s t r i n g Employee : : g e t P o s i t i o n ( )
{
return p o s i t i o n ;
}
v o i d Employee : : s e t P o s i t i o n ( s t r i n g p o s i t i o n )
{
t h i s −>p o s i t i o n=p o s i t i o n ;
}
Po pierwsze musimy dołączyć plik nagłówkowy z deklaracją naszej klasy. W odróżnieniu
od plików dołączanych z standardowych bibliotek, nasze pliki nagłówkowe dołączane są
poprzez ujęcie nazwy pliku w podwójny cudzysłów. Jeżeli plik znajduje się w jakimś
podkatalogu to musimy podać w jego nazwie ścieżkę (względną lub bezwzględną) do
tego pliku np. #include ”src/employee.h“.
Drugi problem znowu wiąże się z przestrzeniami nazw - nie używamy całej przestrzeni
nazw, wystarczy używać tych nazw które są wykorzystywane w naszym kodzie w danym
pliku (tutaj std::string - linia 4).
Trzecia sprawa, na którą należy zwrócić uwagę, to definicja metod. Nazwa metody MUSI
być poprzedzona nazwą klasy (i dwoma znakami ::). Jest to wyraźne określenie do jakiej
klasy metoda należy. W jednym pliku źródłowym może znajdować się wiele deklaracji
metod należących do różnych klas, co więcej, część tych metod może mieć takie same
nazwy i przyjmować takie same parametry. Kompilator musi wiedzieć dokładnie do
jakiej klasy dana metoda należy. W przykładach prezentowanych w poprzednich podrozdziałach nie było problemu z jednoznacznością przynależności metod, bo były one
definiowane wewnątrz klasy. Znowu łatwa do zapamiętania jest analogia z naszego otoczenia. Nie możemy zdefiniować czynności ”włącz“, możemy co najwyżej powiedzieć jak
włączyć światło, jak włączyć telewizor czy pralkę, a więc definiujemy czynność przynależną do konkretnego typu urządzenia (ale nie obiektu, bo każdy obiekt tego typu włącza
się tak samo).
Kolejną ważną sprawą jest przetestowanie napisanej klasy. Problem testowania i pisa21
Materiał dystrybuowany bezpłatnie
2.4
Pliki nagłówkowe
2
KLASY
nia aplikacji jest bardzo obszernym zagadnieniem, które zdecydowanie wykracza poza
to opracowanie. Dla naszych celów warto stosować prostą zasadę - kompilować kod
programu po wprowadzeniu każdego nowego elementu kodu (nowej metody, nowej funkcjonalności w metodzie, nowych danych etc.) Nikt nie jest w stanie napisać programu od
początku do końca bez żadnego błędu (oczywiście istnieje silna zależność długość kodu profesjonalizm programisty), a szukanie błędu w 100 czy 10000 linii kodu jest po prostu
koszmarem. Dlatego też, zanim wykorzystamy naszą klasę przetestujmy czy nie zawiera
ona błędów.
g++ −c e m p l o y e e . cpp
Ważne jest aby dodać do g++ opcję -c. Opcja ta powoduje, że plik źródłowy jest
kompilowany, ale nie następuje proces linkowania. Bez opcji -c kompilator zasygnalizuje
nam błąd braku funkcji main, a więc braku punktu startowego aplikacji.
Po kompilacji, w katalogu powinien znajdować się plik z rozszerzeniem .o. Na zakończenie wystarczy zastosować naszą klasę w jakimś konkretnym przypadku:
Listing 12:
1 // p r z y k l a d . cpp
2 #i n c l u d e ” e m p l o y e e . h ”
3 #i n c l u d e <i o s t r e a m >
4
5 u s i n g namespace s t d ;
6
7 i n t main ( )
8 {
9
10
Employee p1 ( ” Jan ” , ” K o w a l s k i ” , ” p r o g r a m i s t a ” ) ;
11
12
c o u t <<p1 . g e t F i r s t N a m e ()<< e n d l ;
13
c o u t <<p1 . getSecondName()<< e n d l ;
14
c o u t <<p1 . g e t P o s i t i o n ()<< e n d l ;
15
16
c o u t <<e n d l <<e n d l ;
17
Employee p2 ( ” K a r o l ” , ” J a n k o w s k i ” ) ;
18
c o u t <<p2 . g e t F i r s t N a m e ()<< e n d l ;
19
c o u t <<p2 . getSecondName()<< e n d l ;
20
c o u t <<p2 . g e t P o s i t i o n ()<< e n d l ;
21
22
return 0;
23 }
Jeżeli wcześniej skompilowaliśmy klasę z opcją -c to teraz wystarczy dołączyć skompilowany plik (ten z rozszerzeniem .o):
g++ p r z y k l a d . cpp e m p l o y e e . o
lub możemy też ponownie skompilować wszystkie pliki źródłowe:
g++ p r z y k l a d . cpp e m p l o y e e . cpp
22
Materiał dystrybuowany bezpłatnie
2.5
Pamięć dynamiczna - konstruktor kopiujący i destruktor
2.5
2
KLASY
Pamięć dynamiczna - konstruktor kopiujący i destruktor
Bardzo często zachodzi potrzeba dynamicznej rezerwacji pamięci w trakcie tworzenia
obiektu. Mimo, iż wskaźniki nie stanowią żadnego specjalnie skomplikowanego narzędzia, to w praktyce okazuje się, że podczas nauki, praca z nimi nastręcza najwięcej problemów. Jako przykład rozważmy klasę Tablica, która służy do przechowywania liczb
typu double. Oczywiście w praktycznych zastosowaniach pisanie takiej klasy mija się z
celem (jest STL i np. klasa vector), ale problem ten idealnie nadaje się do omówienia
pracy na wskaźnikach w przypadku klas.
Zacznijmy od deklaracji klasy:
Listing 13:
1 // a r r a y . h
2 #i f n d e f ARRAY H
3 #d e f i n e ARRAY H
4
5 c l a s s Array
6 {
7
public :
8
Array ( in t s i z e ) ;
9
˜ Array ( ) ;
10
v o i d s e t E l e m e n t ( i n t i , double v a l ) ;
11
void c l e a r ( ) ;
12
double g e t E l e m e n t ( i n t i ) ;
13
double getMaximum ( ) ;
14
double getMinimum ( ) ;
15
double g e t S i z e ( ) ;
16
17
private :
18
double ∗ t a b ;
19
int size ;
20 } ;
21 #e n d i f
Pojawiła się po raz pierwszy nowa metoda specjalna zwana destruktorem (linia 9). Destruktor to metoda, która jest wywoływana zawsze gdy obiekt danej klasy przestaje
istnieć (np. po zakończeniu funkcji). Destruktor to metoda o nazwie takiej jak nazwa
klasy, ale poprzedzony znakiem tyldy. Destruktor nie przyjmuje i nie zwraca żadnych
argumentów. Rolą destruktora jest posprzątanie po obiekcie, co najczęściej ma formę
zwolnienia dynamicznie zarezerwowanej pamięci lub zwolnienia innych zasobów (np. zamknięcie plików). Reszta metod w powyższym kodzie jest oczywista i nie wymaga
tłumaczenia.
Kolejnym krokiem jest zdefiniowanie metod:
Listing 14:
1
// a r r a y . cpp
23
Materiał dystrybuowany bezpłatnie
2.5
Pamięć dynamiczna - konstruktor kopiujący i destruktor
2
KLASY
2 #i n c l u d e ” a r r a y . h ”
3
4 Array : : Array ( in t s i z e )
5 {
6
t h i s −>s i z e=s i z e ;
7
t a b=new double [ s i z e ] ;
8
clear ();
9 }
10
11 A r r a y : : ˜ A r r a y ( )
12 {
13
delete [ ] tab ;
14 }
15
16 v o i d A r r a y : : c l e a r ( )
17 {
18
f o r ( i n t i =0; i <s i z e ; i ++)
19
tab [ i ]=0.0;
20 }
21
22 v o i d A r r a y : : s e t E l e m e n t ( i n t i , double v a l )
23 {
24
i f ( i <0 | | i >=s i z e )
25
throw ”Wrong A r r a y I n d e x ” ;
26
t a b [ i ]= v a l ;
27 }
28
29 d ouble A r r a y : : g e t E l e m e n t ( i n t i )
30 {
31
i f ( i <0 | | i >=s i z e )
32
throw ”Wrong A r r a y I n d e x ” ;
33
return tab [ i ] ;
34 }
35 d ouble A r r a y : : getMaximum ( )
36 {
37
double max=t a b [ 0 ] ;
38
f o r ( i n t i =1; i <s i z e ; i ++)
39
i f ( t a b [ i ]>max )
40
max=t a b [ i ] ;
41
r e t u r n max ;
42 }
43
44 d ouble A r r a y : : getMinimum ( )
45 {
46
double min=t a b [ 0 ] ;
47
f o r ( i n t i =1; i <s i z e ; i ++)
48
i f ( t a b [ i ]<min )
49
min=t a b [ i ] ;
50
r e t u r n min ;
51 }
52
53 d ouble A r r a y : : g e t S i z e ( )
54 {
24
Materiał dystrybuowany bezpłatnie
2.5
55
56
Pamięć dynamiczna - konstruktor kopiujący i destruktor
2
KLASY
return s i z e ;
}
Konstruktor pobiera informacje o rozmiarze tablicy i na jej podstawie dynamicznie rezerwuje stosowny obszar pamięci, a następnie zeruje elementy tablicy poprzez wywołanie
metody clear(). Destruktor (linia 11) zwalnia zarezerwowany w konstruktorze obszar
pamięci. Jest to niezwykle ważne ponieważ w momencie zniszczenia obiektu klasy Array, informacja o tym gdzie znajduje się przydzielona dynamicznie pamięć jest tracona,
zniszczenie obiektu nie powoduje jej zwolnienia, a więc mamy zarezerwowaną pamięć,
do której nie mamy dostępu. Kolejną nowością pojawiającą się w kodzie programu jest
wyrzucenie wyjątku. Metody naszej klasy dbają o to, aby nie odnosić się do elementów spoza tablicy. Jeżeli użytkownik spróbuje odnieść się do komórki tablicy spoza jej
zakresu, program wyrzuci wyjątek i przestanie działać (linia 25 i 32).
Przykład :
Listing 15:
1 // t a b l i c a . cpp
2 #i n c l u d e <i o s t r e a m >
3 #i n c l u d e ” a r r a y . h ”
4
5 u s i n g namespace s t d ;
6
7 main ( )
8 {
9
Array t a b l i c a ( 1 0 ) ;
10
f o r ( i n t i =0; i <10; i ++)
11
t a b l i c a . setElement ( i , i ) ;
12
13
f o r ( i n t i =0; i <12; i ++)
14
c o u t << t a b l i c a . g e t E l e m e n t ( i )<< e n d l ;
15
16
return 0;
17 }
kompilacja:
g++
a r r a y . cpp t a b l i c a . cpp
wynik programu:
0
1
2
3
4
5
6
7
8
25
Materiał dystrybuowany bezpłatnie
2.5
Pamięć dynamiczna - konstruktor kopiujący i destruktor
2
KLASY
9
t e r m i n a t e c a l l e d a f t e r t h r o w i n g an i n s t a n c e o f ’ c h a r c o n s t ∗ ’
Aborted
Pętla wyświetlająca elementy tablicy przekracza jej zakres i program przestaje działać
w wyniku wyrzucenia i nieobsłużenia wyjątku.
Przy omawianiu konstruktorów wspomnieliśmy, że jeżeli sami nie zdefiniujemy żadnego
konstruktora to automatycznie są tworzone dwa konstruktory - bezparametrowy i kopiujący. Jeżeli deklarujemy jakikolwiek konstruktor (za wyjątkiem kopiującego), to domyślnie tworzony jest tylko konstruktor kopiujący. Rolą konstruktora kopiującego jest
utworzenie obiektu klasy, na podstawie innego obiektu tej samej klasy. Domyślnie jest
to realizowane poprzez kopiowanie wartości pól. Skoro kopiowana jest wartość pól, to
w przypadku gdy pole jest wskaźnikiem, kopiowany jest adres przechowywany w zmiennej a nie wartość na jaką on wskazuje. W praktyce oznacza to, że dwa obiekty będą
wskazywać na ten sam obszar pamięci.
Listing 16:
1 #i n c l u d e <i o s t r e a m >
2 #i n c l u d e ” a r r a y . h ”
3
4 u s i n g namespace s t d ;
5
6 main ( )
7 {
8
Array t a b l i c a ( 1 0 ) ;
9
f o r ( i n t i =0; i <10; i ++)
10
t a b l i c a . setElement ( i , i ) ;
11
12
// t 2 b e d z i e k o p i a t a b l i c a
13
Array t2 ( t a b l i c a ) ;
14
15
t a b l i c a . setElement (0 ,100);
16
c o u t <<t 2 . g e t E l e m e n t (0)<< e n d l ;
17
18
19
return 0;
20 }
Zmiany w obiekcie tablica będą widoczne również w obiekcie t2. Jednak to nie koniec
problemów. Wynik działania tego programu będzie wyglądał mniej więcej tak:
100
∗∗∗ g l i b c d e t e c t e d ∗∗∗ . / a . o u t : d o u b l e f r e e o r c o r r u p t i o n ( t o p ) : 0 x09b6b008 ∗∗∗
======= B a c k t r a c e : =========
/ l i b / t l s / i 6 8 6 /cmov/ l i b c . s o . 6 [ 0 x17b0d1 ]
/ l i b / t l s / i 6 8 6 /cmov/ l i b c . s o . 6 [ 0 x17c7d2 ]
/ l i b / t l s / i 6 8 6 /cmov/ l i b c . s o . 6 ( c f r e e +0x6d ) [ 0 x 1 7 f 8 a d ]
/ u s r / l i b / l i b s t d c ++.s o . 6 ( Z d l P v+0x21 ) [ 0 x a b 8 6 f 1 ]
/ u s r / l i b / l i b s t d c ++.s o . 6 ( ZdaPv+0x1d ) [ 0 xab874d ]
26
Materiał dystrybuowany bezpłatnie
2.5
Pamięć dynamiczna - konstruktor kopiujący i destruktor
2
KLASY
. / a . o u t [ 0 x8048a72 ]
. / a . o u t [ 0 x8048951 ]
/ l i b / t l s / i 6 8 6 /cmov/ l i b c . s o . 6 ( l i b c s t a r t m a i n +0x e 6 ) [ 0 x126b56 ]
. / a . o u t [ 0 x80487d1 ]
======= Memory map : ========
00110000 −0024 e000 r−xp 00000000 0 8 : 0 3 553359 / l i b / t l s / i 6 8 6 /cmov/ l i b c − 2 . 1 0 . 1 . s o
0024 e000 −0024 f 0 0 0 −−−p 0013 e000 0 8 : 0 3 553359 / l i b / t l s / i 6 8 6 /cmov/ l i b c − 2 . 1 0 . 1 . s o
0024 f 0 0 0 −00251000 r−−p 0013 e000 0 8 : 0 3 553359 / l i b / t l s / i 6 8 6 /cmov/ l i b c − 2 . 1 0 . 1 . s o
00251000 −00252000 rw−p 00140000 0 8 : 0 3 553359 / l i b / t l s / i 6 8 6 /cmov/ l i b c − 2 . 1 0 . 1 . s o
00252000 −00255000 rw−p 00000000 0 0 : 0 0 0
00309000 −0032 d000 r−xp 00000000 0 8 : 0 3 551417 / l i b / t l s / i 6 8 6 /cmov/ l i b m − 2 . 1 0 . 1 . s o
0032 d000 −0032 e000 r−−p 00023000 0 8 : 0 3 551417 / l i b / t l s / i 6 8 6 /cmov/ l i b m − 2 . 1 0 . 1 . s o
0032 e000 −0032 f 0 0 0 rw−p 00024000 0 8 : 0 3 551417 / l i b / t l s / i 6 8 6 /cmov/ l i b m − 2 . 1 0 . 1 . s o
00539000 −0053 a000 r−xp 00000000 0 0 : 0 0 0
[ vdso ]
00 a00000 −00 ae6000 r−xp 00000000 0 8 : 0 3 852161 / u s r / l i b / l i b s t d c ++.s o . 6 . 0 . 1 3
00 ae6000 −00 a e a 0 0 0 r−−p 000 e6000 0 8 : 0 3 852161 / u s r / l i b / l i b s t d c ++.s o . 6 . 0 . 1 3
00 aea000 −00 aeb000 rw−p 000 ea000 0 8 : 0 3 852161 / u s r / l i b / l i b s t d c ++.s o . 6 . 0 . 1 3
00 aeb000 −00 a f 2 0 0 0 rw−p 00000000 0 0 : 0 0 0
00 c58000 −00 c73000 r−xp 00000000 0 8 : 0 3 524539 / l i b / l d − 2 . 1 0 . 1 . s o
00 c73000 −00 c74000 r−−p 0001 a000 0 8 : 0 3 524539 / l i b / l d − 2 . 1 0 . 1 . s o
00 c74000 −00 c75000 rw−p 0001 b000 0 8 : 0 3 524539 / l i b / l d − 2 . 1 0 . 1 . s o
00 de4000 −00 e00000 r−xp 00000000 0 8 : 0 3 524345 / l i b / l i b g c c s . s o . 1
00 e00000 −00 e01000 r−−p 0001 b000 0 8 : 0 3 524345 / l i b / l i b g c c s . s o . 1
00 e01000 −00 e02000 rw−p 0001 c000 0 8 : 0 3 524345 / l i b / l i b g c c s . s o . 1
08048000 −08049000 r−xp 00000000 0 8 : 0 6 7446599 /home/ u s e r 1 / a . o u t
08049000 −0804 a000 r−−p 00001000 0 8 : 0 6 7446599 /home/ u s e r 1 / a . o u t
0804 a000 −0804 b000 rw−p 00002000 0 8 : 0 6 7446599 /home/ u s e r 1 / a . o u t
09 b6b000 −09b8c000 rw−p 00000000 0 0 : 0 0 0
[ heap ]
b7600000−b7621000 rw−p 00000000 0 0 : 0 0 0
b7621000−b7700000 −−−p 00000000 0 0 : 0 0 0
b7767000−b7769000 rw−p 00000000 0 0 : 0 0 0
b7784000−b7787000 rw−p 00000000 0 0 : 0 0 0
b f e 9 0 0 0 0 −b f e a 5 0 0 0 rw−p 00000000 0 0 : 0 0 0
[ stack ]
Aborted
Program wykonał się poprawnie (wypisało na ekranie liczbę 100), jednak na samym
końcu programu wystąpił jakiś błąd związany z pamięcią. Koniec programu oznacza
zniszczenie zmiennych występujących w funkcji main. W przypadku naszego programu
powinno nastąpić zniszczenie dwóch obiektów klasy Array - obiektu tablica i t2. Zanim
jednak obiekty przestaną istnieć, nastąpi wywołanie destruktora na rzecz każdego z tych
obiektów. Destruktor zwalania przydzieloną pamięć na podstawie adresu, który jest
przechowywany w zmiennej tab. I tutaj właśnie tkwi problem. Domyślny konstruktor
kopiujący w obiekcie t2 ustawił pole tab na ten sam obszar pamięci co pole tab w obiekcie
tablica. Znajduje to swoje potwierdzenie w wypisaniu elementów t2 po ich zmianie
w obiekcie tablica. A więc w momencie, kiedy wywołany został destruktor obiektu
tablica, zarezerwowana pamięć została zwolniona. Następnie, likwidowany jest obiekt
t2 i uruchamiany jego destruktor. Tym razem destruktor działający w obiekcie t2 chce
zwolnić pamięć wskazywaną przez wskaźnik tab, ale jest to ta sama pamięć, która została
zwolniona w destruktorze obiektu tablica. Nie można zwolnić niezarezerwowanej pamięci
i właśnie to jest powodem pojawiającego się błędu.
27
Materiał dystrybuowany bezpłatnie
2.5
Pamięć dynamiczna - konstruktor kopiujący i destruktor
2
KLASY
Rozwiązanie opisanego problemu jest jedno - jeżeli w klasie korzystamy z wskaźników,
ZAWSZE sami piszemy konstruktor kopiujący. Zmodyfikujmy naszą klasę dodając konstruktor kopiujący (pamiętajmy, że zmiany dokonać trzeba zarówno w pliku array.h jak
i array.cpp):
Listing 17:
1
2
3
4
5
6
7
A r r a y : : A r r a y ( A r r a y &a r r a y )
{
s i z e=a r r a y . g e t S i z e ( ) ;
t a b=new double [ s i z e ] ;
f o r ( i n t i =0; i <s i z e ; i ++)
t a b [ i ]= a r r a y . g e t E l e m e n t ( i ) ;
}
Ponowne uruchomienie naszego programu da na ekranie wartość 0, czyli faktycznie mamy
kopię obiektu, a nie kopię wskaźnika.
Z konstruktorem kopiującym, a właściwie z każdą metodą, która pobiera referencję do
jakiegoś obiektu, wiąże się kwestia zapewnienia braku zmian w obiekcie. Kiedy tworzymy kopię obiektu chcemy mieć pewność, że obiekt ”wzorcowy“ nie ulegnie podczas
tej operacji zmianie. Pierwszym rozwiązaniem jest przesyłanie do metod obiektów poprzez wartość (kopie),jednak sposób ten jest zarówno czasochłonny jaki i niepotrzebnie
zużywa pamięć. Drugi sposób to przesłanie obiektu przez referencję i użycie słowa kluczowego const zapewniającego, że referencja dotyczy stałego obiektu, a więc obiektu w
którym nic zmienić nie możemy(pamiętajmy, że zmianę trzeba wprowadzić i w deklaracji,
i w definicji).
A r r a y : : A r r a y ( c o n s t A r r a y &a r r a y )
Jednak tym razem kompilacja się nie powiedzie:
a r r a y . cpp : I n copy c o n s t r u c t o r A r r a y : : A r r a y ( c o n s t A r r a y &):
a r r a y . cpp : 1 3 : e r r o r : p a s s i n g c o n s t A r r a y a s t h i s argument
of double Array : : getSize () d i s c a r d s q u a l i f i e r s
a r r a y . cpp : 1 6 : e r r o r : p a s s i n g c o n s t A r r a y a s t h i s argument
of double Array : : getElement ( i n t ) d i s c a r d s q u a l i f i e r s
Problem tkwi w metodach getSize() i getElement(int) wywoływanych na rzecz obiektu
przekazanego do konstruktora. W deklaracji metody wyraźnie wskazaliśmy, że obiekt
przekazany do konstruktora ma pozostać niezmienny, jednak w samym konstruktorze
wywołujemy na rzecz tego obiektu metody, a te mogą zmienić stan tego obiektu (choćby
tylko hipotetycznie), a więc niejako próbujemy oszukiwać. W tym wypadku musimy
wyraźne wskazać, że wymienione metody nie zmieniają stanu obiektu na rzecz którego
są wywoływane. Wskazanie takie odbywa się poprzez użycie słowa kluczowego const
zaraz za nazwą Metody (i znowu pamiętajmy o pliku .h i .cpp)
double getElement ( i n t i ) const ;
double g e t S i z e ( ) const ;
28
Materiał dystrybuowany bezpłatnie
3
3
DZIEDZICZENIE
Dziedziczenie
Dziedziczenie to kolejne fundamentalne pojęcie w programowaniu obiektowym. Technika ta, tak jak i pozostałe koncepcje programowania obiektowego wynika bezpośrednio
z analizy tego jak postrzegamy otaczający nas świat. Aby zrozumieć ideę dziedziczenia,
dla przykładu rozważmy obiekty jakimi są samochody. Od razu widzimy, że samochody
są różne i to nie tylko pod względem wyglądu i cech, ale również pod względem funkcjonalności - jedne mają abs inne nie, jedne mają klimatyzację inne nie. Niejako intuicyjnie
wiemy, że wszystkie auta należą do jednego typu bazowego, który określa zespół takich
samych zachowań i cech. Każdy samochód ma kierownicę, każdy ma silnik, każdy ma
skrzynie biegów. Podobną analizę można wykonać praktycznie dla każdych otaczających
nas przedmiotów.
Przekładając obserwacje na programowanie powiemy, że możemy wyróżnić klasę bazową
z której innej klasy dziedziczą. A więc klasy dziedziczące (potomne) mają wszystkie cechy klasy bazowej, ale dodatkowo mogą definiować swoje pola, swoje metody jak również
modyfikować zachowania odziedziczone z klasy bazowej. Taki podział i rozplanowanie
klas jest właśnie jednym z fundamentalnych zagadnień przy projektowaniu aplikacji. Z
zespołu klas należy wyróżnić fundamentalne klasy bazowe oraz klasy pochodne dziedziczące z tak wyróżnionych klas.
Przypomnijmy sobie klasę Employee:
Listing 18:
1 // e m p l o y e e . h
2
3 #i f n d e f EMPLOYEE H
4 #d e f i n e EMPLOYEE H
5
6 #i n c l u d e < s t r i n g >
7
8
9 c l a s s Employee
10 {
11
// p u b l i c i n t e r f a c e
12
public :
13
Employee ( ) { } ;
14
Employee ( s t d : : s t r i n g f i r s t N a m e , s t d : : s t r i n g secondName ,
15
std : : s t r i n g position ) ;
16
Employee ( s t d : : s t r i n g f i r s t N a m e , s t d : : s t r i n g secondName ) ;
17
18
std : : s t r i n g getFirstName ( ) ;
19
s t d : : s t r i n g getSecondName ( ) ;
20
std : : s t r i n g getPosition ( ) ;
21
22
void s e t P o s i t i o n ( std : : s t r i n g p o s i t i o n ) ;
23
24
// p r i v a t e d a t a
29
Materiał dystrybuowany bezpłatnie
3
DZIEDZICZENIE
25 p r i v a t e :
26
s t d : : s t r i n g f i r s t N a m e , secondName , p o s i t i o n ;
27
28 } ;
29 #e n d i f
Klasa ta reprezentuje zwykłego pracownika. Jednak od razu widzimy, że pracownicy
w firmie są różni. Są zwykli pracownicy, są szefowie działów, są kierownicy. Każdy
z nich jest pracownikiem, każdy z nich ma imię, nazwisko oraz stanowisko w pracy,
jednak są między nimi również duże różnice np. w wynagrodzeniach, w nagrodach czy
ilości podwładnych. Pierwszym etapem rozwoju programu będzie stworzenie nowej klasy
Manager dziedziczącej z klasy Employee i rozszerzającej jej funkcjonalność. Dodatkowo
dodamy do klasy Employee metodę info() zwracającą string z opisem danego obiektu.
Najpierw w pliku nagłówkowym employee.h dodajemy w części publicznej deklarację
metody info:
std : : s t r i n g info ( ) ;
Kod metody dodajemy do pliku employee.cpp
Listing 19:
1 // e m p l o y e e . cpp
2 #i n c l u d e ” e m p l o y e e . h ”
3
4 using std : : s t r i n g ;
5
6 Employee : : Employee ( s t r i n g f i r s t N a m e , s t r i n g secondName , s t r i n g p o s i t i o n )
7 {
8
t h i s −>f i r s t N a m e=f i r s t N a m e ;
9
t h i s −>secondName=secondName ;
10
t h i s −>p o s i t i o n=p o s i t i o n ;
11 }
12
13 Employee : : Employee ( s t r i n g f i r s t N a m e , s t r i n g secondName )
14 {
15
t h i s −>f i r s t N a m e=f i r s t N a m e ;
16
t h i s −>secondName=secondName ;
17
p o s i t i o n=” b r a k p r z y d z i a l u ” ;
18 }
19
20 s t r i n g Employee : : g e t F i r s t N a m e ( )
21 {
22
return firstName ;
23 }
24
25 s t r i n g Employee : : getSecondName ( )
26 {
27
r e t u r n secondName ;
28 }
29
30
Materiał dystrybuowany bezpłatnie
3
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
DZIEDZICZENIE
s t r i n g Employee : : g e t P o s i t i o n ( )
{
return p o s i t i o n ;
}
v o i d Employee : : s e t P o s i t i o n ( s t r i n g p o s i t i o n )
{
t h i s −>p o s i t i o n=p o s i t i o n ;
}
s t r i n g Employee : : i n f o ( )
{
s t r i n g tmp ;
tmp=” I m i e : ”+f i r s t N a m e+” \n ”+” N a z wi s k o : ”+secondName+” \n ”
+” S t a n o w i s k o : ”+p o s i t i o n ;
r e t u r n tmp ;
}
Następny krok to stworzenie klasy Manager jako klasy dziedziczącej z klasy Employee i
rozszerzającej tą klasę o pole employees. Pole to reprezentować będzie liczbę pracowników podległych danemu kierownikowi..
Listing 20:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// manager . h
#i f n d e f MANAGER H
#d e f i n e MANAGER H
#i n c l u d e ” e m p l o y e e . h ”
c l a s s Manager : p u b l i c Employee
{
public :
Manager ( s t d : : s t r i n g f i r s t N a m e , s t d : : s t r i n g secondName ,
std : : s t r i n g p o s i t i o n , i n t employees ) ;
private :
i n t employees ;
};
#e n d i f
W linii 7 deklarujemy klasę Manager jako klasę dziedziczącą z klasy Employee. Dziedziczenie oznaczone jest znakiem ”:“. Przy dziedziczeniu klasa potomna (w tym wypadku
Manager) określa jak dziedziczy publiczne składniki klasy bazowej. W naszym wypadku słowo kluczowe public poprzedzające nazwę klasy Employee mówi, że wszystkie
publiczne składniki klasy Employee mają być publiczne w klasie Manager, a więc będzie do nich dostęp poza obszarem klasy Manager. Oznacza to, że można wykonać na
obiekcie klasy Manager publiczne metody z klasy Employee takie jak np. info() czy
getFirstName().
31
Materiał dystrybuowany bezpłatnie
3.1
3.1
Konstruktor, destruktor i wywołanie metod klasy bazowej
3
DZIEDZICZENIE
Konstruktor, destruktor i wywołanie metod klasy bazowej
W momencie tworzenia obiektu danej klasy, najpierw wykonywany jest jeden z jej konstruktorów. W przypadku klas dziedziczących wykonane muszą zostać dwa konstruktory
- ten z klasy potomnej i ten z klasy bazowej. Zawsze jako pierwszy jest wykonywany konstruktor z klasy bazowej, a dopiero potem konstruktor z klasy potomnej. Jeżeli chcemy
wywołać w klasie potomnej konstruktor klasy bazowej inny niż bezparametrowy (bo ten
jest uruchamiany domyślnie) to po znaku ”:“ wywołujemy konstruktor klasy bazowej z
stosownymi parametrami (tzw. lista inicjalizacyjna).
Listing 21:
1 // manager . cpp
2 #i n c l u d e ” manager . h ”
3
4 using std : : s t r i n g ;
5
6 Manager : : Manager ( s t r i n g f i r s t N a m e , s t r i n g secondName ,
7
s t r i n g p o s i t i o n , i n t employees )
8
: Employee ( f i r s t N a m e , secondName , p o s i t i o n )
9
// l i s t a i n i c j a l i z a c y j n a
10 {
11
t h i s −>e m p l o y e e s=e m p l o y e e s ;
12 }
W naszym przykładzie lista inicjalizacyjna zawiera wywołanie konstruktora klasy Employee z trzema obiektami klasy string - firstName, secondName i position. Obiekty te
są parametrami pobieranymi przez konstruktor klasy Manager. Sam konstruktor klasy
Manager ustawia tylko pole employees, pole które zostało dodane w klasie Manager.
Przetestujmy obie klasy
Listing 22:
1
// t e s t . cpp
2 #i n c l u d e ” e m p l o y e e . h ”
3 #i n c l u d e ” manager . h ”
4 #i n c l u d e <i o s t r e a m >
5
6 u s i n g namespace s t d ;
7
8 main ( )
9 {
10
11
Employee p1 ( ” Jan ” , ” K o w a l s k i ” , ” p r o g r a m i s t a ” ) ;
12
13
c o u t <<p1 . i n f o ()<< e n d l ;
14
15
Manager m1( ” K a r o l ” , ” J a n k o w s k i ” , ” s t a r s z y p r o g r a m i s t a ” , 1 0 ) ;
16
c o u t <<m1 . i n f o ()<< e n d l ;
17 }
32
Materiał dystrybuowany bezpłatnie
3.1
Konstruktor, destruktor i wywołanie metod klasy bazowej
3
DZIEDZICZENIE
Po uruchomieniu na ekranie zostaną wyświetlone informacje o pracownikach:
I m i e : Jan
Nazwisko : Kowalski
Stanowisko : programista
Imie : Karol
Nazwisko : Jankowski
Stanowisko : s t a r s z y programista
Metoda info() jest zdeklarowana w klasie Employee, a więc nie wyświetla dodatkowych
danych dostępnych w klasie Manager. Kolejnym krokiem będzie przedefiniowanie metody info() w klasie Manager. W tym celu po pierwsze w sekcji metod publicznych w
pliku nagłówkowym manager.h dodajemy deklarację metody info():
std : : s t r i n g info ( ) ;
Po drugie modyfikujemy plik źródłowy manager.cpp:
Listing 23:
1 #i n c l u d e ” manager . h ”
2 #i n c l u d e <s s t r e a m >
3
4 using std : : s t r i n g ;
5 using std : : ostrin gstream ;
6
7 Manager : : Manager ( s t r i n g f i r s t N a m e , s t r i n g secondName , s t r i n g p o s i t i o n ,
8
i n t employees )
9
: Employee ( f i r s t N a m e , secondName , p o s i t i o n )
10 {
11
t h i s −>e m p l o y e e s=e m p l o y e e s ;
12 }
13
14 s t r i n g Manager : : i n f o ( )
15 {
16
ostringstream buffor ;
17
b u f f o r <<Employee : : i n f o ()<<” \ n P o d w l a d n i : ”<<e m p l o y e e s ;
18
return buffor . s t r ( ) ;
19 }
W linii 2 dołączyliśmy plik nagłówkowy z biblioteką umożliwiającą wykonywanie operacji strumieniowych na stringach. Metoda info w klasie Employee pracowała tylko na
obiektach klasy string. Klasa string przedefiniowuje operator dodawania, a więc łączenie stringów nie nastręczało żadnych problemów. W przypadku metody info w klasie
Manager do stringów musimy dołączyć liczbę typu int, a więc nie możemy po prostu
użyć operatora dodawania. Konwersji liczby na reprezentujący ją ciąg znaków można
dokonać na dwa sposoby - albo wykorzystując funkcję sprintf z języka C, albo właśnie
poprzez operacje strumieniowe na stringach. Skoro niniejsze opracowanie dotyczy programowania obiektowego to oczywisty jest wybór drugiej opcji. Klasa ostringstream
(linia 14) reprezentuje formatowany, wyjściowy strumień związany z stringiem. Praca z
33
Materiał dystrybuowany bezpłatnie
3.1
Konstruktor, destruktor i wywołanie metod klasy bazowej
3
DZIEDZICZENIE
obiektem tej klasy właściwie nie różni się niczym od pracy z obiektem cout czy obiektami plikowym (linia 15). Aby otrzymać zapisany string wystarczy wywołać metodę
str() na rzecz obiektu klasy ostringstream (linia 16). Ostatnim elementem wymagającym omówienia jest odwołanie się do metod z klasy bazowej. Dopóki klasa bazowa i
klasa potomna nie mają metod o tej samej nazwie i tych samych parametrach nie ma
problemu. Odwołanie do metody jest jednoznaczne i wiadomo o którą metodę chodzi.
Klasa Manager definiuje swoją metodę info(), która przesłania metodę info() z klasy
bazowej. Mino, iż klasa Manager definiuje swoją metodę info(), to częścią jej działania
jest pobranie wyniku wywołania metody info() z klasy Employee, a więc musimy jakoś
określić, o którą metodę info() nam chodzi.. Sposób dostępu do przesłoniętych metod
klasy bazowej przedstawiony jest w linii 15.
Kompilacja i uruchomienie programu tym razem da nam poprawny wynik:
I m i e : Jan
Nazwisko : Kowalski
Stanowisko : programista
Imie : Karol
Nazwisko : Jankowski
Stanowisko : s t a r s z y programista
P o d w l a d n i : 10
Nasza klasa nie pracuje na wskaźnikach, ani nie wykorzystuje żadnych dodatkowych
zasobów (pliki, sockety itp), a więc nie musieliśmy tworzyć do tej pory własnego destruktora. Jednak czasami konieczne jest stworzenie konstruktora ze względu na specyfikę klasy i wtedy powstaje to samo pytanie co w przypadku konstruktora - co jest
wywoływane najpierw - destruktor z klasy bazowej czy z klasy potomnej. Po chwili
zastanowienia odpowiedź jest oczywista - destruktory będą wywoływane w odwrotnej
kolejności, a więc najpierw ten z kasy potomnej, a potem z bazowej. Najlepiej zobaczyć kolejność wywoływania na konkretnym przykładzie. Załóżmy że mam dwie klasy klasę Basic jako klasę podstawową i klasę Advance jako klasę dziedziczącą. Klasy poza
wypisywaniem informacji na ekranie nie robią nic więcej.
Listing 24:
1 #i n c l u d e <i o s t r e a m >
2
3 u s i n g namespace s t d ;
4
5 class Basic
6 {
7
public :
8
Basic ()
9
{
10
c o u t <<” K o n s t r u k t o r k l a s y B a s i c ”<<e n d l ;
11
}
12
13
˜ Basic ()
14
{
15
c o u t <<” D e s t r u k t o r k l a s y B a s i c ”<<e n d l ;
34
Materiał dystrybuowany bezpłatnie
3.2
Dziedziczenie i enkapsulacja
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
3
DZIEDZICZENIE
}
};
c l a s s Advance : p u b l i c B a s i c
{
public :
Advance ( )
{
c o u t <<” K o n s t r u k t o r k l a s y Advance ”<<e n d l ;
}
˜ Advance ( )
{
c o u t <<” D e s t r u k t o r k l a s y Advance ”<<e n d l ;
}
};
main ( )
{
Advance a ;
return 0;
}
Wynik:
Konstruktor klasy Basic
K o n s t r u k t o r k l a s y Advance
D e s t r u k t o r k l a s y Advance
Destruktor klasy Basic
3.2
Dziedziczenie i enkapsulacja
Do tej pory korzystaliśmy z dwóch modyfikatorów dostępu do składowych klasy - public
i private. Zmienne i metody prywatne są dostępne tylko z wnętrza ich klasy. Nawet
klasa pochodna nie ma do nich dostępu. Zmienne publiczne są dostępne poza obszarem
klasy. W przypadku dziedziczenia klasa pochodna decyduje jak zmienne publiczne klasy
podstawowej będą widoczne w klasie pochodnej, natomiast klasa pochodna nie może w
żaden sposób decydować o zmiennych prywatnych klasy podstawowej (bo są przecież one
prywatne). Rozszerzmy przykład klas Basic i Advance o dodanie prywatnej i publicznej
zmiennej w klasie Basic (linia 16 i 19). Dodatkowo niech klasa Advance odziedziczy
publiczne składniki klasy Basic jako prywatne (linia 22):
Listing 25:
1 #i n c l u d e <i o s t r e a m >
2
3 u s i n g namespace s t d ;
35
Materiał dystrybuowany bezpłatnie
3.2
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
Dziedziczenie i enkapsulacja
3
DZIEDZICZENIE
class Basic
{
public :
Basic ()
{
c o u t <<” K o n s t r u k t o r k l a s y B a s i c ”<<e n d l ;
p u b l i c a =10;
p r i v a t e b =100;
}
˜ Basic ()
{
c o u t <<” D e s t r u k t o r k l a s y B a s i c ”<<e n d l ;
}
int public a ;
private :
int private b ;
};
c l a s s Advance : p r i v a t e B a s i c
{
public :
Advance ( )
{
c o u t <<” K o n s t r u k t o r k l a s y Advance ”<<e n d l ;
}
˜ Advance ( )
{
c o u t <<” D e s t r u k t o r k l a s y Advance ”<<e n d l ;
}
void i n f o ()
{
c o u t <<p u b l i c a <<”
}
”<<p r i v a t e b <<e n d l ;
};
main ( )
{
Advance a ;
c o u t <<a . p u b l i c a <<e n d l ;
return 0;
}
Próba kompilacji zakończy się błędem:
d z i e d z i c z e n i e . cpp : I n member f u n c t i o n v o i d Advance : : i n f o ( ) :
d z i e d z i c z e n i e . cpp : 2 3 : e r r o r : i n t B a s i c : : p r i v a t e b i s p r i v a t e
36
Materiał dystrybuowany bezpłatnie
3.2
Dziedziczenie i enkapsulacja
dziedziczenie
dziedziczenie
dziedziczenie
dziedziczenie
. cpp : 4 1 :
. cpp : I n
. cpp : 2 0 :
. cpp : 5 0 :
3
DZIEDZICZENIE
error : within t h i s context
f u n c t i o n i n t main ( ) :
e r r o r : i n t Basic : : public a i s i n a c c e s s i b l e
error : within t h i s context
Błędy są dwa. Po pierwsze, w linii 41 odwołujemy się w klasie potomnej do prywatnego
składnika klasy bazowej co oczywiście jest niedozwolone. Po drugie, klasa Advanced
zdeklarowała, że publiczne składowe klasy Basic zostaną odziedziczone jako składowe
prywatne, a więc nie będzie do nich dostępu z zewnątrz za pośrednictwem obiektów
klasy Advance (linia 26). W głównej części programu (linia 50) próbujemy odwołać się
do pola public a, ale pole to w klasie Advance jest prywatne mimo, iż w klasie bazowej
pole było polem publicznym.
Brak dostępu do prywatnych składowych klasy bazowej z poziomu klasy dziedziczącej
czasami jest kłopotliwy. Oczywiście zawsze można w klasie bazowej zdefiniować publiczne metody dostępowe i z nich korzystać w klasie potomnej. Jednak co w sytuacji
kiedy chcemy ukryć dane przed ”światem zewnętrznym“, ale z drugiej strony dać do nich
bezpośredni dostęp w klasach dziedziczących ? Właśnie w takich sytuacjach używa się w
klasie bazowej modyfikatora dostępu protected:. Modyfikator ten powoduje, że składowe są na zewnątrz traktowane jak składowe prywatne, ale w klasach dziedziczących
są one dostępne bezpośrednio (tak jakby były publiczne). Poprawmy nasz program w
dwóch miejscach - po pierwsze niech klasa potomna dziedziczy składowe publiczne jako
publiczne (linia 26), po drugie zmienną private b poprzedźmy modyfikatorem protected
(linia 22).
Listing 26:
1 #i n c l u d e <i o s t r e a m >
2
3 u s i n g namespace s t d ;
4
5 class Basic
6 {
7
public :
8
Basic ()
9
{
10
c o u t <<” K o n s t r u k t o r k l a s y B a s i c ”<<e n d l ;
11
p u b l i c a =10;
12
p r i v a t e b =100;
13
}
14
15
˜ Basic ()
16
{
17
c o u t <<” D e s t r u k t o r k l a s y B a s i c ”<<e n d l ;
18
}
19
20
int public a ;
21
22
protected :
23
int private b ;
37
Materiał dystrybuowany bezpłatnie
4
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
POLIMORFIZM
};
c l a s s Advance : p u b l i c B a s i c
{
public :
Advance ( )
{
c o u t <<” K o n s t r u k t o r k l a s y Advance ”<<e n d l ;
}
˜ Advance ( )
{
c o u t <<” D e s t r u k t o r k l a s y Advance ”<<e n d l ;
}
void i n f o ()
{
c o u t <<p u b l i c a <<”
}
”<<p r i v a t e b <<e n d l ;
};
main ( )
{
Advance a ;
c o u t <<a . p u b l i c a <<e n d l ;
return 0;
}
Teraz program skompiluje się bez problemu.
4
Polimorfizm
Z mechanizmem dziedziczenia nierozerwalnie związane jest pojęcie polimorfizmu. Polimorfizm to kolejny fundament programowania obiektowego, mechanizm pozwalający
na wyabstrahowanie zespołu zachowań (metod) dla klas posiadających wspólne klasy
bazowe. Mimo, iż brzmi to bardzo formalnie to sprawa jest dość prosta. Wróćmy do
przykładu z klasami Employee i Manager. Język C++ jest językiem, w którym musimy
określać typ danych. Jeżeli będziemy próbować przypisać dane jednego typu do zmiennej innego typu, to albo nastąpi konwersja (o ile jest możliwa), albo zgłoszony zostanie
błąd w trakcie kompilacji. Jednak w przypadku dziedziczenia sprawa wgląda nieco bardziej skomplikowanie (ale tylko pozornie) Wykorzystajmy klasy Employee i Manager w
następującym programie:
Listing 27:
1 #i n c l u d e ” e m p l o y e e . h ”
38
Materiał dystrybuowany bezpłatnie
4
POLIMORFIZM
2 #i n c l u d e ” manager . h ”
3 #i n c l u d e <i o s t r e a m >
4
5 u s i n g namespace s t d ;
6
7 main ( )
8 {
9
10
Employee t a b [ ] = { Employee ( ” Jan ” , ” K o w a l s k i ” , ” p r o g r a m i s t a ” ) ,
11
Manager ( ” K a r o l ” , ” J a n k o w s k i ” , ” s t a r s z y p r o g r a m i s t a ” , 1 0 ) ,
12
Employee ( ” K a r o l i n a ” , ” W i s n i e w s k a ” , ” g r a f i k ” ) } ;
13
14
f o r ( i n t i =0; i <3; i ++)
15
c o u t <<t a b [ i ] . i n f o ()<<” \n\n\n ” ;
16
17
return 0;
18 }
W linii 10 mamy deklarację, że tablica tab jest tablicą do przechowywania obiektów
klasy Employee, ale zapisujemy do niej obiekty zarówno klasy Employee jak i klasy
dziedziczącej - Manager. Uruchomienie programu da nam następujący wynik:
I m i e : Jan
Nazwisko : Kowalski
Stanowisko : programista
Imie : Karol
Nazwisko : Jankowski
Stanowisko : s t a r s z y programista
Imie : Karolina
Nazwisko : Wisniewska
Stanowisko : g r a f i k
Wynik działania programu jest dość oczywisty. Obiekt klasy Manager został zrzutowany
(zawężony) na typ reprezentowany przez klasę Employee, a więc metoda info() jest metodą z klasy Employee a nie Manager. Takie rzutowanie jest w tym wypadku zupełnie
domyślnym rzutowaniem - klasa Manager zawiera wszystko to co klasa Employee plus
dodatkowe elementy, które w rzutowaniu zostają ”pominięte“. Z tego powodu nie można
tablicy zdeklarować jako tablicy obiektów klasy Manager, bo nie można zrzutować obiektów z klasy Employee na klasę Manager - nie ma przepisu jak np. dodać i ustawić pole
employees. Taka modyfikacja:
Manager t a b [ ] = { Employee ( ” Jan ” , ” K o w a l s k i ” , ” p r o g r a m i s t a ” ) ,
Manager ( ” K a r o l ” , ” J a n k o w s k i ” , ” s t a r s z y p r o g r a m i s t a ” , 1 0 ) ,
Employee ( ” K a r o l i n a ” , ” W i s n i e w s k a ” , ” g r a f i k ” ) } ;
spowoduje błąd w trakcie kompilacji:
39
Materiał dystrybuowany bezpłatnie
4
POLIMORFIZM
p r z y k l a d . cpp : I n f u n c t i o n i n t main ( ) :
p r z y k l a d . cpp : 1 2 : e r r o r : c o n v e r s i o n from Employee t o non−s c a l a r
t y p e Manager r e q u e s t e d
p r z y k l a d . cpp : 1 2 : e r r o r : c o n v e r s i o n from Employee t o non−s c a l a r
t y p e Manager r e q u e s t e d
Jak do tej pory o żadnym polimorfiźmie nie ma mowy i nic specjalnego czy rewolucyjnego
się nie stało. Wprowadźmy do programu pierwszą małą zmianę - zdeklarujmy, że tablica
nie będzie tablicą obiektów, a tablicą wskaźników do obiektów i sprawdźmy działanie
programu.
Listing 28:
1 #i n c l u d e ” e m p l o y e e . h ”
2 #i n c l u d e ” manager . h ”
3 #i n c l u d e <i o s t r e a m >
4
5 u s i n g namespace s t d ;
6
7 main ( )
8 {
9
10
Employee ∗ t a b [ ] = { new Employee ( ” Jan ” , ” K o w a l s k i ” , ” p r o g r a m i s t a ” ) ,
11
new Manager ( ” K a r o l ” , ” J a n k o w s k i ” , ” s t a r s z y p r o g r a m i s t a ” , 1 0 ) ,
12
new Employee ( ” K a r o l i n a ” , ” W i s n i e w s k a ” , ” g r a f i k ” ) } ;
13
14
f o r ( i n t i =0; i <3; i ++)
15
c o u t <<t a b [ i ]−> i n f o ()<<” \n\n\n ” ;
16
17
// z w o l n i e n i e p a m i e c i
18
f o r ( i n t i =0; i <3; i ++)
19
delete tab [ i ] ;
20
21
return 0;
22 }
Oprócz zmiany w deklaracji tablicy, drugą zmianą konieczną do wprowadzenia było zwolnienie dynamicznie zarezerwowanej pamięci dla obiektów(linie 18-19). Zmiany wprowadzone do programu na razie nie wpłynęły na uzyskiwany wynik:
I m i e : Jan
Nazwisko : Kowalski
Stanowisko : programista
Imie : Karol
Nazwisko : Jankowski
Stanowisko : s t a r s z y programista
Imie : Karolina
Nazwisko : Wisniewska
Stanowisko : g r a f i k
40
Materiał dystrybuowany bezpłatnie
4
POLIMORFIZM
Mimo, iż w zapisie zmiany są niewielkie, to znaczeniowo bardzo istotne. Tablica zawiera
wskaźniki a nie obiekty, co więcej nie nastąpiło rzutowanie na obiekt klasy bazowej,
a jedynie zrzutowanie wskaźnika. Ta ogromna różnica uwidoczni się po wprowadzenie
kolejnej zmiany. Aby wykorzystać mechanizm polimorfizmu w klasie bazowej przy deklaracji metod, które mają być polimorficzne musimy dodać słowo kluczowe virtual.
Obecnie deklaracja klasy Employee (employee.h) wyglądać będzie następująco:
Listing 29:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// e m p l o y e e . h
#i f n d e f EMPLOYEE H
#d e f i n e EMPLOYEE H
#i n c l u d e < s t r i n g >
c l a s s Employee
{
// p u b l i c i n t e r f a c e
public :
Employee ( ) { } ;
Employee ( s t d : : s t r i n g f i r s t N a m e , s t d : : s t r i n g secondName ,
std : : s t r i n g position ) ;
Employee ( s t d : : s t r i n g f i r s t N a m e , s t d : : s t r i n g secondName ) ;
std : : s t r i n g getFirstName ( ) ;
s t d : : s t r i n g getSecondName ( ) ;
std : : s t r i n g getPosition ( ) ;
v i r t u a l std : : s t r i n g info ( ) ;
void s e t P o s i t i o n ( std : : s t r i n g p o s i t i o n ) ;
// p r i v a t e d a t a
private :
s t d : : s t r i n g f i r s t N a m e , secondName , p o s i t i o n ;
};
#e n d i f
Skompilujmy nasz program
g++ e m p l o y e e . cpp manager . cpp p r z y k l a d . cpp
i zobaczmy wynik:
I m i e : Jan
Nazwisko : Kowalski
Stanowisko : programista
Imie : Karol
Nazwisko : Jankowski
41
Materiał dystrybuowany bezpłatnie
5
ABSTRAKCJA
Stanowisko : s t a r s z y programista
P o d w l a d n i : 10
Imie : Karolina
Nazwisko : Wisniewska
Stanowisko : g r a f i k
Wynik jest zupełnie inny niż do tej pory. Mimo, iż deklaracja tablicy pozostała taka jak
była (a więc jako tablica wskaźników do obiektów typu Employee) to dzięki temu, że
klasa bazowa deklaruje metodę info() jako metodę wirtualną, to program niejako sam
orientuje się na obiekt jakiej klasy wskazuje wskaźnik, mimo iż został zdeklarowany jako
wskaźnik do klasy bazowej. Dzięki temu zawsze wywołana zostanie właściwa metoda
(raz info() dla Employee a raz dla Manager). Właśnie ten mechanizm nazywa się polimorfizmem. Podkreślić należy wyraźnie jeszcze raz - mechanizm ten działa TYLKO
w przypadku wskaźników do obiektów, zmiana deklaracji tablicy na tablicę obiektów
spowoduje rzutowanie obiektów na typ reprezentujący klasę bazową i o żadnym polimorfizmie nie ma mowy.
5
Abstrakcja
Abstrakcja to ostatni (ale równie ważny jak te poprzednio omówione) z elementów składowych programowania obiektowego i jednocześnie chyba najtrudniejszy do wyjaśnienia
w kategorii opisowej. Zacznijmy od pojęcia metody czysto wirtualnej. W naszym przykładzie z klasami Employee i Manager zdeklarowaliśmy, że metoda info() jest metodą
wirtualną co w efekcie pozwoliło na wykorzystanie polimorfizmu. Sama zmiana w stosunku do wcześniejszej wersji tej klasy polegała tylko na dopisaniu słowa virtual przed
nazwą metody. Jednak zagadnienie tworzenia metod wirtualnych jest znacznie szersze
niż tylko dodanie jednego słowa. Zastanówmy się ponownie nad klasami Employee i
Manager i nad problemem ”mini bazy danych“ pracowników firmy. Pojęcie pracownik
w takim ujęciu jest pojęciem dość abstrakcyjnym. Oczywiście każdy pracownik firmy
ma wspólne cechy (dla nas to imię, nazwisko i stanowisko), ale reszta ”parametrów“
jaki i możliwych zachować w głównej mierze zależeć będzie od statusu pracownika. W
najprostszym przybliżeniu możemy wyróżnić - pracownika fizycznego, pracownika biurowego, majstra, kierownika działu, dyrektora działu i wielu innych pracowników. Niby
nic nowego w tej analizie nie ma, ale czy ma sens tworzenie obiektu klasy Employee ?
Czy w firmie może być tak prosty obiekt jak obiekt klasy Pracownik ? Otóż nie, bo samo
imię, nazwisko i stanowisko oraz kilka prostych metod nie określa w żaden sposób konkretnego pracownika. Konkretny pracownik ma te cechy, ale ma jeszcze mnóstwo innych
cech, odróżniających go od innych pracowników (innych typów), a zarazem upodabniających do jeszcze innych (obiektów tej samej klasy). Dodajmy do klasy Employee nową
metodę - getSalary() zwracającą wypłatę. Ale czy możemy obliczyć wypłatę pracownika ? Nie. Obliczyć możemy wypłatę ze względu na staż pracy, stawkę zaszeregowania,
42
Materiał dystrybuowany bezpłatnie
5
ABSTRAKCJA
ewentualne dodatki, potrącenia o pożyczki, ubezpieczenia i wiele innych czynników. Niewątpliwie więc taka funkcjonalność powinna być zaimplementowana w klasach innych niż
Employee, ale jak wtedy uzyskać polimorfizm dla wszystkich klas bazujących na klasie
Employee i jak np. stworzyć tablicę zawierającą różnych (w sensie klas) pracowników ?
Co więcej - jak wymusić na klasach dziedziczących, aby dodały swoją własną implementację metod wspólnych w sensie nazwy i parametrów dla wszystkich klas dziedziczących
i klasy bazowej (np. właśnie getSalary()).
Zmodyfikujmy nasz program - po pierwsze, dodajmy klasę OrdinaryEmployee reprezentującą szeregowego pracownika firmy, po drugie, dodajmy do wszystkich klas metodę
getSallary(), po trzecie załóżmy, że klasa Employee będzie klasą bazową dla pozostałych
dwóch klas.
Zacznijmy od modyfikacji klasy Employee (employee.h) dodając do niej wirtualną metodę getSallary(), jednak ze względu na to, że w klasie Employee nie możemy w żaden
sposób zdefiniować kodu tej metody, oczywiście z przyczyn sposobu naliczania pensji a
nie ograniczeń formalnych języka programowania, wyraźnie zaznaczymy, że metoda ta
nie ma implementacji w tej klasie. Metodę taką nazywamy metodą czysto wirtualną, co
zaznaczamy zapisem = 0; w deklaracji metody:
Listing 30:
1 // e m p l o y e e . h
2
3 #i f n d e f EMPLOYEE H
4 #d e f i n e EMPLOYEE H
5
6 #i n c l u d e < s t r i n g >
7
8
9 c l a s s Employee
10 {
11
// p u b l i c i n t e r f a c e
12
public :
13
Employee ( ) { } ;
14
Employee ( s t d : : s t r i n g f i r s t N a m e , s t d : : s t r i n g secondName ,
15
std : : s t r i n g position ) ;
16
Employee ( s t d : : s t r i n g f i r s t N a m e , s t d : : s t r i n g secondName ) ;
17
18
std : : s t r i n g getFirstName ( ) ;
19
s t d : : s t r i n g getSecondName ( ) ;
20
std : : s t r i n g getPosition ( ) ;
21
v i r t u a l std : : s t r i n g info ( ) ;
22
23
// nowa metoda g e t S a l l a r y ( )
24
v i r t u a l double g e t S a l l a r y ( ) = 0 ;
25
26
void s e t P o s i t i o n ( std : : s t r i n g p o s i t i o n ) ;
27
28
29
// p r i v a t e d a t a
43
Materiał dystrybuowany bezpłatnie
5
ABSTRAKCJA
30 p r i v a t e :
31
s t d : : s t r i n g f i r s t N a m e , secondName , p o s i t i o n ;
32
33 } ;
34
35 #e n d i f
Skoro metoda jest tylko zdeklarowana i co więcej jest wyraźnie zaznaczone, że klasa nie
implementuje jej kodu to nie możemy w takim razie utworzyć obiektów tej klasy. Próba
wykonania polecenia:
Employee p r a c o w n i k ( ” Jan ” , ” K o w a l s k i ” , ” p r o g r a m i s t a ” ) ;
spowoduje w trakcie kompilacji następujący błąd:
p r z y k l a d . cpp : I n f u n c t i o n i n t main ( ) :
p r z y k l a d . cpp : 1 0 : e r r o r : c a n n o t d e c l a r e v a r i a b l e p r a c o w n i k t o be o f
a b s t r a c t t y p e Employee
employee . h : 1 0 : note :
because the f o l l o w i n g v i r t u a l f u n c t i o n s are
p u r e w i t h i n Employee :
employee . h : 2 3 : note :
v i r t u a l d o u b l e Employee : : g e t S a l l a r y ( )
Nie możemy utworzyć obiektu klasy Employee ponieważ zawiera ona metodę czysto
wirtualną getSalary(),a więc sama klasa jest klasą abstrakcyjną. Klasy abstrakcyjne
są klasami najczęściej służącymi do określenia wspólnego interfejsu dla całej rodziny
klas dziedziczących z klasy abstrakcyjnej. Takie rozwiązanie zapewnia, że każda klasa
dziedzicząca z takiej klasy musi zdeklarować swój kod metod określonych jako czysto
wirtualne lub jeżeli tego nie zrobi, to sama będzie klasą abstrakcyjną czyli nie będziemy
mogli utworzyć obiektów tej klasy. Podkreślmy wyraźnie obiektów tej klasy a nie
wskaźników do obiektów tej klasy, bo te będziemy mieli i to bardzo często (ze względu
na wykorzystanie polimorfizmu).
Kod klasy Employee poza deklaracją metody czysto wirtualnej nie ulegnie dalszym modyfikacjom. Reszta plików może wyglądać np. tak:
Listing 31:
1 // o r d i n a r y . h
2 #i f n d e f ORIDINARY EMPLOYEE H
3 #d e f i n e ORIDINARY EMPLOYEE H
4
5 #i n c l u d e ” e m p l o y e e . h ”
6
7 c l a s s O r d i n a r y E m p l o y e e : p u b l i c Employee
8 {
9
public :
10
O r d i n a r y E m p l o y e e ( s t d : : s t r i n g f i r s t N a m e , s t d : : s t r i n g secondName ,
11
std : : s t r i n g position ) ;
12
13
double g e t S a l a r y ( ) ;
14
44
Materiał dystrybuowany bezpłatnie
5
ABSTRAKCJA
15 } ;
16 #e n d i f
Listing 32:
1 // o r d i n a r y . cpp
2 #i n c l u d e ” o r d i n a r y . h ”
3 #i n c l u d e <s s t r e a m >
4
5 using std : : s t r i n g ;
6
7 O r d i n a r y E m p l o y e e : : O r d i n a r y E m p l o y e e ( s t r i n g f i r s t N a m e , s t r i n g secondName ,
8
string position )
9
: Employee ( f i r s t N a m e , secondName , p o s i t i o n )
10
{}
11
12 d ouble O r d i n a r y E m p l o y e e : : g e t S a l a r y ( )
13 {
14
double po dst awa =40∗4∗20;
15
r e t u r n po dst awa ;
16 }
Listing 33:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// manager . h
#i f n d e f MANAGER H
#d e f i n e MANAGER H
#i n c l u d e ” e m p l o y e e . h ”
c l a s s Manager : p u b l i c Employee
{
public :
Manager ( s t d : : s t r i n g f i r s t N a m e , s t d : : s t r i n g secondName ,
std : : s t r i n g p o s i t i o n , i n t employees ) ;
std : : s t r i n g info ( ) ;
double g e t S a l a r y ( ) ;
private :
i n t employees ;
};
#e n d i f
Listing 34:
1 // manager . cpp
2 #i n c l u d e ” manager . h ”
3 #i n c l u d e <s s t r e a m >
4
5 using std : : s t r i n g ;
6 using std : : ostrin gstream ;
7
8 Manager : : Manager ( s t r i n g f i r s t N a m e , s t r i n g secondName , s t r i n g p o s i t i o n ,
45
Materiał dystrybuowany bezpłatnie
5
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ABSTRAKCJA
i n t employees )
: Employee ( f i r s t N a m e , secondName , p o s i t i o n )
{
t h i s −>e m p l o y e e s=e m p l o y e e s ;
}
s t r i n g Manager : : i n f o ( )
{
ostringstream buffor ;
b u f f o r <<Employee : : i n f o ()<<” \ n P o d w l a d n i : ”<<e m p l o y e e s ;
return buffor . s t r ( ) ;
}
d ouble Manager : : g e t S a l a r y ( )
{
double po dst awa =40∗4∗20;
//20% w i e c e j j a k o c o m i e s i e c z n a p r e m i a z r a c j i s t a n o w i s k a
r e t u r n 1 . 2 ∗ po dst awa ;
}
no i ”główna“ część programu:
Listing 35:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// p r z y k l a d . cpp
#i n c l u d e ” e m p l o y e e . h ”
#i n c l u d e ” o r d i n a r y . h ”
#i n c l u d e ” manager . h ”
#i n c l u d e <i o s t r e a m >
u s i n g namespace s t d ;
main ( )
{
Employee ∗ t a b [ ] = { new O r d i n a r y E m p l o y e e ( ” Jan ” , ” K o w a l s k i ” , ” p r o g r a m i s t a ” ) ,
new Manager ( ” K a r o l ” , ” J a n k o w s k i ” , ” s t a r s z y p r o g r a m i s t a ” , 1 0 ) ,
new O r d i n a r y E m p l o y e e ( ” K a r o l i n a ” , ” W i s n i e w s k a ” , ” g r a f i k ” ) } ;
f o r ( i n t i =0; i <3; i ++)
c o u t <<t a b [ i ]−> i n f o ()<<” \ n P e n s j a = ”<<t a b [ i ]−> g e t S a l a r y ()<<” \n\n\n ” ;
// z w o l n i e n i e p a m i e c i
f o r ( i n t i =0; i <3; i ++)
delete tab [ i ] ;
return 0;
}
46
Materiał dystrybuowany bezpłatnie

Podobne dokumenty