C++ Strategie i taktyki Vademecum profesjonalisty

background image

Wydawnictwo Helion
ul. Chopina 6
44-100 Gliwice
tel. (32)230-98-63

e-mail: helion@helion.pl

PRZYK£ADOWY ROZDZIA£

PRZYK£ADOWY ROZDZIA£

IDZ DO

IDZ DO

ZAMÓW DRUKOWANY KATALOG

ZAMÓW DRUKOWANY KATALOG

KATALOG KSI¥¯EK

KATALOG KSI¥¯EK

TWÓJ KOSZYK

TWÓJ KOSZYK

CENNIK I INFORMACJE

CENNIK I INFORMACJE

ZAMÓW INFORMACJE

O NOWOCIACH

ZAMÓW INFORMACJE

O NOWOCIACH

ZAMÓW CENNIK

ZAMÓW CENNIK

CZYTELNIA

CZYTELNIA

FRAGMENTY KSI¥¯EK ONLINE

FRAGMENTY KSI¥¯EK ONLINE

SPIS TRECI

SPIS TRECI

DODAJ DO KOSZYKA

DODAJ DO KOSZYKA

KATALOG ONLINE

KATALOG ONLINE

C++. Strategie i taktyki.
Vademecum profesjonalisty

Autor: Robert B. Murray
T³umaczenie: Przemys³aw Steæ
ISBN: 83-7361-323-4
Tytu³ orygina³u:

C++ Strategies and Tactics

Format: B5, stron: 240

Poznanie ruchów figur szachowych to dopiero pierwszy krok w nauce tej gry.
Aby j¹ opanowaæ, trzeba zrozumieæ strategie i taktyki, które wp³ywaj¹ na ka¿dy ruch.
To samo dotyczy jêzyka C++. Znajomoæ w³aciwych strategii pomaga unikaæ pu³apek
i pracowaæ o wiele skuteczniej. Rob Murray dziel¹c siê swoim dowiadczeniem pomaga
programistom C++ wykonaæ nastêpny krok w kierunku tworzenia wydajnych aplikacji.

Licznie wystêpuj¹ce w ca³ej ksi¹¿ce przyk³ady kodu maj¹ na celu zilustrowanie
przydatnych strategii programistycznych i ostrzec przed nabyciem niebezpiecznych
nawyków. Aby dodatkowo u³atwiæ przyswajanie nowych umiejêtnoci, ka¿dy rozdzia³
koñczy siê list¹ poruszonych w nim kluczowych zagadnieñ oraz pytaniami maj¹cymi
spowodowaæ przemylenia i dyskusje.

Ksi¹¿ka przedstawia miêdzy innymi:

• Tworzenie w³aciwych abstrakcji dla projektu i przekszta³canie abstrakcji
w klasy C++
• Mechanizmy dziedziczenia pojedynczego i wielokrotnego
• Metody tworzenia klas
• Szczegó³owy opis mechanizmu szablonów
• Wskazówki dotycz¹ce stosowania wyj¹tków
• Metody tworzenia kodu nadaj¹cego siê do wielokrotnego wykorzystania
• Przenoszenie programów z jêzyka C do C++

Robert B. Murray jest wicedyrektorem ds. in¿ynierii oprogramowania w firmie
Quantitative Data Systems dostarczaj¹cej niestandardowych rozwi¹zañ z zakresu
oprogramowania dla czo³owych firm. Wczenie pracowa³ w AT&T Bell Labs, gdzie bra³
udzia³ w rozwoju jêzyka C++, jego kompilatorów i bibliotek. Jest pierwszym redaktorem
magazynu „The C++ Report”. Od 1987 prowadzi zajêcia dotycz¹ce jêzyka C++ na
konferencjach naukowych i technicznych.

background image

5RKUVTGħEK


  

     

1.1. Abstrakcja numeru telefonu............................................................................................17
1.2. Związki między abstrakcjami .........................................................................................19
1.3. Problem warunków brzegowych ....................................................................................24
1.4. Projektowanie z wykorzystaniem kart CRC ...................................................................25
1.5. W skrócie ........................................................................................................................26
1.6. Pytania ............................................................................................................................26

     

2.1. Konstruktory ...................................................................................................................27
2.2. Przypisanie......................................................................................................................34
2.3. Dane publiczne ...............................................................................................................36
2.4. Niejawne konwersje typów.............................................................................................40
2.5. Operatory przeciążone — składowe czy nie?.................................................................44
2.6. Przeciążenie, argumenty domyślne i wielokropek .........................................................47
2.7. Słowo kluczowe const ....................................................................................................48
2.8. Zwracanie referencji .......................................................................................................54
2.9. Konstruktory statyczne ...................................................................................................55
2.10. W skrócie ......................................................................................................................56
2.11. Pytania ..........................................................................................................................57

 !  "# 

3.1. Klasa Lancuch.................................................................................................................60
3.2. Unikanie kopiowania przez zastosowanie liczników użycia ..........................................61
3.3. Zapobieganie powtórnym kompilacjom — „Kot z Cheshire”........................................66
3.4. Stosowanie uchwytów w celu ukrycia szczegółów projektu..........................................68
3.5. Implementacje wielokrotne.............................................................................................69
3.6. Uchwyty jako obiekty .....................................................................................................72
3.7. Podsumowanie ................................................................................................................73
3.8. W skrócie ........................................................................................................................73
3.9. Pytania ............................................................................................................................73

 $  % 

4.1. Związek generalizacji (specjalizacji)..............................................................................75
4.2. Dziedziczenie publiczne .................................................................................................78
4.3. Dziedziczenie prywatne ..................................................................................................78
4.4. Dziedziczenie chronione.................................................................................................82
4.5. Zgodność z abstrakcjami klasy bazowej.........................................................................83
4.6. Funkcje czysto wirtualne ................................................................................................85

