SlideShare a Scribd company logo
1 of 59
Download to read offline
IDZ DO
         PRZYK£ADOWY ROZDZIA£
                                         C++. 50 efektywnych
                           SPIS TRE CI   sposobów na udoskonalenie
           KATALOG KSI¥¯EK
                                         Twoich programów
                                         Autor: Scott Meyers
                      KATALOG ONLINE     T³umaczenie: Miko³aj Szczepaniak
                                         ISBN: 83-7361-345-5
       ZAMÓW DRUKOWANY KATALOG           Tytu³ orygina³u: Effective C++: 50 Specific Ways
                                         to Improve Your Programs and Design
                                         Format: B5, stron: 248
              TWÓJ KOSZYK
                                         Pierwsze wydanie ksi¹¿ki „C++. 50 efektywnych sposobów na udoskonalenie twoich
                    DODAJ DO KOSZYKA     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ê
         CENNIK I INFORMACJE             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
                   ZAMÓW INFORMACJE
                     O NOWO CIACH        pisania lepszych programów w C++, za odpowiednie rozwa¿ania s¹ poparte
                                         konkretnymi przyk³adami. Z my l¹ o nowym wydaniu, Scott Meyers opracowa³
                       ZAMÓW CENNIK      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++.
                 CZYTELNIA               Do najwa¿niejszych zalet ksi¹¿ki „C++. 50 efektywnych sposobów na udoskonalenie
                                         twoich programów” nale¿¹:
          FRAGMENTY KSI¥¯EK ONLINE          • 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++;
Wydawnictwo Helion                       zapewnia us³ugi doradcze dla klientów na ca³ym wiecie i jest cz³onkiem rady
ul. Chopina 6                            redakcyjnej pisma C++ Report. Regularnie przemawia na technicznych konferencjach na
44-100 Gliwice                           ca³ym wiecie, jest tak¿e autorem ksi¹¿ek „More Effective C++” oraz „Effective C++ CD”.
tel. (32)230-98-63                       W 1993. roku otrzyma³ tytu³ doktora informatyki na Brown University.
e-mail: helion@helion.pl
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......................................................................28
    Sposób 2. Wybieraj <iostream> zamiast <stdio.h>.....................................................................31
    Sposób 3. Wybieraj new i delete zamiast malloc i free...............................................................33
    Sposób 4. Stosuj komentarze w stylu C++ ..................................................................................34
 Zarządzanie pamięcią .................................................................................................. 37
    Sposób 5. U ywaj tych samych form w odpowiadających sobie zastosowaniach
               operatorów new i delete ..............................................................................................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 ........................................................................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.........................................................................................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....................................................................................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 ...................................................................113
         Sposób 25. Unikaj przecią ania funkcji dla wskaźników i typów numerycznych......................117
         Sposób 26. Strze się niejednoznaczności...................................................................................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 ................................................................................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 ................................................................................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....................................................178
       Sposób 40. Modelując relacje posiadania („ma”) i implementacji z wykorzystaniem,
                  stosuj podział na warstwy .........................................................................................186
       Sposób 41. Rozró niaj dziedziczenie od stosowania szablonów ................................................189
       Sposób 42. Dziedziczenie prywatne stosuj ostro nie ..................................................................193
       Sposób 43. Dziedziczenie wielobazowe stosuj ostro nie............................................................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...........................................................226
       Sposób 49. Zapoznaj się ze standardową biblioteką C++ ...........................................................227
       Sposób 50. Pracuj bez przerwy nad swoją znajomością C++ .....................................................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 zniechę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 elementów wspólnych.
      Powinieneś wykorzystać mechanizm dziedziczenia i stworzyć klasy
      potomne względem jednej wspólnej klasy bazowej czy powinieneś
      wykorzystać szablony i wygenerować wszystkie potrzebne klasy
      ze wspólnym szkieletem kodu?
160                                                                                                          Dziedziczenie i projektowanie zorientowane obiektowo


                                                                                 Klasa A ma zostać zaimplementowana w oparciu o klasę B. Czy A powinna
                                                                                 zawierać składową reprezentującą obiekt klasy B czy te powinna prywatnie
                                                                                 dziedziczyć po klasie B?
                                                                                 Potrzebujesz projektu bezpiecznej pod względem typu i homogenicznej klasy
                                                                                 pojemnikowej, która nie jest dostępna w standardowej bibliotece C++ (listę
                                                                                 pojemników udostępnianych przez tę bibliotekę podano, prezentując sposób 49.).
                                                                                 Czy lepszym rozwiązaniem będzie skonstruowanie szablonów czy budowa
                                                                                 bezpiecznych pod względem typów interfejsów wokół tej 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 jeszcze 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ści.



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 Brytyjczykó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 wszyscy 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, ale nie odwrotnie.

        Taką interpretację publicznego dziedziczenia wymusza język C++. Przeanalizujmy
        poni szy przykład:
           ENCUU 2GTUQP ]  _
           ENCUU 5VWFGPV RWDNKE 2GTUQP ]  _

        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 jest 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):
           XQKF FCPEG
