Wydawnictwo Helion
ul. Chopina 6
44-100 Gliwice
tel. (32)230-98-63
IDZ DO
IDZ DO
KATALOG KSI¥¯EK
KATALOG KSI¥¯EK
TWÓJ KOSZYK
TWÓJ KOSZYK
CENNIK I INFORMACJE
CENNIK I INFORMACJE
CZYTELNIA
CZYTELNIA
C++. Strategie i taktyki.
Vademecum profesjonalisty
Autor: Robert B. Murray
T³umaczenie: Przemys³aw Steæ
ISBN: 83-7361-323-4
Tytu³ orygina³u:
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.
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
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
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
)
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ą:
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
:
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:
) ! ,''',
)!
)
$"%
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
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 !)! )
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:
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(
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 +
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.
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
' -'
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 &>(""
$"%
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
' -'
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
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
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.
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?