background image

6

C++. Strategie i taktyki. Vademecum profesjonalisty

4.7. Szczegóły i pułapki związane z dziedziczeniem ............................................................87
4.8. W skrócie ........................................................................................................................90
4.9. Pytania ............................................................................................................................90

   % 

5.1. Dziedziczenie wielokrotne jako iloczyn zbiorów ...........................................................91
5.2. Wirtualne klasy bazowe..................................................................................................96
5.3. Pewne szczegóły dotyczące dziedziczenia wielokrotnego .............................................99
5.4. W skrócie ......................................................................................................................101
5.5. Pytania ..........................................................................................................................101

 &    ' (!

6.1. Interfejs chroniony ........................................................................................................103
6.2. Czy należy projektować pod kątem dziedziczenia?......................................................106
6.3. Projektowanie pod kątem dziedziczenia — kilka przykładów .....................................111
6.4. Podsumowanie ..............................................................................................................116
6.5. W skrócie ......................................................................................................................116
6.6. Pytania ..........................................................................................................................117

   )  

7.1. Szablon klasy Para ........................................................................................................119
7.2. Kilka szczegółów dotyczących szablonów...................................................................122
7.3. Konkretyzacja szablonu ................................................................................................123
7.4. Inteligentne wskaźniki ..................................................................................................125
7.5. Argumenty wyrażeniowe szablonów............................................................................131
7.6. Szablony funkcji ...........................................................................................................132
7.7. W skrócie ......................................................................................................................135
7.8. Pytania ..........................................................................................................................136

 *  )     !

8.1. Klasy kontenerowe wykorzystujące szablony ..............................................................139
8.2. Przykład — klasa Blok .................................................................................................141
8.3. Szczegóły projektowe klasy Blok.................................................................................143
8.4. Kontenery z iteratorami — klasa Lista .........................................................................148
8.5. Zagadnienia dotyczące projektowania iteratorów ........................................................154
8.6. Zagadnienia dotyczące wydajności ..............................................................................157
8.7. Ograniczenia dotyczące argumentów szablonów .........................................................160
8.8. Specjalizacje szablonów ...............................................................................................162
8.9. W skrócie ......................................................................................................................168
8.10. Pytania ........................................................................................................................168

   +,-. /   

9.1. Poznanie i nabycie ........................................................................................................172
9.2. Odporność .....................................................................................................................173
9.3. Zarządzanie pamięcią ...................................................................................................179
9.4. Alternatywne metody alokacji pamięci ........................................................................181
9.5. Przekazywanie argumentów do operatora new ............................................................184
9.6. Zarządzanie zasobami zewnętrznymi ...........................................................................187
9.7. Znajdowanie błędów pamięci .......................................................................................187
9.8. Konflikty nazw .............................................................................................................192
9.9. Wydajność ....................................................................................................................195
9.10. Nie zgaduj — zmierz!.................................................................................................195
9.11. Algorytmy ...................................................................................................................196
9.12. Wąskie gardła w dynamicznej alokacji pamięci.........................................................197
9.13. Funkcje rozwijane w miejscu wywołania ...................................................................202

background image

Spis treści

7

9.14. Prawo Tiemanna .........................................................................................................204
9.15. W skrócie ....................................................................................................................204
9.16. Pytania ........................................................................................................................205

 (  ' (

10.1. Sprostowanie...............................................................................................................209
10.2. Dlaczego wyjątki?.......................................................................................................209
10.3. Przykład wyjątku ........................................................................................................212
10.4. Wyjątki powinny być wyjątkowe ...............................................................................213
10.5. Zrozumieć wyjątki ......................................................................................................215
10.6. Oszacowanie winy ......................................................................................................215
10.7. Projektowanie obiektu wyjątku ..................................................................................217
10.8. W skrócie ....................................................................................................................219
10.9. Pytania ........................................................................................................................219

    0122 

11.1. Wybór języka C++......................................................................................................221
11.2. Przyswajanie C++ .......................................................................................................223
11.3. Projektowanie i implementacja...................................................................................224
11.4. Tworzenie bazy zasobów............................................................................................226
11.5. Uwagi końcowe ..........................................................................................................227
11.6. W skrócie ....................................................................................................................227
11.7. Pytania ........................................................................................................................228

) 

background image

Rozdział 4.



Wiele dyskusji dotyczących dziedziczenia rozpoczyna się od objaśnienia reguł języka.
Chociaż poznanie tych reguł jest niezbędne do korzystania z samego mechanizmu, to
najpierw powinniśmy się upewnić, że rozumiemy, gdzie w projekcie dziedziczenie po-
winno zostać zastosowane. Programy z nieodpowiednio zaprojektowanym dziedzicze-
niem można wprawdzie doprowadzić do kompilacji, lecz będą one trudne do zrozumienia
i utrzymania.

   

Dziedzicznie powinno zostać zastosowane w przypadku, gdy nowa klasa (klasa po-
chodna) opisuje pewien zbiór obiektów, który jest podzbiorem obiektów opisywanych
przez klasę bazową. Zależność ta jest związkiem generalizacji (lub specjalizacji):

 



         



  



         



Każdy obiekt typu



jest również obiektem typu



(zauważmy, że relacja

odwrotna nie jest prawdziwa — mogą istnieć obiekty typu



, które nie są obiektami

typu



). Każda operacja, którą można zastosować do obiektu typu



powinna

również mieć sens przy zastosowaniu do obiektu typu