EQPUV 2GTUQP R       MC F[ OQ G VC E[è

           XQKF UVWF[
EQPUV 5VWFGPV U      V[NMQ UVWFGPEK OQIæ UVWFKQYCè
162                                    Dziedziczenie i projektowanie zorientowane obiektowo


        2GTUQP R                          R TGRTGGPVWLG QUQDú 
QDKGMV MNCU[ 2GTUQP
        5VWFGPV U                         U TGRTGGPVWLG UVWFGPVC 
QDKGMV MNCU[ 5VWFGPV

        FCPEG
R                          FQDTG R TGRTGGPVWLG QUQDú
        FCPEG
U                          FQDTG U TGRTGGPVWLG UVWFGPVC
                                           C YKúE VCM G QUQDú

        UVWF[
U                          FQDTG
        UVWF[
R                          D æF R PKG TGRTGGPVWLG UVWFGPVC

      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 chronionego 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 podobny do poni szego:
        ENCUU $KTF ]
        RWDNKE
          XKTVWCN XQKF HN[
                RVCMK OQIæ NCVCè
          
        _

        ENCUU 2GPIWKP RWDNKE $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 naszą 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ść, hierarchię klas:
        ENCUU $KTF ]
                                        DTCM FGMNCTCELK HWPMELK HN[
        _

        ENCUU (N[KPI$KTF RWDNKE $KTF ]
        RWDNKE
          XKTVWCN XQKF HN[

          
        _

        ENCUU 0QP(N[KPI$KTF RWDNKE $KTF ]
                                       DTCM FGMNCTCELK HWPMELK HN[
        _
Sposób 35. Dopilnuj, by publiczne dziedziczenie modelowało relację „jest”                 163


           ENCUU 2GPIWKP RWDNKE 0QP(N[KPI$KTF ]
                                          DTCM FGMNCTCELK HWPMELK HN[
           _

        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ędzy 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:
           XQKF GTTQT
EQPUV UVTKPI OUI       HWPMELC FGHKPKQYCPC Y KPP[O OKGLUEW

           ENCUU 2GPIWKP RWDNKE $KTF ]
           RWDNKE
             XKTVWCN XQKF HN[
 ] GTTQT
 2KPIYKP[ PKG OQIæ NCVCè  _
             
           _

        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 programu.

        Aby wyrazić ograniczenie „pingwiny nie mogą latać”, wystarczy nie definiować
        odpowiedniej funkcji dla obiektów klasy 2GPIWKP:
           ENCUU $KTF ]
                                          DTCM FGMNCTCELK HWPMELK HN[
           _
164                                 Dziedziczenie i projektowanie zorientowane obiektowo


        ENCUU 0QP(N[KPI$KTF RWDNKE $KTF ]
                                       DTCM FGMNCTCELK HWPMELK HN[
        _

        ENCUU 2GPIWKP RWDNKE 0QP(N[KPI$KTF ]
                                       DTCM FGMNCTCELK HWPMELK HN[
        _

      Jeśli spróbujesz teraz wywołać funkcję HN[ dla obiektu reprezentującego pingwina,
      kompilator zasygnalizuje błąd:
        2GPIWKP R
        RHN[
                         D æF

      Zaprezentowane rozwiązanie jest całkowicie odmienne od podejścia stosowanego
      w języku Smalltalk. Stosowana tam strategia powoduje, 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ócili do nauki na tym poziomie.

      Przeanalizuj więc poni szy kod:
        ENCUU 4GEVCPING ]
        RWDNKE
          XKTVWCN XQKF UGV*GKIJV
KPV PGY*GKIJV
          XKTVWCN XQKF UGV9KFVJ
KPV PGY9KFVJ

          XKTVWCN KPV JGKIJV
 EQPUV          HWPMELG YTCECLæ DKG æEG
          XKTVWCN KPV YKFVJ
 EQPUV           YCTVQ EK
          
        _
Sposób 35. Dopilnuj, by publiczne dziedziczenie modelowało relację „jest”                 165


           XQKF OCMG$KIIGT
4GEVCPING T            HWPMELC YKúMUCLæEC RQNG
           ]                                       RQYKGTEJPK RTQUVQMæVC T
             KPV QNF*GKIJV  TJGKIJV


               TUGV9KFVJ
TYKFVJ
 
          FQFCLG  FQ UGTQMQ EK T

               CUUGTV
TJGKIJV
  QNF*GKIJV    WRGYPKC UKú G Y[UQMQ è
           _                                       RTQUVQMæVC T PKG OKGPK C UKú

        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 prostokątów:
           ENCUU 5SWCTG RWDNKE 4GEVCPING ]  _
           5SWCTG U
           
           CUUGTV
UYKFVJ
  UJGKIJV
        YCTWPGM OWUK D[è RTCYFKY[
                                                   FNC YU[UVMKEJ MYCFTCVÎY

           OCMG$KIIGT
U                            PC UMWVGM FKGFKEGPKC U
                                                     LGUV Y TGNCELK LGUV  MNCUæ
                                                     4GEVCPING OQ GO[ YKúE YKúMU[è
                                                     RQNG LGIQ RQYKGTEJPK

           CUUGTV
U9KFVJ
  UJGKIJV
        YCTWPGM PCFCN OWUK D[è RTCYFKY[
                                                   FNC YU[UVMKEJ MYCFTCVÎY

        Tak e teraz oczywiste jest, e ostatni warunek nigdy nie powinien być fałszywy.
        Zgodnie z definicją, szerokość kwadratu jest przecie taka sama jak jego wysokość.

        Tym razem mamy jednak problem. Jak mo na pogodzić poni 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 , 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 oczekiwaniami.

                                                                               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ą wystę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 wstę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 czegokolwiek.
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:
           ENCUU 5JCRG ]
           RWDNKE
             XKTVWCN XQKF FTCY
 EQPUV  
             XKTVWCN XQKF GTTQT
EQPUV UVTKPI OUI
             KPV QDLGEV+
 EQPUV
             
           _

           ENCUU 4GEVCPING RWDNKE 5JCRG ]  _
           ENCUU 'NNKRUG RWDNKE 5JCRG ]  _

        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ą, poniewa :
             Interfejsy funkcji składowych zawsze są dziedziczone. W sposobie 35.
             wyjaśniłem, e dziedziczenie publiczne oznacza faktycznie relację „jest”,
             zatem wszystkie elementy istniejące w klasie bazowej muszą tak e istnieć
             w klasach potomnych. Jeśli więc daną funkcję mo na wykonać dla danej
             klasy, musi tak e istnieć sposób jej wykonania dla jej 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, e:
             Celem deklarowania czystych funkcji wirtualnych jest otrzymanie 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ą zaimplementować”.
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
RU  PGY 5JCRG            D æF 5JCRG LGUV MNCUæ CDUVTCME[LPæ

        5JCRG
RU  PGY 4GEVCPING       FQDTG
        RU FTCY
                      Y[YQ WLG 4GEVCPINGFTCY

        5JCRG
RU  PGY 'NNKRUG         FQDTG
        RU FTCY
                      Y[YQ WLG 'NNKRUGFTCY

        RU 5JCRGFTCY
               Y[YQ WLG 5JCRGFTCY
        RU 5JCRGFTCY
               Y[YQ WLG 5JCRGFTCY

      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 bezpieczniejszej 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 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 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 otrzymanie 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ę 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 ]  _        TGRTGGPVWLG NQVPKUMC

           ENCUU #KTRNCPG ]
           RWDNKE
             XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP
             
           _

           XQKF #KTRNCPGHN[
EQPUV #KTRQTV FGUVKPCVKQP
           ]
             FQO[ NP[ MQF OQFGNWLæE[ RTGNQV UCOQNQVW
             FQ FCPGIQ EGNW
           _

           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 systemu.

        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 ]
                                      DTCM FGMNCTCELK HWPMELK HN[
           _

        Ich kod zawiera więc coś podobnego do poni szego fragmentu:
           #KTRQTV 1MGEKG
          1MúEKG VQ NQVPKUMQ Y 9CTUCYKG

           #KTRNCPG
RC  PGY /QFGN%
           
           RC HN[
1MGEKG              Y[YQ WLG HWPMELú #KTRNCPGHN[
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 linii 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
          XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP  
          

        RTQVGEVGF
          XQKF FGHCWNV(N[
EQPUV #KTRQTV FGUVKPCVKQP
        _

        XQKF #KTRNCPGFGHCWNV(N[
EQPUV #KTRQTV FGUVKPCVKQP
        ]
          FQO[ NP[ MQF OQFGNWLæE[ RTGNQV UCOQNQVW
          FQ FCPGIQ EGNW
        _

      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
          XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP
          ] FGHCWNV(N[
FGUVKPCVKQP _
          
        _

        ENCUU /QFGN$ RWDNKE #KTRNCPG ]
        RWDNKE
          XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP
          ] 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łasnej wersji funkcji HN[:
Sposób 36. Odróżniaj dziedziczenie interfejsu od dziedziczenia implementacji            171


           ENCUU /QFGN% RWDNKE #KTRNCPG ]
           RWDNKE
             XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP
             
           _

           XQKF /QFGN%HN[
EQPUV #KTRQTV FGUVKPCVKQP
           ]
             MQF OQFGNWLæE[ RTGNQV UCOQNQVW V[RW /QFGN% FQ FCPGIQ EGNW
           _

        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 funkcji wirtualnej:
           ENCUU #KTRNCPG ]
           RWDNKE
             XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP  
             
           _

           XQKF #KTRNCPGHN[
EQPUV #KTRQTV FGUVKPCVKQP
           ]
             FQO[ NP[ MQF OQFGNWLæE[ RTGNQV UCOQNQVW
             FQ FCPGIQ EGNW
           _

           ENCUU /QFGN# RWDNKE #KTRNCPG ]
           RWDNKE
             XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP
             ] #KTRNCPGHN[
FGUVKPCVKQP _
             
           _
172                                 Dziedziczenie i projektowanie zorientowane obiektowo


        ENCUU /QFGN$ RWDNKE #KTRNCPG ]
        RWDNKE
          XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP
          ] #KTRNCPGHN[
FGUVKPCVKQP _
          
        _

        ENCUU /QFGN% RWDNKE #KTRNCPG ]
        RWDNKE
          XKTVWCN XQKF HN[
EQPUV #KTRQTV FGUVKPCVKQP
          
        _

        XQKF /QFGN%HN[
EQPUV #KTRQTV FGUVKPCVKQP
        ]
          MQF OQFGNWLæE[ RTGNQV UCOQNQVW V[RW /QFGN% FQ FCPGIQ EGNW
        _

      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 tego wniosek:
          Celem deklarowania niewirtualnej funkcji jest otrzymanie 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ółowo 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:
RF  PGY 
           D
RD  RF

           RD OH
              Y[YQ WLG HWPMELú OH C RQOQEæ
                                  YUMC PKMC FQ MNCU[ DCQYGL

           RF OH
              Y[YQ WLG HWPMELú OH C RQOQEæ
                                  YUMC PKMC FQ MNCU[ RQVQOPGL

        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 przyzwyczajeni 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 sposób:
                                                                                 ENCUU $ ]
                                                                                 RWDNKE
                                                                                   XQKF OH

                                                                                   
                                                                                 _

                                                                                 ENCUU  RWDNKE $ ]  _

                                                                               Nawet gdybyśmy nic nie wiedzieli o $,  i OH, mając dany obiekt Z klasy :
                                                                                  Z                    Z LGUV QDKGMVGO V[RW 

                                                                               bylibyśmy bardzo zaskoczeni, gdyby instrukcje:
                                                                                 $
R$  Z              QVT[OWLG YUMC PKM FQ Z
                                                                                 R$ OH
               Y[YQ WLG HWPMELú OH C RQOQEæ YUMC PKMC

                                                                               powodowały inne działanie, ni instrukcje:
R  Z              QVT[OWLG YUMC PKM FQ Z
                                                                                 R OH
               Y[YQ WLG HWPMELú OH C RQOQEæ YUMC PKMC

                                                                               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
                                                                                   XQKF OH
            WMT[YC FGHKPKELú $OH RCVT URQUÎD 
                                                                                   
                                                                                 _

                                                                                 R$ OH
               Y[YQ WLG HWPMELú $OH
                                                                                 R OH
               Y[YQ WLG HWPMELú 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 referencje 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 przyjemnoś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ładowej $OH, wówczas:
             Wszystkie funkcje, które mo na stosować dla obiektów 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 klasie $ 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ć podobnych 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:
                                                                                    ENCUU 5JCRG%QNQT ] 4' )4''0 $.7' _

                                                                                     MNCUC TGRTGGPVWLæEC HKIWT[ IGQOGVT[EPG
                                                                                    ENCUU 5JCRG ]
                                                                                    RWDNKE
                                                                                       YU[UVMKG HKIWT[ OWUæ WFQUVúRPKCè T[UWLæEG LG HWPMELG
                                                                                      XKTVWCN XQKF FTCY
5JCRG%QNQT EQNQT  4' EQPUV  
                                                                                      
                                                                                    _

                                                                                    ENCUU 4GEVCPING RWDNKE 5JCRG ]
                                                                                    RWDNKE
                                                                                       YTÎè WYCIú PC KPPC FQO[ NPæ YCTVQ è RCTCOGVTW  NG
                                                                                      XKTVWCN XQKF FTCY
5JCRG%QNQT EQNQT  )4''0 EQPUV
                                                                                      
                                                                                    _

                                                                                    ENCUU %KTENG RWDNKE 5JCRG ]
                                                                                    RWDNKE
                                                                                      XKTVWCN XQKF FTCY
5JCRG%QNQT EQNQT 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
RU                     UVCV[EP[ V[R  5JCRG
5JCRG
RE  PGY %KTENG        UVCV[EP[ V[R  5JCRG
5JCRG
RT  PGY 4GEVCPING     UVCV[EP[ V[R  5JCRG
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:
          RU  RE                       V[RGO F[PCOKEP[O YUMC PKMC RU
                                         LGUV VGTC %KTENG
RU  RT                       V[RGO F[PCOKEP[O YUMC PKMC RU
                                         LGUV VGTC 4GEVCPING
Wirtualne funkcje są wiązane dynamicznie, co oznacza, e konkretna wywoływana
        funkcja zale y od dynamicznego typu obiektu wykorzystywanego do jej wywołania:
          RE FTCY
4'                 Y[YQ WLG HWPMELú %KTENGFTCY
4'
          RT FTCY
4'                 Y[YQ WLG HWPMELú 4GEVCPINGFTCY
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 WLG 4GEVCPINGFTCY
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ę nie przekonuje, zaufaj mi — na pewno
                                                              z takiego zachowania Twojego oprogramowania nie będą 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 ponownie 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 z myślą o szybkości i prostocie implementacji. Efektem
                                                              jest wydajny mechanizm wykonywania programów, ale tak e — jeśli nie będziesz
                                                              stosował zaleceń zawartych w tym sposobie — potencjalne nieporozumienia.



Sposób 39.
Unikaj rzutowania
w dół hierarchii dziedziczenia
Sposób 39. Unikaj rzutowania w dół hierarchii dziedziczenia




                                                              W dzisiejszych niespokojnych czasach warto mieć na oku poczynania instytucji
                                                              finansowych, rozwa my więc klasę protokołu (patrz sposób 34.) dla kont bankowych:
                                                                ENCUU 2GTUQP ]  _

                                                                ENCUU $CPM#EEQWPV ]
                                                                RWDNKE
                                                                  $CPM#EEQWPV
EQPUV 2GTUQP
RTKOCT[1YPGT
                                                                              EQPUV 2GTUQP
LQKPV1YPGT
                                                                  XKTVWCN `$CPM#EEQWPV


                                                                  XKTVWCN XQKF OCMGGRQUKV
FQWDNG COQWPV  
                                                                  XKTVWCN XQKF OCMG9KVJFTCYCN
FQWDNG COQWPV  

                                                                  XKTVWCN FQWDNG DCNCPEG
 EQPUV  
                                                                  
                                                                _
Sposób 39. Unikaj rzutowania w dół hierarchii dziedziczenia                               179


        Wiele banków przedstawia dzisiaj swoim klientom niezwykle szeroką ofertę typów
        kont bankowych, załó my jednak (dla uproszczenia), e istnieje tylko jeden typ konta
        bankowego, zwykłe konto oszczędnościowe:
           ENCUU 5CXKPIU#EEQWPV RWDNKE $CPM#EEQWPV ]
           RWDNKE
             5CXKPIU#EEQWPV
EQPUV 2GTUQP
RTKOCT[1YPGT
                            EQPUV 2GTUQP
LQKPV1YPGT
             `5CXKPIU#EEQWPV


             XQKF ETGFKV+PVGTGUV
          FQFCL QFUGVMK FQ MQPVC
             
           _

        Nie jest to mo e zbyt zaawansowane konto oszczędnościowe, jest jednak w zupełno-
        ści wystarczające dla naszych rozwa ań.

        Bank prawdopodobnie przechowuje listę wszystkich swoich kont, która mo e być zaim-
        plementowana za pomocą szablonu klasy NKUV ze standardowej biblioteki C++ (patrz
        sposób 49.). Przypuśćmy, e w naszym banku taka lista nosi nazwę CNN#EEQWPVU:
           NKUV$CPM#EEQWPV
CNN#EEQWPVU         YU[UVMKG MQPVC QDU WIKYCPG
                                                  RTG FCP[ DCPM

        Jak wszystkie standardowe pojemniki, listy przechowują jedynie kopie umieszczanych
        w nich obiektów, zatem, aby uniknąć przechowywania wielu kopii poszczególnych
        obiektów klasy $CPM#EEQWPV, programiści zdecydowali, e lista CNN#EEQWPVU powinna
        składać się jedynie ze wskaźników do tych obiektów, a nie samych obiektów repre-
        zentujących konta.

        Przypuśćmy, e naszym zadaniem jest kolejne przejście przez wszystkie konta i doliczenie
        do nich nale nych odsetek. Mo emy zrealizować tę usługę w następujący sposób:
            RúVNC PKG QUVCPKG UMQORKNQYCPC 
LG NK PKIF[ YEG PKGL PKG OKC G
            FQ E[PKGPKC  MQFGO Y[MQT[UVWLæE[O KVGTCVQT[ RCVT PK GL
           HQT 
NKUV$CPM#EEQWPV
KVGTCVQT R  CNN#EEQWPVUDGIKP

                R  CNN#EEQWPVUGPF

                

R ]
R ETGFKV+PVGTGUV
       D æF
           _

        Nasze kompilatory szybko zasygnalizują, e lista CNN#EEQWPVU zawiera wskaźniki do
        obiektów klasy $CPM#EEQWPV, a nie obiektów klasy 5CXKPIU#EEQWPV, zatem w ka dej
        kolejnej iteracji zmienna R będzie wskazywała na obiekt klasy $CPM#EEQWPV. Oznacza
        to, e wywołanie funkcji ETGFKV+PVGTGUV jest nieprawidłowe, poniewa została ona
        zadeklarowana wyłącznie dla obiektów klasy 5CXKPIU#EEQWPV, a nie $CPM#EEQWPV.

        Jeśli wiersz NKUV$CPM#EEQWPV
KVGTCVQT R  CNN#EEQWPVUDGIKP
 jest dla Ciebie
        niezrozumiały i nie przypomina Ci kodu C++, z którym miałeś do czynienia do tej pory,
        oznacza to, e prawdopodobnie nie miałeś wcześniej przyjemności korzystać z szablo-
        nów klas pojemnikowych ze standardowej biblioteki C++. Tę część biblioteki nazywa
        się często Standardową Biblioteką Szablonów (ang. Standard Template Library — STL),
        więcej informacji na jej temat znajdziesz w sposobie 49. Na tym etapie wystarczy Ci
180                                  Dziedziczenie i projektowanie zorientowane obiektowo


      wiedza, e zmienna R zachowuje się jak wskaźnik wskazujący na przeglądane w pętli
      kolejne elementy listy CNN#EEQWPVU (od jej początku do końca). Oznacza to, e zmien-
      na R jest traktowana tak, jakby jej typem był $CPM#EEQWPV
, a elementy przeglądanej
      listy były przechowywane w tablicy.

      Powy szy kod nie zostanie niestety skompilowany. Wiemy oczywiście, e lista
      CNN#EEQWPVU została zdefiniowana jako pojemnik na wskaźniki typu $CPM#EEQWPV
,
      ale mamy tak e świadomość, e w powy szej pętli faktycznie przechowuje wskaźniki
      typu 5CXKPIU#EEQWPV
, poniewa 5CXKPIU#EEQWPV jest jedyną klasą, dla której mo emy
      tworzyć obiekty. Głupie kompilatory! Zdecydowaliśmy się przekazać im wiedzę, któ-
      ra jest dla nas zupełnie oczywista i okazało się, e są zbyt tępe, by zauwa yć, e lista
      CNN#EEQWPVU przechowuje w rzeczywistości wskaźniki typu 5CXKPIU#EEQWPV

More Related Content

What's hot

Wyjątkowy język C++. 40 nowych łamigłówek, zadań programistycznych i rozwiązań
Wyjątkowy język C++. 40 nowych łamigłówek, zadań programistycznych i rozwiązańWyjątkowy język C++. 40 nowych łamigłówek, zadań programistycznych i rozwiązań
Wyjątkowy język C++. 40 nowych łamigłówek, zadań programistycznych i rozwiązańWydawnictwo Helion
 
C++Builder Borland Developer Studio 2006. Kompendium programisty
C++Builder Borland Developer Studio 2006. Kompendium programistyC++Builder Borland Developer Studio 2006. Kompendium programisty
C++Builder Borland Developer Studio 2006. Kompendium programistyWydawnictwo Helion
 
C++. Inżynieria programowania
C++. Inżynieria programowaniaC++. Inżynieria programowania
C++. Inżynieria programowaniaWydawnictwo Helion
 
Język C++. Gotowe rozwiązania dla programistów
Język C++. Gotowe rozwiązania dla programistówJęzyk C++. Gotowe rozwiązania dla programistów
Język C++. Gotowe rozwiązania dla programistówWydawnictwo Helion
 
Visual C# 2005 Express Edition. Od podstaw
Visual C# 2005 Express Edition. Od podstawVisual C# 2005 Express Edition. Od podstaw
Visual C# 2005 Express Edition. Od podstawWydawnictwo Helion
 
Wstęp do programowania w języku C#
Wstęp do programowania w języku C#Wstęp do programowania w języku C#
Wstęp do programowania w języku C#Wydawnictwo Helion
 
Aplikacje w Visual C++ 2005. Przykłady
Aplikacje w Visual C++ 2005. PrzykładyAplikacje w Visual C++ 2005. Przykłady
Aplikacje w Visual C++ 2005. PrzykładyWydawnictwo Helion
 
C++Builder. Kompendium programisty
C++Builder. Kompendium programistyC++Builder. Kompendium programisty
C++Builder. Kompendium programistyWydawnictwo Helion
 
MS Project 2003. Zarządzanie projektami. Edycja limitowana
MS Project 2003. Zarządzanie projektami. Edycja limitowanaMS Project 2003. Zarządzanie projektami. Edycja limitowana
MS Project 2003. Zarządzanie projektami. Edycja limitowanaWydawnictwo Helion
 
Więcej niż architektura oprogramowania
Więcej niż architektura oprogramowaniaWięcej niż architektura oprogramowania
Więcej niż architektura oprogramowaniaWydawnictwo Helion
 

What's hot (20)

Wyjątkowy język C++. 40 nowych łamigłówek, zadań programistycznych i rozwiązań
Wyjątkowy język C++. 40 nowych łamigłówek, zadań programistycznych i rozwiązańWyjątkowy język C++. 40 nowych łamigłówek, zadań programistycznych i rozwiązań
Wyjątkowy język C++. 40 nowych łamigłówek, zadań programistycznych i rozwiązań
 
C++Builder Borland Developer Studio 2006. Kompendium programisty
C++Builder Borland Developer Studio 2006. Kompendium programistyC++Builder Borland Developer Studio 2006. Kompendium programisty
C++Builder Borland Developer Studio 2006. Kompendium programisty
 
C++. Inżynieria programowania
C++. Inżynieria programowaniaC++. Inżynieria programowania
C++. Inżynieria programowania
 
C++ bez obaw
C++ bez obawC++ bez obaw
C++ bez obaw
 
Język C. Programowanie
Język C. ProgramowanieJęzyk C. Programowanie
Język C. Programowanie
 
Język C++. Gotowe rozwiązania dla programistów
Język C++. Gotowe rozwiązania dla programistówJęzyk C++. Gotowe rozwiązania dla programistów
Język C++. Gotowe rozwiązania dla programistów
 
C#. Wzorce projektowe
C#. Wzorce projektoweC#. Wzorce projektowe
C#. Wzorce projektowe
 
Visual C# 2005 Express Edition. Od podstaw
Visual C# 2005 Express Edition. Od podstawVisual C# 2005 Express Edition. Od podstaw
Visual C# 2005 Express Edition. Od podstaw
 
C++. Sztuka programowania
C++. Sztuka programowaniaC++. Sztuka programowania
C++. Sztuka programowania
 
Wstęp do programowania w języku C#
Wstęp do programowania w języku C#Wstęp do programowania w języku C#
Wstęp do programowania w języku C#
 
C++Builder 6. Ćwiczenia
C++Builder 6. ĆwiczeniaC++Builder 6. Ćwiczenia
C++Builder 6. Ćwiczenia
 
Visual C# .NET. Encyklopedia
Visual C# .NET. EncyklopediaVisual C# .NET. Encyklopedia
Visual C# .NET. Encyklopedia
 
Aplikacje w Visual C++ 2005. Przykłady
Aplikacje w Visual C++ 2005. PrzykładyAplikacje w Visual C++ 2005. Przykłady
Aplikacje w Visual C++ 2005. Przykłady
 
C++Builder. Kompendium programisty
C++Builder. Kompendium programistyC++Builder. Kompendium programisty
C++Builder. Kompendium programisty
 
C++. Leksykon kieszonkowy
C++. Leksykon kieszonkowyC++. Leksykon kieszonkowy
C++. Leksykon kieszonkowy
 
MS Project 2003. Zarządzanie projektami. Edycja limitowana
MS Project 2003. Zarządzanie projektami. Edycja limitowanaMS Project 2003. Zarządzanie projektami. Edycja limitowana
MS Project 2003. Zarządzanie projektami. Edycja limitowana
 
Więcej niż architektura oprogramowania
Więcej niż architektura oprogramowaniaWięcej niż architektura oprogramowania
Więcej niż architektura oprogramowania
 
Programowanie. Od podstaw
Programowanie. Od podstawProgramowanie. Od podstaw
Programowanie. Od podstaw
 
C++BuilderX. Ćwiczenia
C++BuilderX. ĆwiczeniaC++BuilderX. Ćwiczenia
C++BuilderX. Ćwiczenia
 
C# i ASP.NET. Szybki start
C# i ASP.NET. Szybki startC# i ASP.NET. Szybki start
C# i ASP.NET. Szybki start
 

Viewers also liked

Adobe After Effects 6.0. Oficjalny podręcznik
Adobe After Effects 6.0. Oficjalny podręcznikAdobe After Effects 6.0. Oficjalny podręcznik
Adobe After Effects 6.0. Oficjalny podręcznikWydawnictwo Helion
 
STL w praktyce. 50 sposobów efektywnego wykorzystania
STL w praktyce. 50 sposobów efektywnego wykorzystaniaSTL w praktyce. 50 sposobów efektywnego wykorzystania
STL w praktyce. 50 sposobów efektywnego wykorzystaniaWydawnictwo Helion
 
PC hardware. Almanach. Wydanie III
PC hardware. Almanach. Wydanie IIIPC hardware. Almanach. Wydanie III
PC hardware. Almanach. Wydanie IIIWydawnictwo Helion
 
Bezpieczeństwo w Linuksie. Podręcznik administratora
Bezpieczeństwo w Linuksie. Podręcznik administratoraBezpieczeństwo w Linuksie. Podręcznik administratora
Bezpieczeństwo w Linuksie. Podręcznik administratoraWydawnictwo Helion
 
PowerPoint 2003 PL. Ćwiczenia
PowerPoint 2003 PL. ĆwiczeniaPowerPoint 2003 PL. Ćwiczenia
PowerPoint 2003 PL. ĆwiczeniaWydawnictwo Helion
 
The Shellcoders Handbook. Edycja polska
The Shellcoders Handbook. Edycja polskaThe Shellcoders Handbook. Edycja polska
The Shellcoders Handbook. Edycja polskaWydawnictwo Helion
 
Flash MX 2004. Skuteczne rozwiązania
Flash MX 2004. Skuteczne rozwiązaniaFlash MX 2004. Skuteczne rozwiązania
Flash MX 2004. Skuteczne rozwiązaniaWydawnictwo Helion
 
INTERsoft IntelliCAD 4.0 PL. Pierwsze kroki
INTERsoft IntelliCAD 4.0 PL. Pierwsze krokiINTERsoft IntelliCAD 4.0 PL. Pierwsze kroki
INTERsoft IntelliCAD 4.0 PL. Pierwsze krokiWydawnictwo Helion
 

Viewers also liked (15)

Mandrake Linux
Mandrake LinuxMandrake Linux
Mandrake Linux
 
Adobe After Effects 6.0. Oficjalny podręcznik
Adobe After Effects 6.0. Oficjalny podręcznikAdobe After Effects 6.0. Oficjalny podręcznik
Adobe After Effects 6.0. Oficjalny podręcznik
 
STL w praktyce. 50 sposobów efektywnego wykorzystania
STL w praktyce. 50 sposobów efektywnego wykorzystaniaSTL w praktyce. 50 sposobów efektywnego wykorzystania
STL w praktyce. 50 sposobów efektywnego wykorzystania
 
PC hardware. Almanach. Wydanie III
PC hardware. Almanach. Wydanie IIIPC hardware. Almanach. Wydanie III
PC hardware. Almanach. Wydanie III
 
Bezpieczeństwo w Linuksie. Podręcznik administratora
Bezpieczeństwo w Linuksie. Podręcznik administratoraBezpieczeństwo w Linuksie. Podręcznik administratora
Bezpieczeństwo w Linuksie. Podręcznik administratora
 
Optymalizacja systemu Windows
Optymalizacja systemu WindowsOptymalizacja systemu Windows
Optymalizacja systemu Windows
 
Contribute 2. Szybki start
Contribute 2. Szybki startContribute 2. Szybki start
Contribute 2. Szybki start
 
PowerPoint 2003 PL. Ćwiczenia
PowerPoint 2003 PL. ĆwiczeniaPowerPoint 2003 PL. Ćwiczenia
PowerPoint 2003 PL. Ćwiczenia
 
Red Hat Linux 9. Biblia
Red Hat Linux 9. BibliaRed Hat Linux 9. Biblia
Red Hat Linux 9. Biblia
 
Linux. Leksykon kieszonkowy
Linux. Leksykon kieszonkowyLinux. Leksykon kieszonkowy
Linux. Leksykon kieszonkowy
 
The Shellcoders Handbook. Edycja polska
The Shellcoders Handbook. Edycja polskaThe Shellcoders Handbook. Edycja polska
The Shellcoders Handbook. Edycja polska
 
Flash MX 2004. Skuteczne rozwiązania
Flash MX 2004. Skuteczne rozwiązaniaFlash MX 2004. Skuteczne rozwiązania
Flash MX 2004. Skuteczne rozwiązania
 
Cisco. Receptury
Cisco. RecepturyCisco. Receptury
Cisco. Receptury
 
INTERsoft IntelliCAD 4.0 PL. Pierwsze kroki
INTERsoft IntelliCAD 4.0 PL. Pierwsze krokiINTERsoft IntelliCAD 4.0 PL. Pierwsze kroki
INTERsoft IntelliCAD 4.0 PL. Pierwsze kroki
 
Złóż własny komputer
Złóż własny komputerZłóż własny komputer
Złóż własny komputer
 

Similar to C++. 50 efektywnych sposobów na udoskonalenie Twoich programów

Język C++. Standardy kodowania. 101 zasad, wytycznych i zalecanych praktyk
Język C++. Standardy kodowania. 101 zasad, wytycznych i zalecanych praktykJęzyk C++. Standardy kodowania. 101 zasad, wytycznych i zalecanych praktyk
Język C++. Standardy kodowania. 101 zasad, wytycznych i zalecanych praktykWydawnictwo Helion
 
C++. Potęga języka. Od przykładu do przykładu
C++. Potęga języka. Od przykładu do przykładuC++. Potęga języka. Od przykładu do przykładu
C++. Potęga języka. Od przykładu do przykładuWydawnictwo Helion
 
Visual C# 2005. Zapiski programisty
Visual C# 2005. Zapiski programistyVisual C# 2005. Zapiski programisty
Visual C# 2005. Zapiski programistyWydawnictwo Helion
 
Visual Basic .NET. Wzorce projektowe
Visual Basic .NET. Wzorce projektoweVisual Basic .NET. Wzorce projektowe
Visual Basic .NET. Wzorce projektoweWydawnictwo Helion
 
C++Builder i Turbo C++. Podstawy
C++Builder i Turbo C++. PodstawyC++Builder i Turbo C++. Podstawy
C++Builder i Turbo C++. PodstawyWydawnictwo Helion
 
Programowanie w języku C. FAQ
Programowanie w języku C. FAQProgramowanie w języku C. FAQ
Programowanie w języku C. FAQWydawnictwo Helion
 
Język C. Wskaźniki. Vademecum profesjonalisty
Język C. Wskaźniki. Vademecum profesjonalistyJęzyk C. Wskaźniki. Vademecum profesjonalisty
Język C. Wskaźniki. Vademecum profesjonalistyWydawnictwo Helion
 

Similar to C++. 50 efektywnych sposobów na udoskonalenie Twoich programów (14)

Język C++. Standardy kodowania. 101 zasad, wytycznych i zalecanych praktyk
Język C++. Standardy kodowania. 101 zasad, wytycznych i zalecanych praktykJęzyk C++. Standardy kodowania. 101 zasad, wytycznych i zalecanych praktyk
Język C++. Standardy kodowania. 101 zasad, wytycznych i zalecanych praktyk
 
C++Builder 6 i bazy danych
C++Builder 6 i bazy danychC++Builder 6 i bazy danych
C++Builder 6 i bazy danych
 
C++. Potęga języka. Od przykładu do przykładu
C++. Potęga języka. Od przykładu do przykładuC++. Potęga języka. Od przykładu do przykładu
C++. Potęga języka. Od przykładu do przykładu
 
C#. Ćwiczenia
C#. ĆwiczeniaC#. Ćwiczenia
C#. Ćwiczenia
 
Visual C# 2005. Zapiski programisty
Visual C# 2005. Zapiski programistyVisual C# 2005. Zapiski programisty
Visual C# 2005. Zapiski programisty
 
C#. Programowanie
C#. ProgramowanieC#. Programowanie
C#. Programowanie
 
Visual Basic .NET. Wzorce projektowe
Visual Basic .NET. Wzorce projektoweVisual Basic .NET. Wzorce projektowe
Visual Basic .NET. Wzorce projektowe
 
C++Builder i Turbo C++. Podstawy
C++Builder i Turbo C++. PodstawyC++Builder i Turbo C++. Podstawy
C++Builder i Turbo C++. Podstawy
 
Programowanie w języku C. FAQ
Programowanie w języku C. FAQProgramowanie w języku C. FAQ
Programowanie w języku C. FAQ
 
C++. Styl programowania
C++. Styl programowaniaC++. Styl programowania
C++. Styl programowania
 
ABC Delphi 2006
ABC Delphi 2006ABC Delphi 2006
ABC Delphi 2006
 
Uczta programistów
Uczta programistówUczta programistów
Uczta programistów
 
Język C. Wskaźniki. Vademecum profesjonalisty
Język C. Wskaźniki. Vademecum profesjonalistyJęzyk C. Wskaźniki. Vademecum profesjonalisty
Język C. Wskaźniki. Vademecum profesjonalisty
 
SolidWorks 2006 w praktyce
SolidWorks 2006 w praktyceSolidWorks 2006 w praktyce
SolidWorks 2006 w praktyce
 

More from Wydawnictwo Helion

Tworzenie filmów w Windows XP. Projekty
Tworzenie filmów w Windows XP. ProjektyTworzenie filmów w Windows XP. Projekty
Tworzenie filmów w Windows XP. ProjektyWydawnictwo Helion
 
Blog, więcej niż internetowy pamiętnik
Blog, więcej niż internetowy pamiętnikBlog, więcej niż internetowy pamiętnik
Blog, więcej niż internetowy pamiętnikWydawnictwo Helion
 
Pozycjonowanie i optymalizacja stron WWW. Ćwiczenia praktyczne
Pozycjonowanie i optymalizacja stron WWW. Ćwiczenia praktycznePozycjonowanie i optymalizacja stron WWW. Ćwiczenia praktyczne
Pozycjonowanie i optymalizacja stron WWW. Ćwiczenia praktyczneWydawnictwo Helion
 
E-wizerunek. Internet jako narzędzie kreowania image&#39;u w biznesie
E-wizerunek. Internet jako narzędzie kreowania image&#39;u w biznesieE-wizerunek. Internet jako narzędzie kreowania image&#39;u w biznesie
E-wizerunek. Internet jako narzędzie kreowania image&#39;u w biznesieWydawnictwo Helion
 
Microsoft Visual C++ 2008. Tworzenie aplikacji dla Windows
Microsoft Visual C++ 2008. Tworzenie aplikacji dla WindowsMicrosoft Visual C++ 2008. Tworzenie aplikacji dla Windows
Microsoft Visual C++ 2008. Tworzenie aplikacji dla WindowsWydawnictwo Helion
 
Co potrafi Twój iPhone? Podręcznik użytkownika. Wydanie II
Co potrafi Twój iPhone? Podręcznik użytkownika. Wydanie IICo potrafi Twój iPhone? Podręcznik użytkownika. Wydanie II
Co potrafi Twój iPhone? Podręcznik użytkownika. Wydanie IIWydawnictwo Helion
 
Makrofotografia. Magia szczegółu
Makrofotografia. Magia szczegółuMakrofotografia. Magia szczegółu
Makrofotografia. Magia szczegółuWydawnictwo Helion
 
Java. Efektywne programowanie. Wydanie II
Java. Efektywne programowanie. Wydanie IIJava. Efektywne programowanie. Wydanie II
Java. Efektywne programowanie. Wydanie IIWydawnictwo Helion
 
Ajax, JavaScript i PHP. Intensywny trening
Ajax, JavaScript i PHP. Intensywny treningAjax, JavaScript i PHP. Intensywny trening
Ajax, JavaScript i PHP. Intensywny treningWydawnictwo Helion
 
PowerPoint 2007 PL. Seria praktyk
PowerPoint 2007 PL. Seria praktykPowerPoint 2007 PL. Seria praktyk
PowerPoint 2007 PL. Seria praktykWydawnictwo Helion
 
Serwisy społecznościowe. Budowa, administracja i moderacja
Serwisy społecznościowe. Budowa, administracja i moderacjaSerwisy społecznościowe. Budowa, administracja i moderacja
Serwisy społecznościowe. Budowa, administracja i moderacjaWydawnictwo Helion
 

More from Wydawnictwo Helion (20)

Tworzenie filmów w Windows XP. Projekty
Tworzenie filmów w Windows XP. ProjektyTworzenie filmów w Windows XP. Projekty
Tworzenie filmów w Windows XP. Projekty
 
Blog, więcej niż internetowy pamiętnik
Blog, więcej niż internetowy pamiętnikBlog, więcej niż internetowy pamiętnik
Blog, więcej niż internetowy pamiętnik
 
Access w biurze i nie tylko
Access w biurze i nie tylkoAccess w biurze i nie tylko
Access w biurze i nie tylko
 
Pozycjonowanie i optymalizacja stron WWW. Ćwiczenia praktyczne
Pozycjonowanie i optymalizacja stron WWW. Ćwiczenia praktycznePozycjonowanie i optymalizacja stron WWW. Ćwiczenia praktyczne
Pozycjonowanie i optymalizacja stron WWW. Ćwiczenia praktyczne
 
E-wizerunek. Internet jako narzędzie kreowania image&#39;u w biznesie
E-wizerunek. Internet jako narzędzie kreowania image&#39;u w biznesieE-wizerunek. Internet jako narzędzie kreowania image&#39;u w biznesie
E-wizerunek. Internet jako narzędzie kreowania image&#39;u w biznesie
 
Microsoft Visual C++ 2008. Tworzenie aplikacji dla Windows
Microsoft Visual C++ 2008. Tworzenie aplikacji dla WindowsMicrosoft Visual C++ 2008. Tworzenie aplikacji dla Windows
Microsoft Visual C++ 2008. Tworzenie aplikacji dla Windows
 
Co potrafi Twój iPhone? Podręcznik użytkownika. Wydanie II
Co potrafi Twój iPhone? Podręcznik użytkownika. Wydanie IICo potrafi Twój iPhone? Podręcznik użytkownika. Wydanie II
Co potrafi Twój iPhone? Podręcznik użytkownika. Wydanie II
 
Makrofotografia. Magia szczegółu
Makrofotografia. Magia szczegółuMakrofotografia. Magia szczegółu
Makrofotografia. Magia szczegółu
 
Windows PowerShell. Podstawy
Windows PowerShell. PodstawyWindows PowerShell. Podstawy
Windows PowerShell. Podstawy
 
Java. Efektywne programowanie. Wydanie II
Java. Efektywne programowanie. Wydanie IIJava. Efektywne programowanie. Wydanie II
Java. Efektywne programowanie. Wydanie II
 
JavaScript. Pierwsze starcie
JavaScript. Pierwsze starcieJavaScript. Pierwsze starcie
JavaScript. Pierwsze starcie
 
Ajax, JavaScript i PHP. Intensywny trening
Ajax, JavaScript i PHP. Intensywny treningAjax, JavaScript i PHP. Intensywny trening
Ajax, JavaScript i PHP. Intensywny trening
 
PowerPoint 2007 PL. Seria praktyk
PowerPoint 2007 PL. Seria praktykPowerPoint 2007 PL. Seria praktyk
PowerPoint 2007 PL. Seria praktyk
 
Excel 2007 PL. Seria praktyk
Excel 2007 PL. Seria praktykExcel 2007 PL. Seria praktyk
Excel 2007 PL. Seria praktyk
 
Access 2007 PL. Seria praktyk
Access 2007 PL. Seria praktykAccess 2007 PL. Seria praktyk
Access 2007 PL. Seria praktyk
 
Word 2007 PL. Seria praktyk
Word 2007 PL. Seria praktykWord 2007 PL. Seria praktyk
Word 2007 PL. Seria praktyk
 
Serwisy społecznościowe. Budowa, administracja i moderacja
Serwisy społecznościowe. Budowa, administracja i moderacjaSerwisy społecznościowe. Budowa, administracja i moderacja
Serwisy społecznościowe. Budowa, administracja i moderacja
 
AutoCAD 2008 i 2008 PL
AutoCAD 2008 i 2008 PLAutoCAD 2008 i 2008 PL
AutoCAD 2008 i 2008 PL
 
Bazy danych. Pierwsze starcie
Bazy danych. Pierwsze starcieBazy danych. Pierwsze starcie
Bazy danych. Pierwsze starcie
 
Inventor. Pierwsze kroki
Inventor. Pierwsze krokiInventor. Pierwsze kroki
Inventor. Pierwsze kroki
 

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

  • 1. IDZ DO PRZYK£ADOWY ROZDZIA£ C++. 50 efektywnych SPIS TRE CI sposobów na udoskonalenie KATALOG KSI¥¯EK Twoich programów Autor: Scott Meyers KATALOG ONLINE T³umaczenie: Miko³aj Szczepaniak ISBN: 83-7361-345-5 ZAMÓW DRUKOWANY KATALOG Tytu³ orygina³u: Effective C++: 50 Specific Ways to Improve Your Programs and Design Format: B5, stron: 248 TWÓJ KOSZYK Pierwsze wydanie ksi¹¿ki „C++. 50 efektywnych sposobów na udoskonalenie twoich DODAJ DO KOSZYKA 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ê CENNIK I INFORMACJE 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 ZAMÓW INFORMACJE O NOWO CIACH pisania lepszych programów w C++, za odpowiednie rozwa¿ania s¹ poparte konkretnymi przyk³adami. Z my l¹ o nowym wydaniu, Scott Meyers opracowa³ ZAMÓW CENNIK 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++. CZYTELNIA Do najwa¿niejszych zalet ksi¹¿ki „C++. 50 efektywnych sposobów na udoskonalenie twoich programów” nale¿¹: FRAGMENTY KSI¥¯EK ONLINE • 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++; Wydawnictwo Helion zapewnia us³ugi doradcze dla klientów na ca³ym wiecie i jest cz³onkiem rady ul. Chopina 6 redakcyjnej pisma C++ Report. Regularnie przemawia na technicznych konferencjach na 44-100 Gliwice ca³ym wiecie, jest tak¿e autorem ksi¹¿ek „More Effective C++” oraz „Effective C++ CD”. tel. (32)230-98-63 W 1993. roku otrzyma³ tytu³ doktora informatyki na Brown University. e-mail: helion@helion.pl
  • 2. 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......................................................................28 Sposób 2. Wybieraj <iostream> zamiast <stdio.h>.....................................................................31 Sposób 3. Wybieraj new i delete zamiast malloc i free...............................................................33 Sposób 4. Stosuj komentarze w stylu C++ ..................................................................................34 Zarządzanie pamięcią .................................................................................................. 37 Sposób 5. U ywaj tych samych form w odpowiadających sobie zastosowaniach operatorów new i delete ..............................................................................................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 ........................................................................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.........................................................................................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....................................................................................93
  • 3. 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 ...................................................................113 Sposób 25. Unikaj przecią ania funkcji dla wskaźników i typów numerycznych......................117 Sposób 26. Strze się niejednoznaczności...................................................................................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 ................................................................................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 ................................................................................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....................................................178 Sposób 40. Modelując relacje posiadania („ma”) i implementacji z wykorzystaniem, stosuj podział na warstwy .........................................................................................186 Sposób 41. Rozró niaj dziedziczenie od stosowania szablonów ................................................189 Sposób 42. Dziedziczenie prywatne stosuj ostro nie ..................................................................193 Sposób 43. Dziedziczenie wielobazowe stosuj ostro nie............................................................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...........................................................226 Sposób 49. Zapoznaj się ze standardową biblioteką C++ ...........................................................227 Sposób 50. Pracuj bez przerwy nad swoją znajomością C++ .....................................................234 Skorowidz ................................................................................................................. 239
  • 4. 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 zniechę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 elementów wspólnych. Powinieneś wykorzystać mechanizm dziedziczenia i stworzyć klasy potomne względem jednej wspólnej klasy bazowej czy powinieneś wykorzystać szablony i wygenerować wszystkie potrzebne klasy ze wspólnym szkieletem kodu?
  • 5. 160 Dziedziczenie i projektowanie zorientowane obiektowo Klasa A ma zostać zaimplementowana w oparciu o klasę B. Czy A powinna zawierać składową reprezentującą obiekt klasy B czy te powinna prywatnie dziedziczyć po klasie B? Potrzebujesz projektu bezpiecznej pod względem typu i homogenicznej klasy pojemnikowej, która nie jest dostępna w standardowej bibliotece C++ (listę pojemników udostępnianych przez tę bibliotekę podano, prezentując sposób 49.). Czy lepszym rozwiązaniem będzie skonstruowanie szablonów czy budowa bezpiecznych pod względem typów interfejsów wokół tej klasy, która sama byłaby zaimplementowana za pomocą ogólnych (XQKF
  • 6. ) 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 jeszcze 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ści. 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.
  • 7. 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 Brytyjczykó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 wszyscy 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, ale nie odwrotnie. Taką interpretację publicznego dziedziczenia wymusza język C++. Przeanalizujmy poni szy przykład: ENCUU 2GTUQP ] _ ENCUU 5VWFGPV RWDNKE 2GTUQP ] _ 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 jest 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): XQKF FCPEG EQPUV 2GTUQP R MC F[ OQ G VC E[è XQKF UVWF[ EQPUV 5VWFGPV U V[NMQ UVWFGPEK OQIæ UVWFKQYCè
  • 8. 162 Dziedziczenie i projektowanie zorientowane obiektowo 2GTUQP R R TGRTGGPVWLG QUQDú QDKGMV MNCU[ 2GTUQP 5VWFGPV U U TGRTGGPVWLG UVWFGPVC QDKGMV MNCU[ 5VWFGPV FCPEG R FQDTG R TGRTGGPVWLG QUQDú FCPEG U FQDTG U TGRTGGPVWLG UVWFGPVC C YKúE VCM G QUQDú UVWF[ U FQDTG UVWF[ R D æF R PKG TGRTGGPVWLG UVWFGPVC 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 chronionego 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 podobny do poni szego: ENCUU $KTF ] RWDNKE XKTVWCN XQKF HN[ RVCMK OQIæ NCVCè _ ENCUU 2GPIWKP RWDNKE $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 naszą 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ść, hierarchię klas: ENCUU $KTF ] DTCM FGMNCTCELK HWPMELK HN[ _ ENCUU (N[KPI$KTF RWDNKE $KTF ] RWDNKE XKTVWCN XQKF HN[ _ ENCUU 0QP(N[KPI$KTF RWDNKE $KTF ] DTCM FGMNCTCELK HWPMELK HN[ _
  • 9. Sposób 35. Dopilnuj, by publiczne dziedziczenie modelowało relację „jest” 163 ENCUU 2GPIWKP RWDNKE 0QP(N[KPI$KTF ] DTCM FGMNCTCELK HWPMELK HN[ _ 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ędzy 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: XQKF GTTQT EQPUV UVTKPI OUI HWPMELC FGHKPKQYCPC Y KPP[O OKGLUEW ENCUU 2GPIWKP RWDNKE $KTF ] RWDNKE XKTVWCN XQKF HN[ ] GTTQT 2KPIYKP[ PKG OQIæ NCVCè _ _ 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 programu. Aby wyrazić ograniczenie „pingwiny nie mogą latać”, wystarczy nie definiować odpowiedniej funkcji dla obiektów klasy 2GPIWKP: ENCUU $KTF ] DTCM FGMNCTCELK HWPMELK HN[ _
  • 10. 164 Dziedziczenie i projektowanie zorientowane obiektowo ENCUU 0QP(N[KPI$KTF RWDNKE $KTF ] DTCM FGMNCTCELK HWPMELK HN[ _ ENCUU 2GPIWKP RWDNKE 0QP(N[KPI$KTF ] DTCM FGMNCTCELK HWPMELK HN[ _ Jeśli spróbujesz teraz wywołać funkcję HN[ dla obiektu reprezentującego pingwina, kompilator zasygnalizuje błąd: 2GPIWKP R RHN[ D æF Zaprezentowane rozwiązanie jest całkowicie odmienne od podejścia stosowanego w języku Smalltalk. Stosowana tam strategia powoduje, 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ócili do nauki na tym poziomie. Przeanalizuj więc poni szy kod: ENCUU 4GEVCPING ] RWDNKE XKTVWCN XQKF UGV*GKIJV KPV PGY*GKIJV XKTVWCN XQKF UGV9KFVJ KPV PGY9KFVJ XKTVWCN KPV JGKIJV EQPUV HWPMELG YTCECLæ DKG æEG XKTVWCN KPV YKFVJ EQPUV YCTVQ EK _
  • 11. Sposób 35. Dopilnuj, by publiczne dziedziczenie modelowało relację „jest” 165 XQKF OCMG$KIIGT 4GEVCPING T HWPMELC YKúMUCLæEC RQNG ] RQYKGTEJPK RTQUVQMæVC T KPV QNF*GKIJV TJGKIJV TUGV9KFVJ TYKFVJ FQFCLG FQ UGTQMQ EK T CUUGTV TJGKIJV QNF*GKIJV WRGYPKC UKú G Y[UQMQ è _ RTQUVQMæVC T PKG OKGPK C UKú 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 prostokątów: ENCUU 5SWCTG RWDNKE 4GEVCPING ] _ 5SWCTG U CUUGTV UYKFVJ UJGKIJV YCTWPGM OWUK D[è RTCYFKY[ FNC YU[UVMKEJ MYCFTCVÎY OCMG$KIIGT U PC UMWVGM FKGFKEGPKC U LGUV Y TGNCELK LGUV MNCUæ 4GEVCPING OQ GO[ YKúE YKúMU[è RQNG LGIQ RQYKGTEJPK CUUGTV U9KFVJ UJGKIJV YCTWPGM PCFCN OWUK D[è RTCYFKY[ FNC YU[UVMKEJ MYCFTCVÎY Tak e teraz oczywiste jest, e ostatni warunek nigdy nie powinien być fałszywy. Zgodnie z definicją, szerokość kwadratu jest przecie taka sama jak jego wysokość. Tym razem mamy jednak problem. Jak mo na pogodzić poni 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 , 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
  • 12. 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 oczekiwaniami. 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ą wystę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 wstę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 czegokolwiek.
  • 13. 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: ENCUU 5JCRG ] RWDNKE XKTVWCN XQKF FTCY EQPUV XKTVWCN XQKF GTTQT EQPUV UVTKPI OUI KPV QDLGEV+ EQPUV _ ENCUU 4GEVCPING RWDNKE 5JCRG ] _ ENCUU 'NNKRUG RWDNKE 5JCRG ] _ 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ą, poniewa : Interfejsy funkcji składowych zawsze są dziedziczone. W sposobie 35. wyjaśniłem, e dziedziczenie publiczne oznacza faktycznie relację „jest”, zatem wszystkie elementy istniejące w klasie bazowej muszą tak e istnieć w klasach potomnych. Jeśli więc daną funkcję mo na wykonać dla danej klasy, musi tak e istnieć sposób jej wykonania dla jej 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, e: Celem deklarowania czystych funkcji wirtualnych jest otrzymanie 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ą zaimplementować”.
  • 14. 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
  • 15. RU PGY 5JCRG D æF 5JCRG LGUV MNCUæ CDUVTCME[LPæ 5JCRG
  • 16. RU PGY 4GEVCPING FQDTG RU FTCY Y[YQ WLG 4GEVCPINGFTCY 5JCRG
  • 17. RU PGY 'NNKRUG FQDTG RU FTCY Y[YQ WLG 'NNKRUGFTCY RU 5JCRGFTCY Y[YQ WLG 5JCRGFTCY RU 5JCRGFTCY Y[YQ WLG 5JCRGFTCY 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 bezpieczniejszej 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 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 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 otrzymanie 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ę 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:
  • 18. Sposób 36. Odróżniaj dziedziczenie interfejsu od dziedziczenia implementacji 169 ENCUU #KTRQTV ] _ TGRTGGPVWLG NQVPKUMC ENCUU #KTRNCPG ] RWDNKE XKTVWCN XQKF HN[ EQPUV #KTRQTV FGUVKPCVKQP _ XQKF #KTRNCPGHN[ EQPUV #KTRQTV FGUVKPCVKQP ] FQO[ NP[ MQF OQFGNWLæE[ RTGNQV UCOQNQVW FQ FCPGIQ EGNW _ 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 systemu. 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 ] DTCM FGMNCTCELK HWPMELK HN[ _ Ich kod zawiera więc coś podobnego do poni szego fragmentu: #KTRQTV 1MGEKG 1MúEKG VQ NQVPKUMQ Y 9CTUCYKG #KTRNCPG
  • 19. RC PGY /QFGN% RC HN[ 1MGEKG Y[YQ WLG HWPMELú #KTRNCPGHN[
  • 20. 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 linii 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 XKTVWCN XQKF HN[ EQPUV #KTRQTV FGUVKPCVKQP RTQVGEVGF XQKF FGHCWNV(N[ EQPUV #KTRQTV FGUVKPCVKQP _ XQKF #KTRNCPGFGHCWNV(N[ EQPUV #KTRQTV FGUVKPCVKQP ] FQO[ NP[ MQF OQFGNWLæE[ RTGNQV UCOQNQVW FQ FCPGIQ EGNW _ 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 XKTVWCN XQKF HN[ EQPUV #KTRQTV FGUVKPCVKQP ] FGHCWNV(N[ FGUVKPCVKQP _ _ ENCUU /QFGN$ RWDNKE #KTRNCPG ] RWDNKE XKTVWCN XQKF HN[ EQPUV #KTRQTV FGUVKPCVKQP ] 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łasnej wersji funkcji HN[:
  • 21. Sposób 36. Odróżniaj dziedziczenie interfejsu od dziedziczenia implementacji 171 ENCUU /QFGN% RWDNKE #KTRNCPG ] RWDNKE XKTVWCN XQKF HN[ EQPUV #KTRQTV FGUVKPCVKQP _ XQKF /QFGN%HN[ EQPUV #KTRQTV FGUVKPCVKQP ] MQF OQFGNWLæE[ RTGNQV UCOQNQVW V[RW /QFGN% FQ FCPGIQ EGNW _ 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 funkcji wirtualnej: ENCUU #KTRNCPG ] RWDNKE XKTVWCN XQKF HN[ EQPUV #KTRQTV FGUVKPCVKQP _ XQKF #KTRNCPGHN[ EQPUV #KTRQTV FGUVKPCVKQP ] FQO[ NP[ MQF OQFGNWLæE[ RTGNQV UCOQNQVW FQ FCPGIQ EGNW _ ENCUU /QFGN# RWDNKE #KTRNCPG ] RWDNKE XKTVWCN XQKF HN[ EQPUV #KTRQTV FGUVKPCVKQP ] #KTRNCPGHN[ FGUVKPCVKQP _ _
  • 22. 172 Dziedziczenie i projektowanie zorientowane obiektowo ENCUU /QFGN$ RWDNKE #KTRNCPG ] RWDNKE XKTVWCN XQKF HN[ EQPUV #KTRQTV FGUVKPCVKQP ] #KTRNCPGHN[ FGUVKPCVKQP _ _ ENCUU /QFGN% RWDNKE #KTRNCPG ] RWDNKE XKTVWCN XQKF HN[ EQPUV #KTRQTV FGUVKPCVKQP _ XQKF /QFGN%HN[ EQPUV #KTRQTV FGUVKPCVKQP ] MQF OQFGNWLæE[ RTGNQV UCOQNQVW V[RW /QFGN% FQ FCPGIQ EGNW _ 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 tego wniosek: Celem deklarowania niewirtualnej funkcji jest otrzymanie 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ółowo 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
  • 23. 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:
  • 24. RF PGY D
  • 25. RD RF RD OH Y[YQ WLG HWPMELú OH C RQOQEæ YUMC PKMC FQ MNCU[ DCQYGL RF OH Y[YQ WLG HWPMELú OH C RQOQEæ YUMC PKMC FQ MNCU[ RQVQOPGL 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.
  • 26. 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 przyzwyczajeni 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 sposób: ENCUU $ ] RWDNKE XQKF OH _ ENCUU RWDNKE $ ] _ Nawet gdybyśmy nic nie wiedzieli o $, i OH, mając dany obiekt Z klasy : Z Z LGUV QDKGMVGO V[RW bylibyśmy bardzo zaskoczeni, gdyby instrukcje: $
  • 27. R$ Z QVT[OWLG YUMC PKM FQ Z R$ OH Y[YQ WLG HWPMELú OH C RQOQEæ YUMC PKMC powodowały inne działanie, ni instrukcje:
  • 28. R Z QVT[OWLG YUMC PKM FQ Z R OH Y[YQ WLG HWPMELú OH C RQOQEæ YUMC PKMC 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 XQKF OH WMT[YC FGHKPKELú $OH RCVT URQUÎD _ R$ OH Y[YQ WLG HWPMELú $OH R OH Y[YQ WLG HWPMELú OH
  • 29. 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 referencje 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 przyjemnoś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ładowej $OH, wówczas: Wszystkie funkcje, które mo na stosować dla obiektów 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 klasie $ 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ć podobnych prób.
  • 30. 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: ENCUU 5JCRG%QNQT ] 4' )4''0 $.7' _ MNCUC TGRTGGPVWLæEC HKIWT[ IGQOGVT[EPG ENCUU 5JCRG ] RWDNKE YU[UVMKG HKIWT[ OWUæ WFQUVúRPKCè T[UWLæEG LG HWPMELG XKTVWCN XQKF FTCY 5JCRG%QNQT EQNQT 4' EQPUV _ ENCUU 4GEVCPING RWDNKE 5JCRG ] RWDNKE YTÎè WYCIú PC KPPC FQO[ NPæ YCTVQ è RCTCOGVTW NG XKTVWCN XQKF FTCY 5JCRG%QNQT EQNQT )4''0 EQPUV _ ENCUU %KTENG RWDNKE 5JCRG ] RWDNKE XKTVWCN XQKF FTCY 5JCRG%QNQT EQNQT EQPUV _
  • 31. 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
  • 32. RU UVCV[EP[ V[R 5JCRG
  • 33. 5JCRG
  • 34. RE PGY %KTENG UVCV[EP[ V[R 5JCRG
  • 35. 5JCRG
  • 36. RT PGY 4GEVCPING UVCV[EP[ V[R 5JCRG
  • 37. 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
  • 38. . 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
  • 39. , zaś typem dynamicznym zmiennej RT jest 4GEVCPING
  • 40. . 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: RU RE V[RGO F[PCOKEP[O YUMC PKMC RU LGUV VGTC %KTENG
  • 41. RU RT V[RGO F[PCOKEP[O YUMC PKMC RU LGUV VGTC 4GEVCPING
  • 42. Wirtualne funkcje są wiązane dynamicznie, co oznacza, e konkretna wywoływana funkcja zale y od dynamicznego typu obiektu wykorzystywanego do jej wywołania: RE FTCY 4' Y[YQ WLG HWPMELú %KTENGFTCY 4' RT FTCY 4' Y[YQ WLG HWPMELú 4GEVCPINGFTCY 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 WLG 4GEVCPINGFTCY 4'
  • 43. 178 Dziedziczenie i projektowanie zorientowane obiektowo W tym przypadku typem dynamicznym zmiennej wskaźnikowej RT jest 4GEVCPING
  • 44. , 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
  • 45. , 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ę nie przekonuje, zaufaj mi — na pewno z takiego zachowania Twojego oprogramowania nie będą 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 ponownie 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 z myślą o szybkości i prostocie implementacji. Efektem jest wydajny mechanizm wykonywania programów, ale tak e — jeśli nie będziesz stosował zaleceń zawartych w tym sposobie — potencjalne nieporozumienia. Sposób 39. Unikaj rzutowania w dół hierarchii dziedziczenia Sposób 39. Unikaj rzutowania w dół hierarchii dziedziczenia W dzisiejszych niespokojnych czasach warto mieć na oku poczynania instytucji finansowych, rozwa my więc klasę protokołu (patrz sposób 34.) dla kont bankowych: ENCUU 2GTUQP ] _ ENCUU $CPM#EEQWPV ] RWDNKE $CPM#EEQWPV EQPUV 2GTUQP
  • 46. RTKOCT[1YPGT EQPUV 2GTUQP
  • 47. LQKPV1YPGT XKTVWCN `$CPM#EEQWPV XKTVWCN XQKF OCMGGRQUKV FQWDNG COQWPV XKTVWCN XQKF OCMG9KVJFTCYCN FQWDNG COQWPV XKTVWCN FQWDNG DCNCPEG EQPUV _
  • 48. Sposób 39. Unikaj rzutowania w dół hierarchii dziedziczenia 179 Wiele banków przedstawia dzisiaj swoim klientom niezwykle szeroką ofertę typów kont bankowych, załó my jednak (dla uproszczenia), e istnieje tylko jeden typ konta bankowego, zwykłe konto oszczędnościowe: ENCUU 5CXKPIU#EEQWPV RWDNKE $CPM#EEQWPV ] RWDNKE 5CXKPIU#EEQWPV EQPUV 2GTUQP
  • 49. RTKOCT[1YPGT EQPUV 2GTUQP
  • 50. LQKPV1YPGT `5CXKPIU#EEQWPV XQKF ETGFKV+PVGTGUV FQFCL QFUGVMK FQ MQPVC _ Nie jest to mo e zbyt zaawansowane konto oszczędnościowe, jest jednak w zupełno- ści wystarczające dla naszych rozwa ań. Bank prawdopodobnie przechowuje listę wszystkich swoich kont, która mo e być zaim- plementowana za pomocą szablonu klasy NKUV ze standardowej biblioteki C++ (patrz sposób 49.). Przypuśćmy, e w naszym banku taka lista nosi nazwę CNN#EEQWPVU: NKUV$CPM#EEQWPV
  • 51. CNN#EEQWPVU YU[UVMKG MQPVC QDU WIKYCPG RTG FCP[ DCPM Jak wszystkie standardowe pojemniki, listy przechowują jedynie kopie umieszczanych w nich obiektów, zatem, aby uniknąć przechowywania wielu kopii poszczególnych obiektów klasy $CPM#EEQWPV, programiści zdecydowali, e lista CNN#EEQWPVU powinna składać się jedynie ze wskaźników do tych obiektów, a nie samych obiektów repre- zentujących konta. Przypuśćmy, e naszym zadaniem jest kolejne przejście przez wszystkie konta i doliczenie do nich nale nych odsetek. Mo emy zrealizować tę usługę w następujący sposób: RúVNC PKG QUVCPKG UMQORKNQYCPC LG NK PKIF[ YEG PKGL PKG OKC G FQ E[PKGPKC MQFGO Y[MQT[UVWLæE[O KVGTCVQT[ RCVT PK GL HQT NKUV$CPM#EEQWPV
  • 52. KVGTCVQT R CNN#EEQWPVUDGIKP R CNN#EEQWPVUGPF R ]
  • 53. R ETGFKV+PVGTGUV D æF _ Nasze kompilatory szybko zasygnalizują, e lista CNN#EEQWPVU zawiera wskaźniki do obiektów klasy $CPM#EEQWPV, a nie obiektów klasy 5CXKPIU#EEQWPV, zatem w ka dej kolejnej iteracji zmienna R będzie wskazywała na obiekt klasy $CPM#EEQWPV. Oznacza to, e wywołanie funkcji ETGFKV+PVGTGUV jest nieprawidłowe, poniewa została ona zadeklarowana wyłącznie dla obiektów klasy 5CXKPIU#EEQWPV, a nie $CPM#EEQWPV. Jeśli wiersz NKUV$CPM#EEQWPV
  • 54. KVGTCVQT R CNN#EEQWPVUDGIKP jest dla Ciebie niezrozumiały i nie przypomina Ci kodu C++, z którym miałeś do czynienia do tej pory, oznacza to, e prawdopodobnie nie miałeś wcześniej przyjemności korzystać z szablo- nów klas pojemnikowych ze standardowej biblioteki C++. Tę część biblioteki nazywa się często Standardową Biblioteką Szablonów (ang. Standard Template Library — STL), więcej informacji na jej temat znajdziesz w sposobie 49. Na tym etapie wystarczy Ci
  • 55. 180 Dziedziczenie i projektowanie zorientowane obiektowo wiedza, e zmienna R zachowuje się jak wskaźnik wskazujący na przeglądane w pętli kolejne elementy listy CNN#EEQWPVU (od jej początku do końca). Oznacza to, e zmien- na R jest traktowana tak, jakby jej typem był $CPM#EEQWPV
  • 56.
  • 57. , a elementy przeglądanej listy były przechowywane w tablicy. Powy szy kod nie zostanie niestety skompilowany. Wiemy oczywiście, e lista CNN#EEQWPVU została zdefiniowana jako pojemnik na wskaźniki typu $CPM#EEQWPV
  • 58. , ale mamy tak e świadomość, e w powy szej pętli faktycznie przechowuje wskaźniki typu 5CXKPIU#EEQWPV
  • 59. , poniewa 5CXKPIU#EEQWPV jest jedyną klasą, dla której mo emy tworzyć obiekty. Głupie kompilatory! Zdecydowaliśmy się przekazać im wiedzę, któ- ra jest dla nas zupełnie oczywista i okazało się, e są zbyt tępe, by zauwa yć, e lista CNN#EEQWPVU przechowuje w rzeczywistości wskaźniki typu 5CXKPIU#EEQWPV
  • 60. : RúVNC QUVCPKG UMQORKNQYCPC EJQEKC PKG LGUV VQ FQDTG TQYKæCPKG HQT NKUV$CPM#EEQWPV
  • 61. KVGTCVQT R CNN#EEQWPVUDGIKP R CNN#EEQWPVUGPF R ] UVCVKEAECUV5CXKPIU#EEQWPV
  • 62.
  • 63. R ETGFKV+PVGTGUV _ Rozwiązaliśmy wszystkie nasze problemy! Zrobiliśmy to przejrzyście, elegancko i zwięźle — wystarczyło jedno proste rzutowanie. Wiemy, jakiego typu wskaźniki faktycznie znajdują się na liście CNN#EEQWPVU, nasze ogłupiałe kompilatory tego nie wiedzą, więc zastosowaliśmy rzutowanie, by przekazać im naszą wiedzę. Czy mo na znaleźć bar- dziej logiczne rozwiązanie? Chciałbym w tym momencie przedstawić pewną biblijną analogię. Operacje rzutowa- nia są dla programistów C++ jak jabłko dla biblijnej Ewy. Zaprezentowany w powy szym kodzie rodzaj rzutowania (ze wskaźnika do klasy bazowej do wskaźnika do klasy potomnej) nosi nazwę rzutowania w dół, poniewa rzutujemy w dół hierarchii dziedziczenia. W powy szym przykładzie operacja rzutowania w dół zadziałała, jednak — jak się za chwilę przekonasz — takie rozwiązanie prowadzi do ogromnych problemów podczas konserwacji oprogramowania. Wróćmy jednak do naszego banku. Zachęcony sukcesem, jakim było rozwiązanie pro- blemu kont oszczędnościowych, bank postanawia zaoferować swoim klientom tak e konta czekowe. Co więcej, załó my, e do tego typu kont tak e dolicza się nale ne od- setki (podobnie, jak w przypadku kont oszczędnościowych): ENCUU %JGEMKPI#EEQWPV RWDNKE $CPM#EEQWPV ] RWDNKE XQKF ETGFKV+PVGTGUV FQFCL QFUGVMK FQ MQPVC _ Nie muszę chyba dodawać, e lista CNN#EEQWPVU będzie teraz zawierała wskaźniki zarówno do obiektów reprezentujących konta oszczędnościowe, jak i do tych repre- zentujących konta czekowe. Okazuje się, e stworzona przed chwilą pętla naliczająca odsetki przestaje działać.
  • 64. Sposób 39. Unikaj rzutowania w dół hierarchii dziedziczenia 181 Pierwszy problem polega na tym, e pętla nadal będzie poprawnie kompilowana, mimo e nie wprowadziliśmy jeszcze zmian uwzględniających istnienie obiektów nowej klasy %JGEMKPI#EEQWPV. Wynika to z faktu, e nasze kompilatory nadal będą pochopnie zakładały, e kiedy sygnalizujemy (za pomocą UVCVKEAECUV), e
  • 65. R w rzeczywistości wskazuje na 5CXKPIU#EEQWPV
  • 66. , tak jest w istocie. W końcu to do nas nale y przewi- dywanie skutków wykonywania poszczególnych instrukcji. Drugi problem wynika z typowego sposobu radzenia sobie z pierwszym problemem, co zwykle skutkuje tworzeniem podobnego kodu: HQT NKUV$CPM#EEQWPV
  • 67. KVGTCVQT R CNN#EEQWPVUDGIKP R CNN#EEQWPVUGPF R ] KH
  • 68. R YUMCWLG PC QDKGMV MNCU[ 5CXKPIU#EEQWPV UVCVKEAECUV5CXKPIU#EEQWPV
  • 69.
  • 70. R ETGFKV+PVGTGUV GNUG UVCVKEAECUV%JGEMKPI#EEQWPV
  • 71.
  • 72. R ETGFKV+PVGTGUV _ Jeśli kiedykolwiek stwierdzisz, e Twój kod zawiera instrukcję warunkową w postaci: „jeśli obiekt jest typu T1, zrób coś, jeśli jednak jest typu T2, zrób coś innego”, na- tychmiast uderz się w pierś. Tego rodzaju instrukcje są nienaturalne dla języka C++; podobną strategię mo na stosować w C lub Pascalu, ale nigdy w C++, w którym mo- emy przecie u yć funkcji wirtualnych. Pamiętaj, e zastosowanie funkcji wirtualnych sprawia, e za zapewnianie prawi- dłowych wywołań funkcji — w zale ności od typu wykorzystywanego obiektu — odpowiadają kompilatory. Nie zaśmiecaj więc swojego kodu instrukcjami warunko- wymi ani przełącznikami; zamiast tego wykorzystuj mo liwości swoich kompilato- rów. Oto przykład: ENCUU $CPM#EEQWPV ] _ LCM Y[ GL PQYC MNCUC TGRTGGPVWLæEC MQPVC RT[PQUæEG MNKGPVQO QFUGVMK ENCUU +PVGTGUV$GCTKPI#EEQWPV RWDNKE $CPM#EEQWPV ] RWDNKE XKTVWCN XQKF ETGFKV+PVGTGUV _ ENCUU 5CXKPIU#EEQWPV RWDNKE +PVGTGUV$GCTKPI#EEQWPV ] LCM Y[ GL _ ENCUU %JGEMKPI#EEQWPV RWDNKE +PVGTGUV$GCTKPI#EEQWPV ] LCM Y[ GL _ Powy szą hierarchię mo na przedstawić graficznie:
  • 73. 182 Dziedziczenie i projektowanie zorientowane obiektowo Poniewa zarówno konta oszczędnościowe, jak i konta czekowe wymagają naliczania odsetek, naturalnym rozwiązaniem jest przeniesienie wspólnej operacji na wy szy poziom hierarchii klas — do wspólnej klasy bazowej. Jednak przy zało eniu, e nie wszystkie konta w danym banku muszą mieć naliczane odsetki (jak wynika z moich doświadczeń, takie zało enie jest sensowne), nie mo emy przenieść wspomnianej operacji do najwy szej (w hierarchii) klasy $CPM#EEQWPV. Wprowadziliśmy więc nową podklasę klasy $CPM#EEQWPV, którą nazwaliśmy +PVGTGUV$GCTKPI#EEQWPV, i która jest klasą bazową dla klas 5CXKPIU#EEQWPV i %JGEMKPI#EEQWPV. Wymaganie, by zarówno dla konta oszczędnościowego, jak i dla konta czekowego naliczać odsetki, uwzględniliśmy, deklarując w klasie +PVGTGUV$GCTKPI#EEQWPV czystą funkcję wirtualną ETGFKV+PVGTGUV, która zostanie przypuszczalnie zdefiniowana w pod- klasach 5CXKPIU#EEQWPV i %JGEMKPI#EEQWPV. Nowa hierarchia klas umo liwia nam napisanie omawianej wcześniej pętli od początku: TQYKæCPKG NGRUG CNG PCFCN PKGFQUMQPC G HQT NKUV$CPM#EEQWPV
  • 74. KVGTCVQT R CNN#EEQWPVUDGIKP R CNN#EEQWPVUGPF R ] UVCVKEAECUV+PVGTGUV$GCTKPI#EEQWPV
  • 75.
  • 76. R ETGFKV+PVGTGUV _ Mimo e powy sza pętla nadal zawiera skrytykowane wcześniej rzutowanie, zapropo- nowane rozwiązanie jest znacznie lepsze od omawianych do tej pory, poniewa będzie poprawnie funkcjonowało nawet po dodaniu do naszej aplikacji nowych podklas do klasy +PVGTGUV$GCTKPI#EEQWPV. Aby całkowicie pozbyć się rzutowania, musimy wprowadzić do naszego projektu kilka dodatkowych modyfikacji. Jedną z nich jest doprecyzowanie specyfikacji wykorzy- stywanej listy kont. Gdybyśmy mogli wykorzystać listę obiektów klasy +PVGTGUV$G CTKPI#EEQWPV, zamiast obiektów klasy $CPM#EEQWPV, rozwiązanie byłoby trywialne: YU[UVMKG QDU WIKYCPG RTG DCPM MQPVC QFUGVMCOK NKUV+PVGTGUV$GCTKPI#EEQWPV
  • 77. CNN+$#EEQWPVU RúVNC QUVCPKG UMQORKNQYCPC K DúFKG FKC C C RTCYKF QYQ HQT NKUV+PVGTGUV$GCTKPI#EEQWPV
  • 78. KVGTCVQT R CNN+$#EEQWPVUDGIKP R CNN+$#EEQWPVUGPF R ] R
  • 80. Sposób 39. Unikaj rzutowania w dół hierarchii dziedziczenia 183 Jeśli tworzenie bardziej specjalizowanej listy nie jest brane pod uwagę, mo na stwier- dzić, e operację ETGFKV+PVGTGUV mo na stosować dla wszystkich kont bankowych; jednak w przypadku kont, dla których nie nalicza się odsetek, wspomniana operacja jest pusta. To samo mo emy wyrazić za pomocą poni szego fragmentu kodu: ENCUU $CPM#EEQWPV ] RWDNKE XKTVWCN XQKF ETGFKV+PVGTGUV ]_ _ ENCUU 5CXKPIU#EEQWPV RWDNKE $CPM#EEQWPV ] _ ENCUU %JGEMKPI#EEQWPV RWDNKE $CPM#EEQWPV ] _ NKUV$CPM#EEQWPV
  • 81. CNN#EEQWPVU RCVT PKG OC TWVQYCPKC HQT NKUV$CPM#EEQWPV
  • 82. KVGTCVQT R CNN#EEQWPVUDGIKP R CNN#EEQWPVUGPF R ] R
  • 83. ETGFKV+PVGTGUV _ Zauwa , e wirtualna funkcja $CPM#EEQWPVETGFKV+PVGTGUV udostępnia pustą do- myślną implementację. Jest to wygodny sposób definiowania funkcji, która domyślnie jest operacją pustą, mo e jednak w przyszłości doprowadzić do nieprzewidywalnych trudności. Omówienie przyczyn takiego niebezpieczeństwa i sposobów jego elimino- wania znajdziesz w sposobie 36. Zauwa tak e, e funkcja ETGFKV+PVGTGUV jest (nie- jawnie) wbudowana. Oczywiście nie ma w tym nic złego, jednak z uwagi na fakt, e omawiana funkcja jest tak e wirtualna, dyrektywa KPNKPG będzie prawdopodobnie zignorowana przez kompilator (patrz sposób 33.). Jak się przekonałeś, rzutowanie w dół hierarchii klas mo e być eliminowane na wiele sposobów. Najlepszym z nich jest zastąpienie tego typu operacji wywołaniami funkcji wirtualnych — mo na wówczas zdefiniować domyślne implementacje tych funkcji jako operacje puste, co pozwoli na ich prawidłową obsługę przez obiekty klasy, dla których dane działania nie mają sensu. Drugą metodą jest uściślenie wykorzystywa- nych typów, co pozwala wyeliminować nieporozumienia wynikające z rozbie ności pomiędzy typami deklarowanymi a typami reprezentowanymi przez u ywane obiekty w rzeczywistości. Wysiłek związany z eliminowaniem rzutowania w dół nie idzie na marne, poniewa kod zawierający operacje tego typu jest brzydki i mo e być źródłem błędów, jest tak e trudny do zrozumienia, rozszerzania i konserwacji. To, co napisałem do tej pory, jest prawdą i tylko prawdą. Nie jest jednak całą prawdą. Ist- nieją bowiem sytuacje, w których naprawdę musisz wykonać operację rzutowania w dół. Przykładowo, przypuśćmy, e mamy do czynienia z rozwa aną na początku tego spo- sobu sytuacją, w której lista CNN#EEQWPVU przechowuje wskaźniki do obiektów klasy $CPM#EEQWPV, funkcja ETGFKV+PVGTGUV jest zdefiniowana wyłącznie dla obiektów klasy 5CXKPIU#EEQWPV i musimy napisać pętlę naliczającą odsetki dla wszystkich kont. Przypuśćmy tak e, e wszystkie wspomniane elementy są poza naszą kontrolą, co