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?