(tj. funkcje składowe klasy

bazowej mogą być wywoływane dla obiektów klasy pochodnej). W klasie pochodnej
można zmienić implementację funkcji składowej przez przesłonięcie jej, lecz operacja
pojęciowa powinna nadal mieć sens w klasie pochodnej. Każdy pojazd można przyspie-
szyć — rower będzie korzystał z innej implementacji tej operacji niż pociąg, lecz operacja
pojęciowa będzie taka sama.

      

Dziedziczenie nie powinno być stosowane w przypadku, gdy klasa bazowa jest składni-
kiem obiektu opisywanego przez klasę pochodną:

background image

76

C++. Strategie i taktyki. Vademecum profesjonalisty

  



    



     !"#!   

  $" %



Obiekt typu



nie jest specjalnym rodzajem obiektu typu

 

, któremu

przypadkiem doczepiono kadłub —



jest raczej obiektem złożonym z innych

obiektów, w tym obiektu typu

 

(obiekt typu



nie jest obiektem typu

 

, on ma

 

).

Takie niewłaściwe użycie mechanizmu dziedziczenia pozwala użytkownikom stosować
operacje klasy

 

do obiektu typu



:



  &'  

W wyniku wywołania funkcji

 

zostanie zwrócona długość obiektu typu

 



, a nie obiektu typu



. Nie oznacza to wcale, że taki kod nie może działać, jest

on jednak mylący — dlaczego skrzydło traktowane jest inaczej niż silnik czy śmigło?
W jaki sposób skonstruować dwupłat — samolot o dwóch skrzydłach?

W przypadku, gdy obiekt składa się z innych obiektów, właściwym podejściem będzie
uczynienie tych obiektów składowymi, a nie klasami bazowymi:



  



 '



Oznacza to, że w operacjach na części

 

obiektu typu



trzeba będzie

jawnie wymieniać składową



typu

 

, lecz dzięki temu związek pomiędzy kla-

sami



a

 

jest jaśniejszy — jest to związek ma (agregacji), a nie jest (ge-

neralizacji).

     

Każda operacja występująca w bardziej ogólnej klasie bazowej powinna mieć zastosowanie
do każdego obiektu klasy pochodnej. Chociaż w klasie pochodnej można zdefiniować
nową implementację operacji przez przesłonięcie funkcji składowej klasy bazowej, to nie
należy próbować usunąć operacji, która jest dozwolona w klasie bazowej przez zadekla-
rowanie jej jako prywatnej.

Poniżej przedstawiamy przykład (wadliwej) hierarchii realizującej obiekty typu



o dwóch rodzajach prędkości — prędkości normalnej,

  

, mierzonej względem

podłoża oraz prędkości lotu,

  

, mierzonej względem powietrza, która

w obecności wiatru można być różna od wartości

  

:

background image

Rozdział 4.



Dziedziczenie

77

  (" 



    

    ) 



 ) !  



 * !  * % #

    ) 



  $" %



Ponowna deklaracja składowej

     

jako prywatnej stanowi próbę

uniemożliwienia wywołania funkcji

  

dla obiektu typu

  

:

+   ) !

  $" %



 ) !, &!+ 

 & -.  ) ,/"*  

" !  )  !,

Próba ta jest jednak nieudana, ponieważ zakazaną funkcję składową można mimo wszystko
wywołać poprzez wskaźnik typu

 

:

 , &!+ 

 & -.  ) 0 !

Fakt podejmowania prób ograniczenia operacji w klasach pochodnych wskazuje zazwy-
czaj na to, że projekt hierarchii klas jest błędny. Aby rozwiązać ten problem w naszym
przykładzie, musimy rozstrzygnąć, czy mówienie o składowej

  

pojazdu

lądowego ma w ogóle sens. Jeśli uznamy, że nie, to będzie znaczyło, że deklaracja funkcji
składowej

  

nie powinna występować w żadnej klasie, która jest klasą

bazową dla klasy

  

. Zamiast tego, powinna zostać przeniesiona do takiego

miejsca w hierarchii klas, gdzie pytanie o prędkość lotu postawione wobec obiektów tej
klasy i wszystkich jej klas pochodnych będzie miało zawsze sens. W naszym przykła-
dzie powinniśmy utworzyć nową klasę

  

, z której będą wyprowadzane

wszystkie pojazdy posiadające prędkość lotu:

 



    



 ) !   ,''',

 ) !   



    ) 

  $" %



background image

78

C++. Strategie i taktyki. Vademecum profesjonalisty

Przy takiej hierarchii klas nie mamy możliwości wywołania funkcji

  

wobec

obiektu typu

  

, nawet poprzez wskaźnik typu

 

(propozycję innego

sposobu rozwiązania tego problemu zawiera pytanie 1 na końcu tego rozdziału).

          

Klasa określa dwa interfejsy dla świata zewnętrznego — jeden dla użytkowników (skład-
niki publiczne) oraz drugi dla implementatorów klas pochodnych (składniki chronione
i prywatne). Mechanizm dziedziczenia działa w taki sam sposób: jeśli dziedziczenie jest
publiczne, to wchodzi w skład interfejsu przeznaczonego dla użytkowników, którzy
mogą tym samym tworzyć kod zależny od tego dziedziczenia. Jeśli dziedziczenie jest
chronione, to jest jedynie częścią interfejsu przeznaczonego dla implementatorów klas
pochodnych. Jeśli natomiast jest prywatne, to w ogóle nie wchodzi w skład interfejsu —
może z niego korzystać jedynie implementator klasy (oraz klasy zaprzyjaźnione).

   

Dziedziczenie publiczne stosowane jest w przypadku, gdy dziedziczenie wchodzi w skład
interfejsu, tj. pragniemy poinformować naszych użytkowników o fakcie, że obiekt typu
X jest obiektem typu Y (klasa X jest wyprowadzona z klasy Y). Podobnie jak w przy-
padku wszystkich pozostałych elementów interfejsu, zobowiązujemy się (do pewnego
stopnia) nigdy nie zmieniać tego elementu klasy! A to dlatego, że użytkownicy mogą
stworzyć kod uzależniony od niejawnej konwersji wskaźnika lub referencji do klasy



 

na wskaźnik lub referencję do klasy

 

:

  !)1 231

14"  4

!)3

Powyższy kod opiera się na fakcie, że Koło jest Kształtem, a więc referencję do Koła

można przekazać do każdej funkcji posiadającej parametr typu

 !

. Oznacza to,

że nie możemy w przyszłości zmodyfikować tej klasy, usuwając z niej dziedziczenie
i oczekiwać, że istniejący już kod będzie działał! Byłaby to niezgodna modyfikacja in-
terfejsu — równoważna usunięciu publicznej funkcji składowej.

    

Dziedziczenie prywatne stosowane jest w przypadku, gdy dziedziczenie nie stanowi ele-
mentu interfejsu, a jedynie implementacyjny szczegół. Użytkownicy nie mogą tworzyć
kodu uzależnionego od takiego dziedziczenia, dzięki czemu zachowujemy możliwość mo-
dyfikacji implementacji polegającej na rezygnacji z używania danej klasy bazowej.

Dziedziczenie prywatne stosowane jest znacznie rzadziej niż dziedziczenie publiczne,
ponieważ realizacja złożenia (czyli wykorzystanie części „klasy bazowej” jako danej
składowej) jest prostsza i działa zazwyczaj równie dobrze. Zamiast dziedziczenia po
klasie bazowej, pojedynczy obiekt tej klasy bazowej umieszczany jest jako składowa
w klasie (dawnej) pochodnej. Takie rozwiązanie nie powinno powodować żadnej utraty

background image

Rozdział 4.



Dziedziczenie

79

Powtórka: Dziedziczenie publiczne, chronione i prywatne

W języku C++ istnieją trzy rodzaje dziedziczenia:

",   oraz  #. We wszystkich

formach dziedziczenia funkcje składowe klas pochodnych mają dostęp do składowych publicz-
nych i chronionych klasy bazowej — lecz nie do składowych prywatnych. Te trzy typy dziedziczenia
różnią się elementami, które są widoczne dla użytkownika klasy pochodnej (a nie twórcy klasy
pochodnej) oraz okolicznościami, w których użytkownik może niejawnie przekonwertować wskaź-
nik do klasy pochodnej na wskaźnik do klasy bazowej.

Najczęstszą formą dziedziczenia jest dziedziczenie publiczne:

1 



1



1 1 

  $" %



1  

   



Przy zastosowaniu dziedziczenia publicznego, składowe publiczne klasy bazowej pozostają pu-
bliczne w klasie pochodnej, a składowe chronione klasy bazowej pozostają chronione w klasie
pochodnej:

15 !$ "  5

'61-7 " !*  *

   !

Wskaźnik do klasy pochodnej może zostać niejawnie przekonwertowany na wskaźnik do publicznej
klasy bazowej:

1 , &!1561-1   *

* !*

Przy zastosowaniu dziedziczenia prywatnego, składowe publiczne i chronione klasy bazowej stają
się prywatne w klasie pochodnej. Dostęp do nich mają składowe oraz funkcje i klasy zaprzyjaź-
nione klasy pochodnej, lecz nie użytkownicy:

8 +



8 ++,&99

+ :;- 

  



 )7  8 +



 )7 +,

 !)! ) 



Składowe klasy

$ % mogą korzystać ze składowych publicznych i chronionych klasy

&:

< = '+.7  



)7  !)! ) 

background image

80

C++. Strategie i taktyki. Vademecum profesjonalisty

7&>=  ??

7@ ,+:;

 4

 >



Użytkownicy klasy

$ % nie mogą jednak wywoływać żadnych składowych klasy &:



 )7 95554A4A9

  &'  /"*  

" !   !

Użytkownicy nie mogą również wykonać niejawnej konwersji wskaźnika do klasy pochodnej na
wskaźnik do klasy bazowej:

 7

 )7 95554A4A9

8 +,!&2/"*  8 +

 !** !*



Przy zastosowaniu dziedziczenia prywatnego, składowe publiczne i chronione klasy bazowej stają
się chronione w klasie pochodnej. Klasy pochodne mogą wywoływać funkcje składowe chronio-
nej klasy bazowej, a także niejawnie przekonwertować wskaźnik do klasy pochodnej na wskaźnik
do chronionej klasy bazowej.

Składową publiczną prywatnej lub chronionej klasy bazowej można uczynić publiczną w klasie po-
chodnej za pomocą tzw. deklaracji dostępu:

8 +



'''

  



 )7  8 +



'''

8 +  0 %



Dzięki temu użytkownicy będą mieli możliwość wywoływania dla obiektu typu

$ %

funkcji

 tak, jak gdyby została ona zadeklarowana w następujący sposób:

 )7  8 +



'''

    8 +  



wydajności ani wymagać dodatkowego obszaru pamięci, a powstała w ten sposób klasa
będzie łatwiejsza do zrozumienia, ponieważ czytając kod nie będzie trzeba pamiętać,
które funkcje składowe dziedziczone są po prywatnej klasie bazowej.

Implementacja przykładowej klasy

$ %

zaprezentowanej w ramce „Powtórka:

Dziedziczenie publiczne, chronione i prywatne” powinna zostać zmodyfikowana w na-
stępujący sposób:

background image

Rozdział 4.



Dziedziczenie

81

< = '+.7  

8 + B! #-  



8 ++,&99

+ :;- 

  



 )7



8 +



 )7 +,

 !)! ) 





)7  !)! ) 

7&>='  ??

7@ :;

 4

 >



Jedyne zmiany w treści kodu funkcji składowych klasy

$ %

wynikają z ko-

nieczności uściślenia niejawnych odwołań do składowych klasy bazowej

&

nazwą

składowej



typu

&

. Zmiana ta jest niewidoczna dla użytkowników — ich kod

będzie działał jak dotąd. Mało prawdopodobne jest również, żeby zmianie uległy czas
wykonania lub przestrzeń wykorzystywana przez ich programy. I w jednym, i w drugim
przypadku obiekt musi zawierać jedną kopię składnika odpowiadającego klasie

&

.

W większości przypadków klasa nieposiadająca klas bazowych będzie łatwiejsza do zro-
zumienia i rozbudowy niż równoważna klasa wykorzystująca dziedziczenie prywatne.
Zastosowanie złożenia oznacza również, że późniejsze dodanie nowej klasy bazowej
będzie wymagać dziedziczenia pojedynczego, a nie wielokrotnego. Na wielu platformach
kod wykorzystujący dziedziczenie wielokrotne jest zauważalnie wolniejszy i większy
od kodu, którym zastosowano dziedziczenie pojedyncze, a ponadto jest zawsze trudniej-
szy do zrozumienia.

Wyjątek od tej reguły ma miejsce w przypadku, gdy w klasie pochodnej trzeba przesłonić
funkcję wirtualną klasy bazowej, a nie chcemy tej klasy bazowej udostępniać w pu-
blicznym interfejsie. Dziedziczenie prywatne stanowi w takiej sytuacji najprostsze, a nie-
kiedy jedyne rozwiązanie (jeśli przesłaniana funkcja wirtualna to destruktor).

Załóżmy, na przykład, że korzystamy ze środowiska języka C++, które obsługuje me-
chanizm tzw. zbierania nieużytków (ang. garbage collection) w przypadku obiektów wy-
prowadzonych z klasy

'" 

. Każde wywołanie funkcji

"   (

będzie

powodować usunięcie tych „zbieralnych” obiektów, do których nie można się odwołać
za pomocą istniejących wskaźników:

(



(

 C( 



background image

82

C++. Strategie i taktyki. Vademecum profesjonalisty



) 

(, 

!+ &  ))

  



Załóżmy ponadto, że projektujemy klasę podlegającą procesowi zbieraniu nieużytków,
która reprezentuje węzły grafu. Chociaż przed użytkownikami nie będziemy mogli naj-
prawdopodobniej ukryć faktu, że nasz węzeł podlega zbieraniu nieużytków, to możemy
ukryć wybór procedury zbierania nieużytków. Realizujemy to przez użycie klasy

'"



jako prywatnej klasy bazowej:

D  (



 CD 

  $" %



Przesłaniając destruktor wirtualny zapewniamy, że instrukcja

()

występująca

w treści funkcji

"   

w przypadku, gdy będzie dotyczyć obiektu klasy

* 

, spowoduje wywołanie destruktora klasy

* 

. Dzięki zastosowaniu dziedzicze-

nia prywatnego zachowujemy możliwość zmiany implementacji klasy

* 

polegającej

na użyciu jakiegoś innego mechanizmu zbierania nieużytków.

     

Dziedziczenie chronione stosowane jest w przypadku, gdy dziedziczenie wchodzi w skład
interfejsu dla klas pochodnych, lecz nie jest elementem interfejsu dla użytkowników.
Chroniona klasa bazowa jest jak prywatna klasa bazowa, która jest znana wszystkim
klasom pochodnym:

8 + ,''',

 )7   8 + ,''',

 )!  )7  ,''',

Funkcje składowe klasy

$ 

mają dostęp do składowych publicznych i chro-

nionych części podchodzącej od klasy

&

.

Autor osobiście nigdy nie wykorzystywał dziedziczenia chronionego i nigdy nie słyszał
także o jego zastosowaniu w poważnym projekcie. Wszystkie powody do niestosowania
dziedziczenia prywatnego dotyczą również dziedziczenia chronionego — zamiast chro-
nionej klasy bazowej prościej jest zazwyczaj posiadać chronioną składową:

8 + ,''',

 )7

 

8 +

background image

Rozdział 4.



Dziedziczenie

