Cyfroteka.pl

klikaj i czytaj online

Cyfro
Czytomierz
00372 008919 10486926 na godz. na dobę w sumie
C++. 50 efektywnych sposobów na udoskonalenie Twoich programów - książka
C++. 50 efektywnych sposobów na udoskonalenie Twoich programów - książka
Autor: Liczba stron: 248
Wydawca: Helion Język publikacji: polski
ISBN: 83-7361-345-5 Data wydania:
Lektor:
Kategoria: ebooki >> komputery i informatyka >> programowanie >> c++ - programowanie
Porównaj ceny (książka, ebook, audiobook).

Pierwsze wydanie książki 'C++. 50 efektywnych sposobów na udoskonalenie twoich programów' zostało sprzedane w nakładzie 100 000 egzemplarzy i zostało przetłumaczone na cztery języki. Nietrudno zrozumieć, dlaczego tak się stało. Scott Meyers w charakterystyczny dla siebie, praktyczny sposób przedstawił wiedzę typową dla ekspertów -- czynności, które niemal zawsze wykonują lub czynności, których niemal zawsze unikają, by tworzyć prosty, poprawny i efektywny kod. Każda z zawartych w tej książce pięćdziesięciu wskazówek jest streszczeniem metod pisania lepszych programów w C++, zaś odpowiednie rozważania są poparte konkretnymi przykładami. Z myślą o nowym wydaniu, autor opracował od początku wszystkie opisywane w tej książce wskazówki. Wynik jego pracy jest wyjątkowo zgodny z międzynarodowym standardem C++, technologią aktualnych kompilatorów oraz najnowszymi trendami w świecie rzeczywistych aplikacji C++.

Do najważniejszych zalet książki 'C++. 50 efektywnych sposobów na udoskonalenie twoich programów' należą:

Książka 'C++. 50 efektywnych sposobów na udoskonalenie twoich programów' pozostaje jedną z najważniejszych publikacji dla każdego programisty pracującego z C++.

Znajdź podobne książki Ostatnio czytane w tej kategorii

Darmowy fragment publikacji:

