Professional Documents
Culture Documents
C++. 50 Efektywnych Sposobów Na Udoskonalenie Twoich Programów
C++. 50 Efektywnych Sposobów Na Udoskonalenie Twoich Programów
PRZYKADOWY ROZDZIA
SPIS TRECI
KATALOG KSIEK
KATALOG ONLINE
ZAMW DRUKOWANY KATALOG
TWJ KOSZYK
DODAJ DO KOSZYKA
CENNIK I INFORMACJE
ZAMW INFORMACJE
O NOWOCIACH
ZAMW CENNIK
CZYTELNIA
FRAGMENTY KSIEK ONLINE
C++. 50 efektywnych
sposobw na udoskonalenie
Twoich programw
Autor: Scott Meyers
Tumaczenie: Mikoaj Szczepaniak
ISBN: 83-7361-345-5
Tytu oryginau: Effective C++: 50 Specific Ways
to Improve Your Programs and Design
Format: B5, stron: 248
Pierwsze wydanie ksiki C++. 50 efektywnych sposobw na udoskonalenie twoich
programw zostao sprzedane w nakadzie 100 000 egzemplarzy i zostao
przetumaczone na cztery jzyki. Nietrudno zrozumie, dlaczego tak si stao.
Scott Meyers w charakterystyczny dla siebie, praktyczny sposb przedstawi wiedz
typow dla ekspertw czynnoci, ktre niemal zawsze wykonuj lub czynnoci,
ktrych niemal zawsze unikaj, by tworzy prosty, poprawny i efektywny kod.
Kada z zawartych w tej ksice pidziesiciu wskazwek jest streszczeniem metod
pisania lepszych programw w C++, za odpowiednie rozwaania s poparte
konkretnymi przykadami. Z myl o nowym wydaniu, Scott Meyers opracowa
od pocztku wszystkie opisywane w tej ksice wskazwki. Wynik jego pracy jest
wyjtkowo zgodny z midzynarodowym standardem C++, technologi aktualnych
kompilatorw oraz najnowszymi trendami w wiecie rzeczywistych aplikacji C++.
Do najwaniejszych zalet ksiki C++. 50 efektywnych sposobw na udoskonalenie
twoich programw nale:
Eksperckie porady dotyczce projektowania zorientowanego obiektowo,
projektowania klas i waciwego stosowania technik dziedziczenia
Analiza standardowej biblioteki C++, wcznie z wpywem standardowej biblioteki
szablonw oraz klas podobnych do string i vector na struktur dobrze napisanych
programw
Rozwaania na temat najnowszych moliwoci jzyka C++: inicjalizacji staych
wewntrz klas, przestrzeni nazw oraz szablonw skadowych
Wiedza bdca zwykle w posiadaniu wycznie dowiadczonych programistw
Ksika C++. 50 efektywnych sposobw na udoskonalenie twoich programw
pozostaje jedn z najwaniejszych publikacji dla kadego programisty pracujcego z C++.
Wydawnictwo Helion
ul. Chopina 6
44-100 Gliwice
tel. (32)230-98-63
e-mail: helion@helion.pl
Spis treci
Przedmowa ................................................................................................................... 7
Podzikowania ............................................................................................................ 11
Wstp......................................................................................................................... 15
Przejcie od jzyka C do C++....................................................................................... 27
Sposb 1. Wybieraj const i inline zamiast #define......................................................................28
Sposb 2. Wybieraj <iostream> zamiast <stdio.h>.....................................................................31
Sposb 3. Wybieraj new i delete zamiast malloc i free...............................................................33
Sposb 4. Stosuj komentarze w stylu C++ ..................................................................................34
Zarzdzanie pamici .................................................................................................. 37
Sposb 5. Uywaj tych samych form w odpowiadajcych sobie zastosowaniach
operatorw new i delete ..............................................................................................38
Sposb 6. Uywaj delete w destruktorach dla skadowych wskanikowych ..............................39
Sposb 7. Przygotuj si do dziaania w warunkach braku pamici.............................................40
Sposb 8. Podczas pisania operatorw new i delete trzymaj si istniejcej konwencji ..............48
Sposb 9. Unikaj ukrywania normalnej formy operatora new ................................................51
Sposb 10. Jeli stworzye wasny operator new, opracuj take wasny operator delete ............53
Konstruktory, destruktory i operatory przypisania ......................................................... 61
Sposb 11. Deklaruj konstruktor kopiujcy i operator przypisania dla klas
z pamici przydzielan dynamicznie ........................................................................61
Sposb 12. Wykorzystuj konstruktory do inicjalizacji, a nie przypisywania wartoci .................64
Sposb 13. Umieszczaj skadowe na licie inicjalizacji w kolejnoci zgodnej
z kolejnoci ich deklaracji.........................................................................................69
Sposb 14. Umieszczaj w klasach bazowych wirtualne destruktory ............................................71
Sposb 15. Funkcja operator= powinna zwraca referencj do *this ...........................................76
Sposb 16. Wykorzystuj operator= do przypisywania wartoci do wszystkich skadowych klasy......79
Sposb 17. Sprawdzaj w operatorze przypisania, czy nie przypisujesz wartoci samej sobie......82
Klasy i funkcje projekt i deklaracja.......................................................................... 87
Sposb 18. Staraj si dy do kompletnych i minimalnych interfejsw klas..............................89
Sposb 19. Rozrniaj funkcje skadowe klasy, funkcje niebdce skadowymi
klasy i funkcje zaprzyjanione....................................................................................93
Spis treci
Sposb 20. Unikaj deklarowania w interfejsie publicznym skadowych reprezentujcych dane ........98
Sposb 21. Wykorzystuj stae wszdzie tam, gdzie jest to moliwe...........................................100
Sposb 22. Stosuj przekazywanie obiektw przez referencje, a nie przez wartoci ...................106
Sposb 23. Nie prbuj zwraca referencji, kiedy musisz zwrci obiekt ...................................109
Sposb 24. Wybieraj ostronie pomidzy przecianiem funkcji
a domylnymi wartociami parametrw ...................................................................113
Sposb 25. Unikaj przeciania funkcji dla wskanikw i typw numerycznych......................117
Sposb 26. Strze si niejednoznacznoci...................................................................................120
Sposb 27. Jawnie zabraniaj wykorzystywania niejawnie generowanych funkcji
skadowych, ktrych stosowanie jest niezgodne z Twoimi zaoeniami..................123
Sposb 28. Dziel globaln przestrze nazw ................................................................................124
Implementacja klas i funkcji ...................................................................................... 131
Sposb 29. Unikaj zwracania uchwytw do wewntrznych danych .......................................132
Sposb 30. Unikaj funkcji skadowych zwracajcych zmienne wskaniki
lub referencje do skadowych, ktre s mniej dostpne od tych funkcji ..................136
Sposb 31. Nigdy nie zwracaj referencji do obiektu lokalnego ani do wskanika
zainicjalizowanego za pomoc operatora new wewntrz tej samej funkcji .............139
Sposb 32. Odkadaj definicje zmiennych tak dugo, jak to tylko moliwe ...............................142
Sposb 33. Rozwanie stosuj atrybut inline ................................................................................144
Sposb 34. Ograniczaj do minimum zalenoci czasu kompilacji midzy plikami....................150
Dziedziczenie i projektowanie zorientowane obiektowo ............................................... 159
Sposb 35. Dopilnuj, by publiczne dziedziczenie modelowao relacj jest ............................160
Sposb 36. Odrniaj dziedziczenie interfejsu od dziedziczenia implementacji ........................166
Sposb 37. Nigdy nie definiuj ponownie dziedziczonych funkcji niewirtualnych .....................174
Sposb 38. Nigdy nie definiuj ponownie dziedziczonej domylnej wartoci parametru ............176
Sposb 39. Unikaj rzutowania w d hierarchii dziedziczenia....................................................178
Sposb 40. Modelujc relacje posiadania (ma) i implementacji z wykorzystaniem,
stosuj podzia na warstwy .........................................................................................186
Sposb 41. Rozrniaj dziedziczenie od stosowania szablonw ................................................189
Sposb 42. Dziedziczenie prywatne stosuj ostronie ..................................................................193
Sposb 43. Dziedziczenie wielobazowe stosuj ostronie............................................................199
Sposb 44. Mw to, o co czym naprawd mylisz. Zdawaj sobie spraw z tego, co mwisz ....213
Rozmaitoci .............................................................................................................. 215
Sposb 45. Miej wiadomo, ktre funkcje s niejawnie tworzone i wywoywane przez C++....... 215
Sposb 46. Wykrywanie bdw kompilacji i czenia jest lepsze
od wykrywania bdw podczas wykonywania programw ....................................219
Sposb 47. Upewnij si, e nielokalne obiekty statyczne s inicjalizowane przed ich uyciem .......222
Sposb 48. Zwracaj uwag na ostrzeenia kompilatorw...........................................................226
Sposb 49. Zapoznaj si ze standardow bibliotek C++ ...........................................................227
Sposb 50. Pracuj bez przerwy nad swoj znajomoci C++ .....................................................234
Skorowidz ................................................................................................................. 239
Dziedziczenie
i projektowanie
zorientowane obiektowo
Wielu programistw wyraa opini, e moliwo dziedziczenia jest jedyn korzyci
pync z programowania zorientowanego obiektowo. Mona mie oczywicie rne
zdanie na ten temat, jednak liczba zawartych w innych czciach tej ksiki sposobw
powiconych efektywnemu programowaniu w C++ pokazuje, e masz do dyspozycji
znacznie wicej rozmaitych narzdzi, ni tylko okrelanie, ktre klasy powinny dziedziczy po innych klasach.
Projektowanie i implementowanie hierarchii klas rni si od zasadniczo od wszystkich mechanizmw dostpnych w jzyku C. Problem dziedziczenia i projektowania
zorientowanego obiektowo z pewnoci zmusza do ponownego przemylenia swojej
strategii konstruowania systemw oprogramowania. Co wicej, jzyk C++ udostpnia
bardzo szeroki asortyment blokw budowania obiektw, wcznie z publicznymi,
chronionymi i prywatnymi klasami bazowymi, wirtualnymi i niewirtualnymi klasami
bazowymi oraz wirtualnymi i niewirtualnymi funkcjami skadowymi. Kada z wymienionych wasnoci moe wpywa take na pozostae komponenty jzyka C++.
W efekcie, prby zrozumienia, co poszczeglne wasnoci oznaczaj, kiedy powinny
by stosowane oraz jak mona je w najlepszy sposb poczy z nieobiektowymi czciami jzyka C++ moe niedowiadczonych programistw zniechci.
Dalsz komplikacj jest fakt, e rne wasnoci jzyka C++ s z pozoru odpowiedzialne za te same zachowania. Oto przykady:
Potrzebujesz zbioru klas zawierajcych wiele elementw wsplnych.
160
Sposb 35.
Dopilnuj, by publiczne dziedziczenie
modelowao relacj jest
Sposb 35. Dopilnuj, by publiczne dziedziczenie modelowao relacj jest
William Dement w swojej ksice pt. Some Must Watch While Some Must Sleep
(W. H. Freeman and Company, 1974) opisa swoje dowiadczenia z pracy ze studentami, kiedy prbowa utrwali w ich umysach najistotniejsze tezy swojego wykadu.
161
Oczywiste jest, e kady student jest osob, nie kada osoba jest jednak studentem.
Dokadnie takie samo znaczenie ma powysza hierarchia. Oczekujemy, e wszystkie
istniejce cechy danej osoby (np. to, e ma jak dat urodzenia) istniej take dla studenta; nie oczekujemy jednak, e wszystkie dane dotyczce studenta (np. adres szkoy,
do ktrej uczszcza) bd istotne dla wszystkich ludzi. Pojcie osoby jest bowiem
bardziej oglne, ni pojcie studenta student jest specyficznym rodzajem osoby.
W jzyku C++ kada funkcja oczekujca argumentu typu
(lub wskanika do
obiektu klasy
bd referencji do obiektu klasy
) moe zamiennie pobiera obiekt klasy
(lub wskanik do obiektu klasy
bd referencj do
obiektu klasy
):
!"
#$ %"
162
163
#
.
-
#+
&,
&,
Powysza hierarchia jest znacznie blisza naszej rzeczywistej wiedzy na temat ptakw, ni ta zaprezentowana wczeniej.
Nasze rozwizanie nie jest jednak jeszcze skoczone, poniewa w niektrych systemach oprogramowania, proste stwierdzenie, e pingwin jest ptakiem, bdzie cakowicie
poprawne. W szczeglnoci, jeli nasza aplikacja dotyczy wycznie dziobw i skrzyde,
a w adnym stopniu nie wie si z lataniem, oryginalna hierarchia bdzie w zupenoci wystarczajca. Mimo e jest to dosy irytujce, omawiana sytuacja jest prostym
odzwierciedleniem faktu, e nie istnieje jedna doskonaa metoda projektowania dowolnego oprogramowania. Dobry projekt musi po prostu uwzgldnia wymagania
stawiane przed tworzonym systemem, zarwno te w danej chwili oczywiste, jak i te,
ktre mog si pojawi w przyszoci. Jeli nasza aplikacja nie musi i nigdy nie bdzie
musiaa uwzgldnia moliwoci latania, rozwizaniem w zupenoci wystarczajcym
bdzie stworzenie klasy
jako potomnej klasy . W rzeczywistoci taki
projekt moe by nawet lepszy ni rozrnienie ptakw latajcych od nielatajcych,
poniewa takie rozrnienie moe w ogle nie istnie w modelowanym wiecie.
Dodawanie do hierarchii niepotrzebnych klas jest bdn decyzj projektow, poniewa narusza prawidowe relacje dziedziczenia pomidzy klasami.
Istnieje jeszcze inna strategia postpowania w przypadku omawianego problemu:
wszystkie ptaki mog lata, pingwiny s ptakami, pingwiny nie mog lata. Strategia polega na wprowadzeniu takich zmian w definicji funkcji , by dla pingwinw
generowany by bd wykonania:
##,
&!,
%
%
&
#
+
, /
#%
#$"*/
Do takiego rozwizania d twrcy jzykw interpretowanych (jak Smalltalk), jednak istotne jest prawidowe rozpoznanie rzeczywistego znaczenia powyszego kodu,
ktre jest zupenie inne, ni mgby przypuszcza. Ciao funkcji nie oznacza bowiem,
e pingwiny nie mog lata. Jej faktyczne znaczenie to: pingwiny mog lata, jednak kiedy prbuj to robi, powoduj bd. Na czym polega rnica pomidzy tymi
znaczeniami? Wynika przede wszystkim z moliwoci wykrycia bdu ograniczenie pingwiny nie mog lata moe by egzekwowane przez kompilatory, natomiast
naruszenie ograniczenia podejmowana przez pingwiny prba latania powoduje bd
moe zosta wykryte tylko podczas wykonywania programu.
Aby wyrazi ograniczenie pingwiny nie mog lata, wystarczy nie definiowa
odpowiedniej funkcji dla obiektw klasy
:
+
&,
&,
164
Jeli sprbujesz teraz wywoa funkcj dla obiektu reprezentujcego pingwina,
kompilator zasygnalizuje bd:
#
,)$*
Powiesz pewnie: Te co! Kade dziecko wie, e kwadrat jest prostoktem, ale w oglnoci prostokt nie musi by kwadratem. Tak, to prawda, przynajmniej na poziomie
gimnazjum. Nie sdz jednak, bymy kiedykolwiek wrcili do nauki na tym poziomie.
Przeanalizuj wic poniszy kod:
0
#
1#2
%1#2
32
%32
2#2
,
&!%&$$
%2
% 4
165
+##0
#,
&!%'!&$
%!2
$
1#252#2
32%2678 &78 ! 4
2#255 1#2%
'(% 4"
$
!
)'
Take teraz oczywiste jest, e ostatni warunek nigdy nie powinien by faszywy.
Zgodnie z definicj, szeroko kwadratu jest przecie taka sama jak jego wysoko.
Tym razem mamy jednak problem. Jak mona pogodzi ponisze twierdzenia?
Przed wywoaniem funkcji wysoko kwadratu reprezentowanego
przez obiekt jest taka sama jak jego szeroko.
Wewntrz funkcji modyfikowana jest szeroko kwadratu,
Jak to moliwe?
Witaj w cudownym wiecie publicznego dziedziczenia, w ktrym Twj instynkt
sprawdzajcy si do tej pory w innych dziedzinach, wcznie z matematyk moe
nie by tak pomocny, jak tego oczekujesz. Zasadniczym problemem jest w tym przypadku to, e operacja, ktr mona stosowa dla prostoktw (jego szeroko moe
by zmieniana niezalenie od wysokoci), nie moe by stosowana dla kwadratw
(definicja figury wymusza rwno jej szerokoci i wysokoci). Mechanizm publicznego dziedziczenia zakada jednak, e absolutnie wszystkie operacje stosowane z powodzeniem dla obiektw klasy bazowej mog by stosowane take dla obiektw klasy
166
potomnej. W przypadku prostoktw i kwadratw (podobny przykad dotyczcy zbiorw i list omawiam w sposobie 40.) to zaoenie si nie sprawdza, zatem stosowanie
publicznego dziedziczenia do modelowania wystpujcej midzy nimi relacji jest po
prostu bdne. Kompilatory oczywicie umoliwi Ci zaprogramowanie takiego modelu, jednak jak si ju przekonalimy nie mamy gwarancji, e nasz program
bdzie si zachowywa prawidowo. Od czasu do czasu kady programista musi si
przekona (niektrzy czciej, inni rzadziej), e poprawne skompilowanie programu
nie oznacza, e bdzie on dziaa zgodnie z oczekiwaniami.
Nie denerwuj si, e rozwijana przez lata intuicja dotyczca tworzonego oprogramowania traci moc w konfrontacji z projektowaniem zorientowanym obiektowo. Twoja
wiedza jest nadal cenna, jednak dodae wanie do swojego arsenau rozwiza projektowych silny mechanizm dziedziczenia i bdziesz musia rozszerzy swoj intuicj
w taki sposb, by prowadzia Ci do waciwego wykorzystywania nowych umiejtnoci. Z czasem problem klasy
dziedziczcej po klasie lub klasie
dziedziczcej po klasie
bdzie dla Ciebie rwnie zabawny jak prezentowane Ci przez niedowiadczonych programistw funkcje zajmujce wiele stron. Moliwe, e proponowane podejcie do tego typu problemw jest waciwe, nadal jednak
nie jest to bardzo prawdopodobne.
Relacja jest nie jest oczywicie jedyn relacj wystpujc pomidzy klasami. Dwie
pozostae powszechnie stosowane relacje midzy klasami to relacja ma (ang. has-a)
oraz relacja implementacji z wykorzystaniem (ang. is-implemented-in-terms-of).
Relacje te przeanalizujemy podczas prezentacji sposobw 40. i 42. Nierzadko projekty C++ ulegaj znieksztaceniom, poniewa ktra z pozostaych najwaniejszych
relacji zostaa bdnie zamodelowana jako jest, powinnimy wic by pewni, e
waciwie rozrniamy te relacje i wiemy, jak naley je najlepiej modelowa w C++.
Sposb 36.
Odrniaj dziedziczenie interfejsu
od dziedziczenia implementacji
Sposb 36. Odrniaj dziedziczenie interfejsu od dziedziczenia implementacji
167
jest klas abstrakcyjn. Mona to pozna po obecnoci czystej funkcji wirtualnej . W efekcie klienci nie mog tworzy egzemplarzy klasy , mog to robi
wycznie klasy potomne. Mimo to klasa wywiera ogromny nacisk na wszystkie klasy, ktre (publicznie) po niej dziedzicz, poniewa:
Interfejsy funkcji skadowych zawsze s dziedziczone. W sposobie 35.
168
Poza faktem, e powysze rozwizanie moe zrobi wraenie na innych programistach podczas imprezy, w oglnoci znajomo zaprezentowanego fenomenu jest
w praktyce mao przydatna. Jak si jednak za chwil przekonasz, moe by wykorzystywana jako mechanizm udostpniania bezpieczniejszej domylnej implementacji dla
prostych (nieczystych) funkcji wirtualnych.
Niekiedy dobrym rozwizaniem jest zadeklarowanie klasy zawierajcej wycznie
czyste funkcje wirtualne. Takie klasy protokou udostpniaj klasom potomnym jedynie interfejsy funkcji, ale nigdy ich implementacje. Klasy protokou opisaem, prezentujc sposb 34., i wspominam o nich ponownie w sposobie 43.
Znaczenie prostych funkcji wirtualnych jest nieco inne ni znaczenie czystych funkcji
wirtualnych. W obu przypadkach klasy dziedzicz interfejsy funkcji, jednak proste
funkcje wirtualne zazwyczaj udostpniaj take swoje implementacje, ktre mog
(ale nie musz) by przykryte w klasach potomnych. Po chwili namysu powiniene
doj do wniosku, e:
Celem deklarowania prostej funkcji wirtualnej jest otrzymanie klas potomnych
169
Aby wyrazi fakt, e wszystkie samoloty musz obsugiwa jak funkcj oraz
z uwagi na moliwe wymagania dotyczce innych implementacji tej funkcji generowane przez nowe modele samolotw, funkcja !
zostaa zadeklarowana
jako wirtualna. Aby unikn pisania identycznego kodu w klasach "! i ",
domylny model latania zosta jednak zapisany w formie ciaa funkcji !
,
ktre jest dziedziczone zarwno przez klas "!, jak i klas ".
Jest to klasyczny projekt zorientowany obiektowo. Dwie klasy wspdziel wsplny
element (sposb implementacji funkcji ), zatem element ten zostaje przeniesiony
do klasy bazowej i jest dziedziczony przez te dwie klasy. Takie rozwizanie ma wiele
istotnych zalet: pozwala unikn powielania tego samego kodu, uatwia przysze rozszerzenia systemu i upraszcza konserwacj w dugim okresie czasu wszystkie wymienione wasnoci s charakterystyczne wanie dla technologii obiektowej. Linie
lotnicze XYZ powinny wic by dumne ze swojego systemu.
Przypumy teraz, e firma XYZ rozwija si i postanowia pozyska nowy typ samolotu Model C. Nowy samolot rni si nieco od Modelu A i Modelu B, a w szczeglnoci ma inne waciwoci lotu.
Programici omawianych linii lotniczych dodaj wic do hierarchii klas reprezentujc samoloty Model C, jednak w popiechu zapomnieli ponownie zdefiniowa
funkcj :
C DB
&,
&,
170
171
C DB
,
B
C D,
B
Powyszy schemat postpowania nie jest oczywicie cakowicie bezpieczny (programici nadal maj moliwo popeniania fatalnych w skutkach bdw), jednak jest
znacznie bardziej niezawodny od oryginalnego projektu. Funkcja !
$
jest chroniona, poniewa w rzeczywistoci jest szczegem implementacyjnym klasy
!
i jej klas potomnych. Klienci wykorzystujcy te klasy powinni zajmowa si
wycznie wasnociami lotu reprezentowanych samolotw, a nie sposobami implementowania tych wasnoci.
Wane jest take to, e funkcja !
$ jest niewirtualna. Wynika to
z faktu, e adna z podklas nie powinna jej ponownie definiowa temu zagadnieniu
powiciem sposb 37. Gdyby funkcja
$ bya wirtualna, mielibymy do
czynienia ze znanym nam ju problemem: co stanie si, jeli projektant ktrej z podklas zapomni ponownie zdefiniowa funkcj
$ w sytuacji, gdzie bdzie to
konieczne?
Niektrzy programici sprzeciwiaj si idei definiowania dwch osobnych funkcji dla
interfejsu i domylnej implementacji (jak i
$). Z jednej strony zauwaaj, e takie rozwizanie zamieca przestrze nazw klasy wystpujcymi wielokrotnie
zblionymi do siebie nazwami funkcji. Z drugiej strony zgadzaj si z tez, e naley
oddzieli interfejs od domylnej implementacji. Jak wic powinnimy radzi sobie
z t pozorn sprzecznoci? Wystarczy wykorzysta fakt, e czyste funkcje wirtualne
musz by ponownie deklarowane w podklasach, ale mog take zawiera wasne
implementacje. Oto sposb, w jaki moemy wykorzysta w hierarchii klas reprezentujcych samoloty moliwo definiowania czystej funkcji wirtualnej:
B
,
B
58
B
,
B
C BB
,
B
B
,
172
173
typy deklaracji oznaczaj zupenie inne mechanizmy dziedziczenia, podczas deklarowania funkcji skadowych musisz bardzo ostronie wybra jedn z omawianych metod.
Pierwszym popularnym bdem jest deklarowanie wszystkich funkcji jako niewirtualnych. Eliminujemy w ten sposb moliwo specjalizacji klas potomnych; szczeglnie kopotliwe s w tym przypadku take niewirtualne destruktory (patrz sposb 14.).
Jest to oczywicie dobre rozwizanie dla klas, ktre w zaoeniu nie bd wykorzystywane w charakterze klas bazowych. W takim przypadku, zastosowanie zbioru wycznie niewirtualnych funkcji skadowych jest cakowicie poprawne. Zbyt czsto
jednak wynika to wycznie z braku wiedzy na temat rni pomidzy funkcjami
wirtualnymi a niewirtualnymi lub nieuzasadnionych obaw odnonie wydajnoci funkcji
wirtualnych. Naley wic pamita o fakcie, e niemal wszystkie klasy, ktre w przyszoci maj by wykorzystane jako klasy bazowe, powinny zawiera funkcje wirtualne (ponownie patrz sposb 14.).
Jeli obawiasz si kosztw zwizanych z funkcjami wirtualnymi, pozwl, e przypomn Ci o regule 80-20 (patrz take sposb 33.), ktra mwi, e 80 procent czasu
dziaania programu jest powicona wykonywaniu 20 procent jego kodu. Wspomniana regua jest istotna, poniewa oznacza, e rednio 80 procent naszych wywoa
funkcji moe by wirtualnych i bdzie to miao niemal niezauwaalny wpyw na cakowit wydajno naszego programu. Zanim wic zaczniesz si martwi, czy moesz
sobie pozwoli na koszty zwizane z wykorzystaniem funkcji wirtualnych, upewnij
si, czy Twoje rozwaania dotycz tych 20 procent programu, gdzie decyzja bdzie
miaa istotny wpyw na wydajno caego programu.
Innym powszechnym problemem jest deklarowanie wszystkich funkcji jako wirtualne.
Niekiedy jest to oczywicie waciwe rozwizanie np. w przypadku klas protokou
(patrz sposb 34.). Moe jednak wiadczy take o zwykej niewiedzy projektanta
klasy. Niektre deklaracje funkcji nie powinny umoliwia ponownego ich definiowania w klasach potomnych w takich przypadkach jedynym sposobem osignicia
tego celu jest deklarowanie tych funkcji jako niewirtualnych. Nie ma przecie najmniejszego sensu udostpnianie innym programistom klas, ktre maj by dziedziczone przez inne klasy i ktrych wszystkie funkcje skadowe bd ponownie definiowane. Pamitaj, e jeli masz klas bazow , klas potomn oraz funkcj skadow
, wwczas kade z poniszych wywoa funkcji musi by prawidowe:
<>5
%<
>5
?@,%% )&,
&',! $
%F
! %&
?@,%% )&,
&',! $
%F
&
174
Sposb 37.
Nigdy nie definiuj ponownie dziedziczonych
funkcji niewirtualnych
Sposb 37. Nigdy nie definiuj ponownie dziedziczonych funkcji niewirtualnych
Istniej dwa podejcia do tego problemu: teoretyczne i pragmatyczne. Zacznijmy od podejcia pragmatycznego (teoretycy s w kocu przyzwyczajeni do cierpliwego czekania).
Przypumy, e powiem Ci, e klasa publicznie dziedziczy po klasie i istnieje
publiczna funkcja skadowa zdefiniowana w klasie . Parametry i warto zwracane przez funkcj s dla nas na tym etapie nieistotne, zamy wic, e maj posta
. Innymi sowy, moemy to wyrazi w nastpujcy sposb:
+
,
<+
Nawet gdybymy nic nie wiedzieli o , i , majc dany obiekt % klasy :
<GG& <
175
Jeli w klasie ponownie zdefiniujemy teraz funkcj , w naszym projekcie powstanie sprzeczno. Jeli klasa faktycznie potrzebuje wasnej implementacji funkcji ,
ktra bdzie si rnia od implementacji dziedziczonej po klasie , oraz jeli kady
obiekt klasy (niezalenie od poziomu specjalizacji) rzeczywicie musi wykorzystywa implementacji tej funkcji z klasy , wwczas stwierdzenie, e jest jest zwyczajnie nieprawdziwe. Klasa nie powinna w takim przypadku publicznie dziedziczy po klasie . Z drugiej strony, jeli naprawd musi publicznie dziedziczy po
oraz jeli naprawd musi implementowa funkcj inaczej, ni implementuje j
klasa , wwczas nieprawd jest, e odzwierciedla niezaleno od specjalizacji
klasy . W takim przypadku funkcja powinna zosta zadeklarowana jako wirtualna.
Wreszcie, jeli kady obiekt klasy naprawd musi by w relacji jest z obiektem
klasy oraz jeli funkcja rzeczywicie reprezentuje niezaleno od specjalizacji
klasy , wwczas klasa nie powinna potrzebowa wasnej implementacji funkcji
i jej projektant nie powinien wic podejmowa podobnych prb.
176
Sposb 38.
Nigdy nie definiuj ponownie
dziedziczonej domylnej wartoci parametru
Sposb 38. Nigdy nie definiuj ponownie dziedziczonej domylnej wartoci parametru
4
$% 4"?F*
%2D 5I0==.
D2
%2D
Sposb 38. Nigdy nie definiuj ponownie dziedziczonej domylnej wartoci parametru
177
W powyszym przykadzie , i s zadeklarowane jako zmienne typu wskanikowego do obiektw klasy , zatem wszystkie nale do typu statycznego.
Zauwa, e nie ma w tym przypadku znaczenia, na co wymienione zmienne faktycznie wskazuj ich statycznym typem jest .
Typ dynamiczny obiektu zaley od typu obiektu aktualnie przez niego wskazywanego.
Oznacza to, e od dynamicznego typu zaley zachowanie obiektu. W powyszym
przykadzie typem dynamicznym zmiennej jest #, za typem dynamicznym
zmiennej jest
. Inaczej jest w przypadku zmiennej , ktra nie ma
dynamicznego typu, poniewa w rzeczywistoci nie wskazuje na aden obiekt.
Typy dynamiczne (jak sama nazwa wskazuje) mog si zmienia w czasie wykonywania programu, tego rodzaju zmiany odbywaj si zazwyczaj na skutek wykonania
operacji przypisania:
5
!
%F
&!D>
5
!
%F
&!0
#>
Uwaasz pewnie, e nie ma powodw wraca do tego tematu z pewnoci rozumiesz ju znaczenie funkcji wirtualnych. Problemy ujawniaj si dopiero w momencie,
gdy analizujemy funkcje wirtualne z domylnymi wartociami parametrw, poniewa
jak ju wspomniaem funkcje wirtualne s wizane dynamicznie, a domylne
parametry C++ wie statycznie. Oznacza to, e moesz wywoa wirtualn funkcj
zdefiniowan w klasie potomnej, ale z domyln wartoci parametru z klasy bazowej:
?@%%% )&0
#%0=<
178
Sposb 39.
Unikaj rzutowania
w d hierarchii dziedziczenia
Sposb 39. Unikaj rzutowania w d hierarchii dziedziczenia
179
Wiele bankw przedstawia dzisiaj swoim klientom niezwykle szerok ofert typw
kont bankowych, zamy jednak (dla uproszczenia), e istnieje tylko jeden typ konta
bankowego, zwyke konto oszczdnociowe:
#B
+
B
#B
>E%
(
>&
E%
L
#B
;
&
Nie jest to moe zbyt zaawansowane konto oszczdnociowe, jest jednak w zupenoci wystarczajce dla naszych rozwaa.
Bank prawdopodobnie przechowuje list wszystkich swoich kont, ktra moe by zaimplementowana za pomoc szablonu klasy ze standardowej biblioteki C++ (patrz
sposb 49.). Przypumy, e w naszym banku taka lista nosi nazw !
:
M+
B
>@B
%!
)#%
!!
180
181
Pierwszy problem polega na tym, e ptla nadal bdzie poprawnie kompilowana, mimo
e nie wprowadzilimy jeszcze zmian uwzgldniajcych istnienie obiektw nowej
klasy #
!
. Wynika to z faktu, e nasze kompilatory nadal bd pochopnie
zakaday, e kiedy sygnalizujemy (za pomoc 1 ), e w rzeczywistoci
wskazuje na
!
, tak jest w istocie. W kocu to do nas naley przewidywanie skutkw wykonywania poszczeglnych instrukcji. Drugi problem wynika
z typowego sposobu radzenia sobie z pierwszym problemem, co zwykle skutkuje
tworzeniem podobnego kodu:
, M+
B
>@ 5B
#
*5B
66
,>
#B
NM
#B
>@>?@;
NMD2
#B
>@>?@;
182
Mimo e powysza ptla nadal zawiera skrytykowane wczeniej rzutowanie, zaproponowane rozwizanie jest znacznie lepsze od omawianych do tej pory, poniewa bdzie
poprawnie funkcjonowao nawet po dodaniu do naszej aplikacji nowych podklas do
klasy
!
.
Aby cakowicie pozby si rzutowania, musimy wprowadzi do naszego projektu kilka
dodatkowych modyfikacji. Jedn z nich jest doprecyzowanie specyfikacji wykorzystywanej listy kont. Gdybymy mogli wykorzysta list obiektw klasy
&
!
, zamiast obiektw klasy
!
, rozwizanie byoby trywialne:
%! )#%
!!
!
M;
+
#B
>@;+B
'!
%
'!!))%) %
, M;
+
#B
>@ 5;+B
#
*5;+B
66
>?@;
183
Jeli tworzenie bardziej specjalizowanej listy nie jest brane pod uwag, mona stwierdzi, e operacj
mona stosowa dla wszystkich kont bankowych;
jednak w przypadku kont, dla ktrych nie nalicza si odsetek, wspomniana operacja
jest pusta. To samo moemy wyrazi za pomoc poniszego fragmentu kodu:
+
B
;
#B
+
B
D2
#B
+
B
M+
B
>@B
!(
! %
*
, M+
B
>@ 5B
#
*5B
66
>?@;
184
185
186
Sposb 40.
Modelujc relacje posiadania (ma)
i implementacji z wykorzystaniem,
stosuj podzia na warstwy
Sposb 40. Modelujc relacje posiadania (ma) i implementacji...
Dzielenie na warstwy jest procesem budowania pewnych klas ponad innymi klasami
w taki sposb, e cz klas zawiera w postaci skadowych reprezentujcych dane
obiekty klas znajdujcych si na innych warstwach. Przykadowo:
B
!
&!!
2
.
#
!
&%%
B&%
2
. .&%
2
.,G.&%
187
Kiedy zagbisz si w dokumentacji szablonu , odkryjesz jednak pewne ograniczenia tej struktury, ktre s nie do przyjcia w Twojej aplikacji struktura
wymaga, by przechowywane w niej elementy byy cakowicie uporzdkowane, co
oznacza, e dla kadej pary obiektw i nalecych do struktury musi istnie
moliwo okrelenia, czy ,*, i czy ,*,. W przypadku wielu typw spenienie takiego wymagania jest bardzo atwe, a posiadanie cakowicie uporzdkowanych obiektw pozwala strukturze na zapewnianie bardzo korzystnych warunkw w zakresie
efektywnoci dziaania (wicej szczegw na temat wydajnoci struktur udostpnianych przez standardow bibliotek C++ znajdziesz w sposobie 49.). Potrzebujesz
jednak struktury bardziej oglnej, klasy podobnej do , w ktrej przechowywane
obiekty nie musz spenia relacji cakowitego porzdku, a jedynie takie, dla ktrych
mona wyznaczy relacj rwnoci (dla ktrych istnieje moliwo okrelenia, czy
-- dla obiektw i tego samego typu). To skromniejsze wymaganie znacznie lepiej pasuje do typw reprezentujcych np. kolory. Czy czerwony jest mniejszy od
zielonego, czy te zielony jest mniejszy od czerwonego? Wyglda na to, e w takim
przypadku bdziemy musieli ostatecznie opracowa wasny szablon.
Ponowne wykorzystywanie gotowych rozwiza jest nadal wietnym rozwizaniem.
Jeli jeste ekspertem w dziedzinie struktur danych, z pewnoci wiesz, e niemal
nieograniczone moliwoci implementowania zbiorw daje (stosunkowo prosta) struktura listy jednokierunkowej. Co wicej, szablon (generujcy klasy list jednokierunkowych) jest dostpny w standardowej bibliotece C++! Decydujesz si wic na jego
(ponowne) wykorzystanie.
W szczeglnoci postanawiasz, e Twj nowy szablon bdzie dziedziczy po szablonie . Oznacza to, e struktura *2+ bdzie dziedziczya po *2+. Twoja
implementacja zakada w kocu, e obiekt w rzeczywistoci bdzie obiektem
. Deklarujesz wic swj szablon w nastpujcy sposb:
%)4% :% !
!
MO@
MO@
By moe wszystko na tym etapie wyglda prawidowo, jednak w rzeczywistoci zaprezentowane rozwizanie zawiera zasadnicze usterki. W sposobie 35. wyjaniem, e
jeli D jest B, wszystkie poprawne wasnoci klasy B s take poprawne dla klasy D.
Obiekt klasy moe jednak zawiera duplikaty, zatem jeli warto 3051 zostanie
wstawiona do struktury *
+ dwukrotnie, reprezentowana lista bdzie zawieraa
dwie kopie tej liczby. Inaczej jest w przypadku struktury , ktra nie moe zawiera
duplikatw jeli wic warto 3051 zostanie wstawiona do *
+ dwukrotnie,
reprezentowany zbir i tak bdzie zawiera tylko jedn jej kopi. Stwierdzenie, e
jest jest wic faszywe, poniewa istniej wasnoci poprawne dla obiektw
klasy , ktre nie s poprawne dla obiektw klasy .
Poniewa omawiane dwie klasy s zwizane relacj inn ni jest, modelowanie rzeczywistej relacji nie powinno si opiera na mechanizmie publicznego dziedziczenia.
Waciwym rozwizaniem jest stwierdzenie, e obiekt klasy moe by implementowany z wykorzystaniem obiektu klasy :
188
Funkcje skadowe klasy mog w duej mierze opiera si na funkcjonalnoci udostpnianej zarwno przez szablon , jak i innych czci standardowej biblioteki
C++, zatem implementacja powyszej klasy nie jest trudna do napisania ani do pniejszego zrozumienia:
MO@
MO@
O
,
#
(
(*5
MO@
MO@
O
,*2N
MO@
MO@
O
MO@ 5,
#
(
(
,*5
MO@
MO@
!
189
Sposb 41.
Rozrniaj dziedziczenie
od stosowania szablonw
Sposb 41. Rozrniaj dziedziczenie od stosowania szablonw
190
obiekty rasy 2), jednak pytanie, ktre musimy sobie postawi, brzmi nastpujco:
Czy typ 2 wpywa na zachowanie tworzonej klasy?. Jeli 2 nie wpywa na zachowanie klasy, moemy zastosowa szablon. Jeli 2 ma wpyw na zachowanie projektowanej klasy, bdziemy potrzebowali wirtualnych funkcji, co oznacza, e konieczne
bdzie wykorzystanie mechanizmu dziedziczenia.
Oto jak moemy zdefiniowa opart na licie jednokierunkowej implementacj klasy
, zakadajc, e przechowywane na stosie obiekty s typu 2:
L
2
O &
O
! &P
.
&
%&
O
!
%
!!
. >
G
'
.
&!&
.
O
%<(. >
G.
%<(
G
G.
. > !!
2
%&$ %
5
2!%
! :AQ
Sama lista jednokierunkowa skada si z obiektw typu ), jest to jednak wycznie szczeg implementacyjny klasy , zatem struktur ) zadeklarowalimy jako prywatny typ tej klasy. Zauwa, e dla typu ) zdefiniowalimy
konstruktor zapewniajcy waciwe inicjalizowanie wszystkich pl tej struktury.
Twoje umiejtnoci umoliwiajce tworzenie kodu dla list jednokierunkowy z zamknitymi oczami nie mog Ci przesania korzyci pyncych z wykorzystania tego
typu konstruktorw.
191
Oto pierwsza prba zaimplementowania funkcji skadowych klasy . Jak wikszo prototypowych implementacji (dalekich od ostatecznej, handlowej wersji oprogramowania), poniszy kod nie zawiera instrukcji wykrywajcych bdy, poniewa
w wiecie prototypw nic si nigdy nie psuje:
8
&!& %F
! %
2
O &
5
%. &( !!
%
!$
O
. > E,5 !2 %&
!!!
5 ?@
G
O5 E,?@!2 %&
E,
L%%!
!
%2
. > <5 !&%F
!!!
5 ?@
G!2 !
'
#
<% !
!!!
558
Powysze implementacje nie zawieraj w sobie adnych nowych, fascynujcych elementw. W rzeczywistoci jedynym interesujcym szczegem w powyszym kodzie
jest to, e kada z zaprezentowanych funkcji skadowych zostaa opracowana bez najmniejszej znajomoci docelowego typu 2 (zakadamy jedynie, e moemy wywoywa
konstruktor kopiujcy typu 2, ale jak wynika z treci sposobu 45. mamy do tego
prawo). Stworzony kod konstruujcy i niszczcy stos, kadcy i zdejmujcy elementy
ze stosu oraz okrelajcy, czy stos jest pusty, jest cakowicie niezaleny od typu 2.
Wyjtkiem jest zaoenie, e moemy dla tego typu wywoywa konstruktor kopiujcy, jednak zachowanie obiektw zdefiniowanej powyej klasy w aden inny sposb
nie jest uzalenione od 2. Powyszy przykad doskonale obrazuje podstawow zasad
szablonw klas zachowanie klasy nie zaley od typu przetwarzanych obiektw.
Przeksztacenie klasy w szablon jest zreszt tak proste, e mgby to zrobi kady:
MO@
% !
!
192
Wrmy teraz do kotw. Dlaczego szablony nie bd dobrym rozwizaniem dla klas
reprezentujcych koty?
Przeczytaj raz jeszcze specyfikacj i zwr uwag na jedno z wymaga: kada rasa
kotw je i pi w charakterystyczny dla siebie, ujmujcy sposb. Oznacza to, e musimy zaimplementowa rne zachowania dla poszczeglnych typw (ras) kotw.
Nie moemy po prostu napisa jednej funkcji obsugujcej zachowania wszystkich
kotw, moemy jedynie stworzy specyfikacj interfejsu dla funkcji, ktr kady typ
kotw musi implementowa. Aha! Moemy przekaza wycznie interfejs funkcji,
deklarujc jedynie czyst funkcj wirtualn (patrz sposb 36.):
D
LD! :7R
58%! &!$
58%! 4$
Dobrze, wiemy ju, dlaczego szablony s dobrym rozwizaniem dla klasy , i dlaczego nie powinnimy ich stosowa w przypadku klasy #. Wiemy take, dlaczego
waciwym rozwizaniem dla klasy # jest dziedziczenie. Pozostaje wic tylko pytanie,
dlaczego nie powinnimy stosowa dziedziczenia dla klasy . Aby sobie na to
pytanie odpowiedzie, sprbujmy zadeklarowa najwysz klas () z hierarchii
klas reprezentujcych stosy klas bazow, po ktrej wszystkie pozostae klasy
reprezentujce stosy bd dziedziczyy:
& %
2
PPP &58
PPP 58
Wszystko jest ju jasne. Jaki typ powinnimy zadeklarowa dla czystych funkcji
wirtualnych
i ? Pamitaj, e kada podklasa musi ponownie zadeklarowa
dziedziczone funkcje wirtualne z dokadnie tymi samymi typami parametrw i typami
zwracanych wynikw, z ktrymi funkcje te zostay zadeklarowane w klasie bazowej.
193
Sposb 42.
Dziedziczenie prywatne stosuj ostronie
Sposb 42. Dziedziczenie prywatne stosuj ostronie
Prezentujc sposb 35., wykazaem, e C++ traktuje publiczne dziedziczenie jak relacj jest. Zademonstrowaem to na przykadzie sytuacji, w ktrej kompilatory, majc
hierarchi, w ktrej klasa
publicznie dziedziczy po klasie
, niejawnie
przeksztacaj obiekty klasy
w obiekty klasy
, gdy jest to niezbdne do
poprawnej realizacji wywoania funkcji. Warto w tym momencie przypomnie fragment tamtego przykadu z jedn zmian zamiast dziedziczenia publicznego zastosujemy dziedziczenie prywatne:
! &
!!!
%
!"
&$
194
Oczywisty wniosek jest taki, e dziedziczenie prywatne nie oznacza relacji jest. Co
wic faktycznie oznacza?
Mylisz pewnie, e zanim zajmiemy si rzeczywistym znaczeniem dziedziczenia
prywatnego, powinnimy przeanalizowa zachowanie programw, w ktrych wykorzystujemy ten mechanizm. Dobrze, pierwsz regu rzdzc prywatnym dziedziczeniem moglimy wanie zaobserwowa w przeciwiestwie do dziedziczenia
publicznego, kompilatory w oglnoci nie przeksztacaj obiektw klasy potomnej
(np. klasy
) w obiekty klasy bazowej (np. klasy
), jeli zdefiniowano
midzy tymi klasami relacj dziedziczenia prywatnego. Dlatego wanie wywoanie
funkcji
dla obiektu zakoczyo si niepowodzeniem. Druga regua okrela, e
skadowe odziedziczone po prywatnej klasie bazowej staj si prywatnymi skadowymi klasy potomnej, nawet jeli w klasie bazowej zostay zadeklarowane jako skadowe chronione lub publiczne. To wszystko, co moemy powiedzie o zachowaniu
kodu zawierajcego dziedziczenie prywatne.
Przejdmy wic do rzeczywistego znaczenia tej relacji. Dziedziczenie prywatne modeluje relacj implementacji z wykorzystaniem. Kiedy deklarujemy klas prywatnie
dziedziczc po klasie , robimy to dlatego, e chcemy w klasie wykorzysta cz
kodu napisanego dla klasy ; pojciowe relacje czce obiekty typu z obiektami typu
nie maj tutaj adnego znaczenia. Oznacza to, e dziedziczenie prywatne ma wycznie charakter techniki implementacji klas. Posugujc si jzykiem ze sposobu 36.,
moemy powiedzie, e dziedziczenie prywatne oznacza, e dziedziczona powinna by
tylko implementacja, interfejs powinien by cakowicie ignorowany. Jeli klasa
dziedziczy prywatnie po klasie , oznacza to tylko tyle, e obiekty klasy s implementowane z wykorzystaniem obiektw klasy . Dziedziczenie prywatne nie jest
technik wykorzystywan w fazie projektowania oprogramowania, a jedynie podczas
implementowania programw.
Fakt, e prywatne dziedziczenie oznacza w rzeczywistoci relacj implementacji
z wykorzystaniem jest nieco mylcy, poniewa, prezentujc sposb 40. stwierdziem,
e takie samo znaczenie ma rozmieszczanie obiektw w warstwach. Na jakiej podstawie powinnimy wic wybiera waciw technik z tej pary? Odpowied jest prosta stosuj podzia na warstwy zawsze, gdy jest to moliwe; stosuj dziedziczenie
prywatne tylko wtedy, gdy jest to jedyne rozwizanie. Kiedy moemy mie do czynienia z t drug sytuacj? Kiedy mamy do czynienia z chronionymi skadowymi
i (lub) wirtualnymi funkcjami.
W sposobie 41. opisaem sposb tworzenia szablonu , ktrego zadaniem byo
generowanie klas przechowujcych obiekty rnych typw. By moe powiniene
zapozna si teraz z treci tego sposobu. Szablony s jednym z najbardziej przydatnych elementw udostpnianych w jzyku C++, kiedy jednak zaczniesz je stosowa
regularnie, szybko odkryjesz, e tworzc wiele obiektw danego szablonu, prawdopodobnie zwielokrotniasz take jego kod. W przypadku szablonu kod funkcji
195
Poniewa powysza klasa przechowuje wskaniki zamiast obiektw, istnieje moliwo, e dany obiekt jest wskazywany przez wicej ni jeden stos (zosta pooony na
wielu stosach). Zasadnicze znaczenie ma w takiej sytuacji zapewnienie, by funkcja
i destruktor klasy nie usuway wskanika w adnym niszczonym obiekcie
typu ) i jednoczenie nadal usuway sam obiekt ). Obiekty typu
) maj w kocu przydzielan pami wewntrz klasy '
, zatem
196
take tam musz by usuwane. Oznacza to, e implementacja klasy ze sposobu
41. niemal w zupenoci wystarcza take dla klasy '
. Jedyne potrzebne
zmiany to zastpienie typu 2 typem .
Sama klasa '
jest mao uyteczna jest te zbyt prosta, by moliwe byo
jej niewaciwe wykorzystanie. Klient mgby jednak przez pomyk pooy na stosie przechowujcym wycznie wskaniki do liczb cakowitych typu
wskanik do
obiektu klasy #, a kompilatory i tak zaakceptowayby takie posunicie. W kocu
parametr bdcy wskanikiem typu moe dotyczy dowolnych obiektw.
Aby odzyska bezpieczestwo typw, do ktrego zdylimy si ju przyzwyczai,
musimy stworzy klasy interfejsu do '
:
;
,&% 4
2
>
2
>
NM
>@
I
&
D
,&% 4D
2D>2
D>
NMD>@
I
&
197
Nic, nic nie moe zmusi do tego potencjalnych klientw Twojego kodu. A moe
jednak istnieje co, co moe pomc?
Na pocztku tego sposobu wspomniaem, e alternatywnym sposobem ustanawiania
pomidzy klasami relacji implementacji z wykorzystaniem jest wizanie ich dziedziczeniem prywatnym. W tym przypadku technika ta okazuje si lepsza ni dzielenie na
warstwy, poniewa umoliwia wyraanie zaoenia, e klasa '
nie jest
wystarczajco bezpieczna dla oglnych zastosowa i powinna by wykorzystywana
wycznie w charakterze implementacji innych klas. Mona to wyrazi, umieszczajc
funkcje skadowe klasy '
w bloku :
I
I
LI
2 > &
>
%!4
&
I
)$*
&2
;
2
>
I
2
>
NM
>@I
I
D
2D>I
2
D>
NMD>@I
I
;
!
D !
198
By moe nie jest to dla Ciebie od razu takie oczywiste, jednak powyszy kod jest
niesamowity dziki zastosowaniu szablonu kompilatory automatycznie wygeneruj tyle klas interfejsw, ile bdziemy potrzebowa. Poniewa generowane klasy s
bezpieczne pod wzgldem obsugi typw, popeniane przez klienta bdy w tym zakresie bd wykrywane ju w czasie kompilacji. Poniewa funkcje skadowe klasy
'
s chronione oraz poniewa klasy interfejsw wykorzystuj '
&
jako prywatn klas bazow, klienci nie mog obej klas interfejsw i uzyska
bezporedniego dostpu do klasy implementacji. Poniewa kada funkcja skadowa
klasy interfejsu jest (niejawnie) deklarowana z atrybutem
, stosowanie klas
bezpiecznie obsugujcych typy nie powoduje adnych dodatkowych kosztw w trakcie wykonywania programu wygenerowany kod jest identyczny jak kod obsugujcy bezporedni dostp do skadowych klasy '
(zakadajc, e kompilatory uwzgldniaj dyrektywy
patrz sposb 33.). Poniewa w klasie
'
zastosowalimy wskaniki , ponosimy koszty operowania na stosach przez dokadnie jedn kopi kodu, niezalenie od liczby rnych typw stosw
wykorzystywanych w programie. Krtko mwic, zaprezentowany projekt zapewnia
naszemu programowi maksymaln efektywno i maksymalne bezpieczestwo typw. Nieatwo bdzie skonstruowa lepsze rozwizanie.
Jednym z wnioskw pyncych z tej ksiki jest ten, e rne wasnoci jzyka C++
wzajemnie na siebie oddziaywaj niekiedy w sposb niezwyky. Myl, e zgodzisz
si ze mn, e powysze rozwizanie jest tego dobrym przykadem.
Nie moglibymy zrealizowa omawianego przykadu za pomoc mechanizmu podziau na warstwy. Wynika to z faktu, e tylko mechanizm dziedziczenia daje moliwo dostpu do chronionych skadowych i tylko dziedziczenie umoliwia ponowne
definiowanie wirtualnych funkcji (przykad funkcji wirtualnych, ktrych obecno
moe skoni programist do zastosowania prywatnego dziedziczenia, znajdziesz
w sposobie 43.). W sytuacjach, w ktrych mamy do czynienia z klas zawierajc
funkcje wirtualne i chronione skadowe prywatne, dziedziczenie jest niekiedy jedynym sposobem wyraania relacji implementacji z wykorzystaniem pomidzy klasami.
Nie powiniene wic obawia si stosowania dziedziczenia prywatnego, kiedy okae
si, e jest to najbardziej waciwa technika, jak masz w danym przypadku do dyspozycji. Powiniene jednak pamita, e lepsz technik jest w oglnoci dzielenie na
warstwy, powiniene wic stosowa j zawsze, kiedy moesz.
199
Sposb 43.
Dziedziczenie wielobazowe stosuj ostronie
Sposb 43. Dziedziczenie wielobazowe stosuj ostronie
200
201
J
J (
I2E&
<%
#2E&<%
202
Niezalenie od tego, czy prawd jest, e diamenty s najlepszymi przyjacimi kobiety, z pewnoci zaprezentowana powyej hierarchia dziedziczenia w ksztacie
diamentu nie jest przyjacielem programisty. Kiedy tworzymy podobn hierarchi, na
samym pocztku musimy sobie odpowiedzie na pytanie, czy ! powinna by wirtualn
klas bazow (czyli, czy dziedziczenie po tej klasie powinno by wirtualne). W praktyce odpowied niemal zawsze powinna by twierdzca; tylko w szczeglnych przypadkach bdziemy chcieli, by obiekt klasy zawiera wiele kopii danych bdcych
skadowymi klasy !. W powyszym przykadzie w klasach i # zadeklarowalimy !
jako wirtualn klas bazow.
Niestety, kiedy definiujemy klasy i #, moemy nie wiedzie, czy jakakolwiek inna
klasa bdzie jednoczenie dziedziczya po nich obu i w rzeczywistoci do poprawnego
zdefiniowania tych klas taka wiedza nie powinna nam by potrzebna. Stawia nas to
w bardzo trudnym pooeniu, przynajmniej jako projektantw tych klas. Jeli nie zadeklarujemy ! jako wirtualnej klasy bazowej klas i #, pniejsi projektanci klasy
bd by moe zmuszeni do zmodyfikowania definicji klas i #, by umoliwi sobie
ich efektywne wykorzystanie. Takie rozwizanie jest zazwyczaj nie do przyjcia,
zwykle dlatego, e definicje klas !, i # s dostpne tylko do odczytu. Moe to wynika np. z faktu, e klasy !, i # znajduj si w bibliotece, a klasa jest tworzona
przez klienta tej biblioteki.
Z drugiej strony, jeli zadeklarujemy ! jako wirtualn klas bazow dla klas i #,
bdziemy musieli zazwyczaj ponie dodatkowe koszty zarwno w wymiarze wykorzystywanej przestrzeni pamiciowej, jak i czasu dziaania programw klientw tych
klas. Wynika to z faktu, e wirtualne klasy bazowe s zwykle implementowane jako
wskaniki do obiektw, nie za jako same obiekty. Rozmieszczanie obiektw w pamici zaley zwykle od konkretnych dziaa poszczeglnych kompilatorw, jednak
faktem jest, e obiekt klasy z niewirtualn klas bazow ! jest zazwyczaj umieszczany w szeregu przylegajcych komrek pamici, natomiast obiekt klasy z wirtualn klas bazow ! jest niekiedy umieszczany w szeregu przylegajcych komrek
pamici, z ktrych dwa zawieraj wskaniki do komrek zawierajcych skadowe
z danymi wirtualnej klasy bazowej:
203
204
205
&
>! 4
%
,
&) %2
%
!
&
206
Moesz pomyle, e powysza klasa jest stara, poniewa jej funkcje skadowe zwracaj acuchy typu
, zamiast obiektw typu
. Jeli buty pasuj, dlaczego nie mielibymy ich nosi? Nazwy funkcji skadowych powyszej klasy sugeruj, e efekt prawdopodobnie bdzie dla nas satysfakcjonujcy.
Dochodzimy wreszcie do odkrycia, e klasa
zostaa jednak zaprojektowana po to, by uatwia wypisywanie z bazy danych pl z danymi w rnych formatach,
gdzie kade pole jest z gry i z dou ograniczone specjalnymi acuchami. Domylnymi
ogranicznikami otwierajcymi i zamykajcymi wartoci pl s nawiasy kwadratowe,
zatem warto pola lemur gruboogoniasty bdzie reprezentowana przez acuch:
SJ# #
T
207
2>
;
, <D
/T/ 4
#
!
!&$
2>
;
, 2.
% !, !%
&% 4
%&
!
(!
!
&! %
!
2SCBUN-E0CBOO=<N-;=J<NVBJK=NJ=.IO1T
!& #
!
%&$
(<E
!& #
!
!&$
(<D
208
Klasa "
musi jednak take implementowa interfejs klasy
, co wie si
z publicznym dziedziczeniem. Prowadzi nas to do ciekawego przykadu dziedziczenia
wielobazowego poczenia publicznego dziedziczenia interfejsu z prywatnym
dziedziczeniem implementacji:
%!
!
,&(:
"
%
L
#
58
#2<
58
#
58
#
58
<;<
% !%
&!!#:)
$
;
, !%,
&!
!
&
;
, <;<
L
;
,
2>2.
2>2+2<
2>2B
2>2.
2><E
!
2><D
&
C
(!%:"%#'
;
, !!!
% ! %
C
<;<
;
,
%
,
& !!!
2%
2,
& #
!
:%
2><E
//
2><D
//
&%#
2,
&) %2
#
;
, 2.
#2<
;
, 2+2<
#
;
, 2B
#
;
, 2.
209
210
Kiedy zabierzesz si za implementowanie klasy #, uwiadomisz sobie, e moesz ponownie wykorzysta wikszo kodu napisanego wczeniej dla klasy ' &
. Kody funkcji nalecych do obu klas musz si jednak w paru szczegach
rni uwzgldniamy w ten sposb rnice pomidzy technikami taczenia i piewania konikw polnych i wierszczy. Nagle przychodzi nam do gowy sprytny sposb ponownego wykorzystania istniejcego kodu zaimplementujemy klas #
z wykorzystaniem klasy ' i wykorzystamy wirtualne funkcje, ktre umoliwi klasie # modyfikowanie zachowa z klasy ' !
Od razu powinnimy si zorientowa, e poczenie obu wymaga relacji implementacji z wykorzystaniem z moliwoci ponownego definiowania wirtualnych
funkcji oznacza, e klasa # musiaaby prywatnie dziedziczy po klasie ' &
, jednak wierszcz pozostaby oczywicie postaci z kreskwki, zatem klas
# musielibymy zdefiniowa w taki sposb, by dziedziczya zarwno po klasie
' , jak i #
#:
DD
D2(
I2
#
Dochodzimy teraz do momentu, w ktrym musimy wprowadzi niezbdne modyfikacje do klasy ' . W szczeglnoci musimy zadeklarowa kilka nowych wirtualnych funkcji, ktre bd ponownie definiowane w klasie #:
I2 D
D2
#
D !
7
D !
A
#D !
211
DD
D2(
I2
I2
#I2
#
D !
7
D !
A
#D !
Wyglda na to, e wszystko powinno dziaa prawidowo. Kiedy obiekt klasy #
ma zataczy, wykona wsplny kod funkcji
z klasy ' , wykona waciwy tylko do wierszczy kod funkcji
z klasy #, wykona kod funkcji
'
itd.
Zaprezentowany projekt zawiera jednak powan wad lepo dc do celu zamae bowiem zasad zwan brzytw Ockhama1. Ockhamizm gosi, e bytw nie naley mnoy bez koniecznoci, odrzuca tym samym wszelkie byty, do ktrych uznania
nie zmusza dowiadczenie. W tym przypadku tymi bytami s relacje dziedziczenia.
Jeli sdzisz, e dziedziczenie wielobazowe jest bardziej skomplikowane od zwykego
dziedziczenia (mam nadziej, e tak wanie sdzisz), zaproponowany projekt klasy
# jest niepotrzebnie tak skomplikowany.
Zasadniczy problem polega na tym, e nieprawd jest, e klasa # jest zaimplementowana z wykorzystaniem klasy ' . Klasy # i ' maj
po prostu troch wsplnego kodu. W szczeglnoci wykorzystuj wsplny kod definiujcy te zachowania podczas taczenia i piewania konikw polnych i wierszczy,
ktre dla obu typw postaci s identyczne.
Dziedziczenie jednej klasy po drugiej nie jest dobrym sposobem wyraania ich zalenoci polegajcej na wykorzystywaniu wsplnego kodu w takim przypadku obie
klasy powinny dziedziczy po jednej wsplnej klasie bazowej. Wsplny kod dla konikw polnych i wierszczy nie powinien nalee ani do klasy ' , ani do
klasy #; powinien nalee do nowej klasy bazowej, po ktrej obie wymienione
klasy powinny dziedziczy, powiedzmy do klasy
:
D
D2
;
D
D2
%:
:%
2
#4%!!
D !
758
D !
A58
1
212
Zwr uwag na prostot tego projektu. Wykorzystujemy wycznie technik pojedynczego dziedziczenia. Co wicej, stosujemy wycznie dziedziczenie publiczne. Klasy
' i # definiuj jedynie funkcje charakterystyczne dla reprezentowanych przez siebie postaci wsplny kod funkcji
i
dziedzicz po klasie
. Wilhelm Ockham byby z nas dumny.
Mimo e nowy projekt jest prostszy od omawianego wczeniej wymagajcego dziedziczenia wielobazowego, moe pocztkowo robi wraenie bardziej skomplikowanego. W porwnaniu z wczeniejsz koncepcj (wykorzystujc technik dziedziczenia
wielobazowego), proponowana architektura z pojedynczym dziedziczeniem wie si
z koniecznoci wprowadzenia zupenie nowej klasy, ktra wczeniej nie bya konieczna. Po co wprowadza dodatkow klas, skoro nie jest potrzebna?
Ten przykad demonstruje uwodzicielski charakter techniki dziedziczenia wielobazowego. Dziedziczenie wielobazowe z zewntrz wyglda na atwiejsze nie wymaga
stosowania dodatkowych klas i chocia wie si z wywoywaniem kilku nowych
wirtualnych funkcji z klasy ' , nowe funkcje i tak musz zosta gdzie zdefiniowane.
Wyobramy sobie teraz programist konserwujcego wielk bibliotek klas C++, do
ktrej naley doda now klas (#) do istniejcej hierarchii #
#' . Programista wie, e z istniejcej hierarchii korzysta mnstwo klientw,
213
zatem im wiksze zmiany wprowadzi do biblioteki, tym wiksze bdzie ich niezadowolenie. Programista stawia sobie jednak za cel zminimalizowanie tego zjawiska.
Dokadnie analizujc wszystkie moliwoci, dochodzi do wniosku, e jeli doda relacj pojedynczego dziedziczenia po klasie ' do nowej klasy #, hierarchia nie bdzie wymagaa adnych dodatkowych modyfikacji. Jego rado jest
uzasadniona udao mu si znaczco zwikszy funkcjonalno biblioteki kosztem
minimalnego zwikszenia jej zoonoci.
Wyobra sobie teraz, e to Ty jeste tym programist. Nie daj si wic skusi technice
dziedziczenia wielokrotnego.
Sposb 44.
Mw to, o co czym naprawd mylisz.
Zdawaj sobie spraw z tego, co mwisz
Sposb 44. Mw to, o co czym naprawd mylisz...
214
Ponisze stwierdzenia dotycz sytuacji, w ktrych wykorzystywana jest technika publicznego dziedziczenia:
Istnienie w klasie czystej funkcji wirtualnej oznacza, e dziedziczony
bdzie wycznie interfejs tej klasy. Jeli klasa # deklaruje czyst funkcj
wirtualn , podklasy klasy # musz dziedziczy interfejs tej funkcji,
a konkretne podklasy klasy # musz dostarczy wasn implementacj
funkcji (patrz sposb 36.).
Deklaracja prostej funkcji wirtualnej oznacza, e dziedziczony bdzie