83

  $" %



 )!  )7  ,''',

Taka przeróbka znacznie upraszcza hierarchię dziedziczenia. Wydajność jest taka sama,
a funkcje składowe klasy

$ 

mają wciąż dostęp do części obiektu pocho-

dzącej od klasy

&

(chociaż teraz muszą odwoływać się do składowej



).

Nie oznacza to wcale, że dziedziczenie chronione nigdy się nie przydaje — jeśli skła-
dowe klasy pochodnej muszą przesłonić funkcje wirtualne występujące w (chronionej)
klasie bazowej, to dziedziczenie chronione może stanowić odpowiednie rozwiązanie.
Jeśli jednak można zastosować złożenie, to tak należy zrobić — korzystanie z mało
znanych „zakamarków” języka (takich jak dziedziczenie chronione) sprawia, że programy
są trudniejsze do zrozumienia.

 !   "#  $   

W klasie pochodnej można przesłonić wirtualną funkcję składową klasy bazowej, dekla-
rując ją ponownie z tą samą nazwą i z taką samą listą argumentów:

 



        

    



+   



        



6)  !   



        



Przy dziedziczeniu istnieje jednak znacznie silniejsze ograniczenie dotyczące składowych

 

klas



oraz

+   

niż samo wymaganie poprawności typów.

Funkcja składowa klasy pochodnej powinna być zgodna z modelem abstrakcyjnym klasy
bazowej. Chociaż poszczególne implementacje mogą być rożne, to każdy obiekt klasy
wyprowadzonej z klasy



powinien „przyspieszać tak, jak robi to



” — co-

kolwiek miałoby to znaczyć. Jest to ograniczenie semantyczne — nie można go wyrazić
w języku C++, kompilator C++ nie może więc sprawdzić, czy zostało spełnione.

W przypadku klasy



, model abstrakcyjny funkcji

 

mógłby określać, że

przyspieszenie pojazdu zmienia jego

  

o określoną wartość, tj.:

( ) (

)

x

predkosc

predkosc

x

przyspiesz

stara

nowa

+

==

Gdy tylko ta część abstrakcji zostanie opisana, wszystkie klasy pochodne powinny być
z nią zgodne.

background image

84

C++. Strategie i taktyki. Vademecum profesjonalisty

Dlaczego jest to ważne? Jeśli wszystkie klasy pochodne są zgodne z modelem abstrak-
cyjnym, użytkownicy mogą tworzyć kod oparty na tym modelu:



  2 

 '    - '  



i kod ten będzie działać w przypadku wszystkich Pojazdów:

6)  !  

  

+ !

 !

W przyszłości mogą zostać dodane nowe rodzaje obiektów typu



i będą one po-

prawnie działać z kodem, który został zaimplementowany w czasach, kiedy one jeszcze
nie istniały!

  



        



)EFE

 )EFEG %3H# @

Zaimplementowaliśmy funkcję, która, dzięki zastosowaniu wywołań kilku operacji abs-
trakcyjnych (wirtualnych funkcji składowych), działa z każdą klasą, która jest wyprowa-
dzona z klasy



i poprawnie realizuje te operacje abstrakcyjne, nie posiadając jedno-

cześnie żadnej innej wiedzy na temat tych obiektów. To jest właśnie jedna z głównych
zalet projektowania obiektowego.

Jeśli związek pomiędzy składowymi

  

i

 

nie będzie wyraźnie udoku-

mentowany i rozumiany przez projektantów, znajdzie się ktoś, kto zaimplementuje klasę
pochodną, która nie będzie zgodna z modelem abstrakcyjnym, np.:

0 + 



        





0       

0    *  @

+     A, 



Autor klasy

, 

źle zrozumiał, jak powinna działać funkcja składowa

 

.

Dlatego klasa

, 

nie jest zgodna z modelem abstrakcyjnym klasy



.

Rozważmy, co się stanie, jeśli dla obiektu typu

, 

poruszającego się z prędkością

100 km/h wywołamy funkcję

 

. Funkcja

 

wykona następujące wywołanie

'    - '  

background image

Rozdział 4.



Dziedziczenie

85

które w tym przypadku spowoduje wywołanie funkcji

0    -4>>

co z kolei wywoła funkcję

+     -A>>

Po wywołaniu funkcji

 

nasz

, 

będzie jechał z prędkością 100 km/h

w przeciwnym kierunku! Z pewnością programista nie to miał na myśli. Pomimo że kod
spełnia ograniczenia dotyczące typów narzucane przez język — kompilacja przebiega
bez problemów — to jego działanie nie jest prawidłowe, ponieważ klasa

, 

nie

jest zgodna z modelem abstrakcyjnym klasy



.

 % &  

Nasza pierwotna klasa



zawiera deklarację funkcji składowej

 

. Umie-

ściliśmy ją w tej klasie, ponieważ

 

jest operacją, która jest pojęciowo po-

prawna dla wszystkich pojazdów. Oczekujemy, że wersja tej funkcji występująca w kla-
sie bazowej zostanie przesłonięta w każdej klasie pochodnej.

W jaki sposób powinniśmy zaimplementować funkcję

  

? Nie prze-

widujemy w ogóle tworzenia obiektów typu



. Klasa



jest za to klasą bazową,

która opisuje pojęcia wspólne dla zbioru klas pochodnych. W zamierzeniu klasa



ma być używana wyłącznie jako klasa bazowa, a funkcja

 

zostanie przesło-

nięta w każdej klasie pochodnej. Nie spodziewamy się więc, żeby ktoś kiedykolwiek
wywołał funkcję

  

. Jedno podejście mogłoby polegać na zdefinio-

waniu wersji, która w przypadku wywołania wyświetli komunikat o błędzie:



       

==9D!"7      IJ9





lecz takie podejście będzie wykrywać brak przesłonięcia funkcji

 

dopiero

podczas wykonywania. Lepszym rozwiązaniem będzie wykorzystanie pewnego mecha-
nizmu języka C++, który pozwoli wykryć to podczas kompilacji — przez deklarację funk-
cji

  

jako tzw. funkcji czysto wirtualnej.

Dzięki zadeklarowaniu klasy



jako klasy abstrakcyjnej, kompilator będzie genero-

wać błąd kompilacji przy każdej próbie utworzenia obiektu typu



. Nie musimy sobie

zadawać trudu definiowania namiastek funkcji dla funkcji składowych klasy bazowej.
Z tego powodu zastosowanie funkcji czysto wirtualnych i abstrakcyjnych klas bazowych
zalecane jest w przypadku klas takich jak



, które opisują zbiory klasy pochodnych.

Destruktor nigdy nie powinien być funkcją czysto wirtualną:

 



 C &>(" "

  $" %



background image

86

C++. Strategie i taktyki. Vademecum profesjonalisty

Powtórka: Funkcje czysto wirtualne i abstrakcyjne klasy bazowe

Wirtualna funkcja składowa, w której w deklaracji po liście argumentów występuje wyrażenie

-(.:

K

  7&>



jest tzw. funkcją czysto wirtualną. Nie trzeba podawać żadnej definicji funkcji czysto wirtualnej
/%. Każda klasa, która deklaruje lub dziedziczy funkcję czysto wirtualną jest abstrakcyjną klasą
bazową. Próba utworzenia obiektu abstrakcyjnej klasy bazowej spowoduje błąd podczas kompilacji.

Jeśli w klasie wyprowadzonej z klasy

/ funkcja /% zostanie przesłonięta, to ta klasa będzie już

klasą konkretną (nieabstrakcyjną):

0 K

 7



Abstrakcyjna klasa bazowa służy do deklarowania interfejsu bez deklarowania pełnego zbioru
implementacji dla tego interfejsu. Taki interfejs określa operacje abstrakcyjne realizowane przez
wszystkie obiekty wyprowadzone z tej klasy — obowiązek zapewnienia implementacji dla tych
operacji abstrakcyjnych spoczywa już na klasach pochodnych. Na przykład:

 



         &>

     &>



Ponieważ klasa

 jest abstrakcyjna, próba utworzenia obiektu typu  powoduje błąd

kompilacji:

  /"*  

 

Aby móc użyć klasy

 0( dla niej utworzyć klasy pochodne:

+   



         

     



L!  



         

     



Ponieważ w klasach

 oraz 1 wszystkie funkcje czysto wirtualne klasy bazowej zostały

przesłonięte, możemy tworzyć obiekty obydwu klas.

Chociaż nie mogą istnieć żadne obiekty typu

 , to jednak możemy używać wskaźników i refe-

rencji do tego typu:



  2 

 '    - '  



background image

Rozdział 4.



Dziedziczenie

87

Klasa pochodna, która dziedziczy (nie przesłania) funkcję czysto wirtualną jest także abstrakcyjna:

 ) !  



 ) ! /"*  

 ) !

Destruktor

 2

będą wywoływać destruktory każdej klasy wyprowadzonej

z klasy



. Ponieważ definicja tego destruktora musi istnieć (w przeciwnym razie otrzy-

mamy błędy modułu ładującego), deklarowanie go jako czysto wirtualnego nie ma sensu.

 ' ()*  *    $

Sposób obsługi dziedziczenia przez język C++ zawiera kilka sztuczek. Przyjrzyjmy się im:

 !"   # $     

Podczas korzystania z mechanizmu dziedziczenia należy zawsze pamiętać o elementach,
które nie są dziedziczone po klasie bazowej:



Konstruktory (w tym konstruktor kopiujący). Jeśli nie zadeklarujemy konstruktora
kopiującego, automatycznie zostanie utworzony konstruktor kopiujący, który
będzie wywoływać konstruktory kopiujące niestatycznych danych składowych
oraz klas bazowych.



Destruktor. Jeśli nie zadeklarujemy destruktora, a dowolna z niestatycznych danych
składowych lub klas bazowych posiada destruktor, to automatycznie zostanie
utworzony destruktor, który będzie wywoływać destruktory niestatycznych danych
składowych oraz klas bazowych. Destruktor ten będzie wirtualny, jeśli dowolna
z klas bazowych posiada destruktor wirtualny.



Operator przypisania. Jeśli nie zadeklarujemy operatora przypisania, automatycznie
zostanie utworzony operator przypisania, który będzie wywoływać operatory
przypisania niestatycznych danych składowych oraz klas bazowych.



Ukryte funkcje składowe. Jeśli funkcja składowa klasy bazowej nie jest przesłonięta
w klasie pochodnej, a w tej klasie pochodnej zadeklarowana jest funkcja o tej samej
nazwie, lecz o różnych argumentach, to funkcja występująca w klasie bazowej
będzie ukryta. Na przykład:

+ 



 "*#M %

   



N  



N  



background image

88

C++. Strategie i taktyki. Vademecum profesjonalisty

O)+  + 



 "*#M %

  N  2P!7 %+  



Nasz

3 

może być kierowany za pomocą autopilota, lecz

jednocześnie ukryliśmy wersję funkcji

 

występującą w klasie bazowej:

O)+ 

' F5/"*  H ! M

  N    

Jeśli nie chcemy, aby funkcja klasy bazowej była ukryta, musimy ją ponownie
zadeklarować w klasie pochodnej:

O)+  + 



 "*#M %

   +  

  N  2



Niektóre kompilatory języka C++, w przypadku gdy funkcja zadeklarowana
w klasie pochodnej powoduje ukrycie funkcji klasy bazowej, generują ostrzeżenie.

 %    #&    

Przy przesłanianiu funkcji wirtualnej (lub czysto wirtualnej) nie trzeba jawnie określać
słowa kluczowego

# 

— kompilator „zauważy”, że dana funkcja składowa posiada

tę samą nazwę i te samy typy argumentów co funkcja wirtualna zadeklarowana w klasie
bazowej:

/ !



  7



+  / !



 7$!!H 9  79



Umieszczanie słowa kluczowego

# 

jest jednak dobrym zwyczajem w takim przy-

padku, ponieważ dzięki niemu kod staje się bardziej oczywisty. Znaczenie programu
jest w obydwu przypadkach takie samo.

 '    

  " ( (

W przypadku gdy funkcje wirtualne wywoływane są z poziomu konstruktora lub de-
struktora obiektu, ich działanie jest nieco inne. Gdy konstruktor tworzy część bazową
klasy pochodnej, konstruowany obiekt traktowany jest tak, jakby był obiektem klasy
bazowej, a nie klasy pochodnej. Oznacza to, że wywołanie funkcji wirtualnej spowoduje

background image

Rozdział 4.



Dziedziczenie

89

wykonanie takiej wersji tej funkcji, która będzie odpowiednia dla klasy bazowej, której
konstruktor będzie aktualnie wykonywany, a



dla klasy pochodnej.

Na przykład:

/ !



/ !

   ) 

  ==9/ !/ !J9



+  / !



+ 

   ) 

  ==9+ + J9



/ !/ !

 ) 





/ !

+  



Program ten wypisze na ekranie:

/ !/ !

/ !/ !

Wywołanie funkcji

  

w treści konstruktora klasy

 

będzie zawsze

powodować wywołanie składowej

    

, nawet jeśli konstruktor

ten tworzy część typu

 

obiektu typu

 

.

To zagadkowe zachowanie spowodowane jest faktem, że części obiektu pochodzące od
klasy bazowej konstruowane są przed jego danymi składowymi. W momencie tworzenia
części

 

obiektu typu

 

, nie istnieje jeszcze żadna z danych składowych klasy

 

. Wywołanie wersji funkcji wirtualnej z klasy

 

nie miałoby więc sensu,

ponieważ wersja ta próbowałby prawdopodobnie odwoływać się do niezainicjalizowanych
danych składowych klasy

 

(patrząc na ten problem z innej strony, można powie-

dzieć, że gdy wywoływany jest konstruktor klasy

 

, obiekt nie jest jeszcze właściwie

obiektem typu

 

, więc składowe klasy

 

nie powinny być wywoływane).

Ta sama logika ma zastosowanie wobec wywołań funkcji wirtualnych w treści destruktorów:

/ !C/ !

 ) 



Ten destruktor będzie zawsze wywoływał funkcję

    

, nawet jeśli

niszczymy część typu

 

obiektu typu

 

. W chwili wywołania destruktora klasy

 

dane składowe klasy

 

są już zniszczone, więc wywołanie wersji funkcji

  

z klasy

 

nie miałoby sensu.

background image

90

C++. Strategie i taktyki. Vademecum profesjonalisty

Pamiętajmy, że takie szczególne zachowanie ma miejsce tylko w przypadku, gdy funkcja
wirtualna wywoływana jest dla obiektu będącego w trakcie konstrukcji lub niszczenia.
Wywołanie funkcji wirtualnej dla jakiegoś innego obiektu będzie działać normalnie,
nawet jeśli ma miejsce w treści konstruktora czy destruktora:

/ !/ !

 ) D!" 7 %/ ! ) 

/ !, &!+ 

 -. ) D!" 7 %+  ) 



 + , )



Dziedziczenie jest związkiem generalizacji (specjalizacji), czyli relacją typu jest
— obiekty implementowane przez klasę pochodną powinny reprezentować podzbiór
obiektów implementowanych przez klasę bazową.



Dziedziczenie publiczne stosujemy w przypadku, gdy dziedziczenie jest elementem
interfejsu. Dziedziczenie prywatne lub chronione stosujemy tylko wtedy, gdy
dziedziczenie stanowi ukryty szczegół implementacyjny.



W większości przypadków zamiast dziedziczenia prywatnego należy zastosować
złożenie — wyjątkiem jest sytuacja, gdy w klasie pochodnej trzeba przesłonić
funkcję wirtualną zadeklarowaną w (prywatnej) klasie bazowej.



Funkcja wirtualna przesłonięta w klasie pochodnej powinna być zgodna z modelem
abstrakcyjnym klasy bazowej.



Konstruktory, destruktory oraz operatory przypisania nie podlegają dziedziczeniu.

 - .



Załóżmy, że w naszej hierarchii klas



chcielibyśmy zdefiniować

  



pojazdu lądowego jako synonim jego składowej

  

. W jaki sposób

wpłynie to na hierarchię klas



? Czy zmiana ta uprości, czy raczej utrudni

korzystanie z tych klas?



W jaki sposób zapewnisz, aby klasa



potwierdzała, że każda implementacja

funkcji

 

w klasie pochodnej jest zgodna z modelem abstrakcyjnym?

(Wskazówka: możesz sprawić, aby klasy pochodne zamiast funkcji

 

przesłaniały jakieś inne funkcje.) Jaki to będzie miało wpływ na czas wykonania?



Funkcję czysto wirtualną można (lecz nie trzeba) zdefiniować. Taką funkcję
można później wywołać jedynie bezpośrednio przy użyciu specyfikatora



:

+ 

'     A>

W jaki sposób można by wykorzystać tę właściwość? Czy istnieje jakieś lepsze
rozwiązanie, które nie będzie wykorzystywać takiej niejasnej cechy języka?


Wyszukiwarka

Podobne podstrony:
Asembler dla procesorow Intel Vademecum profesjonalisty asinvp
CorelDRAW 11 Vademecum profesjonalisty Tom 2
C Szablony Vademecum profesjonalisty cpszav
Delphi dla NET Vademecum profesjonalisty
Firewalle i bezpieczenstwo w sieci Vademecum profesjonalisty firevp
Jezyk C Wskazniki Vademecum profesjonalisty cwskvp
PHP Programowanie w systemie Windows Vademecum profesjonalisty
Firewalle i bezpieczeństwo w sieci Vademecum profesjonalisty
Język C WskaĽniki Vademecum profesjonalisty
Protokoly SNMP i RMON Vademecum profesjonalisty
C++ Szablony Vademecum profesjonalisty
ASP NET Vademecum profesjonalisty
C++ Programowanie zorientowane obiektowo Vademecum profesjonalisty
helion windows 2000 server vademecum profesjonalisty 8 projektowanie domen windows 2000 YHNURPZ44
BIOS i usuwanie usterek Vademecum profesjonalisty biosvp
perełki programowania gier vademecum profesjonalisty tom i (fragment) wykrywanie zdarzeń w trójwymi
Firewalle i bezpieczenstwo w sieci Vademecum profesjonalisty firevp

więcej podobnych podstron