IDZ DO IDZ DO PRZYK£ADOWY ROZDZIA£ PRZYK£ADOWY ROZDZIA£ SPIS TREĎCI SPIS TREĎCI KATALOG KSI¥¯EK KATALOG KSI¥¯EK KATALOG ONLINE KATALOG ONLINE ZAMÓW DRUKOWANY KATALOG ZAMÓW DRUKOWANY KATALOG TWÓJ KOSZYK TWÓJ KOSZYK DODAJ DO KOSZYKA DODAJ DO KOSZYKA CENNIK I INFORMACJE CENNIK I INFORMACJE ZAMÓW INFORMACJE ZAMÓW INFORMACJE O NOWOĎCIACH O NOWOĎCIACH ZAMÓW CENNIK ZAMÓW CENNIK CZYTELNIA CZYTELNIA FRAGMENTY KSI¥¯EK ONLINE FRAGMENTY KSI¥¯EK ONLINE Wydawnictwo Helion ul. Chopina 6 44-100 Gliwice tel. (32)230-98-63 e-mail: helion@helion.pl C++. 50 efektywnych sposobów na udoskonalenie Twoich programów Autor: Scott Meyers T³umaczenie: Miko³aj Szczepaniak ISBN: 83-7361-345-5 Tytu³ orygina³u: Effective C++: 50 Specific Ways to Improve Your Programs and Design Format: B5, stron: 248 Pierwsze wydanie ksi¹¿ki „C++. 50 efektywnych sposobów na udoskonalenie twoich programów” zosta³o sprzedane w nak³adzie 100 000 egzemplarzy i zosta³o przet³umaczone na cztery jêzyki. Nietrudno zrozumieæ, dlaczego tak siê sta³o. Scott Meyers w charakterystyczny dla siebie, praktyczny sposób przedstawi³ wiedzê typow¹ dla ekspertów — czynnoġci, które niemal zawsze wykonuj¹ lub czynnoġci, których niemal zawsze unikaj¹, by tworzyæ prosty, poprawny i efektywny kod. Ka¿da z zawartych w tej ksi¹¿ce piêædziesiêciu wskazówek jest streszczeniem metod pisania lepszych programów w C++, zaġ odpowiednie rozwa¿ania s¹ poparte konkretnymi przyk³adami. Z myġl¹ o nowym wydaniu, Scott Meyers opracowa³ od pocz¹tku wszystkie opisywane w tej ksi¹¿ce wskazówki. Wynik jego pracy jest wyj¹tkowo zgodny z miêdzynarodowym standardem C++, technologi¹ aktualnych kompilatorów oraz najnowszymi trendami w ġwiecie rzeczywistych aplikacji C++. Do najwa¿niejszych zalet ksi¹¿ki „C++. 50 efektywnych sposobów na udoskonalenie twoich programów” nale¿¹: • Eksperckie porady dotycz¹ce projektowania zorientowanego obiektowo, projektowania klas i w³aġciwego stosowania technik dziedziczenia • Analiza standardowej biblioteki C++, w³¹cznie z wp³ywem standardowej biblioteki szablonów oraz klas podobnych do string i vector na strukturê dobrze napisanych programów • Rozwa¿ania na temat najnowszych mo¿liwoġci jêzyka C++: inicjalizacji sta³ych wewn¹trz klas, przestrzeni nazw oraz szablonów sk³adowych • Wiedza bêd¹ca zwykle w posiadaniu wy³¹cznie doġwiadczonych programistów Ksi¹¿ka „C++. 50 efektywnych sposobów na udoskonalenie twoich programów” pozostaje jedn¹ z najwa¿niejszych publikacji dla ka¿dego programisty pracuj¹cego z C++. Scott Meyers jest znanym autorytetem w dziedzinie programowania w jêzyku C++; zapewnia us³ugi doradcze dla klientów na ca³ym ġwiecie i jest cz³onkiem rady redakcyjnej pisma C++ Report. Regularnie przemawia na technicznych konferencjach na ca³ym ġwiecie, jest tak¿e autorem ksi¹¿ek „More Effective C++” oraz „Effective C++ CD”. W 1993. roku otrzyma³ tytu³ doktora informatyki na Brown University. Spis treści Przedmowa ..................................................................................................................... 7 Podziękowania .............................................................................................................. 11 Wstęp........................................................................................................................... 15 Przejście od języka C do C++........................................................................................ 27 Sposób 1. Wybieraj const i inline zamiast #define...................................................W...................28 Sposób 2. Wybieraj iostream zamiast stdio.h ...................................................W..................31 Sposób 3. Wybieraj new i delete zamiast malloc i free ...................................................W............33 Sposób 4. Stosuj komentarze w stylu C++ ...................................................W...............................34 Zarządzanie pamięcią ................................................................................................... 37 Sposób 5. Używaj tych samych form w odpowiadających sobie zastosowaniach operatorów new i delete ...................................................W...........................................38 Sposób 6. Używaj delete w destruktorach dla składowych wskaźnikowych ..............................39 Sposób 7. Przygotuj się do działania w warunkach braku pamięci .............................................40 Sposób 8. Podczas pisania operatorów new i delete trzymaj się istniejącej konwencji ..............48 Sposób 9. Unikaj ukrywania „normalnej” formy operatora new ................................................51 Sposób 10. Jeśli stworzyłeś własny operator new, opracuj także własny operator delete ............53 Konstruktory, destruktory i operatory przypisania .......................................................... 61 Sposób 11. Deklaruj konstruktor kopiujący i operator przypisania dla klas z pamięcią przydzielaną dynamicznie ...................................................W.....................61 Sposób 12. Wykorzystuj konstruktory do inicjalizacji, a nie przypisywania wartości .................64 Sposób 13. Umieszczaj składowe na liście inicjalizacji w kolejności zgodnej z kolejnością ich deklaracji ...................................................W......................................69 Sposób 14. Umieszczaj w klasach bazowych wirtualne destruktory ............................................71 Sposób 15. Funkcja operator= powinna zwracać referencję do *this ...........................................76 Sposób 16. Wykorzystuj operator= do przypisywania wartości do wszystkich składowych klasy......79 Sposób 17. Sprawdzaj w operatorze przypisania, czy nie przypisujesz wartości samej sobie......82 Klasy i funkcje — projekt i deklaracja........................................................................... 87 Sposób 18. Staraj się dążyć do kompletnych i minimalnych interfejsów klas ..............................89 Sposób 19. Rozróżniaj funkcje składowe klasy, funkcje niebędące składowymi klasy i funkcje zaprzyjaźnione...................................................W.................................93 6 Spis treści Sposób 20. Unikaj deklarowania w interfejsie publicznym składowych reprezentujących dane ........98 Sposób 21. Wykorzystuj stałe wszędzie tam, gdzie jest to możliwe...........................................100 Sposób 22. Stosuj przekazywanie obiektów przez referencje, a nie przez wartości ...................106 Sposób 23. Nie próbuj zwracać referencji, kiedy musisz zwrócić obiekt ...................................109 Sposób 24. Wybieraj ostrożnie pomiędzy przeciążaniem funkcji a domyślnymi wartościami parametrów ...................................................W................113 Sposób 25. Unikaj przeciążania funkcji dla wskaźników i typów numerycznych......................117 Sposób 26. Strzeż się niejednoznaczności...................................................W................................120 Sposób 27. Jawnie zabraniaj wykorzystywania niejawnie generowanych funkcji składowych, których stosowanie jest niezgodne z Twoimi założeniami..................123 Sposób 28. Dziel globalną przestrzeń nazw ...................................................W.............................124 Implementacja klas i funkcji ....................................................................................... 131 Sposób 29. Unikaj zwracania „uchwytów” do wewnętrznych danych .......................................132 Sposób 30. Unikaj funkcji składowych zwracających zmienne wskaźniki lub referencje do składowych, które są mniej dostępne od tych funkcji ..................136 Sposób 31. Nigdy nie zwracaj referencji do obiektu lokalnego ani do wskaźnika zainicjalizowanego za pomocą operatora new wewnątrz tej samej funkcji .............139 Sposób 32. Odkładaj definicje zmiennych tak długo, jak to tylko możliwe ...............................142 Sposób 33. Rozważnie stosuj atrybut inline ...................................................W.............................144 Sposób 34. Ograniczaj do minimum zależności czasu kompilacji między plikami....................150 Dziedziczenie i projektowanie zorientowane obiektowo ............................................... 159 Sposób 35. Dopilnuj, by publiczne dziedziczenie modelowało relację „jest” ............................160 Sposób 36. Odróżniaj dziedziczenie interfejsu od dziedziczenia implementacji ........................166 Sposób 37. Nigdy nie definiuj ponownie dziedziczonych funkcji niewirtualnych .....................174 Sposób 38. Nigdy nie definiuj ponownie dziedziczonej domyślnej wartości parametru ............176 Sposób 39. Unikaj rzutowania w dół hierarchii dziedziczenia...................................................W.178 Sposób 40. Modelując relacje posiadania („ma”) i implementacji z wykorzystaniem, stosuj podział na warstwy ...................................................W......................................186 Sposób 41. Rozróżniaj dziedziczenie od stosowania szablonów ................................................189 Sposób 42. Dziedziczenie prywatne stosuj ostrożnie ...................................................W...............193 Sposób 43. Dziedziczenie wielobazowe stosuj ostrożnie...................................................W.........199 Sposób 44. Mów to, o co czym naprawdę myślisz. Zdawaj sobie sprawę z tego, co mówisz ....213 Rozmaitości ................................................................................................................ 215 Sposób 45. Miej świadomość, które funkcje są niejawnie tworzone i wywoływane przez C++....... 215 Sposób 46. Wykrywanie błędów kompilacji i łączenia jest lepsze od wykrywania błędów podczas wykonywania programów ....................................219 Sposób 47. Upewnij się, że nielokalne obiekty statyczne są inicjalizowane przed ich użyciem .......222 Sposób 48. Zwracaj uwagę na ostrzeżenia kompilatorów...................................................W........226 Sposób 49. Zapoznaj się ze standardową biblioteką C++ ...................................................W........227 Sposób 50. Pracuj bez przerwy nad swoją znajomością C++ ...................................................W..234 Skorowidz ................................................................................................................... 239 Dziedziczenie i projektowanie zorientowane obiektowo Wielu programistów wyraża opinię, że możliwość dziedziczenia jest jedyną korzyścią płynącą z programowania zorientowanego obiektowo. Można mieć oczywiście różne zdanie na ten temat, jednak liczba zawartych w innych częściach tej książki sposobów poświęconych efektywnemu programowaniu w C++ pokazuje, że masz do dyspozycji znacznie więcej rozmaitych narzędzi, niż tylko określanie, które klasy powinny dzie- dziczyć po innych klasach. Projektowanie i implementowanie hierarchii klas różni się od zasadniczo od wszyst- kich mechanizmów dostępnych w języku C. Problem dziedziczenia i projektowania zorientowanego obiektowo z pewnością zmusza do ponownego przemyślenia swojej strategii konstruowania systemów oprogramowania. Co więcej, język C++ udostępnia bardzo szeroki asortyment bloków budowania obiektów, włącznie z publicznymi, chronionymi i prywatnymi klasami bazowymi, wirtualnymi i niewirtualnymi klasami bazowymi oraz wirtualnymi i niewirtualnymi funkcjami składowymi. Każda z wy- mienionych własności może wpływać także na pozostałe komponenty języka C++. W efekcie, próby zrozumienia, co poszczególne własności oznaczają, kiedy powinny być stosowane oraz jak można je w najlepszy sposób połączyć z nieobiektowymi czę- ściami języka C++ może niedoświadczonych programistów gzniechęcić. Dalszą komplikacją jest fakt, że różne własności języka C++ są z pozoru odpowie- dzialne za te same zachowania. Oto przykłady:  Potrzebujesz zbioru klas zawierających wiele elementgów wspólnych. Powinieneś wykorzystać mechanizm dziedziczenia i stwgorzyć klasy potomne względem jednej wspólnej klasy bazowej czy gpowinieneś wykorzystać szablony i wygenerować wszystkie potrzegbne klasy ze wspólnym szkieletem kodu? 160 Dziedziczenie i projektowanie zorientowane obiektowo  Klasa A ma zostać zaimplementowana w oparciu o klasęg B. Czy A powinna zawierać składową reprezentującą obiekt klasy B czyg też powinna prywatnie dziedziczyć po klasie B?  Potrzebujesz projektu bezpiecznej pod względem typug i homogenicznej klasy pojemnikowej, która nie jest dostępna w standardowegj bibliotece C++ (listę pojemników udostępnianych przez tę bibliotekę podano, prezentując sposób 49.). Czy lepszym rozwiązaniem będzie skonstruowanie szabglonów czy budowa bezpiecznych pod względem typów interfejsów wokół tegj klasy, która sama byłaby zaimplementowana za pomocą ogólnych (XQKF ) wskaźników? W sposobach prezentowanych w tej części zawarłem wskazówki, jak należy znajdo- wać odpowiedzi na powyższe pytania. Nie mogę jednak liczyć na to, że uda mi się znaleźć właściwe rozwiązania dla wszystkich aspektów projektowania zorientowane- go obiektowo. Zamiast tego skoncentrowałem się więc na wyjaśnianiu, co faktycznie oznaczają poszczególne własności języka C++ i co tak naprawdę sygnalizujesz, sto- sując poszczególne dyrektywy czy instrukcje. Przykładowo, publiczne dziedziczenie oznacza relację „jest” lub specjalizacji-generalizacji (ang. isa, patrz sposób 35.) i jeśli musisz nadać mu jakikolwiek inny sens, możesz napotkać pewne problemy. Podobnie, funkcje wirtualne oznaczają, że „interfejs musi być dziedziczony”, natomiast funkcje niewirtualne oznaczają, że „dziedziczony musi być zarówno interfejs, jak i imple- mentacja”. Brak rozróżnienia tych znaczeń doprowadził już wielu programistów C++ do trudnych do opisania nieszczęść. Jeśli rozumiesz znaczenia rozmaitych własności języka C++, odkryjesz, że Twój po- gląd na projektowanie zorientowane obiektowo powoli ewoluuje. Zamiast przekonywać Cię o istniejących różnicach pomiędzy konstrukcjami językowymi, treść poniższych sposobów ułatwi Ci ocenę jakości opracowanych dotychczas systemów oprogramo- wania. Będziesz potem w stanie przekształcić swoją wiedzę w swobodne i właściwe operowanie własnościami języka C++ celem tworzenia jegszcze lepszych programów. Wartości wiedzy na temat znaczeń i konsekwencji stosowania poszczególnych kon- strukcji nie da się przecenić. Poniższe sposoby zawierają szczegółową analizę metod efektywnego stosowania omawianych własności języka C++. W sposobie 44. podsu- mowałem cechy i znaczenia poszczególnych konstrukcji obiektowych tego języka. Treść tego sposobu należy traktować jak zwieńczenie całej części, a także zwięzłe streszczenie, do którego warto zaglądać w przyszłośgci. Sposób 35. Dopilnuj, by publiczne dziedziczenie modelowało relację „jest” Sposób 35. Dopilnuj, by publiczne dziedziczenie modelowało relację „jest” William Dement w swojej książce pt. Some Must Watch While Some Must Sleep (W. H. Freeman and Company, 1974) opisał swoje doświadczenia z pracy ze studen- tami, kiedy próbował utrwalić w ich umysłach najistotniejsze tezy swojego wykładu. Sposób 35. Dopilnuj, by publiczne dziedziczenie modelowało relację „jest” 161 Mówił im, że przyjmuje się, że świadomość historyczna przeciętnego brytyjskiego dziecka w wieku szkolnym wykracza poza wiedzę, że bitwa pod Hastings odbyła się w roku 1066. William Dement podkreśla, że jeśli dziecko pamięta więcej szczegółów, musi także pamiętać o tej historycznej dla Brytyjczygków dacie. Na tej podstawie autor wnioskuje, że w umysłach jego studentów zachowuje się tylko kilka istotnych i naj- ciekawszych faktów, włącznie z tym, że np. tabletki nasenne powodują bezsenność. Namawiał studentów, by zapamiętali przynajmniej tych kilka najważniejszych faktów, nawet jeśli mają zapomnieć wszystkie pozostałe zagadnienia dyskutowane podczas wykładów. Autor książki przekonywał do tego swoich studentów wielo- krotnie w czasie semestru. Ostatnie pytanie testu w sesji egzaminacyjnej brzmiało: „wymień jeden fakt, który wy- niosłeś z moich wykładów, i który na pewno zapamiętasz do końca życia”. Po spraw- dzeniu egzaminów Dement był zszokowany — niemal wszygscy napisali „1066”. Jestem teraz pełen obaw, że jedynym istotnym wnioskiem, który wyniesiesz z tej książki na temat programowania zorientowanego obiektowo w C++ będzie to, że me- chanizm publicznego dziedziczenia oznacza relację „jest”. Zachowaj jednak ten fakt w swojej pamięci. Jeśli piszesz klasę D (od ang. Derived, czyli klasę potomną), która publicznie dziedzi- czy po klasie B (od ang. Base, czyli klasy bazowej), sygnalizujesz kompilatorom C++ (i przyszłym czytelnikom Twojego kodu), że każdy obiekt typu D jest także obiektem typu B, ale nie odwrotnie. Sygnalizujesz, że B reprezentuje bardziej ogólne pojęcia niż D, natomiast D reprezentuje bardziej konkretne pojęcia niż B. Utrzymujesz, że wszędzie tam, gdzie może być użyty obiekt typu B, może być także wykorzystany obiekt typu D, ponieważ każdy obiekt typu D jest także obiektem typu B. Z drugiej strony, jeśli potrzebujesz obiektu typu D, obiekt typu B nie będzie mógł go zastąpić — publiczne dziedziczenie oznacza relację D „jest” B, gale nie odwrotnie. Taką interpretację publicznego dziedziczenia wymusza język C++. Przeanalizujmy poniższy przykład: ENCUU2GTUQP]_ ENCUU5VWFGPVRWDNKE2GTUQP]_ Oczywiste jest, że każdy student jest osobą, nie każda osoba jest jednak studentem. Dokładnie takie samo znaczenie ma powyższa hierarchia. Oczekujemy, że wszystkie istniejące cechy danej osoby (np. to, że ma jakąś datę urodzenia) istnieją także dla stu- denta; nie oczekujemy jednak, że wszystkie dane dotyczące studenta (np. adres szkoły, do której uczęszcza) będą istotne dla wszystkich ludzi. Pojęcie osoby jest bowiem bardziej ogólne, niż pojęcie studenta — student jesgt specyficznym „rodzajem” osoby. W języku C++ każda funkcja oczekująca argumentu typu 2GTUQP (lub wskaźnika do obiektu klasy 2GTUQP bądź referencji do obiektu klasy 2GTUQP) może zamiennie pobie- rać obiekt klasy 5VWFGPV (lub wskaźnik do obiektu klasy 5VWFGPV bądź referencję do obiektu klasy 5VWFGPV): XQKFFCPEG EQPUV2GTUQPR MCľF[OQľGVCēEP\[è XQKFUVWF[ EQPUV5VWFGPVU V[NMQUVWFGPEKPOQIæUVWFKQYCè 162 Dziedziczenie i projektowanie zorientowane obiektowo 2GTUQPRRTGRTG\GPVWLGQUQDú QDKGMVMNCU[2GTUQP 5VWFGPVUUTGRTG\GPVWLGUPVWFGPVC QDKGMVMNCU[5VWFGPV FCPEG R FQDT\GRTGRTG\GPPVWLGQUQDú FCPEG U FQDT\GUTGRTG\GPPVWLGUVWFGPVC CYKúEVCMľGQUQPDú UVWF[ U FQDT\G UVWF[ R DđæFRPKGTGRTGP\GPVWLGUVWFGPVC Powyższe komentarze są prawdziwe tylko dla publicznego dziedziczenia. C++ będzie się zachowywał w opisany sposób tylko w przypadku, gdy klasa 5VWFGPV będzie publicz- nie dziedziczyła po klasie 2GTUQP. Dziedziczenie prywatne oznacza coś zupełnie innego (patrz sposób 42.), natomiast znaczenie dziedziczenia gchronionego jest nieznane. Równoważność dziedziczenia publicznego i relacji „jest” wydaje się oczywista, w prakty- ce jednak właściwe modelowanie tej relacji nie jest już takie proste. Niekiedy nasza intuicja może się okazać zawodna. Przykładowo, faktem jest, że pingwin to ptak; faktem jest także, że ptaki mogą latać. Gdybyśmy w swojej naiwności spróbowali wy- razić to w C++, nasze wysiłki przyniosłyby efekt podobgny do poniższego: ENCUU$KTF] RWDNKE XKTVWCNXQKFHN[ RVCMKOQIæNCVCPè  _ ENCUU2GPIWKPRWDNKE$KTF]RKPIYKP[UæRVCMCOK  _ Mamy teraz problem, ponieważ z powyższej hierarchii wynika, że pingwiny mogą latać, co jest oczywiście nieprawdą. Co stało się z gnaszą strategią? W tym przypadku padliśmy ofiarą nieprecyzyjnego języka naturalnego (polskiego). Kiedy mówimy, że ptaki mogą latać, w rzeczywistości nie mamy na myśli tego, że wszystkie ptaki potrafią latać, a jedynie, że w ogólności ptaki mają możliwość latania. Gdybyśmy byli bardziej precyzyjni, wyrazilibyśmy się inaczej, by podkreślić fakt, że istnieje wiele gatunków ptaków, które nie latają — otrzymalibyśmy wówczas poniż- szą, znacznie lepiej modelującą rzeczywistość, hiergarchię klas: ENCUU$KTF] DTCMFGMNCTCELKPHWPMELKHN[ _ ENCUU(N[KPI$KTFRWDNKE$KTF] RWDNKE XKTVWCNXQKFHN[   _ ENCUU0QP(N[KPI$KTFRWDNKE$KTF] DTCMFGMNCTCELKPHWPMELKHN[ _ Sposób 35. Dopilnuj, by publiczne dziedziczenie modelowało relację „jest” 163 ENCUU2GPIWKPRWDNKE0QP(N[KPI$KTF] DTCMFGMNCTCELKPHWPMELKHN[ _ Powyższa hierarchia jest znacznie bliższa naszej rzeczywistej wiedzy na temat pta- ków, niż ta zaprezentowana wcześniej. Nasze rozwiązanie nie jest jednak jeszcze skończone, ponieważ w niektórych syste- mach oprogramowania, proste stwierdzenie, że pingwin jest ptakiem, będzie całkowicie poprawne. W szczególności, jeśli nasza aplikacja dotyczy wyłącznie dziobów i skrzydeł, a w żadnym stopniu nie wiąże się z lataniem, oryginalna hierarchia będzie w zupełno- ści wystarczająca. Mimo że jest to dosyć irytujące, omawiana sytuacja jest prostym odzwierciedleniem faktu, że nie istnieje jedna doskonała metoda projektowania do- wolnego oprogramowania. Dobry projekt musi po prostu uwzględniać wymagania stawiane przed tworzonym systemem, zarówno te w danej chwili oczywiste, jak i te, które mogą się pojawić w przyszłości. Jeśli nasza aplikacja nie musi i nigdy nie będzie musiała uwzględniać możliwości latania, rozwiązaniem w zupełności wystarczającym będzie stworzenie klasy 2GPIWKP jako potomnej klasy $KTF. W rzeczywistości taki projekt może być nawet lepszy niż rozróżnienie ptaków latających od nielatających, ponieważ takie rozróżnienie może w ogóle nie istnieć w modelowanym świecie. Dodawanie do hierarchii niepotrzebnych klas jest błędną decyzją projektową, ponie- waż narusza prawidłowe relacje dziedziczenia pomiędgzy klasami. Istnieje jeszcze inna strategia postępowania w przypadku omawianego problemu: „wszystkie ptaki mogą latać, pingwiny są ptakami, pingwiny nie mogą latać”. Strate- gia polega na wprowadzeniu takich zmian w definicji funkcji HN[, by dla pingwinów generowany był błąd wykonania: XQKFGTTQT EQPUVUVTKPIOUI HWPMELC\FGHKPKQYCPPCYKPP[OOKGLUEW ENCUU2GPIWKPRWDNKE$KTF] RWDNKE XKTVWCNXQKFHN[ ]GTTQT 2KPIYKP[PKGOQIæNCVCè P_  _ Do takiego rozwiązania dążą twórcy języków interpretowanych (jak Smalltalk), jed- nak istotne jest prawidłowe rozpoznanie rzeczywistego znaczenia powyższego kodu, które jest zupełnie inne, niż mógłbyś przypuszczać. Ciało funkcji nie oznacza bowiem, że „pingwiny nie mogą latać”. Jej faktyczne znaczenie to: „pingwiny mogą latać, jed- nak kiedy próbują to robić, powodują błąd”. Na czym polega różnica pomiędzy tymi znaczeniami? Wynika przede wszystkim z możliwości wykrycia błędu — ogranicze- nie „pingwiny nie mogą latać” może być egzekwowane przez kompilatory, natomiast naruszenie ograniczenia „podejmowana przez pingwiny próba latania powoduje błąd” może zostać wykryte tylko podczas wykonywania progrgamu. Aby wyrazić ograniczenie „pingwiny nie mogą latać”, wystarczy nie definiować odpowiedniej funkcji dla obiektów klasy 2GPIWKP: ENCUU$KTF] DTCMFGMNCTCELKPHWPMELKHN[ _ 164 Dziedziczenie i projektowanie zorientowane obiektowo ENCUU0QP(N[KPI$KTFRWDNKE$KTF] DTCMFGMNCTCELKPHWPMELKHN[ _ ENCUU2GPIWKPRWDNKE0QP(N[KPI$KTF] DTCMFGMNCTCELKPHWPMELKHN[ _ Jeśli spróbujesz teraz wywołać funkcję HN[ dla obiektu reprezentującego pingwina, kompilator zasygnalizuje błąd: 2GPIWKPR RHN[ DđæF Zaprezentowane rozwiązanie jest całkowicie odmienne od podejścia stosowanego w języku Smalltalk. Stosowana tam strategia powoduje,g że kompilator skompilowałby podobny kod bez przeszkód. Filozofia języka C++ jest jednak zupełnie inna niż filozofia języka Smalltalk, dopóki jednak programujesz w C++, powinieneś stosować się wyłącznie do reguł obowiązu- jących w tym języku. Co więcej, wykrywanie błędów w czasie kompilacji (a nie w czasie wykonywania) programu wiąże się z pewnymi technicznymi korzyściami — patrz sposób 46. Być może przyznasz, że Twoja wiedza z zakresu ornitologii ma raczej intuicyjny charakter i może być zawodna, zawsze możesz jednak polegać na swojej biegłości w dziedzinie podstawowej geometrii, prawda? Nie martw się, mam na myśli wyłącz- nie prostokąty i kwadraty. Spróbuj więc odpowiedzieć na pytanie: czy reprezentująca kwadraty klasa 5SWCTG publicznie dziedziczy po reprezentującej prostokąty klasie 4GEVCPING? Powiesz pewnie: „Też coś! Każde dziecko wie, że kwadrat jest prostokątem, ale w ogól- ności prostokąt nie musi być kwadratem”. Tak, to prawda, przynajmniej na poziomie gimnazjum. Nie sądzę jednak, byśmy kiedykolwiek wrócgili do nauki na tym poziomie. Przeanalizuj więc poniższy kod: ENCUU4GEVCPING] RWDNKE XKTVWCNXQKFUGV*GKIJV KPVPGY*GKIJV  XKTVWCNXQKFUGV9KFVJ KPVPGY9KFVJ  XKTVWCNKPVJGKIJV EQPUVHWPMELG\YTCECLæDKGľæEG XKTVWCNKPVYKFVJ EQPUVYCTVQħEK  _ Sposób 35. Dopilnuj, by publiczne dziedziczenie modelowało relację „jest” 165 XQKFOCMG$KIIGT 4GEVCPINGT HWPMELC\YKúMU\CLæPECRQNG ]RQYKGT\EJPKPRTQUVQMæVCT KPVQNF*GKIJVTJGKIJV  TUGV9KFVJ TYKFVJ   FQFCLGFQU\GTPQMQħEKT CUUGTV TJGKIJV QNF*GKIJV WRGYPKCUKúľGYP[UQMQħè _RTQUVQMæVPCTPKG\OKGPKđCUKú Jest oczywiste, że ostatnia instrukcja nigdy nie zakończy się niepowodzeniem, ponieważ funkcja OCMG$KIIGT modyfikuje wyłącznie szerokość prostokąta reprezen- towanego przez T. Rozważ teraz poniższy fragment kodu, w którym wykorzystujemy publiczne dziedzi- czenie umożliwiające traktowanie kwadratów jak prosgtokątów: ENCUU5SWCTGRWDNKE4GEVCPING]_ 5SWCTGU  CUUGTV UYKFVJ UJGKIJV YCTWPGMOWUKD[PèRTCYF\KY[ FNCYU\[UVPMKEJMYCFTCVÎY OCMG$KIIGT U PCUMWVGMFP\KGF\KE\GPKCU LGUVYTGNPCELKLGUV\MNCUæ 4GEVCPINGPOQľGO[YKúE\YKúMU\[è RQNGLGIQPRQYKGT\EJPK CUUGTV U9KFVJ UJGKIJV YCTWPGMPCFCNOWUKD[èRTCYF\KY[ FNCYU\[UVPMKEJMYCFTCVÎY Także teraz oczywiste jest, że ostatni warunek nigdy nie powinien być fałszywy. Zgodnie z definicją, szerokość kwadratu jest przeciegż taka sama jak jego wysokość. Tym razem mamy jednak problem. Jak można pogodzić pongiższe twierdzenia?  Przed wywołaniem funkcji OCMG$KIIGT wysokość kwadratu reprezentowanego przez obiekt U jest taka sama jak jego szerokość.  Wewnątrz funkcji OCMG$KIIGT modyfikowana jest szerokość kwadratu, jednak wysokość pozostaje niezmieniona.  Po zakończeniu wykonywania funkcji OCMG$KIIGT wysokość kwadratu U ponownie jest taka sama jak jego szerokość (zauważ,g że obiekt U jest przekazywany do funkcji OCMG$KIIGT przez referencję, zatem funkcja modyfikuje ten sam obiekt U, nie jego kopię). Jak to możliwe? Witaj w cudownym świecie publicznego dziedziczenia, w którym Twój instynkt — sprawdzający się do tej pory w innych dziedzinach, włącznie z matematyką — może nie być tak pomocny, jak tego oczekujesz. Zasadniczym problemem jest w tym przy- padku to, że operacja, którą można stosować dla prostokątów (jego szerokość może być zmieniana niezależnie od wysokości), nie może być stosowana dla kwadratów (definicja figury wymusza równość jej szerokości i wysokości). Mechanizm publiczne- go dziedziczenia zakłada jednak, że absolutnie wszystkie operacje stosowane z powo- dzeniem dla obiektów klasy bazowej mogą być stosowane także dla obiektów klasy 166 Dziedziczenie i projektowanie zorientowane obiektowo potomnej. W przypadku prostokątów i kwadratów (podobny przykład dotyczący zbio- rów i list omawiam w sposobie 40.) to założenie się nie sprawdza, zatem stosowanie publicznego dziedziczenia do modelowania występującej między nimi relacji jest po prostu błędne. Kompilatory oczywiście umożliwią Ci zaprogramowanie takiego mo- delu, jednak — jak się już przekonaliśmy — nie mamy gwarancji, że nasz program będzie się zachowywał prawidłowo. Od czasu do czasu każdy programista musi się przekonać (niektórzy częściej, inni rzadziej), że poprawne skompilowanie programu nie oznacza, że będzie on działał zgodnie z oczekiwganiami. Nie denerwuj się, że rozwijana przez lata intuicja dotycząca tworzonego oprogramo- wania traci moc w konfrontacji z projektowaniem zorientowanym obiektowo. Twoja wiedza jest nadal cenna, jednak dodałeś właśnie do swojego arsenału rozwiązań pro- jektowych silny mechanizm dziedziczenia i będziesz musiał rozszerzyć swoją intuicję w taki sposób, by prowadziła Cię do właściwego wykorzystywania nowych umiejęt- ności. Z czasem problem klasy 2GPIWKP dziedziczącej po klasie $KTF lub klasie 5SWCTG dziedziczącej po klasie 4GEVCPING będzie dla Ciebie równie zabawny jak prezentowa- ne Ci przez niedoświadczonych programistów funkcje zajmujące wiele stron. Możli- we, że proponowane podejście do tego typu problemów jest właściwe, nadal jednak nie jest to bardzo prawdopodobne. Relacja „jest” nie jest oczywiście jedyną relacją wysgtępującą pomiędzy klasami. Dwie pozostałe powszechnie stosowane relacje między klasami to relacja „ma” (ang. has-a) oraz relacja implementacji z wykorzystaniem (ang. is-implemented-in-terms-of). Relacje te przeanalizujemy podczas prezentacji sposobów 40. i 42. Nierzadko pro- jekty C++ ulegają zniekształceniom, ponieważ któraś z pozostałych najważniejszych relacji została błędnie zamodelowana jako „jest”, powinniśmy więc być pewni, że właściwie rozróżniamy te relacje i wiemy, jak należy je najlepiej modelować w C++. Sposób 36. Odróżniaj dziedziczenie interfejsu od dziedziczenia implementacji Sposób 36. Odróżniaj dziedziczenie interfejsu od dziedziczenia implementacji Po przeprowadzeniu dokładnej analizy okazuje się, że pozornie oczywiste pojęcie (publicznego) dziedziczenia składa się w rzeczywistości z dwóch rozdzielnych części — dziedziczenia interfejsów funkcji oraz dziedziczenia implementacji funkcji. Różnica pomiędzy wspomnianymi rodzajami dziedziczenia ściśle odpowiada różnicy pomię- dzy deklaracjami a definicjami funkcji (omówionej we gwstępie do tej książki). Jako projektant klasy potrzebujesz niekiedy takich klas potomnych, które dziedziczą wyłącznie interfejs (deklarację) danej funkcji składowej; czasem potrzebujesz klas potomnych dziedziczących zarówno interfejs, jak i implementację danej funkcji, jednak masz zamiar przykryć implementację swoim rozwiązaniem; zdarza się także, że potrzebujesz klas potomnych dziedziczących zarówno interfejs, jak i implementację danej funkcji, ale bez możliwości przykrywania czeggokolwiek. Sposób 36. Odróżniaj dziedziczenie interfejsu od dziedziczenia implementacji 167 Aby lepiej zrozumieć różnicę pomiędzy zaproponowanymi opcjami, przeanalizuj poniższą hierarchię klas reprezentującą figury geometryczne w aplikacji graficznej: ENCUU5JCRG] RWDNKE XKTVWCNXQKFFTCY EQPUV XKTVWCNXQKFGTTQT EQPUVUVTKPIOUI  KPVQDLGEV+ EQPUV  _ ENCUU4GEVCPINGRWDNKE5JCRG]_ ENCUU NNKRUGRWDNKE5JCRG]_ 5JCRG jest klasą abstrakcyjną. Można to poznać po obecności czystej funkcji wirtual- nej FTCY. W efekcie klienci nie mogą tworzyć egzemplarzy klasy 5JCRG, mogą to robić wyłącznie klasy potomne. Mimo to klasa 5JCRG wywiera ogromny nacisk na wszyst- kie klasy, które (publicznie) po niej dziedziczą, pognieważ:  Interfejsy funkcji składowych zawsze są dziedziczone. W sposobie 35. wyjaśniłem, że dziedziczenie publiczne oznacza faktgycznie relację „jest”, zatem wszystkie elementy istniejące w klasie bazowegj muszą także istnieć w klasach potomnych. Jeśli więc daną funkcję można wykgonać dla danej klasy, musi także istnieć sposób jej wykonania dla gjej podklas. W funkcji 5JCRG zadeklarowaliśmy trzy funkcje. Pierwsza, FTCY, rysuje na ekranie bieżący obiekt. Druga, GTTQT, jest wywoływana przez inne funkcje składowe w mo- mencie, gdy konieczne jest zasygnalizowanie błędu. Trzecia, QDLGEV+ , zwraca unikalny całkowitoliczbowy identyfikator bieżącego obiektu (przykład wykorzystania tego typu funkcji znajdziesz w sposobie 17.). Każda z wymienionych funkcji została zadekla- rowana w inny sposób: FTCY jest czystą funkcją wirtualną, GTTQT jest prostą (nieczystą?) funkcją wirtualną, natomiast QDLGEV+ jest funkcją niewirtualną. Jakie jest znaczenie tych trzech różnych deklaracji? Rozważmy najpierw czystą funkcję wirtualną FTCY. Dwie najistotniejsze cechy czys- tych funkcji wirtualnych to konieczność ich ponownego zadeklarowania w każdej dziedziczącej je konkretnej klasie oraz brak ich definicji w klasach abstrakcyjnych. Jeśli połączymy te własności, uświadomimy sobie, żeg:  Celem deklarowania czystych funkcji wirtualnych jest ogtrzymanie klas potomnych dziedziczących wyłącznie interfejs. Jest to idealne rozwiązanie dla funkcji 5JCRGFTCY, ponieważ naturalne jest udostęp- nienie możliwości rysowania wszystkich obiektów klasy 5JCRG, jednak niemożliwe jest opracowanie jednej domyślnej implementacji dla takiej funkcji. Algorytm ryso- wania np. elips różni się przecież znacznie od algorytmu rysowania prostokątów. Właściwym sposobem interpretowania znaczenia deklaracji funkcji 5JCRGFTCY jest instrukcja skierowana do projektantów podklas: „musicie stworzyć funkcję FTCY, jed- nak nie mam pojęcia, jak moglibyście ją zaimplementgować”. 168 Dziedziczenie i projektowanie zorientowane obiektowo Istnieje niekiedy możliwość opracowania definicji czystej funkcji wirtualnej. Oznacza to, że możesz stworzyć taką implementację dla funkcji 5JCRGFTCY, że kompilatory C++ nie zgłoszą żadnych zastrzeżeń, jednak jedynym sposobem jej wywołania byłoby wykorzystanie pełnej nazwy włącznie z nazwą klasy: 5JCRG RUPGY5JCRGDđæF5JCRGLGUVMNCUæCPDUVTCME[LPæ 5JCRG RUPGY4GEVCPINGFQDT\G RU FTCY Y[YQđWLG4GEVCPINGFTCPY 5JCRG RUPGY NNKRUGFQDT\G RU FTCY Y[YQđWLG NNKRUGFTCY RU 5JCRGFTCY Y[YQđWLG5JCRGFTCY RU 5JCRGFTCY Y[YQđWLG5JCRGFTCY Poza faktem, że powyższe rozwiązanie może zrobić wrażenie na innych programi- stach podczas imprezy, w ogólności znajomość zaprezentowanego fenomenu jest w praktyce mało przydatna. Jak się jednak za chwilę przekonasz, może być wykorzy- stywana jako mechanizm udostępniania bezpieczniejszgej domyślnej implementacji dla prostych (nieczystych) funkcji wirtualnych. Niekiedy dobrym rozwiązaniem jest zadeklarowanie klasy zawierającej wyłącznie czyste funkcje wirtualne. Takie klasy protokołu udostępniają klasom potomnym jedy- nie interfejsy funkcji, ale nigdy ich implementacje. Klasy protokołu opisałem, pre- zentując sposób 34., i wspominam o nich ponownie w spogsobie 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 udostępniają także swoje implementacje, które mogą (ale nie muszą) być przykryte w klasach potomnych. Po chwili namysłu powinieneś dojść do wniosku, że:  Celem deklarowania prostej funkcji wirtualnej jest ogtrzymanie klas potomnych dziedziczących zarówno interfejs, jak i domyślną implementację tej funkcji. W przypadku funkcji 5JCRGGTTQT interfejs określa, że każda klasa musi udostępniać funkcję wywoływaną w momencie wykrycia błędu, jednak obsługa samych błędów jest dowolna i zależy wyłącznie od projektantów klas potomnych. Jeśli nie przewidują oni żadnych specjalnych działań w przypadku znalezienia błędu, mogą wykorzystać udostęp- niany przez klasę 5JCRG domyślny mechanizm obsługi błędów. Oznacza to, że rzeczywi- stym znaczeniem deklaracji funkcji 5JCRGGTTQT dla projektantów podklas jest zda- nie: „musisz obsłużyć funkcję GTTQT, jednak jeśli nie chcesz tworzyć własnej wersji tej funkcji, możesz wykorzystać jej domyślną wersjęg zdefiniowaną dla klasy 5JCRG”. Okazuje się, że zezwalanie prostym funkcjom wirtualnym na precyzowanie zarówno deklaracji, jak i domyślnej implementacji może być niebezpieczne. Aby przekonać się dlaczego, przeanalizuj zaprezentowaną poniżej hierarchię samolotów należących do linii lotniczych XYZ. Linie XYZ posiadają tylko dwa typy samolotów, Model A i Model B, z których oba latają w identyczny sposób. Linie lotnicze XYZ zaprojek- towały więc następującą hierarchię klas: Sposób 36. Odróżniaj dziedziczenie interfejsu od dziedziczenia implementacji 169 ENCUU#KTRQTV]_TGRTG\GPVWLGNQVPKUMCP ENCUU#KTRNCPG] RWDNKE XKTVWCNXQKFHN[ EQPUV#KTRQTVFGUVKPCVKQP   _ XQKF#KTRNCPGHN[ EQPUV#KTRQTVFGUVKPCVKQP ] FQO[ħNP[MQFOQFGNWLæE[RT\GNQVUCOQNQVW FQFCPGIQEGNW _ ENCUU/QFGN#RWDNKE#KTRNCPG]_ ENCUU/QFGN$RWDNKE#KTRNCPG]_ Aby wyrazić fakt, że wszystkie samoloty muszą obsługiwać jakąś funkcję HN[ oraz z uwagi na możliwe wymagania dotyczące innych implementacji tej funkcji genero- wane przez nowe modele samolotów, funkcja #KTRNCPGHN[ została zadeklarowana jako wirtualna. Aby uniknąć pisania identycznego kodu w klasach /QFGN# i /QFGN$, domyślny model latania został jednak zapisany w formie ciała funkcji #KTRNCPGHN[, które jest dziedziczone zarówno przez klasę /QFGN#, jak i klasę /QFGN$. Jest to klasyczny projekt zorientowany obiektowo. Dwie klasy współdzielą wspólny element (sposób implementacji funkcji HN[), zatem element ten zostaje przeniesiony do klasy bazowej i jest dziedziczony przez te dwie klasy. Takie rozwiązanie ma wiele istotnych zalet: pozwala uniknąć powielania tego samego kodu, ułatwia przyszłe roz- szerzenia systemu i upraszcza konserwację w długim okresie czasu — wszystkie wy- mienione własności są charakterystyczne właśnie dla technologii obiektowej. Linie lotnicze XYZ powinny więc być dumne ze swojego systemgu. Przypuśćmy teraz, że firma XYZ rozwija się i postanowiła pozyskać nowy typ samo- lotu — Model C. Nowy samolot różni się nieco od Modelu A i Modelu B, a w szcze- gólności ma inne właściwości lotu. Programiści omawianych linii lotniczych dodają więc do hierarchii klasę repre- zentującą samoloty Model C, jednak w pośpiechu zapomnieli ponownie zdefiniować funkcję HN[: ENCUU/QFGN RWDNKE#KTRNCPG] DTCMFGMNCTCELKHWPMELKHN[ _ Ich kod zawiera więc coś podobnego do poniższego fraggmentu: #KTRQTV1MGEKG  1MúEKGVQNQVPKUMQY9CPTU\CYKG #KTRNCPG RCPGY/QFGN   RC HN[ 1MGEKG Y[YQđWLGHWPMELú#KTRNCPGPHN[ 170 Dziedziczenie i projektowanie zorientowane obiektowo Mamy do czynienia z prawdziwą katastrofą, a mianowicie z próbą obsłużenia lotu obiektu klasy /QFGN , jakby był obiektem klasy /QFGN# lub klasy /QFGN$. Z pewnością nie wzbudzimy w ten sposób zaufania u klientów linigi lotniczych. Problem nie polega tutaj na tym, że zdefiniowaliśmy domyślne zachowanie funkcji #KTRNCPGHN[, tylko na tym, że klasa /QFGN mogła przypadkowo (na skutek nieuwagi programistów) dziedziczyć to zachowanie. Na szczęście istnieje możliwość przekazywa- nia domyślnych zachowań funkcji do podklas wyłącznie w przypadku, gdy ich twórcy wyraźnie tego zażądają. Sztuczka polega na przerwaniu połączenia pomiędzy interfejsem wirtualnej funkcji a jej domyślną implementacją. Oto sposób realizacji tego zadania: ENCUU#KTRNCPG] RWDNKE XKTVWCNXQKFHN[ EQPUV#KTRQTVFGUVKPCVKQP   RTQVGEVGF XQKFFGHCWNV(N[ EQPUV#KTRQTVFGUVKPCVKQP  _ XQKF#KTRNCPGFGHCWNV(N[ EQPUV#KTRQTVFGUVKPCVKQP ] FQO[ħNP[MQFOQFGNWLæE[RT\GNQVUCOQNQVW FQFCPGIQEGNW _ Zwróć uwagę na sposób, w jaki przekształciliśmy #KTRNCPGHN[ w czystą funkcję wirtualną. Zaprezentowana klasa udostępnia tym samym interfejs funkcji obsługują- cej latanie samolotów. Klasa #KTRNCPG zawiera także jej domyślną implementację, jednak tym razem w formie niezależnej funkcji, FGHCWNV(N[. Klasy podobne do /QFGN# i /QFGN$ mogą wykorzystać domyślną implementację, zwyczajnie wywołując funkcję FGHCWNV(N[ wbudowaną w ciało ich funkcji HN[ (jednak zanim to zrobisz, prze- czytaj sposób 33., gdzie przeanalizowałem wzajemne oddziaływanie atrybutów KPNKPG i XKTVWCN dla funkcji składowych): ENCUU/QFGN#RWDNKE#KTRNCPG] RWDNKE XKTVWCNXQKFHN[ EQPUV#KTRQTVFGUVKPCVKQP ]FGHCWNV(N[ FGUVKPCVKQP _  _ ENCUU/QFGN$RWDNKE#KTRNCPG] RWDNKE XKTVWCNXQKFHN[ EQPUV#KTRQTVFGUVKPCVKQP ]FGHCWNV(N[ FGUVKPCVKQP _  _ W przypadku klasy /QFGN nie możemy już przypadkowo dziedziczyć niepoprawnej implementacji funkcji HN[, ponieważ czysta funkcja wirtualna w klasie #KTRNCPG wy- musza na projektantach nowej klasy stworzenie własngej wersji funkcji HN[: Sposób 36. Odróżniaj dziedziczenie interfejsu od dziedziczenia implementacji 171 ENCUU/QFGN RWDNKE#KTRNCPG] RWDNKE XKTVWCNXQKFHN[ EQPUV#KTRQTVFGUVKPCVKQP   _ XQKF/QFGN HN[ EQPUV#KTRQTVFGUVKPCVKQP ] MQFOQFGNWLæE[RT\GNQVUCOQNQVWV[RW/QFGN FQFCGPGIQEGNW _ Powyższy schemat postępowania nie jest oczywiście całkowicie bezpieczny (progra- miści nadal mają możliwość popełniania fatalnych w skutkach błędów), jednak jest znacznie bardziej niezawodny od oryginalnego projektu. Funkcja #KTRNCPGFGHCWNV(N[ jest chroniona, ponieważ w rzeczywistości jest szczegółem implementacyjnym klasy #KTRNCPG i jej klas potomnych. Klienci wykorzystujący te klasy powinni zajmować się wyłącznie własnościami lotu reprezentowanych samolotów, a nie sposobami imple- mentowania tych własności. Ważne jest także to, że funkcja #KTRNCPGFGHCWNV(N[ jest niewirtualna. Wynika to z faktu, że żadna z podklas nie powinna jej ponownie definiować — temu zagadnieniu poświęciłem sposób 37. Gdyby funkcja FGHCWNV(N[ była wirtualna, mielibyśmy do czynienia ze znanym nam już problemem: co stanie się, jeśli projektant którejś z pod- klas zapomni ponownie zdefiniować funkcję FGHCWNV(N[ w sytuacji, gdzie będzie to konieczne? Niektórzy programiści sprzeciwiają się idei definiowania dwóch osobnych funkcji dla interfejsu i domyślnej implementacji (jak HN[ i FGHCWNV(N[). Z jednej strony zauwa- żają, że takie rozwiązanie zaśmieca przestrzeń nazw klasy występującymi wielokrotnie zbliżonymi do siebie nazwami funkcji. Z drugiej strony zgadzają się z tezą, że należy oddzielić interfejs od domyślnej implementacji. Jak więc powinniśmy radzić sobie z tą pozorną sprzecznością? Wystarczy wykorzystać fakt, że czyste funkcje wirtualne muszą być ponownie deklarowane w podklasach, ale mogą także zawierać własne implementacje. Oto sposób, w jaki możemy wykorzystać w hierarchii klas reprezen- tujących samoloty możliwość definiowania czystej funkgcji wirtualnej: ENCUU#KTRNCPG] RWDNKE XKTVWCNXQKFHN[ EQPUV#KTRQTVFGUVKPCVKQP   _ XQKF#KTRNCPGHN[ EQPUV#KTRQTVFGUVKPCVKQP ] FQO[ħNP[MQFOQFGNWLæE[RT\GNQVUCOQNQVW FQFCPGIQEGNW _ ENCUU/QFGN#RWDNKE#KTRNCPG] RWDNKE XKTVWCNXQKFHN[ EQPUV#KTRQTVFGUVKPCVKQP ]#KTRNCPGHN[ FGUVKPCVKQP _  _ 172 Dziedziczenie i projektowanie zorientowane obiektowo ENCUU/QFGN$RWDNKE#KTRNCPG] RWDNKE XKTVWCNXQKFHN[ EQPUV#KTRQTVFGUVKPCVKQP ]#KTRNCPGHN[ FGUVKPCVKQP _  _ ENCUU/QFGN RWDNKE#KTRNCPG] RWDNKE XKTVWCNXQKFHN[ EQPUV#KTRQTVFGUVKPCVKQP   _ XQKF/QFGN HN[ EQPUV#KTRQTVFGUVKPCVKQP ] MQFOQFGNWLæE[RT\GNQVUCOQNQVWV[RW/QFGN FQFCGPGIQEGNW _ Powyższy schemat niemal nie różni się od wcześniejszego projektu z wyjątkiem ciała czystej funkcji wirtualnej #KTRNCPGHN[, która zastąpiła wykorzystywaną wcześniej niezależną funkcję #KTRNCPGFGHCWNV(N[. W rzeczywistości funkcja HN[ została roz- bita na dwa najważniejsze elementy. Pierwszym z nich jest deklaracja określająca jej interfejs (który musi być wykorzystywany przez klasy potomne), natomiast drugim jest definicja określająca domyślne zachowanie funkcji (która może być wykorzystana w klasach domyślnych, ale tylko na wyraźne żądanie ich projektantów). Łącząc funk- cje HN[ i FGHCWNV(N[, straciliśmy jednak możliwość nadawania im różnych ograniczeń dostępu — kod, który wcześniej był chroniony (funkcja FGHCWNV(N[ była zadeklaro- wana w bloku RTQVGEVGF) będzie teraz publiczny (ponieważ znajduje się w zadekla- rowanej w bloku RWDNKE funkcji HN[). Wróćmy do należącej do klasy 5JCRG niewirtualnej funkcji QDLGEV+ . Kiedy funkcja składowa jest niewirtualna, w zamierzeniu nie powinna zachowywać się w klasach potomnych inaczej niż w klasie bazowej. W rzeczywistości niewirtualne funkcje składowe opisują zachowanie niezależne od specjalizacji, ponieważ implementacja funkcji nie powinna ulegać żadnym zmianom, niezależnie od specjalizacji kolejnych poziomów w hierarchii klas potomnych. Oto płynący z teggo wniosek:  Celem deklarowania niewirtualnej funkcji jest otrzymganie klas potomnych dziedziczących zarówno interfejs, jak i wymaganą implementację tej funkcji. Możesz pomyśleć, że deklaracja funkcji 5JCRGQDLGEV+ oznacza: „każdy obiekt klasy 5JCRG zawiera funkcję zwracającą identyfikator obiektu, który zawsze jest wyznaczany w ten sam sposób (opisany w definicji funkcji 5JCRGQDLGEV+ ), którego żadna klasa potomna nie powinna próbować modyfikować”. Ponieważ niewirtualna funkcja opi- suje zachowanie niezależne od specjalizacji, nigdy nie powinna być ponownie dekla- rowana w żadnej podklasie (to zagadnienie szczegółogwo omówiłem w sposobie 37.). Różnice pomiędzy deklaracjami czystych funkcji wirtualnych, prostych funkcji wirtu- alnych oraz funkcji niewirtualnych umożliwiają dokładne precyzowanie właściwych dla danego mechanizmów dziedziczenia tych funkcji przez klasy potomne: dzie- dziczenia samego interfejsu, dziedziczenia interfejsu i domyślnej implementacji lub dziedziczenia interfejsu i wymaganej implementacji. Ponieważ wymienione różne Sposób 36. Odróżniaj dziedziczenie interfejsu od dziedziczenia implementacji 173 typy deklaracji oznaczają zupełnie inne mechanizmy dziedziczenia, podczas de- klarowania funkcji składowych musisz bardzo ostrożnie wybrać jedną z omawia- nych metod. Pierwszym popularnym błędem jest deklarowanie wszystkich funkcji jako niewirtual- nych. Eliminujemy w ten sposób możliwość specjalizacji klas potomnych; szczegól- nie kłopotliwe są w tym przypadku także niewirtualne destruktory (patrz sposób 14.). Jest to oczywiście dobre rozwiązanie dla klas, które w założeniu nie będą wykorzy- stywane w charakterze klas bazowych. W takim przypadku, zastosowanie zbioru wy- łącznie niewirtualnych funkcji składowych jest całkowicie poprawne. Zbyt często jednak wynika to wyłącznie z braku wiedzy na temat różnić pomiędzy funkcjami wirtualnymi a niewirtualnymi lub nieuzasadnionych obaw odnośnie wydajności funkcji wirtualnych. Należy więc pamiętać o fakcie, że niemal wszystkie klasy, które w przy- szłości mają być wykorzystane jako klasy bazowe, powinny zawierać funkcje wirtu- alne (ponownie patrz sposób 14.). Jeśli obawiasz się kosztów związanych z funkcjami wirtualnymi, pozwól, że przypo- mnę Ci o regule 80-20 (patrz także sposób 33.), która mówi, że 80 procent czasu działania programu jest poświęcona wykonywaniu 20 procent jego kodu. Wspomnia- na reguła jest istotna, ponieważ oznacza, że średnio 80 procent naszych wywołań funkcji może być wirtualnych i będzie to miało niemal niezauważalny wpływ na cał- kowitą wydajność naszego programu. Zanim więc zaczniesz się martwić, czy możesz sobie pozwolić na koszty związane z wykorzystaniem funkcji wirtualnych, upewnij się, czy Twoje rozważania dotyczą tych 20 procent programu, gdzie decyzja będzie miała istotny wpływ na wydajność całego programu. Innym powszechnym problemem jest deklarowanie wszystkich funkcji jako wirtualne. Niekiedy jest to oczywiście właściwe rozwiązanie — np. w przypadku klas protokołu (patrz sposób 34.). Może jednak świadczyć także o zwykłej niewiedzy projektanta klasy. Niektóre deklaracje funkcji nie powinny umożliwiać ponownego ich definio- wania w klasach potomnych — w takich przypadkach jedynym sposobem osiągnięcia tego celu jest deklarowanie tych funkcji jako niewirtualnych. Nie ma przecież naj- mniejszego sensu udostępnianie innym programistom klas, które mają być dziedzi- czone przez inne klasy i których wszystkie funkcje składowe będą ponownie definio- wane. Pamiętaj, że jeśli masz klasę bazową $, klasę potomną oraz funkcję składową OH, wówczas każde z poniższych wywołań funkcji OH musi być prawidłowe:  RFPGY  D RDRF RD OH Y[YQđWLGHWPMELúOH\CRQOQEæ YUMCļPKMCFQMNCU[DC\QYGL RF OH Y[YQđWLGHWPMELúOH\CRQOQEæ YUMCļPKMCFQMNCU[RQVQOPGPL Niekiedy musisz zadeklarować funkcję OH jako niewirtualną, by upewnić się, że wszystko będzie działało zgodnie z Twoimi oczekiwaniami (patrz sposób 37.). Jeśli działanie funkcji powinno być niezależne od specjalizacji, nie obawiaj się takiego rozwiązania. 174 Dziedziczenie i projektowanie zorientowane obiektowo Sposób 37. Nigdy nie definiuj ponownie dziedziczonych funkcji niewirtualnych Sposób 37. Nigdy nie definiuj ponownie dziedziczonych funkcji niewirtualnych Istnieją dwa podejścia do tego problemu: teoretyczne i pragmatyczne. Zacznijmy od po- dejścia pragmatycznego (teoretycy są w końcu przyzwygczajeni do cierpliwego czekania). Przypuśćmy, że powiem Ci, że klasa publicznie dziedziczy po klasie $ i istnieje publiczna funkcja składowa OH zdefiniowana w klasie $. Parametry i wartość zwraca- ne przez funkcję OH są dla nas na tym etapie nieistotne, załóżmy więc, że mają postać XQKF. Innymi słowy, możemy to wyrazić w następujący sposgób: ENCUU$] RWDNKE XQKFOH   _ ENCUU RWDNKE$]_ Nawet gdybyśmy nic nie wiedzieli o $, i OH, mając dany obiekt Z klasy : ZZLGUVQDKGMVGOV[RW bylibyśmy bardzo zaskoczeni, gdyby instrukcje: $ R$ZQVT\[OWLGYUMCļPKMFQZ R$ OH Y[YQđWLGHWPMELúOH\CRQOQEæYUMCļPPKMC powodowały inne działanie, niż instrukcje:  R ZQVT\[OWLGYUMCļPKMFQZ R  OH Y[YQđWLGHWPMELúOH\CRQOQEæYUMCļPPKMC Wynika to z faktu, że w obu przypadkach wywołujemy funkcję składową OH dla obiektu Z. Ponieważ w obu przypadkach jest to ta sama funkcja i ten sam obiekt, efekt wywołania powinien być identyczny, prawda? Tak, powinien, ale nie jest. W szczególności, rezultaty wywołania będą inne, jeśli OH będzie funkcją niewirtualną, a klasa będzie zawierała definicję własnej wersji tej funkcji: ENCUU RWDNKE$] RWDNKE XQKFOH WMT[YCFGHKPKELú$OHRCVT\UPRQUÎD  _ R$ OH Y[YQđWLGHWPMELú$OH R  OH Y[YQđWLGHWPMELú OH Sposób 37. Nigdy nie definiuj ponownie dziedziczonych funkcji niewirtualnych 175 Powodem takiego dwulicowego zachowania jest fakt, że niewirtualne funkcje $OH i OH są wiązane statycznie (patrz sposób 38.). Oznacza to, że ponieważ zmienna R$ została zadeklarowana jako wskaźnik do $, niewirtualne funkcje wywoływane za po- średnictwem tej zmiennej zawsze będą tymi zdefiniowanymi dla klasy $, nawet jeśli R$ wskazuje na obiekt klasy pochodnej względem $ (jak w powyższym przykładzie). Z drugiej strony, funkcje wirtualne są wiązane dynamicznie (ponownie patrz sposób 38.), co oznacza, że opisywany problem ich nie dotyczy. Gdyby OH była funkcją wirtualną, jej wywołanie (niezależnie od tego, czy z wykorzystaniem wskaźnika do R$ czy do R ) spowodowałoby wywołanie wersji OH, ponieważ R$ i R w rzeczywistości wskazują na obiekt klasy . Należy pamiętać, że jeśli tworzymy klasę i ponownie definiujemy dziedziczoną po klasie $ niewirtualną funkcję OH, obiekty klasy będą się prawdopodobnie okazywały zachowania godne schizofrenika. W szczególności dowolny obiekt klasy może — w odpowiedzi na wywołanie funkcji OH — zachowywać się albo jak obiekt klasy $, albo jak obiekt klasy ; czynnikiem rozstrzygającym nie będzie tutaj sam obiekt, ale zadeklarowany typ wskazującego na ten obiekt wskaźnika. Równie zdumiewające za- chowanie zaprezentowałyby w takim przypadku referencgje do obiektów. To już wszystkie argumenty wysuwane przez praktyków. Chcesz pewnie teraz poznać jakieś teoretyczne uzasadnienie, dlaczego nie należy ponownie definiować dziedzi- czonych funkcji niewirtualnych. Wyjaśnię to z przyjemngością. W sposobie 35. pokazałem, że publiczne dziedziczenie oznacza w rzeczywistości relację „jest”; w sposobie 36. opisałem, dlaczego deklarowanie niewirtualnych funk- cji w klasie powoduje niezależność od ewentualnych specjalizacji tej klasy. Jeśli wła- ściwie wykorzystasz wnioski wyniesione z tych sposobów podczas projektowania klas $ i oraz podczas tworzenia niewirtualnej funkcji składgowej $OH, wówczas:   Wszystkie funkcje, które można stosować dla obiektógw klasy $, można stosować także dla obiektów klasy , ponieważ każdy obiekt klasy „jest” obiektem klasy $. Podklasy klasy $ muszą dziedziczyć zarówno interfejs, jak i implementację funkcji OH, ponieważ funkcja ta została zadeklarowana w klasige $ jako niewirtualna. Jeśli w klasie ponownie zdefiniujemy teraz funkcję OH, w naszym projekcie powsta- nie sprzeczność. Jeśli klasa faktycznie potrzebuje własnej implementacji funkcji OH, która będzie się różniła od implementacji dziedziczonej po klasie $, oraz jeśli każdy obiekt klasy $ (niezależnie od poziomu specjalizacji) rzeczywiście musi wykorzysty- wać implementacji tej funkcji z klasy $, wówczas stwierdzenie, że „jest” $ jest zwy- czajnie nieprawdziwe. Klasa nie powinna w takim przypadku publicznie dziedzi- czyć po klasie $. Z drugiej strony, jeśli naprawdę musi publicznie dziedziczyć po $ oraz jeśli naprawdę musi implementować funkcję OH inaczej, niż implementuje ją klasa $, wówczas nieprawdą jest, że OH odzwierciedla niezależność od specjalizacji klasy $. W takim przypadku funkcja OH powinna zostać zadeklarowana jako wirtualna. Wreszcie, jeśli każdy obiekt klasy naprawdę musi być w relacji „jest” z obiektem klasy $ oraz jeśli funkcja OH rzeczywiście reprezentuje niezależność od specjalizacji klasy $, wówczas klasa nie powinna potrzebować własnej implementacji funkcji OH i jej projektant nie powinien więc podejmować podobgnych prób. 176 Dziedziczenie i projektowanie zorientowane obiektowo Niezależnie od tego, który argument najbardziej pasuje do naszej sytuacji, oczywiste jest, że ponowne definiowanie dziedziczonych funkcji niewirtualnych jest całkowicie pozbawione sensu. Sposób 38. Nigdy nie definiuj ponownie dziedziczonej domyślnej wartości parametru Sposób 38. Nigdy nie definiuj ponownie dziedziczonej domyślnej wartości parametru Spróbujmy uprościć nasze rozważania od samego początku. Domyślny parametr może istnieć wyłącznie jako część funkcji, a nasze klasy mogą dziedziczyć tylko dwa ro- dzaje funkcji — wirtualne i niewirtualne. Jedynym sposobem ponownego zdefinio- wania wartości domyślnej parametru jest więc ponowne zdefiniowanie całej dziedzi- czonej funkcji. Ponowne definiowanie dziedziczonej niewirtualnej funkcji jest jednak zawsze błędne (patrz sposób 37.), możemy więc od razu ograniczyć naszą analizę do sytuacji, w której dziedziczymy funkcję wirtualną z domyślną wartością parametru. W takim przypadku wyjaśnienie sensu umieszczania tego sposobu w książce jest bar- dzo proste — funkcje wirtualne są wiązane dynamicznie, ale domyślne wartości ich parametrów są wiązane statycznie. Co to oznacza? Być może nie posługujesz się biegle najnowszym żargonem związa- nym z programowaniem obiektowym lub zwyczajnie zapomniałeś, jakie są różnice pomiędzy wiązaniem statycznym a wiązaniem dynamicznym. Przypomnijmy więc sobie, o co tak naprawdę chodzi. Typem statycznym obiektu jest ten typ, który wykorzystujemy w deklaracji obiektu w kodzie programu. Przeanalizujmy poniższą hierarchię klas: ENCUU5JCRG QNQT]4 )4 0$.7 _ MNCUCTGRTG\GPVWLæECHKIWT[IGQOGVT[E\PG ENCUU5JCRG] RWDNKE YU\[UVMKGHKIWT[OWU\æWFQUVúRPKCèT[UWLæEGLGHWPMELG XKTVWCNXQKFFTCY 5JCRG QNQTEQNQT4 EQPUV  _ ENCUU4GEVCPINGRWDNKE5JCRG] RWDNKE \YTÎèWYCIúPCKPPCFQO[ħNPæYCTVQħèRCTCOGVTWļNG XKTVWCNXQKFFTCY 5JCRG QNQTEQNQT)4 0 EQPUV  _ ENCUU KTENGRWDNKE5JCRG] RWDNKE XKTVWCNXQKFFTCY 5JCRG QNQTEQNQT EQPUV  _ Sposób 38. Nigdy nie definiuj ponownie dziedziczonej domyślnej wartości parametru 177 Powyższą hierarchię można przedstawić graficznie: Rozważmy teraz poniższe wskaźniki: 5JCRG RUUVCV[E\P[V[R5JCRPG 5JCRG REPGY KTENGUVCV[E\P[V[R5JCRG 5JCRG RTPGY4GEVCPINGUVCV[E\P[V[R5JCRG W powyższym przykładzie RU, RE i RT są zadeklarowane jako zmienne typu wskaźni- kowego do obiektów klasy 5JCRG, zatem wszystkie należą do typu statycznego. Zauważ, że nie ma w tym przypadku znaczenia, na co wymienione zmienne faktycz- nie wskazują — ich statycznym typem jest 5JCRG . Typ dynamiczny obiektu zależy od typu obiektu aktualnie przez niego wskazywanego. Oznacza to, że od dynamicznego typu zależy zachowanie obiektu. W powyższym przykładzie typem dynamicznym zmiennej RE jest KTENG , zaś typem dynamicznym zmiennej RT jest 4GEVCPING . Inaczej jest w przypadku zmiennej RU, która nie ma dynamicznego typu, ponieważ w rzeczywistości nie wskazuje na żaden obiekt. Typy dynamiczne (jak sama nazwa wskazuje) mogą się zmieniać w czasie wykony- wania programu, tego rodzaju zmiany odbywają się zazwyczaj na skutek wykonania operacji przypisania: RUREV[RGOF[PCOKE\P[OPYUMCļPKMCRU LGUVVGTC\ KTENG RURTV[RGOF[PCOKE\P[OYUMCļPKMCRU LGUVVGTC\4GEVCPINGP Wirtualne funkcje są wiązane dynamicznie, co oznacza, że konkretna wywoływana funkcja zależy od dynamicznego typu obiektu wykorzygstywanego do jej wywołania: RE FTCY 4 Y[YQđWLGHWPMELú KTENGFTCY 4 RT FTCY 4 Y[YQđWLGHWPMELú4GEVCPINGPFTCY 4 Uważasz pewnie, że nie ma powodów wracać do tego tematu — z pewnością rozu- miesz już znaczenie funkcji wirtualnych. Problemy ujawniają się dopiero w momencie, gdy analizujemy funkcje wirtualne z domyślnymi wartościami parametrów, ponieważ — jak już wspomniałem — funkcje wirtualne są wiązane dynamicznie, a domyślne parametry C++ wiąże statycznie. Oznacza to, że możesz wywołać wirtualną funkcję zdefiniowaną w klasie potomnej, ale z domyślną wartością parametru z klasy bazowej: RT FTCY Y[YQđWLG4GEVCPINGFTCPY 4  178 Dziedziczenie i projektowanie zorientowane obiektowo W tym przypadku typem dynamicznym zmiennej wskaźnikowej RT jest 4GEVCPING , zatem zostanie wywołana (zgodnie z naszymi oczekiwaniami) funkcja wirtualna FTCY zdefiniowana w klasie 4GEVCPING. Domyślną wartością parametru funkcji 4GEVCP INGFTCY jest )4 0. Ponieważ jednak typem statycznym zmiennej RT jest 5JCRG , domyślna wartość parametru dla tego wywołania funkcji będzie pochodziła z definicji klasy 5JCRG, a nie 4GEVCPING! Otrzymujemy w efekcie wywołanie składające się z nie- oczekiwanej kombinacji dwóch deklaracji funkcji FTCY — z klas 5JCRG i 4GEVCPING. Możesz mi wierzyć, tworzenie oprogramowania zachowującego się w taki sposób jest ostatnią rzeczą, którą chciałbyś robić; jeśli to Cię gnie przekonuje, zaufaj mi — na pewno z takiego zachowania Twojego oprogramowania nie będąg zadowoleni Twoi klienci. Nie muszę chyba dodawać, że nie ma w tym przypadku żadnego znaczenia fakt, że RU, RE i RT są wskaźnikami. Gdyby były referencjami, problem nadal by istniał. Jedy- nym istotnym źródłem naszego problemu jest to, że FTCY jest funkcją wirtualną i jedna z jej domyślnych wartości parametrów została ponowngie zdefiniowana w podklasie. Dlaczego C++ umożliwia tworzenie oprogramowania zachowującego się w tak nienatu- ralny sposób? Odpowiedzią jest efektywność wykonywania programów. Gdyby domyślne wartości parametrów były wiązane dynamicznie, kompilatory musiałyby stosować dodatkowe mechanizmy określania właściwych domyślnych wartości parametrów funkcji wirtualnych podczas wykonywania programów, co prowadziłoby do spowolnienia i komplikacji stosowanego obecnie mechanizmu ich wyznaczania w czasie kompilacji. Decyzję podjęto więc wyłącznie
Pobierz darmowy fragment (pdf)

Gdzie kupić całą publikację:

C++. 50 efektywnych sposobów na udoskonalenie Twoich programów
Autor:

Opinie na temat publikacji:


Inne popularne pozycje z tej kategorii:


Czytaj również:


Prowadzisz stronę lub blog? Wstaw link do fragmentu tej książki i współpracuj z Cyfroteką: