C++. Strategie i taktyki. Vademecum profesjonalisty
-
Upload
wydawnictwo-helion -
Category
Technology
-
view
555 -
download
0
description
Transcript of C++. Strategie i taktyki. Vademecum profesjonalisty
Wydawnictwo Helion
ul. Chopina 6
44-100 Gliwice
tel. (32)230-98-63
e-mail: [email protected]
PRZYK£ADOWY ROZDZIA£PRZYK£ADOWY ROZDZIA£
IDZ DOIDZ DO
ZAMÓW DRUKOWANY KATALOGZAMÓW DRUKOWANY KATALOG
KATALOG KSI¥¯EKKATALOG KSI¥¯EK
TWÓJ KOSZYKTWÓJ KOSZYK
CENNIK I INFORMACJECENNIK I INFORMACJE
ZAMÓW INFORMACJEO NOWO�CIACH
ZAMÓW INFORMACJEO NOWO�CIACH
ZAMÓW CENNIKZAMÓW CENNIK
CZYTELNIACZYTELNIA
FRAGMENTY KSI¥¯EK ONLINEFRAGMENTY KSI¥¯EK ONLINE
SPIS TRE�CISPIS TRE�CI
DODAJ DO KOSZYKADODAJ DO KOSZYKA
KATALOG ONLINEKATALOG 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³a�ciwych strategii pomaga unikaæ pu³apek
i pracowaæ o wiele skuteczniej. Rob Murray dziel¹c siê swoim do�wiadczeniem 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êtno�ci, ka¿dy rozdzia³
koñczy siê list¹ poruszonych w nim kluczowych zagadnieñ oraz pytaniami maj¹cymi
spowodowaæ przemy�lenia i dyskusje.
Ksi¹¿ka przedstawia miêdzy innymi:
• Tworzenie w³a�ciwych 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. Wcze�nie 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.
����������������� �
� ��������� ��
���������� �������� ��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
����������� ������������� ������0�����122 ���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�����23�1������1������4��������"������������ �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 wszystkichformach 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 dziedziczeniaróżnią się elementami, które są widoczne dla użytkownika klasy pochodnej (a nie twórcy klasypochodnej) 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 klasiepochodnej:
1������5������ �!$����"������������ �5�'������������61�-�7 ���������������������"��!*�� �����*��������������� �����������������!��
Wskaźnik do klasy pochodnej może zostać niejawnie przekonwertowany na wskaźnik do publicznejklasy bazowej:
1�����,����&���!�1����5������61�-�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 nawskaź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źnikdo 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.:
( ) ( )xpredkoscpredkoscxprzyspieszstaranowa
+==⇒
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���������������)EFE������G�����������%3�H�������#����������@
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 ��������������������IJ�9��������������
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 zbioruimplementacji dla tego interfejsu. Taki interfejs określa operacje abstrakcyjne realizowane przezwszystkie obiekty wyprowadzone z tej klasy — obowiązek zapewnienia implementacji dla tychoperacji abstrakcyjnych spoczywa już na klasach pochodnych. Na przykład:
������������� ������������� ���� �������������� �����&�>��������� ���� ��������������&�>���
Ponieważ klasa ���� jest abstrakcyjna, próba utworzenia obiektu typu ���� powoduje błądkompilacji:
�����������/"*�����������������������������������������������������
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łyprzesł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 wyprowadzonejz 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 konstruktorakopiującego, automatycznie zostanie utworzony konstruktor kopiujący, którybędzie wywoływać konstruktory kopiujące niestatycznych danych składowychoraz klas bazowych.
� Destruktor. Jeśli nie zadeklarujemy destruktora, a dowolna z niestatycznych danychskładowych lub klas bazowych posiada destruktor, to automatycznie zostanieutworzony destruktor, który będzie wywoływać destruktory niestatycznych danychskładowych oraz klas bazowych. Destruktor ten będzie wirtualny, jeśli dowolnaz klas bazowych posiada destruktor wirtualny.
� Operator przypisania. Jeśli nie zadeklarujemy operatora przypisania, automatyczniezostanie utworzony operator przypisania, który będzie wywoływać operatoryprzypisania niestatycznych danych składowych oraz klas bazowych.
� Ukryte funkcje składowe. Jeśli funkcja składowa klasy bazowej nie jest przesłoniętaw klasie pochodnej, a w tej klasie pochodnej zadeklarowana jest funkcja o tej samejnazwie, lecz o różnych argumentach, to funkcja występująca w klasie bazowejbędzie ukryta. Na przykład:
�����������+���� ������������������"*����#M�������%�������������� �����������������
������N ���������� ���������N ������������
88 C++. Strategie i taktyki. Vademecum profesjonalisty
������O�����������)�����+����� ����������+���� ������������������"*����#M�������%�������������� ��N �������2������P���!��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���� �������7��9��
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/��!���/��!����J�9�����
���������+������� �����/��!���� ������������+�������������� ���������� �����)������������������ ��==�9���+��������+������J�9�����
/��!���/��!������������ �����)��������
������������/��!�����������+�������
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?