Professional Documents
Culture Documents
Kursc
Kursc
Kursc
Kurs C++ Tutorial ten jest kompletnym opisem jzyka C++. Rozpoczyna si od wstpu do programowania i jzyka C++, by potem przeprowadzi Czytelnika przez proces konstruowania jego pierwszych programw. Po nauce podstaw przychodzi czas na programowanie obiektowe, a potem zaawansowane aspekty jzyka - z wyjtkami i szablonami wcznie. Kurs jest czci megatutoriala Od zera do gier kodera.
Copyright 2004 Karol Kuczmarski Udziela si zezwolenia do kopiowania, rozpowszechniania i/lub modyfikacji tego dokumentu zgodnie z zasadami Licencji GNU Wolnej Dokumentacji w wersji 1.1 lub dowolnej pniejszej, opublikowanej przez Free Software Foundation; bez Sekcji Niezmiennych, bez Tekstu na Przedniej Okadce, bez Tekstu na Tylniej Okadce. Kopia licencji zaczona jest w sekcji Licencja GNU Wolnej Dokumentacji. Wszystkie znaki wystpujce w tekcie s zastrzeonymi znakami firmowymi lub towarowymi ich wacicieli.
Autorzy dooyli wszelkich stara, aby zawarte w tej publikacji informacje byy kompletne i rzetelne. Nie bior jednak adnej odpowiedzialnoci ani za ich wykorzystanie, ani za zwizane z tym ewentualne naruszenie praw patentowych i autorskich. Autorzy nie ponosz rwnie adnej odpowiedzialnoci za ewentualne szkody wynike z wykorzystania informacji zawartych w tej publikacji.
SPIS TRECI
PODSTAWY PROGRAMOWANIA __________________________ 17
Krtko o programowaniu _________________________________________ 19
Krok za krokiem _____________________________________________________________ Jak rozmawiamy z komputerem? ________________________________________________ Jzyki programowania ________________________________________________________ Przegld najwaniejszych jzykw programowania ________________________________ Brzemienna w skutkach decyzja ______________________________________________ Kwestia kompilatora ________________________________________________________ Podsumowanie ______________________________________________________________ Pytania i zadania __________________________________________________________ Pytania ________________________________________________________________ wiczenia ______________________________________________________________ 19 21 23 23 26 27 28 28 28 28
4
Ptla krokowa for _________________________________________________________ Instrukcje break i continue _________________________________________________ Podsumowanie ______________________________________________________________ Pytania i zadania __________________________________________________________ Pytania ________________________________________________________________ wiczenia ______________________________________________________________ 62 65 66 67 67 67
5
Proste tablice ____________________________________________________________ Inicjalizacja tablicy______________________________________________________ Przykad wykorzystania tablicy ____________________________________________ Wicej wymiarw _________________________________________________________ Deklaracja i inicjalizacja__________________________________________________ Tablice w tablicy________________________________________________________ Nowe typy danych __________________________________________________________ Wyliczania nadszed czas ___________________________________________________ Przydatno praktyczna __________________________________________________ Definiowanie typu wyliczeniowego __________________________________________ Uycie typu wyliczeniowego _______________________________________________ Zastosowania __________________________________________________________ Kompleksowe typy ________________________________________________________ Typy strukturalne i ich definiowanie ________________________________________ Struktury w akcji _______________________________________________________ Odrobina formalizmu - nie zaszkodzi! _______________________________________ Przykad wykorzystania struktury __________________________________________ Unie _________________________________________________________________ Wikszy projekt ____________________________________________________________ Projektowanie ____________________________________________________________ Struktury danych w aplikacji ______________________________________________ Dziaanie programu _____________________________________________________ Interfejs uytkownika____________________________________________________ Kodowanie ______________________________________________________________ Kilka moduw i wasne nagwki___________________________________________ Tre pliku nagwkowego ________________________________________________ Waciwy kod gry _______________________________________________________ 113 115 116 119 120 121 123 123 123 125 126 126 128 128 129 131 131 136 137 138 138 139 141 142 142 144 145
146 147 147 147 152
Funkcja main(), czyli skadamy program ____________________________________ Uroki kompilacji ________________________________________________________ Uruchamiamy aplikacj __________________________________________________ Wnioski _________________________________________________________________ Dziwaczne projektowanie_________________________________________________ Do skomplikowane algorytmy____________________________________________ Organizacja kodu _______________________________________________________ Podsumowanie _____________________________________________________________ Pytania i zadania _________________________________________________________ Pytania _______________________________________________________________ wiczenia _____________________________________________________________
Zaczynamy __________________________________________________________________ Deklarujemy zmienne __________________________________________________________ Funkcja StartGry() ___________________________________________________________ Funkcja Ruch() _______________________________________________________________ Funkcja RysujPlansze() _______________________________________________________
155 156 158 158 158 159 159 159 160 160 160
Obiekty______________________________________________________ 161
Przedstawiamy klasy i obiekty _________________________________________________ Skrawek historii __________________________________________________________ Mao zachcajce pocztki ________________________________________________ Wyszy poziom_________________________________________________________ Skostniae standardy ____________________________________________________ Obiektw czar__________________________________________________________ Co dalej? _____________________________________________________________ Pierwszy kontakt _________________________________________________________ Obiektowy wiat ________________________________________________________ 161 161 161 162 163 163 163 164 164
Wszystko jest obiektem ________________________________________________________ 164 Okrelenie obiektu_____________________________________________________________ 165 Obiekt obiektowi nierwny ______________________________________________________ 166
Obiekty i klasy w C++ _______________________________________________________ 169 Klasa jako typ obiektowy ___________________________________________________ 169 Dwa etapy okrelania klasy _______________________________________________ 170
6
Definicja klasy _________________________________________________________ 170
Kontrola dostpu do skadowych klasy _____________________________________________ Deklaracje pl ________________________________________________________________ Metody i ich prototypy__________________________________________________________ Konstruktory i destruktory ______________________________________________________ Co jeszcze? _________________________________________________________________
Implementacja metod ___________________________________________________ 178 Praca z obiektami _________________________________________________________ 179 Zmienne obiektowe _____________________________________________________ 180
Deklarowanie zmiennych i tworzenie obiektw _______________________________________ onglerka obiektami ___________________________________________________________ Dostp do skadnikw __________________________________________________________ Niszczenie obiektw ___________________________________________________________ Podsumowanie _______________________________________________________________ Deklarowanie wskanikw i tworzenie obiektw ______________________________________ Jeden dla wszystkich, wszystkie do jednego _________________________________________ Dostp do skadnikw __________________________________________________________ Niszczenie obiektw ___________________________________________________________ Stosowanie wskanikw na obiekty _______________________________________________ 180 180 182 182 183 Wskanik this _______________________________________________________________ 179
Definicja klasy bazowej i specyfikator protected_____________________________________ 195 Definicja klasy pochodnej _______________________________________________________ 197
Metody wirtualne i polimorfizm ________________________________________________ 204 Wirtualne funkcje skadowe _________________________________________________ 204 To samo, ale inaczej ____________________________________________________ 204
Deklaracja metody wirtualnej ____________________________________________________ Przedefiniowanie metody wirtualnej _______________________________________________ Pojedynek: metody wirtualne przeciwko zwykym ____________________________________ Wirtualny destruktor ___________________________________________________________ 205 206 206 207
Projektowanie zorientowane obiektowo __________________________________________ 223 Rodzaje obiektw _________________________________________________________ 223 Singletony ____________________________________________________________ 224
Przykady wykorzystania ________________________________________________________ 224
7
Praktyczna implementacja z uyciem skadowych statycznych___________________________ 225
Obiekty zasadnicze______________________________________________________ Obiekty narzdziowe ____________________________________________________ Definiowanie odpowiednich klas______________________________________________ Abstrakcja ____________________________________________________________
Identyfikacja klas _____________________________________________________________ 232 Abstrakcja klasy ______________________________________________________________ 233 Skadowe interfejsu klasy _______________________________________________________ 234
Implementacja _________________________________________________________ 234 Zwizki midzy klasami ____________________________________________________ 235 Dziedziczenie i zawieranie si _____________________________________________ 235
Zwizek generalizacji-specjalizacji ________________________________________________ 235 Zwizek agregacji _____________________________________________________________ 236 Odwieczny problem: by czy mie?________________________________________________ 237 Krotno zwizku______________________________________________________________ 238 Tam i (by moe) z powrotem ___________________________________________________ 239
Zwizek asocjacji _______________________________________________________ 237 Podsumowanie _____________________________________________________________ Pytania i zadania _________________________________________________________ Pytania _______________________________________________________________ wiczenia _____________________________________________________________ 240 240 241 241
Wskaniki____________________________________________________ 243
Ku pamici ________________________________________________________________ 244 Rodzaje pamici __________________________________________________________ 244 Rejestry procesora ______________________________________________________ 244
Zmienne przechowywane w rejestrach _____________________________________________ 245 Dostp do rejestrw ___________________________________________________________ 246
Pami trwaa__________________________________________________________ 247 Organizacja pamici operacyjnej _____________________________________________ 247 Adresowanie pamici ____________________________________________________ 248
Epoka niewygodnych segmentw _________________________________________________ 248 Paski model pamici ___________________________________________________________ 249
Wskaniki na zmienne _______________________________________________________ 250 Uywanie wskanikw na zmienne____________________________________________ 250 Deklaracje wskanikw __________________________________________________ 252
Nieodaowany spr o gwiazdk __________________________________________________ Wskaniki do staych ___________________________________________________________ Stae wskaniki _______________________________________________________________ Podsumowanie deklaracji wskanikw _____________________________________________ 252 253 254 255
Dane otrzymywane poprzez parametry_____________________________________________ 265 Zapobiegamy niepotrzebnemu kopiowaniu __________________________________________ 266
Dynamiczna alokacja pamici _______________________________________________ 268 Przydzielanie pamici dla zmiennych ________________________________________ 268
Alokacja przy pomocy new_______________________________________________________ 268 Zwalnianie pamici przy pomocy delete ___________________________________________ 269 Nowe jest lepsze ______________________________________________________________ 270
8
Tablice jednowymiarowe ________________________________________________________ 271 Opakowanie w klas ___________________________________________________________ 272 Tablice wielowymiarowe ________________________________________________________ 274
Wskaniki do funkcji _________________________________________________________ 281 Cechy charakterystyczne funkcji _____________________________________________ 282 Typ wartoci zwracanej przez funkcj _______________________________________ 282
Instrukcja czy wyraenie________________________________________________________ 283 O czym mwi konwencja wywoania? ______________________________________________ 284 Typowe konwencje wywoania ___________________________________________________ 285
Parametry funkcji _______________________________________________________ 286 Uywanie wskanikw do funkcji _____________________________________________ 287 Typy wskanikw do funkcji_______________________________________________ 287
Wasnoci wyrniajce funkcj __________________________________________________ 287 Typ wskanika do funkcji _______________________________________________________ 287
Zastosowania __________________________________________________________ Podsumowanie _____________________________________________________________ Pytania i zadania _________________________________________________________ Pytania _______________________________________________________________ wiczenia _____________________________________________________________
Preprocesor a reszta kodu ________________________________________________ Makra ____________________________________________________________________ Proste makra ____________________________________________________________ Definiowianie prostych makr ______________________________________________
Zastpowanie wikszych fragmentw kodu _________________________________________ 308 W kilku linijkach ____________________________________________________________ 309 Makra korzystajce z innych makr ________________________________________________ 309 Makra nie s zmiennymi ________________________________________________________ Zasig ____________________________________________________________________ Miejsce w pamici i adres _____________________________________________________ Typ ______________________________________________________________________ Efekty skadniowe _____________________________________________________________ rednik ___________________________________________________________________ Nawiasy i priorytety operatorw________________________________________________ Dygresja: odpowied na pytanie o sens ycia _____________________________________
9
Nazwa pliku z kodem ________________________________________________________ Dyrektywa #line ___________________________________________________________ Data i czas___________________________________________________________________ Czas kompilacji_____________________________________________________________ Czas modyfikacji pliku _______________________________________________________ Typ kompilatora ______________________________________________________________ Inne nazwy __________________________________________________________________ 315 315 315 315 316 316 316
Kontrola procesu kompilacji ___________________________________________________ Dyrektywy #ifdef i #ifndef ________________________________________________ Puste makra ___________________________________________________________ Dyrektywa #ifdef ______________________________________________________ Dyrektywa #ifndef _____________________________________________________ Dyrektywa #else _______________________________________________________ Dyrektywa warunkowa #if _________________________________________________ Konstruowanie warunkw ________________________________________________
Skomplikowane warunki kompilacji _________________________________________ 329 Dyrektywa #error ______________________________________________________ Reszta dobroci _____________________________________________________________ Doczanie plikw _________________________________________________________ Dwa warianty #include __________________________________________________
Zagniedanie dyrektyw ________________________________________________________ 329 Dyrektywa #elif______________________________________________________________ 330
Z nawiasami ostrymi ___________________________________________________________ Z cudzysowami_______________________________________________________________ Ktry wybra? ________________________________________________________________ Nasz czy biblioteczny ________________________________________________________ cieki wzgldne____________________________________________________________
Polecenia zalene od kompilatora ____________________________________________ 334 Dyrektywa #pragma _____________________________________________________ 334 Waniejsze parametry #pragma w Visual C++ .NET ____________________________ 334
Komunikaty kompilacji _________________________________________________________ message __________________________________________________________________ deprecated _______________________________________________________________ warning __________________________________________________________________ Funkcje inline ________________________________________________________________ auto_inline ______________________________________________________________ inline_recursion __________________________________________________________ inline_depth______________________________________________________________ 335 335 336 336 337 337 338 338
10
Inne________________________________________________________________________ 339 comment __________________________________________________________________ 339 once _____________________________________________________________________ 339
Funkcja zaprzyjaniona nie jest metod ____________________________________________ Deklaracja przyjani jest te deklaracj funkcji ______________________________________ Deklaracja przyjani jako prototyp funkcji ________________________________________ Dodajemy definicj __________________________________________________________
Klasy zaprzyjanione ______________________________________________________ Przyja z pojedynczymi metodami _________________________________________ Przyja z ca klas ____________________________________________________ Jeszcze kilka uwag ________________________________________________________ Cechy przyjani klas w C++ ______________________________________________
Przyja nie jest automatycznie wzajemna__________________________________________ 351 Przyja nie jest przechodnia ____________________________________________________ 351 Przyja nie jest dziedziczna_____________________________________________________ 351
Konstruktory w szczegach ___________________________________________________ 352 Maa powtrka ___________________________________________________________ 352 Konstruktory __________________________________________________________ 353
Cechy konstruktorw___________________________________________________________ Definiowanie _________________________________________________________________ Przecianie _______________________________________________________________ Konstruktor domylny _______________________________________________________ Kiedy wywoywany jest konstruktor _______________________________________________ Niejawne wywoanie _________________________________________________________ Jawne wywoanie ___________________________________________________________ Kiedy si odbywa _____________________________________________________________ Jak wyglda__________________________________________________________________ Inicjalizacja typw podstawowych ______________________________________________ Agregaty __________________________________________________________________ Inicjalizacja konstruktorem ___________________________________________________ 353 353 353 354 355 355 355 355 355 356 356 356
Listy inicjalizacyjne________________________________________________________ Inicjalizacja skadowych __________________________________________________ Wywoanie konstruktora klasy bazowej ______________________________________ Konstruktory kopiujce ____________________________________________________ O kopiowaniu obiektw __________________________________________________
Pole po polu__________________________________________________________________ 360 Gdy to nie wystarcza _________________________________________________________ 360 Do czego suy konstruktor kopiujcy ______________________________________________ Konstruktor kopiujcy a przypisanie - rnica maa lecz wana________________________ Dlaczego konstruktor kopiujcy ________________________________________________ Definiowanie konstruktora kopiujcego ____________________________________________ Inicjalizator klasy CIntArray __________________________________________________
11
Cechy operatorw ________________________________________________________ 371 Liczba argumentw _____________________________________________________ 371
Operatory jednoargumentowe____________________________________________________ 372 Operatory dwuargumentowe_____________________________________________________ 372
Priorytet ______________________________________________________________ czno ______________________________________________________________ Operatory w C++ _________________________________________________________ Operatory arytmetyczne _________________________________________________
Unarne operatory arytmetyczne __________________________________________________ 374 Inkrementacja i dekrementacja ________________________________________________ 374 Binarne operatory arytmetyczne __________________________________________________ 375
Operacje logiczno-bitowe _______________________________________________________ 375 Przesunicie bitowe ____________________________________________________________ 376 Operatory strumieniowe ______________________________________________________ 376
Operatory porwnania ___________________________________________________ 376 Operatory logiczne ______________________________________________________ 377 Operatory przypisania ___________________________________________________ 377
Zwyky operator przypisania _____________________________________________________ L-warto i r-warto ________________________________________________________ Rezultat przypisania _________________________________________________________ Uwaga na przypisanie w miejscu rwnoci ________________________________________ Zoone operatory przypisania ___________________________________________________
Wyuskanie z obiektu __________________________________________________________ 387 Wyuskanie ze wskanika _______________________________________________________ 388 Operator zasigu ______________________________________________________________ 388 Nawiasy okrge ______________________________________________________________ 388 Operator warunkowy ___________________________________________________________ 389 Przecinek ____________________________________________________________________ 389
Nowe znaczenia dla operatorw ______________________________________________ 389 Funkcje operatorowe ____________________________________________________ 389
Kilka uwag wstpnych __________________________________________________________ Oglna skadnia funkcji operatorowej____________________________________________ Operatory, ktre moemy przecia ____________________________________________ Czego nie moemy zmieni ___________________________________________________ Pozostae sprawy ___________________________________________________________ Definiowanie przecionych wersji operatorw _______________________________________ Operator jako funkcja skadowa klasy ___________________________________________ Problem przemiennoci_______________________________________________________ Operator jako zwyka funkcja globalna___________________________________________ Operator jako zaprzyjaniona funkcja globalna ____________________________________
390 390 390 391 391 391 392 393 393 394
12
Typowe operatory jednoargumentowe ___________________________________________ Inkrementacja i dekrementacja ________________________________________________ Typowe operatory dwuargumentowe ____________________________________________ Operatory przypisania _______________________________________________________ Operator indeksowania _________________________________________________________ Operatory wyuskania __________________________________________________________ Operator -> _______________________________________________________________ Ciekawostka: operator ->*____________________________________________________ Operator wywoania funkcji______________________________________________________ Operatory zarzdzania pamici __________________________________________________ Lokalne wersje operatorw____________________________________________________ Globalna redefinicja _________________________________________________________ Operatory konwersji ___________________________________________________________ Zachowujmy sens, logik i konwencj _____________________________________________ Symbole operatorw powinny odpowiada ich znaczeniom ___________________________ Zapewnijmy analogiczne zachowania jak dla typw wbudowanych _____________________ Nie przeciajmy wszystkiego ____________________________________________________ 395 396 397 399 402 404 404 406 407 409 410 411 412 412 412 413 413
Wskaniki do skadowych klasy ________________________________________________ 414 Wskanik na pole klasy ____________________________________________________ 414 Wskanik do pola wewntrz obiektu ________________________________________ 414
Wskanik na obiekt ____________________________________________________________ 414 Pokazujemy na skadnik obiektu __________________________________________________ 415 Miejsce pola w definicji klasy ____________________________________________________ Pobieranie wskanika __________________________________________________________ Deklaracja wskanika na pole klasy _______________________________________________ Uycie wskanika _____________________________________________________________
Wskanik na metod klasy __________________________________________________ 420 Wskanik do statycznej metody klasy _______________________________________ 420 Wskanik do niestatycznej metody klasy_____________________________________ 421
Wykorzystanie wskanikw na metody _____________________________________________ Deklaracja wskanika ________________________________________________________ Pobranie wskanika na metod klasy ____________________________________________ Uycie wskanika ___________________________________________________________ Ciekawostka: wskanik do metody obiektu__________________________________________ Wskanik na metod obiektu konkretnej klasy ____________________________________ Wskanik na metod obiektu dowolnej klasy ______________________________________ 421 421 423 423 423 425 427
Wyjtki______________________________________________________ 433
Mechanizm wyjtkw w C++ __________________________________________________ 433 Tradycyjne metody obsugi bdw ___________________________________________ 434 Dopuszczalne sposoby ___________________________________________________ 434
Zwracanie nietypowego wyniku __________________________________________________ Specjalny rezultat___________________________________________________________ Wady tego rozwizania_______________________________________________________ Oddzielenie rezultatu od informacji o bdzie ________________________________________ Wykorzystanie wskanikw ___________________________________________________ Uycie struktury ____________________________________________________________ Wywoanie zwrotne ____________________________________________________________ Uwaga o wygodnictwie _______________________________________________________ Uwaga o logice _____________________________________________________________ Uwaga o niedostatku mechanizmw_____________________________________________ Zakoczenie programu _________________________________________________________
434 435 435 436 436 437 438 438 439 439 439
13
Szczegy przodem __________________________________________________________ Zagniedone bloki try-catch ___________________________________________________ Zapanie i odrzucenie ________________________________________________________ Blok catch(...), czyli chwytanie wszystkiego ____________________________________ 445 446 448 448
Odwijanie stosu ____________________________________________________________ 449 Midzy rzuceniem a zapaniem_______________________________________________ 449 Wychodzenie na wierzch _________________________________________________ 449
Porwnanie throw z break i return _______________________________________________ Wyjtek opuszcza funkcj _______________________________________________________ Specyfikacja wyjtkw _______________________________________________________ Kamstwo nie popaca ________________________________________________________ Niezapany wyjtek ____________________________________________________________ Niszczenie obiektw lokalnych ___________________________________________________ Wypadki przy transporcie _______________________________________________________ Niedozwolone rzucenie wyjtku ________________________________________________ Strefy bezwyjtkowe ________________________________________________________ Skutki wypadku ____________________________________________________________ 450 450 451 451 453 454 454 454 455 456
Wykorzystanie wyjtkw _____________________________________________________ 463 Wyjtki w praktyce________________________________________________________ 463 Projektowanie klas wyjtkw ______________________________________________ 464
Definiujemy klas _____________________________________________________________ 464 Hierarchia wyjtkw ___________________________________________________________ 465 Umiejscowienie blokw try i catch _______________________________________________ Kod warstwowy_____________________________________________________________ Podawanie bdw wyej _____________________________________________________ Dobre wyporodkowanie______________________________________________________ Chwytanie wyjtkw w blokach catch _____________________________________________ Szczegy przodem - druga odsona_____________________________________________ Lepiej referencj____________________________________________________________
Naduywanie wyjtkw __________________________________________________ 471 Podsumowanie _____________________________________________________________ Pytania i zadania _________________________________________________________ Pytania _______________________________________________________________ wiczenia _____________________________________________________________ 472 472 472 472
Kod niezaleny od typu _________________________________________________________ 476 Kompilator to potrafi ___________________________________________________________ 476 Skadnia szablonu___________________________________________________________ 476
14
Co moe by szablonem ______________________________________________________ 477
497 497 498 500 501 501 503 504 504 504 506 506 508
Wicej informacji ___________________________________________________________ 508 Parametry szablonw ______________________________________________________ 508 Typy _________________________________________________________________ 509 Stae _________________________________________________________________ 510
Uycie parametrw pozatypowych ________________________________________________ Przykad szablonu klasy ______________________________________________________ Przykad szablonu funkcji _____________________________________________________ Dwie wartoci, dwa rne typy_________________________________________________ Ograniczenia dla parametrw pozatypowych ________________________________________ Wskaniki jako parametry szablonu _____________________________________________ Inne restrykcje _____________________________________________________________ Przypominamy banalny przykad__________________________________________________ 509 class zamiast typename ________________________________________________________ 510
510 511 512 512 513 513 514 514 515 516 516
15
Sowo kluczowe typename ____________________________________________________ 520 Ciekawostka: konstrukcje ::template, .template i ->template ______________________ 521 522 523 523 524 525 525 525 526 526 527 527
Zastosowania szablonw _____________________________________________________ Zastpienie makrodefinicji __________________________________________________ Szablon funkcji i makro __________________________________________________ Pojedynek na szczycie ___________________________________________________
Starcie drugie: problem dopasowania tudzie wydajnoci ______________________________ Jak zadziaa szablon _________________________________________________________ Jak zadziaa makro __________________________________________________________ Wynik ____________________________________________________________________ Starcie trzecie: problem rozwinicia albo poprawnoci _________________________________ Jak zadziaa szablon _________________________________________________________ Jak zadziaa makro __________________________________________________________ Wynik ____________________________________________________________________ Konkluzje____________________________________________________________________
1
P ODSTAWY
PROGRAMOWANIA
1
KRTKO O PROGRAMOWANIU
Programy nie spadaj z nieba, najpierw tym niebem potrz trzeba.
gemGreg
Rozpoczynamy zatem nasz kurs programowania gier. Zanim jednak napiszesz swojego wasnego Quakea, Warcrafta czy te inny wielki przebj, musisz nauczy si tworzenia programw (gry to przecie te programy, prawda?) czyli programowania. Jeszcze niedawno czynno ta bya traktowana na poy mistycznie: oto bowiem programista (czytaj jajogowy) wpisuje jakie dziwne cigi liter i numerkw, a potem w niemal magiczny sposb zamienia je w edytor tekstu, kalkulator czy wreszcie gr. Obecnie obraz ten nie przystaje ju tak bardzo do rzeczywistoci, a tworzenie programw jest prostsze ni jeszcze kilkanacie lat temu. Nadal jednak wiele zaley od umiejtnoci samego kodera oraz jego dowiadczenia, a zyskiwanie tyche jest kwesti dugiej pracy i realizacji wielu projektw. Nagrod za ten wysiek jest moliwo urzeczywistnienia dowolnego praktycznie pomysu i wielka satysfakcja. Czas wic przyjrze si, jak powstaj programy.
Krok za krokiem
Wikszo aplikacji zostaa stworzona do realizacji jednego, konkretnego, cho obszernego zadania. Przykadowo, Notatnik potrafi edytowa pliki tekstowe, Winamp odtwarza muzyk, a Paint tworzy rysunki.
Moemy wic powiedzie, e gwn funkcj kadego z tych programw bdzie odpowiednio edycja plikw tekstowych, odtwarzanie muzyki czy tworzenie rysunkw. Funkcj t mona jednak podzieli na mniejsze, bardziej szczegowe. I tak Notatnik potrafi otwiera i zapisywa pliki, drukowa je i wyszukiwa w nich tekst. Winamp za pozwala nie tylko odtwarza utwory, ale te ukada z nich playlisty.
20
Podstawy programowania
Idc dalej, moemy dotrze do nastpnych, coraz bardziej szczegowych funkcji danego programu. Przypominaj one wic co w rodzaju drzewka, ktre pozwala nam niejako rozoy dan aplikacj na czci.
Edycja plikw tekstowych Otwieranie i zapisywanie plikw Otwieranie plikw Zapisywanie plikw Drukowanie plikw Wyszukiwanie i zamiana tekstu Wyszukiwanie tekstu w pliku Zamiana danego tekstu na inny
Schemat 1. Podzia programu Notatnik na funkcje skadowe
Zastanawiasz si pewnie, na jak drobne czci moemy w ten sposb dzieli programy. Innymi sowy, czy dojdziemy wreszcie do takiego elementu, ktry nie da si rozdzieli na mniejsze. Spiesz z odpowiedzi, i oczywicie tak w przypadku Notatnika bylimy zreszt bardzo blisko. Czynno zatytuowana Otwieranie plikw wydaje si by ju jasno okrelona. Kiedy wybieramy z menu Plik programu pozycj Otwrz, Notatnik robi kilka rzeczy: najpierw pokazuje nam okno wyboru pliku. Gdy ju zdecydujemy si na jaki, pyta nas, czy chcemy zachowa zmiany w ju otwartym dokumencie (jeeli jakiekolwiek zmiany rzeczywicie poczynilimy). W przypadku, gdy je zapiszemy w innym pliku lub odrzucimy, program przystpi do odczytania zawartoci danego przez nas dokumentu i wywietli go na ekranie. Proste, prawda? :) Przedstawiona powyej charakterystyka czynnoci otwierania pliku posiada kilka znaczcych cech: okrela dokadnie kolejne kroki wykonywane przez program wskazuje rne moliwe warianty sytuacji i dla kadego z nich przewiduje odpowiedni reakcj Pozwalaj one nazwa niniejszy opis algorytmem. Algorytm to jednoznacznie okrelony sposb, w jaki program komputerowy realizuje jak elementarn czynno.1 Jest to bardzo wane pojcie. Myl o algorytmie jako o czym w rodzaju przepisu albo instrukcji, ktra mwi aplikacji, co ma zrobi gdy napotka tak czy inn sytuacj. Dziki swoim algorytmom programy wiedz co zrobi po naciniciu przycisku myszki, jak zapisa, otworzy czy wydrukowa plik, jak wywietli poprawnie stron WWW, jak odtworzy utwr w formacie MP3, jak rozpakowa archiwum ZIP i oglnie jak wykonywa zadania, do ktrych zostay stworzone. Jeli nie podoba ci si, i cay czas mwimy o programach uytkowych zamiast o grach, to wiedz, e gry take dziaaj w oparciu o algorytmy. Najczciej s one nawet znacznie
1 Nie jest to cisa matematyczna definicja algorytmu, ale na potrzeby programistyczne nadaje si bardzo dobrze :)
Krtko o programowaniu
bardziej skomplikowane od tych wystpujcych w uywanych na co dzie aplikacjach. Czy nie atwiej narysowa prost tabelk z liczbami ni skomplikowan scen trjwymiarow? :) Z tego wanie powodu wymylanie algorytmw jest wan czci pracy twrcy programw, czyli programisty. Wanie t drog koder okrela sposb dziaania (zachowanie) pisanego programu.
21
Podsumujmy: w kadej aplikacji moemy wskaza wykonywane przez ni czynnoci, ktre z kolei skadaj si z mniejszych etapw, a te jeszcze z mniejszych itd. Zadania te realizowane s poprzez algorytmy, czyli przepisy okrelone przez programistw twrcw programw.
Jak poradzi sobie z tym, zdawaoby si nierozwizalnym, problemem? Jak radz sobie wszyscy twrcy oprogramowania, skoro budujc swoje programy musz przecie rozmawia z komputerem? Poniewa nie moemy peceta nauczy naszego wasnego jzyka i jednoczenie sami nie potrafimy porozumie si z nim w jego mowie, musimy zastosowa rozwizanie kompromisowe. Na pocztek ucilimy wic i przejrzycie zorganizujemy nasz opis algorytmw. W przypadku otwierania plikw w Notatniku moe to wyglda na przykad tak: Algorytm Plik -> Otwrz Poka okno wyboru plikw Jeeli uytkownik klikn Anuluj, To Przerwij
22
Podstawy programowania
Jeeli poczyniono zmiany w aktualnym dokumencie, To Wywietl komunikat "Czy zachowa zmiany w aktualnym dokumencie?" z przyciskami Tak, Nie, Anuluj Sprawd decyzj uytkownika Decyzja Tak: wywoaj polecenie Plik -> Zapisz Decyzja Anuluj: Przerwij Odczytaj wybrany plik Wywietl zawarto pliku Koniec Algorytmu Jak wida, sprecyzowalimy tu kolejne kroki wykonywane przez program tak aby wiedzia, co naley po kolei zrobi. Fragmenty zaczynajce si od Jeeli i Sprawd pozwalaj odpowiednio reagowa na rne sytuacje, takie jak zmiana decyzji uytkownika i wcinicie przycisku Anuluj. Czy to wystarczy, by komputer wykona to, co mu kaemy? Ot nie bardzo Chocia wprowadzilimy ju nieco porzdku, nadal uywamy jzyka naturalnego jedynie struktura zapisu jest bardziej cisa. Notacja taka, zwana pseudokodem, przydaje si jednak bardzo do przedstawiania algorytmw w czytelnej postaci. Jest znacznie bardziej przejrzysta oraz wygodniejsza ni opis w formie zwykych zda, ktre musiayby by najczciej wielokrotnie zoone i niezbyt poprawne gramatycznie. Dlatego te, kiedy bdziesz wymyla wasne algorytmy, staraj si uywa pseudokodu do zapisywania ich oglnego dziaania. No dobrze, wyglda to cakiem niele, jednak nadal nie potrafimy si porozumie z tym mao inteligentnym stworem, jakim jest nasz komputer. Wszystko dlatego, i nie wie on, w jaki sposb przetworzy nasz algorytm, napisany w powstaym ad hoc jzyku, do postaci zrozumiaych dla niego krzaczkw, ktre widziae wczeniej. Dla rozwizania tego problemu stworzono sztuczne jzyki o dokadnie okrelonej skadni i znaczeniu, ktre dziki odpowiednim narzdziom mog by zamieniane na kod binarny, czyli form zrozumia dla komputera. Nazywamy je jzykami programowania i to wanie one su do tworzenia programw komputerowych. Wiesz ju zatem, czego najpierw musisz si nauczy :) Jzyk programowania to forma zapisu instrukcji dla komputera i programw komputerowych, porednia midzy jzykiem naturalnym a kodem maszynowym. Program zapisany w jzyku programowania jest, podobnie jak nasz algorytm w pseudokodzie, zwykym tekstem. Podobiestwo tkwi rwnie w fakcie, e sam taki tekst nie wystarczy, aby napisan aplikacj uruchomi najpierw naley j zamieni w plik wykonywalny (w systemie Windows s to pliki z rozszerzeniem EXE). Czynno ta jest dokonywana w dwch etapach. Podczas pierwszego, zwanego kompilacj, program nazywany kompilatorem zamienia instrukcje jzyka programowania (czyli kod rdowy, ktry, jak ju mwilimy, jest po prostu tekstem) w kod maszynowy (binarny). Zazwyczaj na kady plik z kodem rdowym (zwany moduem) przypada jeden plik z kodem maszynowym. Kompilator program zamieniajcy kod rdowy, napisany w jednym z jzykw programowania, na kod maszynowy w postaci oddzielnych moduw. Drugi etap to linkowanie (zwane te konsolidacj lub po prostu czeniem). Jest to budowanie gotowego pliku EXE ze skompilowanych wczeniej moduw. Oprcz nich mog tam zosta wczone take inne dane, np. ikony czy kursory. Czyni to program zwany linkerem.
Krtko o programowaniu
Linker czy skompilowane moduy kodu i inne pliki w jeden plik wykonywalny, czyli program (w przypadku Windows plik EXE). Tak oto zdjlimy nimb magii z procesu tworzenia programu ;D
23
Skoro kompilacja i linkowanie s przeprowadzane automatycznie, a programista musi jedynie wyda polecenie rozpoczcia tego procesu, to dlaczego nie pj dalej niech komputer na bieco tumaczy sobie program na swj kod maszynowy. Rzeczywicie, jest to moliwe powstao nawet kilka jzykw programowania dziaajcych w ten sposb (tak zwanych jzykw interpretowanych, przykadem jest choby PHP, sucy do tworzenia stron internetowych). Jednake ogromna wikszo programw jest nadal tworzona w tradycyjny sposb. Dlaczego? C jeeli w programowaniu nie wiadomo, o co chodzi, to na pewno chodzi o wydajno2 ;)) Kompilacja i linkowanie trwa po prostu dugo, od kilkudziesiciu sekund w przypadku niewielkich programw, do nawet kilkudziesiciu minut przy duych. Lepiej zrobi to raz i uywa szybkiej, gotowej aplikacji ni nie robi w ogle i czeka dwie minuty na rozwinicie menu podrcznego :DD Zatem czas na konkluzj i usystematyzowanie zdobytej wiedzy. Programy piszemy w jzykach programowania, ktre s niejako form komunikacji z komputerem i wydawania mu polece. S one nastpnie poddawane procesom kompilacji i konsolidacji, ktre zamieniaj zapis tekstowy w binarny kod maszynowy. W wyniku tych czynnoci powstaje gotowy plik wykonywalny, ktry pozwala uruchomi program.
Jzyki programowania
Przegld najwaniejszych jzykw programowania
Obecnie istnieje bardzo, bardzo wiele jzykw programowania. Niektre przeznaczono do konkretnych zastosowa, na przykad sieci neuronowych, inne za s narzdziami oglnego przeznaczenia. Zazwyczaj wiksze korzyci zajmuje znajomo tych drugich, dlatego nimi wanie si zajmiemy. Od razu musz zaznaczy, e mimo to nie ma czego takiego jak jzyk, ktry bdzie dobry do wszystkiego. Spord jzykw oglnych niektre s nastawione na szybko, inne na rozmiar kodu, jeszcze inne na przejrzysto itp. Jednym sowem, panuje totalny rozgardiasz ;) Naley koniecznie odrnia jzyki programowania od innych jzykw uywanych w informatyce. Na przykad HTML jest jzykiem opisu, gdy za jego pomoc definiujemy jedynie wygld stron WWW (wszelkie interaktywne akcje to ju domena JavaScriptu). Inny rodzaj to jzyki zapyta w rodzaju SQL, suce do pobierania danych z rnych rde (na przykad baz danych). Niepoprawne jest wic (popularne skdind) stwierdzenie programowa w HTML. Przyjrzyjmy si wic najwaniejszym uywanym obecnie jzykom programowania: 1. Visual Basic Jest to nastpca popularnego swego czasu jzyka BASIC. Zgodnie z nazw (basic znaczy prosty), by on przede wszystkim atwy do nauki. Visual Basic pozwala na tworzenie programw dla rodowiska Windows w sposb wizualny, tzn. poprzez konstruowanie okien z takich elementw jak przyciski czy pola tekstowe. Jzyk ten posiada dosy spore moliwoci, jednak ma rwnie jedn, za to bardzo
2
24
Podstawy programowania
powan wad. Programy w nim napisane nie s kompilowane w caoci do kodu maszynowego, ale interpretowane podczas dziaania. Z tego powodu s znacznie wolniejsze od tych kompilowanych cakowicie. Obecnie Visual Basic jest jednym z jzykw, ktry umoliwia tworzenie aplikacji pod lansowan przez Microsoft platform .NET, wic pewnie jeszcze o nim usyszymy :)
2. Object Pascal (Delphi) Delphi z kolei wywodzi si od popularnego jzyka Pascal. Podobnie jak VB jest atwy do nauczenia, jednake oferuje znacznie wiksze moliwoci zarwno jako jzyk programowania, jak i narzdzie do tworzenia aplikacji. Jest cakowicie kompilowany, wic dziaa tak szybko, jak to tylko moliwe. Posiada rwnie moliwo wizualnego konstruowania okien. Dziki temu jest to obecnie chyba najlepsze rodowisko do budowania programw uytkowych.
3. C++ C++ jest teraz chyba najpopularniejszym jzykiem do zastosowa wszelakich. Powstao do niego bardzo wiele kompilatorw pod rne systemy operacyjne i dlatego jest uwaany za najbardziej przenony. Istnieje jednak druga strona medalu mnogo tych narzdzi prowadzi do niewielkiego rozgardiaszu i pewnych
Krtko o programowaniu
25
trudnoci w wyborze ktrego z nich. Na szczcie sam jzyk zosta w 1997 roku ostatecznie ustandaryzowany. O C++ nie mwi si zwykle, e jest atwy by moe ze wzgldu na dosy skondensowan skadni (na przykad odpowiednikiem pascalowych sw begin i end s po prostu nawiasy klamrowe { i }). To jednak dosy powierzchowne przekonanie, a sam jzyk jest spjny i logiczny. Jeeli chodzi o moliwoci, to w przypadku C++ s one bardzo due w sumie mona powiedzie, e nieco wiksze ni Delphi. Jest on te chyba najbardziej elastyczny niejako dopasowuje si do preferencji programisty. 4. Java Ostatnimi czasy Java staa si niemal czci kultury masowej wystarczy choby wspomnie o telefonach komrkowych i przeznaczonych do aplikacjach. Ilustruje to dobrze gwny cel Javy, a mianowicie przenono i to nie kodu, lecz skompilowanych programw! Osignito to poprzez kompilacj do tzw. bytecode, ktry jest wykonywany w ramach specjalnej maszyny wirtualnej. W ten sposb, program w Javie moe by uruchamiany na kadej platformie, do ktrej istnieje maszyna wirtualna Javy a istnieje prawie na wszystkich, od Windowsa przez Linux, OS/2, QNX, BeOS, palmtopy czy wreszcie nawet telefony komrkowe. Z tego wanie powodu Java jest wykorzystywana do pisania niewielkich programw umieszczanych na stronach WWW, tak zwanych apletw. Cen za t przenono jest rzecz jasna szybko bytecode Javy dziaa znacznie wolniej ni zwyky kod maszynowy, w dodatku jest strasznie pamicioerny. Poniewa jednak zastosowaniem tego jzyka nie s wielkie i wymagajce aplikacje, lecz proste programy, nie jest to a tak wielki mankament. Skadniowo Java bardzo przypomina C++.
5. PHP PHP (skrt od Hypertext Preprocessor) jest jzykiem uywanym przede wszystkim w zastosowaniach internetowych, dokadniej na stronach WWW. Pozwala doda im znacznie wiksz funkcjonalno ni ta oferowana przez zwyky HTML. Obecnie miliony serwisw wykorzystuje PHP du rol w tym sukcesie ma zapewne jego licencja, oparta na zasadach Open Source (czyli brak ogranicze w rozprowadzaniu i modyfikacji). Moliwoci PHP s cakiem due, nie mona tego jednak powiedzie o szybkoci jest to jzyk interpretowany. Jednake w przypadku gwnego zastosowania PHP, czyli obsudze serwisw internetowych, nie ma ona wikszego znaczenia czas
26
Podstawy programowania
wczytywania strony WWW to przecie w wikszoci czas przesyania gotowego kodu HTML od serwera do odbiorcy. Jeeli chodzi o skadni, to troch przypomina ona C++. Kod PHP mona jednak swobodnie przeplata znacznikami HTML. Z punktu widzenia programisty gier jzyk ten jest w zasadzie zupenie bezuyteczny (chyba e kiedy sam bdziesz wykonywa oficjaln stron internetow swojej wielkiej produkcji ;D), wspominam o nim jednak ze wzgldu na bardzo szerokie grono uytkownikw, co czyni go jednym z waniejszych jzykw programowania.
Screen 6. Popularny skrypt forw dyskusyjnych, phpBB, take dziaa w oparciu o PHP
To oczywicie nie wszystkie jzyki jak ju pisaem, jest ich cae mnstwo. Jednake w ogromnej wikszoci przypadkw gwn rnic midzy nimi jest skadnia, a wic sprawa mao istotna (szczeglnie, jeeli dysponuje si dobr dokumentacj :D). Z tego powodu poznanie jednego z nich bardzo uatwia nauk nastpnych po prostu im wicej jzykw ju znasz, tym atwiej uczysz si nastpnych :)
Krtko o programowaniu
27
Czybymy mieli zatem remis, a prawda leaa (jak zwykle) porodku? :) Ot niezupenie nie uwzgldnilimy bowiem wanego czynnika, jakim jest popularno danego jzyka. Jeeli jest on szeroko znany i uywany (do programowania gier), to z pewnoci istnieje o nim wicej przydatnych rde informacji, z ktrych mgby korzysta. Z tego wanie powodu Delphi jest gorszym wyborem, poniewa ogromna wikszo dokumentacji, artykuw, kursw itp. dotyczy jzyka C++. Wystarczy chociaby wspomnie, i Microsoft nie dostarcza narzdzi pozwalajcych na wykorzystanie DirectX w Delphi s one tworzone przez niezalene zespoy3 i ich uywanie wymaga pewnego dowiadczenia. A wic C++! Jzyk ten wydaje si najlepszym wyborem, jeeli chodzi o programowanie gier komputerowych. A skoro mamy ju t wan decyzj za sob, zostaa nam jeszcze tylko pewna drobnostka trzeba si tego jzyka nauczy :))
Kwestia kompilatora
Jak ju wspominaem kilkakrotnie, C++ jest bardzo przenonym jzykiem, umoliwiajcym tworzenie aplikacji na rnych platformach sprztowych i programowych. Z tego powodu istnieje do niego cae mnstwo kompilatorw. Ale kompilator to tylko program do zamiany kodu C++ na kod maszynowy w dodatku dziaa on zwykle w trybie wiersza polece, a wic nie jest zbyt wygodny w uyciu. Dlatego rwnie wane jest rodowisko programistyczne, ktre umoliwiaoby wygodne pisanie kodu, zarzdzanie caymi projektami i uatwiaoby kompilacj. rodowisko programistyczne (ang. integrated development environment w skrcie IDE) to pakiet aplikacji uatwiajcych tworzenie programw w danym jzyku programowania. Umoliwia najczciej organizowanie plikw z kodem w projekty, atw kompilacj, czasem te wizualne tworzenie okien dialogowych. Popularnie, rodowisko programistyczne nazywa si po prostu kompilatorem (gdy jest jego gwn czci). Przykady takich rodowisk zaprezentowaem na screenach przy okazji przegldu jzykw programowania. Nietrudno si domyle, i dla C++ rwnie przewidziano takie narzdzia. W przypadku rodowiska Windows, ktre rzecz jasna interesuje nas najbardziej, mamy ich kilka: 1. Bloodshed Dev-C++ Pakiet ten ma niewtpliw zalet jest darmowy do wszelakich zastosowa, take komercyjnych. Niestety zdaje si, e na tym jego zalety si kocz :) Posiada wprawdzie cakiem wygodne IDE, ale nie moe si rwna z profesjonalnymi narzdziami: nie posiada na przykad moliwoci edycji zasobw (ikon, kursorw itd.) Mona go znale na stronie producenta. 2. Borland C++Builder Z wygldu bardzo przypomina Delphi oczywicie poza zastosowanym jzykiem programowania, ktrym jest C++. Niemniej, tak samo jak swj kuzyn jest on przeznaczony gwnie do tworzenia aplikacji uytkowych, wic nie odpowiadaby nam zbytnio :) 3. Microsoft Visual C++ Poniewa jest to produkt firmy Microsoft, znakomicie integruje si z innym produktem tej firmy, czyli DirectX wobec czego dla nas, (przyszych)
28
Podstawy programowania
programistw gier, wypada bardzo korzystnie. Nic dziwnego zatem, e uywaj go nawet profesjonalni twrcy. Tak jest, dobrze mylisz zalecam Visual C++ :) Warto naladowa najlepszych, a skoro ogromna wikszo komercyjnych gier powstaje przy uyciu tego narzdzia (i to nie tylko w poczeniu z DirectX), musi to chyba znaczy, e faktycznie jest dobre4. Jeeli upierasz si przy innym rodowisku, to pamitaj, e przedstawione przeze mnie opisy niektrych polece i opcji mog nie odpowiada twojemu IDE. W wikszoci nie dotyczy to jednak samego jzyka C++, ktrego skadni i moliwoci zachowuj wszystkie kompilatory. W razie jakichkolwiek kopotw moesz zawsze odwoa si do dokumentacji :)
Podsumowanie
Uff, to ju koniec tego rozdziau :) Zaczlimy go od dokadnego zlustrowania Notatnika i podzieleniu go na drobne czci a doszlimy do algorytmw. Dowiedzielimy si, i to gwnie one skadaj si na gotowy program i e zadaniem programisty jest wanie wymylanie algorytmw. Nastpnie rozwizalimy problem wzajemnego niezrozumienia czowieka i komputera, dziki czemu w przyszoci bdziemy mogli tworzy wasne programy. Poznalimy suce do tego narzdzia, czyli jzyki programowania. Wreszcie, podjlimy (OK, ja podjem :D) wane decyzje, ktre wytyczaj nam kierunek dalszej nauki a wic wybr jzyka C++ oraz rodowiska Visual C++. Nastpny rozdzia jest wcale nie mniej znaczcy, a moe nawet waniejszy. Napiszesz bowiem swj pierwszy program :)
Pytania i zadania
C, prace domowe s nieuniknione :) Odpowiedzenie na ponisze pytania i wykonanie wicze pozwoli ci lepiej zrozumie i zapamita informacje z tego rozdziau.
Pytania
1. Dlaczego jzyki interpretowane s wolniejsze od kompilowanych?
wiczenia
1. Wybierz dowolny program i sprbuj nazwa jego gwn funkcj. Postaraj si te wyrni te bardziej szczegowe. 2. Zapisz w postaci pseudokodu algorytm parzenia herbaty :D Pamitaj o uwzgldnieniu takich sytuacji jak: peny/pusty czajnik, brak zapaek lub zapalniczki czy brak herbaty
Wiem, e dla niektrych pojcia dobry produkt i Microsoft wzajemnie si wykluczaj, ale akurat w tym przypadku wcale tak nie jest :)
2
Z CZEGO SKADA SI PROGRAM?
Kady dziaajcy program jest przestarzay.
pierwsze prawo Murphyego o oprogramowaniu
Gdy mamy ju przyswojon niezbdn dawk teoretycznej i pseudopraktycznej wiedzy, moemy przej od sw do czynw :) W aktualnym rozdziale zapoznamy si z podstawami jzyka C++, ktre pozwol nam opanowa umiejtno tworzenia aplikacji w tym jzyku. Napiszemy wic swj pierwszy program (drugi, trzeci i czwarty zreszt te :D), zaznajomimy si z podstawowymi pojciami uywanymi w programowaniu i zdobdziemy gar niezbdnej wiedzy :)
30
Podstawy programowania
Goy kompilator jest tylko maszynk zamieniajc kod C++ na kod maszynowy, dziaajc na zasadzie ty mi podajesz plik ze swoim kodem, a ja go kompiluj. Gdy uwiadomimy sobie, e przecitny program skada si z kilku(nastu) plikw kodu rdowego, z ktrych kady naleaoby kompilowa oddzielnie i wreszcie linkowa je w jeden plik wykonywalny, docenimy zawarte w rodowiskach programistycznych mechanizmy zarzdzania projektami. Projekt w rodowiskach programistycznych to zbir moduw kodu rdowego i innych plikw, ktre po kompilacji i linkowaniu staj si pojedynczym plikiem EXE, czyli programem. Do najwaniejszych zalet projektu naley bardzo atwa kompilacja wystarczy wyda jedno polecenie (na przykad wybra opcj z menu), a projekt zostanie automatycznie skompilowany i zlinkowany. Zwaywszy, i tak nie tak dawno temu czynno ta wymagaa wpisania kilkunastu dugich polece lub napisania oddzielnego skryptu, widzimy tutaj duy postp :) Kilka projektw mona pogrupowa w tzw. rozwizania5 (ang. solutions). Przykadowo, jeeli tworzysz gr, do ktrej doczysz edytor etapw, to zasadnicza gra oraz edytor bd oddzielnymi projektami, ale rozsdnie bdzie zorganizowa je w jedno rozwizanie. *** Teraz, gdy wiemy ju sporo na temat sposobu dziaania naszego rodowiska oraz przyczyn, dlaczego uatwia nam ono ycie, przydaoby si je uruchomi uczy wic to niezwocznie. Powiniene zobaczy na przykad taki widok:
C, czas wic co napisa - skoro mamy nauczy si programowania, pisanie programw jest przecie koniecznoci :D Na podstawie tego, co wczeniej napisaem o projektach, nietrudno si domyle, i rozpoczcie pracy nad aplikacj oznacza wanie stworzenie nowego projektu. Robi si to bardzo prosto: najbardziej elementarna metoda to po prostu kliknicie w przycisk New
5
31
Project widoczny na ekranie startowym; mona rwnie uy polecenia File|New|Project z menu. Twoim oczom ukae si wtedy okno zatytuowane, a jake, New Project6 :) Moesz w nim wybra typ projektu my zdecydujemy si oczywicie na Visual C++ oraz Win32 Project, czyli aplikacj Windows.
Nadaj swojemu projektowi jak dobr nazw (chociaby tak, jak na screenie), wybierz dla niego katalog i kliknij OK. Najlepiej jeeli utworzysz sobie specjalny folder na programy, ktre napiszesz podczas tego kursu. Pamitaj, porzdek jest bardzo wany :) Po krtkiej chwili ujrzysz nastpne okno kreator :) Obsesja Microsoftu na ich punkcie jest powszechnie znana, wic nie bd zdziwiony widzc kolejny przejaw ich radosnej twrczoci ;) Tene egzemplarz suy dokadnemu dopasowaniu parametrw projektu do osobistych ycze. Najbardziej interesujca jest dla nas strona zatytuowana Application Settings przecz si zatem do niej.
Rodzaje aplikacji
Skoncentrujemy si przede wszystkim na opcji Application Type, a z kilku dopuszczalnych wariantw wemiemy pod lup dwa: Windows application to zgodnie z nazw aplikacja okienkowa. Skada si z jednego lub kilku okien, zawierajcych przyciski, pola tekstowe, wyboru itp. czyli wszystko to, z czym stykamy si w Windows nieustannie. Console application jest programem innego typu: do komunikacji z uytkownikiem uywa tekstu wypisywanego w konsoli std nazwa. Dzisiaj moe wydawa si to archaizmem, jednak aplikacje konsolowe s szeroko wykorzystywane przez dowiadczonych uytkownikw systemw operacyjnych. Szczeglnie dotyczy to tych z rodziny Unixa, ale w Windows take mog by bardzo przydatne. Programy konsolowe nie s tak efektowne jak ich okienkowi bracia, posiadaj za to bardzo wan dla pocztkujcego programisty cech s proste :) Najprostsza aplikacja tego typu to kwestia kilku linijek kodu, podczas gdy program okienkowy wymaga ich
Analogiczne okno w Visual C++ 6 wygldao zupenie inaczej, jednak ma podobne opcje
32
Podstawy programowania
kilkudziesiciu. Idee dziaania takiego programu s rwnie troch bardziej skomplikowane. Z tych wanie powodw zajmiemy si na razie wycznie aplikacjami konsolowymi pozwol nam w miar atwo nauczy si samego jzyka C++ (co jest przecie naszym aktualnym priorytetem), bez zagbiania si w skomplikowane meandry programowania Windows.
Wybierz wic pozycj Console application na licie Application type. Dodatkowo zaznacz te opcj Empty project spowoduje to utworzenie pustego projektu, a oto nam aktualnie chodzi.
Pierwszy program
Gdy wreszcie ustalimy i zatwierdzimy wszystkie opcje projektu, moemy przystpi do waciwej czci tworzenia programu, czyli kodowania. Aby doda do naszego projektu pusty plik z kodem rdowym, wybierz pozycj menu Project|Add New Item. W okienku, ktre si pojawi, w polu Templates zaznacz ikon C++ File (.cpp), a jako nazw wpisz po prostu main. W ten sposb utworzysz plik main.cpp, ktry wypenimy kodem naszego programu. Plik ten zostanie od razu otwarty, wic moesz bez zwoki wpisa do taki oto kod: // First - pierwszy program w C++ #include <iostream> #include <conio.h> void main() { std::cout << "Hurra! Napisalem pierwszy program w C++!" << std::endl; getch(); } Tak jest, to wszystko te kilka linijek kodu skadaj si na cay nasz program. Pewnie niezbyt wielka to teraz pociecha, bo w kod jest dla ciebie zapewne troch niejasny, ale spokojnie powoli wszystko sobie wyjanimy :)
33
Na razie wcinij klawisz F7 (lub wybierz z menu Build|Build Solution), by skompilowa i zlinkowa aplikacj. Jak widzisz, jest to proces cakowicie automatyczny i, jeeli tylko kod jest poprawny, nie wymaga adnych dziaa z twojej strony. W kocu, wcinij F5 (lub wybierz Debug|Start) i podziwiaj konsol z wywietlonym entuzjastycznym komunikatem :D (A gdy si nim nacieszysz, nacinij dowolny klawisz, by zakoczy program.)
Kod programu
Naturalnie, teraz przyjrzymy si bliej naszemu elementarnemu projektowi, przy okazji odkrywajc najwaniejsze aspekty programowania w C++.
Komentarze
Pierwsza linijka: // First pierwszy program w C++ to komentarz, czyli dowolny opis sowny. Jest on cakowicie ignorowany przez kompilator, natomiast moe by pomocny dla piszcego i czytajcego kod. Komentarze piszemy w celu wyjanienia pewnych fragmentw kodu programu, oddzielenia jednej jego czci od drugiej, oznaczania funkcji i moduw itp. Odpowiednia ilo komentarzy uatwia zrozumienie kodu, wic stosuj je czsto :) W C++ komentarze zaczynamy od // (dwch slashy): // To jest komentarz lub umieszczamy je midzy /* i */, na przykad: /* Ten komentarz moe by bardzo dugi i skada si z kilku linijek. */ W moim komentarzu do programu umieciem jego tytu7 oraz krtki opis; bd t zasad stosowa na pocztku kadego przykadowego kodu. Oczywicie, ty moesz komentowa swoje rda wedle wasnych upodoba, do czego ci gorco zachcam :D
Funkcja main()
Kiedy uruchamiamy nasz program, zaczyna on wykonywa kod zawarty w funkcji main(). Od niej wic rozpoczyna si dziaanie aplikacji a nawet wicej: na niej te to dziaanie si koczy. Zatem program (konsolowy) to przede wszystkim kod zawarty w funkcji main() determinuje on bezporednio jego zachowanie. W przypadku rozwaanej aplikacji funkcja ta nie jest zbyt obszerna, niemniej zawiera wszystkie niezbdne elementy. Najwaniejszym z nich jest nagwek, ktry u nas prezentuje si nastpujco: void main()
Takim samym tytuem jest oznaczony gotowy program przykadowy doczony do kursu
34
Podstawy programowania
Wystpujce na pocztku sowo kluczowe void mwi kompilatorowi, e nasz program nie bdzie informowa systemu operacyjnego o wyniku swojego dziaania. Niektre programy robi to poprzez zwracanie liczby zazwyczaj zera w przypadku braku bdw i innych wartoci, gdy wystpiy jakie problemy. Nam to jest jednak zupenie niepotrzebne w kocu nie wykonujemy adnych zoonych operacji, zatem nie istnieje moliwo jakiekolwiek niepowodzenia8. Gdybymy jednak chcieli uczyni systemowi zado, to powinnimy zmieni nagwek na int main() i na kocu funkcji dopisa return 0; - a wic poinformowa system o sukcesie operacji. Jak jednak przekonalimy si wczeniej, nie jest to niezbdne. Po nagwku wystpuje nawias otwierajcy {. Jego gwna rola to informowanie kompilatora, e tutaj co si zaczyna. Wraz z nawiasem zamykajcym } tworzy on blok kodu na przykad funkcj. Z takimi parami nawiasw bdziesz si stale spotyka; maj one znaczenie take dla programisty, gdy porzdkuj kod i czyni go bardziej czytelnym. Przyjte jest, i nastpne linijki po nawiasie otwierajcym {, a do zamykajcego }, powinny by przesunite w prawo za pomoc wci (uzyskiwanych spacjami lub klawiszem TAB). Poprawia to oczywicie przejrzysto kodu, lecz pamitanie o tej zasadzie podczas pisania moe by uciliwe. Na szczcie dzisiejsze rodowiska programistyczne s na tyle sprytne, e same dbaj o waciwe umieszczanie owych wci. Nie musisz wic zawraca sobie gowy takimi bahostkami grunt, eby wiedzie, komu naley by wdzicznym ;)) Takim oto sposobem zapoznalimy si ze struktur funkcji main(), bdcej gwn czci programu konsolowego w C++. Teraz czas zaj si jej zawartoci i dowiedzie si, jak i dlaczego nasz program dziaa :)
Podobny optymizm jest zazwyczaj grub przesad i moemy sobie na niego pozwoli tylko w najprostszych programach, takich jak niniejszy :)
35
Niniejsze dwa nale do przestrzeni std, gdy s czci Biblioteki Standardowej jzyka C++ (wszystkie jej elementy nale do tej przestrzeni). Moemy uwolni si od koniecznoci pisania przedrostka przestrzeni nazw std, jeeli przed funkcj main() umiecimy deklaracj using namespace std;. Wtedy moglibymy uywa krtszych nazw cout i endl. Konkludujc: strumie wyjcia pozwala nam na wywietlanie tekstu w konsoli, za uywamy go poprzez std::cout oraz strzaki <<. *** Druga linijka funkcji main() jest bardzo krtka: getch(); Podobnie krtko powiem wic, e odpowiada ona za oczekiwanie programu na nacinicie dowolnego klawisza. Traktujc rzecz cilej, getch()jest funkcj podobnie jak main(), jednake do zwizanego z tym faktem zagadnienia przejdziemy nieco pniej. Na razie zapamitaj, i jest to jeden ze sposobw na wstrzymanie dziaania programu do momentu wcinicia przez uytkownika dowolnego klawisza na klawiaturze.
36
Podstawy programowania
Nie znaczy to jednak, e kady plik nagwkowy odpowiada tylko za jedn instrukcj. Jest wrcz odwrotnie, na przykad wszystkie funkcje systemu Windows (a jest ich kilka tysicy) wymagaj doczenia tylko jednego pliku! Konieczno doczania plikw nagwkowych (zwanych w skrcie nagwkami) moe ci si wydawa celowym utrudnianiem ycia programicie. Ma to jednak gboki sens, gdy zmniejsza rozmiary programw. Dlaczego kompilator miaby powiksza plik EXE zwykej aplikacji konsolowej o nazwy (i nie tylko nazwy) wszystkich funkcji Windows czy DirectX, skoro i tak nie bdzie ona z nich korzysta? Mechanizm plikw nagwkowych pozwala temu zapobiec i t drog korzystnie wpyn na objto programw. *** Tym zagadnieniem zakoczylimy omawianie naszego programu - moemy sobie pogratulowa :) Nie by on wprawdzie ani wielki, ani szczeglnie imponujcy, lecz pocztki zawsze s skromne. Nie spoczywajmy zatem na laurach i kontynuujmy
Procedury i funkcje
Pierwszy napisany przez nas program skada si wycznie z jednej funkcji main(). W praktyce takie sytuacje w ogle si nie zdarzaj, a kod aplikacji zawiera najczciej bardzo wiele procedur i funkcji. Poznamy zatem dogbnie istot tych konstrukcji, by mc pisa prawdziwe programy. Procedura lub funkcja to fragment kodu, ktry jest wpisywany raz, ale moe by wykonywany wielokrotnie. Realizuje on najczciej jak pojedyncz czynno przy uyciu ustalonego przez programist algorytmu. Jak wiemy, dziaanie wielu algorytmw skada si na prac caego programu, moemy wic powiedzie, e procedury i funkcje s podprogramami, ktrych czstkowa praca przyczynia si do funkcjonowania programu jako caoci. Gdy mamy ju (a mamy? :D) pen jasno, czym s podprogramy i rozumiemy ich rol, wyjanijmy sobie rnic midzy procedur a funkcj. Procedura to wydzielony fragment kodu programu, ktrego zadaniem jest wykonywanie jakiej czynnoci. Funkcja zawiera kod, ktrego celem jest obliczenie i zwrcenie jakiej wartoci9. Procedura moe przeprowadza dziaania takie jak odczytywanie czy zapisywanie pliku, wypisywanie tekstu czy rysowanie na ekranie. Funkcja natomiast moe policzy ilo wszystkich znakw a w danym pliku czy te wyznaczy najmniejsz liczb spord wielu podanych. W praktyce (i w jzyku C++) rnica midzy procedur a funkcj jest dosy subtelna, dlatego czsto obie te konstrukcje nazywa si dla uproszczenia funkcjami. A poniewa lubimy wszelk prostot, te bdziemy tak czyni :)
Wasne funkcje
Na pocztek dokonamy prostej modyfikacji programu napisanego wczeniej. Jego kod bdzie teraz wyglda tak:
9
37
// Functions przykad wasnych funkcji #include <iostream> #include <conio.h> void PokazTekst() { std::cout << "Umiem juz pisac wlasne funkcje! :)" << std::endl; } void main() { PokazTekst(); getch(); } Po kompilacji i uruchomieniu programu nie wida wikszych zmian nadal pokazuje on komunikat w oknie konsoli (oczywicie o innej treci, ale to raczej rednio wane :)). Jednak wyranie wida, e kod uleg powanym zmianom. Na czym one polegaj? Ot wydzielilimy fragment odpowiedzialny za wypisywanie tekstu do osobnej funkcji o nazwie PokazTekst(). Jest ona teraz wywoywana przez nasz gwn funkcj, main(). Zmianie uleg wic sposb dziaania programu rozpoczyna si od funkcji main(), ale od razu przeskakuje do PokazTekst(). Po jej zakoczeniu ponownie wykonywana jest funkcja main(), ktra z kolei wywouje funkcj getch(). Czeka ona na wcinicie dowolnego klawisza i gdy to nastpi, wraca do main(), ktrej jedynym zadaniem jest teraz zakoczenie programu.
Tryb ledzenia
Przekonajmy si, czy to faktycznie prawda! W tym celu uruchomimy nasz program w specjalnym trybie krokowym. Aby to uczyni wystarczy wybra z menu opcj Debug|Step Into lub Debug|Step Over, ewentualnie wcisn F11 lub F10. Zamiast konsoli z wypisanym komunikatem widzimy nadal okno kodu z t strzak, wskazujc nawias otwierajcy funkcj main(). Jest to aktualny punkt wykonania (ang. execution point) programu, czyli bieco wykonywana instrukcja kodu tutaj pocztek funkcji main(). Wcinicie F10 lub F11 spowoduje wejcie w ow funkcj i sprawi, e strzaka spocznie teraz na linijce PokazTekst(); Jest to wywoanie naszej wasnej funkcji PokazTekst(). Chcemy dokadnie przeledzi jej przebieg, dlatego wciniemy F11 (lub skorzystamy z menu Debug|Step Into), by skierowa si do jej wntrza. Uycie klawisza F10 (albo Debug|Step Over) spowodowaoby ominicie owej funkcji i przejcie od razu do nastpnej linijki w main(). Oczywicie mija si to z naszym zamysem i dlatego skorzystamy z F11. Punkt wykonania osiad obecnie na pocztku funkcji PokazTekst(), wic korzystajc z ktrego z dwch uywanych ostatnio klawiszy moemy umieci go w jej kodzie. Dokadniej, w pierwszej linijce std::cout << "Umiem juz pisac wlasne funkcje! :)" << std::endl; Jak wiemy, wypisuje ona tekst do okna konsoli. W tym momencie uyj Alt+Tab lub jakiego innego windowsowego sposobu, by przeczy si do niego. Przekonasz si (czarno na czarnym ;)), i jest cakowicie puste. Wr wic do okna kodu, wcinij
38
Podstawy programowania
F10/F11 i ponownie spjrz na konsol. Zobaczysz naocznie, i std::cout faktycznie wypisuje nam to, co chcemy :D Po kolejnych dwch uderzeniach w jeden z klawiszy powrcimy do funkcji main(), a strzaka zwana punktem wykonania ustawi si na getch(); Moglibymy teraz uy F11 i zobaczy kod rdowy funkcji getch(), ale to ma raczej niewielki sens. Poza tym, darowanemu koniowi (jakim s z pewnocia tego rodzaju funkcje!) nie patrzy si w zby :) Posuymy si przeto klawiszem F10 i pozwolimy funkcji wykona si bez przeszkd. Zaraz zaraz Gdzie podziaa si strzaka? Czyby co poszo nie tak? Spokojnie, wszystko jest w jak najlepszym porzdku. Pamitamy, e funkcja getch() oczekuje na przycinicie dowolnego klawisza, wic teraz wraz z ni czeka na to caa aplikacja. Aby nie przedua nadto wyczekiwania, przecz si do okna konsoli i uczy mu zado :) I tak oto dotarlimy do epilogu punkt wykonania jest teraz na kocu funkcji main(). Tu koczy si kod napisany przez nas, na program skada si jednak take dodatkowy kod, dodany przez kompilator. Oszczdzimy go sobie i wciniciem F5 (tudzie Debug|Continue) pozwolimy przebiec po nim sprintem i w konsekwencji zakoczy program. T oto drog zapoznalimy si z bardzo wanym narzdziem pracy programisty, czyli trybem krokowym, prac krokow lub trybem ledzenia10. Widzielimy, e pozwala on dokadnie przestudiowa dziaanie programu od pocztku do koca, poprzez wszystkie jego instrukcje. Z tego powodu jest nieocenionym pomocnikiem przy szukaniu i usuwaniu bdw w kodzie.
Przebieg programu
Konkluzj naszej przygody z funkcjami i prac krokow bdzie diagram, obrazujcy dziaanie programu od pocztku do koca:
Czarne linie ze strzakami oznaczaj wywoywanie i powrt z funkcji, za due biae ich wykonywanie. Program zaczyna si u z lewej strony schematu, a koczy po prawej; zauwamy te, e w obu tych miejscach wykonywan funkcj jest main(). Prawd jest zatem fakt, i to ona jest gwn czci aplikacji konsolowej. Jeli opis sowny nie by dla ciebie nie do koca zrozumiay, to ten schemat powinien wyjani wszystkie wtpliwoci :)
10
39
Zmienne i stae
Umiemy ju wypisywa tekst w konsoli i tworzy wasne funkcje. Niestety, nasze programy s na razie zupenie bierne, jeeli chodzi o kontakt z uytkownikiem. Nie ma on przy nich nic do roboty oprcz przeczytania komunikatu i wcinicia dowolnego klawisza. Najwyszy czas to zmieni. Napiszmy wic program, ktry bdzie porozumiewa si z uytkownikiem. Moe on wyglda na przykad tak: // Input uycie zmiennych i strumienia wejcia #include <string> #include <iostream> #include <conio.h> void main() { std::string strImie; std::cout << "Podaj swoje imie: "; std::cin >> strImie; std::cout << "Twoje imie to " << strImie << "." << std::endl; } getch();
Po kompilacji i uruchomieniu wida ju wyrany postp w dziedzinie form komunikacji :) Nasza aplikacja oczekuje na wpisanie imienia uytkownika i potwierdzenie klawiszem ENTER, a nastpnie chwali si dopiero co zdobyt informacj. Patrzc w kod programu widzimy kilka nowych elementw, zatem nie bdzie niespodziank, jeeli teraz przystpi do ich omawiania :D
40
Podstawy programowania
Zasady te dotycz wszystkich nazw w C++ i, jak sdz, nie szczeglnie trudne do przestrzegania. Z brakiem spacji mona sobie poradzi uywajc w jej miejsce podkrelenia (jakas_zmienna) lub rozpoczyna kady wyraz z wielkiej litery (JakasZmienna). W jednej linijce moemy ponadto zadeklarowa kilka zmiennych, oddzielajc ich nazwy przecinkami. Wszystkie bd wtedy przynalene do tego samego typu. Typ okrela nam rodzaj informacji, jakie mona przechowywa w naszej zmiennej. Mog to by liczby cakowite, rzeczywiste, tekst (czyli acuchy znakw, ang. strings), i tak dalej. Moemy take sami tworzy wasne typy zmiennych, czym zreszt niedugo si zajmiemy. Na razie jednak powinnimy zapozna si z do szerokim wachlarzem typw standardowych, ktre to obrazuje niniejsza tabelka: nazwa typu int float bool char std::string opis liczba cakowita (dodatnia lub ujemna) liczba rzeczywista (z czci uamkow) warto logiczna (prawda lub fasz) pojedynczy znak acuch znakw (tekst)
Uywajc tych typw moemy zadeklarowa takie zmienne jak: int nLiczba; float fLiczba; std::string strNapis; // liczba cakowita, np. 45 czy 12 + 89 // liczba rzeczywista (1.4, 3.14, 1.0e-8 itp.) // dowolny tekst
By moe zauwaye, e na pocztku kadej nazwy widnieje tu przedrostek, np. str czy n. Jest to tak zwana notacja wgierska; pozwala ona m.in. rozrni typ zmiennej na podstawie nazwy. Zapis ten sta si bardzo popularny, szczeglnie wrd programistw jzyka C++ - spora ich cz uwaa, e znacznie poprawia on czytelno kodu. Szerszy opis notacji wgierskiej moesz znale w Dodatku A.
Strumie wejcia
C by nam jednak byo po zmiennych, jeli nie mielimy skd wzi dla nich danych? Prostych sposobem uzyskania ich jest proba do uytkownika o wpisanie odpowiednich informacji z klawiatury. Tak te czynimy w aktualnie analizowanym programie odpowiada za to kod: std::cin >> strImie; Wyglda on podobnie do tego, ktry jest odpowiedzialny za wypisywanie tekstu w konsoli. Wykonuje jednak czynno dokadnie odwrotn: pozwala na wprowadzenie sekwencji znakw i zapisuje j do zmiennej strImie. std::cin symbolizuje strumie wejcia, ktry zadaniem jest wanie pobieranie wpisanego przez uytkownika tekstu. Nastpnie kieruje go (co obrazuj strzaki >>) do wskazanej przez nas zmiennej. Zauwamy, e w naszej aplikacji kursor pojawia si w tej samej linijce, co komunikat Podaj swoje imi. Nietrudno domyle si, dlaczego nie umiecilimy po nim std::endl, wobec czego nie jest wykonywane przejcie do nastpnego wiersza.
41
***
Strumienie wejcia i wyjcia stanowi razem nierozczn par mechanizmw, ktre umoliwiaj nam pen swobod komunikacji z uytkownikiem w aplikacjach konsolowych.
Stae
Stae s w swoim przeznaczeniu bardzo podobne do zmiennych - tyle tylko e s niezmienne :)) Uywamy ich, aby nada znaczce nazwy jakim niezmieniajcym si wartociom w programie. Staa to niezmienna warto, ktrej nadano nazw celem atwego jej odrnienia od innych, czsto podobnych wartoci, w kodzie programu. Jej deklaracja, na przykad taka: const int STALA = 10; przypomina nieco sposb deklarowania zmiennych naley take poda typ oraz nazw. Swko const (ang. constant staa) mwi jednak kompilatorowi, e ma do czynienia ze sta, dlatego oczekuje rwnie podania jej wartoci. Wpisujemy j po znaku rwnoci =. W wikszoci przypadkw staych uywamy do identyfikowania liczb - zazwyczaj takich, ktre wystpuj w kodzie wiele razy i maj po kilka znacze w zalenoci od kontekstu. Pozwala to unikn pomyek i poprawia czytelno programu. Stae maj te t zalet, e ich wartoci moemy okrela za pomoc innych staych, na przykad: const int NETTO = 2000; const int PODATEK = 22; const int BRUTTO = NETTO + NETTO * PODATEK / 100; Jeeli kiedy zmieni si jedna z tych wartoci, to bdziemy musieli dokona zmiany tylko w jednym miejscu kodu bez wzgldu na to, ile razy uylimy danej staej w naszym programie. I to jest pikne :) Inne przykady staych:
42
Podstawy programowania
const int DNI_W_TYGODNIU = 7; const float PI = 3.141592653589793; const int MAX_POZIOM = 50;
Operatory arytmetyczne
Przyznajmy szczerze: nasze dotychczasowe aplikacje nie wykonyway adnych sensownych zada bo czy mona nimi nazwa wypisywanie cigle tego samego tekstu? Z pewnoci nie. Czy to si szybko zmieni? Niczego nie obiecuj, jednak z czasem powinno by w tym wzgldzie coraz lepiej :D Znajomo operatorw arytmetycznych z pewnoci poprawi ten stan rzeczy w kocu od dawien dawna podstawowym przeznaczeniem wszelkich programw komputerowych jest wanie liczenie.
Umiemy liczy!
Tradycyjnie ju zaczniemy od przykadowego programu: // Arithmetic - proste dziaania matematyczne #include <iostream> #include <conio.h> void main() { int nLiczba1; std::cout << "Podaj pierwsza liczbe: "; std::cin >> nLiczba1; int nLiczba2; std::cout << "Podaj druga liczbe: "; std::cin >> nLiczba2; int nWynik = nLiczba1 + nLiczba2; std::cout << nLiczba1 << " + " << nLiczba2 << " = " << nWynik; getch();
Po uruchomieniu skompilowanej aplikacji przekonasz si, i jest to prosty kalkulator :) Prosi on najpierw o dwie liczby cakowite i zwraca pniej wynik ich dodawania. Nie jest to moe imponujce, ale z pewnoci bardzo poyteczne ;) Zajrzyjmy teraz w kod programu. Pocztkowa cz funkcji main(): int nLiczba1; std::cout << "Podaj pierwsza liczbe: "; std::cin >> nLiczba1; odpowiada za uzyskanie od uytkownika pierwszej z liczb. Mamy tu deklaracj zmiennej, w ktrej zapiszemy ow liczb, wywietlenie proby przy pomocy strumienia wyjcia oraz pobranie wartoci za pomoc strumienia wejcia. Kolejne trzy linijki s bardzo podobne do powyszych, gdy ich zadanie jest niemal identyczne chodzi oczywicie o zdobycie drugiej liczby naszej sumy. Nie ma wic potrzeby dokadnego ich omawiania.
43
Jest to deklaracja zmiennej nWynik, poczona z przypisaniem do niej sumy dwch liczb uzyskanych poprzednio. Tak czynno (natychmiastowe nadanie wartoci deklarowanej zmiennej) nazywamy inicjalizacj. Oczywicie mona by zrobi to w dwch instrukcjach, ale tak jest adniej, prociej i efektywniej :) Znak = nie wskazuje tu absolutnie na rwno dwch wyrae jest to bowiem operator przypisania, ktrego uywamy do ustawiania wartoci zmiennych. Ostatnie dwie linijki nie wymagaj zbyt wiele komentarza jest to po prostu wywietlenie obliczonego wyniku i przywoanie znanej ju skdind funkcji getch(), ktra oczekuje na dowolny klawisz.
Pierwsze trzy pozycje s na tyle jasne i oczywiste, e darujemy sobie ich opis :) Przyjrzymy si za to bliej operatorom zwizanym z dzieleniem. Operator / dziaa na dwa sposoby w zalenoci od tego, jakiego typu liczby dzielimy. Rozrnia on bowiem dzielenie cakowite, kiedy interesuje nas jedynie wynik bez czci po przecinku, oraz rzeczywiste, gdy yczymy sobie uzyska dokadny iloraz. Rzecz jasna, w takich przypadkach jak 25 / 5, 33 / 3 czy 221 / 13 wynik bdzie zawsze liczb cakowit. Gdy jednak mamy do czynienia z liczbami niepodzielnymi przez siebie, sytuacja nie wyglda ju tak prosto. Kiedy zatem mamy do czynienia z ktrym z typw dzielenia? Zasada jest bardzo prosta jeli obie dzielone liczby s cakowite, wynik rwnie bdzie liczb cakowit; jeeli natomiast cho jedna jest rzeczywista, wtedy otrzymamy iloraz wraz z czci uamkow. No dobrze, wynika std, e takie przykadowe dziaanie float fWynik = 11.5 / 2.5; da nam prawidowy wynik 4.6. Co jednak zrobi, gdy dzielimy dwie niepodzielne liczby cakowite i chcemy uzyska dokadny rezultat? Musimy po prostu obie liczby zapisa
44
Podstawy programowania
jako rzeczywiste, a wic wraz z czci uamkow choby bya rwna zeru, przykadowo: float fWynik = 8.0 / 5.0; Uzyskamy w ten sposb prawidowy wynik 1.6. A co z tym dziwnym procentem, czyli operatorem %? Zwizany jest on cile z dzieleniem cakowitym, mianowicie oblicza nam reszt z dzielenia jednej liczby przez drug. Dobr ilustracj dziaania tego operatora mog by zakupy :) Powiedzmy, e wybralimy si do sklepu z siedmioma zotymi w garci celem nabycia drog kupna jakiego towaru, ktry kosztuje 3 zote za sztuk i jest moliwy do sprzeday jedynie w caoci. W takiej sytuacji dzielc (cakowicie!) 7 przez 3 otrzymamy ilo sztuk, ktre moemy kupi. Za int nReszta = 7 % 3; bdzie kwot, ktra pozostanie nam po dokonaniu transakcji czyli jedn zotwk. Czy to nie banalne? ;)
Priorytety operatorw
Proste obliczenia, takie jak powysze, rzadko wystpuj w prawdziwych programach. Najczciej czymy kilka dziaa w jedno wyraenie i wtedy moe pojawi si problem pierwszestwa (priorytetu) operatorw, czyli po prostu kolejnoci wykonywania dziaa. W C++ jest ona na szczcie identyczna z t znan nam z lekcji matematyki. Najpierw wic wykonywane jest mnoenie i dzielenie, a potem dodawanie i odejmowanie. Moemy uoy obrazujc ten fakt tabelk: priorytet 1 2 operator(y) *, /, % +, -
Najlepiej jednak nie polega na tej wasnoci operatorw i uywa nawiasw w przypadku jakichkolwiek wtpliwoci. Nawiasy chroni przed trudnymi do wykrycia bdami zwizanymi z pierwszestwem operatorw, dlatego stosuj je w przypadku kadej wtpliwoci co do kolejnoci dziaa. *** W taki oto sposb zapoznalimy si wanie z operatorami arytmetycznymi.
Tajemnicze znaki
Twrcy jzyka C++ mieli chyba na uwadze oszczdno palcw i klawiatur programistw, uczynili wic jego skadni wyjtkowo zwart i dodali kilka mechanizmw skracajcych zapis kodu. Z jednym z nich, bardzo czsto wykorzystywanym, zapoznamy si za chwil. Ot instrukcje w rodzaju nZmienna = nZmienna + nInnaZmienna; nX = nX * 10;
45
mog by, przy uyciu tej techniki, napisane nieco krcej. Zanim j poznamy, zauwamy, i we wszystkich przedstawionych przykadach po obu stronach znaku = znajduj si te same zmienne. Instrukcje powysze nie s wic przypisywaniem zmiennej nowej wartoci, ale modyfikacj ju przechowywanej liczby. Korzystajc z tego faktu, pierwsze dwie linijki moemy zapisa jako nZmienna += nInnaZmienna; nX *= 10; Jak widzimy, operator + przeszed w +=, za * w *=. Podobna sztuczka moliwa jest take dla trzech pozostaych znakw dziaa11. Sposb ten nie tylko czyni kod krtszym, ale take przyspiesza jego wykonywanie (pomyl, dlaczego!). Jeeli chodzi o nastpne wiersze, to oczywicie dadz si one zapisa w postaci i += 1; j -= 1; Mona je jednak skrci (i przyspieszy) nawet bardziej. Dodawanie i odejmowanie jedynki s bowiem na tyle czstymi czynnociami, e dorobiy si wasnych operatorw ++ i - (tzw. inkrementacji i dekrementacji), ktrych uywamy tak: i++; j--; lub12 tak: ++i; --j; Na pierwszy rzut oka wyglda to nieco dziwnie, ale gdy zaczniesz stosowa t technik w praktyce, szybko docenisz jej wygod.
Podsumowanie
Bohatersko brnc przez kolejne akapity dotarlimy wreszcie do koca tego rozdziau :)) Przyswoilimy sobie przy okazji spory kawaek koderskiej wiedzy. Rozpoczlimy od bliskiego spotkania z IDE Visual Studio, nastpnie napisalimy swj pierwszy program. Po zapoznaniu si z dziaaniem strumienia wyjcia, przeszlimy do funkcji (przy okazji poznajc uroki trybu ledzenia), a potem wreszcie do zmiennych i strumienia wejcia. Gdy ju dowiedzielimy si, czym one s, okrasilimy wszystko drobn porcj informacji na temat operatorw arytmetycznych. Smacznego! ;)
Pytania i zadania
Od niestrawnoci uchroni ci odpowiedzi na ponisze pytania i wykonanie wicze :)
A take dla niektrych innych rodzajw operatorw, ktre poznamy pniej Istnieje rnica midzy tymi dwoma formami zapisu, ale na razie nie jest ona dla nas istotna co nie znaczy, e nie bdzie :)
12
11
46
Podstawy programowania
Pytania
1. Dziki jakim elementom jzyka C++ moemy wypisywa tekst w konsoli i zwraca si do uytkownika? 2. Jaka jest rola funkcji w kodzie programu? 3. Czym s stae i zmienne? 4. Wymie poznane operatory arytmetyczne.
wiczenia
1. Napisz program wywietlajcy w konsoli trzy linijki tekstu i oczekujcy na dowolny klawisz po kadej z nich. 2. Zmie program napisany przy okazji poznawania zmiennych (ten, ktry pyta o imi) tak, aby zadawa rwnie pytanie o nazwisko i wywietla te dwie informacje razem (w rodzaju Nazywasz si Jan Kowalski). 3. Napisz aplikacj obliczajc iloczyn trzech podanych liczb. 4. (Trudne) Poczytaj, na przykad w MSDN, o deklarowaniu staych za pomoc dyrektywy #define. Zastanw si, jakie niebezpieczestwo bdw moe by z tym zwizane. Wskazwka: chodzi o priorytety operatorw.
3
DZIAANIE PROGRAMU
Nic nie dzieje si wbrew naturze, lecz wbrew temu, co o niej wiemy.
Fox Mulder w serialu Z archiwum X
Poznalimy ju przedsmak urokw programowania w C++ i stworzylimy kilka wasnych programw. Opanowalimy jednoczenie najbardziej podstawowe podstawy podstaw kodowania w tym jzyku :) Moemy wic odkry kolejne, wane jego elementy, ktre pozwol nam tworzy bardziej interesujce i przydatne programy. Przysza bowiem pora na spotkanie z instrukcjami sterujcymi przebiegiem aplikacji i sposobem jej dziaania.
Parametry funkcji
Nie tylko w programowaniu trudno wskaza operacj, ktr mona wykona bez posiadania o niej dodatkowych informacji. Przykadowo, nie mona wykona operacji kopiowania czy przesunicia pliku do innego katalogu, jeli nie jest znana nazwa tego pliku oraz nazwa docelowego folderu. Gdybymy napisali funkcj realizujc tak czynno, to nazwy pliku oraz katalogu finalnego byyby jej parametrami. Parametry funkcji to dodatkowe dane, przekazywane do funkcji podczas jej wywoania. Parametry peni rol dodatkowych zmiennych wewntrz funkcji i mona ich uywa podobnie jak innych zmiennych, zadeklarowanych w niej bezporednio. Rni si one oczywicie tym, e wartoci parametrw pochodz z zewntrz s im przypisywane podczas wywoania funkcji.
48
Podstawy programowania
Po tym krtkim opisie czas na obrazowy przykad. Oto zmodyfikujemy nasz program liczcy tak, eby korzysta z dobrodziejstw parametrw funkcji: // Parameters - wykorzystanie parametrw funkcji #include <iostream> #include <conio.h> void Dodaj(int nWartosc1, int nWartosc2) { int nWynik = nWartosc1 + nWartosc2; std::cout << nWartosc1 << " + " << nWartosc2 << " = " << nWynik; std::cout << std::endl; } void main() { int nLiczba1; std::cout << "Podaj pierwsza liczbe: "; std::cin >> nLiczba1; int nLiczba2; std::cout << "Podaj druga liczbe: "; std::cin >> nLiczba2; Dodaj (nLiczba1, nLiczba2); getch();
Rzut oka na dziaajcy program pozwala stwierdzi, i wykonuje on tak sam prac, jak jego poprzednia wersja. Z kolei spojrzenie na kod ujawnia w nim widoczne zmiany przyjrzyjmy si im. Zasadnicza czynno programu, czyli dodawanie dwch liczb, zostaa wyodrbniona w postaci osobnej funkcji Dodaj(). Posiada ona dwa parametry nWartosc1 i nWartosc2, ktre s w niej dodawane do siebie i wywietlane w konsoli. Wielce interesujcy jest w zwizku z tym nagwek funkcji Dodaj(), zawierajcy deklaracj owych dwch parametrw: void Dodaj(int nWartosc1, int nWartosc2) Jak sam widzisz, wyglda ona bardzo podobnie do deklaracji zmiennych najpierw piszemy typ parametru, a nastpnie jego nazw. Nazwa ta pozwala odwoywa si do wartoci parametru w kodzie funkcji, a wic na przykad uy jej jako skadnika sumy. Okrelenia kolejnych parametrw oddzielamy od siebie przecinkami, za ca deklaracj umieszczamy w nawiasie po nazwie funkcji. Wywoanie takiej funkcji jest raczej oczywiste: Dodaj (nLiczba1, nLiczba2); Podajemy tu w nawiasie kolejne wartoci, ktre zostan przypisane jej parametrom; oddzielamy je tradycyjnie ju przecinkami. W niniejszym przypadku parametrowi nWartosc1 zostanie nadana warto zmiennej nLiczba1, za nWartosc2 nLiczba2. Myl, e jest to do intuicyjne i nie wymaga wicej wyczerpujcego komentarza :)
***
Dziaanie programu
49
Reasumujc: parametry pozwalaj nam przekazywa funkcjom dodatkowe dane, ktrych mog uy do wykonania swoich dziaa. Ilo, typy i nazwy parametrw deklarujemy w nagwku funkcji, za podczas jej wywoywania podajemy ich wartoci w analogiczny sposb.
Cao programu jest doczona do tutoriala, tutaj zaprezentujemy tylko jego najwaniejsze fragmenty
50
Podstawy programowania
void przez nazw typu, int. To oczywicie nie przypadek w taki wanie sposb informujemy kompilator, i nasza funkcja ma zwraca warto oraz wskazujemy jej typ. Kod funkcji to tylko jeden wiersz, zaczynajcy si od return (powrt). Okrela on ni mniej, ni wicej, jak tylko ow warto, ktra bdzie zwrcona i stanie si wynikiem dziaania funkcji. Rezultat ten zobaczymy w kocu i my w oknie konsoli:
return powoduje jeszcze jeden efekt, ktry nie jest tu tak wyranie widoczny. Uycie tej instrukcji skutkuje mianowicie natychmiastowym przerwaniem dziaania funkcji i powrotem do miejsca jej wywoania. Wprawdzie nasze proste funkcje i tak kocz si niemal od razu, wic nie ma to wikszego znaczenia, jednak w przypadku powaniejszych podprogramw naley o tym fakcie pamita. Wynika z niego take moliwo uycia return w funkcjach niezwracajcych adnej wartoci mona je w ten sposb przerwa, zanim wykonaj swj kod w caoci. Poniewa nie mamy wtedy adnej wartoci do zwracania, uywamy samego sowa return; - bez wskazywania nim jakiego wyraenia.
***
Dowiedzielimy si zatem, i funkcja moe zwraca warto jako wynik swej pracy. Rezultat taki winien by okrelonego typu deklarujemy go w nagwku funkcji jeszcze przed jej nazw. Natomiast w kodzie funkcji moemy uy instrukcji return, by wskaza warto bdc jej wynikiem. Jednoczenie instrukcja ta spowoduje zakoczenie dziaania funkcji.
Skadnia funkcji
Gdy ju poznalimy zawioci i niuanse zwizane z uywaniem funkcji w swoich aplikacjach, moemy t wydatn porcj informacji podsumowa oglnymi reguami skadniowymi dla podprogramw w C++. Ot posta funkcji w tym jzyku jest nastpujca: typ_zwracanej_wartoci/void nazwa_funkcji([typ_parametru nazwa, ...]) { instrukcje_1 return warto_funkcji_1; instrukcje_2 return warto_funkcji_2; instrukcje_3 return warto_funkcji_3; ... return warto_funkcji_n; } Jeeli dokadnie przestudiowae (i zrozumiae! :)) wiadomoci z tego paragrafu i z poprzedniego rozdziau, nie powinna by ona dla ciebie adnym zaskoczeniem.
Dziaanie programu
51
Zauwa jeszcze, ze fraza return moe wystpowa kilka razy, zwraca w kadym z wariantw rne wartoci, a cao funkcji mie logiczny sens i dziaa poprawnie. Jest to moliwe midzy innymi dziki instrukcjom warunkowym, ktre poznamy ju za chwil. *** W ten oto sposb uzyskalimy bardzo wan umiejtno programistyczn, jak jest poprawne uywanie funkcji we wasnych programach. Upewnij si przeto, i skrupulatnie przyswoie sobie informacje o tym zagadnieniu, jakie serwowaem w aktualnym i poprzednim rozdziale. W dalszej czci kursu bdziemy czsto korzysta z funkcji w przykadowych kodach i omawianych tematach, dlatego wane jest, by nie mia kopotw z nimi. Jeli natomiast czujesz si na siach i chcesz dowiedzie czego wicej o funkcjach, zajrzyj do pomocy MSDN zawartej w Visual Studio.
Sterowanie warunkowe
Dobrze napisany program powinien by przygotowany na kad ewentualno i nietypow sytuacj, jaka moe si przydarzy w czasie jego dziaania. W niektrych przypadkach nawet proste czynnoci mog potencjalnie koczy si niepowodzeniem, za porzdna aplikacja musi radzi sobie z takimi drobnymi (lub cakiem sporymi) kryzysami. Oczywicie program nie uczyni nic, czego nie przewidziaby jego twrca. Dlatego te wanym zadaniem programisty jest opracowanie kodu reagujcego odpowiednio na nietypowe sytuacje, w rodzaju bdnych danych wprowadzonych przez uytkownika lub braku pliku potrzebnego aplikacji do dziaania. Moliwe s te przypadki, w ktrych dla kilku cakowicie poprawnych sytuacji, danych itp. trzeba wykona zupenie inne operacje. Wane jest wtedy rozrnienie tych wszystkich wariantw i skierowanie dziaania programu na waciwe tory, gdy ma miejsce ktry z nich. Do wszystkich tych zada stworzono w C++ (i w kadym jzyku programowania) zestaw odpowiednich narzdzi, zwanych instrukcjami warunkowymi. Ich przeznaczeniem jest wanie dokonywanie rnorakich wyborw, zalenych od ustalonych warunkw. Jak wida, przydatno tych konstrukcji jest nadspodziewanie dua, al byoby wic omin je bez wnikania w ich szczegy, prawda? :) Niezwocznie zatem zajmiemy si nimi, poznajc ich skadni i sposb funkcjonowania.
Instrukcja warunkowa if
Instrukcja if (jeeli) pozwala wykona jaki kod tylko wtedy, gdy speniony jest okrelony warunek. Jej dziaanie sprowadza si wic do sprawdzenia tego warunku i, jeli zostanie stwierdzona jego prawdziwo, wykonania wskazanego bloku kodu. T prost ide moe ilustrowa choby taki przykad: // SimpleIf prosty przykad instrukcji if void main() { int nLiczba; std::cout << "Wprowadz liczbe wieksza od 10: "; std::cin >> nLiczba;
52
Podstawy programowania
if (nLiczba > 10) { std::cout << "Dziekuje." << std::endl; std::cout << "Wcisnij dowolny klawisz, by zakonczyc."; getch(); }
Uruchom ten program dwa razy najpierw podaj liczb mniejsz od 10, za za drugim razem spenij yczenie aplikacji. Zobaczysz, e w pierwszym przypadku zostaniesz potraktowany raczej mao przyjemnie, gdy program bez sowa zakoczy si. W drugim natomiast otrzymasz stosowne podzikowanie za swoj uprzejmo ;) Winna jest temu, jakeby inaczej, wanie instrukcja if. W linijce: if (nLiczba > 10) wykonywane jest bowiem sprawdzenie, czy podana przez ciebie liczba jest rzeczywicie wiksza od 10. Wyraenie nLiczba > 10 jest tu wic warunkiem instrukcji if. W przypadku, gdy okae si on prawdziwy, wykonywane s trzy instrukcje zawarte w nawiasach klamrowych. Jak pamitamy, sekwencj tak nazywamy blokiem kodu. Jeeli za warunek jest nieprawdziwy (a liczba mniejsza lub rwna 10), program omija ten blok i wykonuje nastpn instrukcj wystpujc za nim. Poniewa jednak u nas po bloku if nie ma adnych instrukcji, aplikacja zwyczajnie koczy si, gdy nie ma nic konkretnego do roboty :) Po takim sugestywnym przykadzie nie od rzeczy bdzie przedstawienie skadni instrukcji warunkowej if w jej prostym wariancie: if (warunek) { instrukcje } Stopie jej komplikacji z pewnoci sytuuje si poniej przecitnej ;) Nic w tym dziwnego to w zasadzie najprostsza, lecz jednoczenie bardzo czsto uywana konstrukcja programistyczna. Warto jeszcze zapamita, e blok instrukcji skadajcy si tylko z jednego polecenia moemy zapisa nawet bez nawiasw klamrowych14. Wtedy jednak naley postawi na jego kocu rednik: if (warunek) instrukcja; Taka skrcona wersja jest uywana czsto do sprawdzania wartoci parametrw funkcji, na przykad: void Funkcja(int nParametr) { // sprawdzenie, czy parametr nie jest mniejszy lub rwny zeru // jeeli tak, to funkcja koczy si if (nParametr <= 0) return; } // ... (reszta funkcji)
14
Dziaanie programu
53
Fraza else
Prosta wersja instrukcji if nie zawsze jest wystarczajca nieeleganckie zachowanie naszego przykadowego programu jest dobrym tego uzasadnieniem. Powinien on wszake pokaza stosowny komunikat rwnie wtedy, gdy uytkownik nie wykae si chci wsppracy i nie wprowadzi danej liczby. Musi wic uwzgldni przypadek, w ktrym warunek badany przez instrukcj if (u nas nLiczba > 10) nie jest prawdziwy i zareagowa na w odpowiedni sposb. Naturalnie, mona by umieci stosowny kod po konstrukcji if, ale jednoczenie naleaoby zadba, aby nie by on wykonywany w razie prawdziwoci warunku. Sztuczka z dodaniem instrukcji return; (przerywajcej funkcj main(), a wic i cay program) na koniec bloku if zdaaby oczywicie egzamin, lecz straty w przejrzystoci i prostocie kodu byyby zdecydowanie niewspmierne do efektw :)) Dlatego te C++, jako pretendent do miana nowoczesnego jzyka programowania, posiada bardziej sensowny i logiczny sposb rozwizania tego problemu. Jest nim mianowicie fraza else (w przeciwnym wypadku) cz instrukcji warunkowej if. Korzystajca z niej, ulepszona wersja poprzedniej aplikacji przykadowej moe zatem wyglda chociaby tak: // Else blok alternatywny w instrukcji if void main() { int nLiczba; std::cout << "Wprowadz liczbe wieksza od 10: "; std::cin >> nLiczba; if (nLiczba > 10) { std::cout << "Dziekuje." << std::endl; std::cout << "Wcisnij dowolny klawisz, by zakonczyc."; } else { std::cout << "Liczba " << nLiczba << " nie jest wieksza od 10." << std::endl; std::cout << "Czuj sie upomniany :P"; } } getch();
Gdy uruchomisz powyszy program dwa razy, w podobny sposb jak poprzednio, w kadym wypadku zostaniesz poczstowany jakim komunikatem. Zalenie od wpisanej przez ciebie liczby bdzie to podzikowanie albo upomnienie :)
Screeny 11 i 12. Dwa warianty dziaania programu, czyli instrukcje if i else w caej swej krasie :)
Wystpujcy tu blok else jest uzupenieniem instrukcji if kod w nim zawarty zostanie wykonany tylko wtedy, gdy okrelony w if warunek nie bdzie speniony. Dziki temu moemy odpowiednio zareagowa na kad ewentualno, a zatem nasz program zachowuje si porzdnie w obu moliwych przypadkach :)
54
Podstawy programowania
Funkcja getch() jest w tej aplikacji wywoywana poza blokami warunkowymi, gdy niezalenie od wpisanej liczby i treci wywietlanego komunikatu istnieje potrzeba poczekania na dowolny klawisz. Zamiast wic umieszcza t instrukcj zarwno w bloku if, jak i else, mona j zostawi cakowicie poza nimi. Czas teraz zaprezentowa skadni penej wersji instrukcji if, uwzgldniajcej take blok alternatywny else: if (warunek) { instrukcje_1 } else { instrukcje_2 } Kiedy warunek jest prawdziwy, uruchamiane s instrukcje_1, za w przeciwnym przypadku (else) instrukcje_2. Czy wiat widzia kiedy co rwnie elementarnego? ;) Nie daj si jednak zwie tej prostocie instrukcja warunkowa if jest w istocie potnym narzdziem, z ktrego intensywnie korzystaj wszystkie programy.
ax + b = 0
Jak zapewne pamitamy ze szkoy, mog mie one zero, jedno lub nieskoczenie wiele rozwiza, a wszystko zaley od wartoci wspczynnikw a i b. Mamy zatem due pole do popisu dla instrukcji if :D Program realizujcy to zadanie wyglda wic tak: // LinearEq rozwizywanie rwna liniowych float fA; std::cout << "Podaj wspolczynnik a: "; std::cin >> fA; float fB; std::cout << "Podaj wspolczynnik b: "; std::cin >> fB; if (fA == 0.0) { if (fB == 0.0) std::cout << "Rownanie spelnia kazda liczba rzeczywista." << std::endl; else std::cout << "Rownanie nie posiada rozwiazan." << std::endl; } else std::cout << "x = " << -fB / fA << std::endl; getch();
Dziaanie programu
55
Zagniedona instrukcja if wyglda moe cokolwiek tajemniczo, ale w gruncie rzeczy istota jej dziaania jest w miar prosta. Wyjanimy j za moment. Najpierw powtrka z matematyki :) Przypomnijmy, i rwnanie liniowe ax + b = 0: posiada nieskoczenie wiele rozwiza, jeeli wspczynniki a i b s jednoczenie rwne zeru nie posiada w ogle rozwiza, jeeli a jest rwne zeru, za b nie ma dokadnie jedno rozwizanie (-b/a), gdy a jest rne od zera Wynika std, e istniej trzy moliwe przypadki i scenariusze dziaania programu. Zauwamy jednak, e warunek a jest rwne zeru jest konieczny do realizacji dwch z nich moemy wic go wyodrbni i zapisa w postaci pierwszej (bardziej zewntrznej) instrukcji if. Nadal wszake pozostaje nam problem wspczynnika b sam fakt zerowej wartoci a nie przecie pozwala na obsuenie wszystkich moliwoci. Rozwizaniem jest umieszczenie instrukcji sprawdzajcej b (czyli take if) wewntrz bloku if, sprawdzajcego a! Umoliwia to poprawne wykonanie programu dla wszystkich wartoci liczb a i b.
Uywamy tote dwch instrukcji if, ktre razem odpowiadaj za waciwe zachowanie si aplikacji w trzech moliwych przypadkach. Pierwsza z nich: if (fA == 0.0) kontroluje warto wspczynnika a i tworzy pierwsze rozgazienie na szlaku dziaania programu. Jedna z wychodzcych z niego drg prowadzi do celu zwanego dokadnie jedno rozwizanie rwnania, druga natomiast do kolejnego rozwidlenia: if (fB == 0.0) Ono te kieruje wykonywanie aplikacji albo do nieskoczenie wielu rozwiza, albo te do braku rozwiza rwnania zaley to oczywicie od ewentualnej rwnoci b z zerem. Operatorem rwnoci w C++ jest ==, czyli podwjny znak rwna si (=). Naley koniecznie odrnia go od operatora przypisania, czyli pojedynczego znaku =. Jeli omykowo uylibymy tego drugiego w wyraeniu bdcym warunkiem, to najprawdopodobniej byby on zawsze albo prawdziwy, albo faszywy15 na pewno jednak nie dziaaby tak, jak bymy tego oczekiwali. Co gorsza, mona by si o tym przekona dopiero w czasie dziaania programu, gdy jego kompilacja przebiegaby bez zakce. Pamitajmy wic, by w wyraeniach warunkowych do sprawdzania rwnoci uywa zawsze operatora ==, rezerwujc znak = do przypisywania wartoci zmiennym. ***
15 Zaleaoby to od wartoci po prawej stronie znaku rwnoci jeli byaby rwna zeru, warunek byby faszywy, w przeciwnym wypadku - prawdziwy
56
Podstawy programowania
Ostrzeeniem tym koczymy nasze nieco przydugie spotkanie z instrukcj warunkow if. Mona miao powiedzie, e oto poznalimy jeden z fundamentw, na ktrych opiera si dziaanie wszelkich algorytmw w programach komputerowych. Twoje aplikacje nabior przez to elastycznoci i bd zdolne do wykonywania mniej trywialnych zada jeeli sumiennie przestudiowae ten podrozdzia! :))
" = "
No, to ju jest program, co si zowie: posiada szerok funkcjonalno, prosty interfejs krtko mwic peen profesjonalizm ;) Tym bardziej wic powinnimy przejrze
Dziaanie programu
57
dokadniej jego kod rdowy - zwaywszy, i zawiera interesujc nas w tym momencie instrukcj switch. Zajmuje ona zreszt pokan cz listingu; na dodatek jest to ten fragment, w ktrym wykonywane s obliczenia, bdce podstaw dziaania programu. Jaka jest zatem rola tej konstrukcji?
C, nie jest trudno domyle si jej skoro mamy w naszym programie menu, bdziemy te mieli kilka wariantw jego dziaania. Wybranie przez uytkownika jednego z nich zostaje wcielone w ycie wanie poprzez instrukcj switch. Porwnuje ona kolejno warto zmiennej nOpcja (do ktrej zapisujemy numer wskazanej pozycji menu) z picioma wczeniej ustalonymi przypadkami. Kademu z nich odpowiada fragment kodu, zaczynajcy si od swka case (przypadek) i koczcy na break; (przerwij). Gdy ktry z nich zostanie uznany za waciwy (na podstawie wartoci wspomnianej ju zmiennej), wykonywane s zawarte w nim instrukcje. Jeeli za aden nie bdzie pasowa, program skoczy do dodatkowego wariantu default (domylny) i uruchomi jego kod. Ot, i caa filozofia :) Po tym pobienym wyjanieniu dziaania instrukcji switch, poznamy jej pen posta skadniow: switch (wyraenie) { case warto_1: instrukcje_1 [break;] case warto_2: instrukcje_2 [break;] ... case warto_n; instrukcje_n; [break;] [default: instrukcje_domylne] } Korzystajc z niej, jeszcze prociej zrozumie przeznaczenie konstrukcji switch oraz wykonywane przez ni czynnoci. Mianowicie, oblicza ona wpierw wynik wyraenia, by potem porwnywa go kolejno z podanymi (w instrukcjach case) wartociami. Kiedy stwierdzi, e zachodzi rwno, skacze na pocztek pasujcego wariantu i wykonuje cay kod a do koca bloku switch. Zaraz jak to do koca bloku? Przecie w naszym przykadowym programie, gdy wybralimy, powiedzmy, operacj odejmowania, to otrzymywalimy wycznie rnic liczb bez iloczynu i ilorazu (czyli dalszych opcji). Przyczyna tego tkwi w instrukcji
58
Podstawy programowania
break, umieszczonej na kocu kadej pozycji rozpocztej przez case. Polecenie to powoduje bowiem przerwanie dziaania konstrukcji switch i wyjcie z niej; tym sposobem zapobiega ono wykonaniu kodu odpowiadajcego nastpnym wariantom. W wikszoci przypadkw naley zatem koczy fragment kodu rozpoczty przez case instrukcj break - gwarantuje to, i tylko jedna z moliwoci ustalonych w switch zostanie wykonana. Znaczenie ostatniej, nieobowizkowej frazy default wyjanilimy sobie ju wczeniej. Mona jedynie doda, e peni ona w switch podobn rol, co else w if i umoliwia wykonanie jakiego kodu take wtedy, gdy adna z przewidzianych wartoci nie bdzie zgadza si z wyraeniem. Brak tej instrukcji bdzie za skutkowa niepodjciem adnych dziaa w takim przypadku. *** Omwilimy w ten sposb obie konstrukcje, dziki ktrym mona sterowa przebiegiem programu na podstawie ustalonych warunkw czy te wartoci wyrae. Potrafimy wic ju sprawi, aby nasze aplikacje zachowyway si prawidowo niezalenie od okolicznoci. Nie zmienia to jednak faktu, e nadal potrafi one co najwyej tyle, ile mao funkcjonalny kalkulator i nie wykorzystuj w peni w ogromnych moliwoci komputera. Zmieni to moe kolejny element jzyka C++, ktry teraz wanie poznamy. Przy pomocy ptli, bo o nich mowa, zdoamy zatrudni leniuchujcy dotd procesor do wytonej pracy, ktra wycinie z niego sidme poty ;)
Ptle
Ptle (ang. loops), zwane te instrukcjami iteracyjnymi, stanowi podstaw prawie wszystkich algorytmw. Lwia cz zada wykonywanych przez programy komputerowe opiera si w caoci lub czciowo wanie na ptlach. Ptla to element jzyka programowania, pozwalajcy na wielokrotne, kontrolowane wykonywanie wybranego fragmentu kodu. Liczba takich powtrze (zwanych cyklami lub iteracjami ptli) jest przy tym ograniczona w zasadzie tylko inwencj i rozsdkiem programisty. Te potne narzdzia daj wic moliwo zrealizowania niemal kadego algorytmu. Ptle s te niewtpliwie jednym z atutw C++: ich elastyczno i prostota jest wiksza ni w wielu innych jzykach programowania. Jeeli zatem bdziesz kiedy kodowa jak zoon funkcj przy uyciu skomplikowanych ptli, z pewnoci przypomnisz sobie i docenisz te zalety :)
Ptla do
Prosty przykad obrazujcy ten mechanizm prezentuje si nastpujco: // Do pierwsza ptla warunkowa
Dziaanie programu
59
std::cout << "Wprowadz liczbe wieksza od 10: "; std::cin >> nLiczba; } while (nLiczba <= 10);
Program ten, podobnie jak jeden z poprzednich, oczekuje od nas o liczby wikszej ni dziesi. Tym razem jednak nie daje si zby byle czym - jeeli nie bdziemy skonni od razu przychyli si do jego proby, bdzie j niezomnie powtarza a do skutku (lub do uycia Ctrl+Alt+Del ;D).
Upr naszej aplikacji bierze si oczywicie z umieszczonej wewntrz niej ptli do (czy) . Wykonuje ona kod odpowiedzialny za prob do uytkownika tak dugo, jak dugo ten jest konsekwentny w ignorowaniu jej :) Przejawia si to rzecz jasna wprowadzaniem liczb, ktre nie s wiksze od 10, lecz mniejsze lub rwne tej wartoci odpowiada to warunkowi ptli nLiczba <= 10. Instrukcja niniejsza wykonuje si wic dopty, dopki (ang. while) zmienna nLiczba, ktra przechowuje liczb pobran od uytkownika, nie przekracza granicznej wartoci dziesiciu. Przedstawia to pogldowo poniszy diagram:
60
Podstawy programowania
Co si jednak dzieje przy pierwszym obrocie ptli, gdy program nie zdy jeszcze pobra od uytkownika adnej liczby? Jak mona porwnywa warto zmiennej nLiczba, ktra na samym pocztku jest przecie nieokrelona? Tajemnica tkwi w fakcie, i ptla do dokonuje sprawdzenia swojego warunku na kocu kadego cyklu dotyczy to take pierwszego z nich. Wynika z tego do oczywisty wniosek: Ptla do wykona zawsze co najmniej jeden przebieg. Fakt ten sprawia, e nadaje si ona znakomicie do uzyskiwania jakich danych od uytkownika przy jednoczesnym sprawdzaniu ich poprawnoci. Naturalnie, w prawdziwym programie naleaoby zapewni swobod zakoczenia aplikacji bez wpisywania czegokolwiek. Nasz obrazowy przykad jest jednak wolny od takich fanaberii to wszak tylko kod pomocny w nauce, wic piszc go nie musimy przejmowa si takimi bahostkami ;)) Podsumowaniem naszego spotkania z ptl do bdzie jej skadnia: do {
instrukcje } while (warunek) Wystarczy przyjrze si jej cho przez chwil, by odkry cay sens. Samo tumaczenie wyjania waciwie wszystko: Wykonuj (ang. do) instrukcje, dopki (ang. while) zachodzi warunek. I to jest wanie spiritus movens caej tej konstrukcji.
Ptla while
Przysza pora na poznanie drugiego typu ptli warunkowych, czyli while. Swko bdce jej nazw widziae ju wczeniej, przy okazji ptli do nie jest to bynajmniej przypadek, gdy obydwie konstrukcje s do siebie bardzo podobne. Dziaanie ptli while przeledzimy zatem na poniszym ciekawym przykadzie: // While - druga ptla warunkowa #include <iostream> #include <ctime> #include <conio.h> void main() { // wylosowanie liczby srand ((int) time(NULL)); int nWylosowana = rand() % 100 + 1; std::cout << "Wylosowano liczbe z przedzialu 1-100." << std::endl; // pierwsza prba odgadnicia liczby int nWprowadzona; std::cout << "Sprobuj ja odgadnac: "; std::cin >> nWprowadzona; // kolejne prby, a do skutku - przy uyciu ptli while while (nWprowadzona != nWylosowana) { if (nWprowadzona < nWylosowana) std::cout << "Liczba jest zbyt mala."; else std::cout << "Za duza liczba.";
Dziaanie programu
61
std::cout << " Sprobuj jeszcze raz: "; std::cin >> nWprowadzona;
Jest to nic innego, jak prosta gra :) Twoim zadaniem jest w niej odgadnicie pomylanej przez komputer liczby (z przedziau od jednoci do stu). Przy kadej prbie otrzymujesz wskazwk, mwic czy wpisana przez ciebie warto jest za dua, czy za maa.
Tak przedstawia si to w dziaaniu. Jako programici chcemy jednak zajrze do kodu rdowego i przekona si, w jaki sposb mona byo taki efekt osign. Czym prdzej wic zimy te pragnienia :D Pierwsz czynnoci podjt przez nasz program jest wylosowanie liczby, ktr uytkownik bdzie odgadywa. Zasadniczo odpowiadaj za to dwie pocztkowe linijki: srand ((int) time(NULL)); int nWylosowana = rand() % 100 + 1; Nie bdziemy obecnie zagbia si w szczegy ich funkcjonowania, gdy te zostan omwione w nastpnym rozdziale. Teraz moesz jedynie zapamita, i pierwszy wiersz, zawierajcy funkcj srand() (i jej osobliwy parametr), jest czym w rodzaju zakrcenia koem ruletki. Jego obecno sprawia, e aplikacja za kadym razem losuje nam inn liczb. Za samo losowanie odpowiada natomiast wyraenie z funkcj rand(). Obliczona warto tego jest od razu przypisywana do zmiennej nWylosowana i to o ni toczy bj niestrudzony gracz :) Kolejny pakiet kodu pozwala na wykonanie pierwszej prby odgadnicia waciwego wyniku. Nie wida tu adnych nowoci z podobnymi fragmentami spotykalimy si ju wielokrotnie i wyjanilimy je dogbnie. Zauwamy tylko, e liczba wpisana przez uytkownika jest zapamitywana w zmiennej nWprowadzona. O wiele bardziej interesujca jest dla nas ptla while, wystpujca dalej. To na niej spoczywa zadanie wywietlania graczowi wskazwek, umoliwiania mu kolejnych prb i sprawdzania wpisanych wartoci. Podobnie jak w przypadku do, wykonywanie tej ptli uzalenione jest spenieniem okrelonego kryterium. Tutaj jest nim niezgodno midzy liczb wylosowan na pocztku (zawart w zmiennej nWylosowana), a wprowadzon przez uytkownika
62
Podstawy programowania
(zmienna nWprowadzona). Zapisujemy to w postaci warunku nWprowadzona != nWylosowana. Oczywicie ptla wykonuje si do chwili, w ktrej zaoenie to przestaje by prawdziwe, a uytkownik poda waciw liczb. Wewntrz bloku ptli podejmowane za s dwie czynnoci. Najpierw wywietlana jest podpowied dla uytkownika. Mwi mu ona, czy wpisana przed chwil liczba jest wiksza czy mniejsza od szukanej. Gracz otrzymuje nastpnie kolejn szans na odgadnicie podanej wartoci. Gdy wreszcie uda mu si ta sztuka, raczony jest w nagrod odpowiednim komunikatem :) Tak oto przedstawia si funkcjonowanie powyszego programu przykadowego, ktrego witaln czci jest ptla while. Wczeniej natomiast zdylimy si dowiedzie i przekona, i konstrukcja ta bardzo przypomina poznan poprzednio ptl do. Na czym wic polega rnica midzy nimi? Jest ni mianowicie moment sprawdzania warunku ptli. Jak pamitamy, do czyni to na kocu kadego cyklu. Analogicznie, while dokonuje tego zawsze na pocztku swego przebiegu. Determinuje to do oczywiste nastpstwo: Ptla while moe nie wykona si ani razu, jeeli jej warunek bdzie od pocztku nieprawdziwy. W naszym przykadowym programie odpowiada to sytuacji, gdy gracz od razu trafia we waciw liczb. Naturalnie, jest to bardzo mao prawdopodobne (rzdu 1%), lecz jednak moliwe. Trzeba zatem przewidzie i odpowiednio zareagowa na taki przypadek, za ptla while rozwizuje nam ten problem praktycznie sama :) Na koniec tradycyjnie ju przyjrzymy si skadni omawianej konstrukcji: while (warunek) { instrukcje } Ponownie wynika z niej praktycznie wszystko: Dopki (while) zachodzi warunek, wykonuj instrukcje. Czy nie jest to wyjtkowo intuicyjne? ;) *** Tak oto poznalimy dwa typy ptli warunkowych ich dziaanie, skadni i sposb uywania. Tym samym dostae do rki narzdzia, ktre pozwol ci tworzy lepsze i bardziej skomplikowane programy. Jakkolwiek oba te mechanizmy maj bardzo due moliwoci, korzystanie z nich moe by w niektrych wypadkach nieco niewygodne. Na podobne okazje obmylono trzeci rodzaj ptli, z ktrym wanie teraz si zaznajomimy.
Dziaanie programu
// wypisanie dziesiciu liczb cakowitych w osobnych linijkach while (nLicznik <= 10) { std::cout << nLicznik << std::endl; nLicznik++; }
63
Powysze rozwizanie jest z pewnoci poprawne, aczkolwiek istnieje jeszcze lepsze :) W przypadku, gdy znamy z gry liczb przebiegw ptli, bardziej naturalne staje si uycie instrukcji for (dla). Zostaa ona bowiem stworzona specjalnie na takie okazje16 i sprawdza si w nich o wiele lepiej ni uniwersalna while. Korzystajcy z niej ekwiwalent powyszego kodu moe wyglda na przykad tak: for (int i = 1; i <= 10; i++) { std::cout << i << std::endl; } Jeeli uwanie przyjrzysz si obu jego wersjom, z pewnoci zdoasz domyle si oglnej zasady dziaania ptli for. Zanim dokadnie j wyjani, posu si bardziej wyrafinowanym przykadem do jej ilustracji: // For - ptla krokowa int Suma(int nLiczba) { int nSuma = 0; for (int i = 1; i <= nLiczba; i++) nSuma += i; return nSuma; } void main() { int nLiczba; std::cout << "Program oblicza sume od 1 do podanej liczby." << std::endl; std::cout << "Podaj ja: "; std::cin >> nLiczba; std::cout << "Suma liczb od 1 do " << nLiczba << " wynosi " << Suma(nLiczba) << "."; getch();
Mamy zatem kolejny superuyteczny programik do przeanalizowania ;) Bezzwocznie wic przystpmy do wykonania tego poytecznego zadania. Rzut oka na kod tudzie kompilacja i uruchomienie aplikacji prowadzi do susznego wniosku, i przeznaczeniem programu jest obliczanie sumy kilku pocztkowych liczb naturalnych. Zakres dodawania ustala przy tym sam uytkownik programu. Czynnoci sumowania zajmuje si tu odrbna funkcja Suma(), na ktrej skupimy obecnie ca nasz uwag.
for nie jest tylko wymysem twrcw C++. Podobne konstrukcje spotka mona waciwie w kadym jzyku programowania, istniej te nawet bardziej wyspecjalizowane ich odmiany. Trudno wic uzna t poczciw ptl za zbdne udziwnienie :)
16
64
Podstawy programowania
Pierwsza linijka tej funkcji to znana ju nam deklaracja zmiennej, poczona z jej inicjalizacj wartoci 0. Owa zmienna, nSuma, bdzie przechowywa obliczony wynik dodawania, ktry zostanie zwrcony jako rezultat caej funkcji. Najbardziej interesujcym fragmentem jest wystpujca dalej ptla for: for (int i = 1; i <= nLiczba; i++) nSuma += i; Wykonuje ona zasadnicze obliczenia: dodaje do zmiennej nSuma kolejne liczby naturalne, zatrzymujc si na podanym w funkcji parametrze. Cao odbywa si w nastpujcy, do prosty sposb: Instrukcja int i = 1 jest wykonywana raz na samym pocztku. Jak wida, jest to deklaracja i inicjalizacja zmiennej i. Nazywamy j licznikiem ptli. W kolejnych cyklach bdzie ona przyjmowa wartoci 1, 2, 3, itd. Kod nSuma += i; stanowi blok ptli17 i jest uruchamiany przy kadym jej przebiegu. Skoro za licznik i jest po kolei ustawiany na nastpujce po sobie liczby naturalne, ptla for staje si odpowiednikiem sekwencji instrukcji nSuma += 1; nSuma += 2; nSuma += 3; nSuma += 4; itd. Warunek i <= nLiczba okrela grn granic sumowania. Jego obecno sprawia, e ptla jest wykonywana tylko wtedy, gdy licznik i jest mniejszy lub rwny zmiennej nLiczba. Zgadza si to oczywicie z naszym zamysem. Wreszcie, na koniec kadego cyklu instrukcja i++ powoduje zwikszenie wartoci licznika o jeden. Po duszym zastanowieniu nad powyszym opisem mona niewtpliwie doj do wniosku, e nie jest on wcale taki skomplikowany, prawda? :) Zrozumienie go nie powinno nastrcza ci zbyt wielu trudnoci. Gdyby jednak tak byo, przypomnij sobie podan w tytule nazw ptli for krokowa. To cakiem trafne okrelenie dla tej konstrukcji. Jej zadaniem jest bowiem przebycie pewnej drogi (u nas s to liczby od 1 do wartoci zmiennej nLiczba) poprzez seri maych krokw i wykonanie po drodze jakich dziaa. Klarownie przedstawia to tene rysunek:
Mam nadziej, e teraz nie masz ju adnych kopotw ze zrozumieniem zasady dziaania naszego programu. Przyszed czas na zaprezentowanie skadni omawianej przez nas ptli: for ([pocztek]; [warunek]; [cykl]) { instrukcje }
Jak zapewne pamitasz, jedn linijk w bloku kodu moemy zapisa bez nawiasw klamrowych {} dowiedzielimy si tego przy okazji instrukcji if :)
17
Dziaanie programu
65
Na jej podstawie moemy dogbnie pozna funkcjonowanie tego wanego tworu programistycznego. Dowiemy si te, dlaczego konstrukcja for jest uwaana za jedn z mocnych stron jzyka C++. Zaczniemy od pocztku, czyli komendy oznaczonej jako pocztek :) Wykonuje si ona jeden raz, jeszcze przed wejciem we waciwy krg ptli. Zazwyczaj umieszczamy tu instrukcj, ktra ustawia licznik na warto pocztkow (moe to by poczone z jego deklaracj). warunek jest sprawdzany przed kadym cyklem instrukcji. Jeeli nie jest on speniony, ptla natychmiast koczy si. Zwykle wic wpisujemy w jego miejsce kod porwnujcy licznik z wartoci kocow. W kadym przebiegu, po wykonaniu instrukcji, ptla uruchamia jeszcze fragment zaznaczony jako cykl. Naturaln jego treci bdzie zatem zwikszenie lub zmniejszenie licznika (w zalenoci od tego, czy liczymy w gr czy w d). Inkrementacja czy dekrementacja nie jest bynajmniej jedyn czynnoci, jak moemy tutaj wykona na liczniku. Posuenie si choby mnoeniem, dzieleniem czy nawet bardziej zaawansowanymi funkcjami jest jak najbardziej dopuszczalne. Wpisujc na przykad i *= 2 otrzymamy kolejne potgi dwjki (2, 4, 8, 16 itd.), i += 10 wielokrotnoci dziesiciu, itp. Jest to znaczna przewaga nad wieloma innymi jzykami programowania, w ktrych liczniki analogicznych ptli mog si zmienia jedynie w postpie arytmetycznym (o sta warto - niekiedy nawet dopuszczalna jest tu wycznie jedynka!). Elastyczno ptli for polega midzy innymi na fakcie, i aden z trzech podanych w nawiasie parametrw nie jest obowizkowy! Wprawdzie na pierwszy rzut oka obecno kadego wydaje si tu absolutnie niezbdna, jednake pominicie ktrego (czasem nawet wszystkich) moe mie swoje logiczne uzasadnienie. Brak pocztku lub cyklu powoduje do przewidywalny skutek w chwili, gdy miayby zosta wykonane, program nie podejmie po prostu adnych akcji. O ile nieobecno instrukcji ustawiajcej licznik na warto pocztkow jest okolicznoci rzadko spotykan, o tyle pominicie frazy cykl jest konieczne, jeeli nie chcemy zmienia licznika przy kadym przebiegu ptli. Moemy to osign, umieszczajc odpowiedni kod np. wewntrz zagniedonego bloku if. Gdy natomiast opucimy warunek, iteracja nie bdzie miaa czego weryfikowa przy kadym swym obrocie, wic zaptli si w nieskoczono. Przerwanie tego bdnego koa bdzie moliwe tylko poprzez instrukcj break, ktr ju za chwil poznamy bliej. *** W ten oto sposb zawarlimy blisz znajomo z ptla krokow for. Nie jest to moe atwa konstrukcja, ale do wielu zastosowa zdaje si by bardzo wygodna. Z tego wzgldu bdziemy jej czsto uywali tak te robi wszyscy programici C++.
66
Podstawy programowania
Rola tej instrukcji w kontekcie ptli nie zmienia si ani na jot: jej wystpienie wewntrz bloku do, while lub for powoduje dokadnie ten sam efekt. Bez wzgldu na prawdziwo lub nieprawdziwo warunku ptli jest ona byskawicznie przerywana, a punkt wykonania programu przesuwa si do kolejnego wiersza za ni. Przy pomocy break moemy teraz nieco poprawi nasz program demonstrujcy ptl do: // Break przerwanie ptli void main() { int nLiczba; do {
std::cout << "Wprowadz liczbe wieksza od 10" << std::endl; std::cout << "lub zero, by zakonczyc program: "; std::cin >> nLiczba;
if (nLiczba == 0) break; } while (nLiczba <= 10); std::cout << "Nacisnij dowolny klawisz."; getch();
Mankament niemonoci zakoczenia aplikacji bez spenienia jej proby zosta tutaj skutecznie usunity. Mianowicie, gdy wprowadzimy liczb zero, instrukcja if skieruje program ku komendzie break, ktra natychmiast zakoczy ptl i uwolni uytkownika od irytujcego dania :) Podobny skutek (przerwanie ptli po wpisaniu przez uytkownika zera) osignlibymy zmieniajc warunek ptli tak, by stawa si prawdziwy rwnie wtedy, gdy zmienna nLiczba miaaby warto 0. W nastpnym rozdziale dowiemy si, jak poczyni podobn modyfikacj. Instrukcja continue jest uywana nieco rzadziej. Gdy program natrafi na ni wewntrz bloku ptli, wtedy automatycznie koczy biecy cykl i rozpoczyna nowy przebieg iteracji. Z instrukcji tej korzystamy najczciej wtedy, kiedy cz (zwykle wikszo) kodu ptli ma by wykonywana tylko pod okrelonym, dodatkowym warunkiem. *** Zakoczylimy wanie poznawanie bardzo wanych elementw jzyka C++, czyli ptli. Dowiedzielimy si o zasadach ich dziaania, skadni oraz przykadowych zastosowaniach. Tych ostatnich bdzie nam systematycznie przybywao wraz z postpami w sztuce programowania, gdy ptle to bardzo intensywnie wykorzystywany mechanizm nie tylko zreszt w C++.
Podsumowanie
Ten dugi i wany rozdzia prezentowa moliwoci C++ w zakresie sterowania przebiegiem aplikacji oraz sposobem jej dziaania. Pierwszym zagadnieniem byo bystrzejsze spojrzenie na funkcje, co obejmowao poznanie ich parametrw oraz zwracanych wartoci. Dalej zerknlimy na instrukcje warunkowe, ktre wreszcie dopuszczay nam przewidywa rne ewentualnoci pracy programu. Na
Dziaanie programu
67
koniec, ptle day nam okazj stworzy nieco mniej banalne aplikacje ni zwykle w tym i jedn gr! :D T drog nabylimy przeto umiejtno tworzenia programw wykonujcych niemal dowolne zadania. Pewnie teraz nie jeste o tym szczeglnie przekonany, jednak pamitaj, e poznanie instrumentw to tylko pierwszy krok do osignicia wirtuozerii. Niezastpiona jest praktyka w prawdziwym programowaniu, a sposobnoci do niej bdziesz mia z pewnoci bez liku - take w niniejszym kursie :)
Pytania i zadania
Tak obszerny i kluczowy rozdzia nie moe si obej bez susznego pakietu zada domowych ;) Oto i one:
Pytania
1. Jaka jest rola parametrw funkcji? 2. Czy ilo parametrw w deklaracji i wywoaniu funkcji moe by rna? Wskazwka: Poczytaj w MSDN o domylnych wartociach parametrw funkcji. 3. Co si stanie, jeeli nie umiecimy instrukcji break po wariancie case w bloku switch? 4. W jakich sytuacjach, oprcz niepodania warunku, ptla for bdzie si wykonywaa w nieskoczono? A kiedy nie wykona si ani razu? Czy podobnie jest z ptl while?
wiczenia
1. Stwrz program, ktry poprosi uytkownika o liczb cakowit i przyporzdkuje j do jednego z czterech przedziaw: liczb ujemnych, jednocyfrowych, dwucyfrowych lub pozostaych. Ktra z instrukcji if czy switch bdzie tu odpowiednia? 2. Napisz aplikacj wywietlajc list liczb od 1 do 100 z podanymi obok wartociami ich drugich potg (kwadratw). Jak ptl do, while czy for naleaoby tu zastosowa? 3. Zmodyfikuj program przykadowy prezentujcy ptl while. Niech zlicza on prby zgadnicia liczby podjte przez gracza i wywietla na kocu ich ilo.
4
OPERACJE NA ZMIENNYCH
S plusy dodatnie i plusy ujemne.
Lech Wasa
W tym rozdziale przyjrzymy si dokadnie zmiennym i wyraeniom w jzyku C++. Jak wiemy, su one do przechowywania wszelkich danych i dokonywania na rnego rodzaju manipulacji. Dziaania takie s podstaw kadej aplikacji, a w zoonych algorytmach gier komputerowych maj niebagatelne znaczenie. Poznamy wic szczegowo wikszo aspektw programowania zwizanych ze zmiennymi oraz zobaczymy czsto uywane operacje na danych liczbowych i tekstowych.
Zasig zmiennych
Gdy deklarujemy zmienn, podajemy jej typ i nazw to oczywiste. Mniej dostrzegalny jest fakt, i jednoczenie okrelamy te obszar obowizywania takiej deklaracji. Innymi sowy, definiujemy zasig zmiennej. Zasig (zakres, ang. scope) zmiennej to cz kodu, w ramach ktrej dana zmienna jest dostpna. Wyrniamy kilka rodzajw zasigw. Do wszystkich jednak stosuje si oglna, naturalna regua: niepoprawne jest jakiekolwiek uycie zmiennej przed jej deklaracj. Tak wic poniszy kod: std::cin >> nZmienna; int nZmienna; niechybnie spowoduje bd kompilacji. Sdz, e jest to do proste i logiczne nie moemy przecie wymaga od kompilatora znajomoci czego, o czym sami go wczeniej nie poinformowalimy. W niektrych jzykach programowania (na przykad Visual Basicu czy PHP) moemy jednak uywa niezadeklarowanych zmiennych. Wikszo programistw uwaa to za
70
Podstawy programowania
niedogodno i przyczyn powstawania trudnych do wykrycia bdw (spowodowanych choby literwkami). Ja osobicie cakowicie podzielam ten pogld :D Na razie poznamy dwa rodzaje zasigw lokalny i moduowy.
Zasig lokalny
Zakres lokalny obejmuje pojedynczy blok kodu. Jak pamitasz, takim blokiem nazywamy fragment listingu zawarty midzy nawiasami klamrowymi { }. Dobrym przykadem mog by tu bloki warunkowe instrukcji if, bloki ptli, a take cae funkcje. Ot kada zmienna deklarowana wewntrz takiego bloku ma wanie zasig lokalny. Zakres lokalny obejmuje kod od miejsca deklaracji zmiennej a do koca bloku, wraz z ewentualnymi blokami zagniedonymi. Te do mgliste stwierdzenia bd pewnie bardziej wymowne, jeeli zostan poparte odpowiednimi przykadami. Zerknijmy wic na poniszy kod: void main() { int nX; std::cin >> nX; if (nX > 0) { std::cout << nX; getch(); }
Jego dziaanie jest, mam nadziej, zupenie oczywiste (zreszt nieszczeglnie nas teraz interesuje :)). Przyjrzyjmy si raczej zmiennej nX. Jako e zadeklarowalimy j wewntrz bloku kodu w tym przypadku funkcji main() posiada ona zasig lokalny. Moemy zatem korzysta z niej do woli w caym tym bloku, a wic take w zagniedonej instrukcji if. Dla kontrastu spjrzmy teraz na inny, cho podobny kod: void main() { int nX = 1; if (nX > 0) { int nY = 10; } std::cout << nY; getch();
Powinien on wypisa liczb 10, prawda? C niezupenie :) Sama prba uruchomienia programu skazana jest na niepowodzenie: kompilator przyczepi si do przedostatniego wiersza, zawierajcego nazw zmiennej nY. Wyda mu si bowiem kompletnie nieznana! Ale dlaczego?! Przecie zadeklarowalimy j ledwie dwie linijki wyej! Czy nie moemy wic uy jej tutaj? Jeeli uwanie przeczytae poprzednie akapity, to zapewne znasz ju przyczyn niezadowolenia kompilatora. Mianowicie, zmienna nY ma zasig lokalny, obejmujcy
Operacje na zmiennych
71
wycznie blok if. Reszta funkcji main() nie naley ju do tego bloku, a zatem znajduje si poza zakresem nY. Nic dziwnego, e zmienna jest tam traktowana jako obca poza swoim zasigiem ona faktycznie nie istnieje, gdy jest usuwana z pamici w momencie jego opuszczenia. Zmiennych o zasigu lokalnym relatywnie najczciej uywamy jednak bezporednio we wntrzu funkcji. Przyjo si nawet nazywa je zmiennymi lokalnymi18 lub automatycznymi. Ich rol jest zazwyczaj przechowywanie tymczasowych danych, wykorzystywanych przez podprogramy, lub czciowych wynikw oblicze. Tak jak poszczeglne funkcje w programie, tak i ich zmienne lokalne s od siebie cakowicie niezalene. Istniej w pamici komputera jedynie podczas wykonywania funkcji i znikaj po jej zakoczeniu. Niemoliwe jest wic odwoanie do zmiennej lokalnej spoza jej macierzystej funkcji. Poniszy przykad ilustruje ten fakt: // LocalVariables - zmienne lokalne void Funkcja1() { int nX = 7; std::cout << "Zmienna lokalna nX funkcji Funkcja1(): " << nX << std::endl; } void Funkcja2() { int nX = 5; std::cout << "Zmienna lokalna nX funkcji Funkcja2(): " << nX << std::endl; } void main() { int nX = 3; Funkcja1(); Funkcja2(); std::cout << "Zmienna lokalna nX funkcji main(): " << nX << std::endl; } getch();
Mimo e we wszystkich trzech funkcjach (Funkcja1(), Funkcja2() i main()) nazwa zmiennej jest identyczna (nX), w kadym z tych przypadkw mamy do czynienia z zupenie inn zmienn.
Screen 17. Ta sama nazwa, lecz inne znaczenie. Kada z trzech lokalnych zmiennych cakowicie odrbna i niezalena od pozostaych
nX jest
Nie tylko zreszt w C++. Wprawdzie sporo jzykw jest uboszych o moliwo deklarowania zmiennych wewntrz blokw warunkowych, ptli czy podobnych, ale niemal wszystkie pozwalaj na stosowanie zmiennych lokalnych. Nazwa ta jest wic obecnie uywana w kontekcie dowolnego jzyka programowania.
18
72
Podstawy programowania
Mog one wspistnie obok siebie pomimo takich samych nazw, gdy ich zasigi nie pokrywaj si. Kompilator susznie wic traktuje je jako twory absolutnie niepowizane ze sob. I tak te jest w istocie s one wewntrznymi sprawami kadej z funkcji, do ktrych nikt nie ma prawa si miesza :) Takie wyodrbnianie niektrych elementw aplikacji nazywamy hermetyzacj (ang. encapsulation). Najprostszym jej wariantem s wanie podprogramy ze zmiennymi lokalnymi, niedostpnymi dla innych. Dalszym krokiem jest tworzenie klas i obiektw, ktre dokadnie poznamy w dalszej czci kursu. Zalet takiego dzielenia kodu na mniejsze, zamknite czci jest wiksza atwo modyfikacji oraz niezawodno. W duych projektach, realizowanych przez wiele osb, podzia na odrbne fragmenty jest w zasadzie nieodzowny, aby wsppraca midzy programistami przebiegaa bez problemw. Ze zmiennymi o zasigu lokalnym spotykalimy si dotychczas nieustannie w naszych programach przykadowych. Prawdopodobnie zatem nie bdziesz mia wikszych kopotw ze zrozumieniem sensu tego pojcia. Jego precyzyjne wyjanienie byo jednak nieodzowne, abym z czystym sumieniem mg kontynuowa :D
Zasig moduowy
Szerszym zasigiem zmiennych jest zakres moduowy. Posiadajce go zmienne s widoczne w caym module kodu. Moemy wic korzysta z nich we wszystkich funkcjach, ktre umiecimy w tyme module. Jeeli za jest to jedyny plik z kodem programu, to oczywicie zmienne te bd dostpne dla caej aplikacji. Nazywamy si je wtedy globalnymi. Aby zobaczy, jak dziaaj zmienne moduowe, przyjrzyj si nastpujcemu przykadowi: // ModularVariables - zmienne moduowe int nX = 10; void Funkcja() { std::cout << "Zmienna nX wewnatrz innej funkcji: " << nX << std::endl; } void main() { std::cout << "Zmienna nX wewnatrz funkcji main(): " << nX << std::endl; Funkcja(); } getch();
Zadeklarowana na pocztku zmienna nX ma wanie zasig moduowy. Odwoujc si do niej, obie funkcje (main() i Funkcja()) wywietlaj warto jednej i tej samej zmiennej.
Operacje na zmiennych
73
Jak wida, deklaracj zmiennej moduowej umieszczamy bezporednio w pliku rdowym, poza kodem wszystkich funkcji. Wyczenie jej na zewntrz podprogramw daje zatem atwy do przewidzenia skutek: zmienna staje si dostpna w caym module i we wszystkich zawartych w nim funkcjach. Oczywistym zastosowaniem dla takich zmiennych jest przechowywanie danych, z ktrych korzysta wiele procedur. Najczciej musz by one zachowane przez wikszo czasu dziaania programu i osigalne z kadego miejsca aplikacji. Typowym przykadem moe by chociaby numer aktualnego etapu w grze zrcznociowej czy nazwa pliku otwartego w edytorze tekstu. Dziki zastosowaniu zmiennych o zasigu moduowym dostp do takich kluczowych informacji nie stanowi ju problemu. Zakres moduowy dotyczy tylko jednego pliku z kodem rdowym. Jeli nasza aplikacja jest na tyle dua, bymy musieli podzieli j na kilka moduw, moe on wszake nie wystarcza. Rozwizaniem jest wtedy wyodrbnienie globalnych deklaracji we wasnym pliku nagwkowym i uycie dyrektywy #include. Bdziemy o tym szerzej mwi w niedalekiej przyszoci :)
Przesanianie nazw
Gdy uywamy zarwno zmiennych o zasigu lokalnym, jak i moduowym (czyli w normalnym programowaniu w zasadzie nieustannie), moliwa jest sytuacja, w ktrej z danego miejsca w kodzie dostpne s dwie zmienne o tej samej nazwie, lecz rnym zakresie. Wyglda to moe chociaby tak: int nX = 5; void main() { int nX = 10; std::cout << nX; } Pytanie brzmi: do ktrej zmiennej nX lokalnej czy moduowej - odnosi si instrukcja std::cout? Inaczej mwic, czy program wypisze liczb 10 czy 5? A moe w ogle si nie skompiluje? Zjawisko to nazywamy przesanianiem nazw (ang. name shadowing), a pojawio si ono wraz ze wprowadzeniem idei zasigu zmiennych. Tego rodzaju kolizja oznacze nie powoduje w C++19 bdu kompilacji, gdy jest ona rozwizywana w nieco inny sposb: Konflikt nazw zmiennych o rnym zasigu jest rozstrzygany zawsze na korzy zmiennej o wszym zakresie. Zazwyczaj oznacza to zmienn lokaln i tak te jest w naszym przypadku. Nie oznacza to jednak, e jej moduowy imiennik jest w funkcji main() niedostpny. Sposb odwoania si do niego ilustruje poniszy przykadowy program: // Shadowing - przesanianie nazw int nX = 4; void main() {
19
74
Podstawy programowania
int nX = 7; std::cout << "Lokalna zmienna nX: " << nX << std::endl; std::cout << "Modulowa zmienna nX: " << ::nX << std::endl; } getch();
Pierwsze odniesienie do nX w funkcji main() odnosi si wprawdzie do zmiennej lokalnej, lecz jednoczenie moemy odwoa si take do tej moduowej. Robimy to bowiem w nastpnej linijce: std::cout << "Modulowa zmienna nX: " << ::nX << std::endl; Poprzedzamy tu nazw zmiennej dwoma znakami dwukropka ::. Jest to tzw. operator zasigu. Wstawienie go mwi kompilatorowi, aby uy zmiennej globalnej zamiast lokalnej - czyli zrobi dokadnie to, o co nam chodzi :) Operator ten ma te kilka innych zastosowa, o ktrych powiemy niedugo (dokadniej przy okazji klas). Chocia C++ udostpnia nam tego rodzaju mechanizm20, do dobrej praktyki programistycznej naley niestosowanie go. Identyczne nazwy wprowadzaj bowiem zamt i pogarszaj czytelno kodu. Dlatego te do nazw zmiennych moduowych dodaje si zazwyczaj przedrostek21 g_ (od global), co pozwala atwo odrni je od lokalnych. Po zastosowaniu tej reguy nasz przykad wygldaby mniej wicej tak: int g_nX = 4; void main() { int nX = 7; std::cout << "Lokalna zmienna: " << nX << std::endl; std::cout << "Modulowa zmienna: " << g_nX << std::endl; } getch();
Nie ma ju potrzeby stosowania mao czytelnego operatora :: i cao wyglda przejrzycie i profesjonalnie ;) *** Zapoznalimy si zatem z nieatw ide zasigu zmiennych. Jest to jednoczenie bardzo wane pojcie, ktre trzeba dobrze zna, by nie popenia trudnych do wykrycia bdw. Mam nadziej, e jego opis oraz przykady byy na tyle przejrzyste, e nie miae powaniejszych kopotw ze zrozumieniem tego aspektu programowania.
Modyfikatory zmiennych
W aktualnym podrozdziale szczeglnie upodobalimy sobie deklaracje zmiennych. Oto bowiem omwimy kolejne zagadnienie z nimi zwizane tak zwane modyfikatory
Wikszo jzykw go nie posiada! Jest to element notacji wgierskiej, aczkolwiek szeroko stosowany przez wielu programistw. Wicej informacji w Dodatku A.
21
20
Operacje na zmiennych
(ang. modifiers). S to mianowicie dodatkowe okrelenia umieszczane w deklaracji zmiennej, nadajce jej pewne specjalne wasnoci. Zajmiemy si dwoma spord trzech dostpnych w C++ modyfikatorw. Pierwszy static chroni zmienn przed utrat wartoci po opuszczeniu jej zakresu przez program. Drugi za znany nam const oznacza sta, opisan ju jaki czas temu.
75
Zmienne statyczne
Kiedy aplikacja opuszcza zakres zmiennej lokalnej, wtedy ta jest usuwana z pamici. To cakowicie naturalne po co zachowywa zmienn, do ktrej i tak nie byoby dostpu? Logiczniejsze jest zaoszczdzenie pamici operacyjnej i pozbycie si nieuywanej wartoci, co te program skrztnie czyni. Z tego powodu przy ponownym wejciu w porzucony wczeniej zasig wszystkie podlegajce mu zmienne bd ustawione na swe pocztkowe wartoci. Niekiedy jest to zachowanie niepodane czasem wolelibymy, aby zmienne lokalne nie traciy swoich wartoci w takich sytuacjach. Najlepszym rozwizaniem jest wtedy uycie modyfikatora static. Rzumy okiem na poniszy przykad: // Static - zmienne statyczne void Funkcja() { static int nLicznik = 0; ++nLicznik; std::cout << "Funkcje wywolano po raz " << nLicznik << std::endl;
void main() { std::string strWybor; do { Funkcja(); std::cout << "Wpisz 'q', aby zakonczyc: "; std::cin >> strWybor; } while (strWybor != "q"); } w program jest raczej trywialny i jego jedynym zadaniem jest kilkukrotne uruchomienie podprogramu Funkcja(), dopki yczliwy uytkownik na to pozwala :) We wntrzu teje funkcji mamy zadeklarowan zmienn statyczn, ktra suy tam jako licznik uruchomie.
76
Podstawy programowania
Jego warto jest zachowywana pomidzy kolejnymi wywoaniami funkcji, gdy istnieje w pamici przez cay czas dziaania aplikacji22. Moemy wic kadorazowo inkrementowa t warto i pokazywa jako ilo uruchomie funkcji. Tak wanie dziaaj zmienne statyczne :) Deklaracja takiej zmiennej jest, jak widzielimy, nad wyraz prosta: static int nLicznik = 0; Wystarczy poprzedzi oznaczenie jej typu swkiem static i voila :) Nadal moemy take stosowa inicjalizacj do ustawienia pocztkowej wartoci zmiennej. Jest to wrcz konieczne gdybymy bowiem zastosowali zwyke przypisanie, odbywaoby si ono przy kadym wejciu w zasig zmiennej. Wypaczaoby to cakowicie sens stosowania modyfikatora static.
Stae
Stae omwilimy ju wczeniej, wic nie s dla ciebie nowoci. Obecnie podkrelimy ich zwizek ze zmiennymi. Jak (mam nadziej) pamitasz, aby zadeklarowa sta naley uy sowa const, na przykad: const float GRAWITACJA = 9.80655; const, podobnie jak static, jest modyfikatorem zmiennej. Stae posiadaj zatem wszystkie cechy zmiennych, takie jak typ czy zasig. Jedyn rnic jest oczywicie niemono zmiany wartoci staej. *** Tak oto uzupenilimy swe wiadomoci na temat zmiennych o ich zasig oraz modyfikatory. Uzbrojeni w t now wiedz moemy teraz miao poda dalej :D
Typy zmiennych
W C++ typ zmiennej jest spraw niezwykle wan. Gdy okrelamy go przy deklaracji, zostaje on trwale przywizany do zmiennej na cay czas dziaania programu. Nie moe wic zaj sytuacja, w ktrej zmienna zadeklarowana na przykad jako liczba cakowita zawiera informacj tekstow czy liczb rzeczywist. Niektre jzyki programowania pozwalaj jednak na to. Delphi i Visual Basic s wyposaone w specjalny typ Variant, ktry potrafi przechowywa zarwno dane liczbowe, jak i tekstowe. PHP natomiast w ogle nie wymaga podawania typu zmiennych. Chocia wymg ten wyglda na powany mankament C++, w rzeczywistoci wcale nim nie jest. Bardzo trudno wskaza czynno, ktra wymagaaby zmiennej uniwersalnego typu, mogcej przechowywa kady rodzaj danych. Jeeli nawet zaszaby takowa konieczno, moliwe jest zastosowanie przynajmniej kilku niemal rwnowanych
22
Operacje na zmiennych
rozwiza23. Generalnie jednak jestemy skazani na korzystanie z typw zmiennych, co mimo wszystko nie powinno nas smuci :) Na osod proponuj blisze przyjrzenie si im. Bdziemy mieli okazj zobaczy, e ich moliwoci, elastyczno i zastosowania s niezwykle szerokie.
77
Analogicznie, moglibymy doda przeciwstawny modyfikator signed (oznakowany, czyli ze znakiem; dodatni lub ujemny) do typw zmiennych, ktre maj zawiera zarwno liczby dodatnie, jak i ujemne: signed int nZmienna; // przechowuje liczby cakowite
Mona wykorzysta chociaby szablony, unie czy wskaniki. O kadym z tych elementw C++ powiemy sobie w dalszej czci kursu, wic cierpliwoci ;) 24 To oczywicie jedynie przykad. Na adnym wspczesnym systemie typ int nie ma tak maego zakresu. 25 Co nie jest wcale niemoliwe, a przy stosowaniu tablic (opisanych w nastpnym rozdziale) staje cakiem czste.
23
78
Podstawy programowania
Zazwyczaj tego nie robimy, gdy modyfikator ten jest niejako domylnie tam umieszczony i nie ma potrzeby jego wyranego stosowania. Jako podsumowanie proponuj diagram obrazujcy dziaanie poznanych modyfikatorw:
Schemat 6. Przedzia wartoci typw liczbowych ze znakiem (signed) i bez znaku (unsigned)
Widzimy, e zastosowanie unsigned powoduje przeniesienie ujemnej poowy przedziau zmiennej bezporednio za jej cz dodatni. Nie mamy wwczas moliwoci korzystania z liczb ujemnych, ale w zamian otrzymujemy dwukrotnie wicej miejsca na wartoci dodatnie. Tak to ju jest w programowaniu, e nie ma nic za darmo :D
26
1 bajt to 8 bitw.
Operacje na zmiennych
79
Dlatego te C++ udostpnia nam porczny zestaw dwch modyfikatorw, ktrymi moemy wpywa na wielko typu cakowitego. S to: short (krtki) oraz long (dugi). Uywamy ich podobnie jak signed i unsigned poprzedzajc typ int ktrym z nich: short int nZmienna; long int nZmienna; // "krtka" liczba cakowita // "duga" liczba cakowita
C znacz jednak te, nieco artobliwe, okrelenia krtkiej i dugiej liczby? Chyba najlepsz odpowiedzi bdzie tu stosowna tabelka :) nazwa int short int long int rozmiar 4 bajty 2 bajty 4 bajty przedzia wartoci od 231 do +231 - 1 od -32 768 do +32 767 od 231 do +231 - 1
Niespodziank moe by brak typu o rozmiarze 1 bajta. Jest on jednak obecny w C++ to typ char :) Owszem, reprezentuje on znak. Nie zapominajmy jednak, e komputer operuje na znakach jak na odpowiadajcym im kodom liczbowym. Dlatego te typ char jest w istocie take typem liczb cakowitych! Visual C++ udostpnia te nieco lepszy sposb na okrelenie wielkoci typu liczbowego. Jest nim uycie frazy __intn, gdzie n oznacza rozmiar zmiennej w bitach. Oto przykady: __int8 nZmienna; __int16 nZmienna; __int32 nZmienna; __int64 nZmienna; // // // // 8 bitw == 1 bajt, wartoci od -128 do 127 16 bitw == 2 bajty, wartoci od -32768 do 32767 32 bity == 4 bajty, wartoci od -231 do 231 1 64 bity == 8 bajtw, wartoci od -263 do 263 1
__int8 jest wic rwny typowi char, __int16 short int, a __int32 int lub long int. Gigantyczny typ __int64 nie ma natomiast swojego odpowiednika.
To zastrzeenie jest konieczne. Wprawdzie int zajmuje 4 bajty we wszystkich 32-bitowych kompilatorach, ale w przypadku pozostaych typw moe by inaczej! Standard C++ wymaga jedynie, aby short int by mniejszy lub rwny od int-a, a long int wikszy lub rwny int-owi. 28 Zainteresowanych odsyam do Dodatku B.
27
80
Podstawy programowania
dokadnoci, z kilkoma cyframi po przecinku. Ze wzgldu na t waciwo (zmienn precyzj) typy rzeczywiste nazywamy czsto zmiennoprzecinkowymi. Zgadza si typy. Podobnie jak w przypadku liczb cakowitych moemy doda do typu float odpowiednie modyfikatory. I podobnie jak wwczas, ujrzymy je w naleytej tabelce :) nazwa float double float rozmiar 4 bajty 8 bajtw precyzja 67 cyfr 15-16 cyfr
double (podwjny), zgodnie ze swoj nazw, zwiksza dwukrotnie rozmiar zmiennej oraz poprawia jej dokadno. Tak zmodyfikowana zmienna jest nazywana czasem liczb podwjnej precyzji - w odrnieniu od float, ktra ma tylko pojedyncz precyzj.
Skrcone nazwy
Na koniec warto nadmieni jeszcze o monoci skrcenia nazw typw zawierajcych modyfikatory. W takich sytuacjach moemy bowiem cakowicie pomin sowa int i float. Przykadowe deklaracje: unsigned int uZmienna; short int nZmienna; unsigned long int nZmienna; double float fZmienna; mog zatem wyglda tak: unsigned uZmienna; short nZmienna; unsigned long nZmienna; double fZmienna; Maa rzecz, a cieszy ;) Mamy te kolejny dowd na du kondensacj skadni C++. *** Poznane przed chwil modyfikatory umoliwiaj nam wiksz kontrol nad zmiennymi w programie. Pozwalaj bowiem na dokadne okrelenie, jak zmienn chcemy w danej chwili zadeklarowa i nie dopuszczaj, by kompilator myla za nas ;D
Pomocne konstrukcje
Zapoznamy si teraz z dwoma elementami jzyka C++, ktre uatwiaj nieco prac z rnymi typami zmiennych. Bdzie to instrukcja typedef oraz operator sizeof.
Instrukcja typedef
Wprowadzenie modyfikatorw sprawio, e oto mamy ju nie kilka, a przynajmniej kilkanacie typw zmiennych. Nazwy tyche typw s przy tym dosy dugie i wielokrotne ich wpisywanie moe nam zabiera duo czasu. Zbyt duo.
Operacje na zmiennych
81
Dlatego te (i nie tylko dlatego) C++ posiada instrukcj typedef (ang. type definition definicja typu). Moemy jej uy do nadania nowej nazwy (aliasu) dla ju istniejcego typu. Zastosowanie tego mechanizmu moe wyglda choby tak: typedef unsigned int UINT; Powysza linijka kodu mwi kompilatorowi, e od tego momentu typ unsigned int posiada take dodatkow nazw - UINT. Staj si ona dokadnym synonimem pierwotnego okrelenia. Odtd bowiem obie deklaracje: unsigned int uZmienna; oraz UINT uZmienna; s w peni rwnowane. Uycie typedef, podobnie jak jej skadnia, jest bardzo proste: typedef typ nazwa; Skutkiem skorzystania z tej instrukcji jest moliwo wstawiania nowej nazwy tam, gdzie wczeniej musielimy zadowoli si jedynie starym typem. Obejmuje to zarwno deklaracje zmiennych, jak i parametrw funkcji tudzie zwracanych przez nie wartoci. Dotyczy wic wszystkich sytuacji, w ktrych moglimy korzysta ze starego typu nowa nazwa nie jest pod tym wzgldem w aden sposb uomna. Jaka jest praktyczna korzy z definiowania wasnych okrele dla istniejcych typw? Pierwsz z nich jest przytoczone wczeniej skracanie nazw, ktre z pewnoci pozytywnie wpynie na stan naszych klawiatur ;)) Oszczdnociowe przydomki w rodzaju zaprezentowanego wyej UINT s przy tym na tyle wygodne i szeroko wykorzystywane, e niektre kompilatory (w tym i nasz Visual C++) nie wymagaj nawet ich jawnego okrelenia! Moliwo dowolnego oznaczania typw pozwala rwnie na nadawanie im znaczcych nazw, ktre obrazuj ich zastosowania w aplikacji. Z przykadem podobnego postpowania spotkasz si przy tworzeniu programw okienkowych w Windows. Uywa si tam wielu typw o nazwach takich jak HWND, HINSTANCE, WPARAM, LRESULT itp., z ktrych kady jest jedynie aliasem na 32-bitow liczb cakowit bez znaku. Stosowanie takiego nazewnictwa powanie poprawia czytelno kodu oczywicie pod warunkiem, e znamy znaczenie stosowanych nazw :) Zauwamy pewien istotny fakt. Mianowicie, typedef nie tworzy nam adnych nowych typw, a jedynie duplikuje ju istniejce. Zmiany, ktre czyni w sposobie programowania, s wic stricte kosmetyczne, cho na pierwszy rzut oka mog wyglda na do znaczne. Do kreowania zupenie nowych typw su inne elementy jzyka C++, z ktrych cz poznamy w nastpnym rozdziale.
Operator sizeof
Przy okazji prezentacji rnych typw zmiennych podawaem zawsze ilo bajtw, ktr zajmuje w pamici kady z nich. Przypominaem te kilka razy, e wielkoci te s prawdziwe jedynie w przypadku kompilatorw 32-bitowych, a niektre nawet tylko w Visual C++.
82
Podstawy programowania
Z tego powodu mog one szybko sta si po prostu nieaktualne. Przy dzisiejszym tempie postpu technicznego, szczeglnie w informatyce, wszelkie zmiany dokonuj si w zasadzie nieustannie29. W tej gonitwie take programici nie mog pozostawa w tyle w przeciwnym wypadku przystosowanie ich starych aplikacji do nowych warunkw technologicznych moe kosztowa mnstwo czasu i wysiku. Jednoczenie wiele programw opiera swe dziaanie na rozmiarze typw podstawowych. Wystarczy napomkn o tak czstej czynnoci, jak zapisywanie danych do plikw albo przesyanie ich poprzez sie. Jeliby kady program musia mie wpisane na sztywno rzeczone wielkoci, wtedy spora cz pracy programistw upywaaby na dostosowywaniu ich do potrzeb nowych platform sprztowych, na ktrych miayby dziaa istniejce aplikacje. A co z tworzeniem cakiem nowych produktw? Szczliwie twrcy C++ byli na tyle zapobiegliwi, eby uchroni nas, koderw, od tej koszmarnej perspektywy. Wprowadzili bowiem operator sizeof (rozmiar czego), ktry pozwala na uzyskanie wielkoci zmiennej (lub jej typu) w trakcie dziaania programu. Spojrzenie na poniszy przykad powinno nam przybliy funkcjonowanie tego operatora: // Sizeof - pobranie rozmiaru zmiennej lub typu #include <iostream> #include <conio.h> void main() { std::cout std::cout std::cout std::cout std::cout std::cout
"Typy liczb calkowitych:" << std::endl; "- int: " << sizeof(int) << std::endl; "- short int: " << sizeof(short int) << std::endl; "- long int: " << sizeof(long int) << std::endl; "- char: " << sizeof(char) << std::endl; std::endl;
std::cout << "Typy liczb zmiennoprzecinkowych:" << std::endl; std::cout << "- float: " << sizeof(float) << std::endl; std::cout << "- double: " << sizeof(double) << std::endl; } getch();
Uruchomienie programu z listingu powyej, jak susznie mona przypuszcza, bdzie nam skutkowao krtkim zestawieniem rozmiarw typw podstawowych.
29 W chwili pisania tych sw pod koniec roku 2003 mamy ju coraz wyraniejsze widoki na powane wykorzystanie procesorw 64-bitowych w domowych komputerach. Jednym ze skutkw tego zwikszenia bitowoci bdzie zmiana rozmiaru typu liczbowego int.
Operacje na zmiennych
Po uwanym zlustrowaniu kodu rdowego wida jak na doni dziaanie oraz sposb uycia operatora sizeof. Wystarczy poda mu typ lub zmienn jako parametr, by otrzyma w wyniku jego rozmiar w bajtach30. Potem moemy zrobi z tym rezultatem dokadnie to samo, co z kad inn liczb cakowit chociaby wywietli j w konsoli przy uyciu strumienia wyjcia.
83
Zastosowanie sizeof nie ogranicza si li tylko do typw wbudowanych. Gdy w kolejnych rozdziaach nauczymy si tworzy wasne typy zmiennych, bdziemy mogli w identyczny sposb ustala ich rozmiary przy pomocy poznanego przed momentem operatora. Nie da si ukry, e bardzo lubimy takie uniwersalne rozwizania :D Warto, ktr zwraca operator sizeof, naley do specjalnego typu size_t. Zazwyczaj jest on tosamy z unsigned int, czyli liczb bez znaku (bo przecie rozmiar nie moe by ujemny). Naley wic uwaa, aby nie przypisywa jej do zmiennej, ktra jest liczb ze znakiem.
Rzutowanie
Idea typw zmiennych wprowadza nam pewien sposb klasyfikacji wartoci. Niektre z nich uznajemy bowiem za liczby cakowite (3, -17, 44, 67*88 itd.), inne za zmiennoprzecinkowe (7.189, 12.56, -1.41, 8.0 itd.), jeszcze inne za tekst ("ABC", "Hello world!" itp.) czy pojedyncze znaki31 ('F', '@' itd.). Kady z tych rodzajw odpowiada nam ktremu z poznanych typw zmiennych. Najczciej te nie s one ze sob kompatybilne innymi sowy, nie pasuj do siebie, jak chociaby tutaj: int nX = 14; int nY = 0.333 * nX; Wynikiem dziaania w drugiej linijce bdzie przecie liczba rzeczywista z czci uamkow, ktr nijak nie mona wpasowa w ciasne ramy typu int, zezwalajcego jedynie na wartoci cakowite32. Oczywicie, w podanym przykadzie wystarczy zmieni typ drugiej zmiennej na float, by rozwiza nurtujcy nas problem. Nie zawsze jednak bdziemy mogli pozwoli sobie na podobne kompromisy, gdy czsto jedynym wyjciem stanie si wymuszenie na kompilatorze zaakceptowania kopotliwego kodu. Aby to uczyni, musimy rzutowa (ang. cast) przypisywan warto na docelowy typ na przykad int. Rzutowanie dziaa troch na zasadzie umowy z kompilatorem, ktra w naszym przypadku mogaby brzmie tak: Wiem, e naprawd jest to liczba zmiennoprzecinkowa, ale wanie tutaj chc, aby staa si liczb cakowit typu int, bo musz j przypisa do zmiennej tego typu. Takie porozumienie wymaga ustpstw od obu stron kompilator musi pogodzi si z chwilowym zaprzestaniem kontroli typw, a programista powinien liczy si z ewentualn utrat czci danych (w naszym przykadzie powicimy cyfry po przecinku).
30 cilej mwic, sizeof podaje nam rozmiar obiektu w stosunku do wielkoci typu char. Jednake typ ten ma najczciej wielko dokadnie 1 bajta, zatem utaro si stwierdzenie, i sizeof zwraca w wyniku ilo bajtw. Nie ma w zasadzie adnego powodu, by uzna to za bd. 31 Znaki s typu char, ktry jak wiemy jest take typem liczbowym. W C++ kod znaku jest po prostu jednoznaczny z nim samym, dlatego moemy go interpretowa zarwno jako symbol, jak i warto liczbow. 32 Niektre kompilatory (w tym i Visual C++) zaakceptuj powyszy kod, jednake nie obejdzie si bez ostrzee o moliwej (i faktycznej!) utracie danych. Wprawdzie niektrzy nie przejmuj si w ogle takimi ostrzeeniami, my jednak nie bdziemy tak krtkowzroczni :D
84
Podstawy programowania
Proste rzutowanie
Zatem do dziea! Zobaczmy, jak w praktyce wygldaj takie negocjacje :) Zostawimy na razie ten trywialny, dwulinijkowy przykad (wrcimy jeszcze do niego) i zajmiemy si powaniejszym programem. Oto i on: // SimpleCast - proste rzutowanie typw void main() { for (int i = 32; i { std::cout << std::cout << std::cout << std::cout << std::cout << } } getch();
< 256; i += 4) "| " << (char) (char) (i + 1) (char) (i + 2) (char) (i + 3) std::endl; (i) << " == " << << " == " << i + << " == " << i + << " == " << i + i 1 2 3 << << << << " " " " | "; | "; | "; |";
Huh, faktycznie nie jest to banalny kod :) Wykonywana przeze czynno jest jednak do prosta. Aplikacja ta pokazuje nam tablic kolejnych znakw wraz z odpowiadajcymi im kodami ANSI.
Najwaniejsza jest tu dla nas sama operacja rzutowania, ale warto przyjrze si funkcjonowaniu programu jako caoci. Zawarta w nim ptla for wykonuje si dla co czwartej wartoci licznika z przedziau od 32 do 255. Skutkuje to faktem, i znaki s wywietlane wierszami, po 4 w kadym. Pomijamy znaki o kodach mniejszych od 32 (czyli te z zakresu 031), poniewa s to specjalne symbole sterujce, zasadniczo nieprzeznaczone do wywietlania na ekranie. Znajdziemy wrd nich na przykad tabulator (kod 9), znak powrotu karetki (kod 13), koca wiersza (kod 10) czy sygna bdu (kod 7).
Operacje na zmiennych
Za prezentacj pojedynczego wiersza odpowiadaj te wielce interesujce instrukcje: std::cout std::cout std::cout std::cout << "| " << (char) (i) << " << (char) (i + 1) << " << (char) (i + 2) << " << (char) (i + 3) << " == == == == " " " " << << << << i << i + 1 << i + 2 << i + 3 << " " " " | "; | "; | "; |";
85
Sdzc po widocznym ich efekcie, kada z nich wywietla nam jeden znak oraz odpowiadajcy mu kod ANSI. Przygldajc si bliej temu listingowi, widzimy, e zarwno pokazanie znaku, jak i przynalenej mu wartoci liczbowej odbywa si zawsze przy pomocy tego samego wyraenia. Jest nim odpowiednio i, i + 1, i + 2 lub i + 3. Jak to si dzieje, e raz jest ono interpretowane jako znak, a innym razem jako liczba? Domylasz si zapewne niebagatelnej roli rzutowania w dziaaniu tej magii :) Istotnie, jest ono konieczne. Jako e licznik i jest zmienn typu int, zacytowane wyej cztery wyraenia take nale do tego typu. Przesanie ich do strumienia wyjcia w niezmienionej postaci powoduje wywietlenie ich wartoci w formie liczb. W ten sposb pokazujemy kody ANSI kolejnych znakw. Aby wywietli same symbole musimy jednak oszuka nieco nasz strumie std::cout, rzutujc wspomniane wartoci liczbowe na typ char. Dziki temu zostan one potraktowane jako znaki i tako wywietlone w konsoli. Zobaczmy, w jaki sposb realizujemy tutaj to osawione rzutowanie. Spjrzmy mianowicie na jeden z czterech podobnych kawakw kodu: (char) (i + 1) Ten niepozorny fragment wykonuje ca wak operacj, ktr nazywamy rzutowaniem. Zapisanie w nawiasach nazwy typu char przed wyraeniem i + 1 (dla jasnoci umieszczonym rwnie w nawiasach) powoduje bowiem, i wynik tak ujtego dziaania zostaje uznany jako podpadajcy pod typ char. Tak jest te traktowany przez strumie wyjcia, dziki czemu moemy go oglda jako znak, a nie liczb. Zatem, aby rzutowa jakie wyraenie na wybrany typ, musimy uy niezwykle prostej konstrukcji: (typ) wyraenie wyraenie moe by przy tym ujte w nawias lub nie; zazwyczaj jednak stosuje si nawiasy, by unikn potencjalnych kopotw z kolejnoci operatorw. Mona take uy skadni typ(wyraenie). Stosuje si j rzadziej, gdy przypomina wywoanie funkcji i moe by przez to przyczyn pomyek. Wrmy teraz do naszego pierwotnego przykadu. Rozwizanie problemu, ktry wczeniej przedstawia, powinno by ju banalne: int nX = 14; int nY = (int) (0.333 * nX); Po takich manipulacjach zmienna nY bdzie przechowywaa cz cakowit z wyniku podanego mnoenia. Oczywicie tracimy w ten sposb dokadno oblicze, co jest jednak nieuniknion cen kompromisu towarzyszcego rzutowaniu :)
86
Podstawy programowania
Operator static_cast
Umiemy ju dokonywa rzutowania, poprzedzajc wyraenie nazw typu napisan w nawiasach. Taki sposb postpowania wywodzi si jeszcze z zamierzchych czasw jzyka C33, poprzednika C++. Czyby miao to znaczy, e jest on zy? Powiedzmy, e nie jest wystarczajco dobry :) Nie przecz, e na pocztku moe wydawa si wietnym rozwizaniem klarownym, prostym, niewymagajcym wiele pisania etc. Jednak im dalej w las, tym wicej mieci: ju teraz dokadniejsze spojrzenie ujawnia nam wiele mankamentw, a w miar zwikszania si twoich umiejtnoci i wiedzy dostrzeesz ich jeszcze wicej. Spjrzmy choby na sam skadni. Oprcz swojej niewtpliwej prostoty posiada dwie zdecydowanie nieprzyjemne cechy. Po pierwsze, zwiksza nam ilo nawiasw w wyraeniach, ktre zawieraj rzutowanie. A przecie nawet i bez niego potrafi one by dostatecznie skomplikowane. Czste przecie uycie kilku operatorw, kilku funkcji (z ktrych kada ma pewnie po kilka parametrw) oraz kilku dodatkowych nawiasw (aby nie kopota si kolejnoci dziaa) gmatwa nasze wyraenia w dostatecznym ju stopniu. Jeeli dodamy do tego jeszcze par rzutowa, moe nam wyj co w tym rodzaju: int nX = (int) (((2 * nY) / (float) (nZ + 3)) (int) Funkcja(nY * 7)); Konwersje w formie (typ) wyraenie z pewnoci nie poprawiaj tu czytelnoci kodu. Drugim problemem jest znowu kolejno dziaa. Pytanie za pi punktw: jak warto ma zmienna nY w poniszym fragmencie? float fX = 0.75; int nY = (int) fX * 3; Zatem? Jeeli obecne w drugiej linijce rzutowanie na int dotyczy jedynie zmiennej fX, to jej warto (0.75) zostanie zaokrglona do zera, zatem nY bdzie przypisane rwnie zero. Jeli jednak konwersji na int zostanie poddane cae wyraenie (0.75 * 3, czyli 2.25), to nY przyjmie warto 2! Wybrnicie z tego dylematu to kolejna para nawiasw, obejmujca t cz wyraenia, ktr faktycznie chcemy rzutowa. Wyglda wic na to, e nie opdzimy si od czstego stosowania znakw ( i ). Skadnia to jednak nie jedyny kopot. Tak naprawd o wiele waniejsze s kwestie zwizane ze sposobem, w jaki jest realizowane samo rzutowanie. Niestety, na razie jeste w niezbyt komfortowej sytuacji, gdy musisz zaakceptowa pewien fakt bez uzasadnienia (na wiar :D). Brzmi on nastpujco: Rzutowanie w formie (typ) wyraenie, zwane te rzutowaniem w stylu C, nie jest zalecane do stosowania w C++. Dokadnie przyczyny takiego stanu rzeczy poznasz przy okazji omawiania klas i programowania obiektowego34.
Nazywa si go nawet rzutowaniem w stylu C. Dla szczeglnie dociekliwych mam wszake wyjanienie czciowe. Mianowicie, rzutowanie w stylu C nie rozrnia nam tzw. bezpiecznych i niebezpiecznych konwersji. Za bezpieczn moemy uzna zamian jednego typu liczbowego na drugi czy wskanika szczegowego na wskanik bardziej oglny (np. int* na void* - o wskanikach powiemy sobie szerzej, gdy ju uporamy si z podstawami :)). Niebezpieczne rzutowanie to konwersja midzy niezwizanymi ze sob typami, na przykad liczb i tekstem; w zasadzie nie powinno si takich rzeczy robi.
34 33
Operacje na zmiennych
87
No dobrze, zamy, e uznajemy t odgrn rad35 i zobowizujemy si nie stosowa rzutowania nawiasowego w swoich programach. Czy to znaczy, e w ogle tracimy moliwo konwersji zmiennych jednego typu na inne?! Rzeczywisto na szczcie nie jest a tak straszna :) C++ posiada bowiem a cztery operatory rzutowania, ktre s najlepszym sposobem na realizacj zamiany typw w tym jzyku. Bdziemy sukcesywnie poznawa je wszystkie, a zaczniemy od najczciej stosowanego tytuowego static_cast. static_cast (rzutowanie statyczne) nie ma nic wsplnego z modyfikatorem static i zmiennymi statycznymi. Operator ten suy do przeprowadzania najbardziej pospolitych konwersji, ktre jednak s spotykane najczciej. Moemy go stosowa wszdzie, gdzie sposb zamiany jest oczywisty zarwno dla nas, jak i kompilatora ;) Najlepiej po prostu zawsze uywa static_cast, uciekajc si do innych rodkw, gdy ten zawodzi i nie jest akceptowany przez kompilator (albo wie si z pokazaniem ostrzeenia). W szczeglnoci, moemy i powinnimy korzysta ze static_cast przy rzutowaniu midzy typami podstawowymi. Zobaczmy zreszt, jak wygldaoby ono dla naszego ostatniego przykadu: float fX = 0.75; int nY = static_cast<int>(fX * 3); Widzimy, e uycie tego operatora od razu likwiduje nam niejednoznaczno, na ktr poprzednio zwrcilimy uwag. Wyraenie poddawane rzutowaniu musimy bowiem uj w nawiasy okrge. Ciekawy jest sposb zapisu nazwy typu, na ktry rzutujemy. Znaki < i >, oprcz tego e s operatorami mniejszoci i wikszoci, tworz par nawiasw ostrych. Pomidzy nimi wpisujemy okrelenie docelowego typu. Pena skadnia operatora static_cast wyglda wic nastpujco: static_cast<typ>(wyraenie) By moe jest ona bardziej skomplikowana od zwykego rzutowania, ale uywajc jej osigamy wiele korzyci, o ktrych moge si naocznie przekona :) Warto te wspomnie, e trzy pozostae operatory rzutowania maj identyczn posta oczywicie z wyjtkiem sowa static_cast, ktre jest zastpione innym. *** T uwag koczymy omawianie rnych aspektw zwizanych z typami zmiennych w jzyku C++. Wreszcie zajmiemy si tytuowymi zagadnieniami tego rozdziau, czyli czynnociach, ktre moemy wykonywa na zmiennych.
Problem z rzutowaniem w stylu C polega na tym, i zupenie nie rozrnia tych dwch rodzajw zamiany. Pozostaje tak samo niewzruszone na niewinn konwersj z float na int oraz, powiedzmy, na zupenie nienaturaln zmian std::string na bool. Nietrudno domyle si, e zwiksza to prawdopodobiestwo wystpowania rnego rodzaju bdw. 35 Jak wszystko, co dotyczy fundamentw jzyka C++, pochodzi ona od jego Komitetu Standaryzacyjnego.
88
Podstawy programowania
Kalkulacje na liczbach
Poznamy teraz kilka standardowych operacji, ktre moemy wykonywa na danych liczbowych. Najpierw bd to odpowiednie funkcje, ktrych dostarcza nam C++, a nastpnie uzupenienie wiadomoci o operatorach arytmetycznych. Zaczynajmy wic :)
Przydatne funkcje
C++ udostpnia nam wiele funkcji matematycznych, dziki ktrym moemy przeprowadza proste i nieco bardziej zoone obliczenia. Prawie wszystkie s zawarte w pliku nagwkowym cmath, dlatego te musimy doczy ten plik do kadego programu, w ktrym chcemy korzysta z tych funkcji. Robimy to analogicznie jak w przypadku innych nagwkw umieszczajc na pocztku naszego kodu dyrektyw: #include <cmath> Po dopenieniu tej drobnej formalnoci moemy korzysta z caego bogactwa narzdzi matematycznych, jakie zapewnia nam C++. Spjrzmy wic, jak si one przedstawiaj.
Funkcje potgowe
W przeciwiestwie do niektrych jzykw programowania, C++ nie posiada oddzielnego operatora potgowania36. Zamiast niego mamy natomiast funkcj pow() (ang. power potga), ktra prezentuje si nastpujco: double pow(double base, double exponent); Jak wida, bierze ona dwa parametry. Pierwszym (base) jest podstawa potgi, a drugim (exponent) jej wykadnik. W wyniku zwracany jest oczywicie wynik potgowania (a wic warto wyraenia baseexponent). Podobn do powyszej deklaracj funkcji, przedstawiajc jej nazw, ilo i typy parametrw oraz typ zwracanej wartoci, nazywamy prototypem. Oto kilka przykadw wykorzystania funkcji pow(): double fX; fX = pow(2, 8); fX = pow(3, 4); fX = pow(5, -1); // sma potga dwjki, czyli 256 // czwarta potga trjki, czyli 81 // odwrotno pitki, czyli 0.2
Inn rwnie czsto wykonywan czynnoci jest pierwiastkowanie. Realizuje j midzy innymi funkcja sqrt() (ang. square root pierwiastek kwadratowy): double sqrt(double x); Jej jedyny parametr to oczywicie liczba, ktra chcemy pierwiastkowa. Uycie tej funkcji jest zatem niezwykle intuicyjne: fX = sqrt(64); fX = sqrt(2);
36
Znak ^, ktry suy w nich do wykonywania tego dziaania, jest w C++ zarezerwowany dla jednej z operacji bitowych rnicy symetrycznej. Wicej informacji na ten temat moesz znale w Dodatku B, Reprezentacja danych w pamici.
Operacje na zmiennych
fX = sqrt(pow(fY, 2)); // fY
89
Nie ma natomiast wbudowanej formuy, ktra obliczaaby pierwiastek dowolnego stopnia z danej liczby. Moemy jednak atwo napisa j sami, korzystajc z prostej wasnoci:
a
x=x
1 a
Po przeoeniu tego rwnania na C++ uzyskujemy nastpujc funkcj: double root(double x, double a) { return pow(x, 1 / a); }
Zapisanie jej definicji w jednej linijce jest cakowicie dopuszczalne i, jak wida, bardzo wygodne. Elastyczno skadni C++ pozwala wic na zupenie dowoln organizacj kodu. Dokadny opis poznanych funkcji pow() i sqrt() znajdziesz w MSDN.
Natomiast funkcj wykadnicz o dowolnej podstawie uzyskujemy, stosujc omwion ju wczeniej formu pow(). Przeciwstawne do funkcji wykadniczych s logarytmy. Tutaj mamy a dwie odpowiednie funkcje :) Pierwsza z nich to log(): double log(double x); Jest to logarytm naturalny (o podstawie e), a wic funkcja dokadnie do odwrotna do poprzedniej exp(). Ot dla danej liczby x zwraca nam warto wykadnika, do ktrego musielibymy podnie e, by otrzyma x. Dla penej jasnoci zerknijmy na ponisze przykady: fX = log(1); fX = log(10); fX = log(exp(x)); // 0 // 2.302585093 // x
Drug funkcj jest log10(), czyli logarytm dziesitny (o podstawie 10): double log10(double x);
37
Tak zwanej staej Nepera, podstawy logarytmw naturalnych - rwnej w przyblieniu 2.71828182845904.
90
Podstawy programowania
Analogicznie, funkcja ta zwraca wykadnik, do ktrego naleaoby podnie dziesitk, aby otrzyma podan liczb x, na przykad: fX = log10(1000); fX = log10(1); fX = log10(pow(10, x)); // 3 (bo 103 == 1000) // 0 // x
Niestety, znowu (podobnie jak w przypadku pierwiastkw) nie mamy bardziej uniwersalnego odpowiednika tych dwch funkcji, czyli logarytmu o dowolnej podstawie. Ponownie jednak moemy skorzysta z odpowiedniej tosamoci matematycznej38:
log a x =
log b x log b a
Nasza wasna funkcja moe wic wyglda tak: double log_a(double a, double x) { return log(x) / log(a); }
Oczywicie uycie log10() w miejsce log() jest rwnie poprawne. Zainteresowanych ponownie odsyam do MSDN celem poznania dokadnego opisu funkcji exp() oraz log() i log10().
Funkcje trygonometryczne
Dla nas, (przyszych) programistw gier, funkcje trygonometryczne s szczeglnie przydatne, gdy bdziemy korzysta z nich niezwykle czsto choby przy rnorakich obrotach. Wypadaoby zatem dobrze zna ich odpowiedniki w jzyku C++. Na pocztek przypomnijmy sobie (znane, mam nadziej :D) okrelenia funkcji trygonometrycznych. Posuy nam do tego poniszy rysunek:
38
Operacje na zmiennych
Zwrmy uwag, e trzy ostatnie funkcje s okrelone jako odwrotnoci trzech pierwszych. Wynika std fakt, i potrzebujemy do szczcia jedynie sinusa, cosinusa i tangensa reszt funkcji i tak bdziemy mogli atwo uzyska. C++ posiada oczywicie odpowiednie funkcje: double sin(double alfa); double cos(double alfa); double tan(double alfa); // sinus // cosinus // tangens
91
Dziaaj one identycznie do swoich geometrycznych odpowiednikw. Jako jedyny parametr przyjmuj miar kta w radianach i zwracaj wyniki, ktrych bez wtpienia mona si spodziewa :) Jeeli chodzi o trzy brakujce funkcje, to ich definicje s, jak sdz, oczywiste: double cot(double alfa) double sec(double alfa) double csc(double alfa) { return 1 / tan(alfa); } { return 1 / cos(alfa); } { return 1 / sin(alfa); } // cotangens // secant // cosecant
Gdy pracujemy z ktami i funkcjami trygonometrycznymi, nierzadko pojawia si konieczno zamiany miary kta ze stopni na radiany lub odwrotnie. Niestety, nie znajdziemy w C++ odpowiednich funkcji, ktre realizowayby to zadanie. By moe dlatego, e sami moemy je atwo napisa: const double PI = 3.1415923865; double degtorad(double alfa) { return alfa * PI / 180; } double radtodeg(double alfa) { return alfa * 180 / PI; } Pamitajmy te, aby nie myli tych dwch miar ktw i zdawa sobie spraw, i funkcje trygonometryczne w C++ uywaj radianw. Pomyki w tej kwestii s do czste i powoduj nieprzyjemne rezultaty, dlatego naley si ich wystrzega :) Jak zwykle, wicej informacji o funkcjach sin(), cos() i tan() znajdziesz w MSDN. Moesz tam rwnie zapozna si z funkcjami odwrotnymi do trygonometrycznych asin(), acos() oraz atan() i atan2().
Liczby pseudolosowe
Zostawmy ju te zdecydowanie zbyt matematyczne dywagacje i zajmijmy si czym, co bardziej zainteresuje przecitnego zjadacza komputerowego i programistycznego chleba :) Mam tu na myli generowanie wartoci losowych. Liczby losowe znajduj zastosowanie w bardzo wielu programach. W przypadku gier mog suy na przykad do tworzenia realistycznych efektw ognia, deszczu czy niegu. Uywajc ich moemy rwnie kreowa za kadym inn map w grze strategicznej czy zapewni pojawianie si wrogw w przypadkowych miejscach w grach zrcznociowych. Przydatno liczb losowych jest wic bardzo szeroka. Uzyskanie losowej wartoci jest w C++ cakiem proste. W tym celu korzystamy z funkcji rand() (ang. random losowy): int rand(); Jak monaby przypuszcza, zwraca nam ona przypadkow liczb dodatni39. Najczciej jednak potrzebujemy wartoci z okrelonego przedziau na przykad w programie
Liczba ta naley do przedziau <0; RAND_MAX>, gdzie RAND_MAX jest sta zdefiniowan przez kompilator (w Visual C++ .NET ma ona warto 32767).
39
92
Podstawy programowania
ilustrujcym dziaanie ptli while losowalimy liczb z zakresu od 1 do 100. Osignlimy to w do prosty sposb: int nWylosowana = rand() % 100 + 1; Wykorzystanie operatora reszty z dzielenia sprawia, e nasza dowolna warto (zwrcona przez rand()) zostaje odpowiednio przycita w tym przypadku do przedziau <0; 99> (poniewa reszt z dzielenia przez sto moe by 0, 1, 2, , 98, 99). Dodanie jedynki zmienia ten zakres do podanego <1; 100>. W podobny sposb moemy uzyska losow liczb z jakiegokolwiek przedziau. Nie od rzeczy bdzie nawet napisanie odpowiedniej funkcji: int random(int nMin, int nMax) { return rand() % (nMax - nMin + 1) + nMin; } Uywajc jej, potrafimy bez trudu stworzy chociaby symulator rzutu kostk do gry: void main() { std::cout << "Wylosowano " << random(1, 6) << " oczek."; getch(); } Zdaje si jednak, e co jest nie cakiem w porzdku Uruchamiajc parokrotnie powyszy program, za kadym razem zobaczymy jedn i t sam liczb! Gdzie jest wic ta obiecywana losowo?! C, nie ma w tym nic dziwnego. Komputer to tylko wielkie liczydo, ktre dziaa w zaprogramowany i przewidywalny sposb. Dotyczy to take funkcji rand(), ktrej dziaanie opiera si na raz ustalonym i niezmiennym algorytmie. Jej wynik nie jest zatem w aden sposb losowany, lecz wyliczany na podstawie formu matematycznych. Dlatego te liczby uzyskane w ten sposb nazywamy pseudolosowymi, poniewa tylko udaj prawdziw przypadkowo. Wydawa by si mogo, e fakt ten czyni je cakowicie nieprzydatnymi. Na szczcie nie jest to prawd: liczby pseudolosowe mona z powodzeniem wykorzystywa we waciwym im celu pod warunkiem, e robimy to poprawnie. Musimy bowiem pamita, aby przed pierwszym uyciem rand() wywoa inn funkcj srand(): void srand(unsigned int seed); Jej parametr seed to tak zwane ziarno. Jest to liczba, ktra inicjuje generator wartoci pseudolosowych. Dla kadego moliwego ziarna funkcja rand() oblicza nam inny cig liczb. Zatem, logicznie wnioskujc, powinnimy dba o to, by przy kadym uruchomieniu programu warto ziarna bya inna. Dochodzimy tym samym do pozornie bdnego koa eby uzyska liczb losow, potrzebujemy liczby losowej! Jak rozwiza ten, zdawaoby si, nierozwizywalny problem? Ot naley znale tak warto, ktra bdzie si zmienia miedzy kolejnymi uruchomieniami programu. Nietrudno j wskaza to po prostu czas systemowy. Jego pobranie jest bardzo atwe, bowiem C++ udostpnia nam zgrabn funkcj time(), zwracajca aktualny czas40 w sekundach:
40
Funkcja ta zwraca liczb sekund, jakie upyny od pnocy 1 stycznia 1970 roku.
Operacje na zmiennych
93
time_t time(time_t* timer); By moe wyglda ona dziwnie, ale zapewniam ci, e dziaa wietnie :) Wymaga jednak, abymy doczyli do programu dodatkowy nagwek ctime: #include <ctime> Teraz mamy ju wszystko, co potrzebne. Zatem do dziea! Nasza prosta aplikacja powinna obecnie wyglda tak: // Random - losowanie liczby #include <iostream> #include <ctime> #include <conio.h> int random(int nMin, int nMax) { return rand() % nMax + nMin; } void main() { // zainicjowanie generatora liczb pseudolosowych aktualnym czasem srand (static_cast<unsigned int>(time(NULL))); // wylosowanie i pokazanie liczby std::cout << "Wylosowana liczba to " << random(1, 6) << std::endl; } getch();
Kompilacja i kilkukrotne uruchomienie powyszego kodu utwierdzi nas w przekonaniu, i tym razem wszystko funkcjonuje poprawnie.
Dzieje si tak naturalnie za spraw tej linijki: srand (static_cast<unsigned int>(time(NULL))); Wywouje ona funkcj srand(), podajc jej ziarno uzyskane poprzez time(). Ze wzgldu na to, i time() zwraca warto nalec do specjalnego typu time_t, potrzebne jest rzutowanie jej na typ unsigned int. Wyjanienia wymaga jeszcze parametr funkcji time(). NULL to tak zwany wskanik zerowy, niereprezentujcy adnej przydatnej wartoci. Uywamy go tutaj, gdy nie mamy nic konkretnego do przekazania dla funkcji, za ona sama niczego takiego od nas nie wymaga :) Kompletny opis funkcji rand(), srand() i time() znajdziesz, jak poprzednio, w MSDN.
94
Podstawy programowania
Nie jest to wszake jedyny sposb dokonywania podobnej zamiany, gdy C++ posiada te dwie specjalnie do tego przeznaczone funkcje. Dziaaj one w inaczej ni zwyke rzutowanie, co samo w sobie stanowi dobry pretekst do ich poznania :D Owe dwie funkcje s sobie wzajemnie przeciwstawne jedna zaokrgla liczb w gr (wynik jest zawsze wikszy lub rwny podanej wartoci), za druga w d (rezultat jest mniejszy lub rwny). wietne obrazuj to ich nazwy, odpowiednio: ceil() (ang. ceiling sufit) oraz floor() (podoga). Przyjrzyjmy si teraz nagwkom tych funkcji: double ceil(double x); double floor(double x); Nie ma tu adnych niespodzianek no, moe poza typem zwracanego wyniku. Dlaczego nie jest to int? Ot typ double ma po prostu wiksz rozpito przedziau wartoci, jakie moe przechowywa. Poniewa argument funkcji take naley do tego typu, zastosowanie int spowodowaoby otrzymywanie bdnych rezultatw dla bardzo duych liczb (takich, jakie nie zmieciyby si do int-a). Na koniec mamy jeszcze kilka przykadw, ilustrujcych dziaanie poznanych przed chwil funkcji: fX fX fX fX fX = = = = = ceil(6.2); ceil(-5.6); ceil(14); floor(1.7); floor(-2.1); // // // // // 7.0 -5.0 14.0 1.0 -3.0
Szczeglnie dociekliwych czeka kolejna wycieczka wgb MSDN po dokadny opis funkcji ceil() i floor() ;D
Inne funkcje
Ostatnie dwie formuy trudno przyporzdkowa do jakiej konkretnej grupy. Nie znaczy to jednak, e s one mniej wane ni pozostae. Pierwsz z nich jest abs() (ang. absolute value), obliczajca warto bezwzgldn (modu) danej liczby. Jak pamitamy z matematyki, warto ta jest t sam liczb, lecz bez znaku zawsze dodatni. Ciekawa jest deklaracja funkcji abs(). Istnieje bowiem kilka jej wariantw, po jednym dla kadego typu liczbowego: int abs(int n); float abs(float n); double abs(double n); Jest to jak najbardziej moliwe i w peni poprawne. Zabieg taki nazywamy przecianiem (ang. overloading) funkcji. Przecianie funkcji (ang. function overloading) to obecno kilku deklaracji funkcji o tej samej nazwie, lecz posiadajcych rne listy parametrw i/lub typy zwracanej wartoci. Gdy wic wywoujemy funkcj abs(), kompilator stara si wydedukowa, ktry z jej wariantw powinien zosta uruchomiony. Czyni to przede wszystkim na podstawie przekazanego do parametru. Jeeli byaby to liczba cakowita, zostaaby wywoana
Operacje na zmiennych
wersja przyjmujca i zwracajca typ int. Jeeli natomiast podalibymy liczb zmiennoprzecinkow, wtedy do akcji wkroczyby inny wariant funkcji. Zatem dziki mechanizmowi przeciania funkcja abs() moe operowa na rnych typach liczb: int nX = abs(-45); float fX = abs(7.5); double fX = abs(-27.8); // 45 // 7.5 // 27.8
95
Druga funkcja to fmod(). Dziaa ona podobnie do operatora %, gdy take oblicza reszt z dzielenia dwch liczb. Jednak w przeciwiestwie do niego nie ogranicza si jedynie do liczb cakowitych, bowiem potrafi operowa take na wartociach rzeczywistych. Wida to po jej nagwku: double fmod(double x, double y); Funkcja ta wykonuje dzielenie x przez y i zwraca pozosta ze reszt, co oczywicie atwo wydedukowa z jej nagwka :) Dla porzdku zerknijmy jeszcze na par przykadw: fX = fmod(14, 3); fX = fmod(2.75, 0.5); fX = fmod(-10, 3); // 2 // 0.25 // -1
Wielbiciele MSDN mog zaciera rce, gdy z pewnoci znajd w niej szczegowe opisy funkcji abs()41 i fmod() ;) *** Zakoczylimy w ten sposb przegld asortymentu funkcji liczbowych, oferowanego przez C++. Przyswoiwszy sobie wiadomoci o tych formuach bdziesz mg robi z liczbami niemal wszystko, co tylko sobie zamarzysz :)
Dwa rodzaje
Operatory w C++ moemy podzieli na dwie grupy ze wzgldu na liczb parametrw, na ktrych dziaaj. Wyrniamy wic operatory unarne wymagajce jednego parametru oraz binarne potrzebujce dwch. Do pierwszej grupy nale na przykad symbole + oraz -, gdy stawiamy je przed jakim wyraeniem. Wtedy bowiem nie peni roli operatorw dodawania i odejmowania, lecz zachowania lub zmiany znaku. Moe brzmi to do skomplikowanie, ale naprawd jest bardzo proste: int nX = 5;
41 Standardowo doczona do Visual Studio .NET biblioteka MSDN posiada lekko nieaktualny opis tej funkcji nie s tam wymienione jej wersje przeciane dla typw float i double.
96
int nY = +nX; nY = -nX; // nY == 5 // nY == -5
Podstawy programowania
Operator + zachowuje nam znak wyraenia (czyli praktycznie nie robi nic, dlatego zwykle si go nie stosuje), za zmienia go na przeciwny (neguje wyraenie). Operatory te maj identyczn funkcj w matematyce, dlatego, jak sdz, nie powinny sprawi ci wikszego kopotu :) Do grupy operatorw unarnych zaliczamy rwnie ++ oraz --, odpowiadajce za inkrementacj i dekrementacj. Za chwil przyjrzymy im si bliej. Drugi zestaw to operatory binarne; dla nich konieczne s dwa argumenty. Do tej grupy nale wszystkie poznane wczeniej operatory arytmetyczne, a wic + (dodawanie), (odejmowanie), * (mnoenie), / (dzielenie) oraz % (reszta z dzielenia). Poniewa swego czasu powicilimy im sporo uwagi, nie bdziemy teraz dogbnie wnika w dziaanie kadego z nich. Wicej miejsca przeznaczymy tylko na operator dzielenia.
Operacje na zmiennych
97
Umieszczenie operatora ++ (--) przed wyraeniem nazywamy preinkrementacj (predekrementacj). W takiej sytuacji najpierw dokonywane jest zwikszenie (zmniejszenie) jego wartoci o 1. Nowa warto jest potem zwracana jako wynik. Kiedy napiszemy operator ++ (--) po wyraeniu, mamy do czynienia z postinkrementacj (postdekrementacj). W tym przypadku najpierw nastpuje zwrcenie wartoci, ktra dopiero potem jest zwikszana (zmniejszana) o jeden42. Czyby trzeba byo tych reguek uczy si na pami? Oczywicie, e nie :) Jak wikszo rzeczy w programowaniu, moemy je traktowa intuicyjnie. Kiedy napiszemy plusy (lub minusy) przed zmienn, wtedy najpierw zadziaaj wanie one. A skutkiem ich dziaania bdzie inkrementacja lub dekrementacja wartoci zmiennej, a wic otrzymamy w rezultacie ju zmodyfikowan liczb. Gdy za umiecimy je za nazw zmiennej, ustpi jej pierwszestwa i pozwol, aby jej stara warto zostaa zwrcona. Dopiero potem wykonaj swoj prac, czyli in/dekrementacj. Jeeli mamy moliwo dokonania wyboru midzy dwoma pooeniami operatora ++ (lub --), powinnimy zawsze uywa wariantu prefiksowego (przed zmienn). Wersja postfiksowa musi bowiem utworzy w pamici kopi zmiennej, eby mc zwrci jej star warto po in/dekrementacji. Cierpi na tym zarwno szybko programu, jak i jego wymagania pamiciowe (chocia w przypadku typw liczbowych jest to niezauwaalna rnica).
Swko o dzieleniu
W programowaniu mamy do czynienia z dwoma rodzajami dzielenia liczb: cakowitoliczbowym oraz zmiennoprzecinkowym. Oba zwracaj te same rezultaty w przypadku podzielnych przez siebie liczb cakowitych, ale w innych sytuacjach zachowuj si odmiennie. Dzielenie cakowitoliczbowe podaje jedynie cakowit cz wyniku, odrzucajc cyfry po przecinku. Z tego powodu wynik takiego dzielenia moe by bezporednio przypisany do zmiennej typu cakowitego. Wtedy jednak traci si dokadno ilorazu. Dzielenie zmiennoprzecinkowe pozwala uzyska precyzyjny rezultat, gdy zwraca liczb rzeczywist wraz z jej czci uamkow. w wynik musi by wtedy zachowany w zmiennej typu rzeczywistego. Wiksza cz jzykw programowania rozrnia te dwa typy dzielenia poprzez wprowadzenie dwch odrbnych operatorw dla kadego z nich43. C++ jest tu swego rodzaju wyjtkiem, poniewa posiada tylko jeden operator dzielcy, /. Jednake posugujc si nim odpowiednio, moemy uzyska oba rodzaje ilorazw. Zasady, na podstawie ktrych wyrniane s w C++ te dwa typy dzielenia, s ci ju dobrze znane. Przedstawilimy je sobie podczas pierwszego spotkania z operatorami arytmetycznymi. Poniewa jednak powtrze nigdy do, wymienimy je sobie ponownie :) Jeeli obydwa argumenty operatora / (dzielna i dzielnik) s liczbami cakowitymi, wtedy wykonywane jest dzielenie cakowitoliczbowe.
42 To uproszczone wyjanienie, bo przecie zwrcenie wartoci koczyoby dziaanie operatora. Naprawd wic warto wyraenia jest tymczasowo zapisywana i zwracana po dokonaniu in/dekrementacji. 43 W Visual Basicu jest to \ dla dzielenia cakowitoliczbowego i / dla zmiennoprzecinkowego. W Delphi odpowiednio div i /.
98
Podstawy programowania
W przypadku, gdy chocia jedna z liczb biorcych udzia w dzieleniu jest typu rzeczywistego, mamy do czynienia z dzieleniem zmiennoprzecinkowym. Od chwili, w ktrej poznalimy rzutowanie, mamy wiksz kontrol nad dzieleniem. Moemy bowiem atwo zmieni typ jednej z liczb i w ten sposb spowodowa, by zosta wykonany inny rodzaj dzielenia. Moliwe staje si na przykad uzyskanie dokadnego ilorazu dwch wartoci cakowitych: int nX = 12; int nY = 5; float fIloraz = nX / static_cast<float>(nY); Tutaj uzyskamy precyzyjny rezultat 2.4, gdy kompilator przeprowadzi dzielenie zmiennoprzecinkowe. Zrobi tak, bo drugi argument operatora /, mimo e ma warto cakowit, jest traktowany jako wyraenie typu float. Dzieje si tak naturalnie dziki rzutowaniu. Gdybymy go nie zastosowali i wpisali po prostu nX / nY, wykonaoby si dzielenie cakowitoliczbowe i uamkowa cz wyniku zostaaby obcita. Ten okrojony rezultat zmieniby nastpnie typ na float (poniewa przypisalibymy go do zmiennej rzeczywistej), co byoby zupenie zbdne, gdy i tak w wyniku dzielenia dokadno zostaa stracona. Prosty wniosek brzmi: uwaajmy, jak i co tak naprawd dzielimy, a w razie wtpliwoci korzystajmy z rzutowania. *** Koczcy si wanie podrozdzia prezentowa podstawowe instrumentarium operacyjne wartoci liczbowych w C++. Poznajc je zyskae potencja do tworzenia aplikacji wykorzystujcych zoone obliczenia, do ktrych niewtpliwie nale take gry. Jeeli czujesz si przytoczony nadmiarem matematyki, to mam dla ciebie dobr wiadomo: nasza uwaga skupi si teraz na zupenie innym, lecz rwnie wanym typie danych - tekcie.
acuchy znakw
Cigi znakw (ang. strings) stanowi drugi, po liczbach, wany rodzaj informacji przetwarzanych przez programy. Chocia zajmuj wicej miejsca w pamici ni dane binarne, a operacje na nich trwaj duej, maj wiele znaczcych zalet. Jedn z nich jest fakt, i s bardziej zrozumiae dla czowieka ni zwyke sekwencje bitw. W czasie, gdy moce komputerw rosn bardzo szybko, wymienione wczeniej wady nie s natomiast a tak dotkliwe. Wszystko to powoduje, e dane tekstowe s coraz powszechniej spotykane we wspczesnych aplikacjach. Dua jest w tym take rola Internetu. Takie standardy jak HTML czy XML s przecie formatami tekstowymi. Dla programistw napisy byy od zawsze przyczyn czstych blw gowy. W przeciwiestwie bowiem do typw liczbowych, maj one zmienny rozmiar, ktry nie moe by ustalony raz podczas uruchamiania programu. Ilo pamici operacyjnej, ktr zajmuje kady napis musi by dostosowywana do jego dugoci (liczby znakw) i zmienia si podczas dziaania aplikacji. Wymaga to dodatkowego czasu (od programisty
Operacje na zmiennych
99
i od komputera), uwagi oraz dokadnego przemylenia (przez programist, nie komputer ;D) mechanizmw zarzdzania pamici. Zwykli uytkownicy pecetw - szczeglnie ci, ktrzy pamitaj jeszcze zamierzche czasy DOSa - take nie maj dobrych wspomnie zwizanych z danymi tekstowymi. Odwieczne kopoty z polskimi ogonkami nadal daj o sobie zna, cho na szczcie coraz rzadziej musimy oglda na ekranie dziwne krzaczki zamiast znajomych liter w rodzaju , , czy . Wydaje si wic, e przed koderem piszcym programy przetwarzajce tekst pitrz si niebotyczne wrcz trudnoci. Problemy s jednak po to, aby je rozwizywa (lub by inni rozwizywali je za nas ;)), wic oba wymienione dylematy doczekay si ju wielu bardzo dobrych pomysw. Rozszerzajce si wykorzystanie standardu Unicode ograniczyo ju znacznie kopoty zwizane ze znakami specyficznymi dla niektrych jzykw. Kwesti czasu zdaje si chwila, gdy znikn one zupenie. Powstao te mnstwo sposobw na efektywne skadowanie napisw o zmiennej dugoci w pamici komputera. Wprawdzie w tym przypadku nie ma jednego, wiodcego trendu zapewniajcego przenono midzy wszystkimi platformami sprztowymi lub chocia aplikacjami, jednak i tak sytuacja jest znacznie lepsza ni jeszcze kilka lat temu44. Koderzy mog wic sobie pozwoli na uzasadniony optymizm :) Wsparci tymi pokrzepiajcymi faktami moemy teraz przystpi do poznawania elementw jzyka C++, ktre su do pracy z acuchami znakw.
44 Du zasug ma w tym ustandaryzowanie jzyka C++, w ktrym powstaje ponad poowa wspczesnych aplikacji. W przyszoci znaczc rol mog odegra take rozwizania zawarte w platformie .NET. 45 MFC (Microsoft Foundation Classes) zawiera przeznaczon do tego klas CString, za VCL (Visual Component Library) posiada typ String, ktry jest czci kompilatora C++ firmy Borland.
100
Podstawy programowania
std::string jest ci ju dobrze znany, gdy uywalimy go niejednokrotnie. Przechowuje on dowoln (w granicach dostpnej pamici) ilo znakw, z ktrych kady jest typu char. Zajmuje wic dokadnie 1 bajt i moe reprezentowa jeden z 256 symboli zawartych w tablicy ANSI. Wystarcza to do przechowywania tekstw w jzykach europejskich (cho wymaga specjalnych zabiegw, tzw. stron kodowych), jednak staje si niedostateczne w przypadku dialektw o wikszej liczbie znakw (na przykad wschodnioazjatyckich). Dlatego wykoncypowano, aby dla pojedynczego symbolu przeznacza wiksz ilo bajtw i w ten sposb stworzono MBCS (Multi-Byte Character Sets - wielobajtowe zestawy znakw) w rodzaju Unicode. Nie mamy tu absolutnie czasu ani miejsca na opisywanie tego standardu. Warto jednak wiedzie, e C++ posiada typ acuchowy, ktry umoliwia wspprac z nim - jest to std::wstring (ang. wide string - szeroki napis). Kady jego znak jest typu wchar_t (ang. wide char - szeroki znak) i zajmuje 2 bajty. atwo policzy, e umoliwia tym samym przechowywanie jednego z a 65536 (2562) moliwych symboli, co stanowi znaczny postp w stosunku do ANSI :) Korzystanie z std::wstring niewiele rni si przy tym od uywania jego bardziej oszczdnego pamiciowo kuzyna. Musimy tylko pamita, eby poprzedza literk L wszystkie wpisane do kodu stae tekstowe, ktre maj by trzymane w zmiennych typu std::wstring. W ten sposb bowiem mwimy kompilatorowi, e chcemy zapisa dany napis w formacie Unicode. Wyglda to choby tak: std::wstring strNapis = L"To jest tekst napisany znakami dwubajtowymi"; Dobra wiadomo jest taka, e jeli zapomniaby o wspomnianej literce L, to powyszy kod w ogle by si nie skompilowa ;D Jeeli chciaby wywietla takie szerokie napisy w konsoli i umoliwi uytkownikowi ich wprowadzanie, musisz uy specjalnych wersji strumieni wejcia i wyjcia. S to odpowiednio std::wcin i std::wcout Uywa si ich w identyczny sposb, jak poznanych wczeniej zwykych strumieni std::cin i std::cout.
Inicjalizacja
Najprostsza deklaracja zmiennej tekstowej wyglda, jak wiemy, mniej wicej tak: std::string strNapis;
Operacje na zmiennych
101
Wprowadzona w ten sposb nowa zmienna jest z pocztku cakiem pusta - nie zawiera adnych znakw. Jeeli chcemy zmieni ten stan rzeczy, moemy j zainicjalizowa odpowiednim tekstem - tak: std::string strNapis = "To jest jakis tekst"; albo tak: std::string strNapis("To jest jakis tekst"); Ten drugi zapis bardzo przypomina wywoanie funkcji. Istotnie, ma on z nimi wiele wsplnego - na tyle duo, e moliwe jest nawet zastosowanie drugiego parametru, na przykad: std::string strNapis("To jest jakis tekst", 7); Jaki efekt otrzymamy t drog? Ot do naszej zmiennej zostanie przypisany jedynie fragment podanego tekstu - dokadniej mwic, bdzie to podana w drugim parametrze ilo znakw, liczonych od pocztku napisu. U nas jest to zatem sekwencja "To jest". Co ciekawe, to wcale nie s wszystkie sposoby na inicjalizacj zmiennej tekstowej. Poznamy jeszcze jeden, ktry jest wyjtkowo uyteczny. Pozwala bowiem na uzyskanie cile okrelonego kawaka danego tekstu. Rzumy okiem na poniszy kod, aby zrozumie t metod: std::string strNapis1 = "Jakis krotki tekst"; std::string strNapis2(strNapis1, 6, 6); Tym razem mamy a dwa parametry, ktre razem okrelaj fragment tekstu zawartego w zmiennej strNapis1. Pierwszy z nich (6) to indeks pierwszego znaku tego fragmentu - tutaj wskazuje on na sidmy znak w tekcie (gdy znaki liczymy zawsze od zera!). Drugi parametr (znowu 6) precyzuje natomiast dugo podanego urywka - bdzie on w tym przypadku szecioznakowy. Jeeli takie opisowe wyjanienie nie bardzo do ciebie przemawia, spjrz na ten pogldowy rysunek:
Wida wic czarno na biaym (i na zielonym :)), e kopiowan czci tekstu jest wyraz "krotki".
102
Podstawy programowania
Podsumowujc, poznalimy przed momentem trzy nowe sposoby na inicjalizacj zmiennej typu tekstowego: std::[w]string nazwa_zmiennej([L]"tekst"); std::[w]string nazwa_zmiennej([L]"tekst", ilo_znakw); std::[w]string nazwa_zmiennej(inna_zmienna, pocztek [, dugo]); Ich skadnia, podana powyej, dokadnie odpowiada zaprezentowanym wczeniej przykadowym kodom. Zaskoczenie moe jedynie budzi fakt, e w trzeciej metodzie nie jest obowizkowe podanie dugoci kopiowanego fragmentu tekstu. Dzieje si tak, gdy w przypadku jej pominicia pobierane s po prostu wszystkie znaki od podanego indeksu a do koca napisu. Kiedy opucimy parametr dugo, wtedy trzeci sposb inicjalizacji staje si bardzo podobny do drugiego. Nie moesz jednak ich myli, gdy w kadym z nich liczby podawane jako drugi parametr znacz co innego. Wyraaj one albo ilo znakw, albo indeks znaku, czyli wartoci penice zupenie odrbne role.
czenie napisw
Skoro zatem wiemy ju wszystko, co wiedzie naley na temat deklaracji i inicjalizacji zmiennych tekstowych, zajmijmy si dziaaniami, jakie moemy na wykonywa. Jedn z najpowszechniejszych operacji jest zczenie dwch napisw w jeden - tak zwana konkatenacja. Mona j uzna za tekstowy odpowiednik dodawania liczb, szczeglnie e przeprowadzamy j take za pomoc operatora +: std::string strNapis1 = "gra"; std::string strNapis2 = "ty"; std::string strWynik = strNapis1 + strNapis2; Po wykonaniu tego kodu zmienna strWynik przechowuje rezultat poczenia, ktrym s oczywicie "graty" :D Widzimy wic, i scalenie zostaje przeprowadzone w kolejnoci ustalonej przez porzdek argumentw operatora +, za pomidzy poszczeglnymi skadnikami nie s wstawiane adne dodatkowe znaki. Nie rozmin si chyba z prawd, jeli stwierdz, e mona byo si tego spodziewa :) Konkatenacja moe rwnie zachodzi midzy wiksz liczb napisw, a take midzy tymi zapisanymi w sposb dosowny w kodzie: std::string strImie = "Jan"; std::string strNazwisko = "Nowak"; std::string strImieINazwisko = strImie + " " + strNazwisko; Tutaj otrzymamy personalia pana Nowaka zapisane w postaci cigego tekstu, ze spacj wstawion pomidzy imieniem i nazwiskiem. Jeli chciaby poczy dwa teksty wpisane bezporednio w kodzie (np. "jakis tekst" i "inny tekst"), choby po to eby rozbi dugi napis na kilka linijek, nie moesz stosowa do niego operatora +. Zapis "jakis tekst" + "inny tekst" bdzie niepoprawny i odrzucony przez kompilator. Zamiast niego wpisz po prostu "jakis tekst" "inny tekst", stawiajc midzy obydwoma staymi jedynie spacje, tabulatory, znaki koca wiersza itp. Podobiestwo czenia znakw do dodawania jest na tyle due, i moemy nawet uywa skrconego zapisu poprzez operator +=:
Operacje na zmiennych
103
std::string strNapis = "abc"; strNapis += "def"; W powyszy sposb otrzymamy wic sze pierwszych maych liter alfabetu - "abcdef".
void main() { std::string strNapis; std::cout << "Podaj tekst, w ktorym maja byc zliczane znaki: "; std::cin >> strNapis; char chSzukanyZnak; std::cout << "Podaj znak, ktory bedzie liczony: "; std::cin >> chSzukanyZnak; std::cout << "Znak '" << chSzukanyZnak <<"' wystepuje w tekscie " << ZliczZnaki(strNapis, chSzukanyZnak) << " raz(y)." << std::endl; } getch();
104
Podstawy programowania
Ta prosta aplikacja zlicza nam ilo wskazanych znakw w podanym napisie i wywietla wynik.
Czyni to poprzez funkcj ZliczZnaki(), przyjmujc dwa parametry: napis oraz znak, ktry ma by liczony. Poniewa jest to najwaniejsza cz naszego programu, przyjrzymy si jej bliej :) Najbardziej oczywistym sposobem na dokonanie podobnego zliczania jest po prostu przebiegnicie po wszystkich znakach tekstu odpowiedni ptl for i sprawdzanie, czy nie s rwne szukanemu znakowi. Kade udane porwnanie skutkuje inkrementacj zmiennej przechowujcej wynik funkcji. Wszystko to dzieje si w poniszym kawaku kodu: for (unsigned i = 0; i <= strTekst.length() - 1; ++i) { if (strTekst[i] == chZnak) ++uIlosc; } Jak ju kilkakrotnie i natarczywie przypominaem, indeksy znakw w zmiennej tekstowej liczymy od zera, zatem s one z zakresu <0; n-1>, gdzie n to dugo tekstu. Takie te wartoci przyjmuje licznik ptli for, czyli i. Wyraenie strTekst.length() zwraca nam bowiem dugo acucha strTekst. Wewntrz ptli szczeglnie interesujce jest dla nas porwnanie: if (strTekst[i] == chZnak) Sprawdza ono, czy aktualnie przerabiany przez ptl znak (czyli ten o indeksie rwnym i) nie jest takim, ktrego szukamy i zliczamy. Samo porwnanie nie byoby dla nas niczym nadzwyczajnym, gdyby nie owe wyawianie znaku o okrelonym indeksie (w tym przypadku i-tym). Widzimy tu wyranie, e mona to zrobi piszc po prostu dany indeks w nawiasach kwadratowych [ ] za nazw zmiennej tekstowej. Ze swej strony dodam tylko, e moliwe jest nie tylko odczytywanie, ale i zapisywanie takich pojedynczych znakw. Gdybymy wic umiecili w ptli nastpujc linijk: strTekst[i] = '.'; zmienilibymy wszystkie znaki napisu strTekst na kropki. Pamitajmy, eby pojedyncze znaki ujmowa w apostrofy (''), za cudzysowy ("") stosowa dla staych tekstowych. *** Tak oto zakoczylimy ten krtki opis operacji na acuchach znakw w jzyku C++. Nie jest to jeszcze cay potencja, jaki oferuj nam zmienne tekstowe, ale z pomoc
Operacje na zmiennych
zdobytych ju wiadomoci powiniene radzi sobie cakiem niele z prostym przetwarzaniem tekstu. Na koniec tego rozdziau poznamy natomiast typ logiczny i podstawowe dziaania wykonywane na nim. Pozwoli nam to midzy innymi atwiej sterowa przebiegiem programu przy uyciu instrukcji warunkowych.
105
Wyraenia logiczne
Spor cz poprzedniego rozdziau powicilimy na omwienie konstrukcji sterujcych, takich jak na przykad ptle. Pozwalaj nam one wpywa na przebieg wykonywania programu przy pomocy odpowiednich warunkw. Nasze pierwsze wyraenia tego typu byy bardzo proste i miay do ograniczone moliwoci. Przysza wic pora na powtrzenie i rozszerzenie wiadomoci na ten temat. Zapewne bardzo si z tego cieszysz, prawda? ;)) Zatem niezwocznie zaczynajmy.
Dodatkowym uatwieniem jest fakt, e kady z tych operatorw ma swj matematyczny odpowiednik - na przykad dla >= jest to , dla != mamy itd. Sdz wic, e symbole te nie bd ci sprawia adnych trudnoci. Gorzej moe by z nastpnymi ;)
Operatory logiczne
Doszlimy oto do sedna sprawy. Nowy rodzaj operatorw, ktry zaraz poznamy, jest bowiem narzdziem do konstruowania bardziej skomplikowanych wyrae logicznych. Dziki nim moemy na przykad uzaleni wykonanie jakiego kodu od spenienia kilku podanych warunkw lub tylko jednego z wielu ustalonych; moliwe s te bardziej zakrcone kombinacje. Zaznajomienie si z tymi operatorami da nam wic pen swobod sterowania dziaaniem programu. Ubolewam, i nie mog przedstawi ciekawych i interesujcych przykadowych programw na ilustracj tego zagadnienia. Niestety, cho operatory logiczne s niemal stale uywane w programowaniu powanych aplikacji, trudno o ewidentne przykady ich gwnych zastosowa - moe dlatego, e stosuje si je prawie do wszystkiego? :) Musisz wic zadowoli si niniejszymi, do trywialnymi kodami, ilustrujcymi funkcjonowanie tych elementw jzyka.
106
Podstawy programowania
Koniunkcja
Pierwszy z omawianych operatorw, oznaczany poprzez &&, zwany jest koniunkcj lub iloczynem logicznym. Gdy wstawimy go midzy dwoma warunkami, peni rol spjnika i. Takie wyraenie jest prawdziwe tylko wtedy, kiedy oba te warunki s spenione. Operator ten mona wykorzysta na przykad do sprawdzania przynalenoci liczby do zadanego przedziau: int nLiczba; std::cout << "Podaj liczbe z zakresu 1-10: "; std::cin >> nLiczba; if (nLiczba >= 1 && nLiczba <= 10) std::cout << "Dziekujemy."; else std::cout << "Nieprawidlowa wartosc!"; Kiedy dana warto naley do przedziau <1; 10>? Oczywicie wtedy, gdy jest jednoczenie wiksza lub rwna jedynce i mniejsza lub rwna dziesitce. To wanie sprawdzamy w warunku: if (nLiczba >= 1 && nLiczba <= 10) Operator && zapewnia, e cae wyraenie (nLiczba >= 1 && nLiczba <= 10) zostanie uznane za prawdziwe jedynie w przypadku, gdy obydwa skadniki (nLiczba >= 1, nLiczba <= 10) bd przedstawiay prawd. To jest wanie istot koniunkcji.
Alternatywa
Drugi rodzaj operacji, zwany alternatyw lub sum logiczn, stanowi niejako przeciwiestwo pierwszego. O ile koniunkcja jest prawdziwa jedynie w jednym, cile okrelonym przypadku (gdy oba jej argumenty s prawdziwe), o tyle alternatywa jest tylko w jednej sytuacji faszywa. Dzieje si tak wtedy, gdy obydwa zczone ni wyraenia przedstawiaj nieprawd. W C++ operatorem sumy logicznej jest ||, co wida na poniszym przykadzie: int nLiczba; std::cin >> nLiczba; if (nLiczba < 1 || nLiczba > 10) std::cout << "Liczba spoza przedzialu 1-10."; Uruchomienie tego kodu spowoduje wywietlenie napisu w przypadku, gdy wpisana liczba nie bdzie nalee do przedziau <1; 10> (czyli odwrotnie ni w poprzednim przykadzie). Naturalnie, stanie si tak wwczas, jeli bdzie ona mniejsza od 1 lub wiksza od 10. Taki te warunek posiada instrukcja if, a osignlimy go wanie dziki operatorowi alternatywy.
Negacja
Jak mona byo zauway, alternatywa nLiczba < 1 || nLiczba > 10 jest dokadnie przeciwstawna koniunkcji nLiczba >= 1 && nLiczba <= 10 (co jest do oczywiste przecie liczba nie moe jednoczenie nalee i nie nalee do jakiego przedziau :D). Warunki te znacznie rni si od siebie: stosujemy w nich przecie rne dziaania logiczne oraz porwnania. Moglibymy jednak postpi inaczej. Aby zmieni sens wyraenia na odwrotny - tak, eby byo prawdziwe w sytuacjach, kiedy oznaczao fasz i na odwrt - stosujemy operator negacji !. W przeciwiestwie do
Operacje na zmiennych
poprzednich, jest on unarny, gdy przyjmuje tylko jeden argument: warunek do zanegowania. Stosujc go dla naszej przykadowej koniunkcji: if (nLiczba >= 1 && nLiczba <= 10) otrzymalibymy wyraenie: if (!(nLiczba >= 1 && nLiczba <= 10))
107
ktre jest prawdziwe, gdy dana liczba nie naley do przedziau <1; 10>. Jest ono zatem rwnowane alternatywnie nLiczba < 1 || nLiczba > 10, a o to przecie nam chodzio :) W ten sposb (niechccy ;D) odkrylimy te jedno z tzw. praw de Morgana. Mwi ono, e zaprzeczenie (negacja) koniunkcji dwch wyrae rwne jest alternatywnie wyrae przeciwstawnych. A poniewa nLiczba >= 1 jest odwrotne do nLiczba < 1, za nLiczba <= 10 do nLiczba > 10, moemy naocznie stwierdzi, e prawo to jest suszne :) Czasami wic uycie operatora negacji uwalnia od koniecznoci przeksztacania zoonych warunkw na ich przeciwiestwa.
a prawda fasz
!a fasz prawda
Oczywicie, nie ma najmniejszej potrzeby, aby uczy si ich na pami (a ju si bae, prawda? :D). Jeeli uwanie przeczytae opisy kadego z operatorw, to tablice te bd dla ciebie jedynie powtrzeniem zdobytych wiadomoci. Najwaniejsze s bowiem proste reguy, rzdzce omawianymi operacjami. Powtrzmy je zatem raz jeszcze: Koniunkcja (&&) jest prawdziwa tylko wtedy, kiedy oba jej argumenty s prawdziwe. Alternatywa (||) jest faszywa jedynie wwczas, gdy oba jej argumenty s faszywe. Negacja (!) powoduje zmian prawdy na fasz lub faszu na prawd. czenie elementarnych wyrae przy pomocy operatorw pozwala na budow dowolnie skomplikowanych warunkw, regulujcych funkcjonowanie kadej aplikacji. Gdy zaczniesz uywa tych dziaa w swoich programach, zdziwisz si, jakim sposobem moge w ogle kodowa bez nich ;)
108
Podstawy programowania
Poniewa operatory logiczne maj niszy priorytet ni operatory porwnania, nie ma potrzeby stosowania nawiasw w warunkach podobnych do tych zaprezentowanych. Jeeli jednak bdziesz czy wiksz liczb wyrae logicznych, pamitaj o uywaniu nawiasw - to zawsze rozstrzyga wszelkie nieporozumienia i pomaga w unikniciu niektrych bdw.
Typ bool
Przydatno wyrae logicznych byaby do ograniczona, gdyby mona je byo stosowa tylko w warunkach instrukcji if i ptli. Zdecydowanie przydaby si sposb na zapisywanie wynikw obliczania takich wyrae, by mc je potem choby przekazywa do i z funkcji. C++ dysponuje rzecz jasna odpowiednim typem zmiennych, nadajcym si to tego celu. Jest nim tytuowy bool46. Mona go uzna za najprostszy typ ze wszystkich, gdy moe przyjmowa jedynie dwie dozwolone wartoci: prawd (true) lub fasz (false). Odpowiada to prawdziwoci lub nieprawdziwoci wyrae logicznych. Mimo oczywistej prostoty (a moe wanie dziki niej?) typ ten ma cae multum rnych zastosowa w programowaniu. Jednym z ciekawszych jest przerywanie wykonywania zagniedonych ptli: bool bKoniec = false; while (warunek_ptli_zewntrznej) { while (warunek_ptli_wewntrznej) { kod_ptli if (warunek_przerwania_obu_ptli) { // przerwanie ptli wewntrznej bKoniec = true; break; } } // przerwanie ptli zewntrznej, jeeli zmienna bKoniec // jest ustawiona na true if (bKoniec) break;
Wida tu klarownie, e zmienna typu bool reprezentuje warto logiczn - moemy j bowiem bezporednio wpisa jako warunek instrukcji if; nie ma potrzeby korzystania z operatorw porwnania. W praktyce czsto stosuje si funkcje zwracajce warto typu bool. Poprzez taki rezultat mog one powiadamia o powodzeniu lub niepowodzeniu zleconej im czynnoci albo sprawdza, czy dane zjawisko zachodzi, czy nie. Przyjrzyjmy si takiemu wanie przykadowi funkcji: // IsPrime - sprawdzanie, czy dana liczba jest pierwsza
46 Nazwa pochodzi od nazwiska matematyka Georgea Boolea, twrcy zasad logiki matematycznej (zwanej te algebr Boolea).
Operacje na zmiennych
bool LiczbaPierwsza(unsigned uLiczba) { if (uLiczba == 2) return true; for (unsigned i = 2; i <= sqrt(uLiczba); ++i) { if (uLiczba % i == 0) return false; } } return true;
109
void main() { unsigned uWartosc; std::cout << "Podaj liczbe: "; std::cin >> uWartosc; if (LiczbaPierwsza(uWartosc)) std::cout << "Liczba " << uWartosc << " jest pierwsza."; else std::cout << "Liczba " << uWartosc<< " nie jest pierwsza."; } getch();
Mamy tu funkcj LiczbaPierwsza() o prostym przeznaczeniu - sprawdza ona, czy podana liczba jest pierwsza47, czy nie. Produkuje wic wynik, ktry moe by sklasyfikowany w kategoriach logicznych: prawdy (liczba jest pierwsza) lub faszu (nie jest). Naturalne jest zatem, aby zwracaa warto typu bool, co te czyni.
Wykorzystujemy j od razu w odpowiedniej instrukcji if, przy pomocy ktrej wywietlamy jeden z dwch stosownych komunikatw. Dziki temu, e funkcja LiczbaPierwsza() zwraca warto logiczn, wszystko wyglda adnie i przejrzycie :) Algorytm zastosowany tutaj do sprawdzania pierwszoci podanej liczby jest chyba najprostszy z moliwych. Opiera si na pomyle tzw. sita Eratostenesa i, jak wida, polega po prostu na sprawdzaniu po kolei wszystkich liczb jako potencjalnych dzielnikw, a do wartoci pierwiastka kwadratowego badanej liczby.
Operator warunkowy
Z wyraeniami logicznymi cile zwizany jest jeszcze jeden, bardzo przydatny i wygodny, operator. Jest on kolejnym z licznych mechanizmw C++, ktre czyni skadni tego jzyka niezwykle zwart.
47
Liczba pierwsza to taka, ktra ma tylko dwa dzielniki - jedynk i sam siebie.
110
Podstawy programowania
Mowa tu o tak zwanym operatorze warunkowym ?: Uycie go pozwala na uniknicie, nieporcznych niekiedy, instrukcji if. Nierzadko moe si nawet przyczyni do poprawy szybkoci kodu. Jego dziaanie najlepiej zilustrowa na prostym przykadzie. Przypumy, e mamy napisa funkcj zwracaj wiksz warto spord dwch podanych48. Ochoczo zabieramy si wic do pracy i produkujemy kod podobny do tego: int max(int nA, int nB) { if (nA > nB) return nA; else return nB; } Moemy jednak uy operatora ?:, a wtedy funkcja przyjmie bardziej oszczdn posta: int max(int nA, int nB) { return (nA > nB ? nA : nB); } Znika nam tu cakowicie instrukcja if, gdy zastpi j nasz nowy operator. Porwnujc obie (rwnowane) wersje funkcji max(), moemy atwo wydedukowa jego dziaanie. Wyraenie zawierajce tene operator wyglda bowiem tak: warunek ? warto_dla_prawdy : warto_dla_faszu Skada si wic z trzech czci - dlatego ?: nazywany jest czasem operatorem ternarnym, przyjmujcym trzy argumenty (jako jedyny w C++). Jego funkcjonowanie jest nadzwyczaj proste. Sprowadza si do obliczenia warunku oraz podjcia na jego podstawie odpowiedniej decyzji. Jeli bdzie on prawdziwy, operator zwrci warto_dla_prawdy, w innym przypadku - warto_dla_faszu. Dziaalno ta jest w oczywisty sposb podobna do instrukcji if. Rnica polega na tym, e operator warunkowy manipuluje wyraeniami, a nie instrukcjami. Nie zmienia wic przebiegu programu, lecz co najwyej wyniki jego pracy. Kiedy zatem naley go uywa? Odpowied jest prosta: wszdzie tam, gdzie konstrukcja if wykonuje te same instrukcje w obu swoich blokach, lecz operuje na rnych wyraeniach. W naszym przykadzie byo to zawsze zwracanie wartoci przez funkcj (instrukcja return), jednak sam rezultat zalea od warunku. *** I to ju wszystko, co powiniene wiedzie na temat wyrae logicznych, ich konstruowania i uywania we wasnych programach. Umiejtno odpowiedniego stosowania zoonych warunkw przychodzi z czasem, dlatego nie martw si, jeeli na razie wydaj ci si one lekk abstrakcj. Pamitaj, wiczenie czyni mistrza!
Podsumowanie
Nadludzkim wysikiem dobrnlimy wreszcie do samego koca tego niezwykle dugiego i niezwykle wanego rozdziau. Poznae tutaj wikszo szczegw dotyczcych zmiennych oraz trzech podstawowych typw wyrae. Cay ten baga bdzie ci bardzo
48
Operacje na zmiennych
przydatny w dalszym kodowaniu, cho na razie moesz by o tym nieszczeglnie przekonany :) Uzupenieniem wiadomoci zawartych w tym rozdziale moe by Dodatek B, Reprezentacja danych w pamici. Jeeli czujesz si na siach, to zachcam do jego przeczytania :) W kolejnym rozdziale nauczysz si korzystania ze zoonych struktur danych, stanowicych chleb powszedni w powanym kodowaniu - take gier.
111
Pytania i zadania
Nieubaganie zblia si starcie z prac domow ;) Postaraj si zatem odpowiedzie na ponisze pytania oraz wykona zadania.
Pytania
1. 2. 3. 4. 5. 6. 7. 8. Co to jest zasig zmiennej? Czym si rni zakres lokalny od moduowego? Na czym polega zjawisko przesaniania nazw? Omw dziaanie poznanych modyfikatorw zmiennych. Dlaczego zmienne bez znaku mog przechowywa wiksze wartoci dodatnie ni zmienne ze znakiem? Na czym polega rzutowanie i jakiego operatora naley do uywa? Ktry plik nagwkowy zawiera deklaracje funkcji matematycznych? Jak nazywamy czenie dwch napisw w jeden? Opisz funkcjonowanie operatorw logicznych oraz operatora warunkowego
wiczenia
1. Napisz program, w ktrym przypiszesz warto 3000000000 (trzy miliardy) do dwch zmiennych: jednej typu int, drugiej typu unsigned int. Nastpnie wywietl wartoci obu zmiennych. Co stwierdzasz? (Trudne) Czy potrafisz to wyjani? Wskazwka: zapoznaj si z podrozdziaem o liczbach cakowitych w Dodatku B. 2. Wymyl nowe nazwy dla typw short int oraz long int i zastosuj je w programie przykadowym, ilustrujcym dziaanie operatora sizeof. 3. Zmodyfikuj nieco program wywietlajcy tablic znakw ANSI: a) zamie cztery wiersze wywietlajce pojedynczy rzd znakw na jedn ptl for b) zastp rzutowanie w stylu C operatorem static_cast c) (Trudniejsze) spraw, eby program czeka na dowolny klawisz po cakowitym zapenieniu okna konsoli - tak, eby uytkownik mg spokojnie przegldn ca tablic Wskazwka: moesz zaoy na sztywno, e konsola mieci 24 wiersze 4. Stwrz aplikacj podobn do przykadu LinearEq z poprzedniego rozdziau, tyle e rozwizujc rwnania kwadratowe. Pamitaj, aby uwzgldni warto wspczynnikw, przy ktrych rwnanie staje si liniowe (moesz wtedy uy kodu ze wspomnianego przykadu). Wskazwka: jeeli nie pamitasz sposobu rozwizywania rwna kwadratowych (wstyd! :P), moesz zajrze na przykad do encyklopedii WIEM. 5. Przyjrzyj si programowi sprawdzajcemu, czy dana liczba jest pierwsza i sprbuj zastpi wystpujc tam instrukcj if-else operatorem warunkowym ?:.
5
ZOONE ZMIENNE
Myli si jest rzecz ludzk, ale eby naprawd co spapra potrzeba komputera.
Edward Morgan Forster
Dzisiaj prawie aden normalny program nie przechowuje swoich danych jedynie w prostych zmiennych - takich, jakimi zajmowalimy si do tej pory (tzw. skalarnych). Istnieje mnstwo rnych sytuacji, w ktrych s one po prostu niewystarczajce, a konieczne staj si bardziej skomplikowane konstrukcje. Wspomnijmy choby o mapach w grach strategicznych, tabelach w arkuszach kalkulacyjnych czy bazach danych adresowych - wszystkie te informacje maj zbyt zoon natur, aby day si przedstawi przy pomocy pojedynczych zmiennych. Szanujcy si jzyk programowania powinien wic udostpnia odpowiednie konstrukcje, suce do przechowywania takich nieelementarnych typw danych. Naturalnie, C++ posiada takowe mechanizmy - zapoznamy si z nimi w niniejszym rozdziale.
Tablice
Jeeli nasz zestaw danych skada si z wielu drobnych elementw tego samego rodzaju, jego najbardziej naturalnym ekwiwalentem w programowaniu bdzie tablica. Tablica (ang. array) to zesp rwnorzdnych zmiennych, posiadajcych wspln nazw. Jego poszczeglne elementy s rozrnianie poprzez przypisane im liczby - tak zwane indeksy. Kady element tablicy jest wic zmienn nalec do tego samego typu. Nie ma tutaj adnych ogranicze: moe to by liczba (w matematyce takie tablice nazywamy wektorami), acuch znakw (np. lista uczniw lub pracownikw), pojedynczy znak, warto logiczna czy jakikolwiek inny typ danych. W szczeglnoci, elementem tablicy moe by take inna tablica! Takimi podwjnie zoonymi przypadkami zajmiemy si nieco dalej. Po tej garci oglnej wiedzy wstpnej, czas na co przyjemniejszego - czyli przykady :)
Proste tablice
Zadeklarowanie tablicy przypomina analogiczn operacj dla zwykych (skalarnych) zmiennych. Moe zatem wyglda na przykad tak: int aKilkaLiczb[5];
114
Podstawy programowania
Jak zwykle, najpierw piszemy nazw wybranego typu danych, a pniej oznaczenie samej zmiennej (w tym przypadku tablicy - to take jest zmienna). Nowoci jest tu para nawiasw kwadratowych, umieszczona na kocu deklaracji. Wewntrz niej wpisujemy rozmiar tablicy, czyli ilo elementw, jak ma ona zawiera. U nas jest to 5, a zatem z tylu wanie liczb (kadej typu int) bdzie skadaa si nasza wieo zadeklarowana tablica. Skoro emy ju wprowadzili now zmienn, naleaoby co z ni uczyni - w kocu niewykorzystana zmienna to zmarnowana zmienna :) Nadajmy wic jakie wartoci jej kolejnym elementom: aKilkaLiczb[0] aKilkaLiczb[1] aKilkaLiczb[2] aKilkaLiczb[3] aKlikaLiczb[4] = = = = = 1; 2; 3; 4; 5;
Tym razem take korzystamy z nawiasw kwadratowych. Teraz jednak uywamy ich, aby uzyska dostp do konkretnego elementu tablicy, identyfikowanego przez odpowiedni indeks. Niewtpliwie bardzo przypomina to docieranie do okrelonego znaku w zmiennej tekstowej (typu std::string), aczkolwiek w przypadku tablic moemy mie do czynienia z dowolnym rodzajem danych. Analogia do acuchw znakw przejawia si w jeszcze jednym fakcie - s nim oczywicie indeksy kolejnych elementw tablicy. Identycznie jak przy napisach, liczymy je bowiem od zera; tutaj s to kolejno 0, 1, 2, 3 i 4. Na postawie tego przykadu moemy wic sformuowa bardziej ogln zasad: Tablica mieszczca n elementw jest indeksowana wartociami 0, 1, 2, , n - 2, n - 1. Z regu t wie si te bardzo wane ostrzeenie: W tablicy n-elementowej nie istnieje element o indeksie rwnym n. Prba dostpu do niego jest bardzo czstym bdem, zwanym przekroczeniem indeksw (ang. subscript out of bounds). Ponisza linijka kodu spowodowaaby zatem bd podczas dziaania programu i jego awaryjne zakoczenie: aKilkaLiczb[5] = 6; // BD!!!
Pamitaj wic, by zwraca baczn uwag na indeksy tablic, ktrymi operujesz. Przekroczenie indeksw to jeden z przedstawicieli licznej rodziny bdw, noszcych wsplne miano pomyek o jedynk. Wikszo z nich dotyczy wanie tablic, inne mona popeni choby przy pracy z liczbami pseudolosowymi: najwredniejszym jest chyba warunek w rodzaju rand() % 10 == 10, ktry nigdy nie moe by speniony (pomyl, dlaczego49!). Krytyczne spojrzenie na zaprezentowany kilka akapitw wyej kawaek kodu moe prowadzi do wniosku, e idea tablic nie ma wikszego sensu. Przecie rwnie dobrze monaby zadeklarowa 5 zmiennych i zaj si kad z nich osobno - podobnie jak czynimy to teraz z elementami tablicy:
Reszta z dzielenia przez 10 moe by z nazwy rwna jedynie liczbom 0, 1, ..., 8, 9, zatem nigdy nie zrwna si z sam dziesitk. Programista chcia tu zapewne uzyska warto z przedziau <1; 10>, ale nie doda jedynki do wyraenia - czyli pomyli si o ni :)
49
Zoone zmienne
115
int nLiczba1, nLiczba2, nLiczba3, nLiczba4, nLiczba5; nLiczba1 = 1; nLiczba2 = 2; // itd. Takie rozumowanie jest pozornie suszne ale na szczcie, tylko pozornie! :D Uycie piciu instrukcji - po jednej dla kadego elementu tablicy - nie byo bowiem najlepszym rozwizaniem. O wiele bardziej naturalnym jest odpowiednia ptla for: for (int i = 0; i < 5; ++i) aKilkaLiczb[i] = i + 1; // drugim warunkiem moe by te i <= 4
Jej zalety s oczywiste: niezalenie od tego, czy nasza tablica skada si z piciu, piciuset czy piciu tysicy elementw, przytoczona ptla jest w kadym przypadku niemal identyczna! Tajemnica tego faktu tkwi rzecz jasna w indeksowaniu tablicy licznikiem ptli, i. Przyjmuje on odpowiednie wartoci (od zera do rozmiaru tablicy minus jeden), ktre pozwalaj zaj si caoci tablicy przy pomocy jednej tylko instrukcji! Taki manewr nie byby moliwy, gdybymy uywali tutaj piciu zmiennych, zastpujcych tablice. Ich indeksy (bdce de facto czci nazw) musiayby by bowiem staymi wartociami, wpisanymi bezporednio do kodu. Nie daoby si zatem skorzysta z ptli for w podobny sposb, jak to uczynilimy w przypadku tablic.
Inicjalizacja tablicy
Kiedy w tak szczegowy i szczeglny sposb zajmujemy si tablicami, atwo moemy zapomnie, i w gruncie rzeczy s to takie same zmienne, jak kade inne. Owszem, skadaj si z wielu pojedynczych elementw (podzmiennych), ale nie przeszkadza to w wykonywaniu na wikszoci znanych nam operacji. Jedn z nich jest inicjalizacja. Dziki niej moemy chociaby deklarowa tablice bdce staymi. Tablic moemy zainicjalizowa w bardzo prosty sposb, unikajc przy tym wielokrotnych przypisa (po jednym dla kadego elementu): int aKilkaLiczb[5] = { 1, 2, 3, 4, 5 }; Kolejne wartoci wpisujemy w nawiasie klamrowym, oddzielajc je przecinkami. Zostan one umieszczone w nastpujcych po sobie elementach tablicy, poczynajc od pocztku. Tak wic aKilkaLiczb[0] bdzie mia warto 1, aKilkaLiczb[1] - 2, itd. Uzyskamy identyczny efekt, jak w przypadku poprzednich piciu przypisa. Interesujc nowoci w inicjalizacji tablic jest moliwo pominicia ich rozmiaru: std::string aSystemyOperacyjne[] = {"Windows", "Linux", "BeOS", "QNX"}; W takiej sytuacji kompilator domyli si prawidowej wielkoci tablicy na podstawie iloci elementw, jak wpisalimy wewntrz nawiasw klamrowych (w tzw. inicjalizatorze). Tutaj bd to oczywicie cztery napisy. Inicjalizacja jest wic cakiem dobrym sposobem na wstpne ustawienie wartoci kolejnych elementw tablicy - szczeglnie wtedy, gdy nie jest ich zbyt wiele i nie s one ze sob jako zwizane. Dla duych tablic nie jest to jednak efektywna metoda; w takich wypadkach lepiej uy odpowiedniej ptli for.
116
Podstawy programowania
// wywietlamy wylosowane liczby std::cout << "Wyniki losowania:" << std::endl; for (int i = 0; i < ILOSC_LICZB; ++i) std::cout << aLiczby[i] << " "; // czekamy na dowolny klawisz getch();
Huh, trzeba przyzna, i z pewnoci nie naley on do elementarnych :) Nie jeste ju jednak zupenym nowicjuszem w sztuce programowania, wic zrozumienie go nie przysporzy ci wielkich kopotw. Na pocztek sprbuj zobaczy t przykadow aplikacj w dziaaniu:
Zoone zmienne
117
Nie potrzeba przenikliwoci Sherlocka Holmesa, by wydedukowa, e program ten dokonuje losowania zestawu liczb wedug zasad znanej powszechnie gry loteryjnej. Te reguy s determinowane przez dwie stae, zadeklarowane na samym pocztku kodu: const unsigned ILOSC_LICZB = 6; const int MAKSYMALNA_LICZBA = 49; Ich nazwy s na tyle znaczce, i dokumentuj si same. Wprowadzenie takich staych ma te inne wyrane zalety, o ktrych wielokrotnie ju wspominalimy. Ewentualna zmiana zasad losowania bdzie ograniczaa si jedynie do modyfikacji tyche dwch linijek, mimo e te kluczowe wartoci s wielokrotnie uywane w caym programie. Najwaniejsz zmienn w naszym kodzie jest oczywicie tablica, ktra przechowuje wylosowane liczby. Deklarujemy i inicjalizujemy j zaraz na wstpie funkcji main(): unsigned aLiczby[ILOSC_LICZB]; for (int i = 0; i < ILOSC_LICZB; ++i) aLiczby[i] = 0; Posugujc si tutaj ptl for, ustawiamy wszystkie jej elementy na warto 0. Zero jest dla nas neutralne, gdy losowane liczby bd przecie wycznie dodatnie. Identyczny efekt (wyzerowanie tablicy) mona uzyska stosujc funkcj memset(), ktrej deklaracja jest zawarta w nagwku memory.h. Uylibymy jej w nastpujcy sposb: memset (aLiczby, 0, sizeof(aLiczby)); Analogiczny skutek spowodowaaby take specjalna funkcja ZeroMemory() z windows.h: ZeroMemory (aLiczby, sizeof(aLiczby)); Nie uyem tych funkcji w kodzie przykadu, gdy wyjanienie ich dziaania wymaga wiedzy o wskanikach na zmienne, ktrej jeszcze nie posiadasz. Chwilowo jestemy wic zdani na swojsk ptl :) Po wyzerowaniu tablicy przeznaczonej na generowane liczby moemy przystpi do waciwej czynnoci programu, czyli ich losowania. Rozpoczynamy je od niezbdnego wywoania funkcji srand(): srand (static_cast<int>(time(NULL))); Po dopenieniu tej drobnej formalnoci moemy ju zaj si po kolei kad wartoci, ktr chcemy uzyska. Znowu czynimy to poprzez odpowiedni ptl for: for (int i = 0; i < ILOSC_LICZB; ) { // ... } Jak zwykle, przebiega ona po wszystkich elementach tablicy aLiczby. Pewn niespodziank moe by tu nieobecno ostatniej czci tej instrukcji, ktr jest zazwyczaj inkrementacja licznika. Jej brak spowodowany jest koniecznoci sprawdzania, czy wylosowana ju liczba nie powtarza si wrd wczeniej wygenerowanych. Z tego te powodu program bdzie niekiedy zmuszony do kilkakrotnego obrotu ptli przy tej samej wartoci licznika i losowania za kadym razem nowej liczby, a do skutku. Rzeczone losowane przebiega tradycyjn i znan nam dobrze drog: aLiczby[i] = rand() % MAKSYMALNA_LICZBA + 1;
118
Podstawy programowania
Uzyskana w ten sposb warto jest zapisywana w tablicy aLiczby pod i-tym indeksem, abymy mogli j pniej atwo wywietli. W powyszym wyraeniu obecna jest take staa, zadeklarowana wczeniej na pocztku programu. Wspominaem ju par razy, e konieczna jest kontrola otrzymanej t metod wartoci pod ktem jej niepowtarzalnoci. Musimy po prostu sprawdza, czy nie wystpia ju ona przy poprzednich losowaniach. Jeeli istotnie tak si stao, to z pewnoci znajdziemy j we wczeniej przerobionej czci tablicy. Niezbdne poszukiwania realizuje kolejny fragment listingu: bool bPowtarzaSie = false; for (int j = 0; j < i; ++j) { if (aLiczby[j] == aLiczby[i]) { bPowtarzaSie = true; break; } } if (!bPowtarzaSie) ++i; Wprowadzamy tu najpierw pomocnicz zmienn (flag) logiczn, zainicjalizowan wstpnie wartoci false (fasz). Bdzie ona niosa informacj o tym, czy faktycznie mamy do czynienia z duplikatem ktrej z wczeniejszych liczb. Aby si o tym przekona, musimy dokona ponownego przegldnicia czci tablicy. Robimy to poprzez, a jake, kolejn ptl for :) Aczkolwiek tym razem interesuj nas wszystkie elementy tablicy wystpujce przed tym aktualnym, o indeksie i. Jako warunek ptli wpisujemy wic j < i (j jest licznikiem nowej ptli). Koncentrujc si na niuansach zagniedonej instrukcji for nie zapominajmy, e jej celem jest znalezienie ewentualnego bliniaka wylosowanej kilka wierszy wczeniej liczby. Zadanie to wykonujemy poprzez odpowiednie porwnanie: if (aLiczby[j] == aLiczby[i]) aLiczby[i] (i-ty element tablicy aLiczby) reprezentuje oczywicie liczb, ktrej szukamy; jak wiemy doskonale, uzyskalimy j w sawetnym losowaniu :D Natomiast aLiczby[j] (j-ta warto w tablicy) przy kadym kolejnym przebiegu ptli oznacza jeden z przeszukiwanych elementw. Jeeli zatem wrd nich rzeczywicie jest wygenerowana, aktualna liczba, niniejszy warunek instrukcji if z pewnoci j wykryje. Co powinnimy zrobi w takiej sytuacji? Ot nic skomplikowanego - mianowicie, ustawiamy nasz zmienn logiczn na warto true (prawda), a potem przerywamy ptl for: bPowtarzaSie = true; break; Jej dalsze dziaanie nie ma bowiem najmniejszego sensu, gdy jeden duplikat liczby w zupenoci wystarcza nam do szczcia :) W tym momencie jestemy ju w posiadaniu arcywanej informacji, ktry mwi nam, czy warto wylosowana na samym pocztku cyklu gwnej ptli jest istotnie unikatowa, czy te konieczne bdzie ponowne jej wygenerowanie. Ow wiadomo przydaoby si teraz wykorzysta - robimy to w zaskakujco prosty sposb: if (!bPowtarzaSie) ++i; Jak wida, wanie tutaj trafia brakujca inkrementacja licznika ptli, i. Zatem odbywa si ona wtedy, kiedy uzyskana na pocztku liczba losowa spenia nasz warunek
Zoone zmienne
119
niepowtarzalnoci. W innym przypadku licznik zachowuje sw aktualn warto, wic wwczas bdzie przeprowadzona kolejna prba wygenerowania unikalnej liczby. Stanie si to w nastpnym cyklu ptli. Inaczej mwic, jedynie faszywo zmiennej bPowtarzaSie uprawnia ptl for do zajcia si dalszymi elementami tablicy. Inna sytuacja zmusz j bowiem do wykonania kolejnego cyklu na tej samej wartoci licznika i, a wic take na tym samym elemencie tablicy wynikowej. Czyni to a do otrzymania podanego rezultatu, czyli liczby rnej od wszystkich poprzednich. By moe nasuna ci si wtpliwo, czy takie kontrolowanie wylosowanej liczby jest aby na pewno konieczne. Skoro prawidowo zainicjowalimy generator wartoci losowych (przy pomocy srand()), to przecie nie powinien on robi nam wistw, ktrymi z pewnoci byyby powtrzenia wylosowywanych liczb. Jeeli nawet istnieje jaka szansa na otrzymanie duplikatu, to jest ona zapewne znikomo maa Ot nic bardziej bdnego! Sama potencjalna moliwo wyniknicia takiej sytuacji jest wystarczajcym powodem, eby doda do programu zabezpieczajcy przed ni kod. Przecie nie chcielibymy, aby przyszy uytkownik (niekoniecznie tego programu, ale naszych aplikacji w ogle) otrzyma produkt, ktry raz dziaa dobrze, a raz nie! Inna sprawa, e prawdopodobiestwo wylosowania powtarzajcych si liczb nie jest tu wcale takie mae. Moesz sprbowa si o tym przekona50 Na finiszu caego programu mamy jeszcze wywietlanie uzyskanego pieczoowicie wyniku. Robimy to naturalnie przy pomocy adekwatnego fora, ktry tym razem jest o wiele mniej skomplikowany w porwnaniu z poprzednim :) Ostatnia instrukcja, getch();, nie wymaga ju nawet adnego komentarza. Na niej te koczy si wykonywanie naszej aplikacji, a my moemy rwnie zakoczy tutaj jej omawianie. I odetchn z ulg ;) Uff! To wcale nie byo takie atwe, prawda? Wszystko dlatego, e postawiony problem take nie nalea do trywialnych. Analiza algorytmu, sucego do jego rozwizania, powinna jednak bardziej przybliy ci sposb konstruowania kodu, realizujcego konkretne zadanie. Mamy oto przejrzysty i, mam nadziej, zrozumiay przykad na wykorzystanie tablic w programowaniu. Przygldajc mu si dokadnie, moge dobrze pozna zastosowanie tandemu tablica + ptla for do wykonywania dosy skomplikowanych czynnoci na zoonych danych. Jeszcze nie raz uyjemy tego mechanizmu, wic z pewnoci bdziesz mia szans na jego doskonae opanowanie :)
Wicej wymiarw
Dotychczasowym przedmiotem naszego zainteresowania byy tablice jednowymiarowe, czyli takie, ktrych poszczeglne elementy s identyfikowane poprzez jeden indeks. Takie struktury nie zawsze s wystarczajce. Pomylmy na przykad o szachownicy, planszy do gry w statki czy mapach w grach strategicznych. Wszystkie te twory wymagaj wikszej liczby wymiarw i nie daj si przedstawi w postaci zwykej, ponumerowanej listy.
Wyliczenie jest bardzo proste. Zamy, e losujemy n liczb, z ktrych najwiksza moe by rwna a. Wtedy pierwsze losowanie nie moe rzecz jasna skutkowa duplikatem. W drugim jest na to szansa rwna 1/a (gdy mamy ju jedn liczb), w trzecim - 2/a (bo mamy ju dwie liczby), itd. Dla n liczb caociowe prawdopodobiestwo wynosi zatem (1 + 2 + 3 + ... + n-1)/a, czyli n(n - 1)/2a. U nas n = 6, za a = 49, wic mamy 6(6 - 1)/(2*49) 30,6% szansy na otrzymanie zestawu liczb, w ktrym przynajmniej jedna si powtarza. Gdybymy nie umiecili kodu sprawdzajcego, wtedy przecitnie co czwarte uruchomienie programu dawaoby nieprawidowe wyniki. Byaby to ewidentna niedorbka.
50
120
Podstawy programowania
Naturalnie, tablice wielowymiarowe mogyby by z powodzeniem symulowane poprzez ich jednowymiarowe odpowiedniki oraz formuy suce do przeliczania indeksw. Trudno jednak uzna to za wygodne rozwizanie. Dlatego te C++ radzi sobie z tablicami wielowymiarowymi w znacznie prostszy i bardziej przyjazny sposb. Warto wic przyjrze si temu wielkiemu dobrodziejstwu ;)
Deklaracja i inicjalizacja
Domylasz si moe, i aby zadeklarowa tablic wielowymiarow, naley poda wicej ni jedn liczb okrelajc jej rozmiar. Rzeczywicie tak jest: int aTablica[4][5]; Linijka powysza tworzy nam dwuwymiarow tablic o wymiarach 4 na 5, zawierajc elementy typu int. Moemy j sobie wyobrazi w sposb podobny do tego:
Wida wic, e pocztkowa analogia do szachownicy bya cakiem na miejscu :) Nasza dziewicza tablica wymaga teraz nadania wstpnych wartoci swoim elementom. Jak pamitamy, przy korzystaniu z jej jednowymiarowych kuzynw intensywnie uywalimy do tego odpowiednich ptli for. Nic nie stoi na przeszkodzie, aby podobnie postpi i w tym przypadku: for (int i = 0; i < 4; ++i) for (int j = 0; j < 5; ++j) aTablica[i][j] = i + j; Teraz jednak mamy dwa wymiary tablicy, zatem musimy zastosowa dwie zagniedone ptle. Ta bardziej zewntrzna przebiega nam po czterech kolejnych wierszach tablicy, natomiast wewntrzna zajmuje si kadym z piciu elementw wybranego wczeniej wiersza. Ostatecznie, przy kadym cyklu zagniedonej ptli liczniki i oraz j maj odpowiednie wartoci, abymy mogli za ich pomoc uzyska dostp do kadego z dwudziestu (4 * 5) elementw tablicy. Znamy wszake jeszcze inny rodek, sucy do wstpnego ustawiania zmiennych chodzi oczywicie o inicjalizacj. Zobaczylimy niedawno, e moliwe jest zaprzgnicie jej do pracy take przy tablicach jednowymiarowych. Czy bdziemy mogli z niej skorzysta rwnie teraz, gdy dodalimy do nich nastpne wymiary? Jak to zwykle w C++ bywa, odpowied jest pozytywna :) Inicjalizacja tablicy dwuwymiarowej wyglda bowiem nastpujco: int aTablica[4][5] = { { 0, 1, 2, 3, 4 }, { 1, 2, 3, 4, 5 },
Zoone zmienne
{ 2, 3, 4, 5, 6 }, { 3, 4, 5, 6, 7 } };
121
Opiera si ona na tej samej zasadzie, co analogiczna operacja dla tablic jednowymiarowych: kolejne wartoci oddzielamy przecinkami i umieszczamy w nawiasach klamrowych. Tutaj s to cztery wiersze naszej tabeli. Jednak kady z nich sam jest niejako odrbn tablic! W taki te sposb go traktujemy: ostateczne, liczbowe wartoci elementw podajemy albowiem wewntrz zagniedonych nawiasw klamrowych. Dla przejrzystoci rozmieszczamy je w oddzielnych linijkach kodu, co sprawia, e cao udzco przypomina wyobraenie tablicy dwuwymiarowej jako prostokta podzielonego na pola.
Otrzymany efekt jest zreszt taki sam, jak ten osignity przez dwie wczeniejsze, zagniedone ptle. Warto rwnie wiedzie, e inicjalizujc tablic wielowymiarow moemy pomin wielko pierwszego wymiaru: int aTablica[][5] = { { { { { 0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5, 3, 4, 5, 6, 4 5 6 7 }, }, }, } };
Tablice w tablicy
Sposb obsugi tablic wielowymiarowych w C++ rni si zasadniczo od podobnych mechanizmw w wielu innych jzykach. Tutaj bowiem nie s one traktowane wyjtkowo, jako byty odrbne od swoich jednowymiarowych towarzyszy. Powoduje to, e w C++ dozwolone s pewne operacje, na ktre nie pozwala wikszo pozostaych jzykw programowania. Dzieje si to za przyczyn do ciekawego pomysu potraktowania tablic wielowymiarowych jako zwykych tablic jednowymiarowych, ktrych elementami s inne tablice! Brzmi to troch topornie, ale w istocie nie jest takie trudne, jak by moe wyglda :) Najprostszy przykad tego faktu, z jakim mielimy ju do czynienia, to konstrukcja dwuwymiarowa. Z punktu widzenia C++ jest ona jednowymiarow tablic swoich wierszy; zwrcilimy zreszt na to uwag, dokonujc jej inicjalizacji. Kady z owych wierszy jest za take jednowymiarow tablic, tym razem skadajc si ju ze zwykych, skalarnych elementw. Zjawisko to (oraz kilka innych ;D) niele obrazuje poniszy diagram:
122
Podstawy programowania
Uoglniajc, moemy stwierdzi, i: Kada tablica n-wymiarowa skada si z odpowiedniej liczby tablic (n-1)-wymiarowych. Przykadowo, dla trzech wymiarw bdziemy mieli tablic, skadajc si z tablic dwuwymiarowych, ktre z kolei zbudowane s z jednowymiarowych, a te dopiero z pojedynczych skalarw. Nietrudne, prawda? ;) Zadajesz sobie pewnie pytanie: c z tego? Czy ma to jakie praktyczne znaczenie i zastosowanie w programowaniu? Pospieszam z odpowiedzi, brzmic jak zawsze ale oczywicie! :)) Ujcie tablic w takim stylu pozwala na ciekaw operacj wybrania jednego z wymiarw i przypisania go do innej, pasujcej tablicy. Wyglda to mniej wicej tak: // zadeklarowanie tablicy trj- i dwuwymiarowej int aTablica3D[2][2][2] = { { { 1, 2 }, { 2, 3 } }, { { 3, 4 }, { 4, 5 } } }; int aTablica2D[2][2]; // przypisanie drugiej "paszczyzny" tablicy aTablica3D do aTablica2D aTablica2D = aTablica3D[1]; // aTablica2D zawiera teraz liczby: { { 3, 4 }, { 4, 5 } } Przykad ten ma w zasadzie charakter ciekawostki, lecz przyjrzenie mu si z pewnoci nikomu nie zaszkodzi :D
Zoone zmienne
Nieco praktyczniejsze byoby odwoanie do czci tablicy - tak, eby moliwa bya jej zmiana niezalenie od caoci (np. przekazanie do funkcji). Takie dziaanie wymaga jednak poznania wskanikw, a to stanie si dopiero w rozdziale 8. ***
123
Poznalimy wanie tablice jako sposb na tworzenie zoonych struktur, skadajcych si z wielu elementw. Uatwiaj one (lub wrcz umoliwiaj) posugiwanie si zoonymi danymi, jakich nie brak we wspczesnych aplikacjach. Znajomo zasad wykorzystywania tablic z pewnoci zatem zaprocentuje w przyszoci :) Take w tym przypadku niezawodnym rdem uzupeniajcych informacji jest MSDN.
Przydatno praktyczna
W praktyce czsto zdarza si sytuacja, kiedy chcemy ograniczy moliwy zbir wartoci zmiennej do kilku(nastu/dziesiciu) cile ustalonych elementw. Jeeli, przykadowo, tworzylibymy gr, w ktrej pozwalamy graczowi jedynie na ruch w czterech kierunkach (gra, d, lewo, prawo), z pewnoci musielibymy przechowywa w jaki sposb jego wybr. Suca do tego zmienna przyjmowaaby wic jedn z czterech okrelonych wartoci. Jak monaby osign taki efekt? Jednym z rozwiza jest zastosowanie staych, na przykad w taki sposb: const const const const int int int int KIERUNEK_GORA = 1; KIERUNEK_DOL = 2; KIERUNEK_LEWO = 3; KIERUNEK_PRAWO = 4;
int nKierunek;
124
nKierunek = PobierzWybranyPrzezGraczaKierunek(); switch (nKierunek) { case KIERUNEK_GORA: case KIERUNEK_DOL: case KIERUNEK_LEWO: case KIERUNEK_PRAWO: default: }
Podstawy programowania
// // // // //
porusz graczem w gr porusz graczem w d porusz graczem w lewo porusz graczem w prawo a to co za kierunek? :)
Przy swoim obecnym stanie koderskiej wiedzy mgby z powodzeniem uy tego sposobu. Skoro jednak prezentujemy go w miejscu, z ktrego zaraz przejdziemy do omawiania nowych zagadnie, nie jest on pewnie zbyt dobry :) Najpowaniejszym chyba mankamentem jest zupena niewiadomo kompilatora co do specjalnego znaczenia zmiennej nKierunek. Traktuje j wic identycznie, jak kad inn liczb cakowit, pozwalajc choby na przypisanie podobne do tego: nKierunek = 10; Z punktu widzenia skadni C++ jest ono cakowicie poprawne, ale dla nas byby to niewtpliwy bd. 10 nie oznacza bowiem adnego z czterech ustalonych kierunkw, wic warto ta nie miaaby w naszym programie najmniejszego sensu! Jak zatem podej do tego problemu? Najlepszym wyjciem jest zdefiniowanie nowego typu danych, ktry bdzie pozwala na przechowywanie tylko kilku podanych wartoci. Czynimy to w sposb nastpujcy51: enum DIRECTION { DIR_UP, DIR_DOWN, DIR_LEFT, DIR_RIGHT }; Tak oto stworzylimy typ wyliczeniowy zwany DIRECTION. Zmienne, ktre zadeklarujemy jako nalece do tego typu, bd mogy przyjmowa jedynie wartoci wpisane przez nas w jego definicji. S to DIR_UP, DIR_DOWN, DIR_LEFT i DIR_RIGHT, odpowiadajce umwionym kierunkom. Peni one funkcj staych - z t rnic, e nie musimy deklarowa ich liczbowych wartoci (gdy i tak uywa bdziemy jedynie tych symbolicznych nazw). Mamy wic nowy typ danych, wypadaoby zatem skorzysta z niego i zadeklarowa jak zmienn: DIRECTION Kierunek = PobierzWybranyPrzezGraczaKierunek(); switch (Kierunek) { case DIR_UP: case DIR_DOWN: // itd. }
// ... // ...
Deklaracja zmiennej nalecej do naszego wasnego typu nie rni si w widoczny sposb od podobnego dziaania podejmowanego dla typw wbudowanych. Moemy rwnie dokona jej inicjalizacji, co te od razu czynimy.
51
Nowe typy danych bd nazywa po angielsku, aby odrni je od zmiennych czy funkcji.
Zoone zmienne
Kod ten bdzie poprawny oczywicie tylko wtedy, gdy funkcja PobierzWybranyPrzezGraczaKierunek() bdzie zwracaa warto bdc take typu DIRECTION.
125
Wszelkie wtpliwoci powinna rozwia instrukcja switch. Wida wyranie, e uyto jej w identyczny sposb jak wtedy, gdy korzystano jeszcze ze zwykych staych, deklarowanych oddzielnie. Na czym wic polega rnica? Ot tym razem niemoliwe jest przypisanie w rodzaju: Kierunek = 20; Kompilator nie pozwoli na nie, gdy zmienna Kierunek podlega ograniczeniom swego typu DIRECTION. Okrelajc go, ustalilimy, e moe on reprezentowa wycznie jedn z czterech podanych wartoci, a 20 niewtpliwie nie jest ktr z nich :) Tak wic teraz bezmylny program kompilujcy jest po naszej stronie i pomaga nam jak najwczeniej wyapywa bdy zwizane z nieprawidowymi wartociami niektrych zmiennych.
Sowo kluczowe enum (ang. enumerate - wylicza) peni rol informujc: mwi, zarwno nam, jak i kompilatorowi, i mamy tu do czynienia z definicj typu wyliczeniowego. Nazw, ktr chcemy nada owemu typowi, piszemy zaraz za tym sowem; przyjo si, aby uywa do tego wielkich liter alfabetu. Potem nastpuje czsty element w kodzie C++, czyli nawiasy klamrowe. Wewntrz nich umieszczamy tym razem list staych - dozwolonych wartoci typu wyliczeniowego. Jedynie one bd dopuszczone przez kompilator do przechowywania przez zmienne nalece do definiowanego typu. Tutaj rwnie zaleca si, tak jak w przypadku zwykych staych (tworzonych poprzez const), uywanie wielkich liter. Dodatkowo, dobrze jest doda do kadej nazwy odpowiedni przedrostek, powstay z nazwy typu, na przykad: // przykadowy typ okrelajcy poziom trudnoci jakiej gry enum DIFFICULTY { DIF_EASY, DIF_MEDIUM, DIF_HARD }; Wida to byo take w przykadowym typie DIRECTION. Nie zapominajmy o redniku na kocu definicji typu wyliczeniowego! Warto wiedzie, e stae, ktre wprowadzamy w definicji typu wyliczeniowego, reprezentuj liczby cakowite i tak te s przez kompilator traktowane. Kadej z nich nadaje on kolejn warto, poczynajc zazwyczaj od zera. Najczciej nie przejmujemy si, jakie wartoci odpowiadaj poszczeglnym staym. Czasem jednak naley mie to na uwadze - na przykad wtedy, gdy planujemy wspprac naszego typu z jakimi zewntrznymi bibliotekami. W takiej sytuacji moemy
126
Podstawy programowania
wyranie okreli, jakie liczby s reprezentowane przez nasze stae. Robimy to, wpisujc warto po znaku = i nazwie staej. Przykadowo, w zaprezentowanym na pocztku typie DIRECTION moglibymy przypisa kademu wariantowi kod liczbowy odpowiedniego klawisza strzaki: enum DIRECTION { DIR_UP DIR_DOWN DIR_LEFT DIR_RIGHT = = = = 38, 40, 37, 39 };
Nie trzeba jednak wyranie okrela wartoci dla wszystkich staych; moliwe jest ich sprecyzowanie tylko dla kilku. Dla pozostaych kompilator dobierze wtedy kolejne liczby, poczynajc od tych narzuconych, tzn. zrobi co takiego: enum MYENUM { ME_ONE, ME_TWO = 12, ME_THREE, ME_FOUR, ME_FIVE = 26, ME_SIX, ME_SEVEN }; // // // // // // // 0 12 13 14 26 27 28
Zazwyczaj nie trzeba o tym pamita, bo lepiej jest albo cakowicie zostawi przydzielanie wartoci w gestii kompilatora, albo samemu dobra je dla wszystkich staych i nie utrudnia sobie ycia ;)
Zastosowania
Ewentualni fani programw przykadowych mog czu si zawiedzeni, gdy nie zaprezentuj adnego krtkiego, kilkunastolinijkowego, dobitnego kodu obrazujcego wykorzystanie typw wyliczeniowych w praktyce. Powd jest do prosty: taki przykad miaby zoono i celowo porwnywaln do banalnych aplikacji dodajcych dwie liczby,
Zoone zmienne
z ktrymi stykalimy si na pocztku kursu. Zamiast tego pomwmy lepiej o zastosowaniach opisywanych typw w konstruowaniu normalnych, przydatnych programw - take gier.
127
Do czego wic mog przyda si typy wyliczeniowe? Tak naprawd sposobw na ich konkretne uycie jest wicej ni ziaren piasku na pustyni; rwnie dobrze moglibymy, zada pytanie w rodzaju Jakie zastosowanie ma instrukcja if? :) Wszystko bowiem zaley od postawionego problemu oraz samego programisty. Istnieje jednak co najmniej kilka oglnych sytuacji, w ktrych skorzystanie z typw wyliczeniowych jest wrcz naturalne: Przechowywanie informacji o stanie jakiego obiektu czy zjawiska. Przykadowo, jeeli tworzymy gr przygodow, moemy wprowadzi nowy typ okrelajcy aktualnie wykonywan przez gracza czynno: chodzenie, rozmowa, walka itd. Stosujc przy tym instrukcj switch bdziemy mogli w kadej klatce podejmowa odpowiednie kroki sterujce konwersacj czy wymian ciosw. Inny przykad to choby odtwarzacz muzyczny. Wiadomo, e moe on w danej chwili zajmowa si odgrywaniem jakiego pliku, znajdowa si w stanie pauzy czy te nie mie wczytanego adnego utworu i czeka na polecenia uytkownika. Te moliwe stany s dobrym materiaem na typ wyliczeniowy. Wszystkie te i podobne sytuacje, z ktrymi mona sobie radzi przy pomocy enum-w, s przypadkami tzw. automatw o skoczonej liczbie stanw (ang. finite state machine, FSM). Pojcie to ma szczeglne zastosowanie przy programowaniu sztucznej inteligencji, zatem jako (przyszy) programista gier bdziesz si z nim czasem spotyka. Ustawianie parametrw o cile okrelonym zbiorze wartoci. By ju tu przytaczany dobry przykad na wykorzystanie typw wyliczeniowych wanie w tym celu. Jest to oczywicie kwestia poziomu trudnoci jakiej gry; zapisanie wyboru uytkownika wydaje si najbardziej naturalne wanie przy uyciu zmiennej wyliczeniowej. Dobrym reprezentantem tej grupy zastosowa moe by rwnie sposb wyrwnywania akapitu w edytorach tekstu. Ustawienia: do lewej, do prawej, do rodka czy wyjustowanie s przecie wietnym materiaem na odpowiedni enum. Przekazywanie jednoznacznych komunikatw w ramach aplikacji. Nie tak dawno temu poznalimy typ bool, ktry moe by uywany midzy innymi do informowania o powodzeniu lub niepowodzeniu jakiej operacji (zazwyczaj wykonywanej przez osobn funkcj). Taka czarno-biaa informacja jest jednak mao uyteczna - w kocu jeeli wystpi jaki bd, to wypadaoby wiedzie o nim co wicej. Tutaj z pomoc przychodz typy wyliczeniowe. Moemy bowiem zdefiniowa sobie taki, ktry posuy nam do identyfikowania ewentualnych bdw. Okrelajc odpowiednie stae dla braku pamici, miejsca na dysku, nieistnienia pliku i innych czynnikw decydujcych o niepowodzeniu pewnych dziaa, bdziemy mogli je atwo rozrnia i raczy uytkownika odpowiednimi komunikatami. To tylko niektre z licznych metod wykorzystywania typw wyliczeniowych w programowaniu. W miar rozwoju swoich umiejtnoci sam odkryjesz dla nich mnstwo specyficznych zastosowa i bdziesz czsto z nich korzysta w pisanych kodach. Upewnij si zatem, e dobrze rozumiesz, na czym one polegaj i jak wyglda ich uycie w C++. To z pewnoci sowicie zaprocentuje w przyszoci. A kiedy uznasz, i jeste ju gotowy, bdziemy mogli przej dalej :)
128
Podstawy programowania
Kompleksowe typy
Tablice, opisane na pocztku tego rozdziau, nie s jedynym sposobem na modelowanie zoonych danych. Chocia przydaj si wtedy, gdy informacje maj jednorodn posta zestawu identycznych elementw, istnieje wiele sytuacji, w ktrych potrzebne s inne rozwizania Wemy chociaby banalny, zdawaoby si, przykad ksiki adresowej. Na pierwszy rzut oka jest ona idealnym materiaem na prost tablic, ktrej elementami byyby jej kolejne pozycje - adresy. Zauwamy jednak, e sama taka pojedyncza pozycja nie daje si sensownie przedstawi w postaci jednej zmiennej. Dane dotyczce jakiej osoby obejmuj przecie jej imi, nazwisko, ewentualnie pseudonim, adres e-mail, miejsce zamieszkania, telefon Jest to przynajmniej kilka elementarnych informacji, z ktrych kada wymagaaby oddzielnej zmiennej. Podobnych przypadkw jest w programowaniu mnstwo i dlatego te dzisiejsze jzyki posiadaj odpowiednie mechanizmy, pozwalajce na wygodne przetwarzanie informacji o budowie hierarchicznej. Domylasz si zapewne, e teraz wanie rzucimy okiem na ofert C++ w tym zakresie :)
Zoone zmienne
129
Struktury w akcji
Nie zapominajmy, e zdefiniowane przed chwil co o nazwie CONTACT jest nowym typem, a wic moemy skorzysta z niego tak samo, jak z innych typw w jzyku C++ (wbudowanych lub poznanych niedawno enumw). Zadeklarujmy wic przy jego uyciu jak przykadow zmienn: CONTACT Kontakt; Logiczne byoby teraz nadanie jej pewnej wartoci Pamitamy jednak, e powyszy Kontakt to tak naprawd trzy zmienne w jednym (co jak szampon przeciwupieowy ;D). Niemoliwe jest zatem przypisanie mu zwykej, pojedynczej wartoci, waciwej typom skalarnym. Moemy za to zaj si osobno kadym z jego pl. S one znanymi nam bardzo dobrze tworami programistycznymi (napisem i liczb), wic nie bdziemy mieli z nimi najmniejszych kopotw. C zatem zrobi, aby si do nich dobra? Skorzystamy ze specjalnego operatora wyuskania, bdcego zwyk kropk (.). Pozwala on midzy innymi na uzyskanie dostpu do okrelonego pola w strukturze. Uycie go jest bardzo proste i dobrze widoczne na poniszym przykadzie: // wypenienie struktury danymi Kontakt.strNick = "Hakier"; Kontakt.strEmail = "gigahaxxor@abc.pl"; Kontakt.nNumerIM = 192837465; Postawienie kropki po nazwie struktury umoliwia nam niejako wejcie w jej gb. W dobrych rodowiskach programistycznych wywietlana jest nawet lista wszystkich jej pl, jakby na potwierdzenie tego faktu oraz uatwienie pisania dalszego kodu. Po kropce wprowadzamy wic nazw pola, do ktrego chcemy si odwoa. Wykonawszy ten prosty zabieg moemy zrobi ze wskazanym polem wszystko, co si nam ywnie podoba. W przykadzie powyej czynimy do zwyke przypisanie wartoci, lecz rwnie dobrze mogoby to by jej odczytanie, uycie w wyraeniu, przekazanie do funkcji, itp. Nie ma bowiem adnej praktycznej rnicy w korzystaniu z pola struktury i ze zwykej zmiennej tego samego typu - oczywicie poza faktem, i to pierwsze jest tylko czci wikszej caoci. Sdz, e wszystko to powinno by dla ciebie w miar jasne :) Co uwaniejsi czytelnicy (czyli pewnie zdecydowana wikszo ;D) by moe zauwayli, i nie jest to nasze pierwsze spotkanie z kropk w C++. Gdy zajmowalimy si dokadniej acuchami znakw, uywalimy formuki napis.length() do pobrania dugoci tekstu. Czy znaczy to, e typ std::string rwnie naley do strukturalnych? C, sprawa jest generalnie dosy zoona, jednak czciowo wyjani si ju w nastpnym rozdziale. Na razie wiedz, e cel uycia operatora wyuskania by tam podobny do aktualnie omawianego (czyli wejcia w rodek zmiennej), chocia wtedy nie chodzio nam wcale o odczytanie wartoci jakiego pola. Sugeruj to zreszt nawiasy wieczce wyraenie Pozwl jednak, abym chwilowo z braku czasu i miejsca nie zajmowa si bliej tym zagadnieniem. Jak ju nadmieniem, wrcimy do niego cakiem niedugo, zatem uzbrj si w cierpliwo :) Spogldajc krytycznym okiem na trzy linijki kodu, ktre wykonuj przypisania wartoci do kolejnych pl struktury, moemy nabra pewnych wtpliwoci, czy aby skadnia C++ jest rzeczywicie taka oszczdna, jak si zdaje. Przecie wyranie wida, i musielimy tutaj za w kadym wierszu wpisywa nieszczsn nazw struktury, czyli Kontakt! Nie daoby si czego z tym zrobi? Kilka jzykw, w tym np. Delphi i Visual Basic, posiada bloki with, ktre odciaj nieco
130
Podstawy programowania
palce programisty i zezwalaj na pisanie jedynie nazw pl struktur. Jakkolwiek jest to niewtpliwie wygodne, to czasem powoduje do nieoczekiwane i nieatwe do wykrycia bdy logiczne. Wydaje si, e brak tego rodzaju instrukcji w C++ jest raczej rozsdnym skutkiem bilansu zyskw i strat, co jednak nie przeszkadza mi osobicie uwaa tego za pewien feler :D Istnieje jeszcze jedna droga nadania pocztkowych wartoci polom struktury, a jest ni naturalnie znana ju szeroko inicjalizacja :) Poniewa podobnie jak w przypadku tablic mamy tutaj do czynienia ze zoonymi zmiennymi, naley tedy posuy si odpowiedni form inicjalizatora - tak, jak podana poniej: // inicjalizacja struktury CONTACT Kontakt = { "MasterDisaster", "md1337@ajajaj.com.pl", 3141592 }; Uywamy wic w znajomy sposb nawiasw klamrowych, umieszczajc wewntrz nich wyraenia, ktre maj by przypisane kolejnym polom struktury. Naley przy tym pamita, by zachowa taki sam porzdek pl, jaki zosta okrelony w definicji typu strukturalnego. Inaczej moemy spodziewa si niespodziewanych bdw :) Kolejno pl w definicji typu strukturalnego oraz w inicjalizacji nalecej do struktury musi by identyczna. Uff, zdaje si, e w ferworze poznawania szczegowych aspektw struktur zapomnielimy ju cakiem o naszym pierwotnym zamyle. Przypominam wic, i byo nim stworzenie elektronicznej wersji notesu z adresami, czyli po prostu listy internetowych kontaktw. Nabyta wiedza nie pjdzie jednak na marne, gdy teraz potrafimy ju z atwoci wymyli stosowne rozwizanie pierwotnego problemu. Zasadnicz list bdzie po prostu odpowiednia tablica struktur: const unsigned LICZBA_KONTAKTOW = 100; CONTACT aKontakty[LICZBA_KONTAKTOW]; Jej elementami stan si dane poszczeglnych osb zapisanych w naszej ksice adresowej. Zestawione w jednowymiarow tablic bd dokadnie tym, o co nam od pocztku chodzio :)
Metody obsugi takiej tablicy nie rni si wiele od porwnywalnych sposobw dla tablic skadajcych si ze zwykych zmiennych. Moemy wic atwo napisa przykadow, prost funkcj, ktra wyszukuje osob o danym nicku: int WyszukajKontakt(std::string strNick) { // przebiegnicie po caej tablicy kontaktw przy pomocy ptli for for (unsigned i = 0; i < LICZBA_KONTAKTOW; ++i) // porwnywanie nicku kadej osoby z szukanym
Zoone zmienne
if (aKontakty[i].strNick == strNick) // zwrcenie indeksu pasujcej osoby return i; // ewentualnie, jeli nic nie znaleziono, zwracamy -1 return -1;
131
Zwrmy w niej szczegln uwag na wyraenie, poprzez ktre pobieramy pseudonimy kolejnych osb na naszej licie. Jest nim: aKontakty[i].strNick W zasadzie nie powinno by ono zaskoczeniem. Jak wiemy doskonale, aKontakty[i] zwraca nam i-ty element tablicy. U nas jest on struktur, zatem dostanie si do jej konkretnego pola wymaga te uycia operatora wyuskania. Czynimy to i uzyskujemy ostatecznie oczekiwany rezultat, ktry porwnujemy z poszukiwanym nickiem. W ten sposb przegldamy nasz tablic a do momentu, gdy faktycznie znajdziemy poszukiwany kontakt. Wtedy te koczymy funkcj i oddajemy indeks znalezionego elementu jako jej wynik. W przypadku niepowodzenia zwracamy natomiast -1, ktra to liczba nie moe by indeksem tablicy w C++. Caa operacja wyszukiwania nie naley wic do szczeglnie skomplikowanych :)
132
Podstawy programowania
czy DirectX. Su one nierzadko jako sposb na przekazywanie do i z funkcji duej iloci wymaganych informacji. Zamiast kilkunastu parametrw lepiej przecie uy jednego, kompleksowego, ktrym znacznie wygodniej jest operowa. My posuymy si takim wanie typem strukturalnym oraz kilkoma funkcjami pomocniczymi, aby zrealizowa nasz prost aplikacj. Wszystkie te potrzebne elementy znajdziemy w pliku nagwkowym ctime, gdzie umieszczona jest take definicja typu tm: struct { int int int int int int int int int }; tm tm_sec; tm_min; tm_hour; tm_mday; tm_mon; tm_year; tm_wday; tm_yday; tm_isdst; // // // // // // // // // sekundy minuty godziny dzie miesica miesic (0..11) rok (od 1900) dzie tygodnia (0..6, gdzie 0 == niedziela) dzie roku (0..365, gdzie 0 == 1 stycznia) czy jest aktywny czas letni?
Patrzc na nazwy jego pl oraz komentarze do nich, nietrudno uzna, i typ ten ma za zadanie przechowywa dat i czas w formacie przyjaznym dla czowieka. To za prowadzi do wniosku, i nasz program bdzie wykonywa czynno zwizan w jaki sposb z upywem czasu. Istotnie tak jest, gdy jego przeznaczeniem stanie si obliczanie biorytmu. Biorytm to modny ostatnio zestaw parametrw, ktre okrelaj aktualne moliwoci psychofizyczne kadego czowieka. Wedug jego zwolennikw, nasz potencja fizyczny, emocjonalny i intelektualny waha si okresowo w cyklach o staej dugoci, rozpoczynajcych si w chwili narodzin.
100
50
0 04-01-07 04-01-08 04-01-09 04-01-10 04-01-11 04-01-12 04-01-13 04-01-14 04-01-15 04-01-16 04-01-17 04-01-18 04-01-19 04-01-20 04-01-21 04-01-22 04-01-23 04-01-24 04-01-25 04-01-26 04-01-27 04-01-28 04-01-29 04-01-30 04-01-31 04-02-01 04-02-02 04-02-03 04-02-04 04-02-05 04-02-06
-50
Moliwe jest przy tym okrelenie liczbowej wartoci kadego z trzech rodzajw biorytmu w danym dniu. Najczciej przyjmuje si w tym celu przedzia procentowy, obejmujcy liczby od -100 do +100. Same obliczenia nie s szczeglnie skomplikowane. Patrzc na wykres biorytmu, widzimy bowiem wyranie, i ma on ksztat trzech sinusoid, rnicych si jedynie okresami. Wynosz one tyle, ile dugoci trwania poszczeglnych cykli biorytmu, a przedstawia je ponisza tabelka: cykl fizyczny dugo 23 dni
Zoone zmienne
cykl emocjonalny intelektualny dugo 28 dni 33 dni
133
Uzbrojeni w te informacje moemy ju napisa program, ktry zajmie si liczeniem biorytmu. Oczywicie nie przedstawi on wynikw w postaci wykresu (w kocu mamy do dyspozycji jedynie konsol), ale pozwoli zapozna si z nimi w postaci liczbowej, ktra take nas zadowala :) Spjrzmy zatem na ten spory kawaek kodu: // Biorhytm - pobieranie aktualnego czasu w postaci struktury // i uycie go do obliczania biorytmu // typ wyliczeniowy, okrelajcy rodzaj biorytmu enum BIORHYTM { BIO_PHYSICAL = 23, BIO_EMOTIONAL = 28, BIO_INTELECTUAL = 33 }; // pi :) const double PI = 3.1415926538; //---------------------------------------------------------------------// funkcja wyliczajca dany rodzaj biorytmu double Biorytm(double fDni, BIORHYTM Cykl) { return 100 * sin((2 * PI / Cykl) * fDni); } // funkcja main() void main() { /* trzy struktury, przechowujce dat urodzenia delikwenta, aktualny czas oraz rnic pomidzy nimi */ tm DataUrodzenia = { 0, 0, 0, 0, 0, 0, 0, 0, 0 }; tm AktualnyCzas = { 0, 0, 0, 0, 0, 0, 0, 0, 0 }; tm RoznicaCzasu = { 0, 0, 0, 0, 0, 0, 0, 0, 0 }; /* pytamy uytkownika o dat urodzenia */ std::cout << "Podaj date urodzenia" << std::endl; // dzie std::cout << "- dzien: "; std::cin >> DataUrodzenia.tm_mday; // miesic - musimy odj 1, bo uytkownik poda go w systemie 1..12 std::cout << "- miesiac: "; std::cin >> DataUrodzenia.tm_mon; DataUrodzenia.tm_mon--; // rok - tutaj natomiast musimy odj 1900 std::cout << "- rok: "; std::cin >> DataUrodzenia.tm_year; DataUrodzenia.tm_year -= 1900; /* obliczamy liczb przeytych dni */
134
Podstawy programowania
// pobieramy aktualny czas w postaci struktury time_t Czas = time(NULL); AktualnyCzas = *localtime(&Czas); // obliczamy rnic midzy nim a dat urodzenia RoznicaCzasu.tm_mday = AktualnyCzas.tm_mday - DataUrodzenia.tm_mday; RoznicaCzasu.tm_mon = AktualnyCzas.tm_mon - DataUrodzenia.tm_mon; RoznicaCzasu.tm_year = AktualnyCzas.tm_year - DataUrodzenia.tm_year; // przeliczamy to na dni double fPrzezyteDni = RoznicaCzasu.tm_year * 365.25 + RoznicaCzasu.tm_mon * 30.4375 + RoznicaCzasu.tm_mday; /* obliczamy biorytm i wywielamy go */ std::endl; "Twoj biorytm" << std::endl; "- fizyczny: " << Biorytm(fPrzezyteDni, BIO_PHYSICAL) std::endl; "- emocjonalny: " << Biorytm(fPrzezyteDni, BIO_EMOTIONAL) << std::endl; std::cout << "- intelektualny: " << Biorytm(fPrzezyteDni, BIO_INTELECTUAL) << std::endl; // czekamy na dowolny klawisz getch(); on << << << << std::cout << // ot i std::cout std::cout std::cout
Jaki jest efekt tego pokanych rozmiarw listingu? S nim trzy wartoci okrelajce dzisiejszy biorytm osoby o podanej dacie urodzenia:
Za jego wyznaczenie odpowiada prosta funkcja Biorytm() wraz towarzyszcym jej typem wyliczeniowym, okrelajcym rodzaj biorytmu: enum BIORHYTM { BIO_PHYSICAL = 23, BIO_EMOTIONAL = 28, BIO_INTELECTUAL = 33 }; double Biorytm(double fDni, BIORHYTM Cykl) { return 100 * sin((2 * PI / Cykl) * fDni); }
Zoone zmienne
135
Godn uwagi sztuczk, jak tu zastosowano, jest nadanie staym typu BIORHYTM wartoci, bdcych jednoczenie dugociami odpowiednich cykli biorytmu. Dziki temu funkcja zachowuje przyjazn posta wywoania, na przykad Biorytm(liczba_dni, BIO_PHYSICAL), a jednoczenie unikamy instrukcji switch wewntrz niej. Sama formuka liczca opiera si na oglnym wzorze sinusoidy, tj.:
2 y ( x) = A sin x T
w ktrym A jest jej amplitud, za T - okresem. U nas okresem jest dugo trwania poszczeglnych cykli biorytmu, za amplituda 100 powoduje rozcignicie przedziau wartoci do zwyczajowego <-100; +100>. Stanowica wikszo kodu duga funkcja main() dzieli si na trzy czci. W pierwszej z nich pobieramy od uytkownika jego dat urodzenia i zapisujemy j w strukturze o nazwie DataUrodzenia :) Zauwamy, e uywamy tutaj jej pl jako miejsca docelowego dla strumienia wejcia w identyczny sposb, jak to czynilimy dla pojedynczych zmiennych. Po pobraniu musimy jeszcze odpowiednio zmodyfikowa dane - tak, eby speniay wymagania podane w komentarzach przy definicji typu tm (chodzi tu o numerowanie miesicy od zera oraz liczenie lat poczwszy od roku 1900). Kolejnym zadaniem jest obliczenie iloci dni, jak dany osobnik przey ju na tym wiecie. W tym celu musimy najpierw pobra aktualny czas, co te czyni dwie ponisze linijki: time_t Czas = time(NULL); AktualnyCzas = *localtime(&Czas); W pierwszej z nich znana nam ju funkcja time() uzyskuje czas w wewntrznym formacie C++53. Dopiero zawarta w drugim wierszu funkcja localtime()konwertuje go na zdatn do wykorzystania struktur, ktr przypisujemy do zmiennej AktualnyCzas. Troszk udziwnion posta tej funkcji musisz na razie niestety zignorowa :) Dalej obliczamy rnic midzy oboma czasami (zapisanymi w DataUrodzenia i AktualnyCzas), odejmujc od siebie liczby dni, miesicy i lat. Otrzymany t drog wiek uytkownika musimy na koniec przeliczy na pojedyncze dni, za co odpowiada wyraenie: double fPrzezyteDni = RoznicaCzasu.tm_year * 365.25 + RoznicaCzasu.tm_mon * 30.4375 + RoznicaCzasu.tm_mday; Zastosowane tu liczby 365.25 i 30.4375 s rednimi ilociami dni w roku oraz w miesicu. Uwalniaj nas one od koniecznoci osobnego uwzgldniania lat przestpnych w przeprowadzanych obliczeniach. Wreszcie, ostatnie wiersze kodu obliczaj biorytm, wywoujc trzykrotnie funkcj o tej nazwie, i prezentuj wyniki w klarownej postaci w oknie konsoli.
53
136
Podstawy programowania
Dziaanie programu koczy si za na tradycyjnym getch(), ktre oczekuje na przycinicie dowolnego klawisza. Po tym fakcie nastpuje ju definitywny i nieodwoalny koniec :D Tak oto przekonalimy si, e struktury warto zna nawet wtedy, gdy nie planujemy tworzenia aplikacji manewrujcych skomplikowanymi danymi. Nie zdziw si zatem, e w dalszym cigu tego kursu bdziesz je cakiem czsto spotyka.
Unie
Drugim, znacznie rzadziej spotykanym rodzajem zoonych typw s unie. S one w pewnym sensie podobne do struktur, gdy ich definicje stanowi take listy poszczeglnych pl: union nazwa_typu { typ_pola_1 nazwa_pola_1; typ_pola_2 nazwa_pola_2; typ_pola_3 nazwa_pola_3; ... typ_pola_n nazwa_pola_n; }; Identycznie wygldaj rwnie deklaracje zmiennych, nalecych do owych typw unijnych, oraz odwoania do ich pl. Na czym wic polegaj rnice? Przypomnijmy sobie, e struktura jest zestawem kilku odrbnych zmiennych, poczonych w jeden kompleks. Kade jego pole zachowuje si dokadnie tak, jakby byo samodzieln zmienn, i posusznie przechowuje przypisane mu wartoci. Rozmiar struktury jest za co najmniej sum rozmiarw wszystkich jej pl. Unia opiera si na nieco innych zasadach. Zajmuje bowiem w pamici jedynie tyle miejsca, eby mc pomieci swj najwikszy element. Nie znaczy to wszak, i w jaki nadprzyrodzony sposb potrafi ona zmieci w takim okrojonym obszarze wartoci wszystkich pl. Przeciwnie, nawet nie prbuje tego robi. Zamiast tego obszary pamici przeznaczone na wartoci pl unii zwyczajnie nakadaj si na siebie. Powoduje to, e: W danej chwili tylko jedno pole unii zawiera poprawn warto. Do czego mog si przyda takie dziwaczne twory? C, ich zastosowania s do swoiste, wic nieczsto bdziesz zmuszony do skorzystania z nich. Jednym z przykadw moe by jednak ch zapewnienia kilku drg dostpu do tych samych danych: union VECTOR3 { // w postaci trjelementowej tablicy float v[3]; // lub poprzez odpowiednie zmienne x, y, z struct { float x, y, z; };
};
Zoone zmienne
137
W powyszej unii, ktra ma przechowywa trjwymiarowy wektor, moliwe s dwa sposoby na odwoanie si do jego wsprzdnych: poprzez pola x, y oraz z lub indeksy odpowiedniej tablicy v. Oba s rwnowane: VECTOR3 vWektor; // ponisze dwie linijki robi to samo vWektor.x = 1.0; vWektor.y = 5.0; vWektor.z = 0.0; vWektor.v[0] = 1.0; vWektor.v[1] = 5.0; vWektor.v[2] = 0.0; Taka uni moemy wic sobie obrazowo przedstawi chociaby poprzez niniejszy rysunek:
Elementy tablicy v oraz pola x, y, z niejako wymieniaj midzy sob wartoci. Oczywicie jest to tylko pozorna wymiana, gdy tak naprawd chodzi po prostu o odwoywanie si do tego samego adresu w pamici, jednak rnymi drogami. Wewntrz naszej unii umiecilimy tzw. anonimow struktur (nieopatrzon adn nazw). Musielimy to zrobi, bo jeeli wpisalibymy float x, y, z; bezporednio do definicji unii, kade z tych pl byoby zalene od pozostaych i tylko jedno z nich miaoby poprawn warto. Struktura natomiast czy je w integraln cao. Mona zauway, e struktury i unie s jakby odpowiednikiem operacji logicznych koniunkcji i alternatywy - w odniesieniu do budowania zoonych typw danych. Struktura peni jak gdyby funkcj operatora && (pozwalajc na niezalene istnienie wszystkim obejmowanym sob zmiennym), za unia - operatora || (dopuszczajc wycznie jedn dan). Zagniedajc frazy struct i union wewntrz definicji kompleksowych typw moemy natomiast uzyska bardziej skomplikowane kombinacje. Naturalnie, rodzi si pytanie Po co?, ale to ju zupenie inna kwestia ;) Wicej informacji o uniach zainteresowani znajd w MSDN. *** Lektura koczcego si wanie podrozdziau daa ci moliwo rozszerzania wachlarza standardowych typw C++ o takie, ktre mog ci uatwi tworzenie przyszych aplikacji. Poznae wic typy wyliczeniowe, struktury oraz unie, uwalniajc cakiem nowe moliwoci programistyczne. Na pewno niejednokrotnie bdziesz z nich korzysta.
Wikszy projekt
Doszedszy do tego miejsca w lekturze niniejszego kursu posiade ju dosy du wiedz programistyczn. Pora zatem na wykorzystanie jej w praktyce: czas stworzy jak
138
Podstawy programowania
wiksz aplikacj, a poniewa docelowo mamy zajmowa si programowaniem gier, wic bdzie ni wanie gra. Nie moesz wprawdzie liczy na oszaamiajce efekty graficzne czy dwikowe, gdy chwilowo potrafimy operowa jedynie konsol, lecz nie powinno ci to mimo wszystko zniechca. rodki tekstowe oka si bowiem cakowicie wystarczajce dla naszego skromnego projektu.
Projektowanie
C wic chcemy napisa? Ot bdzie to produkcja oparta na wielce popularnej i lubianej grze w kko i krzyyk :) Zainteresujemy si jej najprostszym wariantem, w ktrym dwoje graczy stawia naprzemian kka i krzyyki na planszy o wymiarach 33. Celem kadego z nich jest utworzenie linii z trzech wasnych symboli - poziomej, pionowej lub ukonej.
Nasza gra powinna pokazywa rzeczon plansz w czasie rozgrywki, umoliwia wykonywanie graczom kolejnych ruchw oraz sprawdza, czy ktry z nich przypadkiem nie wygra :) I taki wanie efekt bdziemy chcieli osign, tworzc ten program w C++. Najpierw jednak, skoro ju wiemy, co bdziemy pisa, zastanwmy si, jak to napiszemy.
Zoone zmienne
Ostatecznie plansza bdzie wyglda w ten sposb: enum FIELD { FLD_EMPTY, FLD_CIRCLE, FLD_CROSS }; FIELD g_aPlansza[3][3] = { { FLD_EMPTY, FLD_EMPTY, FLD_EMPTY }, { FLD_EMPTY, FLD_EMPTY, FLD_EMPTY }, { FLD_EMPTY, FLD_EMPTY, FLD_EMPTY } };
139
Inicjalizacja jest tu odzwierciedleniem faktu, i na pocztku wszystkie jej pola s puste. Plansza to jednakowo nie wszystko. W naszej grze bdzie si przecie co dzia: gracze dokonywa bd swych kolejnych posuni. Potrzebujemy wic zmiennych opisujcych przebieg rozgrywki. Ich wyodrbnienie nie jest ju takie atwe, aczkolwiek nie powinnimy mie z tym wielkich kopotw. Musimy mianowicie pomyle o grze w kko i krzyyk jako o procesie przebiegajcym etapami, wedug okrelonego schematu. To nas doprowadzi do pierwszej zmiennej, okrelajcej aktualny stan gry: enum GAMESTATE { GS_NOTSTARTED, // gra GS_MOVE, // gra GS_WON, // gra GS_DRAW }; // gra GAMESTATE g_StanGry = GS_NOTSTARTED; nie zostaa rozpoczta rozpoczta, gracze wykonuj ruchy skoczona, wygrana ktrego gracza skoczona, remis
Wyrnilimy tutaj cztery fazy: pocztkowa - waciwa gra jeszcze si nie rozpocza, czynione s pewne przygotowania (o ktrych wspomnimy nieco dalej) rozgrywka - uczestniczcy w niej gracze naprzemiennie wykonuj ruchy. Jest to zasadnicza cz caej gry i trwa najduej. wygrana - jeden z graczy zdoa uoy lini ze swoich symboli i wygra parti remis - plansza zostaa szczelnie zapeniona znakami zanim ktrykolwiek z graczy zdoa zwyciy Czy to wystarczy? Nietrudno si domyli, e nie. Nie przewidzielimy bowiem adnego sposobu na przechowywanie informacji o tym, ktry z graczy ma w danej chwili wykona swj ruch. Czym prdzej zatem naprawimy swj bd: enum SIGN { SGN_CIRCLE, SGN_CROSS }; SIGN g_AktualnyGracz; Zauwamy, i nie posiadamy o graczach adnych dodatkowych wiadomoci ponad fakt, jakie znaki (kko czy krzyyk) stawiaj oni na planszy. Informacja ta jest zatem jedynym kryterium, pozwalajcym na ich odrnienie - tote skrztnie z niej korzystamy, deklarujc zmienn odpowiedniego typu wyliczeniowego. Zamodelowanie waciwych struktur danych kontrolujcych przebieg gry to jedna z waniejszych czynnoci przy jej projektowaniu. W naszym przypadku s one bardzo proste (jedynie dwie zmienne), jednak zazwyczaj przyjmuj znacznie bardziej skomplikowan form. W swoim czasie zajmiemy si dokadniej tym zagadnieniem. Zdaje si, e to ju wszystkie zmienne, jakich bdziemy potrzebowa w naszym programie. Czas zatem zaj si jego drug, rwnie wan czci, czyli kodem odpowiedzialnym za waciwe funkcjonowanie.
Dziaanie programu
Przed chwil wprowadzilimy sobie dwie zmienne, ktre bd nam pomocne w zaprogramowaniu przebiegu naszej gry od pocztku a do koca. Teraz wanie
140
Podstawy programowania
zajmiemy si tyme szlakiem programu, czyli sposobem, w jaki bdzie on dziaa i prowadzi rozgrywk. Moemy go zilustrowa na diagramie podobnym do poniszego:
Widzimy na nim, w jaki sposb nastpuje przejcie pomidzy poszczeglnymi stanami gry, a wic kiedy i jak ma si zmienia warto zmiennej g_StanGry. Na tej podstawie moglibymy te okreli funkcje, ktre s konieczne do napisania oraz oglne czynnoci, jakie powinny one wykonywa. Powyszy rysunek jest uproszczonym diagramem przej stanw. To jeden z wielu rodzajw schematw, jakie mona wykona podczas projektowania programu. Potrzebujemy jednak bardziej szczegowego opisu. Lepiej jest te wykona go teraz, podczas projektowania aplikacji, ni przekada do czasu faktycznego programowania. Przy okazji ucilania przebiegu programu postaramy si uwzgldni w nim take pominite wczeniej, drobne szczegy - jak choby okrelenie aktualnego gracza i jego zmiana po kadym wykonanym ruchu. Nasz nowy szkic moe zatem wyglda tak:
Zoone zmienne
141
Mona tutaj zauway czwrk potencjalnych kandydatw na funkcje - s to sekwencje dziaa zawarte w zielonych polach. Faktycznie jednak dla dwch ostatnich (wygranej oraz remisu) byoby to pewnym naduyciem, gdy zawarte w nich operacje mona z powodzeniem doczy do funkcji obsugujcej rozgrywk. S to bowiem jedynie przypisania do zmiennej. Ostatecznie mamy przewidziane dwie zasadnicze funkcje programu: rozpoczcie gry, realizowane na pocztku. Jej zadaniem jest przygotowanie rozgrywki, czyli przede wszystkim wylosowanie gracza zaczynajcego rozgrywka, a wic wykonywanie kolejnych ruchw przez graczy Skoro wiemy ju, jak nasza gra ma dziaa od rodka, nie od rzeczy bdzie zajcie si metod jej komunikacji z ywymi uytkownikami-graczami.
Interfejs uytkownika
Hmm, jaki interfejs? Zazwyczaj pojcie to utosamiamy z okienkami, przyciskami, pola tekstowymi, paskami przewijania i innymi zdobyczami graficznych systemw operacyjnych. Tymczasem termin ten ma bardziej szersze znaczenie: Interfejs uytkownika (ang. user interface) to sposb, w jaki aplikacja prowadzi dialog z obsugujcymi j osobami. Obejmuje to zarwno pobieranie od nich danych wejciowych, jak i prezentacj wynikw pracy. Niewtpliwie wic moemy czu si uprawnieni, aby nazwa nasz skromn konsol penowartociowym rodkiem do realizacji interfejsu uytkownika! Pozwala ona przecie zarwno na uzyskiwanie informacji od osoby siedzcej za klawiatur, jak i na wypisywanie przeznaczonych dla niej komunikatw programu. Jak zatem mgby wyglda interfejs naszego programu? Twoje dotychczasowe, bogate dowiadczenie z aplikacjami konsolowymi powinny uatwi ci odpowied na to pytanie. Informacja, ktr prezentujemy uytkownikowi, to oczywicie aktualny stan planszy. Nie bdzie ona wprawdzie miaa postaci rysunkowej, jednake zwyky tekst cakiem dobrze sprawdzi si w roli grafiki. Po wywietleniu biecego stanu rozgrywki mona poprosi o gracza o wykonanie swojego ruchu. Gdybymy mogli obsuy myszk, wtedy posunicie byoby po prostu klikniciem, ale w tym wypadku musimy zadowoli si poleceniem wpisanym z klawiatury. Ostatecznie wygld naszego programu moe by podobny do poniszego:
Przy okazji zauway mona jedno z rozwiza problemu pt. Jak umoliwi wykonywanie ruchw, posugujc si jedynie klawiatur? Jest nim tutaj ponumerowanie kolejnych elementw tablicy-planszy liczbami od 1 do 9, a nastpnie proba do gracza o podanie jednej z nich. To chyba najwygodniejsza forma gry, jak potrafimy osign w tych niesprzyjajcych, tekstowych warunkach
142
Podstawy programowania
Metodami na przeliczanie pomidzy zwyczajnymi, dwoma wsprzdnymi tablicy oraz t jedn nibywsprzdn zajmiemy si podczas waciwego programowania. *** Na tym moemy ju zakoczy wstpne projektowanie naszego projektu :) Ustalilimy sposb jego dziaania, uywane przeze struktury danych, a nawet interfejs uytkownika. Wszystko to uatwi nam pisanie kodu caej aplikacji, ktre to rozpoczniemy ju za chwil. To by tylko skromny i bardzo nieformalny wstp do dziedziny informatyki zwanej inynieri oprogramowania. Zajmuje si ona projektowaniem wszelkiego rodzaju programw, poczynajc kady od pomysu i prowadzc poprzez model, kod, testowanie i wreszcie uytkowanie. Jeeli chciaby si dowiedzie wicej na ten interesujcy i przydatny temat, zapraszam do Materiau Pomocniczego C, Podstawy inynierii oprogramowania (aczkolwiek zalecam najpierw skoczenie tej czci kursu).
Kodowanie
Nareszcie moemy uruchomi swoje ulubione rodowisko programistyczne, wspierajce ulubiony jzyk programowania C++ i zacz waciwe programowanie zaprojektowanej ju gry. Uczy to wic, stwrz w nim nowy projekt, nazywajc go dowolnie54, i czekaj na dalsze rozkazy ;D
54
Kompletny kod caej aplikacji jest zawarty w przykadach do tego rozdziau i opatrzony nazw TicTacToe.
Zoone zmienne
143
Po co nam taki wasny nagwek? W jakim celu w ogle tworzy nagwki we wasnych projektach? Na powysze pytania istnieje dosy prosta odpowied. Aby j pozna przypomnijmy sobie, dlaczego doczamy do naszych programw nagwki w rodzaju iostream czy conio.h. Hmm? Tak jest - dziki nim jestemy w stanie korzysta z takich dobrodziejstw jzyka C++ jak strumienie wejcia i wyjcia czy acuchy znakw. Generalizujc, mona powiedzie, e nagwki udostpniaj pewien kod wszystkim moduom, ktre docz je przy pomocy dyrektywy #include. Dotychczas nie zastanawialimy si zbytnio nad miejscem, w ktrym egzystuje kod wykorzystywany przez nas za porednictwem nagwkw. Faktycznie moe on znajdowa si tu obok - w innym module tego samego projektu (i tak bdzie u nas), lecz rwnie dobrze istnie jedynie w skompilowanej postaci, na przykad biblioteki DLL. W przypadku dodanego wanie nagwka game.h mamy jednak niczym nieskrpowany dostp do odpowiadajcego mu moduu game.cpp. Zdawaoby si zatem, e plik nagwkowy jest tu cakowicie zbdny, a z kodu zawartego we wspomnianym module moglibymy z powodzeniem korzysta bezporednio. Nic bardziej bdnego! Za uyciem pliku nagwkowego przemawia wiele argumentw, a jednym z najwaniejszych jest zasada ograniczonego zaufania. Wedug niej kada czstka programu powinna posiada dostp jedynie do tych jego fragmentw, ktre s niezbdne do jej prawidowego funkcjonowania. U nas t czstk bdzie funkcja main(), zawarta w module main.cpp. Nie napisalimy jej jeszcze, ale potrafimy ju okreli, czego bdzie potrzebowaa do swego poprawnego dziaania. Bez wtpienia bd dla konieczne funkcje odpowiedzialne za wykonywanie posuni wskazanych przez graczy czy te procedury wywietlajce aktualny stan rozgrywki. Sposb, w jaki te zadania s realizowane, nie ma jednak adnego znaczenia!
144
Podstawy programowania
Podobnie przecie nie jestemy zobligowani do wiedzy o szczegach funkcjonowania strumieni konsoli, a mimo to stale z nich korzystamy. Plik nagwkowy peni wic rol swoistej zasony, przykrywajcej nieistotne detale implementacyjne, oraz klucza do tych zasobw programistycznych (typw, funkcji, zmiennych, itd.), ktrymi rzeczywicie chcemy si dzieli. Dlaczego w zasadzie mamy si z podobn nieufnoci odnosi do, bd co bd, samego siebie? Czy rzeczywicie w tym przypadku lepiej wiedzie mniej ni wicej? Gwn przyczyn, dla ktrej zasad ograniczonego zaufania uznaje si za powszechnie suszn, jest fakt, i wprowadza ona sporo porzdku do kadego kodu. Chroni te przed wieloma bdami spowodowanymi np. nadaniem jakiej zmiennej wartoci spoza dopuszczalnego zakresu czy te wywoania funkcji w zym kontekcie lub z nieprawidowymi parametrami. Nagwki s te pewnego rodzaju spisem treci kodu rdowego moduu czy biblioteki. Zawieraj najczciej deklaracje wszystkich typw oraz funkcji, wic mog niekiedy suy za prowizoryczn dokumentacj55 danego fragmentu programu, szczeglnie przydatn w jego dalszym tworzeniu. Z tego te powodu pliki nagwkowe s najczciej pierwszymi skadnikami aplikacji, na ktrych programista koncentruje swoj uwag. Pniej stanowi one rwnie podstaw do pisania waciwego kodu algorytmw. My take zaczniemy kodowanie naszego programu od pliku game.h; gotowy nagwek bdzie nam potem doskona pomoc naukow :)
55 Nie chodzi tu o podrcznik uytkownika programu, ale raczej o jego dokumentacj techniczn, czyli opis dziaania aplikacji od strony programisty.
Zoone zmienne
Teraz sprecyzujemy nieco nasze pojcie o tych funkcjach. Do pliku nagwkowego wpiszemy bowiem ich prototypy: // prototypy funkcji //-----------------// rozpoczcie gry bool StartGry(); // wykonanie ruchu bool Ruch(unsigned); // rysowanie planszy bool RysujPlansze();
145
C to takiego? Prototypy, zwane te deklaracjami funkcji, s jakby ich nagwkami oddzielonymi od bloku zasadniczego kodu (ciaa). Majc prototyp funkcji, posiadamy informacje o jej nazwie, typach parametrw oraz typie zwracanej wartoci. S one wystarczajce do jej wywoania, aczkolwiek nic nie mwi o faktycznych czynnociach, jakie dana funkcja wykonuje. Prototyp (deklaracja) funkcji to wstpne okrelenie jej nagwka. Stanowi on informacj dla kompilatora i programisty o sposobie, w jaki funkcja moe by wywoana. Z punktu widzenia kodera doczajcego pliki nagwkowe prototyp jest furtk do skarbca, przez ktr mona przej jedynie z zawizanymi oczami. Niesie wiedz o tym, co prototypowana funkcja robi, natomiast nie daje adnych wskazwek o sposobie, w jaki to czyni. Niemniej jest on nieodzowny, aby rzeczon funkcj mc wywoa. Warto wiedzie, e dotychczas znana nam forma funkcji jest zarwno jej prototypem (deklaracj), jak i definicj (implementacj). Prezentuje bowiem peni wiadomoci potrzebnych do jej wywoania, a poza tym zawiera wykonywalny kod funkcji. Dla nas, przyszych autorw zadeklarowanych wanie funkcji, prototyp jest kolejn okazj do zastanowienia si nad kodem poszczeglnych procedur programu. Precyzujc ich parametry i zwracane wartoci, budujemy wic solidne fundamenty pod ich niedalekie zaprogramowanie. Dla formalnoci zerknijmy jeszcze na skadni prototypu funkcji: typ_zwracanej_wartoci/void nazwa_funkcji([typ_parametru [nazwa], ...]); Oprcz uderzajcego podobiestwa do jej nagwka rzuca si w oczy rwnie fakt, i na etapie deklaracji nie jest konieczne podawanie nazw ewentualnych parametrw funkcji. Dla kompilatora w zupenoci bowiem wystarczaj ich typy. Ju ktry raz z kolei uczulam na koczcy instrukcj rednik. Bez niego kompilator bdzie oczekiwa bloku kodu funkcji, a przecie istot prototypu jest jego niepodawanie.
146
Podstawy programowania
najprostsz moliw obsug ewentualnych bdw. Warto o niej pomyle nawet wtedy, gdy pozornie nic zego nie moe si zdarzy. Wyrabiamy sobie w ten sposb dobre nawyki programistyczne, ktre zaprocentuj w przyszych, znacznie wikszych aplikacjach. A co z parametrami tych funkcji, a dokadniej z jedynym argumentem procedury Ruch()? Wydaje mi si, i atwo jest dociec jego znaczenia: to bowiem elementarna wielko, opisujca posunicie zamierzone przez gracza. Jej sens zosta ju zaprezentowany przy okazji projektu interfejsu uytkownika: chodzi po prostu o wprowadzony z klawiatury numer pola, na ktrym ma by postawione kko lub krzyyk.
Zaczynamy
Skoro wiemy ju dokadnie, jak wygldaj wizytwki naszych funkcji oraz z grubsza znamy naleyte algorytmy ich dziaania, napisanie odpowiedniego kodu powinno by po prostu dziecinn igraszk, prawda? :) Dobre samopoczucie moe si jednak okaza przedwczesne, gdy na twoim obecnym poziomie zaawansowania zadanie to wcale nie naley do najatwiejszych. Nie zostawi ci jednak bez pomocy! Dla szczeglnie ambitnych proponuj aczkolwiek samodzielne dokoczenie caego programu, a nastpnie porwnanie go z kodem doczonym do kursu. Samodzielne rozwizywanie problemw jest bowiem istot i najlepsz drog nauki programowania! Podczas zmagania si z tym wyzwaniem moesz jednak (i zapewne bdziesz musia) korzysta z innych rde informacji na temat programowania w C++, na przykad MSDN. Wiadomociami, ktre niemal na pewno oka ci si przydatne, s dokadne informacje o plikach nagwkowych i zwizanej z nimi dyrektywie #include oraz sowie kluczowym extern. Poszukaj ich w razie napotkania nieprzewidzianych trudnoci Jeeli poradzisz sobie z tym niezwykle trudnym zadaniem, bdziesz mg by z siebie niewypowiedzianie dumny :D Nagrod bdzie te cenne dowiadczenie, ktrego nie zdobdziesz inn drog! Mamy wic zamiar pisa instrukcje stanowice blok kodu funkcji, przeto powinnimy umieci je wewntrz moduu, a nie pliku nagwkowego. Dlatego te chwilowo porzucamy game.h i otwieramy nieskaony jeszcze adnym znakiem plik game.cpp. Nie znaczy to wszak, e nie bdziemy naszego nagwka w ogle potrzebowa. Przeciwnie, jest ona nam niezbdny - zawiera przecie definicje trzech typw wyliczeniowych, bez ktrych nie zdoamy si obej. Powinnimy zatem doczy go do naszego moduu przy pomocy poznanej jaki czas temu i stosowanej nieustannie dyrektywy #include: #include "game.h" Zwrmy uwag, i, inaczej ni to mamy w zwyczaju, ujlimy nazw pliku nagwkowego w cudzysowy zamiast nawiasw ostrych. Jest to konieczne; w ten sposb naley zaznacza nasze wasne nagwki, aby odrni je od fabrycznych (iostream, cmath itp.) Nazw doczanego pliku nagwkowego naley umieszcza w cudzysowach (""), jeli jest on w tym samym katalogu co modu, do ktrego chcemy go doczy. Moe by on take w jego pobliu (nad- lub podkatalogu) - wtedy uywa si wzgldnej cieki do pliku (np. "..\plik.h"). Doczenie wasnego nagwka nie zwalnia nas jednak od wykonania tej samej czynnoci na dwch innych tego typu plikach: #include <iostream> #include <ctime>
Zoone zmienne
147
Deklarujemy zmienne
Wczajc plik nagwkowy game.h mamy do dyspozycji zdefiniowane w nim typy SIGN, FIELD i GAMESTATE. Logiczne bdzie wic zadeklarowanie nalecych do zmiennych g_aPlansza, g_StanGry i g_AktualnyGracz: FIELD g_aPlansza[3][3] = { { FLD_EMPTY, FLD_EMPTY, FLD_EMPTY }, { FLD_EMPTY, FLD_EMPTY, FLD_EMPTY }, { FLD_EMPTY, FLD_EMPTY, FLD_EMPTY } }; GAMESTATE g_StanGry = GS_NOTSTARTED; SIGN g_AktualnyGracz; Skorzystamy z nich niejednokrotnie w kodzie moduu game.cpp, zatem powysze linijki naley umieci poza wszelkimi funkcjami.
Funkcja StartGry()
Nie jest to trudne, skoro nie napisalimy jeszcze absolutnie adnej funkcji :) Niezwocznie wic zabieramy si do pracy. Rozpoczniemy od tej procedury, ktra najszybciej da o sobie zna w gotowym programie - czyli StartGry(). Jak pamitamy, jej rol jest przede wszystkim wylosowanie gracza, ktry rozpocznie rozgrywk. Wczeniej jednak przydaoby si, aby funkcja sprawdzia, czy jest wywoywana w odpowiednim momencie - gdy gra faktycznie si jeszcze nie zacza: if (g_StanGry != GS_NOTSTARTED) return false; Jeeli warunek ten nie zostanie speniony, funkcja zwrci warto wskazujc na niepowodzenie swych dziaa. Jakich dziaa? Nietrudno zapisa je w postaci kodu C++: // losujemy gracza, ktry bdzie zaczyna srand (static_cast<unsigned>(time(NULL))); g_AktualnyGracz = (rand() % 2 == 0 ? SGN_CIRCLE : SGN_CROSS); // ustawiamy stan gry na ruch graczy g_StanGry = GS_MOVE; Losowanie liczby z przedziau <0; 2) jest nam czynnoci na wskro znajom. W poczeniu z operatorem warunkowym ?: pozwala na realizacj pierwszego z celw funkcji. Drugi jest tak elementarny, e w ogle nie wymaga komentarza. W kocu nie od dzi stykamy si z przypisaniem wartoci do zmiennej :) To ju wszystko, co byo przewidziane do zrobienia przez nasz funkcj StartGry(). W peni usatysfakcjonowani moemy wic zakoczy j zwrceniem informacji o pozytywnym rezultacie podjtych akcji: return true; Wywoujcy otrzyma wic wiadomo o tym, e czynnoci zlecone funkcji zostay zakoczone z sukcesem.
Funkcja Ruch()
Kolejn funkcj, na ktrej spocznie nasz wzrok, jest Ruch(). Ma ona za zadanie umieci w podanym polu znak aktualnego gracza (kko lub krzyyk) oraz sprawdzi stan planszy pod ktem ewentualnej wygranej ktrego z graczy lub remisu. Cakiem sporo do zrobienia, zatem do pracy, rodacy! ;D
148
Podstawy programowania
Pamitamy oczywicie, e rzeczona funkcja ma przyjmowa jeden parametr typu unsigned, wic jej szkielet wyglda bdzie nastpujco: bool Ruch(unsigned uNumerPola) { // ... } Na pocztku dokonamy tutaj podobnej co poprzednio kontroli ewentualnego bdu w postaci zego stanu gry. Dodamy jeszcze warunek sprawdzajcy, czy zadany numer pola zawiera si w przedziale <1; 9>. Cao wyglda nastpujco: if (g_StanGry != GS_MOVE) return false; if (!(uNumerPola >= 1 && uNumerPola <= 9)) return false; Jeeli punkt wykonania pokona obydwie te przeszkody, naleaoby uczyni ruch, o ktry uytkownik (za porednictwem parametru uNumerPola) prosi. W tym celu konieczne jest przeliczenie, zamieniajce pojedynczy numer pola (z zakresu od 1 do 9) na dwa indeksy naszej tablicy g_aPlansza (kady z przedziau od 0 do 2). Pomocy moe nam tu udzieli wizualny diagram, na przykad taki:
Odpowiednie formuki, wyliczajce wsprzdn pionow (uY) i poziom (uX) mona napisa, wykorzystujc dzielenie cakowitoliczbowe oraz reszt z niego: unsigned uY = (uNumerPola - 1) / 3; unsigned uX = (uNumerPola - 1) % 3; Odjcie jedynki jest spowodowane faktem, i w C++ tablice s indeksowane od zera (poza tym jest to dobra okazja do przypomnienia tej wanej kwestii :D). Majc ju obliczone oba indeksy, moemy sprbowa postawi symbol aktualnego gracza w podanym polu. Uda si to jednak wycznie wtedy, gdy nikt nas tutaj nie uprzedzi - a wic kiedy wskazane pole jest puste, co kontrolujemy dodatkowym testem: if (g_aPlansza[uY][uX] == FLD_EMPTY) // wstaw znak aktualnego gracza w podanym polu else return false; Jeli owa kontrola si powiedzie, musimy zrealizowa zamierzenie i wstawi kko lub krzyyk - zalenie do tego, ktry gracz jest teraz uprawniony do ruchu - w danie miejsce. Informacj o aktualnym graczu przechowuje rzecz jasna zmienna g_AktualnyGracz. Niemoliwe jest jednak jej zwyke przypisanie w rodzaju: g_aPlansza[uY][uX] = g_AktualnyGracz;
Zoone zmienne
149
Wystpiby tu bowiem konflikt typw, gdy FIELD i SIGN s typami wyliczeniowymi, nijak ze sob niekompatybilnymi. Czybymy musieli zatem uciec si do topornej instrukcji switch? Odpowied na szczcie brzmi nie. Inne, lepsze rozwizanie polega na dopasowaniu do siebie staych obu typw, reprezentujcych kko i krzyyk. Niech bd one sobie rwne; w tym celu zmodyfikujemy definicj FIELD (w pliku game.h): enum FIELD { FLD_EMPTY, FLD_CIRCLE FLD_CROSS = SGN_CIRCLE, = SGN_CROSS };
Po tym zabiegu caa operacja sprowadza si do zwykego rzutowania: g_aPlansza[uY][uX] = static_cast<FIELD>(g_AktualnyGracz); Liczbowe wartoci obu zmiennych bd si zgadza, ale interpretacja kadej z nich bdzie odmienna. Tak czy owak, osignlimy obrany cel, wic wszystko jest w porzdku :) Niedugo zreszt ponownie skorzystamy z tej prostej i efektywnej sztuczki. Nasza funkcja wykonuje ju poow zada, do ktrych j przeznaczylimy. Niestety, mniejsz poow :D Oto bowiem mamy przed sob znacznie powaniejsze wyzwanie ni kilka if-w, a mianowicie zaprogramowanie algorytmu lustrujcego plansz i stwierdzajcego na jej podstawie ewentualn wygran ktrego z graczy lub remis. Trzeba wic zakasa rkawy i wyty intelekt Zajmijmy si na razie wykrywaniem zwycistw. Doskonale chyba wiemy, e do wygranej w naszej grze potrzebne jest graczowi utworzenie z wasnych znakw linii poziomej, pionowej lub ukonej, obejmujcej trzy pola. cznie mamy wic osiem moliwych linii, a dla kadej po trzy pola opisane dwiema wsprzdnymi. Daje nam to, bagatelka, 48 warunkw do zakodowania, czyli 8 makabrycznych instrukcji if z szecioczonowymi (!) wyraeniami logicznymi w kadej! Brr, brzmi to wrcz okropnie Jak to jednak nierzadko bywa, istnieje rozwizanie alternatywne, ktre jest z reguy lepsze :) Tym razem jest nim uycie tablicy przegldowej, w ktr wpiszemy wszystkie wygrywajce zestawy pl: osiem linii po trzy pola po dwie wsprzdne daje nam ostatecznie tak oto, nieco zakrcon, sta56: const LINIE[][3][2] = { { { {{ {{ {{ {{ {{ {{ {{ 0,0 1,0 2,0 0,0 0,1 0,2 0,0 2,0 }, }, }, }, }, }, }, }, { { { { { { { { 0,1 1,1 2,1 1,0 1,1 1,2 1,1 1,1 }, }, }, }, }, }, }, }, { { { { { { { { 0,2 1,2 2,2 2,0 2,1 2,2 2,2 0,2 } } } } } } } } }, // grna pozioma },// rod. pozioma },// dolna pozioma }, // lewa pionowa }, // rod. pionowa }, // prawa pionowa }, // p. backslashowa } }; // p. slashowa
Przy jej deklarowaniu korzystalimy z faktu, i w takich wypadkach pierwszy wymiar tablicy mona pomin, lecz rwnie poprawne byoby wpisanie tam 8 explicit. A zatem mamy ju tablic przegldow Przydaoby si wic jako j przeglda :) Oprcz tego mamy jednak dodatkowy cel, czyli znalezienie linii wypenionej tymi samymi znakami, nasze przegldanie bdzie wobec tego nieco skomplikowane i przedstawia si nastpujco:
56 Brak nazwy typu w deklaracji zmiennej sprawia, i bdzie nalee ona do domylnego typu int. Tutaj oznacza to, e elementy naszej tablicy bd liczbami cakowitymi.
150
Podstawy programowania
FIELD Pole, ZgodnePole; unsigned uLiczbaZgodnychPol; for (int i = 0; i < 8; ++i) { // i przebiega po kolejnych moliwych liniach (jest ich osiem) // zerujemy zmienne pomocnicze Pole = ZgodnePole = FLD_EMPTY; uLiczbaZgodnychPol = 0; // obie zmienne == FLD_EMPTY
for (int j = 0; j < 3; ++j) { // j przebiega po trzech polach w kadej linii // pobieramy rzeczone pole // to zdecydowanie najbardziej pogmatwane wyraenie :) Pole = g_aPlansza[LINIE[i][j][0]][LINIE[i][j][1]]; // jeli sprawdzane pole rne od tego, ktre ma si zgadza... if (Pole != ZgodnePole) { // to zmieniamy zgadzane pole na to aktualne ZgodnePole = Pole; uLiczbaZgodnychPol = 1; } else // jeli natomiast oba pola si zgadzaj, no to // inkrementujemy licznik takich zgodnych pl ++uLiczbaZgodnychPol;
// teraz sprawdzamy, czy udao nam si zgodzi lini if (uLiczbaZgodnychPol == 3 && ZgodnePole != FLD_EMPTY) { // jeeli tak, no to ustawiamy stan gry na wygran g_StanGry = GS_WON; // przerywamy ptl i funkcj return true;
No nie - powiesz pewnie - Teraz to ju przesadzie! ;) Ja jednak upieram si, i nie cakiem masz racj, a podany algorytm tylko wyglda strasznie, lecz w istocie jest bardzo prosty. Na pocztek deklarujemy sobie trzy zmienne pomocnicze, ktre wydatnie przydadz si w caej operacji. Szczegln rol spenia tu uLiczbaZgodnychPol; jej nazwa mwi wiele. Zmienna ta bdzie przechowywaa liczb identycznych pl w aktualnie badanej linii warto rwna 3 stanie si wic podstaw do stwierdzenia obecnoci wygrywajcej kombinacji znakw. Dalej przystpujemy do sprawdzania wszystkich omiu interesujcych sytuacji, determinujcych ewentualne zwycistwo. Na scen wkracza wic ptla for; na pocztku jej cyklu dokonujemy zerowania wartoci zmiennych pomocniczych, aby potem wpa w kolejn ptl :) Ta jednak bdzie przeskakiwaa po trzech polach kadej ze sprawdzanych linii: for (int j = 0; j < 3; ++j) { Pole = g_aPlansza[LINIE[i][j][0]][LINIE[i][j][1]];
Zoone zmienne
151
Koszmarnie wygldajca pierwsza linijka bloku powyszej ptli nie bdzie wydawa si a tak straszne, jeli uwiadomimy sobie, i LINIE[i][j][0] oraz LINIE[i][j][1] to odpowiednio: wsprzdna pionowa oraz pozioma j-tego pola i-tej potencjalnie wygrywajcej linii. Susznie wic uywamy ich jako indeksw tablicy g_aPlansza, pobierajc stan pola do sprawdzenia. Nastpujca dalej instrukcja warunkowa rozstrzyga, czy owe pole zgadza si z ewentualnymi poprzednimi - tzn. jeeli na przykad poprzednio sprawdzane pole zawierao kko, to aktualne take powinno mieci ten symbol. W przypadku gdy warunek ten nie jest speniony, sekwencja zgodnych pl urywa si, co oznacza w tym wypadku wyzerowanie licznika uLiczbaZgodnychPol. Sytuacja przeciwstawna - gdy badane pole jest ju ktrym z kolei kkiem lub krzyykiem - skutkuje naturalnie zwikszeniem tego licznika o jeden. Po zakoczeniu caej ptli (czyli wykonaniu trzech cykli, po jednym dla kadego pola) nastpuje kontrola otrzymanych rezultatw. Najwaniejszym z nich jest wspomniany licznik uLiczbaZgodnychPol, ktrego warto konfrontujemy z trjk. Jednoczenie sprawdzamy, czy zgodzone pole nie jest przypadkiem polem pustym, bo przecie z takiej zgodnoci nic nam nie wynika. Oba te testy wykonuje instrukcja: if (uLiczbaZgodnychPol == 3 && ZgodnePole != FLD_EMPTY) Spenienie tego warunku daje pewno, i mamy do czynienia z prawidow sekwencj trzech kek lub krzyykw. Susznie wic moemy wtedy przyzna palm zwycistwa aktualnemu graczowi i zakoczy ca funkcj: g_StanGry = GS_WON; return true; W przeciwnym wypadku nasza gwna ptla si zaptla w swym kolejnym cyklu i bada w nim kolejn ustalon lini symboli - i tak a do znalezienia pasujcej kolumny, rzdu lub przektnej albo wyczerpania si tablicy przegldowej LINIE. Uff? Nie, to jeszcze nie wszystko! Nie zapominajmy przecie, e zwycistwo nie jest jedynym moliwych rozstrzygniciem rozgrywki. Drugim jest remis - zapenienie wszystkich pl planszy symbolami graczy bez utworzenia adnej wygrywajcej linii. Jak obsuy tak sytuacj? Wbrew pozorom nie jest to wcale trudne, gdy moemy wykorzysta do tego fakt, i przebycie przez program poprzedniej, wariackiej ptli oznacza nieobecno na planszy adnych uoe zapewniajcych zwycistwo. Niejako z miejsca mamy wic speniony pierwszy warunek konieczny do remisu. Drugi natomiast - szczelne wypenienie caej planszy - jest bardzo atwy do sprawdzenia i wymagania jedynie zliczenia wszystkich niepustych jej pl: unsigned uLiczbaZapelnionychPol = 0; for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j) if (g_aPlansza[i][j] != FLD_EMPTY) ++uLiczbaZapelnionychPol;
152
Podstawy programowania
Jeeli jakim dziwnym sposobem ilo ta wyniesie 9, znaczy to bdzie, e gra musi si zakoczy z powodu braku wolnych miejsc :) W takich okolicznociach wynikiem rozgrywki bdzie tylko mao satysfakcjonujcy remis: if (uLiczbaZapelnionychPol == 3*3) { g_StanGry = GS_DRAW; return true; } W taki oto sposb wykrylimy i obsuylimy obydwie sytuacje wyjtkowe, koczce gr - zwycistwo jednego z graczy lub remis. Pozostao nam jeszcze zajcie si bardziej zwyczajnym rezultatem wykonania ruchu, kiedy to nie powoduje on adnych dodatkowych efektw. Naley wtedy przekaza prawo do posunicia drugiemu graczowi, co te czynimy: g_AktualnyGracz = (g_AktualnyGracz == SGN_CIRCLE ? SGN_CROSS : SGN_CIRCLE); Przy pomocy operatora warunkowego zmieniamy po prostu znak aktualnego gracza na przeciwny (z kka na krzyyk i odwrotnie), osigajc zamierzony skutek. Jest to jednoczenie ostatnia czynno funkcji Ruch()! Wreszcie, po dugich bojach i blach gowy ;) moemy j zakoczy zwrceniem bezwarunkowo pozytywnego wyniku: return true; a nastpnie uda si po co do jedzenia ;-)
Funkcja RysujPlansze()
Jako ostatni napiszemy funkcj, ktrej zadaniem bdzie wywietlenie na ekranie (czyli w oknie konsoli) biecego stanu gry:
Najwaniejsz jego skadow bdzie naturalnie osawiona plansza, o zajcie ktrej tocz boje nasi dwaj gracze. Oprcz niej mona jednak wyrni take kilka innych elementw. Wszystkie one bd rysowane przez funkcj RysujPlansze(). Niezwocznie wic rozpocznijmy jej implementacj! Tradycyjnie ju pierwsze linijki s szukaniem dziury w caym, czyli potencjalnego bdu. Tym razem usterk bdzie wywoanie kodowanej wanie funkcji przez rozpoczciem waciwego pojedynku, gdy w tej sytuacji nie ma w zasadzie nic do pokazania. Logiczn konsekwencj jest wtedy przerwanie funkcji: if (g_StanGry == GS_NOTSTARTED) return false;
Zoone zmienne
153
Jako e jednak wierzymy w rozsdek programisty wywoujcego pisan teraz funkcj (czyli nomen-omen w swj wasny), przejdmy raczej do kodowania jej waciwej czci rysujcej. Od czego zaczniemy? Odpowied nie jest szczeglnie trudna; co ciekawe, w przypadku kadej innej gry i jej odpowiedniej funkcji byaby taka sama. Rozpoczniemy bowiem od wyczyszczenia caego ekranu (czyli konsoli) - tak, aby mie wolny obszar dziaania. Dokonamy tego poprzez polecenie systemowe CLS, ktre wywoamy funkcj C++ o nazwie system(): system ("cls"); Majc oczyszczone przedpole przystpujemy do zasadniczego rysowania. Ze wzgldu na specyfik tekstowej konsoli zmuszeni jestemy do zapeniania jej wierszami, od gry do dou. Nie powinno nam to jednak zbytnio przeszkadza. Na samej grze umiecimy tytu naszej gry, stay i niezmienny. Kod odpowiedzialny za t czynno przedstawia si wic raczej trywialnie: std::cout << " KOLKO I KRZYZYK " << std::endl; std::cout << "---------------------" << std::endl; std::cout << std::endl; dnych wrae pocieszam jednak, i dalej bdzie ju ciekawiej :) Oto mianowicie przystpujemy do prezentacji planszy w postaci tekstowej - z zaznaczonymi kkami i krzyykami postawionymi przez graczy oraz numerami wolnych pl. Operacj t przeprowadzamy w sposb nastpujcy: std::cout << " -----" << std::endl; for (int i = 0; i < 3; ++i) { // lewa cz ramki std::cout << " |"; // wiersz for (int j = 0; j < 3; ++j) { if (g_aPlansza[i][j] == FLD_EMPTY) // numer pola std::cout << i * 3 + j + 1; else // tutaj wywietlamy kko lub krzyyk... ale jak? :) } // prawa cz ramki std::cout << "|" << std::endl;
} std::cout << " -----" << std::endl; std::cout << std::endl; Cay kod to oczywicie znowu dwie zagniedone ptle for - stay element pracy z dwuwymiarow tablic. Zewntrzna przebiega po poszczeglnych wierszach planszy, za wewntrzna po jej pojedynczych polach. Wywietlenie takiego pola oznacza pokazanie albo jego numerku (jeeli jest puste), albo duej litery O lub X, symulujcej wstawione we kko lub krzyyk. Numerek wyliczamy poprzez prost formuk i * 3 + j + 1 (dodanie jedynki to znowu kwestia indeksw liczonych od zera), w ktrej i jest numerem wiersza, za j - kolumny. C jednak zrobi z drugim przypadkiem - zajtym polem? Musimy przecie rozrni kka i krzyyki Mona oczywicie skorzysta z instrukcji if lub operatora ?:, jednak ju raz zastosowalimy lepsze rozwizanie. Dopasujmy mianowicie stae typu FIELD (kady
154
Podstawy programowania
element tablicy g_aPlansza naley przecie do tego typu) do znakw 'O' i 'X'. Przypatrzmy si najpierw definicji rzeczonego typu: enum FIELD { FLD_EMPTY, FLD_CIRCLE FLD_CROSS = SGN_CIRCLE, = SGN_CROSS };
Wida nim skutek pierwszego zastosowania sztuczki, z ktrej chcemy znowu skorzysta. Dotyczy on zreszt interesujcych nas staych FLD_CIRCLE i FLD_CROSS, rwnych odpowiednio SGN_CIRCLE i SGN_CROSS. Czy to oznacza, i z triku nici? Bynajmniej nie. Nie moemy wprawdzie bezporednio zmieni wartoci interesujcych nas staych, ale moliwe jest signicie do rde i zmodyfikowanie SGN_CIRCLE oraz SGN_CROSS, zadeklarowanych w typie SIGN: enum SIGN { SGN_CIRCLE = 'O', SGN_CROSS = 'X' }; T drog, porednio, zmienimy te wartoci staych FLD_CIRCLE i FLD_CROSS, przypisujc im kody ANSI wielkich liter O i X. Teraz ju moemy skorzysta z rzutowania na typ char, by wywietli niepuste pole planszy: std::cout << static_cast<char>(g_aPlansza[i][j]); Kod rysujcy obszar rozgrywki jest tym samym skoczony. Pozosta nam jedynie komunikat o stanie gry, wywietlany najniej. Zalenie od biecych warunkw (wartoci zmiennej g_StanGry) moe on przyjmowa form proby o wpisanie kolejnego ruchu lub te zwyczajnej informacji o wygranej lub remisie: switch (g_StanGry) { case GS_MOVE: // proba std::cout std::cout std::cout
break; case GS_WON: // informacja o wygranej std::cout << "Wygral gracz stawiajacy "; std::cout << (g_AktualnyGracz == SGN_CIRCLE ? "kolka" : "krzyzyki") << "!"; break; case GS_DRAW: // informacja o remisie std::cout << "Remis!"; break;
o nastpny ruch << "Podaj numer pola, w ktorym" << std::endl; << "chcesz postawic "; << (g_AktualnyGracz == SGN_CIRCLE ? "kolko" : "krzyzyk") << ": ";
Analizy powyszego kodu moesz z atwoci dokona samodzielnie57. Na tyme elemencie scenografii koczymy nasz funkcj RysujPlansze(), wieczc j oczywicie zwyczajowym oddaniem wartoci true:
57
A jake! Ju coraz rzadziej bd omawia podobnie elementarne kody rdowe, bdce prostym wykorzystaniem doskonale ci znanych konstrukcji jzyka C++. Jeeli solennie przykadae si do nauki, nie powinno by to dla ciebie adn niedogodnoci, za w zamian pozwoli na dogbne zajcie si nowymi zagadnieniami bez koncentrowania wikszej uwagi na banaach.
Zoone zmienne
155
return true; Moemy na koniec zauway, i piszc t funkcj uporalimy si jednoczenie z elementem programu o nazwie interfejs uytkownika :D
156
Podstawy programowania
if (g_StanGry == GS_MOVE) { unsigned uNumerPola; std::cin >> uNumerPola; Ruch (uNumerPola); } Pozytywny wynik wspomnianego testu susznie skania nas do uycia strumienia wejcia i pobrania od uytkownika numeru pola, w ktre chce wstawi swoje kko lub krzyyk. Przekazujemy go potem do funkcji Ruch(), serca naszej gry. Nastpujce po sobie posunicia graczy, czyli kolejne cykle ptli, doprowadz w kocu do rozstrzygnicia rozgrywki - czyjej wygranej albo obustronnego remisu. I to jest wanie warunek, na ktry czekamy: else if (g_StanGry == GS_WON || g_StanGry == GS_DRAW) break; Przerywamy wtedy ptl, zostawiajc na ekranie kocowy stan planszy oraz odpowiedni komunikat. Aby uytkownicy mieli szans go zobaczy, stosujemy rzecz jasna funkcj getch(): getch(); Po odebraniu wcinicia dowolnego klawisza program moe si ju ze spokojem zamkn ;)
Uroki kompilacji
Fanfary! Zdaje si, e wanie zakoczylimy kodowanie naszego wielkiego projektu! Nareszcie zatem moemy przeprowadzi jego kompilacj i uzyska gotowy do uruchomienia plik wykonywalny. Zrbmy wic to! Uruchom Visual Studio (jeeli je przypadkiem zamkne), otwrz swj projekt, zamknij drzwi i okna, wyprowad zwierzta domowe, wcz automatyczn sekretark i wcinij klawisz F7 (lub wybierz pozycj menu Build|Build Solution) *** Co si stao? Wyglda na to, e nie wszystko udao si tak dobrze, jak tego oczekiwalimy. Zamiast dziaajcej aplikacji kompilator uraczy nas czterema bdami:
c:\Programy\TicTacToe\main.cpp(20) : error C2065: 'g_StanGry' : undeclared identifier c:\Programy\TicTacToe\main.cpp(20) : error C2677: binary '==' : no global operator found which takes type 'GAMESTATE' (or there is no acceptable conversion) c:\Programy\TicTacToe\main.cpp(28) : error C2677: binary '==' : no global operator found which takes type 'GAMESTATE' (or there is no acceptable conversion) c:\Programy\TicTacToe\main.cpp(28) : error C2677: binary '==' : no global operator found which takes type 'GAMESTATE' (or there is no acceptable conversion)
Wszystkie one dotycz tego samego, ale najwicej mwi nam pierwszy z nich. Dwukrotnie kliknicie na dotyczcy go komunikat przeniesie nas bowiem do linijki: if (g_StanGry == GS_MOVE) Wystpuje w niej nazwa zmiennej g_StanGry, ktra, sdzc po owym komunikacie, jest tutaj uznawana za niezadeklarowan Ale dlaczego?! Przecie z pewnoci umiecilimy jej deklaracj w kodzie programu. Co wicej, stale korzystalimy z teje zmiennej w funkcjach StartGry(), Ruch() i
Zoone zmienne
157
RysujPlansze(), do ktrych kompilator nie ma najmniejszych zastrzee. Czyby wic tutaj dopada go naga amnezja? Wyjanienie tego, jak by si wydawao, do dziwnego zjawiska jest jednak w miar logiczne. Ot g_StanGry zostaa zadeklarowana wewntrz moduu game.cpp, wic jej zasig ogranicza si jedynie do tego moduu. Funkcja main(), znajdujca si w pliku main.cpp, jest poza tym zakresem, zatem dla niej rzeczona zmienna po prostu nie istnieje. Nic dziwnego, i kompilator staje si wobec nieznanej nazwy g_StanGry zupenie bezradny. Nasuwa si oczywicie pytanie: jak zaradzi temu problemowi? Co zrobi, aby nasza zmienna bya dostpna wewntrz funkcji main()? Chyba najszybciej pomyle mona o przeniesieniu jej deklaracji w obszar wsplny dla obu moduw game.cpp oraz main.cpp. Takim wspdzielonym terenem jest naturalnie plik nagwkowy game.h. Czy naley wic umieci tam deklaracj GAMESTATE g_StanGry = GS_NOTSTARTED;? Niestety, nie jest to poprawne. Musimy bowiem wiedzie, e zmienna nie moe rezydowa wewntrz nagwka! Jej prawidowe zdefiniowanie powinno by zawsze umieszczone w module kodu. W przeciwnym razie kady modu, ktry doczy plik nagwkowy z definicj zmiennej, stworzy swoj wasn kopi teje! U nas znaczyoby to, e zarwno main.cpp, jak i game.cpp posiadaj zmienne o nazwach g_StanGry, ale s one od siebie cakowicie niezalene i nie wiedz o sobie nawzajem! Definicja musi zatem pozosta na swoim miejscu, ale plik nagwkowy niewtpliwie nam si przyda. Mianowicie, wpiszemy do nastpujc linijk: extern GAMESTATE g_StanGry; Jest to tak zwana deklaracja zapowiadajca zmiennej. Jej zadaniem jest poinformowanie kompilatora, e gdzie w programie59 istnieje zmienna o podanej nazwie i typie. Deklaracja ta nie tworzy adnego nowego bytu ani nie rezerwuje dla miejsca w pamici operacyjnej, lecz jedynie zapowiada (std nazwa), i czynno ta zostanie wykonana. Obietnica ta moe by speniona podczas kompilacji lub (tak jak u nas) dopiero w czasie linkowania. Z praktycznego punktu widzenia deklaracja extern (ang. external - zewntrzny) peni bardzo podobn rol, co prototyp funkcji. Podaje bowiem jedynie minimum informacji, potrzebnych do skorzystania z deklarowanego tworu bez marudzenia kompilatora, a jednoczenie odkada jego waciw definicj w inne miejsce i/lub czas. Deklaracja zapowiadajca (ang. forward declaration) to czciowe okrelenie jakiego programistycznego bytu. Nie definiuje dokadnie wszystkich jego aspektw, ale wystarcza do skorzystania z niego wewntrz zakresu umieszczenia deklaracji. Przykadem moe by prototyp funkcji czy uycie sowa extern dla zmiennej. Umieszczenie powyszej deklaracji w pliku nagwkowym game.h udostpnia zatem zmienn g_StanGry wszystkim moduom, ktre docz wspomniany nagwek. Tym samym jest ju ona znana take funkcji main(), wic ponowna kompilacja powinna przebiec bez adnych problemw. Czujny czytelnik zauway pewnie, e do swobodnie operuj terminami deklaracja oraz definicja, uywajc ich zamiennie. Niektrzy puryci ka jednak je rozrnia. Wedug nich jedynie to, co nazwalimy przed momentem deklaracja zapowiadajc, mona nazwa krtko deklaracj. Definicj ma by za to dokadne sprecyzowanie cech danego obiektu, oraz, przede wszystkim, przygotowanie dla niego miejsca w pamici operacyjnej.
59
158
Podstawy programowania
Zgodnie z tak terminologi instrukcje w rodzaju int nX; czy float fY; miayby by definicjami zmiennych, natomiast extern int nX; oraz extern float fY; deklaracjami. Osobicie twierdz, e jest to jeden z najjaskrawszych przykadw szukania dziury w caym i prb niezmiernego gmatwania programistycznego sownika. Czy ktokolwiek przecie mwi o definicjach zmiennych? Pojcie to brzmi tym bardziej sztucznie, e owe definicje nie przynosz adnych dodatkowych informacji w stosunku do deklaracji, a skadniowo s od nich nawet krtsze! Jak wic w takiej sytuacji nie nazwa spierania si o nazewnictwo zwyczajnym malkontenctwem? :)
Uruchamiamy aplikacj
To niemale niewiarygodne, jednak stao si faktem! Zakoczylimy w kocu programowanie naszej gry! Wreszcie moesz wic uy klawisza F5, by cieszy tym oto wspaniaym widokiem:
A po kilkunastominutowym, zasuonym relaksie przy wasnorcznie napisanej grze przejd do dalszej czci tekstu :)
Wnioski
Stworzye wanie (przy drobnej pomocy :D) swj pierwszy w miar powany program, w dodatku to, co lubimy najbardziej - czyli gr. Zdobyte przy tej okazji dowiadczenie jest znacznie cenniejsze od najlepszego nawet, lecz tylko teoretycznego wykadu. Warto wic podsumowa nasz prac, a przy okazji odpowiedzie na pewne oglne pytania, ktre by moe przyszy ci na myl podczas realizacji tego projektu.
Dziwaczne projektowanie
Tworzenie naszej gry rozpoczlimy od jej dokadnego zaprojektowania. Miao ono na celu wykreowanie komputerowego modelu znanej od dziesicioleci gry dwuosobowej i zaadaptowanie go do potrzeb kodowania w C++. W tym celu podzielilimy sobie zadanie na trzy czci: okrelenie struktur danych wykorzystywanych przez aplikacj sprecyzowanie wykonywanych przez ni czynnoci stworzenie interfejsu uytkownika Aby zrealizowa pierwsze dwie, musielimy przyj do dziwn i raczej nienaturaln drog rozumowania. Naleao bowiem zapomnie o takich namacalnych obiektach jak plansza, gracz czy rozgrywka. Zamiast tego mwilimy o pewnych danych, na ktrych program mia wykonywa jakie operacje. Te dwa wiaty - statycznych informacji oraz dynamicznych dziaa - rozdzieliy nam owe naturalne obiekty zwizane z gr i kazay oddzielnie zajmowa si ich cechami (jak np. symbole graczy) oraz realizowanymi przeze czynnociami (np. wykonanie ruchu).
Zoone zmienne
159
Podejcie to, zwane programowaniem strukturalnym, mogo by dla ciebie trudne do zrozumienia i sztuczne. Nie martw si tym, gdy podobnie uwaa wikszo wspczesnych koderw! Czy to znaczy, e programowanie jest udrk? Domylasz si pewnie, e wszystko co niedawno uczynilimy, daoby si zrobi bardziej naturalnie i intuicyjne. Masz w tym cakowit racj! Ju w nastpnym rozdziale poznamy znacznie wygodniejsz i przyjaniejsz technik programowania, ktry zbliy kodowanie do ludzkiego sposobu mylenia.
Do skomplikowane algorytmy
Kiedy ju uporalimy si z projektowaniem, przyszed czas na uruchomienie naszego ulubionego rodowiska programistycznego i wpisanie kodu tworzonej aplikacji. Jakkolwiek wikszo uytych przy tym konstrukcji jzyka C++ bya ci znana od dawna, a dua cz pozostaej mniejszoci wprowadzona w tym rozdziale, sam kod nie nalea z pewnoci do elementarnych. Rnica midzy poprzednimi, przykadowymi programami bya znaczca i widoczna niemal przez cay czas. Na czym ona polegaa? Po prostu jzyk programowania przesta tu by celem, a sta si rodkiem. Ju nie tylko pry swe muskuy i prezentowa szeroki wachlarz moliwoci. Sta si w pokornym sug, ktry spenia nasze wymagania w imi wyszego denia, ktrym byo napisanie dziaajcej i sensownej aplikacji. Oczywiste jest wic, i zaczlimy wymaga wicej take od siebie. Pisane algorytmy nie byy ju trywialnymi przepisami, wywaajcymi otwarte drzwi. Wyyny w tym wzgldzie osignlimy chyba przy sprawdzaniu stanu planszy w poszukiwaniu ewentualnych sekwencji wygrywajcych. Zadanie to byo swoiste i unikalne dla naszego kodu, dlatego te wymagao nieszablonowych rozwiza. Takich, z jakimi bdziesz si czsto spotyka.
Organizacja kodu
Ostatnia uwaga dotyczy porzdku, jaki wprowadzilimy w nasz kod rdowy. Zamiast pojedynczego moduu zastosowalimy dwa i zintegrowalimy je przy pomocy wasnego pliku nagwkowego. Nie obyo si rzecz jasna bez drobnych problemw, ale oglnie zrobilimy to w cakowicie poprawny i efektywny sposb. Nie mona te zapomina o tym, e jednoczenie poznalimy kolejny skrawek informacji na temat programowania w C++, tym razem dotyczcy dyrektywy #include, prototypw funkcji oraz modyfikatora extern. Drogi samodzielny programisto - ty, ktry dokoczye kod gry od momentu, w ktrym rozstalimy si nagwkiem game.h, bez zagldania do dalszej czci tekstu! Jeeli udao ci si dokona tego z zachowaniem zaoonej funkcjonalnoci programu oraz podziau kodu na trzy odrbne pliki, to naprawd chyl czoa :) Znaczy to, e jeste wrcz idealnym kandydatem na wietnego programist, gdy sam potrafie rozwiza postawiony przed tob szereg problemw oraz znalaze brakujce ci informacje w odpowiednich rdach. Gratulacje! Aby jednak unikn ewentualnych kopotw ze zrozumieniem dalszej czci kursu, doradzam powrt do opuszczonego fragmentu tekstu i przeczytanie chocia tych urywkw, ktre dostarczaj wspomnianych nowych informacji z zakresu jzyka C++.
Podsumowanie
Dotarlimy (wreszcie!) do koca tego rozdziau. Nabye w nim bardzo duo wiadomoci na temat modelowania zoonych struktur danych w C++.
160
Podstawy programowania
Zaczlimy od prezentacji tablic, czyli zestaww okrelonej liczby tych samych elementw, opatrzonych wspln nazw. Poznalimy sposoby ich deklaracji oraz uycia w programie, a take moliwe zastosowania. Dalej zajlimy si definiowaniem nowych, wasnych typw danych. Wrd nich byy typy wyliczeniowe, dopuszczajce jedynie kilka moliwych wartoci, oraz agregaty w rodzaju struktur, zamykajce kilka pojedynczych informacji w jedn cao. Zetknlimy si przy tym z wieloma przykadami ich zastosowania w programowaniu. Wreszcie, na ukoronowanie tego i kilku poprzednich rozdziaw stworzylimy cakiem spory i cakiem skomplikowany program, bdcy w dodatku gr! Mielimy niepowtarzaln okazj na zastosowanie zdobytych ostatnimi czasy umiejtnoci w praktyce. Kolejny rozdzia przyniesie nam natomiast zupenie nowe spojrzenie na programowanie w C++.
Pytania i zadania
Jako e mamy za ju sob sporo wyczerpujcego kodowania, nie zadam zbyt wielu programw do samodzielnego napisania. Nie uciekniesz jednak od pyta sprawdzajcych wiedz! :)
Pytania
1. 2. 3. 4. Co to jest tablica? Jak deklarujemy tablice? W jaki sposb uywamy ptli for oraz tablic? Jak C++ obsuguje tablice wielowymiarowe? Czym s one w istocie? Czym s i do czego su typy wyliczeniowe? Dlaczego s lepsze od zwykych staych? 5. Jak definiujemy typy strukturalne? 6. Jak drog mona dosta si do pojedynczych pl struktury?
wiczenia
1. Napisz program, ktry pozwoli uytkownikowi na wprowadzenie dowolnej iloci liczb (ilo t bdzie podawa na pocztku) i obliczenie ich redniej arytmetycznej. Podawane liczby przechowuj w 100-elementowej tablicy (wykorzystasz ze tylko cz). (Trudne) Moesz te zrobi tak, by program nie pyta o ilo liczb, lecz prosi o kolejne a do wpisania innych znakw. (Bardzo trudne) Czy mona jako zapobiec marnotrawstwu pamici, zwizanemu z tak du, lecz uywan tylko czciowo tablic? Jak? 2. Stwrz aplikacj, ktra bdzie pokazywaa liczb dni do koca biecego roku. Wykorzystaj w niej struktur tm i funkcj localtime() w taki sam sposb, jak w przykadzie Biorhytm. 3. (Trudne) W naszej grze w kko i krzyyk jest ukryta pewna usterka. Objawia si wtedy, gdy gracz wpisze co innego ni liczb jako numer pola. Sprbuj naprawi ten bd; niech program reaguje tak samo, jak na warto spoza przedziau <1; 9>. Wskazwka: zadanie jest podobne do trudniejszego wariantu wiczenia 1. 4. (Bardzo trudne) Ulepsz napisan gr. Niech rozmiar planszy nie bdzie zawsze wynosi 33, lecz mg by zdefiniowany jako staa w pliku game.h. Wskazwka: poniewa plansza pozostanie kwadratem, warunkiem zwycistwa bdzie nadal uoenie linii poziomej, pionowej lub ukonej z wasnych symboli. Modyfikacji musi jednak ulec algorytm sprawdzania planszy (ten straszny :D) oraz sposb numerowania i rysowania pl.
6
OBIEKTY
yka nie istnieje
Neo w filmie Matrix
Lektura kilku ostatnich rozdziaw daa ci spore pojcie o programowaniu w jzyku C++, ze szczeglnym uwzgldnieniem sposb realizacji w nim pewnych algorytmw oraz uycia takich konstrukcji jak ptle czy instrukcje warunkowe. Zapoznae si take z moliwociami, jakie oferuje ten jzyk w zakresie manipulowania bardziej zoonymi porcjami informacji. Wreszcie, miae sposobno realizacji konkretnej aplikacji - poczynajc od jej zaprojektowania, a na kodowaniu i ostatecznej kompilacji skoczywszy. Wierz, i samo programowanie byo wtedy raczej zrozumiae - chocia nie pisalimy ju wwczas trywialnego kodu. Podejrzewam jednak, e wstpne konstruowanie programu nosio dla ciebie znamiona co najmniej dziwnej czynnoci; wspominaem o tym zreszt w podsumowaniu caego naszego projektu, obiecujc pokazanie w niniejszym rozdziale znacznie przyjaniejszej, naturalniejszej i, jak sdz, przyjemniejszej techniki programowania. Przyszed czas, by speni t obietnic. Zatem nie tracc czasu, zajmijmy si tym wyczekiwanym tsknie zagadnieniem :)
Skrawek historii
Pomys programowania komputerw jest nawet starszy ni one same. Zanim bowiem powstay pierwsze maszyny zdolne do wykonywania sekwencji oblicze, istniao ju wiele teoretycznych modeli, wedle ktrych miayby funkcjonowa60.
162
Podstawy programowania
ludzi zaczo zajmowa si oprogramowywaniem tych wielkich i topornych urzdze. Bya to praca na wskro heroiczna - zwaywszy, e pisanie programw oznaczao wtedy odpowiednie dziurkowanie zwykych papierowych kart i przepuszczanie je przez wntrznoci maszyny. Najmniejszy bd zmusza do uruchamiania programu od pocztku, co zazwyczaj skutkowao trafieniem na koniec kolejki oczekujcych na moliwo skorzystania z drogocennej mocy obliczeniowej.
Fotografia 1. ENIAC - pierwsza maszyna liczca nazwana komputerem, skonstruowana w 1946 roku. By to doprawdy cud techniki - przy poborze mocy rwnym zaledwie 130 kW mg wykona a 5 tysicy oblicze na sekund (ok. milion razy mniej ni wspczesne komputery). (zdjcie pochodzi z serwisu Internetowe Muzeum Starych Programw i Komputerw)
Zwyczaj jej starannego wydzielania utrzyma si przez wiele lat, cho z czasem techniki programistyczne ulegy usprawnieniu. Kiedy koderzy (a waciwie hakerzy, bo w tych czasach gwnie maniacy zajmowali si komputerami) dostali wreszcie do dyspozycji monitory i klawiatury (prymitywne i prawie w ogle niepodobne do dzisiejszych cacek), programowanie zaczo bardziej przypomina znajom nam czynno i stao si nieco atwiejsze. Jednake okrelenie przyjazne byo jeszcze zdecydowanie przedwczesne :) Zakodowanie programu oznaczao najczciej konieczno wklepywania dugich rzdw numerkw, czyli jego kodu maszynowego. Dopiero pniej pojawiy si bardziej zrozumiae, lecz nadal niezbyt przyjazne jzyki asemblera, w ktrych liczbowe instrukcje procesora zastpiono ich sownymi odpowiednikami. Cay czas byo to jednak operowanie na bardzo niskim poziomie abstrakcji, cile zwizanym ze sprztem. Listingi byy wic mao czytelne i podobne np. do poniszego: mov int ah, 4Ch 21h
Przyznasz chyba, e odgadnicie dziaania tego kodu wymaga nielichych zdolnoci profetycznych61 ;)
Wyszy poziom
Nie dziwi wic, e kiedy tylko potencja komputerw na to pozwoli (a stao si to na pocztku lat 70.), powstay znacznie wygodniejsze w uyciu jzyki programowania wysokiego poziomu (algorytmiczne), zwane te jzykami drugiej generacji. Zawieray one, tak oczywiste dla nas, lecz wwczas nowatorskie, konstrukcje w rodzaju instrukcji warunkowych czy ptli. Nie byy te zalene od konkretnej platformy sprztowej, co czynio programy w nich napisane wielce przenonymi. Tak narodzio si programowanie strukturalne.
61 Nie robi on jednak nic szczeglnego, gdy po prostu koczy dziaanie programu :) O dziwo, te dwie linijki powinny funkcjonowa na prawie wszystkich dzisiejszych pecetach z systemami DOS lub Windows!
Obiekty
163
W tym okresie stworzone zostay znane i uywane do dzi jzyki - Pascal, C czy BASIC. Programowanie stao si atwiejsze, bardziej dostpne i popularniejsze - rwnie wrd niewielkiej jeszcze grupy uytkownikw domowych komputerw. Pocigno to za sob take rozwj oprogramowania: pojawiy si systemy operacyjne w rodzaju Unixa, DOSa czy Windows (wszystkie napisane w C), rosa te liczba przeznaczonych dla aplikacji. Chocia niekiedy pisano jeszcze drobne fragmenty kodu w asemblerze, ogromna wikszo projektw bya ju realizowana wedle zasad programowania strukturalnego. Mona w zasadzie powiedzie, e z posiadanymi umiejtnociami sytuujemy si wanie w tym punkcie historii. Wprawdzie uywamy jzyka C++, ale dotychczas korzystalimy jedynie z tych jego moliwoci, ktre byy dostpne take w C. To si oczywicie wkrtce zmieni :)
Skostniae standardy
Czasy wietnoci metod programowania strukturalnego trway zaskakujco dugo, bo a kilkanacie lat. Moe to si wydawa dziwne - szczeglnie w odniesieniu do, przywoywanego ju niejednokrotnie, wyjtkowo sztucznego projektowania kodu przy uyciu tyche metod. Jeeli dodamy do tego fakt, i ju wtedy istniaa cakiem pokana liczba jzykw trzeciej generacji, pozwalajcych na programowanie obiektowe62, sytuacja jawi si wrcz niedorzecznie. Dlaczego koderzy nie porzucili swych wysuonych i topornych instrumentw przez tak dugi okres? Winowajc jest gwnie jzyk C, ktry zdy przez ten czas urosn do rangi niemal jedynego susznego jzyka programowania. Jako e by on narzdziem, ktrego uywano nawet do pisania systemw operacyjnych, istniao mnstwo jego kompilatorw oraz ogromna liczba stworzonych w nim programw. Zmiana tak silnie zakorzenionego standardu bya w zasadzie niemoliwa, tote przez wiele lat nikt si jej nie podj.
Obiektw czar
A tu w 1983 roku duski programista Bjarne Stroustrup zaprezentowa stworzony przez siebie jzyk C++. Mia on niezaprzeczaln zalet (jzyk, nie jego twrca ;D): czy skadni C (przez co zachowywa kompatybilno z istniejcymi aplikacjami) z moliwociami programowania zorientowanego obiektowo. Fakt ten sprawi, e C++ zacz powoli wypiera swego poprzednika, zajmujc czoowe miejsce wrd uywanych jzykw programowania. Zajmuje je zreszt do dzi. Obiektowych nastpcw dorobiy si te dwa pozostae jzyki strukturalne. Pascal wyewoluowa w Object Pascala, ktry jest podstaw dla popularnego rodowiska Delphi. BASICiem natomiast zaopiekowa si Microsoft, tworzc z niego Visual Basic; dopiero jednak ostatnie wersje tego jzyka (oznaczone jako .NET) mona nazwa w peni obiektowymi.
Co dalej?
Zaraz, w takim razie programowanie obiektowe i nasz ulubiony jzyk C++ maj ju z gr dwadziecia lat - w wiecie komputerw to przecie cay eon! Czy zatem technologii tej nie czeka rychy schyek? Monaby tak przypuszcza, gdyby istniaa inna, rwnorzdna wobec OOPu technika programowania. Dotychczas jednak nikt nie wynalaz niczego takiego i nie zanosi si na to w przewidywalnej przyszoci :) Programowanie obiektowe ma si dzisiaj co najmniej
62
164
Podstawy programowania
tak samo dobrze (a nawet znacznie lepiej), jak w chwili swego powstania i trudno sobie nawet wyobrazi jego ewentualny zmierzch. Naturalnie, zawsze mona si z tym nie zgodzi :) Niektrzy przekonuj nawet, i istnieje co takiego jak jzyki czwartej generacji, zwane rwnie deklaratywnymi. Zaliczaj do nich na przykad SQL (jzyk zapyta do baz danych) czy XSL (transformacje XML). Nie da si jednak ukry faktu, e obszar zastosowa kadego z tych jzykw jest bardzo specyficzny i ograniczony. Jeeli bowiem kiedykolwiek bdzie moliwe tworzenie zwykych aplikacji przy pomocy nastpcw tyche jzykw, to lada dzie zbdni stan si take sami programici ;))
Pierwszy kontakt
Nadesza wreszcie pora, kiedy poznamy podstawowe zaoenia osawionego programowania obiektowego. By moe dowiemy si te, dlaczego jest takie wspaniae ;)
Obiektowy wiat
Z nazwy tej techniki programowania nietrudno wywnioskowa, e jej najwaniejszym pojciem jest obiekt. Tworzc obiekty i definiujc ich nowe rodzaje mona zbudowa dowolny program.
Mylc o programowaniu, znaczenie terminu obiekt nie ulega zasadniczej zmianie. Take tutaj obiektem moe by praktycznie wszystko. Rnica polega jednak na tym, i programista wystpuje wwczas w roli stwrcy, pana i wadcy wykreowanego wiata. Wprowadzajc nowe obiekty i zapewniajc wspprac midzy nimi, tworzy dziaajcy system, podporzdkowany realizacji okrelonego zadania. Zanotujmy wic pierwsze spostrzeenie: Obiekt moe reprezentowa cokolwiek. Programista wykorzystuje obiekty jako cegieki, z ktrych buduje gotowy program.
Obiekty
165
Okrelenie obiektu
Przed chwil wykazalimy, e programowanie nie jest wcale tak oderwane od rzeczywistoci, jak si powszechnie sdzi :D Faktycznie techniki obiektowe powstay wanie dlatego, eby przybliy nieco kodowanie do prawdziwego wiata. O ile jednak w odniesieniu do niego moemy swobodnie uywa do enigmatycznego stwierdzenia, e obiektem moe by wszystko, o tyle programowanie nie znosi przecie adnych niecisoci. Obiekt musi wic da si jasno zdefiniowa i w jednoznaczny sposb reprezentowa w programie. Wydawa by si mogo, i to due ograniczenie. Ale czy tak jest naprawd? Wiele wskazuje na to, e nie. Pojcie obiektu w rozumieniu programistycznym jest bowiem na tyle elastyczne, e mieci w sobie niemal wszystko, co tylko mona sobie wymarzy. Mianowicie: Obiekt skada si z opisujcych go danych oraz moe wykonywa ustalone czynnoci. Podobnie jak omwione niedawno struktury, obiekty zawieraj pola, czyli zmienne. Ich rol jest przechowywanie pewnych informacji o obiekcie - jego charakterystyki. Oczywicie, liczba i typy pl mog by swobodnie definiowane przez programist. Oprcz tego obiekt moe wykonywa na sobie pewne dziaania, a wic uruchamia zaprogramowane funkcje; nazywamy je metodami albo funkcjami skadowymi. Czyni one obiekt tworem aktywnym - nie jest on jedynie pojemnikiem na dane, lecz moe samodzielnie nimi manipulowa. Co to wszystko oznacza w praktyce? Najlepiej bdzie, jeeli przeledzimy to na przykadzie. Zamy, e chcemy mie w programie obiekt jadcego samochodu (bo moe piszemy wanie gr wycigow?). Ustalamy wic dla niego pola, ktre bd go okrelay, oraz metody, ktre bdzie mg wykonywa. Polami mog by widoczne cechy auta: jego marka czy kolor, a take te mniej rzucajce si w oczy, lecz pewnie wane dla nas: dugo, waga, aktualna prdko i maksymalna szybko. Natomiast metodami uczynimy czynnoci, jakie nasz samochd mgby wykonywa: przyspieszenie, hamowanie albo skrt. W ten oto prosty sposb stworzymy wic komputerow reprezentacj samochodu. W naszej grze moglibymy mie wiele takich aut i nic nie staoby na przeszkodzie, aby kade miao np. inny kolor czy mark. Kiedy za dla jednego z nich wywoalibymy metod skrtu czy hamowania, zmieniaby si prdko tylko tego jednego samochodu - zupenie tak, jakby kierowca poruszy kierownic lub wcisn hamulec.
W idei obiektu wida zatem przeciwiestwo programowania strukturalnego. Tam musielimy rozdziela dane programu od jego kodu, co przy wikszych projektach prowadzioby do sporego baaganu. W programowaniu obiektowym jest zgoa odwrotnie: tworzymy niewielkie czstki, bdce poczeniem informacji oraz dziaania. S one
166
Podstawy programowania
niemal namacalne, dlatego atwiej jest nam myle o nich o skadnikach programu, ktry budujemy. Zapiszmy zatem drugie spostrzeenie: Obiekty zawieraj zmienne, czyli pola, oraz mog wykonywa dla siebie ustalone funkcje, ktre zwiemy metodami.
Schemat 17. Definicja klasy oraz kilka nalecych do obiektw (jej instancji)
W programowaniu obiektowym zadaniem twrcy jest przede wszystkim zaprojektowanie modelu klas programu, zawierajcego definicj wszystkich klas wystpujcych w aplikacji. Podczas dziaania programu bd z nich tworzone obiekty, ktrych wsppraca ma zapewni realizacj celw aplikacji (przynajmniej w teorii ;D).
Obiekty
167
Zatem zamiast zajmowa si oddzielnie danymi oraz kodem, bierzemy pod uwag ich odpowiednie poczenia - obiekty, aktywne struktury. Definiujc odpowiednie klasy oraz umieszczajc w programie instrukcje kreujce obiekty tych klas, budujemy nasz program kawaek po kawaku. By moe brzmi to teraz troch tajemniczo, lecz niedugo zobaczysz, i w gruncie rzeczy jest bardzo proste. Sformuujmy na koniec ostatnie spostrzeenie: Kady obiekt naley do pewnej klasy. Definicja klasy zawiera pola, z ktrych skada si w obiekt, oraz metody, ktrymi dysponuje.
Co na to C++?
Zakoczmy na razie te nieco zbyt teoretyczne dywagacje i zajmijmy si tym, co programici lubi najbardziej, czyli kodowaniem :) Zobaczymy, jak C++ radzi sobie z ide programowania obiektowego. Na razie spojrzymy na to zagadnienie przez kilka prostych przykadw, by pniej zagbi si w nie nieco bardziej.
Definiowanie klas
Pierwszym i bardzo wanym etapem tworzenia kodu opartego na idei OOP jest, jak sobie powiedzielimy, zdefiniowanie odpowiednich klas. W C++ jest to cakiem proste. Klasy s tu de facto nowymi typami danych, podobnymi w pewnym sensie do struktur63. Dlatego te naturalnym miejscem umieszczania ich definicji s pliki nagwkowe umoliwia to atwe wykorzystanie klasy w obrbie caego programu. Spjrzmy zatem na przykadow definicj typu obiektw, ktry pary razy przewija si w tekcie: class CCar { private: float m_fMasa; COLOR m_Kolor; VECTOR2 m_vPozycja; public: VECTOR2 vPredkosc; // ------------------------------------------------------------// metody void Przyspiesz(float fIle); void Hamuj(float fIle); void Skrec(float fKat);
};
Zastosowanie tu typy danych COLOR i VECTOR2 maj charakter umowny. Powiedzmy, e COLOR w jaki sposb reprezentuje kolor, za VECTOR2 jest dwuwymiarowym wektorem (o wsprzdnych x i y). Porwnanie do struktury jest cakiem na miejscu, chocia pojawio nam si kilka nowych elementw, w tym najbardziej oczywiste zastpienie sowa kluczowego struct przez class.
63
168
Podstawy programowania
Najwaniejsze dla nas jest jednak pojawienie si deklaracji metod klasy. Maj one tutaj form prototypw funkcji, wic bd musiay by zaimplementowane gdzie indziej (jak o tym niedugo powiemy). Rwnie dobrze wszak mona wpisywa kod krtkich metod bezporednio w definicji ich klasy. Oprcz tego mamy w naszej klasie take pewne pola, ktre deklarujemy w identyczny sposb jak zmienne czy pola w strukturach. To one stanowi tre obiektw, nalecych do definiowanej klasy. Nietrudno zauway, e caa definicja jest podzielona na dwie czci poprzez etykiety private i public. By moe domylasz , c mog one znaczy; jeeli tak, to punkt dla ciebie :) A jeli nie, nic straconego - niedugo wyjanimy ich dziaanie. Chwilowo moesz je wic zignorowa.
Implementacja metod
Zdefiniowanie typu obiektowego, czyli klasy, nie jest najczciej ostatnim etapem jego okrelania. Jeeli bowiem umiecilimy we prototypy jakich metod, nieodzowne jest wpisanie ich kodu w ktrym z moduw programu. Zobaczmy zatem, jak naley to robi. Przede wszystkim naley udostpni owemu moduowi definicj klasy, co prawie zawsze oznacza konieczno doczenia zawierajcego j pliku nagwkowego. Jeli zatem nasza klasa jest zdefiniowana w pliku klasa.h, to w module kodu musimy umieci dyrektyw: #include "klasa.h" Potem moemy ju przystpi do implementacji metod. Ich kody wprowadzamy w niemal ten sam sposb, ktry stosujemy dla zwykych funkcji. Jedyna rnica tkwi bowiem w nagwkach tyche metod, na przykad: void CCar::Przyspiesz(float fIle) { // tutaj kod metody } Zamiast wic samej nazwy funkcji mamy tutaj take nazw odpowiedniej klasy, umieszczon wczeniej. Oba te miana rozdzielamy znanym ju skdind operatorem zasigu ::. Dalej nastpuje zwyczajowa lista parametrw i wreszcie zasadnicze ciao metody. Wewntrz tego bloku zamieszczamy instrukcje, skadajce si na kod danej funkcji.
Tworzenie obiektw
Posiadajc zdefiniowan i zaimplementowan klas, moemy pokusi si o stworzenie paru przynalenych jej obiektw. Istnieje przynajmniej kilka sposobw na wykonanie tej czynnoci, z ktrych najprostszy nie rni si niczym od zadeklarowania struktury i wyglda chociaby tak: CCar Samochod; Kod ten spowoduje zadeklarowanie nowej zmiennej Samochod typu CCar oraz stworzenie obiektu nalecego do tej klasy. Podkrelam to, gdy moment tworzenia obiektu nie jest wcale tak bah spraw i moe powodowa rne akcje. Powiemy sobie o tym niedugo.
Obiekty
169
Majc ju obiekt (a wic instancj klasy), jestemy w stanie operowa na wartociach jego pl oraz wywoywa przynalene jego klasie metody. Posugujemy si tu znajomym operatorem kropki (.): // przypisanie wartoci polu Samochod.vPredkosc.x = 100.0; Samochod.vPredkosc.y = 50.0; // wywoanie metody obiektu Samochod.Przyspiesz (10.0); Czy nie spotkalimy ju kiedy czego podobnego? Zdaje si, e tak. Przy okazji acuchw znakw pojawia si bowiem konstrukcja typu strTekst.length(), ktrej uylimy do pobrania dugoci napisu strTekst. Byo to nic innego jak tylko wywoanie metody length() dla obiektu strTekst! Napisy w C++ s wic obiektami, pochodzcymi od klasy std::string. Oprcz length() posiadaj zreszt wiele innych metod, uatwiajcych prac z nimi. Wikszo poznamy podczas omawiania Biblioteki Standardowej. Kod wyglda zatem cakiem logicznie i spjnie; atwo bowiem znale wszystkie instrukcje dotyczce obiektu Samochod, bo zaczynaj si one od jego nazwy. To jedna (cho moe mao znaczca) z licznych zalet programowania obiektowego, ktre poznasz wkrtce i na ktre z utsknieniem czekasz ;) *** W tym podrozdziale zaliczylimy pierwsze spotkanie z programowaniem zorientowanym obiektowo. Mamy wic ju jakie pojcie o klasach, obiektach oraz ich polach i metodach - take w odniesieniu do jzyka C++. Dalsza cz rozdziau bdzie miaa charakter systematyzacyjno-uzupeniajcy :) Wyjanimy i uporzdkujemy sobie wikszo szczegw dotyczcych definiowania klas oraz tworzenia obiektw. Informuj przeto, i absencja na tym wanym wykadzie bdzie zdecydowanie nierozsdna!
170
Podstawy programowania
Kady obiekt posiada swj wasny pakiet opisujcych go pl, ktre rezyduj w pamici operacyjnej w identyczny sposb jak pola struktur. Metody s natomiast kodem wsplnym dla caej klasy, zatem w czasie dziaania programu istnieje w pamici tylko jedna ich kopia, wywoywana w razie potrzeby na rzecz rnych obiektw. Jest to, jak sdz, do oczywiste: tworzenie odrbnych kopii tych samych przecie funkcji dla kadego nowego obiektu byoby niewtpliwie szczytem absurdu.
Definicja klasy
Jest to konieczna i czsto pierwsza czynno przy wprowadzaniu do programu nowej klasy. Jej definicja precyzuje bowiem zawarte w niej pola oraz deklaracje metod, ktrymi klasa bdzie dysponowaa. Informacje te s niezbdne, aby mc utworzy obiekt danej klasy; dlatego te umieszczamy je niemal zawsze w pliku nagwkowym - miejscu nalenym wasnym typom danych. Skadnia definicji klasy wyglda natomiast nastpujco: class nazwa_klasy
Obiekty
{
171
}; Nie wida w niej zbytnich restrykcji, gdy faktycznie jest ona cakiem swobodna. Kolejno poszczeglnych elementw (pl lub metod) nie jest cile ustalona i moe by w zasadzie dowolnie zmieniana. Najlepiej jednak zachowa w tym wzgldzie jaki porzdek, grupujc np. pola i metody w zwarte grupy. Na razie wszake trudno byoby stosowa si do tych rad, skoro nie omwilimy dokadnie wszystkich czci definicji klasy. Czym prdzej wic naprawiamy ten bd :)
A take dla unii, chocia jak wiemy, funkcjonuj one inaczej ni struktury i klasy.
172
Podstawy programowania
enum SCALE {SCL_CELSIUS = 'c', SCL_FAHRENHEIT = 'f', SCL_KELVIN = 'k'}; class CDegreesCalc { private: // temperatura w stopniach Celsjusza double m_fStopnieC; public: // ustawienie i pobranie temperatury void UstawTemperature(double fTemperatura, SCALE Skala); double PobierzTemperature(SCALE Skala); }; // ------------------------- funkcja main() ----------------------------void main() { // zapytujemy o skal, w ktrej bdzie wprowadzona warto char chSkala; std::cout << "Wybierz wejsciowa skale temperatur" << std::endl; std::cout << "(c - Celsjusza, f - Fahrenheita, k - Kelwina): "; std::cin >> chSkala; if (chSkala != 'c' && chSkala != 'f' && chSkala != 'k') return; // zapytujemy o rzeczon temperatur float fTemperatura; std::cout << "Podaj temperature: "; std::cin >> fTemperatura; // deklarujemy obiekt kalkulatora i przekazujemy do temp. CDegreesCalc Kalkulator; Kalkulator.UstawTemperature (fTemperatura, static_cast<SCALE>(chSkala)); // pokazujemy wynik - czyli temperatur we wszystkich skalach std::cout << std::endl; std::cout << "- stopnie Celsjusza: " << Kalkulator.PobierzTemperature(SCL_CELSIUS) << std::endl; std::cout << "- stopnie Fahrenheita: " << Kalkulator.PobierzTemperature(SCL_FAHRENHEIT) << std::endl; std::cout << "- kelwiny: " << Kalkulator.PobierzTemperature(SCL_KELVIN) << std::endl; // czekamy na dowolny klawisz getch();
Caa aplikacja jest prostym programem przeliczajcym midzy trzema skalami temperatur:
Obiekty
173
Jej peny kod, z implementacj metod klasy CDegreesCalc, znale mona w programach przykadowych. Nas jednak bardziej interesuje forma definicji teje klasy oraz podzia jej skadowych na prywatne oraz publiczne. Widzimy wic wyranie, i klasa posiada jedno prywatne pole - jest nim m_fStopnieC, w ktrym zapisywana jest temperatura w wewntrznie uywanej, wygodnej skali Celsjusza. Oprcz niego mamy jeszcze dwie publiczne metody - UstawTemperature() oraz PobierzTemperature(), dziki ktrym uzyskujemy dostp do naszego prywatnego pola. Jednoczenie oferuj nam jednak dodatkow funkcjonalno, jak jest dokonywanie przeliczania pomidzy wartociami wyraonymi w rnych miarach. To bardzo czsta sytuacja, gdy prywatne pole klasy obudowane jest publicznymi metodami, zapewniajcymi do dostp. Daje to wiele poytecznych moliwoci, jak choby kontrola przypisywanej polu wartoci czy tworzenie pl tylko do odczytu. Jednoczenie prywatno pola chroni je przed przypadkow, niepodan ingerencj z zewntrz. Takie zjawisko wyodrbniania pewnych fragmentw kodu nazywamy hermetyzacj. Jak wiemy, prywatne skadowe klasy nie s dostpne poza ni sam. Kiedy wic tworzymy nasz obiekt: CDegreesCalc Kalkulator; jestemy niejako skazani na korzystanie tylko z jego publicznych metod; prba odwoania si do prywatnego pola (poprzez Kalkulator.m_fStopnieC) skoczy si bowiem bdem kompilacji. Fakt ten wcale nas jednak nie ogranicza, lecz zabezpiecza przed niepowoanym dostpem do wewntrznych informacji klasy, ktre z zasady powinny by do jej wycznej dyspozycji. Do komunikacji z otoczeniem istniej za to dwie publiczne metody, i to z nich wanie bdziemy korzysta w funkcji main(). Najpierw wic wywoujemy funkcj skadow UstawTemperature(), podajc jej wpisan przez uytkownika warto oraz wybran skal65: Kalkulator.UstawTemperature (fTemperatura, static_cast<SCALE>(chSkala)); W tym momencie w ogle nie interesuj nas dziaania, ktre zostan na tych danych podjte - jest to wewntrzna sprawa klasy CDegreesCalc (podobnie zreszt jak jej pole m_fStopnieC). Wane jest, e w ich nastpstwie moemy uy drugiej metody, PobierzTemperature(), do uzyskania podanej wczeniej wartoci w wybranej przez siebie, nowej skali: std::cout << "- stopnie Celsjusza: " << Kalkulator.PobierzTemperature(SCL_CELSIUS) << std::endl; // itd. Wszystkie kwestie dotyczce szczegowych aspektw przeliczania owych wartoci s zatem szczelnie poukrywane. Kod funkcji main() jest klarowny i wolny od niepotrzebnych detali, co nie zmienia faktu, i w razie potrzeby moliwe jest zajcie si nimi. Wystarczy przecie rzuci okiem implementacje metod klasy CDegreesCalc. Zaprowadzanie porzdku poprzez ograniczanie dostpu do pewnych elementw klasy to jedna z regu, a jednoczenie zalet programowania obiektowego. Do jej praktycznej
65
Znowu stosujemy tu technik odpowiedniego dobrania wartoci typu wyliczeniowego, przez co unikamy instrukcji switch.
174
Podstawy programowania
realizacji su w C++ poznane specyfikatory private oraz public. W miar nabywania dowiadczenia w pracy z klasami bdziesz je coraz efektywniej stosowa w swoim wasnym kodzie.
Deklaracje pl
Pola s waciw treci kadego obiektu klasy, to one stanowi jego reprezentacj w pamici operacyjnej. Pod tym wzgldem nie rni si niczym od znanych ci ju pl w strukturach i s po prostu zwykymi zmiennymi, zgrupowanymi w jedn, kompleksow cao. Jako miejsce na przechowywanie wszelkiego rodzaju danych, pola maj kluczowe znaczenie dla obiektw i dlatego powinny by chronione przez niepowoanym dostpem z zewntrz. Przyjo si wic, e w zasadzie wszystkie pola w klasach deklaruje si jako prywatne; ich nazwy zwykle poprzedza si te przedrostkiem m_, aby odrni je od zmiennych lokalnych: class CFoo66 { private: int m_nJakasLiczba; std::string m_strJakisNapis; Dostp do danych zawartych w polach musi si zatem odbywa za pomoc dedykowanych metod. Rozwizanie to ma wiele rozlicznych zalet: pozwala chociaby na tworzenie pl, ktre mona jedynie odczytywa, daje sposobno wykrywania niedozwolonych wartoci (np. indeksw przekraczajcych rozmiary tablic itp.) czy te podejmowania dodatkowych akcji podczas operacji przypisywania. Rzeczone funkcje mog wyglda chociaby tak: public: int JakasLiczba() { return m_nJakasLiczba; } void JakasLiczba(int nLiczba) { m_nJakasLiczba = nLiczba; } std::string JakisNapis() { return m_strJakisNapis; }
};
Nazwaem je tu identycznie jak odpowiadajce im pola, pomijajc jedynie przedrostki67. Niektrzy stosuj nazwy w rodzaju Pobierz...()/Ustaw...() czy te z angielskiego Get...()/Set...(). Ley to cakowicie w zakresie upodoba programisty. Uycie naszych metod dostpowych moe za przedstawia si na przykad tak: CFoo Foo; Foo.JakasLiczba (10); std::cout << Foo.JakisNapis(); // przypisanie 10 do pola m_nJakasLiczba // wywietlenie pola m_strJakisNapis
Zauwamy przy okazji, e pole m_strJakisNapis moe by tutaj jedynie odczytane, gdy nie przewidzielimy metody do nadania mu jakiej wartoci. Takie postpowanie jest czsto podane, ale zaley rzecz jasna od konkretnej sytuacji, a tu jest jedynie przykadem. Wielkim mankamentem C++ jest brak wsparcia dla tzw. waciwoci (ang. properties), czyli nakadek na pola klas, imitujcych zmienne i pozwalajcych na uycie bardziej
66 foo oraz bar to takie dziwne nazwy, stosowane przez programistw najczciej w przykadowych kodach, dla bliej nieokrelonych bytw, nie majcych adnego praktycznego sensu i sucych jedynie w celach prezentacyjnych. Maj one t zalet, e nie mona ich pomyli tak atwo, jak np. litery A, B, C, D itp. 67 Sprawia to, e funkcje odpowiadajce temu samemu polu, a suce do zapisu i odczytu, s przecione.
Obiekty
175
naturalnej skadni (choby operatora =) ni dedykowane metody. Wiele kompilatorw udostpnia wic tego rodzaju funkcjonalno we wasnym zakresie w Visual C++ jest to konstrukcja __declspec(property(...)), o ktrej moesz przeczyta w MSDN. Nie dorwnuje ona jednak podobnym mechanizmom znanym z Delphi.
No moe nie cakiem adnego; istnieje pewien drobny wyjtek od tej reguy, ale jest on na tyle drobny i na tyle sproadycznie stosowany, e nie wyjaniam go bliej i odsyam tylko purystw do stosownego wyjanienia w MSDN.
176
int m_nPole; public: int Pole() const
Podstawy programowania
};
{ return m_nPole; }
Funkcja Pole() (bdca de facto obudow dla zmiennej m_nPole) bdzie tutaj susznie metod sta. Dla szczeglnie zainteresowanych polecam lektur uzupeniajc o staych metodach, znajdujc si w miejscu wiadomym :)
Konstruktory i destruktory
Przebkiwaem ju parokrotnie o procesie tworzenia obiektw, podkrelaj przy tym znaczenie tego procesu. Za chwil wyjani si, dlatego jest to takie wane Decydujc si na zastosowanie technik obiektowych w konkretnym programie musimy mie na uwadze fakt, i oznacza to zdefiniowane przynajmniej kilku klas oraz instancji tyche. Istot OOPu jest poza tym odpowiednia komunikacja midzy obiektami: wymiana danych, komunikatw, podejmowanie dziaa zmierzajcych do realizacji danego zdania, itp. Aby zapewni odpowiedni przepyw informacji, krystalizuje si mniej lub bardziej rozbudowana hierarchia obiektw, kiedy to jeden obiekt zawiera w sobie drugi, czyli jest jego wacicielem. To do naturalne: wikszo otaczajcych nas rzeczy mona przecie rozoy na czci, z ktrych si skadaj (gorzej moe by z powtrnym zoeniem ich w cao :D). Konsekwencje tego stanu rzeczy dla procesu tworzenie (i niszczenia) obiektw s raczej oczywiste: kreacja obiektu zbiorczego musi pocign za sob stworzenie jego skadnikw; podobnie jest te z jego destrukcj. Jasne, mona te kwestie zostawi kompilatorowi, ale paradoksalnie czyni to kod trudniejszym do zrozumienia, pisania i konserwacji69. C++ oferuje nam na szczcie moliwo podjcia odpowiednich dziaa zarwno podczas tworzenia obiektu, jak i jego niszczenia. Korzystamy z niej, wprowadzajc do naszej klasy dwa specjalne rodzaje metod - s to tytuowe konstruktory oraz destruktory. Konstruktor to specyficzna funkcja skadowa klasy, wywoywana zawsze podczas tworzenia nalecego do obiektu. Typowym zadaniem konstruktora jest zainicjowanie pl ich pocztkowymi wartociami, przydzielenie pamici wykorzystywanej przez obiekt czy te uzyskanie jakich kluczowych danych z zewntrz. Deklaracja konstruktora jest w C++ bardzo prosta. Metoda ta nie zwraca bowiem adnej wartoci (nawet void!), a jej nazwa odpowiada nazwie zawierajcej j klasy. Wyglda wic mniej wicej tak: class CFoo { private: // jakie przykadowe pole... float m_fPewnePole; public: // no i przysza pora na konstruktora ;-) CFoo() { m_fPewnePole = 0.0; }
69
Wbrew pozorom to racjonalna regua: im wicej jest rzeczy, ktre kompilator robi za plecami programisty, tym bardziej zagmatwany jest kod - choby nawet by krtszy.
Obiekty
};
177
Zazwyczaj te konstruktor nie przyjmuje adnych parametrw, co nie znaczy jednak, e nie moe tego czyni. Czsto s to na przykad startowe dane przypisywane do pl: class CSomeObject { private: // jaki rodzaj wsprzdnych float m_fX, m_fY; public: // konstruktory CSomeObject() CSomeObject(float fX, float fY) };
Posiadanie takiego parametryzowanego konstruktora ma pewien wpyw na sposb tworzenia obiektw, gdy musimy wtedy poda dla odpowiednie wartoci. Dokadniej wyjanimy to w nastpnym paragrafie. Warto te wiedzie, e klasa moe posiada kilka konstruktorw - tak jak na powyszym przykadzie. Dziaaj one wtedy podobnie jak funkcje przeciane; decyzja, ktry z nich faktycznie zostanie wywoany, zaley wic od instrukcji tworzcej obiekt. Z wiadomych wzgldw konstruktory czynimy zawsze metodami publicznymi. Umieszczenie ich w sekcji private daoby bowiem do dziwny efekt: taka klasa nie mogaby by normalnie instancjowana, tzn. niemoliwe byoby utworzenie z niej obiektu w zwyky sposb. OK, konstruktory maj zatem niebagateln rol, jak jest powoywania do ycia nowych obiektw. Doskonale jednak wiemy, e nic nie jest wieczne i nawet najduej dziaajcy program kiedy bdzie musia by zakoczony, a jego obiekty zniszczone. T niechlubn robot zajmuje si kolejny, wyspecjalizawany rodzaj metod - destruktory. Destruktor jest specjaln metod, przywoywan podczas niszczenia obiektu zawierajcej j klasy. W naszych przykadowych klasach destruktor nie miaby wiele do zrobienia - zgoa nic, poniewa aden z prezentowanych obiektw nie wykonywa czynnoci, po ktrych naleaoby sprzta. To si wszak niedugo zmieni, zatem poznanie destruktorw z pewnoci nie bdzie szkodliwe :) Posta destruktora jest take niezwykle prosta i w dodatku zawsze identyczna. Funkcja ta nie bierze bowiem adnych parametrw (bo i jakie miaaby bra?) i niczego nie zwraca. Jej nazw jest za nazwa zawierajcej klasy poprzedzona znakiem tyldy (~). Nazewnictwo destruktorw to jedna z niewielu rzeczy, za ktre twrcom C++ nale si tgie baty :D O co dokadnie chodzi? Otz teoretycznie znak tyldy uzyskujemy za pomoc klawisza Shift oraz tego znajdujcego si w lewym grnym rogu alfanumerycznej czci klawiatury. Problem polega na tym, e po pierwszym jego uyciu dany znak nie pojawia si na ekranie. Dzieje si tak dlatego, i dawniej za jego pomoc uzyskiwao si litery specyficzne dla pewnych jzykw, z kreseczkami - np. , czy . Fakt ten monaby zignorowa, jako e wikszo liter nie posiada swoich kreseczkowych odpowiednikw, wic wcinicie ich klawiszy po znaku tyldy powoduje pojawienie si zarwno osawionego szlaczka, jak i samej litery. Do tej grupy nie naley jednak litera C, ktr to przyjo si pisa na pocztku nazw klas. Zamiast wic danej sekwencji ~C uzyskujemy ! Jak sobie z tym radzi? Ja nawykem do dwukrotnego przyciskania klawisza tyldy, a
178
Podstawy programowania
nastpnie usuwania nadmiarowego znaku. Moliwe jest te uycie jakiej neutralnej litery w miejsce C, a nastpnie skasowanie jej. Chyba najlepsze jest jednak wciskanie klawisza tyldy, a nastpnie spacji - wprawdzie to dwa przycinicia, ale w ich wyniku otrzymujemy sam wyk. Klasa wyposaona w odpowiedni destruktor moe zatem jawi si nastpujco: class CBar { public: // konstruktor i destruktor CBar() { /* czynnoci startowe */ } // konstruktor ~CBar() { /* czynnoci koczce */ } // destruktor }; Jako e jego forma jest cile okrelona, jedna klasa moe posiada tylko jeden destruktor.
Co jeszcze?
Pola, zwyke metody oraz konstruktory i destruktory to zdecydowanie najczciej spotykane i chyba najwaniejsze elementy klas. Aczkolwiek nie jedyne; w dalszej czci tego kursu poznamy jeszcze skadowe statyczne, funkcje przeciajce operatory oraz tzw. deklaracje przyjani (naprawd jest co takiego! :D). Poznane tutaj skadniki klasy bd jednak zawsze miay najwiksze znaczenie. Mona jeszcze wspomnie, e wewntrz klasy (a take struktury i unii) moemy zdefiniowa kolejn klas! Tak definicj nazywamy wtedy zagniedon. Technika ta nie jest stosowana zbyt czsto, wic zainteresowani poczytaj o niej w MSDN :) Podobnie zreszt jest z innymi typami, okrelanymi poprzez enum czy typedef.
Implementacja metod
Definicja klasy jest zazwyczaj tylko poow sukcesu i nie stanowie wcale koca jej okrelania. Dzieje si tak przynajmniej wtedy, gdy umiecimy w niej jakie prototypy metod, bez podawania ich kodu. Uzupenieniem definicji klasy jest wwczas jej implementacja, a dokadniej owych prototypowanych funkcji skadowych. Polega ona rzecz jasna na wprowadzeniu instrukcji skadajcych si na kod tyche metod w jednym z moduw programu. Operacj t rozpoczynamy od doczenia do rzeczonego moduu pliku nagwkowego z definicj naszej klasy, np.: #include "klasa.h" Potem moemy ju zaj si kad z niezaimplementowanych metod; postpujemy tutaj bardzo podobnie, jak w przypadku zwykych, globalnych funkcji. Skadnia metody wyglda bowiem nastpujco: [typ_wartoci/void] nazwa_klasy::nazwa_metody([parametry]) [const] { instrukcje } Nowym elementem jest w niej nazwa_klasy, do ktrej naley dana funkcja. Wpisanie jej jest konieczne: po pierwsze mwi ona kompilatorowi, e ma do czynienia z metod klasy, a nie zwyczajn funkcj; po drugie za pozwala bezbdnie zidentyfikowa macierzyst klas danej metody.
Obiekty
179
Midzy nazw klasy a nazw metody widoczny jest operator zasigu ::, z ktrym ju raz mielimy przyjemno si spotka. Teraz moemy oglda go w nowej, chocia zblionej roli. Zaleca si, aby bloki metod tyczce si jednej klasy umieszcza w zwartej grupie, jeden pod drugim. Czyni to kod lepiej zorganizowanym. Dwie jeszcze nowoci mona zauway w nagwku metody. Zaznaczyem mianowicie typ_zwracanej_wartoci lub void jako jego nieobowizkow cz. Faktycznie moe ona by zbdna - ale tylko w przypadku konstruktora tudzie destruktora klasy. Dla zwykych funkcji skadowych musi ona nadal wystpowa. Ostatni rnic jest ewentualny modyfikator const, ktry, jak pamitamy, czyni metod sta. Jego obecno w tym miejscu powinna si pokrywa z wystpowaniem take w prototypie funkcji. Niezgodno w tej kwestii zostanie srodze ukarana przez kompilator :) Oczywicie wikszoci implementacji metody bdzie blok jej instrukcji, tradycyjnie zawarty midzy nawiasami klamrowymi. C ciekawego mona o nim powiedzie? Bynajmniej niewiele: nie rni si prawie wcale od analogicznych blokw globalnych funkcji. Dodatkowo jednak ma on dostp do wszystkich pl i metod swojej klasy - tak, jakby byy one jego zmiennymi albo funkcjami lokalnymi.
Wskanik this
Z poziomu metody mamy dostp do jeszcze jednej, bardzo wanej i przydatnej informacji. Chodzi tutaj o obiekt, na rzecz ktrego nasza metoda jest wywoywana; mwic cile, o odwoanie (wskanik) do niego. C to znaczy? Przypomnijmy sobie zatem ktr z przykadowych klas, prezentowanych na poprzednich stronach. Gdybymy wywoali jak jej metod, przypumy e w ten sposb: CFoo Foo; Foo.JakasMetoda(); to wewntrz bloku funkcji CFoo::JakasMetoda() moglibymy uy omawianego wskanika, by zyska peen wgld w obiekt Foo! Czasem mwi si wic, i jest to dodatkowy, specjalny parametr metody - wystpuje przecie w jej wywoaniu. w wyjtkowy wskanik, o ktrym traktuje powyszy opis, nazywa si this (to). Uywamy go zawsze wtedy, gdy potrzebujemy odwoa si do obiektu jako caoci, a nie tylko do poszczeglnych pl. Najczciej oznacza to przekazanie go do jakiej funkcji, zwykle konstruktora innego obiektu. Jako e jest to wskanik, a nie obiekt explicit, korzystanie z niego rni si nieco od postpowania z normalnymi zmiennymi obiektowymi. Wicej na ten temat powiemy sobie w dalszej czci tego rozdziau, za cakowicie wyjanimy w rozdziale 8, Wskaniki. Dla dociekliwych zawsze jednak istnieje MSDN :]
Praca z obiektami
Nawet dziesitki wymienitych klas nie stanowi jeszcze gotowego programu, a jedynie pewien rodzaj regu, wedle ktrych bdzie on realizowany. Wprowadzenie tych regu w ycie wymaga przeto stworzenia obiektw na podstawie zdefiniowanych klas. W C++ mamy dwa gwne sposoby obchodzenia si z obiektami; rni si one pod wieloma wzgldami, inne jest te zastosowanie kadego z nich. Naturaln i rozsdn kolej rzeczy bdzie wic przyjrzenie si im obu :)
180
Podstawy programowania
Zmienne obiektowe
Pierwsz strategi znamy ju bardzo dobrze, uywalimy jej bowiem niejednokrotnie nie tylko dla samych obiektw, lecz take dla wszystkich innych zmiennych. W tym trybie korzystamy z klasy dokadnie tak samo, jak ze wszystkich innych typw w C++ - czy to wbudowanych, czy te definiowanych przez nas samych (jak enumy, struktury itd.).
Czy nie przypomina nam to czego? Ale oczywicie - identycznie postpowalimy z acuchami znakw (czyli obiektami klasy std::string), tworzc je chociaby tak: #include <string> std::string strBuffer("Jakie te obiekty s proste! ;-)"); Widzimy wic, e znany nam i lubiany typ std::string wyjtkowo podpada pod zasady programowania obiektowego :)
onglerka obiektami
Zadeklarowane przed chwil zmienne obiektowe s w istocie takimi samymi zmiennymi, jak wszystkie inne w programach C++. Moliwe jest zatem przeprowadzanie na operacji, ktrym podlegaj na przykad liczby cakowite, napisy czy tablice.
Obiekty
181
Nie mam tu wcale na myli jakich zoonych manipulacji, wymagajcych skomplikowanych algorytmw, lecz cakiem zwyczajnych i codziennych, jak przypisanie czy przekazywanie do funkcji. Czy mona powiedzie cokolwiek ciekawego o tak trywialnych czynnociach? Okazuje si, e tak. Zwrcimy wprawdzie uwag na do oczywiste fakty z nimi zwizane, lecz znajomo owych banaw okae si pniej niezwykle przydatna. Przy okazji bdzie to dobra okazja to powtrzenia nabytej wiedzy, a tego przecie nigdy do :D Na uytek dalszych wyjanie zdefiniujemy sobie tak oto klas lampy: class CLamp { private: COLOR m_Kolor; bool m_bWlaczona; public: // konstruktory CLamp() CLamp(COLOR Kolor)
// kolor lampy // czy lampa wieci si? { m_Kolor = COLOR_WHITE; } { m_Kolor = Kolor; }
// ------------------------------------------------------------// metody void Wlacz() void Wylacz() { m_bWlaczona = true; } { m_bWlaczona = false; }
// ------------------------------------------------------------// metody dostpowe do pl COLOR Kolor() const { return m_Kolor; } bool Wlaczona() const { return m_bWlaczona; }
};
Klasa ta jest znakomit syntez wszystkich wiadomoci przekazanych w tym podrozdziale. Jeeli wic nie rozumiesz do koca znaczenia ktrego z jej elementw, powiniene powrci do powiconemu mu miejsca w tekcie. Natychmiast te zadeklarujemy i stworzymy dwa obiekty nalece do naszej klasy: CLamp Lampa1(COLOR_RED), Lampa2(COLOR_GREEN); Tym sposobem mamy wic lampy, sztuk dwie, w kolorze czerwonym oraz zielonym. Moglibymy uy ich metod, aby je obie wczy; zrobimy jednak co dziwniejszego przypiszemy jedn lamp do drugiej: Lampa1 = Lampa2; A co to za dziwado?, susznie pomylisz. Taka operacja jest jednak cakowicie poprawna i daje do ciekawe rezultaty. By j dobrze zrozumie musimy pamita, e Lampa1 oraz Lampa2 s to przede wszystkim zmienne, zmienne ktre przechowuj pewne wartoci. Fakt, e tymi wartociami s obiekty, ktre w dodatku interpretujemy w sposb prawie realny, nie ma tutaj wikszego znaczenia. Pomylmy zatem, jaki efekt spowodowaby ten kod, gdybymy zamiast klasy CLamp uyli jakiego zwykego, skalaranego typu? int nLiczba1 = 10, nLiczba2 = 20; nLiczba1 = nLiczba2;
182
Podstawy programowania
Dawna warto zmiennej, do ktrej nastpio przypisanie, zostaaby zapomniana i obie zmienne zawierayby t sam liczb. Dla obiektw rzecz ma si identycznie: po wykonaniu przypisania zarwno Lampa1, jak i Lampa2 reprezentowa bd obiekty zielonych lamp. Czerwona lampa, pierwotnie zawarta w zmiennej Lampa1, zostanie zniszczona70, a w jej miejsce pojawi si kopia zawartoci zmiennej Lampa2. Nie bez powodu zaakcentowaem wyej sowo kopia. Obydwa obiekty s bowiem od siebie cakowicie niezalene. Jeeli wczylibymy jeden z nich: Lampa1.Wlacz(); drugi nie zmieniby si wcale i nie obdarzy nas swym wasnym wiatem. Moemy wic podsumowa nasz wywd krtk uwag na temat zmiennych obiektowych: Zmienne obiektowe przechowuje obiekty w ten sam sposb, w jaki czyni to zwyke zmienne ze swoimi wartociami. Identycznie odbywa si te przypisywanie71 takich zmiennych - tworzone s wtedy odpowiednie kopie obiektw. Wspominaem, e wszystko to moe wydawa si naturalne, oczywiste i niepodwaalne. Konieczne byo jednak dokadne wyjanienie w tym miejscu tych z pozoru prostych zjawisk, gdy drugi sposb postpowania z obiektami (ktry poznamy za moment) wprowadza w tej materii istotne zmiany.
Dostp do skadnikw
Kontrolowanie obiektu jako caoci ma rozliczne zastosowania, ale jednak znacznie czciej bdziemy uywa tylko jego pojedynczych skadnikw, czyli pl lub metod. Doskonale wiemy ju, jak si to robi: z pomoc przychodzi nam zawsze operator wyuskania - kropka (.). Stawiamy wic go po nazwie obiektu, by potem wpisa nazw wybranego elementu, do ktrego chcemy si odwoa. Pamitajmy, e posiadamy wtedy dostp jedynie do skadowych publicznych klasy, do ktrej naley obiekt. Dalsze postpowanie zaley ju od tego, czy nasz uwag zwrcilimy na pole, czy na metod. W tym pierwszym, rzadszym przypadku nie odczujemy adnej rnicy w stosunku do pl w strukturach - i nic dziwnego, gdy nie ma tu rzeczywicie najmniejszej rozbienoci :) Wywoanie metody jest natomiast udzco zblione do uruchomienia zwyczajnej funkcji - tyle e w gr wchodz tutaj nie tylko jej parametry, ale take obiekt, na rzecz ktrego dan metod wywoujemy. Jak wiemy, jest on potem dostpny wewntrz metody poprzez wskanik this.
Niszczenie obiektw
Kady stworzony obiekt musi prdzej czy poniej zosta zniszczony, aby mc odzyska zajmowan przez niego pami i spokojnie zakoczy program. Dotyczy to take zmiennych obiektowych, lecz dzieje si to troch jakby za plecami programisty.
W penym znaczeniu tego sowa - z wywoaniem destruktora i pniejszym zwolnieniem pamici. To samo mona zreszt powiedzie o wszystkich operacjach podobnych do przypisania, tj. inicjalizacji oraz przekazywaniu do funkcji.
71
70
Obiekty
183
Zauwamy bowiem, i w adnym z naszych dotychczasowych programw, wykorzystujcych techniki obiektowe, nie pojawiy si instrukcje, ktre jawnie odpowiadayby za niszczenie stworzonych obiektw. Nie oznacza to bynajmniej, e zalegaj one w pamici operacyjnej72, zajmujc j niepotrzebnie. Po prostu kompilator sam dba o to, by ich destrukcja nastpia w stosownej chwili. A zatem kiedy jest ona faktycznie dokonywana? Nietrudno jest obmyli odpowied na to pytanie, jeeli przypomnimy sobie pojcie zasigu zmiennej. Powiedzielimy sobie ongi, i jest to taki obszar kodu programu, w ktrym dana zmienna jest dostpna. Dostpna to znaczy zadeklarowana, z przydzielon dla siebie pamici, a w przypadku zmiennej obiektowej - posiadajca rwnie obiekt stworzony poprzez konstruktor klasy. Moment opuszczenia zasigu zmiennej przez punkt wykonania programu jest wic kresem jej istnienia. Jeli nieszczsna zmienna bya obiektow, do akcji wkracza destruktor klasy (jeeli zosta okrelony), sprztajc ewentualny baagan po obiekcie i niszczc go. Dalej nastpuje ju tylko zwolnienie pamici zajmowanej przez zmienn i jej kariera koczy si w niebycie :) Zapamitajmy wic, e: Wyjcie programu poza zasig zmiennej obiektowej niszczy zawarty w niej obiekt.
Podsumowanie
Prezentowane tu wasnoci zmiennych obiektowych by moe wygldaj na nieznane i niespotkane wczeniej. Naprawd jednak nie s niczym szczeglnym, gdy spotykalimy si z nimi od samego pocztku nauki programowania - w wikszoci (z wyczeniem wyuskiwania skadnikw) dotycz one bowiem wszystkich zmiennych! Teraz wszake omwilimy je sobie nieco dokadniej, koncentrujc si przede wszystkim na yciu obiektw - chwilach ich tworzenia i niszczenia oraz operacjach na nich. Majc ugruntowan t widz, bdzie nam atwiej zmierzy si z drugim sposobem stosowania obiektw, ktry jest przedstawiony w nastpnym paragrafie.
Wskaniki na obiekty
Przyznam szczerze: miaem pewne wtpliwoci, czy suszne jest zajmowanie si wskanikami na obiekty ju w tej chwili, bez dogebnego przedstawienia samych wskanikw. T naruszon przeze mnie kolejno zachowaaby pewnie wikszo autorw kursw czy ksiek o C++. Ja jednak postawiem sobie za cel nauczenie czytelnika programowania w jzyku C++ (i to w konkretnym celu!), nie za samego jzyka C++. Narzuca to nieco inny porzdek treci, skoncentrowany w pierwszej kolejnoci na najpotrzebniejszych zagadnieniach praktycznych, a dopiero potem na pozostaych moliwociach jzyka. Do tych kwestii pierwszej potrzeby niewtpliwie naley zaliczy ide programowania obiektowego, wskaniki spychajc tym samym na nieco dalszy plan. Jednoczenie jednak nie mog przy okazji OOPu pomin milczeniem tematu wskanikw na obiekty, ktre s praktycznie niezbdne do poprawnego konstruowania aplikacji z wykorzystaniem klas. Dlatego te pojawia si on wanie teraz; mimo wszystko ufam, e zrozumienie go nie bdzie dla ciebie wielkim kopotem. Po tak zachcajcym wstpie nie bd zdziwiony, jeeli w tej chwili dua cz czytelnikw zakoczy lektur ;-) Skrycie wierz jednak, e ambitnym kandydatom na programistw gier adne wskaniki nie bd straszne, a ju na pewno nie przelkn si ich obiektowych odmian. Nie bedziemy zatem traci wicej czasu oraz miejsca i natychmiast przystpimy do dziea.
72 Zjawisko to nazywamy wyciekiem pamici i jest ono wysoce niepodane, za interesowa nas bdzie bardziej w rozdziale traktujcym o wskanikach.
184
Podstawy programowania
Obiekty
CLamp* pLampa1 = new CLamp; Przypominam, i w ten sposb powoalimy do ycia obiekt, ktry zosta umieszczony gdzie w pamici, a wskanik pLampa1 jest tylko odwoaniem do niego. Dalszej czci nietrudno si domyle. Wprowadzamy sobie zatem drugi wskanik i przypisujemy do ten pierwszy, o tak: CLamp* pLampa2 = pLampa1; Mamy teraz dwa takie same wskaniki Czy to znaczy, i posiadamy take par identycznych obiektw?
185
Ot nie! Nasza lampa nadal egzystuje samotnie, bowiem skopiowalimy jedynie samo odwoanie do niej. Obecnie uycie zarwno wskanika pLampa1, jak i pLampa2 bdzie uzyskaniem dostpu do jednego i tego samego obiektu. To znaczca modyfikacja w stosunku do zmiennych obiektowych. Tam kada reprezentowaa i przechowywaa swj wasny obiekt, a instrukcje przypisywania midzy nimi powodoway wykonywanie kopii owych obiektw. Tutaj natomiast mamy tylko jeden obiekt, za to wiele drg dostpu do niego, czyli wskanikw. Przypisywanie midzy nimi dubluje jedynie te drogi, za sam obiekt pozostaje niewzruszony. Podsumowujc: Wskanik na obiekt jest jedynie odwoaniem do niego. Wykonanie przypisania do wskanika moe wic co najwyej skopiowa owo odwoanie, pozostawiajc docelowy obiekt cakowicie niezmienionym. Mwic obrazowo, uzyskiwanie dodatkowego wskanika do obiektu jest jak wyrobienie sobie dodatkowego klucza do tego samego zamka. Chobymy mieli ich cay brelok, wszystkie bd otwieray tylko jedne i te same drzwi.
Dostp do skadnikw
Cay czas napomykam, e wskanik jest pewnego rodzaju czem do obiektu. Wypadaoby wic wresznie poczy si z tym obiektem, czyli uzyska dostp do jego skadnikw. Operacja ta nie jest zbytnio skomplikowana, gdy by j wykona posuymy si znan ju koncepcj operatora wyuskania. W przypadku wskanikw nie jest nim jednak kropka, ale strzaka (->). Otrzymujemy j, wpisujc kolejno dwa znaki: mylnika oraz symbolu wikszoci. Aby zatem wczy nasz lamp, wystarczy wywoa jej odpowiedni metod przy pomocy ktrego z dwch wskanikw oraz poznanego wanie operatora:
186
pLampa1->Wlacz();
Podstawy programowania
Moemy take sprawdzi, czy drugi wskanik istotnie odwouje si do tego samego obiektu co pierwszy. Wystarczy wywoa za jego pomoc metod Wlaczona(): pLampa2->Wlaczona(); Nie bdzie niespodziank fakt, i zwrci ona warto true. Zbierzmy wic w jednym miejscu informacje na temat obu operatorw wyuskania: Operator kropki (.) pozwala uzyska dostp do skadnikw obiektu zawartego w zmiennej obiektowej. Operator strzaki (->) wykonuje analogiczn operacj dla wskanika na obiekt. Jak najlepiej zapamita i rozrnia te dwa operatory? Proponuj prosty sposb: pamitamy, e zmienna obiektowa przechowuje obiekt jako swoj warto. Mamy go wic dosownie na wycignicie rki i nie potrzebujemy zbytnio si wysila, aby uzyska dostp do jego skadnikw. Sucy temu celowi operator moe wic by bardzo may, tak may jak punkt :) kiedy za uywamy wskanika na obiekt, wtedy nasz byt jest daleko std. Potrzebujemy wwczas odpowiednio duszego, dwuznakowego operatora, ktry dodatkowo wskae nam (strzaka!) waciw drog do poszukiwanego obiektu. Takie wyjanienie powinno by w miar pomocne w przyswojeniu sobie znaczenia oraz zastosowania obu operatorw.
Niszczenie obiektw
Wszelkie obiekty kiedy naley zniszczy; czynno ta, oprcz wyrabiania dobrego nawyku sprztania po sobie, zwalnia pami operacyjn, ktre te obiekty zajmoway. Po zniszczeniu wszystkich moliwe jest bezpieczne zakoczenie programu. Podobnie jak tworzenie, tak i niszczenie obiektw dostpnych poprzez wskaniki nie jest wykonywane automatycznie. Wymagana jest do tego odrbna instrukcja - na szczcie nie wyglda ona na wielce skomplikowan i przedstawia si nastpujco: delete pFoo; // pFoo musi tu by wskanikiem na istniejcy obiekt
delete (usu, podobnie jak new jest uwaane za operator) dokonuje wszystkich niezbdnych czynnoci potrzebnych do zniszczenia obiektu reprezentowanego przez wskanik. Wywouje wic jego destruktor, a nastpnie zwalnia pami zajt przez obiekt, ktry koczy wtedy definitywnie swoje istnienie. To tyle jeli chodzi o yciorys obiektu. Co si jednak dzieje z samym wskanikiem? Ot nadal wskazuje on na miejsce w pamici, w ktrym jeszcze niedawno egzystowa nasz obiekt. Teraz jednak ju go tam nie ma; wszelkie prby odwoania si do tego obszaru skocz si wic bedem, zwanym naruszeniem zasad dostpu (ang. access violation). Pamitajmy zatem, i: Nie naley prbowa uzyska dostpu do zniszczonego (lub niestworzonego) obiektu poprzez wskanik na niego. Spowoduje to bowiem bd wykonania programu i jego awaryjne zakoczenie. Musimy by take wiadomi, e w momencie usuwania obiektu traci wano nie tylko ten wskanik, ktrego uylimy do dokonania aktu zniszczenia, ale te wszystkie inne
Obiekty
wskaniki odnoszce si do tego obiektu! To zreszt naturalne, skoro co do jednego wskazuj one na t sam, nieaktualn ju lokacj w pamici.
187
Tak to wyglda w teorii, ale poniewa jeden przykad wart jest tysica sw, najlepiej bdzie, jeeli przyjrzysz si takowemu przykadowi. Przypumy wic, e jestemy w trakcie pisania gry podobnej do sawnego Lode Runnera: naley w niej zebra wszystkie przedmioty znajdujce si na planszy (zazwyczaj s to monety albo inne bogactwa), aby awansowa do kolejnego etapu. Jakie obiekty i jakie zalenoci naleaoby w tym przypadku stworzy? Najlepiej zacz od tego najwikszego i najwaniejszego, grupujcego wszystkie inne na przykad samego etapu. Podrzdnym w stosunku do niego bdzie obiekt gracza oraz, rzecz jasna, pewna ilo obiektw monet (zapewne umieszczonych w tablicy albo innym tego rodzaju pojemniku). Do tego dodamy pewnie jeszcze kilku wrogw; ostatecznie nasz prosty model przedstawia si bdzie nastpujco:
188
Podstawy programowania
Dziki temu, e obiekt etapu posiad dostp (naturalnie poprzez wskanik) do obiektw gracza czy te wrogw, moe chociaby uaktualnia ich pozycj na ekranie w odpowiedzi na wciskanie klawiszy na klawiaturze lub upyw czasu. Odpowiednie rozkazy bdzie zapewne otrzymywa z gry, tj. od obiektu nadrzdnego wobec niego najprawdopodobniej jest to gwny obiekt gry. W podobny sposb, o wiele naturalniejszy ni w programowaniu strukturalnym, projektujemy model obiektowy kadego w zasadzie programu. Nie musimy ju rozdziela swoich koncepcji na dane i kod, wystarczy e stworzymy odpowiednie klasy oraz obiekty i zapewnimy powizania midzy nimi. Rzecz jasna, z wykorzystaniem wskanikw na obiekty :)
Podsumowanie
Koczcy si rozdzia by nieco krtszy ni par poprzednich. Podejrzewam jednak, e przebrnicie przez niego zajo ci moe nawet wicej czasu i byo o wiele trudniejsze. Wszystko dlatego e poznawalimy tutaj zupenie now koncepcj programowania, ktra wprawdzie ideowo jest o wiele blisza czowiekowi ni techniki strukturalne, ale w zamian wymaga od razu przyswojenia sobie sporej porcji nowych wiadomoci i poj. Nie martw si zatem, jeli nie byy one dla ciebie cakiem jasne; zawsze przecie moesz wrci do trudniejszych fragmentw tekstu w przyszoci (ponowne przeczytanie caego rozdziau jest naturalnie rwnie dopuszczalne :D). Nasze spotkanie z programowaniem obiektowym bdziemy zreszt kontynuowali w nastpnym rozdziale, w ktrym to ostatecznie wyjani si, dlaczego jest ono takie wspaniae ;)
Pytania i zadania
Nowopoznane, arcywane zagadnienie wymaga oczywicie odpowiedniego powtrzenia. Nie krpuj si wic i odpowiedz na ponisze pytania :)
Pytania
1. Czym s obiekty i jaka jest ich rola w programowaniu z uyciem technik OOP? 2. Jakie etapy obejmuje wprowadzenie do programu nowej klasy?
Obiekty
3. Jakie skadniki moemy umieci w definicji klasy? 4. (Trudne) Ktre skadowe klasa posiada zawsze, niezalenie od tego czy je zdefiniujemy, czy nie? 5. W jaki sposb moemy z wntrza metody uzyska dostp do obiektu, na rzecz ktrego zostaa ona wywoana? 6. Czym rni si uycie wskanika na obiekt od zmiennej obiektowej? 7. Jak odrbne obiekty w programie mog wiedzie o sobie nawzajem i przekazywa midzy sob informacje?
189
wiczenia
1. Zdefiniuj prost klas reprezentujc ksik. 2. Napisz program podobny do przykadu DegreesCalc, ale przeliczajcy midzy jednostkami informacji (bajtami, kilobajtami itd.).
7
PROGRAMOWANIE OBIEKTOWE
Gdyby murarze budowali domy tak, jak programici pisz programy, to jeden dzicio zniszczyby ca cywilizacj.
ze zbioru prawd o oprogramowaniu
Witam ci serdecznie, drogi Czytelniku! Powitanie to jest tutaj jak najbardziej wskazane. Twoja obecno wskazuje bowiem, e nadzwyczaj szybko wydostae si spod sterty nowych wiadomoci, ktrymi obarczyem ci w poprzednim rozdziale :) A nie byo to wcale takie proste, zwaywszy e poznae tam zupenie now technik programowania, opierajc si na cakiem innych zasadach ni te dotychczas ci znane. Mimo to moge uczu pewien niedosyt. Owszem, idea OOPu bya tam przedstawiona jako w miar naturalna, a nawet intuicyjna (w kadym razie bardziej ni programowanie strukturalne). Potrzeba jednak sporej dozy optymizmu, aby uzna j na tym etapie za co rewolucyjnego, co faktycznie zmienia sposb mylenia o programowaniu (a jednoczenie znacznie je uatwia). By w peni przekona si do tej koncepcji, trzeba o niej wiedzie nieco wicej; kluczowe informacje na ten temat s zawarte w tym oto rozdziale. Sdz wic, e choby z tego powodu bdzie on dla ciebie bardzo interesujcy :D Zajmiemy si w nim dwoma niezwykle wanymi zagadnieniami programowania obiektowego: dziedziczeniem oraz metodami wirtualnymi. Na nich wanie opiera si caa jego potga, pozwalajca tworzy efektowne i efektywne programy. Zobaczymy zreszt, jak owo tworzenie wyglda w rzeczywistoci. Kocow cz rozdziau powiciem bowiem na zestaw rad i wskazwek, ktre, jak sdz, oka si pomocne w projektowaniu aplikacji opartych na modelu OOP. Kontynuujmy zatem poznawanie wspaniaego wiata programowania obiektowego :)
Dziedziczenie
Drugim powodem, dla ktrego techniki obiektowe zyskay tak popularno73, jest znaczcy postp w kwestii ponownego wykorzystywania raz napisanego kodu oraz rozszerzania i dostosywania go do wasnych potrzeb. Cecha ta ley u samych podstaw OOPu: program konstruowany jako zbir wspdziaajcych obiektw nie jest ju bowiem monolitem, cisym poczeniem danych i wykonywanych na operacji. Rozdrobniona struktura zapewnia mu zatem modularno: nie jest trudno doda do gotowej aplikacji now funkcj czy te
73
Pierwszym jest wspominana nie raz naturalno programowania, bez koniecznoci podziau na dane i kod.
192
Podstawy programowania
wyodrbni z niej jeden podsystem i uy go w kolejnej produkcji. Uatwia to i przyspiesza realizacj kolejnych projektw. Wszystko zaley jednak od umiejtnoci i dowiadczenia programisty. Nawet stosujc techniki obiektowe mona stworzy program, ktrego elementy bd ze sob tak cile zespolone, e prba ich uycia w nastpnej aplikacji bdzie przypominaa wciskanie sonia do szklanej butelki. Istnieje jeszcze jedna przyczyna, dla ktrej kod oparty na programowaniu obiektowym atwiej poddaje si recyklingowi, majcemu przygotowa go do ponownego uycia. Jest nim wanie tytuowy mechanizm dziedziczenia. Korzyci pynce z jego stosowania nie ograniczaj si jednake tylko do wtrnego przerobu ju istniejcego kodu. Przeciwnie, jest to fundamentalny aspekt OOPu niezmiernie uatwiajcy i uprzyjemniajcy projektowanie kadej w zasadzie aplikacji. W poczeniu z technologi funkcji wirtualnych oraz polimorfizmu daje on niezwykle szerokie moliwoci, o ktrych szczegowo traktuje praktycznie cay niniejszy rozdzia. Rozpoczniemy zatem od dokadnego opisu tego bardzo poytecznego mechanizmu programistycznego.
Programowanie obiektowe
193
Gdyby by obiektem w programie, wtedy musiaby nalee a do trzech klas naraz74! Byoby to oczywicie niemoliwe, jeeli wszystkie miayby by wobec siebie rwnorzdne. Tutaj jednak tak nie jest: wystpuje midzy nimi hierarchia, jedna klasa pochodzi od drugiej. Zjawisko to nazywamy wanie dziedziczeniem. Dziedziczenie (ang. inheritance) to tworzenie nowej klasy na podstawie jednej lub kilku istniejcych wczeniej klas bazowych. Wszystkie klasy, ktre powstaj w ten sposb (nazywamy je pochodnymi), posiadaj pewne elementy wsplne. Czci te s dziedziczone z klas bazowych, gdy tam wanie zostay zdefiniowane. Ich zbir moe jednak zosta poszerzony o pola i metody specyficzne dla klas pochodnych. Bd one wtedy wspistnie z dorobkiem pochodzcym od klas bazowych, ale mog oferowa dodatkow funkcjonalno. Tak w teorii wyglda system dziedziczenia w programowaniu obiektowym. Najlepiej bdzie, jeeli teraz przyjrzymy si, jak w praktyce moe wyglda jego zastosowanie.
74
A raczej do siedmiu lub omiu, gdy dla prostoty pominem tu wikszo poziomw systematyki.
194
Podstawy programowania
Wszystkie przedstawione na nim klasy wywodz si z jednej, nadrzdnej wobec wszystkich: jest ni naturalnie klasa Zwierz. Dziedziczy z niej kada z pozostaych klas bezporednio, jak Ryba, Ssak oraz Ptak, lub porednio - jak Pies domowy. Tak oto tworzy si kilkupoziomowa klasyfikacja oparta na mechanizmie dziedziczenia.
75
Programowanie obiektowe
195
Wykazuje poza tym pewn budow wewntrzn: niektre jej pola i metody moemy bowiem okreli jako wasne i unikalne, za inne s odziedziczone po klasie bazowej i mog by wsplne dla wielu klas. Nie sprawia to jednak adnej rnicy w korzystaniu z nich: funkcjonuj one identycznie, jakby byy zawarte bezporednio wewntrz klasy.
Dziedziczenie w C++
Pozyskawszy oglne informacje o dziedziczeniu jako takim, moemy zobaczy, jak idea ta zostaa przeoona na nasz nieoceniony jzyk C++ :) Dowiemy si wic, w jaki sposb definiujemy nowe klasy w oparciu o ju istniejce oraz jakie dodatkowe efekty s z tym zwizane.
Podstawy
Mechanizm dziedziczenia jest w C++ bardzo rozbudowany, o wiele bardziej ni w wikszoci pozostalych jzykw zorientowanych obiektowo76. Udostpnia on kilka szczeglnych moliwoci, ktre by moe nie s zawsze niezbdne, ale pozwalaj na du swobod w definiowaniu hierarchii klas. Poznanie ich wszystkich nie jest konieczne, aby sprawnie korzysta z dobrodziejstw programowania obiektowego, jednak wiemy doskonale, e wiedza jeszcze nikomu nie zaszkodzia :D Zaczniemy oczywicie od najbardziej elementarnych zasad dziedziczenia klas oraz przyjrzymy si przykadom ilustrujcym ich wykorzystanie.
196
[deklaracje_publiczne] };
Podstawy programowania
Nieprzypadkowo pojawi si tu nowy specyfikator, protected. Jego wprowadzenie zwizane jest cile z pojciem dziedziczenia. Pojcie to wpywa zreszt na dwa pozostae rodzaje praw dostpu do skadowych klasy. Zbierzmy wic je wszystkie w jednym miejscu, wyjaniajc definitywnie znaczenie kadej z etykiet: private: poprzedza deklaracje skadowych, ktre maj by dostpne jedynie dla metod definiowanej klasy. Oznacza to, i nie mona si do nich dosta, uywajc obiektu lub wskanika na niego oraz operatorw wyuskania . lub ->. Ta wyczno znaczy rwnie, e prywatne skadowe nie s dziedziczone i nie ma do nich dostpu w klasach pochodnych, gdy nie wchodz w ich skad. specyfikator protected (chronione) take nie pozwala, by uytkownicy obiektw naszej klasy grzebali w opatrzonych nimi polach i metodach. Jak sama nazwa wskazuje, s one chronione przed takim dostpem z zewntrz. Jednak w przeciwiestwie do deklaracji private, skadowe zaznaczone przez protected s dziedziczone i wystpuj w klasach pochodnych, bdc dostpnymi dla ich wasnych metod. Pamitajmy zatem, e zarwno private, jak i protected nie pozwala, aby oznaczone nimi skadowe klasy byy dostpne na zewntrz. Ten drugi specyfikator zezwala jednak na dziedziczenie pl i metod. public jest najbardziej liberalnym specyfikatorem. Nie tylko pozwala na odziedziczanie swych skadowych, ale take na udostpnianie ich szerokiej rzeszy obiektw poprzez operatory wyuskania. Powysze opisy brzmi moe nieco sucho i niestrawnie, dlatego przyjrzymy si jakiemu przykadowi, ktry bdzie bardziej przemawia do wyobrani. Mamy wic tak oto klas prostokta: class CRectangle { private: // wymiary prostokta float m_fSzerokosc, m_fWysokosc; protected: // pozycja na ekranie float m_fX, m_fY; public: // konstruktor CRectangle() { m_fX = m_fY = 0.0; m_fSzerokosc = m_fWysokosc = 10.0; } // ------------------------------------------------------------// metody float Pole() const float Obwod() const { return m_fSzerokosc * m_fWysokosc; } { return 2 * (m_fSzerokosc+m_fWysokosc); }
};
Opisuj go cztery liczby, wyznaczajce jego pozycj oraz wymiary. Wsprzdne X oraz Y uczyniem tutaj polami chronionymi, za szeroko oraz wysoko - prywatnymi. Dlaczego wanie tak? Ot powysza klasa bdzie rwnie baz dla nastpnej. Pamitamy z geometrii, e szczeglnym rodzajem prostokta jest kwadrat. Ma on wszystkie boki o tej samej dugoci, zatem nielogiczne jest stosowa do nich pojcia szerokoci i wysokoci.
Programowanie obiektowe
197
Wielko kwadratu okrela bowiem tylko jedna liczba, wic defincja odpowiadajcej mu klasy moe wyglda nastpujco: class CSquare : public CRectangle // dziedziczenie z CRectangle { private: // zamiast szerokoci i wysokoci mamy tylko dugo boku float m_fDlugoscBoku; // pola m_fX i m_fY s dziedziczone z klasy bazowej, wic nie ma // potrzeby ich powtrnego deklarowania public: // konstruktor CSquare { m_fDlugoscBoku = 10.0; } // ------------------------------------------------------------// nowe metody float Pole() const { return m_fDlugoscBoku * m_fDlugoscBoku; } float Obwod() const { return 4 * m_fDlugoscBoku; }
};
Dziedziczy ona z CRectangle, co zostao zaznaczone w pierwszej linijce, ale posta tej frazy chwilowo nas nie interesuje :) Skoncentrujmy si raczej na konsekwencjach owego dziedziczenia. Porozmawiajmy najpierw o nieobecnych. Pola m_fSzerokosc oraz m_fWysokosc byy w klasie bazowej oznaczone jako prywatne, zatem ich zasig ogranicza si jedynie do tej klasy. W pochodnej CSquare nie ma ju po nich ladu; zamiast tego pojawia si bardziej naturalne pole m_fDlugoscBoku z sensown dla kwadratu wielkoci. Zwizane s z ni take dwie nowe-stare metody, zastpujce te z CRectangle. Do obliczania pola i obwodu wykorzystujemy bowiem sam dugo boku kwadratu, nie za jego szerokoc i wysoko, ktrych w klasie w ogle nie ma. W definicji CSquare nie ma take deklaracji m_fX oraz m_fY. Nie znaczy to jednak, e klasa tych pl nie posiada, gdy zostay one po prostu odziedziczone z bazowej CRectangle. Stao si tak oczywicie za spraw specyfikatora protected. Co wic powinnimy o nim pamita? Ot: Naley uywa specyfikatora protected, kiedy chcemy uchroni skadowe przed dostpem z zewntrz, ale jednoczenie mie je do dyspozycji w klasach pochodnych.
198
Podstawy programowania
To w niej wanie podajemy klasy bazowe, z ktrych chcemy dziedziczy. Czynimy to, wpisujc dwukropek po nazwie definiowanej wanie klasy i podajc dalej list jej klas bazowych, oddzielonych przecinkami. Zwykle nie bdzie ona zbyt duga, gdy w wikszoci przypadkw wystarczajce jest pojedyncze dziedziczenie, zakadajce tylko jedn klas bazow. Istotne s natomiast kolejne specyfikatory, ktre opcjonalnie moemy umieci przed kad nazw_klasy_bazowej. Wpywaj one na proces dziedziczenia, a dokadniej na prawa dostpu, na jakich klasa pochodna otrzymuje skadowe klasy bazowej. Kiedy za mowa o tyche prawach, natychmiast przypominamy sobie o swkach private, protected i public, nieprawda? ;) Rzeczywicie, specyfikatory dziedziczenia wystpuj zasadniczo w liczbie trzech sztuk i s identyczne z tymi wystpujcymi wewntrz bloku klasy. O ile jednak tamte pojawiaj si w prawie kadej sytuacji i klasie, o tyle tutaj specyfikator public ma niemal cakowity monopol, a uycie pozostaych dwch naley do niezmiernie rzadkich wyjtkw. Dlaczego tak jest? Ot w 99.9% przypadkw nie ma najmniejszej potrzeby zmiany praw dostpu do skadowych odziedziczonych po klasie bazowej. Jeeli wic ktre z nich zostay tam zadeklarowane jako protected, a inne jako public, to prawie zawsze yczymy sobie, aby w klasie pochodnej zachoway te same prawa. Zastosowanie dziedziczenia public czyni zado tym daniom, dlatego wanie jest ono tak czsto stosowane. O pozostaych dwch specyfikatorach moesz przeczyta w MSDN. Generalnie ich dziaanie nie jest specjalnie skomplikowane, gdy nadaj skadowym klasy bazowej prawa dostpu waciwe swoim etykietowym odpowiednikom. Tak wic dziedziczenie protected czyni wszystkie skadowe klasy bazowej chronionymi w klasie pochodnej, za private sprowadza je do dostpu prywatnego. Formalnie rzecz ujmujc, stosowanie specyfikatorw dziedziczenia jest nieobowizkowe. W praktyce jednak trudno korzysta z tego faktu, poniewa pominicie ich jest rwnoznacznie z zastosowaniem specyfikatora private77 - nie za naturalnego public! Niestety, ale tak wanie jest i trzeba si z tym pogodzi. Nie zapominaj wic o specyfikatorze public, gdy jego brak przed nazw klasy bazowej jest niemal na pewno bdem.
Dziedziczenie pojedyncze
Najprostsz i jednoczenie najczciej wystpujc w dziedziczeniu sytuacj jest ta, w ktrej mamy do czynienia tylko z jedn klasa bazow. Wszystkie dotychczas pokazane przykady reprezentoway to zagadnienie; nazywamy je dziedziczeniem pojedynczym lub jednokrotnym (ang. single inheritance).
Proste przypadki
Najprostsze sytuacje, w ktrych mamy do czynienia z tym rodzajem dziedziczenia, s czsto spotykane w programach. Polegaj one na tym, i jedna klasa jest tworzona na podstawie drugiej poprzez zwyczajne rozszerzenie zbioru pl i metod. Ilustracj bdzie tu kolejny przykad geometryczny :) class CEllipse {
77
Zakadajc, e mwimy o klasach deklaroanych poprzez sowo class. W przypadku struktur (sowo struct), ktre s w C++ niemal tosame z klasami, to public jest domylnym specyfikatorem - zarwno dziedziczenia, jak i dostpu do skadowych.
Programowanie obiektowe
199
private: // wikszy i mniejszy promie elipsy float m_fWiekszyPromien; float m_fMniejszyPromien; protected: // wsprzdne na ekranie float m_fX, m_fY; public: // konstruktor CEllipse() { m_fX = m_fY = 0.0; m_fWiekszyPromien = m_fMniejszyPromien = 10.0; } // ------------------------------------------------------------// metody float Pole() const { return PI * m_fWiekszyPromien * m_fMniejszyPromien; }
};
class CCircle : public CEllipse // koo, klasa pochodna { private: // promie koa float m_fPromien; public: // konstruktor CCircle() ( m_fPromien = 10.0; } // ------------------------------------------------------------// metody float Pole() const float Obwod() const { return PI * m_fPromien * m_fPromien; } { return 2 * PI * m_fPromien; }
};
Jest on podobny do wariantu z prostoktem i kwadratem. Tutaj klasa CCircle jest pochodn od CEllipse, zatem dziedziczy wszystkie jej skadowe, ktre nie s prywatne. Uzupenia ponadto ich zbir o dodatkow metod Obwod(), obliczajc dugo okrgu okalajcego nasze koo.
Sztafeta pokole
Hierarchia klas nierzadko nie koczy si na jednej klasie pochodnej, lecz siga nawet bardziej wgb. Nowo stworzona klasa moe by bowiem bazow dla kolejnych, te za dla nastpnych, itd. Na samym pocztku spotkalimy si zreszt z takim przypadkiem, gdzie klasami byy rodzaje zwierzt. Sprbujemy teraz przeoy tamten ukad na jzyk C++. Zaczynamy oczywicie od klasy, z ktrej wszystkie inne bior swj pocztek - CAnimal: class CAnimal // Zwierz { protected: // pola klasy float m_fMasa; unsigned m_uWiek; public: // konstruktor CAnimal() { m_uWiek = 0; }
200
Podstawy programowania
// ------------------------------------------------------------// metody void Patrz(); void Oddychaj(); // metody dostpowe do pl float Masa() const { return m_fMasa; } void Masa(float fMasa) { m_fMasa = fMasa; } unsigned Wiek() const { return m_uWiek; }
};
Jej posta nie jest chyba niespodziank: mamy tutaj wszystkie ustalone wczeniej, publiczne metody oraz pola, ktre oznaczylimy jako protected. Zrobilimy tak, bo chcemy, by byy one przekazywane do klas pochodnych od CAnimal. A skoro ju wspomnialimy o klasach pochodnych, pomylmy o ich definicjach. Zwaywszy, e kada z nich wprowadza tylko jedn now metod, powinny one by raczej proste - i istotnie takie s: class CFish : public CAnimal { public: void Plyn(); }; class CMammal : public CAnimal { public: void Biegnij(); }; class CBird : public CAnimal { public: void Lec(); }; // Ryba
// Ssak
// Ptak
Nie zapominamy rzecz jasna, e oprcz widocznych powyej deklaracji zawieraj one take wszystkie skadowe wzite od klasy CAnimal. Powtarzam to tak czsto, e chyba nie masz ju co do tego adnych wtpliwoci :D Ostatni klas z naszego drzewa gatunkowego by, jak pamitamy, Pies domowy. Definicja jego klasy take jest dosy prosta: class CHomeDog : public CMammal // Pies domowy { protected: // nowe pola RACE m_Rasa; COLOR m_KolorSiersci; public: // metody void Aportuj(); void Szczekaj(); // metody dostpowe do pl RACE Rasa() const COLOR KolorSiersci() const { return m_Rasa; } { return m_KolorSiersci; }
};
Programowanie obiektowe
201
Jak zwykle typy RACE i COLOR s mocno umowne. Ten pierwszy byby zapewne odpowiednim enumem. Wiemy jednake, i kryje si za ni cae bogactwo pl i metod odziedziczonych po klasach bazowych. Dotyczy to zarwno bezporedniego przodka klasy CHomeDog, czyli CMammal, jak i jej poredniej bazy - CAnimal. Jedyn znaczca tutaj rnic pomidzy tymi dwoma klasami jest fakt, e pierwsza wystpuje w definicji CHomeDog, za druga nie.
Paskie hierarchie
Oprcz rozbudowanych, wielopoziomowych relacji typu baza-pochodna w powszechnym zastosowaniu s te takie modele, w ktrych z jednej klasy bazowej dziedziczy wiele klas pochodnych. Jest to tzw. paska hierarchia i wyglda np. w ten sposb:
Schemat 25. Paska hierarchia klas figur szachowych (ilustracje pochodz z serwisu David Howell Chess)
Po przeoeniu jej na jzyk C++ otrzymalibymy co w tym rodzaju: // klasa bazowa class CChessPiece { /* definicja */ }; // klasy pochodne class CPawn : public CChessPiece { /* ... */ }; class CKnight : public CChessPiece { /* ... */ }; class CBishop : public CChessPiece { /* ... */ }; class CRook : public CChessPiece { /* ... */ }; class CQueen : public CChessPiece { /* ... */ }; class CKing : public CChessPiece { /* ... */ }; // Figura szachowa // // // // // // Pionek Skoczek78 Goniec Wiea Hetman Krl
Oprcz logicznego uporzdkowania rozwizanie to ma te inne zalety. Jeli bowiem zadeklarowalibymy wskanik na obiekt klasy CChessPiece, to poprzez niego moglibymy odwoywa si do obiektw krrejkolwiek z klas pochodnych. Jest to jedna z licznych pozytywnych konsekwencji polimorfizmu, ktre zreszt poznamy wkrtce. W tym przypadku oznaczaaby ona, e za obsug kadej z szeciu figur szachowych odpowiadaby najprawdopodobniej jeden i ten sam kod.
78
Nazwy klas nie s tumaczeniami z jzyka polskiego, lecz po prostu angielskimi nazwami figur szachowych.
202
Podstawy programowania
Mona zauway, ze bazowa klasa CChessPiece nie bdzie tutaj suy do tworzenia obiektw, lecz tylko do wyprowadzania z niej kolejnych klas. Sprawia to, e byaby ona dobrym kandydatem na tzw. klas abstrakcyjn. O tym zagadnieniu bdziemy mwi przy okazji metod wirtualnych.
Podsumowanie
Myl, e po takiej iloci przykadw oraz opisw koncepcja tworzenia klas pochodnych poprzez dziedziczenie powinna by ci ju doskonale znana :) Nie naley ona wszake do trudnych; wane jest jednak, by pozna zwizane z ni niuanse w jzyku C++. O dziedziczeniu pojedynczym mona take poczyta nieco w MSDN.
Dziedziczenie wielokrotne
Skoro moliwe jest dziedziczenie z wykorzystaniem jednej klasy bazowej, to raczej naturalne jest rozszerzenie tego zjawiska take na przypadki, w ktrej z kilku klas bazowych tworzymy jedn klas pochodn. Mwimy wtedy o dziedziczeniu wielokrotnym (ang. multiple inheritance). C++ jest jednym z niewielu jzykw, ktre udostpniaj tak moliwo. Nie wiadczy to jednak o jego niebotycznej wyszoci nad nimi. Tak naprawd technika dziedziczenia wielokrotnego nie daje adnych nadzwyczajnych korzyci, a jej uycie jest przy tym do skomplikowane. Decydujc si na jej wykorzystanie naley wic posiada cakiem spore dowiadczenie w programowaniu. Jakkolwiek zatem dziedziczenie wielokrotne bywa czasem przydatnym narzdziem, stosowanie go (przynajmniej powszechne) w tworzonych aplikacjach nie jest zalecane. Jeeli pojawia si taka konieczno, naley wtedy najprawdopodobniej zweryfikowa swj projekt; w wikszoci sytuacji te same, a nawet lepsze efekty mona osign nie korzystajc z tego wielce wtpliwego rozwizania. Dla szczeglnie zainteresowanych i odwanych istnieje oczywicie opis w MSDN.
Puapki dziedziczenia
Chocia idea dziedziczenia jest teoretycznie cakiem prosta do zrozumienia, jej praktyczne zastosowanie moe niekiedy nastrcza pewnych problemw. S one zazwyczaj specyficzne dla konkretnego jzyka programowania, jako e wystpuj w tym wzgldzie pewne rnice midzy nimi. W tym paragrafie zajmiemy si takimi wanie drobnymi niuansami, ktre s zwizane z dziedziczeniem klas w jzyku C++. Sekcja ta ma raczej charakter formalnego uzupenienia, dlatego pocztkujcy programici mog j ze spokojem pomin szczeglnie podczas pierwszego kontaktu z tekstem.
Programowanie obiektowe
203
destruktory. Sprawa wyglda tu podobnie jak punkt wyej. Dziaanie destruktorw najczciej take opiera si na polach prywatnych, a skoro one nie s dziedziczone, zatem destruktor te nie powinien przechodzi do klas pochodnych. Do ciekawym uzasadnieniem niedziedziczenia konstruktorw i destruktorw s take same ich nazwy, odpowiadajce klasie, w ktrej zostay zadeklarowane. Gdyby zatem przekaza je klasom pochodnych, wtedy zasada ich nazewnictwa zostaaby zamana. Chocia trudno odmwi temu podejciu pomysowoci, nie ma adnego powodu, by uwaa je za bdne. przeciony operator przypisania (=). Zagadnienie przeciania operatorw omwimy dokadnie w jednym z przyszych rozdziaw. Na razie zapamitaj, e skadowa ta odpowiada za sposb, w jaki obiekt jest kopiowany z jednej zmiennej do drugiej. Taki transfer zazwyczaj rwnie wymaga dostpu do pl prywatnych klasy, co od razu wyklucza dziedziczenie. Ze wzgldu na specjalne znaczenie konstruktorw i destruktorw, ich funkcjonowanie w warunkach dziedziczenia jest do specyficzne. Nieco dalej zostao ono bliej opisane.
Obiekty kompozytowe
Sposb, w jaki C++ realizuje pomys dziedziczenia, jest sam w sobie dosy interesujcy. Wikszo koderw uczcych si tego jzyka z pocztku cakiem logicznie przypusza, e kompilator zwyczajnie pobiera deklaracje z klasy bazowej i wstawia je do pochodnej, ewentualne powtrzenia rozwizujc na korzy tej drugiej. Swego czasu te tak mylaem i, niestety, myliem si: faktyczna prawda jest bowiem nieco bardziej zakrcona :) Ot wewntrznie uywana przez kompilator definicja klasy pochodnej jest identyczna z t, ktr wpisujemy do kodu; nie zawiera adnych pl i metod pochodzcych z klas bazowych! Jakim wic cudem s one dostpne? Odpowied jest raczej zaskakujca: podczas tworzenia obiektu klasy pochodnej dokonywana jest take kreacja obiektu klasy bazowej, ktry staje si jego czci. Zatem nasz obiekt pochodny to tak naprawd obiekt bazowy plus dodatkowe pola, zdefiniowane w jego wasnej klasie. Przy bardziej rozbudowanej hierarchii klas zaczyna on przypomina cebul:
Schemat 26. Obiekt klasy pochodnej zawiera w sobie obiekty klas bazowych
Praktyczne konsekwencje tego stanu rzeczy s zwizane chociaby z konstruowaniem i niszczeniem tych wewntrznych obiektw.
204
Podstawy programowania
W C++ obowizuje zasada, i najpierw wywoywany jest konstruktor najbardziej bazowej klasy danego obiektu, a potem te stojce kolejno niej w hierarchii. Poniewa klasa moe posiada wicej ni jeden konstruktor, kompilator musiaby podj decyzj, ktry z nich powinien zosta uyty. Nie robi tego jednak, lecz oczekuje, e zawsze79 bdzie obecny domylny konstruktor bezparametrowy. Dlatego te kada klasa, z ktrej bd dziedziczyy inne, powinna posiada taki wanie bezparametrowy (domylny) konstruktor. Podobny problem nie istnieje dla destruktorw, gdy one nigdy nie posiadaj parametrw. Podczas niszczenia obiektu s one wywoywane w kolejnoci od tego z klasy pochodnej do tych z klas bazowych. *** Koczcy si podrozdzia opisywa mechanizm dziedziczenia - jedn z podstaw techniki programowania zorientowanego obiektowego. Moge wic dowiedzie si, w jaki sposb tworzy nowe klasy na podstawie ju istniejcych i projektowa ich hierarchie, obrazujce naturalne zwizki typu og-szczeg. W nastpnej kolejnoci poznamy zalety metod wirtualnych oraz porozmawiamy sobie o najwikszym osigniciu OOPu, czyli polimorfizmie. Bdzie wic bardzo ciekawie :D
Konieczno t mona obej stosujc tzw. listy inicjalizacyjne, o ktrych dowiesz si za jaki czas.
Programowanie obiektowe
Mniej oczywisty jest natomiast fakt, e techniczny przebieg tej czynnoci moe si zasadniczo rni u poszczeglnych zwierzt. Te yjce na ldzie uywaj do tego narzdw zwanych pucami, za zwierzta wodne - chociaby ryby - maj w tym celu wyksztacone skrzela, funkcjonujce na zupenie innej zasadzie.
205
Spostrzeenia te nietrudno przeoy na bliszy nam sposb mylenia, zwizany bezporednio z programowaniem. Oto wic klasy wywodzce si do Zwierzcia powinny w inny sposb implementowa metod Oddychaj; jej tre musi by odmienna przynajmniej dla Ryby, a i Ssak oraz Gad maj przecie wasne patenty na proces oddychania. Rzeczona metoda podpada zatem pod redefinicj w kadej z klas dziedziczcych od klasy Zwierz:
};
W ten sposb przygotowujemy j na ewentualne ustpienie miejsca bardziej wyspecjalizowanym wersjom, podanym w klasach pochodnych. Skorzystanie z mechanizmu metod wirtualnych jest tutaj lepszym rozwizaniem ni zignorowanie go, gdy uaktywnia to moliwoci polimorfizmu zwizane z obiektami. Zapoznamy si z nimi w dalszej czci tekstu.
206
Podstawy programowania
Programowanie obiektowe
Posuy nam do tego nastpujcy kod, tworzcy obiekt jednej z klasy pochodnych i wywoujcy jego metod Oddychaj(): CAnimal* pZwierzak = new CMammal; pZwierzak->Oddychaj(); delete pZwierzak;
207
Zauwamy, e wskanik pZwierzak, poprzez ktry odwoujemy si do naszego obiektu, jest zasadniczo wskanikiem na klas CAnimal. Stwarzany przez nas (poprzez instrukcj new) obiekt naley natomiast do klasy CMammal. Wszystko jest jednak w porzdku. Klasa CMammal dziedziczy od klasy CAnimal, zatem kady obiekt nalecy do tej pierwszej jednoczenie jest take obiektem tej drugiej. Wyjanilimy to sobie cakiem niedawno, prezentujc dziedziczenie. Zajmijmy si raczej drug linijk powyszego kodu, zawierajc wywoanie interesujcej nas metody Oddychaj(). Rnica midzy zwykymi a wirtualnymi funkcjami skadowymi bdzie miaa okazj uwidoczni si wanie tutaj. Wszystko bowiem zaley od tego, jak metod jest rzeczona funkcja Oddychaj(), za rezultatem rozwaanej instrukcji moe by zarwno wywoanie CAnimal::Oddychaj(), jak i CMammal::Oddychaj()! Dowiedzmy si wic, kiedy zajdzie kada z tych sytuacji. atwiejszym przypadkiem jest chyba niewirtualno rozpatrywanej metody. Kiedy jest ona zwyczajn funkcj skadow, wtedy kompilator nie traktuje jej w aden specjalny sposb. Co to jednak w praktyce oznacza? To dosy proste. W takich bowiem wypadkach decyzja, ktra metoda jest rzeczywicie wywoywana, zostaje podjta ju na etapie kompilacji programu. Nazywamy j wtedy wczesnym wizaniem (ang. early binding) funkcji. Do jej podjcia s zatem wykorzystane jedynie te informacje, ktre s znane w momencie kompilacji programu; u nas jest to typ wskanika pZwierzak, czyli CAnimal. Nie jest przecie moliwe ustalenie, na jaki obiekt bdzie on faktycznie wskazywa - owszem, moe on nalee do klasy CAnimal, jednak rwnie dobrze do jej pochodnej, na przykad CMammal. Wiedza ta nie jest jednak dostpna podczas kompilacji80, dlatego te tutaj zostaje asekuracyjnie wykorzystany jedynie znany typ CAnimal. Faktycznie wywoywan metod bdzie wic CAnimal::Oddychaj()! Huh, to raczej nie jest to, o co nam chodzio. Skoro ju tworzymy obiekt klasy CMammal, to w zasadzie logiczne jest, e zaley nam na wywoaniu funkcji pochodzcej z tej wanie klasy, a nie z jej bazy! Spotyka nas jednak przykra niespodzienka Czy uchroni od niej zastosowanie metod wirtualnych? Domylasz si zapewne, i tak wanie bdzie, i na dodatek masz tutaj absolutn racj :) Kiedy uyjemy magicznego swka virtual, kompilator wstrzyma si z decyzj co do faktycznie przywoywanej metody. Jej podjcie nastpi dopiero w stosowanej chwili podczas dziaania gotowej aplikacji; nazywamy to pnym wizaniem (ang. late binding) funkcji. W tym momencie bdzie oczywicie wiadome, jaki obiekt naprawd kryje si za naszym wskanikiem pZwierzak i to jego wersja metody zostanie wywoana. Uzyskamy zatem skutek, o jaki nam chodzio, czyli wywoanie funkcji CMammal::Oddychaj(). Prezentowany tu problem wyranie podpada ju pod idee polimorfizmu, ktre wyczerpujco poznamy niebawem.
Wirtualny destruktor
Atrybut virtual moemy przyczy do kadej zwyczajnej metody, a nawet takiej niezupenie zwyczajnej :) Czasami zreszt zastosowanie go jest niemal powinnoci
80
Tak naprawd kompilator moe w ogle nie wiedzie, e CAnimal posiada jakie klasy pochodne!
208
Podstawy programowania
Jeeli chodzi o konstruktory, to stosowanie tego modyfikatora w stosunku do nich nie ma zbyt wielkiego sensu. S one przecie domylnie jakby wirtualne: wywoanie konstruktora z klasy pochodnej powoduje przecie uruchomienie take konstruktorw z klas bazowych. Ich przedefiniowanie nie jest przy tym niczym nadzwyczajnym, tak wic uycie sowa virtual w tym przypadku mija si z celem. Zupenie inaczej sprawa ma si z destruktorami. Tutaj uycie omawianego modyfikatora jest nie tylko moliwe, ale te prawie zawsze konieczne i zalecane. Nieobecno wirtualnego destruktora w klasie bazowej moe bowiem prowadzi do tzw. wyciekw pamici, czyli bezpowrotnej utraty zaalokowanej pamici operacyjnej. Dlaczego tak si dzieje? Do wyjanienia posuymy si po raz kolejny naszymi wysuonymi klasami zwierzt :D Przypumy, e czujemy potrzeb, aby dokadniej odpowiaday one rzeczywistoci; by nie byy tylko zbiorami danych, ale te zawieray obiektowe odpowiedniki narzdw wewntrznych, na przykad serca czy puc. Poczynimy wic najpierw pewne zmiany w bazowej klasie CAnimal: // klasa serca class CHeart { /* ... */ }; // bazowa klasa zwierzt class CAnimal { // (pomijamy nieistotne, pozostae skadowe) protected: CHeart* m_pSerce; public: // konstruktor i destruktor CAnimal() { m_pSerce = new CHeart; } ~CAnimal() { delete m_pSerce; }
};
Serce jest oczywicie organem, ktry posiada kade zwierz, zatem obecno wskanika na obiekt klasy CHeart jest tu uzasadniona. Odwouje si on do obiektu tworzonego w konstruktorze, a niszczonego w destruktorze klasy CAnimal. Naturalnie, nie samym sercem zwierz yje :) Ssaki na przykad potrzebuj jeszcze puc: // klasa puc class CLungs { /* ... */ }; // klasa ssakw class CMammal : public CAnimal { protected: CLungs* m_pPluca; public: // konstruktor i destruktor CMammal() { m_pPluca = new CLungs; } ~CMammal() { delete m_pPluca; } }; Podobnie jak wczeniej, obiekt specjalnej klasy jest tworzony w konstruktorze i zwalniany w destruktorze CMammal. W ten sposb nasze ssaki s zaopatrzone zarwno w serce (otrzymane od CAnimal), jak i niezbdne puca, tak wic poyj sobie jeszcze troch i bd mogy nadal suy nam jako przykad ;) OK, gdzie zatem tkwi problem? Powrmy teraz do trzech linijek kodu, za pomoc ktrych rozstrzygnlimy pojedynek midzy wirtualnymi a niewirtualnymi metodami:
Programowanie obiektowe
209
CAnimal* pZwierzak = new CMammal; pZwierzak->Oddychaj(); delete pZwierzak; Przypomnijmy, e pZwierzak jest tu zasadniczo zmienn typu wskanik na obiekt klasy CAnimal, ale tak naprawd wskazuje na obiekt nalecy do pochodnej CMammal. w obiekt musi oczywicie zosta usunity, za co powinna odpowiada ostatnia linijka No wanie - powinna. Szkoda tylko, e tego nie robi. To zreszt nie jest jej wina, przyczyn jest wanie brak wirtualnego destruktora. Jak bowiem wiemy, zniszczenie obiektu oznacza w pierwszej kolejnoci wywoanie tej kluczowej metody. Podlega ono identycznym reguom, jakie stosuj si do wszystkich innych metod, a wic take efektom zwizanym z wirtualnoci oraz wczesnym i pnym wizaniem. Jeeli wic nasz destruktor nie bdzie oznaczony jako virtual, to kompilator potraktuje go jako zwyczajn metod i zastosuje wobec niej technik wczesnego wizania. Zasugeruje si po prostu typem zmiennej pZwierzak (ktrym jest CAnimal*, a wic wskanik na obiekt klasy CAnimal) i wywoa wycznie destruktor klasy bazowej CAnimal! Destruktor ten wprawdzie usunie serce naszego ssaka, ale nie zrobi tego z pucami, bo i nie ma przecie o nich zielonego pojcia. Nie do zatem, e tracimy przez to pami przeznaczon na tene narzd, to jeszcze pozwalamy, by wok fruway nam organy pozbawione wacicieli ;D To oczywicie tylko obrazowy dowcip, jednak konsekwencje niepenego zniszczenia obiektw mog by duo powaniejsze, szczeglnie jeli ich skadniki odwoyway si do siebie nawzajem. Wemy choby wspomniane puca - powinny one przecie dostarcza tlen do serca, a jeeli samo serce ju nie istnieje, no to zaczynaj si nieliche problemy Rozwizanie problemu jest rzecz jasna nadzwyczaj proste - wystarczy uczyni destruktor klasy bazowej CAnimal metod wirtualn: class CAnimal { // (oszczdno jest cnot, wic znowu pomijamy reszt skadowych :D) public: virtual ~CAnimal() { delete m_pSerce; }
};
Wtedy te operator delete bdzie usuwa obiekt, na ktry faktycznie wskazuje podany mu wskanik. My za uchronimy si od perfidnych bdw. Pamitaj zatem, aby zawsze umieszcza wirtualny destruktor w klasie bazowej.
210
Podstawy programowania
};
Jest nim wystpujca na kocu fraza = 0;. Kojarzy si ona troch z domyln wartoci funkcji, ale interpretacja taka upada w obliczu niezwracania przez metod Oddychaj() adnego rezultatu. Faktycznie funkcj czysto wirtualn moemy w ten sposb uczyni kad wirtualn metod, niezalenie od tego, czy zwraca jak warto i jakiego jest ona typu. Sekwencja = 0; jest wic po prostu takim dziwnym oznaczeniem, stosowanym dla tego rodzaju metod. Trzeba si z nim zwyczajnie pogodzi :) Twrcy C++ wyranie nie chcieli wprowadza tutaj dodatkowego sowa kluczowego, ale w tym przypadku trudno si z nimi zgodzi. Osobicie uwaam, e deklaracja w formie na przykad pure virtual void Oddychaj(); byaby znacznie bardziej przejrzysta. Po dokonaniu powyszej operacji metoda CAnimal::Oddychaj() staje si zatem czysto wirtualn funkcj skadow. W tej postaci okrela ju tylko sam czynno, bez podawania adnego algorytmu jej wykonania. Zostanie on ustalony dopiero w klasach dziedziczcych od CAnimal. Mona aczkolwiek poda implementacj metody czysto wirtualnej, jednak bdzie ona moga by wykorzystywana tylko w kodzie metod klas pochodnych, ktre j przedefiniowuj, w formie klasa_bazowa::nazwa_metody([parametry]).
Programowanie obiektowe
211
Klasa abstrakcyjna zawiera przynajmniej jedn czysto wirtualn metod i z jej powodu nie jest przeznaczona do instancjowania (tworzenia z niej obiektw), a jedynie do wyprowadzania ze klas pochodnych. Ze wzgldu na wyej wymienion definicj czysto wirtualne funkcje skadowe okrela si niekiedy mianem metod abstrakcyjnych. Nazwa ta jest szczeglnie popularna wrd programistw jzyka Object Pascal. Takie klasy buduj zawsze najwysze pitra w hierarchiach i s podstawami dla bardziej wyspecjalizowanych typw. W naszym przypadku mamy tylko jedn tak klas, z ktrej dziedzicz wszystkie inne. Nazywa si CAnimal, jednak dobry zwyczaj programistyczny nakazuje, aby klasy abstrakcyjne miay nazwy zaczynajce si od litery I. Rni si one bowiem znacznie od pozostaych klas. Zatem baza w naszej hierarchii bdzie od tej pory zwa si IAnimal. C++ bardzo dosownie traktuje regu, i klasy abstrakcyjne nie s przeznaczone do instancjowania. Prba utworzenia z nich obiektu zakoczy si bowiem bdem; kompilator nie pozwoli na obecno czysto wirtualnej metody w klasie tworzonego obiektu. Moliwe jest natomiast zadeklarowanie wskanika na obiekt takiej klasy i przypisanie mu obiektu klasy potomnej, tak wic poniszy kod bdzie jak najbardziej poprawny: IAnimal* pZwierze = new CBird; pZwierze->Oddychaj(); delete pZwierze; Wywoanie metody Oddychaj() jest tu take dozwolone. Wprawdzie w bazowej klasie IAnimal jest ona czysto wirtualna, jednak w CBird, do obiektu ktrej odwouje si nasz wskanik, posiada ona odpowiedni implementacj. Wydawaoby si, e C++ reaguje nieco zbyt alergicznie na prb utworzenia obiektu klasy abstrakcyjnej - w kocu sama kreacja nie jest niczym niepoprawnym. W ten sposb jednak mamy pewno, e podczas dziaania programu wszystko bdzie dziaa poprawnie i e omykowo nie zostanie wywoana metoda z nieokrelon implementacj.
Polimorfizm
Gdyby programowanie obiektowe porwna do wysokiego budynku, to u jego fundamentw leayby pojcia klasy i obiekty, rodkowe pitra budowaoby dziedziczenie oraz metody wirtualne, za u samego szczytu sytuowaby si polimorfizm. Jest to bowiem najwiksze osignicie tej metody programowania. Z terminem tym spotykalimy si przelotnie ju par razy, ale teraz wreszcie wyjanimy sobie wszystko od pocztku do koca. Zacznijmy choby od samego sowa: polimorfizm pochodzi od greckiego wyrazu polmorphos, oznaczajcego wieloksztatny lub wielopostaciowy. W programowaniu bdzie si wic odnosi do takich tworw, ktre mona interpretowa na rne sposoby - a wic nalecych jednoczenie do kilku rnych typw (klas). Polimorfizm w programowaniu obiektowym oznacza wykorzystanie tego samego kodu do operowania na obiektach przynalenych rnym klasom, dziedziczcym od siebie. Zjawisko to jest zatem cile zwizane z klasami i dziedziczeniem, aczkolwiek w C++ nie dotyczy ono kadej klasy, a jedynie okrelonych typw polimorficznych.
212
Podstawy programowania
Typ polimorficzny to w C++ klasa zawierajca przynajmniej jedn metod wirtualn. W praktyce wikszo klas, do ktrych chcielibymy stosowa techniki polimorfizmu, spenia ten warunek. W szczeglnoci t wymagan metod wirtualn moe by chociaby destruktor. Wszystko to brzmi bardzo adnie, ale trudno nie zada sobie pytania o praktyczne korzyci zwizane z wykorzystaniem polimorfizmu. Dlatego te moim celem bdzie teraz drobiazgowa odpowied na to pytanie - innymi sowy, wreszcie doczekae si konkretw ;D
Sprowadzanie do bazy
Prosty przypadek wykorzystania polimorfizmu opiera si na elementarnej i rozsdnej zasadzie, ktr nie raz ju sprawdzilimy w praktyce. Mianowicie: Wskanik na obiekt klasy bazowej moe wskazywa take na obiekt ktrejkolwiek z jego klas pochodnych. Bezporednie przeoenie tej reguy na konkretne zastosowanie programistyczne jest do proste. Przypumy wic, e mamy tak oto hierarchi klas: #include <string> #include <ctime> // klasa dowolnego dokumentu class CDocument { protected: // podstawowe dane dokumentu std::string m_strAutor; // autor dokumentu std::string m_strTytul; // tytu dokumentu tm m_Data; // data stworzenia public: // konstruktory CDocument() { m_strAutor = m_strTytul = "???"; time_t Czas = time(NULL); m_Data = *localtime(&Czas); } CDocument(std::string strTytul) { CDocument(); m_strTytul = strTytul; } CDocument(std::string strAutor, std::string strTytul) { CDocument(); m_strAutor = strAutor; m_strTytul = strTytul; }
Programowanie obiektowe
213
// ------------------------------------------------------------// metody dostpowe std::string Autor() std::string Tytul() tm Data() do pl const { return m_strAutor; } const { return m_strTytul; } const { return m_Data; }
};
// ---------------------------------------------------------------------// dokument internetowy class COnlineDocument : public CDocument { protected: std::string m_strURL; // adres internetowy dokumentu public: // konstruktory COnlineDocument(std::string strAutor, std::string strTytul) { m_strAutor = strAutor; m_strTytul = strTytul; } COnlineDocument (std::string strAutor, std::string strTytul, std::string strURL) { m_strAutor = strAutor; m_strTytul = strTytul; m_strURL = strURL; } // ------------------------------------------------------------// metody dostpowe do pl std::string URL() const { return m_strURL; }
};
// ksika class CBook : public CDocument { protected: std::string m_strISBN; // numer ISBN ksiki public: // konstruktory CBook(std::string strAutor, std::string strTytul) { m_strAutor = strAutor; m_strTytul = strTytul; } CBook (std::string strAutor, std::string strTytul, std::string strISBN) { m_strAutor = strAutor; m_strTytul = strTytul; m_strISBN = strISBN; } // ------------------------------------------------------------// metody dostpowe do pl std::string ISBN() const { return m_strISBN; }
};
Z klasy CDocument, reprezentujcej dowolny dokument, dziedzicz dwie nastpne: COnlineDocument, odpowiadajca tekstom dostpnym przez Internet, oraz CBook, opisujca ksiki. Napiszmy rwnie odpowiedni funkcj, wywietlajc podstawowe informacje o podanym dokumencie:
214
#include <iostream> void PokazDaneDokumentu(CDocument* pDokument) { // wywietlenie autora std::cout << "AUTOR: "; std::cout << pDokument->Autor() << std::endl;
Podstawy programowania
// pokazanie tytuu dokumentu // (sekwencja \" wstawia cudzysw do napisu) std::cout << "TYTUL: "; std::cout << "\"" << pDokument->Tytul() << "\"" << std::endl; // data utworzenia dokumentu // (pDokument->Data() zwraca struktur typu tm, do ktrej pl // mona dosta si tak samo, jak do wszystkich innych - za // pomoc operatora wyuskania . (kropki)) std::cout << "DATA : "; std::cout << pDokument->Data().tm_mday << "." << (pDokument->Data().tm_mon + 1) << "." << (pDokument->Data().tm_year + 1900) << std::endl;
Bierze ona jeden parametr, bdcy zasadniczo wskanikiem na obiekt typu CDocument. W jego charakterze moe jednak wystpowa take wskazanie na ktry z obiektw potomnych, zatem poniszy kod bdzie absolutnie prawidowy: COnlineDocument* pTutorial = new COnlineDocument("Xion", "Od zera do gier kodera", "http://avocado.risp.pl"); PokazDaneDokumentu (pTutorial); delete pTutorial; // autor // tytu // URL
W pierwszej linijce monaby rwnie dobrze uy typu wskazujcego na obiekt CDocument, gdy wskanik pTutorial i tak zostanie potraktowany w ten sposb przy przekazywaniu go do funkcji PokazDaneDokumentu(). Efektem jego dziaania powyszego listingu bdzie na przykad taki oto widok:
Brak tu informacji o adresie internetowym dokumentu, poniewa naley on do skadowych specyficznych dla klasy COnlineDocument. Funkcja PokazDaneDokumentu() zostaa natomiast stworzona do pracy z obiektami CDocument, zatem wykorzystuje jedynie informacje zawarte w klasie bazowej. Nie przeszkadza to jednak w przekazaniu jej obiektu klasy pochodnej - w takim przypadku dodatkowe dane zostan po prostu zignorowane. To raczej mao satysfakcjonujce rozwizanie, ale lepsze skutki wymagaj ju uycia metod wirtualnych. Uczynimy to w kolejnym przykadzie. Naturalnie, podobny rezultat otrzymalibymy podajc naszej funkcji obiekt klasy CBook czy te jakiejkolwiek innej dziedziczcej od CDocument. Kod procedury jest wic uniwersalny i moe by stosowany do wielu rnych rodzajw obiektw.
Programowanie obiektowe
Eureka! Na tym przecie polega polimorfizm :)
215
Moliwe e zauwaye, i adna z tych przykadowych klas nie jest tutaj typem polimorficznym, a jednak podany wyej kod dziaa bez zarzutu. Powodem tego jest jego wzgldna prostota. Dokadniej mwic, nie jest konieczne sprawdzanie poprawnoci typw podczas dziaania programu, bo wystarczajca jest zwyka kontrola, dokonywana zwyczajowo podczas kompilacji kodu.
};
// (reszty klas nieuwzgldniono z powodu dziury budetowej ;D) // (za ich implementacje s w pliku documents.cpp) // *** main.cpp *** #include <iostream> #include <conio.h> #include "documents.h" void main() { // wskanik na obiekty dokumentw CDocument* pDokument; // pierwszy dokument - internetowy std::cout << std::endl << "--- 1. pozycja ---" << std::endl; pDokument = new COnlineDocument("Regedit", "Cyfrowe przetwarzanie tekstu",
216
Podstawy programowania
"http://programex.risp.pl/?" "strona=cyfrowe_przetwarzanie_tekstu" );
// drugi dokument - ksika std::cout << std::endl << "--- 2. pozycja ---" << std::endl; pDokument = new CBook("Sam Williams", "W obronie wolnosci", "83-7361-247-5"); pDokument->PokazDane(); delete pDokument; } getch();
Zauwamy, e za wywietlenie obu widniejcych na nim pozycji odpowiada wywoanie pozornie tej samej funkcji: pDokument->PokazDane(); Polimorficzny mechanizm metod wirtualnych sprawia jednak, e zawsze wywoywana jest odpowiednia wersja procedury PokazDane() - odpowiednia dla kolejnych obiektw, na ktre wskazuje pDokument. Tutaj mamy wprawdzie tylko dwa takie obiekty, ale nietrudno wyobrazi sobie analogiczne dziaanie dla wikszej ich liczby, np.: CDocument* apDokumenty[100]; for (unsigned i = 0; i < 100; ++i) apDokumenty[i]->PokazDane(); Poszczeglne elementy tablicy apDokumenty mog wskazywa na obiekty dowolnych klas, dziedziczcych od CDocument, a i tak kod wywietlajcy ich dane bdzie ogranicza si do wywoania zaledwie jednej metody! I to wanie jest pikne :D Moliwe zastosowania takiej techniki mona mnoy w nieskoczono, za w grach jest po prostu nieoceniona. Pomylmy tylko, e za pomoc podobnej tablicy i prostej ptli moemy wykona dowoln czynno na zestawie przernych obiektw. Rysowanie, wywietlanie, kontrola animacji - wszystko to moemy wykona poprzez jedn instrukcj!
Programowanie obiektowe
217
Niezalenie od tego, jak bardzo byaby rozbudowana hierarchia naszych klas (np. jednostek w grze strategicznej, wrogw w grze RPG, i tak dalej), zastosowanie polimorfizmu z metodami wirtualnymi upraszcza kod wikszoci operacji do podobnie trywialnych konstrukcji jak powysza. Od tej pory do nas naley wic tylko zdefiniowanie odpowiedniego modelu klas i ich metod, gdy zarzdzanie poszczeglnymi obiektami staje si, jak wida, banalne. Co waniejsze, zastosowanie technik obiektowych nie tylko upraszcza kod, ale te pozwala na znacznie wiksz elastyczno. Pamitaj, e praktyka czyni mistrza! Poznanie teoretycznych aspektw programowania obiektowego jest wprawdzie niezbdne, ale najwicej wartociowych umiejtnoci zdobdziesz podczas samodzielnego projektowania i kodowania programw. Wtedy szybko przekonasz si, e stosowanie technik polimorfizmu jest prawie e intuicyjne nawet jeli teraz nie jeste zbytnio tego pewien.
Operator dynamic_cast
Konwersja wskanika do klasy pochodnej na wskanik do klasy bazowej jest czynnoci do naturaln, wic przebiega cakowicie automatycznie. Niepotrzebne jest nawet zastosowanie jakiej formy rzutowania. Nie powinno to wcale dziwi - w kocu na tym polega sama idea dziedziczenia, e obiekt klasy potomnej jest take obiektem przynalenym klasie bazowej. Inaczej jest z konwersj w odwrotn stron - ta nie zawsze musi si przecie powie. C++ powinien wic udostpnia jaki sposb na sprawdzenie, czy taka zamiana jest moliwa, no i na samo jej przeprowadzanie. Do tych celw suy operator rzutowania dynamic_cast. Jest to drugi z operatorw rzutowania, jakie mamy okazj pozna. Zosta on wprowadzony do jzyka C++ po to, by umoliwi kompleksow obsug typw polimorficznych w zakresie konwersji w d hierarchii klas. Jego przeznaczenie jest zatem nastpujce: Operator dynamic_cast suy do rzutowania wskanika do obiektu klasy bazowej na wskanik do obiektu klasy pochodnej. Powiedzielimy sobie rwnie, e taka konwersja niekoniecznie musi by moliwa. Rol omawianego operatora jest wic take sprawdzanie, czy rzeczywicie mamy do czynienia z wskanikiem do obiektu potomnego, przechowywanym przez zmienn bdc wskanikiem do typu bazowego. Uff, wszystko to wydaje si bardzo zakrcone, zatem najlepiej bdzie, jeeli przyjrzymy si odpowiednim przykadom. Po raz kolejny posuymy si przy tym nasz ulubion systematyk klas zwierzt i napiszemy tak oto funkcj: #include <stdlib.h> #include <ctime> // eby uy rand() i srand() // eby uy time()
IAnimal* StworzLosoweZwierze() {
218
// zainicjowanie generatora liczb losowych srand (static_cast<unsigned>(time(NULL)));
Podstawy programowania
// wylosowanie liczby i stworzenie obiektu zwierza switch (rand() % 4) { case 0: return new CFish; case 1: return new CMammal; case 2: return new CBird; case 3: return new CHomeDog; default: return NULL; }
Losuje ona liczb i na jej podstawie tworzy obiekt jednej z czterech, zdefiniowanych jaki czas temu, klas zwierzt. Nastpnie zwraca wskanik do niego jako wynik swego dziaania. Rezultat ten jest rzecz jasna typu IAnimal*, aby mg pomieci odwoania do jakiegokolwiek zwierzcia, dziedziczcego z klasy bazowej IAnimal. Powysza funkcja jest bardzo prostym wariantem tzw. fabryki obiektw (ang. object factory). Takie fabryki to najczciej osobne obiekty, ktre tworz zalene do siebie byty np. na podstawie staych wyliczeniowych, przekazywanych swoim metodom. Metody takie mog wic zwrci wiele rnych rodzajw obiektw, dlatego deklaruje si je z uyciem wskanikw na klasy bazowe - u nas jest to IAnimal*. Wywoanie tej funkcji zwraca nam wic dowolne zwierz i zdawaoby si, e nijak nie potrafimy sprawdzi, do jakiej klasy ono faktycznie naley. Z pomoc przychodzi nam jednak operator dynamic_cast, dziki ktremu momue sprbowa rzutowania otrzymanego wskanika na przykad do typu CMammal*: IAnimal* pZwierze = StworzLosoweZwierze(); CMammal* pSsak = dynamic_cast<CMammal*>(pZwierze); Taka prba powiedzie si jednak tylko w rednio poowie przypadkw (dlaczego?81). Co zatem bdzie, jeeli pZwierze odnosi si do innego rodzaju zwierzt? Ot w takim przypadku otrzymamy prost informacj o bdzie, mianowicie: dynamic_cast zwrci wskanik pusty (o wartoci NULL), jeeli niemoliwe bdzie dokonanie podanego rzutowania. Aby j wychwyci potrzebujemy oczywicie dodatkowego warunku, porwnujcego zmienn pSsak z t specjaln wartoci NULL (bdc zreszt de facto zerem): if (pSsak != NULL) // sprawdzenie, czy rzutowanie powiodo si { // OK - rzeczywicie mamy do czynienia z obiektem klasy CMammal. // pSsak moe by tu uyty tak samo, jak kady inny wskanik // na obiekt klasy CMammal, na przykad: pSsak->Biegnij(); }
Obiekt klasy CMammal jest tworzony zarwno poprzez new CMammal, jak i new CHomeDog. Klasa CHomeDog dziedziczy przecie po klasie CMammal.
81
Programowanie obiektowe
219
Warunek if (pSsak != NULL) moe by zastpiony przez if (pSsak). Wwczas kompilator dokona automatycznej zamiany wartoci pSsak na logiczn, co da fasz, jeeli jest ona rwna zeru (czyli NULL) oraz prawd w kadej innej sytuacji. Moliwe jest nawet wiksze skondensowanie kodu. Wystarczy wstawi linijk z rzutowaniem bezsporednio do warunku if, tzn. zastosowa instrukcj: if (CMammal* pSsak = dynamic_cast<CMammal*>(pZwierzak)) Pojedynczy znak = jest tutaj umieszczony celowo, gdy w ten sposb cae przypisanie reprezentuje wynik rzutowania, ktry zostaje potem niejawnie przyrwnany do zera. Kontrola otrzymanego wyniku rzutowania jest konieczna; jeeli bowiem sprbowalimy zastosowa operator wyuskania -> do pustego wskanika, spowodowalibymy bd ochrony pamici (access violation). Naley wic zawsze sprawdza, czy rzutowanie dynamic_cast powiodo si, poprzez porwnanie otrzymanego wskanika z wartoci NULL. I to jest w zasadzie wszystko, co naley wiedzie o operatorze dynamic_cast :) Incydentalnie trafiaj si sytuacje, w ktrych zastosowanie omawianego operatora wymaga wczenia specjalnej opcji kompilatora, uaktywniajcej informacje o typie podczas dziaania programu. S to rzadkie przypadki i prawie zawsze dotycz wielodziedziczenia, niemniej warto wiedzie, e takie niespodzianki mog si czasem przytrafi. Sposb wczenia informacji o typie w czasie dziaania programu jest opisany w nastpnym paragrafie. Bliszych szczegow na temat rzutowania dynamic_cast mona doszuka si w MSDN.
2. 1. 3.
220
Podstawy programowania
Screen 34, 35 i 36. Trzy kroki do wczenia RTTI w Visual Studio .NET
W Visual Studio. NET naley w tym celu rozwin zakadk Solution Explorer, klikn prawym przyciskiem myszy na nazw swojego projektu i z menu podrcznego wybra Properties. W pojawiajcym si oknie dialogowym trzeba teraz przej do strony C/C++|Language i przy opcji Enable Run-Time Type Info ustawi wariant Yes (/GR). doczenia do kodu standardowego nagwka typeinfo, czyli dodania dyrektywy: #include <typeinfo> W zamian za te wysiki otrzymamy moliwo korzystania z operatora typeid, pobierajcego informacj o typie podanego mu wyraenia. Skadnia jego uycia jest nastpujca: typeid(wyraenie).informacja Faktycznie instrukcja typeid(wyraenie) zwraca struktur, nalec do wbudowanego typu std::type_info. Struktura ta opisuje typ wyraenia i zawiera takie oto skadowe: informacja opis Jest to nazwa typu w czytelnej i przyjaznej dla czowieka formie. Moemy j przechowywa i operowa ni tak, jak kadym innym napisem. Przykad: #include <typeinfo> #include <iostream> #include <ctime> int nX = 10; float fY = 3.14; time_t Czas = time(NULL); tm Data = *localtime(&Czas); std::cout << typeid(nX).name(); // wynik: "int" std::cout << typeid(fY).name(); // wynik: "float" std::cout << typeid(Data).name(); // wynik: "struct tm" Zwraca nazw typu wewntrznie uywan przez kompilator. Taka nazwa musi by unikalna, dlatego zawiera rne dekoracyjne znaki, jak ? czy @. Nie jest czytelna dla czowieka, ale moe ewentualnie suy w celach porwnawczych.
Tabela 11. Informacje dostpne poprzez operator typeid
name()
raw_name()
Oprcz pobierania nazwy typu w postaci cigu znakw moemy uywa operatorw == oraz != do porwnywania typw dwch wyrae, na przykad: unsigned uX; if (typeid(uX) == typeid(unsigned)) std::cout << "wietnie, nasz kompilator dziaa ;D"; if (typeid(uX) != typeid(uX / 0.618)) std::cout << "No prosz, tutaj te jest dobrze :)"; typeid mgby wic suy nam do sprawdzania klasy, do ktrej naley polimorficzny obiekt wskazywany przez wskanik. Sprawdmy zatem, jak by to mogo wyglda: IAnimal* pZwierze = new CBird; std::cout << typeid(pZwierze).name(); Po wykonaniu tego kodu spotka nas raczej przykra niespodzienka - zamiast oczekiwanego rezultatu "class CBird *" otrzymamy "class IAnimal *"! Wyglda na
Programowanie obiektowe
221
to, e faktyczny typ obiektu, do ktrego odwouje si pZwierze, nie zosta w ogle wzity pod uwag. Przypuszczenia te s suszne. Ot typeid jest leniwym operatorem i zawsze idzie po najmniejszej linii oporu. Typ wyraenia pZwierze mg za okreli nie sigajc nawet do mechanizmw polimorficznych, poniewa wyranie zadeklarowalimy go jako IAnimal*. Aby zmusi krnbrny operator do wikszego wysiku, musimy mu poda sam obiekt, a nie wskanik na niego, co czynimy w ten sposb: std::cout << typeid(*pZwierze).name(); O wystpujcym tu operatorze dereferencji - gwiazdce (*) powiemy sobie bliej, gdy przejdziemy do dokadnego omawiania wskanikw jako takich. Na razie zapamitaj, e przy jego pomocy wyawiamy obiekt poprzez wskanik do niego. Naturalnie, teraz powyszy kod zwrci prawidowy wynik "class CBird". Peny opis operatora typeid znajduje si oczywicie w MSDN.
Alternatywne rozwizania
RTTI jest czsto zbyt cik armat, wytoczon przeciw problemowi pobierania informacji o klasie obiektu podczas dziaania aplikacji. Przy niewielkim nakadzie pracy mona samemu wykona znacznie mniejszy, acz nierzadko wystarczajcy system. Po co? Decydujcym argumentem moe by szybko. Wbudowane mechanizmy RTTI, jak dynamic_cast i typeid, s dosy wolne (szczeglnie dotyczy to tego pierwszego). Wasne, bardziej porczne rozwizanie moe mie spory wpyw na wydajno. Do tego celu mog posuy metody wirtualne oraz odpowiedni typ wyliczeniowy, posiadajcy list wartoci odpowiadajcych poszczeglnym klasom. W przypadku naszych zwierzt mgby on wyglda na przykad tak: enum ANIMAL { A_BASE, A_FISH, A_MAMMAL, A_BIRD, A_HOMEDOG }; // // // // // bazowa klasa IAnimal klasa CFish klasa CMammal klasa CBird klasa CHomeDog
Teraz wystarczy tylko zdefiniowa proste metody wirtualne, ktre bd zwracay stae waciwe swoim klasom: // (pominem pozostae skadowe klas) class IAnimal { public: virtual ANIMAL Typ() const { return A_BASE; } }; // ----------------------bezporednie pochodne -------------------------class CFish : public IAnimal { public: ANIMAL Typ() const { return A_FISH: } }; class CMammal : public IAnimal {
222
public: ANIMAL Typ() const { return A_MAMMAL; }
Podstawy programowania
};
class CBird : public IAnimal { public: ANIMAL Typ() const { return A_BIRD; } }; // ----------------------- porednie pochodne ---------------------------class CHomeDog : public CMammal { public: ANIMAL Typ() const { return A_HOMEDOG; } }; Po zastosowaniu tego rozwizania moemy chociaby uy instrukcji switch, by wykona kod zaleny od typu obiektu: IAnimal* pZwierzak = StworzLosoweZwierze(); switch (pZwierzak->Typ()) { case A_FISH: static_cast<CFish*>(pZwierzak)->Plyn(); case A_BIRD: static_cast<CBird*>(pZwierzak)->Lec(); case A_MAMMAL: static_cast<CMammal*>(pZwierzak)->Biegnij(); case A_HOMEDOG: static_cast<CHomeDog*>(pZwierzak)->Szczekaj(); }
Podobne sprawdzenie, dokonywane przy uyciu dynamic_cast lub typeid, wymagaoby wielopitrowej instrukcji if. Tutaj wystarczy bardziej naturalny switch, za do formalnego rzutowania moemy uy prostego static_cast, ktre dziaa szybciej ni mechanizmy RTTI. Trzeba jednak pamita, e aby bezpiecznie stosowa static_cast do rzutowania w d hierarchii klas, musimy mie pewno, e taka operacja jest faktycznie wykonalna. Tutaj sprawdzamy rzeczywisty typ obiektu82, zatem wszystko jest w porzdku, lecz w innych przypadkach naley skorzysta z dynamic_cast. Systemy identyfikacji i zarzdzania typami, podobne do powyszego, s w praktyce uywane bardzo czsto, szczeglnie w wielkich projektach. Najbardziej zaawansowane warianty umoliwiaj nawet tworzenie obiektw na podstawie nazwy klasy przechowywanej jako napis lub te dynamiczne odtworzenie hierarchii klas podczas dziaania programu. Trzeba jednak przyzna, i jest to nierzadko sztuka dla samej sztuki, bez wielkiego praktycznego znaczenia. Rwnowaga przede wszystkim - pamitajmy t sentencj :D *** Gratulacje! Wanie poznae wszystkie teoretyczne zaoenia programowania obiektowego i ich praktyczn realizacj w C++. Wykorzystujc zdobyt wiedz, bdziesz mg efektywnie programowa aplikacje z uyciem filozofii OOP.
82
Sprawdzenie przy uyciu typeid take upowaniaoby nas do stosowania static_cast podczas rzutowania.
Programowanie obiektowe
223
Sucham? Mwisz, e to wcale nie jest takie proste? Zgadza si, na pocztku mylenie w kategoriach obiektowych moe rzeczywicie sprawia ci trudnoci. Pomylaem wic, e dobrze bdzie powici nieco czasu take na zagadnienia zwizane z samym projektowaniem aplikacji z uyciem poznanych technik. Zajmiemy si tym w nadchodzcym podrozdziale.
Rodzaje obiektw
Kady program zawiera w mniejszej lub wikszej czci nowatorskie rozwizania, stanowice gwne wyzwanie stojce przed jego twrc. Niemniej jednak pewne cechy
224
Podstawy programowania
prawie zawsze pozostaj stae - a do nich naley take podzia obiektw skadowych aplikacji na trzy fundamentalne grupy. Podzia ten jest bardzo oglny i niezbyt sztywny, ale przez to stosuje si w zasadzie do kadego projektu. Bdzie on zreszt punktem wyjcia dla nieco bardziej szczegowych kwestii, opisanych pniej. Pomwmy wic kolejno o kadym rodzaju z owej podstawowej trjki.
Singletony
Wikszo obiektw jest przeznaczonych do istnienia w wielu egzemplarzach, rnicych si przechowywanymi danymi, lecz wykonujcych te same dziaania poprzez metody. Istniej jednake wyjtki od tej reguy, a nale do nich wanie singletony. Singleton (jedynak) to klasa, ktrej jedyna instancja (obiekt) spenia kluczow rol w caym programie. W danym momencie dziaania aplikacji istnieje wic co najwyej jeden egzemplarz klasy, bdcej singletonem. Obiekty takie s dosownie jedyne w swoim rodzaju i dlatego zwykle przechowuj one najwaniejsze dane programu oraz wykonaj wikszo newralgicznych czynnoci. Najczciej s te rodzicami i wacicielami pozostaych obiektw. W jakich sytuacjach przydaj si takie twory? Ot jeeli podzielilibymy nasz projekt na jakie skadowe (sposb podziau jest zwykle spraw mocno subiektywn), to dobrymi kandydatami na singletony byyby przede wszystkim te skadniki, ktre obejmowayby najszerszy zakres funkcji. Moe to by obiekt aplikacji jako takiej albo te reprezentacje poszczeglnych podsystemw - w grach byyby to: grafika, dwik, sie, AI, itd., w edytorach: moduy obsugi plikw, dokumentw, formatowania itp. Niekiedy zastosowanie singletonw wymuszaj warunki zewntrzne, np. jakie dodatkowe biblioteki, uywane przez program. Tak jest chociaby w przypadku funkcji Windows API odpowiedzialnych za zarzdzanie oknami. Si rzeczy singletony stanowi te punkty zaczepienia dla caego modelu klas, gdy ich pola s w wikszoci odwoaniami do innych obiektw: niekiedy do wielu drobnych, ale czciej do kilku kolejnych zarzdcw, czyli nastpnego, niszego poziomu hierarchii zawierania si obiektw. O relacji zawierania si (agregacji) bdziemy jeszcze szerzej mwi.
Przykady wykorzystania
Najbardziej oczywistym przykadem singletonu moe by caociowy obiekt programu, a wic klasa w rodzaju CApplication czy CGame. Bdzie ona nadrzdnym obiektem wobec wszystkich innych, a take przechowywaa bdzie globalne dane dotyczce aplikacji jako caoci. To moe by chociaby cieka do jej katalogu, ale take kluczowe informacje otrzymane od bibliotek Windows API, DirectX czy jakichkolwiek innych. Jeeli chodzi o inne moliwe singletony, to z pewnoci bd to zarzdcy poszczeglnych moduw; w grach s to obiekty klas o tak wiele mwicych nazwach jak CGraphicsSystem, CSoundSystem, CNetworkSystem itp., podobne twory mona te wyrni w programach uytkowych. Wszystkie te klasy wystpuj w pojedynczych instancjach, gdy unikatowa jest ich rola. Kwesti otwart jest natomiast ich ewentualna podlego najbardziej nadrzdnemu obiektowi aplikacji - na przykad w ten sposb:
Programowanie obiektowe
class CGame { private: CGraphicsSystem* m_pGFX; CSoundSystem* m_pSFX; CNetworkSystem* m_pNet; // itd. }; // (reszt skadowych pominiemy)
225
//
83
Rwnie dobrze mog by bowiem samodzielnymi obiektami, dostpnymi poprzez swoje wasne zmienne globalne - bez porednictwa obiektu gwnego. Obydwa podejcia s w zasadzie rwnie dobre (moe z lekkim wskazaniem na pierwsze, jako e nie zapewnia takiej swobody w dostpie do podsystemw z zewntrz). Dlaczego jednak w ogle stosowa singletony, jeeli i tak bd one tylko pojedynczymi kopiami swoich pl? Przecie podobne efekty mona uzyska stosujc zmienne globalne oraz zwyczajne funkcje w miejsce pl i metod takiego obiektu-jedynaka. To jednak tylko cz prawdy. Namnoenie zmiennych i funkcji poza zasadnicz, obiektow struktur programu narusza zasady OOPu, i to a podwjnie. Po pierwsze, nie unikniemy w ten sposb wyranego oddzielenia danych od kodu, a po drugie nie zapewnimy im ochrony przed niepowoanym dostpem, co zwiksza ryzyko bdw. Wreszcie, mieszamy wtedy dwa style programowania, a to nieuchronnie prowadzi do baaganu w kodzie, jego niespjnoci, trudnoci w rozbudowie i konserwacji oraz caej rzeszy innych plag, przy ktrych te egipskie mog zdawa si dziecinn igraszk ;D Uywanie singletonw jest zatem nieodzowne. Przydaoby si wic znale jaki dobry sposb ich implementacji, bo chyba domylasz si, e zwyke zmienne globalne nie s tutaj szczytem marze. No, a jeli nawet nie zastanowie si nad tym, to wanie masz precedens porwnawczy - przedstawi bowiem nieco lepsz drog na realizacj pomysu pojedynczych obiektw w C++.
Pamitajmy, e zmienne zadeklarowane w pliku nagwkowym z uyciem extern wymagaj jeszcze przydzielenia do odpowiedniego moduu kodu poprzez deklaracj bez wspomnianego swka. Powyszy sposb nie jest zreszt najlepsz metod na zaimplementowanie singletonu - bardziej odpowiedni poznamy za chwil.
83
226
Podstawy programowania
Podstawow cech skadowych statycznych jest to, e do skorzystania z nich nie jest potrzebny aden obiekt macierzystej klasy. Odwoujemy si do nich, podajc po prostu nazw klasy oraz oznaczenie skadowej, w ten oto sposb: nazwa_klasy::skadowa_statyczna Moliwe jest take tradycyjne uycie obiektu danej klasy lub wskanika na niego oraz operatorw wyuskania . lub ->. We wszystkich przypadkach efekt bdzie ten sam. Musimy jakkolwiek pamita, e nadal obowizuj tutaj specyfikatory praw dostpu, wic jeli powyszy kod umiecimy poza metodami klasy, to bdzie on poprawny tylko dla skadowych zadeklarowanych jako public. Blisze poznanie statycznych elementw klas wymaga rozrnienia spord nich pl i metod. Dziaanie modyfikatora static jest bowiem nieco inne dla danych oraz dla kodu. I tak statyczne pola s czym w rodzaju zmiennych globalnych dla klasy. Mona si do nich odwoywa z kadej metody, a take z klas pochodnych i/lub z zewntrz - zgodnie ze specyfikatorami praw dostpu. Kade odniesienie do statycznego pola bdzie jednak dostpem do tej samej zmiennej, rezydujcej w tym samym miejscu pamici. W szczeglnoci poszczeglne obiekty danej klasy nie bd posiaday wasnej kopii takiego pola, bo bdzie ono istniao tylko w jednym egzemplarzu. Podobiestwo do zmiennych globalnych przejawia si w jeszcze jednym aspekcie: mianowicie statyczne pola musz zosta w podobny sposb przydzielone do ktrego z moduw kodu w programie. Ich deklaracja w klasie jest bowiem odpowiednikiem deklaracji extern dla zwykych zmiennych. Odpowiednia definicja w module wyglda za nastpujco: typ nazwa_klasy::nazwa_pola [= warto_pocztkowa]; Kwalifikatora nazwa_klasy:: moemy tutaj wyjtkowo uy nawet wtedy, kiedy nasze pole nie jest publiczne. Spostrzemy te, i nie korzystamy ju ze sowa static, jako e poza definicj klasy ma ono odmienne znaczenie. Statyczno metod polega natomiast na ich niezalenoci od jakiegokolwiek obiektu danej klasy. Metody opatrzone kwalifikatorem static moemy bowiem wywoywa bez koniecznoci posiadania instancji klasy. W zamian za to musimy jednak zaakceptowa fakt, i nie posiadamy dostpu do wszelkich niestatycznych skadnikw (zarwno pl, jak i metod) naszej klasy. To aczkolwiek do naturalne: jeli wywoanie funkcji statycznej moe obej si bez obiektu, to skd moglibymy go wzi, aby skorzysta z niestatycznej skadowej, ktra przecie takiego obiektu wymaga? Ot wanie nie mamy skd, gdy w metodach statycznych nie jest dostpny wskanik this, reprezentujcy aktualny obiekt klasy. No dobrze, ale w jaki sposb statyczne skadowe klas mog nam pomc w implementacji singletonw? C, to dosy proste. Zauwa, e takie skadowe s unikalne w skali caej klasy - tak samo, jak unikalny jest pojedynczy obiekt singletonu. Moemy zatem uy ich, by sprawowa kontrol nad naszym jedynym i wyjtkowym obiektem. Najpierw zadeklarujemy wic statyczne pole, ktrego zadaniem bdzie przechowywanie wskanika na w kluczowy obiekt: // *** plik nagwkowy *** // klasa singletonu class CSingleton { private:
Programowanie obiektowe
227
// statyczne pole, przechowujce wskanik na nasz jedyny obiekt static CSingleton* ms_pObiekt; // 84 }; // (tutaj bd dalsze skadowe klasy)
// *** modu kodu *** // trzeba rzecz jasna doczy tutaj nagwek z definicj klasy // inicjujemy pole wartoci zerow (NULL) CSingleton* CSingleton::ms_pObiekt = NULL; Deklaracj pola umiecilimy w sekcji private, aby chroni je przed niepowoan zmian. W takiej sytuacji potrzebujemy jednak metody dostpowej do niego, ktra zreszt take bdzie statyczna: // *** wewntrz klasy CSingleton *** public: static CSingleton* Obiekt() { // tworzymy obiekt, jeeli jeszcze nie istnieje // (tzn. jeli wskanik ms_pObiekt ma pocztkow warto NULL) if (ms_pObiekt == NULL) CSingleton(); // zwracamy wskanik na nasz obiekt return ms_pObiekt;
Oprcz samego zwracania wskanika metoda ta sprawdza, czy dany przez nasz obiekt faktycznie istnieje; jeeli nie, jest tworzony. Jego kreacja nastpuje wic przy pierwszym uyciu. Odbywa si ona poprzez bezporednie wywoanie konstruktora ktrego na razie nie mamy (jest domylny)! Czym prdzej naprawmy zatem to niedopatrzenie, przy okazji definiujc take destruktor: // *** wewntrz klasy CSingleton *** private: CSingleton() public: ~CSingleton() { ms_pObiekt = this; } { ms_pObiekt = NULL; }
Spore zdziwienie moe budzi niepubliczno konstruktora. W ten sposb jednak zabezpieczamy si przed utworzeniem wicej ni jednej kopii naszego singletonu. Uprawniona do wywoania prywatnego konstruktora jest bowiem tylko skadowa klasy, czyli metoda CSingleton::Obiekt(). Wszelkie zewntrzne prby stworzenia obiektu klasy CSingleton zakocz si wic bdem kompilacji, za jedyny jego egzemplarz bdzie dostpny wycznie poprzez wspomnian metod. Powyszy sposb jest zatem odpowiedni dla obiektu stojcego na samym szczycie hierarchii w aplikacji, a wic dla klas w rodzaju CApplication, CApp czy CGame. Jeeli za chcemy mie wygodny dostp do obiektw lecych niej, zawartych wewntrz innych, wtedy nie moemy oczywicie uczyni konstruktora prywatnym. Wwczas warto wic skorzysta z innych rozwiza, ktrych jednak nie chciaem tutaj przedstawia ze
84 Przedrostek s_ wskazuje, e dana zmienna jest statyczna. Tutaj zosta on poczony ze zwyczajowym m_, dodawanym do nazw prywatnych pl.
228
Podstawy programowania
wzgldu konieczno znacznie wikszej znajomoci jzyka C++ do ich poprawnego zastosowania85. Musimy jeszcze pamita, aby usun obiekt, gdy ju nie bdzie nam potrzebny - robimy to w zwyczajny sposb, poprzez operator delete: delete CSingleton::Obiekt(); To konieczne - skoro chcemy zachowa kontrol nad tworzeniem obiektu, to musimy take wzi na siebie odpowiedzialno za jego zniszczenie. Na koniec wypadaoby zastanowi si, czy stosowanie powyszego rozwizania (albo podobnych, gdy istnieje ich wicej) jest na pewno konieczne. By moe sdzisz, e mona si spokojnie bez nich oby - i chwilowo masz rzeczywicie racj! Kiedy nasze programy s zdeterminowane od pocztku do koca, zawarte w caoci w funkcji main(), atwo jest zapanowa nad yciem singletonu. Gdy jednak rozpoczniemy programowa aplikacje okienkowe dla Windows, sterowane zewntrznymi zdarzeniami, wtedy przebieg programu nie bdzie ju taki oczywisty. Powyszy sposb na implementacj singletonu bdzie wwczas znacznie uyteczniejszy.
Obiekty zasadnicze
Drugi rodzaj obiektw skupia te, ktre stanowi najwikszy oraz najwaniejszy fragment modelu w kadym programie. Obiekty zasadnicze s jego ywotn tkank, wykonujc wszelkie zadania przewidziane w aplikacji. Obiekty zasadnicze to gwny budulec programu stworzonego wedug zasad OOP. Wchodzc w zalenoci midzy sob oraz przekazujc dane, realizuj one wszystkie funkcje aplikacji. Budowanie sieci takich obiektw jest wic lwi czci procesu tworzenia obiektowej struktury programu. Definiowanie odpowiednich klas, zwizkw midzy nimi, korzystanie z dziedziczenia, metod wirtualnych i polimorfizmu - wszystko to dotyczy wanie obiektw zasadniczych. Zagadnienie ich waciwego stosowania jest zatem niezwykle szerokie zajmiemy si nim dokadniej w kolejnych paragrafach tego podrozdziau.
Obiekty narzdziowe
Ostatnia grupa obiektw jest oczkiem w gowie programistw, zajmujcych si jedynie klepaniem kodu wedle projektu ustalonego przez kogo innego. Z kolei owi projektanci w ogle nie zajmuj si nimi, koncentrujc si wycznie na obiektach zasadniczych. W swojej karierze jako twrcy oprogramowania bdziesz jednak czsto wciela si w obie role, dlatego znajomo wszystkich rodzajw obiektw z pewnoci okae si pomocna. Czym wic s obiekty nalece do opisywanego rodzaju? Naturalnie, najlepiej wyjani to odpowiednia definicja :D Obiekty narzdziowe, zwane te pomocniczymi lub konkretnymi86, reprezentuj pewien nieskomplikowany typ danych. Zawieraj pola suce przechowywaniu jego danych oraz metody do wykonywania na prostych operacji.
85
Jeden z najlepszych sposobw zosta opisany w rozdziale 1.3, Automatyczne singletony, ksiki Pereki programowania gier, tom 1. 86 Autorem tej ostatniej, dziwnej nazwy jest Bjarne Stroustrup i tylko dlatego j tutaj podaj :)
Programowanie obiektowe
229
Nazwa tej grupy obiektw dobrze oddaje ich rol: s one tylko pomocniczym konstrukcjami, uatwiajcymi realizacj niektrych algorytmw. Czsto zreszt traktuje si je podobnie jak typy podstawowe - zwaszcza w C++. Obiekty narzdziowe posiadaj wszake kilka znaczacych cech: istniej same dla siebie i nie wchodz w interakcje z innymi, rwnolegle istniejcymi obiektami. Mog je wprawdzie zawiera w sobie, ale nie komunikuj si samodzielnie z otoczeniem ich czas ycia jest ograniczony do zakresu, w ktrym zostay zadeklarowane. Zazwyczaj tworzy si je poprzez zmienne obiektowe, w takiej te postaci (a nie poprzez wskaniki) zwracaj je funkcje nierzadko zawieraj publiczne pola, jeeli moliwe jest ich bezpieczne ustawianie na dowolne wartoci. W takim wypadku typy narzdziowe definiuje si zwykle przy uyciu sowa struct, gdy uwalnia to od stosowania specyfikatora public, ktry w typach strukturalnych jest domylnym (w klasach, definiowanych poprzez class, domylne prawa to private; poza tym oba sowa kluczowe niczym si od siebie nie rni) posiadaj najczciej kilka konstruktorw, ale ich przeznaczenie ogranicza si zazwyczaj do wstpnego ustawienia pl na wartoci podane w parametrach. Destruktory s natomiast rzadko uywane - zwykle wtedy, gdy obiekt sam alokuje dodatkow pami i musi j zwolni metody obiektw narzedziowych s zwykle proste obliczeniowo i krtkie w zapisie. Ich implementacja jest wic umieszczana bezporednio w definicji klasy. Bezwzgldnie stosuje si te metody stae, jeeli jest to moliwe obiekty nalece do opisywanego rodzaju prawie nigdy nie wymagaj uycia dziedziczenia, a wic take metod wirtualnych i polimorfizmu jeeli ma to sens, na rzecz tego rodzaju obiektw dokonywane jest przeadowywanie operatorw, aby mogy by uyte w stosunku do nich. O tej technice programistycznej bdziemy mwi w jednym z dalszych rozdziaw nazewnictwo klas narzdziowych jest zwykle takie samo, jak normalnych typw sklarnych. Nie stosuje si wic zwyczajowego przedrostka C, a ca nazw zapisuje t sam wielkoci liter - maymi (jak w Bibliotece Standardowej C++) lub wielkimi (wedug konwencji Microsoftu) Bardzo wiele typw danych moe by reprezentowanych przy pomocy odpowiednich obiektw narzdziowych. Z jednym z takich obiektw masz zreszt stale do czynienia: jest nim typ std::string, bdcy niczym innym jak wanie klas, ktrej rol jest odpowiednie opakowanie acucha znakw w przyjazny dla programisty interfejs. Takie obudowywanie nazywamy enkapsulacj. Klasa ta jest take czci Standardowej Biblioteki Typw C++, ktr poznamy szczegowo po zakoczeniu nauki samego jzyka. Nale do niej take inne typy, ktre z pewnoci moemy uzna za narzdziowe, jak na przykad std::complex, reprezentujcy liczb zespolon czy std::bitset, bdcy cigiem bitw. Matematyka dostarcza zreszt najwikszej liczby kandydatw na potencjalne obiekty narzdziowe. Wystarczy pomyle o wektorach, macierzach, punktach, prostoktach, prostych, powierzchniach i jeszcze wielu innych pojciach. Nie s one przy tym jedynie obrazowym przykadem, lecz niedzownym elementem programowania - gier w szczeglnoci. Wikszo bibliotek zawiera je wic gotowe do uycia; sporo programistw definiuje dla jednak wasne klasy. Zobaczmy zatem, jak moe wyglda taki typ w przypadku trjwymiarowego wektora: #include <cmath> struct VECTOR3
230
{
Podstawy programowania
// wsprzdne wektora float x, y, z; // ------------------------------------------------------------------// konstruktory VECTOR3() VECTOR3(float fX, float fY, float fZ) { x = y = z = 0.0; } { x = fX; y = fY; z = fZ; }
// ------------------------------------------------------------------// metody float Dlugosc() const { return sqrt(x * x + y * y + z * z); } void Normalizuj() { float fDlugosc = Dlugosc(); // dzielimy kad wsprzdn przez dugo x /= fDlugosc; y /= fDlugosc; z /= fDlugosc;
// ------------------------------------------------------------------// // // // tutaj mona by si pokusi o przeadowanie operatorw +, -, *, /, =, +=, -=, *=, /=, == i != tak, eby przy ich pomocy wykonywa dziaania na wektorach. Poniewa na razie tego nie umiemy, wic musimy z tym poczeka :)
};
Najwicej kontrowersji wzbudza pewnie to, e pola x, y, z s publicznie dostpne. Ma to jednak solidne uzasadnienie: ich zmiana jest rzecz naturaln dla wektora, za zakres dopuszczalnych wartoci nie jest niczym ograniczony (mog nimi by dowolne liczby rzeczywiste). Ochrona, ktr zwykle zapewniamy przy pomocy metod dostpowych, byaby zatem niepotrzebnym porednikiem. Uycie powyszej klasy/struktury (jak kto woli) wymaga oczywicie utworzenia jej instancji. Przy prostym zestawie danych, jaki ona reprezentuje, nie potrzeba jednak powica pieczoowitej uwagi na tworzenie i niszczenie obiektw, zatem wystarcz nam zwyke zmienne obiektowe zamiast wskanikw. Nawet wicej - moemy potraktowa VECTOR3 identycznie jak typy wbudowane i napisa na przykad funkcj obliczajc oba rodzaje iloczynw wektorw: float IloczynSkalarny(VECTOR3 vWektor1, VECTOR3 vWektor2) { // iloczyn skalarany jest sum iloczynw odpowiednich wsprzdnych // obu wektorw return (vWektor1.x * vWektor2.x + vWektor1.y * vWektor2.y + vWektor1.z * vWektor2.z);
VECTOR3 IloczynWektorowy(VECTOR3 vWektor1, VECTOR3 vWektor2) { VECTOR3 vWynik; // iloczyn vWynik.x = vWynik.y = vWynik.z = wektorowy ma vWektor1.y * vWektor2.x * vWektor1.x * za to bardziej skomplikowan formuk :) vWektor2.z - vWektor2.y * vWektor1.z; vWektor1.z - vWektor1.x * vWektor1.z; vWektor2.y - vWektor2.x * vWektor1.y;
Programowanie obiektowe
231
return vWynik;
Te operacje maj zreszt niezliczone zastosowania w programowaniu trjwymiarowych gier, zatem ich implementacja ma gboki sens :) Spokojnie moemy w tych funkcjach pobiera i zwraca obiekty typu VECTOR3. Koszt obliczeniowy tych dziaa bdzie bowiem niemal taki sam, jak dla pojedynczych liczb. W przypadku parametrw funkcji stosujemy jednak referencje, ktre optymalizuj kod, uwalniajc od przekazania nawet tych skromnych kilkunastu bajtw. Zapoznamy si z nimi w nastpnym rozdziale. acuchy znakw czy wymysy matematykw to nie s naturalnie wszystkie koncepcje, ktre mona i trzeba realizowa jako obiekty narzdziowe. Do innych nale chociaby wszelkie reprezentacje daty i czasu, kolorw, numerw o okrelonym formacie oraz wszystkie pozostae, nieelementarne typy danych. Szczeglnym przypadkiem obiektw pomocniczych s tak zwane inteligentne wskaniki (ang. smart pointers). Ich zadaniem jest zapewnienie dodatkowej funkcjonalnoci zwykym wskanikom - obejmuje to na przykad zwolnienie wskazywanej przez nie pamici w sytuacjach wyjtkowych czy te zliczanie odwoa do opakowanych nimi obiektw.
Abstrakcja
Jeeli masz pomys na gr, aplikacj uytkow czy te jakikolwiek inny produkt programisty, to chyba najgorsz rzecz, jak moesz zrobi, jest natychmiastowe rozpoczcie jego kodowania. Susznie mwi si, e co nagle, to po diable; niezbdne jest wic stworzenie model abstrakcyjnego zanim przystpi si do waciwego programowania. Model abstrakcyjny powinien opisywa zaoone dziaanie programu bez precyzowania szczegw implementacyjnych.
232
Podstawy programowania
Sama nazwa wskazuje zreszt, e taki model powinien abstrahowa od kodu. Jego zadaniem jest bowiem odpowied na pytanie Co program ma robi?, a w przypadku technik obiektowych, Jakich klas bdzie do tego potrzebowa i jakie czynnoci bd przez nie wykonywane?. Tym kluczowym sprawom powicimy rzecz jasna nieco miejsca.
Identyfikacja klas
Klasy i obiekty stanowi skadniki, z ktrych budujemy program. Aby wic rozpocz t budow, naleaoby mie przynajmniej kilka takich cegieek. Trzeba zatem zidentyfikowa moliwe klasy w projekcie. Musz ci niestety zmartwi, gdy w zasadzie nie ma uniwersalnego i zawsze skutecznego przepisu, ktry pozwaby na wykrycie wszelkich klas, potrzebnych do realizacji programu. Nie powinno to zreszt dziwi: dzisiejsze programy dotykaj przecie prawie wszystkich nauk i dziedzin ycia, wic podanie niezawodnego sposobu na skonstruowanie kadej aplikacji jest zadaniem porwnywalnym z opracowaniem metody pisania ksiek, ktre zawsze bd bestsellerami, lub te krcenia filmw, ktre na pewno otrzymaj Oscara. To oczywicie nie jest moliwe, niemniej dziedzina informatyka powicona projektowaniu aplikacji (zwana inynieri oprogramowania) poczynia w ostatnich latach due postpy. Chocia nadal najlepsz gwarancj sukcesu jest posiadane dowiadczenie, intuicja oraz odrobina szczcia, to jednak pocztkujcy adept sztuki tworzenia programw (taki jak ty :)) nie pozostanie bez pomocy. Programowanie obiektowe zostao przecie wymylone wanie po to, aby uatwi nie tylko kodowanie programw, ale take ich projektowanie a na to skada si rwnie wynajdywanie klas odpowiednich dla realizowanej aplikacji. Ot sama idea OOPu jest tutaj sporym usprawnieniem. Postp, ktry ona przynosi, jest bowiem zwizany z oparciem budowy programu o rzeczowniki, zamiast czasownikw, waciwym programowaniu strukturalnemu. Mylenie kategoriami tworw, bytw, przedmiotw, urzdze - oglnie obiektw, jest naturalne dla ludzkiego umysu. Na rzeczownikach opiera si take jzyk naturalny, i to w kadej czci wiata. Rwnie w programowaniu bardziej intuicyjne jest podejcie skoncentrowane na wykonawcach czynnoci, a nie na czynnociach jako takich. Przykadowo, porwnaj dwa ponisze, abstrakcyjne kody: // 1. kod strukturalny hPrinter = GetPrinter(); PrintText (hPrinter, "Hello world!"); // 2. kod obiektowy pPrinter = GetPrinter(); pPrinter->PrintText ("Hello world!"); Mimo e oba wygldaj podobnie, to wyranie wida, e w kodzie strukturalnym waniejsza jest sama czynno drukowania, za jej wykonawca (drukarka) jest kwesti drugorzedn. Natomiast kod obiektowy wyranie j wyrnia, a wywoanie metody PrintText() mona przyrwna do wcinicia przycisku zamiast wykonywania jakiej mao trafiajcej do wyobrani operacji. Jeeli masz wtpliwo, ktre podejcie jest waciwsze, to pomyl, co zobaczysz, patrzc na to urzdzenie obok monitora - czynno (drukowanie) czy przedmiot (drukark)87? No, ale dosy ju tych lunych dygresji. Mielimy przecie zaj si poszukiwaniem waciwych klas dla naszych programw obiektowych. Odejcie od tematu w poprzednim
87
Oczywicie nie dotyczy to tych, ktrzy drukarki nie maj, bo oni nic nie zobacz :D
Programowanie obiektowe
233
akapicie byo jednak tylko pozorne, gdy niechccy znalelimy cakiem prosty i logiczny sposb, wspomagajcy identyfikacj klas. Mianowicie, powiedzielimy sobie, e OOP przesuwa rodek cikoci programowania z czasownikw na rzeczowniki. Te z kolei s take podstaw jzyka naturalnego, uywanego przez ludzi. Prowadzi to do prostego wniosku i jednoczenie drogi do cakiem dobrego rozwizania drczacego nas problemu: Skuteczn pomoc w poszukiwaniu klas odpowiednich dla tworzonego programu moe by opis jego funkcjonowania w jzyku naturalnym. Taki opis stosunkowo atwo jest sporzdzi, pomaga on te w uporzdkowaniu pomysu na program, czyli klarowanym wyraeniu, o co nam waciwie chodzi :) Przykad takiego raportu moe wyglda choby w ten sposb:
Program Graph jest aplikacj przeznaczon do rysowania wszelkiego rodzaju schematw i diagramw graficznych. Powinien on udostpnia szerok palet przykadowych ksztatw, uywanych w takich rysunkach: blokw, strzaek, drzew, etykiet tekstowych, figur geometrycznych itp. Edytowany przez uytkownika dokument powinien by ponadto zapisywalny do pliku oraz eksportowalny do kilku formatw plikw graficznych.
Nie jest to z pewnoci zbyt szczegowa dokumentacja, ale na jej podstawie moemy atwo wyrni spor ilo klas. Nale do nich przede wszystkim: dokument schemat rne rodzaje obiektw umieszczanych na schematach Warto te zauway, e powyszy opis ukrywa te nieco informacji o zwizkach midzy klasami, np. to, e schemat zawiera w sobie umieszczone przez uytkownika ksztaty. Zbir ten z pewnoci nie jest kompletny, ale stanowi cakiem dobre osignicie na pocztek. Daje te pewne dalsze wskazwki co do moliwych kolejnych klas, jakimi mog by poszczeglne typy ksztatw skadajcych si na schemat. Tak wic analiza opisu w jzyku naturalnym jest dosy efektywnym sposobem na wyszukiwanie potencjalnych klas, skadajcych si na program. Skuteczno tej metody zaley rzecz jasna w pewnym stopniu od umiejtnoci twrcy aplikacji, lecz jej stosowanie szybko przyczynia si take do podniesienia poziomu biegoci w projektowaniu programw. Analizowanie opisu funkcjonalnego programu nie jest oczywicie jedynym sposobem poszukiwania klas. Do pozostaych naley chociaby sprawdzanie klasycznej listy kontrolnej, zawierajcej czsto wystpujce klasy lub te prba okrelenia dziaania jakiej konkretnej funkcji i wykrycia zwizanych z ni klas.
Abstrakcja klasy
Kiedy ju w przyblieniu znamy kilka klas z naszej aplikacji, moemy sprbowa okreli je bliej. Pamitajmy przy tym, e definicja klasy skada si z dwch koncepcyjnych czci: publicznego interfejsu, dostpnego dla uytkownikw klasy prywatnej implementacji, okrelajcej sposb realizacji zachowa okrelonych w interfejsie Ca sztuk w modelowaniu pojedynczej klasy jest skoncentrowanie si na pierwszym z tych skadnikw, bdcym jej abstrakcj. Oznacza to zdefiniowanie roli, spenianej przez klas, bez dokadnego wgbiania si w to, jak bdzie ona t rol odgrywaa.
234
Podstawy programowania
Taka abstrakcja moe by rwnie przedstawiona w postaci krtkiego, najczciej jednozdaniowego opisu w jzyku naturalnym, np.:
Klasa Dokument reprezentuje pojedynczy schemat, ktry moe by edytowany przez uytkownika przy uyciu naszego programu.
Zauwamy, e powysze streszczenie nic nie mwi choby o formie, w jakiej nasz dokument-schemat bdzie przechowywany w pamici. Czy to bdzie bitmapa, rysunek wektorowy, zbir innych obiektw albo moe jeszcze co innego? Wszystkie te odpowiedzi mog by poprawne, jednak na etapie okrelania abstrakcji klasy s one poza obszarem naszego zainteresowania. Abstrakcja klasy jest okreleniem roli, jak ta klasa peni w programie. Jawne formuowanie opisu podobnego do powyszego moe wydawa si niepotrzebne, skoro i tak przecie bdzie on wymaga uszczegowienia. Posiadanie go daje jednak moliwo prostej kontroli poprawnoci definicji klasy. Jeeli nie spenia ona zaoonych rl, to najprawdopodobniej zawiera bdy.
Implementacja
Implementacja klasy wyznacza drog, po jakiej przebiega realizacja zada klasy, okrelonych w abstrakcji oraz przyblionych poprzez jej interfejs. Skadaj si na ni wszystkie wewntrzne skadniki klasy, niedostpne jej uytkownikw - a wic prywatne pola, a take kod poszczeglnych metod. Dogmaty cisej inynierii oprogramowania mwi, aby dokadne implementacje poszczeglnych metod (zwane specyfikacjami algorytmw) byy dokonywane jeszcze podczas projektowania programu. Do tego celu najczciej uywa si pseudokodu, o ktrym ju kiedy wspominaem. W nim zwykle zapisuje si wstpne wersje algorytmw metod. Jednak wedug mnie ma to sens chyba tylko wtedy, kiedy nad projektem pracuje wiele osb albo gdy nie jestemy zdecydowani, w jakim jzyku programowania bdziemy go ostatecznie realizowa. Wydaje si, e obie sytuacje na razie nas nie dotycz :)
Programowanie obiektowe
235
W praktyce wic implementacja klasy jest dokonywana podczas programowania, czyli po prostu pisania jej kodu. Mona by zatem spiera si, czy faktycznie naley ona jeszcze do procesu projektowania. Osobicie uwaam, e to po prostu jego przeduenie, praktyczna kontynuacja, realizacja - rnie mona to nazywa, ale generalnie chodzi po prostu o zaoszczdzenie sobie pracy. czenie projektowania z programowaniem jest w tym wypadku uzasadnione.
Odkadanie implementacji na koniec projektowania, w zasadzie na styk z kodowaniem programu, jest zwykle konieczne. Zaimplementowanie klasy oznacza przecie zadeklarowanie i zdefiniowanie wszystkich jej skadowych - pl i metod, publicznych i prywatnych. Do tego wymagana jest ju pena wiedza o klasie - nie tylko o tym, co ma robi, jak ma to robi, ale take o jej zwizkach z innymi klasami.
Dziedziczenie i zawieranie si
Pierwsze dwa typy relacji bdziemy rozpatrywa razem z tego wzgldu, i przy ich okazji czsto wystpuj pewne nieporzumienia. Nie zawsze jest bowiem oczywiste, ktrego z nich naley uy w niektrych sytuacjach. Postaram si wic rozwia te wtpliwoci, zanim jeszcze zdysz o nich pomyle ;)
Zwizek generalizacji-specjalizacji
Relacja ta jest niczym innym, jak tylko znanym ci ju dobrze dziedziczeniem. Generalizacja-specjalizacja (ang. is-a relationship) to po prostu bardziej uczona nazwa dla tego zwizku.
236
Podstawy programowania
W dziedziczeniu wystpuj dwie klasy, z ktrych jedna jest nadrzdna, za druga podrzdna. Ta pierwsza to klasa bazowa, czyli generalizacja; reprezentuje ona szeroki zbir jakich obiektw. Wrd nich mona jednak wyrni takie, ktre zasuguj na odrbny typ, czyli klas pochodn - specjalizacj.
Klasa bazowa jest czsto nazywana nadtypem, za pochodna - podtypem. Na schemacie bardzo dobrze wida, dlaczego :D Najistotniejsz konsekwencj uycia tego rodzaju relacji jest przejcie przez klas pochodn caej funkcjonalnoci, zawartej w klasie bazowej. Jako e jest ona jej bardziej szczegowym wariantem, moliwe jest te rozszerzenie odziedziczonych moliwoci, lecz nigdy - ich ograniczenie. Klasa pochodna jest wic po prostu pewnym rodzajem klasy bazowej.
Zwizek agregacji
Agregacja (ang. has-a relationship) sugeruje zawieranie si jednego obiektu w innym. Mwic inaczej, obiekt bdcy caoci skada si z okrelonej liczby obiektwskadnikw.
Przykadw na podobne zachowanie nie trzeba daleko szuka. Wystarczy chociaby rozejrze si po dysku twardym we wasnym komputerze: nie do, e zawiera on foldery
Programowanie obiektowe
i pliki, to jeszcze same foldery mog zawiera inne foldery i pliki. Podobne zjawisko wystpuje te na przykad dla kluczy i wartoci w Rejestrze Windows. Implementacja tej relacji w C++ oznacza umieszczenie w deklaracji obiektu agregatu pola, ktre bdzie reprezentowao jego skadnik, np.: // skadnik class CIngredient { /* ... */ }; // obiekt nadrzdny class CAggregate { private: // pole ze skadowym skadnikiem CIngredient* m_pSkladnik; public: // konstruktor i destruktor CAggregate() { m_pSkladnik = new CIngredient; } ~CAggregate() { delete m_pSkladnik; } }; Mona by tu take zastosowa zmienn obiektow, ale wtedy zwizek staby si obligatoryjny, czyli musia zawsze wystpowa. Natomiast w przypadku wskanika istnienie obiektu nie jest konieczne przez cay czas, wic moe by on tworzony i niszczony w razie potrzeby.
237
Trzeba jednak uwaa, aby po kadym zniszczeniu obiektu ustawia jego wskanik na warto NULL. W ten sposb bdziemy mogli atwo sprawdza, czy nasz skadnik istnieje, czy te nie. Unikniemy wic bdw ochrony pamici.
jest poprawne, wstawiajc oczywicie nazwy swoich klas w oznaczonych miejscach, np.:
Kwadrat jest rodzajem Figury. Samochd zawiera obiekt typu Koo.
Mamy wic kolejny przykad na to, e programowanie obiektowe jest bliskie ludzkiemu sposobowi mylenia, co moe nas tylko cieszy :)
Zwizek asocjacji
Najbardziej oglnym zwizkiem midzy klasami jest przyporzdkowanie, czyli wanie asocjacja (ang. uses-a relationship). Obiekty, ktrych klasy s poczone tak relacj, posiadaj po prostu moliwo wymiany informacji midzy sob podczas dziaania programu.
238
Podstawy programowania
Praktyczna realizacja takiego zwizku to zwykle uycie przynajmniej jednego wskanika, a najprostszy wariant wyglda w ten sposb: class CFoo { /* ... */ }; class CBar { private: // wskanik do poczonego obiektu klasy CFoo CFoo* m_pFoo; public: void UstanowRelacje(CFoo* pFoo) { m_pFoo = pFoo; } }; atwo tutaj zauway, e zawieranie si jest szczeglnym przypadkiem asocjacji dwch obiektw. Poczenie klas moe oczywicie przybiera znacznie bardziej pogmatwane formy, my za powinnimy je wszystkie dokadnie pozna :D Pomwmy wic o dwch aspektach tego rodzaju zwizkw: krotnoci oraz kierunkowoci.
Krotno zwizku
Pod dziwn nazw krotnoci kryje si po prostu liczba obiektw, biorcych udzia w relacji. Trzeba bowiem wiedzie, e przy asocjacji dwch klas moliwe s rne iloci obiektw, wystpujcych z kadej strony. Klasy s przecie tylko typami, z nich s dopiero tworzone waciwe obiekty, ktre w czasie dziaania aplikacji bd si ze sob komunikoway i wykonyway zadania programu. Moemy wic wyrni cztery oglne rodzaje krotnoci zwizku: jeden do jednego. W takim przypadku pojedynczemu obiektowi jednej z klas odpowiada rwnie pojedynczy obiekt drugiej klasy. Przyporzdkowanie jest zatem jednoznaczne. Z takimi relacjami mamy do czynienia bardzo czsto. Wemy na przykad dowoln list osb - uczniw, pracownikw itp. Kademu numerowi odpowiada tam jedno nazwisko oraz kade nazwisko ma swj unikalny numer. Podobnie dziaa te choby tablica znakw ANSI. jeden do wielu. Tutaj pojedynczy obiekt jednej z klas jest przyporzdkowany kilku obiektom drugiej klasy. Wyglda to podobnie, jak woenie skarpety do kilku szuflad naraz - by moe w prawdziwym wiecie byoby to trudne, ale w programowaniu wszystko jest moliwe ;) wiele do jednego. Ten rodzaj zwizku oznacza, e kilka obiektw jednej z klas jest poczonych z pojedynczym obiektem drugiej klasy. Dobrym przykadem s tu rozdziay w ksice, ktrych moe by wiele w jednej publikacji. Kady z nich jest jednak przynaleny tylko jednemu tomowi. wiele do wielu. Najbardziej rozbudowany rodzaj relacji to zczenie wielu obiektw od jednej z klas oraz wielu obiektw drugiej klasy. Wracajc do przykadu z ksikami moemy stwierdzi, e zwizek midzy autorem a jego dzieem jest wanie takim typem relacji. Dany twrca moe przecie napisa kilka ksiek, a jednoczenie jedno wydawnictwo moe by redagowane przez wielu autorw. Implementacja wielokrotnych zwizkw polega zwykle na tablicy lub innej tego typu strukturze, przechowujcej wskaniki do obiektw danej klasy. Dokadny sposb zakodowania relacji zaley rzecz jasna take od tego, jak ilo obiektw rozumiemy pod pojciem wiele
Programowanie obiektowe
Pojedyncze zwizki s natomiast z powodzeniem programowane za pomoc pl, bdcych wskanikami na obiekty.
239
Widzimy wic, e poznanie obsugi obiektw poprzez wskaniki w poprzednim rozdziale byo zdecydowanie dobrym pomysem :)
240
// (dalej definicje obu klas, jak w kodzie wyej)
Podstawy programowania
Po tym zabiegu kompilator bdzie ju wiedzia, e CBar jest typem (dokadnie klas) i pozwoli na zadeklarowanie odpowiedniego wskanika jako pola klasy CFoo. Niektrzy, by unikn takich sytuacji, od razu deklaruj deklaruj wszystkie klasy przed ich zdefiniowaniem. Widzimy wic, e zwizki dwukierunkowe, jakkolwiek wygodniejsze ni jednokierunkowe, wymagaj nieco wicej uwagi. S te zwykle mniej wydajne przy czeniu nim duej liczby obiektw. Prowadzi to do prostego wniosku: Nie naley stosowa zwizkw dwukierunkowych, jeeli w konkretnym przypadku wystarcz relacje jednokierunkowe. *** Projektowanie aplikacji nawet z uyciem technik obiektowych nie zawsze jest prostym zadaniem. Ten podrozdzia powinien jednak stanowi jak pomoc w tym zakresie. Nie da si jednak ukry, e praktyka jest zawsze najlepszym nauczycielem, dlatego zdecydowanie nie powiniene jej unika :) Samodzielne zaprojektowanie i wykonanie choby prostego programu obiektowego bdzie bardziej pouczajce ni lektura najobszerniejszych podrcznikw. Koczacy si podrozdzia w wielu miejscach dotyka zagadnie inynierii oprogramowania. Jeeli chciaby poszerzy swoj wiedz na ten temat (a warto), to zapraszam do Materiau Pomocniczego C, Podstawy inynierii oprogramowania.
Podsumowanie
Kolejny bardzo dugi i bardzo wany rozdzia :) Zawiera on bowiem dokoczenie opisu techniki programowania obiektowego. Rozpoczlimy od mechanizmu dziedziczenia oraz jego roli w ponownym wykorzystywaniu kodu. Zobaczylimy te, jak tworzy proste i bardziej zoone hierarchie klasy. Dalej byo nawet ciekawiej: dziki metodom wirtualnym i polimorfizmu przekonalimy si, e programowanie z uyciem technik obiektowych jest efektywniejsze i prostsze ni dotychczas. Na koniec zostae te obdarzony spor porcj informacji z zakresu projektowania aplikacji. Dowiedziae si wic o rodzajach obiektw, sposobach znajdowania waciwych klas oraz zwizkach midzy nimi. W nastpnym rozdziale - ostatnim w podstawowym kursie C++ - przypatrzymy si wskanikom jako takim, ju niekoniecznie w kontekcie OOPu. Pomwimy te o pamici, jej alokowaniu i zwalnianiu.
Pytania i zadania
Na kocu rozdziau nie moe naturalnie zabrakn odpowiedniego pakietu pyta oraz wicze :)
Programowanie obiektowe
241
Pytania
1. Na czym polega mechanizm dziedziczenia i jakie zjawisko jest jego gwnym skutkiem? 2. Jaka jest rnica midzy specyfikatorami praw dostpu do skadowych, private oraz protected? 3. Co nazywamy pask hierarchi klas? 4. Czym rni si metoda wirtualna od zwykej? 5. Co jest szczegoln cech klasy abstrakcyjnej? 6. Kiedy klasa jest typem polimorficznym? 7. Na czym polegaj polimorficzne zachowania klas w C++? 8. Co to jest RTTI? Na jakie dwa sposoby mechanizm ten umoliwia sprawdzenie klasy obiektu, na ktry wskazuje dany wskanik? 9. Jakie trzy rodzaje obiektw mona wyrni w programie? 10. Czym jest abstrakcja klasy, a czym jej implementacja? 11. Podaj trzy typy relacji midzy klasami.
wiczenia
1. Zaprojektuj dowoln, dwupoziomow hierarchi klas. 2. (Trudne) Napisz obiektow wersj gry Kko i krzyyk z rozdziau 1.5. Wskazwki: dobrym kandydatem na obiekt jest oczywicie plansza. Zdefiniuj te klas graczy, przechowujc ich imiona (niech program pyta si o nie na pocztku gry).
8
WSKANIKI
Im bardziej zaglda do rodka, tym bardziej nic tam nie byo.
A. A. Milne Kubu Puchatek
Dwa poprzednie rozdziay upyny nam na poznawaniu rnorodnych aspektw programowania obiektowego. Nawet teraz, w kilkanacie lat po powstaniu, jest ona czasem uwaana moe nie za awangard, ale powan nowo i odstpstwo od klasycznych regu programowania. Takie opinie, pojawiajce si oczywicie coraz rzadziej, s po czci echem dawnej popularnoci jzyka C. Fakt, e C++ zachowuje wszystkie waciwoci swego poprzednika, zdaje si usprawiedliwa podejcie, i s one waniejsze i bardziej znaczce ni dodatki wprowadzone wraz z dwoma plusami w nazwie jzyka. Do owych dodatkw ma rzecz jasna nalee programowanie obiektowe. Sprawia to, e ogromna wikszo kursw i podrcznikw jzyka C++ jest usystematyzowana wedle osobliwej zasady. Ot mwi ona, e najpierw naley wyoy wszystkie zagadnienia zwizane z C, a dopiero potem zaj si nowinkami, w ktre zosta wyposaony jego nastpca. Zastanawiajc si nad tym bliej, mona nieomal nabra wtpliwoci, czy w ten sposb nadal uczymy si przede wszystkim programowania, czy moe bardziej zajmuj nas ju kwestie formalne danego jzyka? Jeeli nawet nie odnosimy takiego wraenia, to nietrudno znale szczliwsze i bardziej naturalne drogi poznania tajnikw kodowania. Pamitajmy, e programowanie jest raczej praktyczn dziedzin informatyki, a jego nauka jest w duej mierze zdobywaniem umiejtnoci, a nie tylko samej wiedzy. Dlatego te wymaga ona mniej teoretycznego nastawienia, a wicej wytrwaoci w osiganiu coraz lepszego wtajemniczenia w zagadnienia programistyczne. Naturaln kolej rzeczy jest wic uszeregowanie tych zagadnie wedug wzrastajcego poziomu trudnoci czy te ze wzgldu na ich wiksz lub mniejsz uyteczno praktyczn. Takie te zaoenie przyjem w tym kursie. Nie chc sobie jednak robi autoreklamy twierdzc, e jest on inny ni wszystkie pozostae; mam nawet nadziej, e to okrelenie jest cakowit nieprawd i e istnieje jeszcze mnstwo innych publikacji, ktrych autorzy skupili si gwnie na nauczaniu programowania, a nie na opisywaniu jzykw programowania. Zatem zgodnie z powysz tez kwestie programowania obiektowego, jako niezwykle wane same w sobie, wprowadziem tak wczenie jak to tylko byo moliwe - nie przywizujc wagi to faktu, czy s one waciwe jeszcze jzykowi C, czy moe ju C++. Bardziej liczya si bowiem ich rzeczywista przydatno. Na tej samej zasadzie opieram si take teraz, gdy przyszed czas na szczegowe omwienie wskanikw. To rwnie wane zagadnienie, ktrego geneza nie wydaje si wcale tak bardzo istotna. Najwaniejsze, i s one czci jzyka C++, w dodatku jedn z kluczowych - chocia moe nie najprostszych. Umiejtno waciwego posugiwania si wskanikami oraz pamici operacyjn jest wic niebagatelna dla programisty C++. Opanowaniu przez ciebie tej umiejtnoci zosta powicony cay niniejszy rozdzia. Moesz wic do woli z niego korzysta :)
244
Podstawy programowania
Ku pamici
Wskaniki s cile zwizane z pamici komputera - a wic miejscem, w ktrym przechowuje on dane. Przydatne bdzie zatem przypomnienie sobie (a moe dopiero poznanie?) kilku podstawowych informacji na ten temat.
Rodzaje pamici
Mona wyrni wiele rodzajw pamici, jakimi dysponuje pecet, kierujc si rnymi przesankami. Najczciej stosuje si kryteria szybkoci i pojemnoci; s one wane nie tylko dla nas, programistw, ale praktycznie dla kadego uytkownika komputera. Nietrudno przy tym zauway, e s one ze sob wzajemnie powizane: im wiksza jest szybko danego typu pamici, tym mniej danych mona w niej przechowywa, i na odwrt. Nie ma niestety pamici zarwno wydajnej, jak i pojemnej - zawsze potrzebny jest jaki kompromis. Zjawisko to obrazuje poniszy wykres:
Zostay na nim umieszczone wszystkie rodzaje pamici komputera, jakimi si zaraz dokadnie przyjrzymy.
Rejestry procesora
Procesor jest jednostk obliczeniow w komputerze. Nieszczeglnie zatem kojarzy si z przechowywaniem danych w jakiej formie pamici. A jednak posiada on wasne jej zasoby, ktre s kluczowe dla prawidowego funkcjonowania caego systemu. Nazywamy je rejestrami. Kady rejestr ma posta pojedynczej komrki pamici, za ich liczba zaley gwnie od modelu procesora (generacji). Wielko rejestru jest natomiast potocznie znana jako bitowo procesora: najpopularniejsze obecnie jednostki 32-bitowe maj wic rejestry o wielkoci 32 bitw, czyli 4 bajtw. Ten sam rozmiar maj te w C++ zmienne typu int, i nie jest to bynajmniej przypadek :) Wikszo rejestrw ma cile okrelone znaczenie i zadania do wykonania. Nie s one wic przeznaczone do reprezentowania dowolnych danych, ktre by si we zmieciy. Zamiast tego peni rne wane funkcje w obrbie caego systemu. Ze wzgldu na wykonywane przez siebie role, wrd rejestrw procesora moemy wyrni:
Wskaniki
245
cztery rejestry uniwersalne (EAX, EBX, ECX i EDX88). Przy ich pomocy procesor wykonuje operacje arytmetyczne (dodawanie, odejmowanie, mnoenie i dzielenie). Niektre wspomagaj te wykonywanie programw, np. EAX jest uywany do zwracania wynikw funkcji, za ECX jako licznik w ptlach. Rejestry uniwersalne maj wic najwiksze znaczenie dla programistw (gwnie asemblera), gdy czsto s wykorzystywane na potrzeby ich aplikacji. Z pozostaych natomiast korzysta prawie wycznie sam procesor. Kady z rejestrw uniwersalnych zawiera w sobie mniejsze, 16-bitowe, a te z kolei po dwa rejestry omiobitowe. Mog one by modyfikowane niezalenie do innych, ale trzeba oczywicie pamita, e zmiana kilku bitw pociga za sob pewn zmian caej wartoci. rejestry segmentowe pomagaj organizowa pami operacyjn. Dziki nim procesor wie, w ktrej czci RAMu znajduje si kod aktualnie dziaajcego programu, jego dane itp. rejestry wskanikowe pokazuj na wane obszary pamici, jak choby aktualnie wykonywana instrukcja programu. dwa rejestry indeksowe s uywane przy kopiowaniu jednego fragmentu pamici do drugiego. Ten podstawowy zestaw moe by oczywicie uzupeniony o inne rejestry, jednak powysze s absolutnie niezbdne do pracy procesora. Najwaniejsz cech wszystkich rejestrw jest byskawiczny czas dostpu. Poniewa ulokowane s w samym procesorze, skorzystanie z nich nie zmusza do odbycia wycieczki wgb pamici operacyjnej i dlatego odbywa si wrcz ekspresowo. Jest to w zasadzie najszybszy rodzaj pamici, jakim dysponuje komputer. Cen za t szybko jest oczywicie znikoma objto rejestrw - na pewno nie mona w nich przechowywa zoonych danych. Co wicej, ich panem i wadc jest tylko i wycznie sam procesor, zatem nigdy nie mona mie pewnoci, czy zapisane w nich informacje nie zostan zastpione innymi. Trzeba te pamita, e nieumiejtne manipulowanie innymi rejestrami ni uniwersalne moe doprowadzi nawet do zawieszenia komputera; na tak niskim poziomie nie ma ju bowiem adnych komunikatw o bdach
88
246
Podstawy programowania
Dostp do rejestrw
Rejestry procesora, jako zwizane cisle ze sprztem, s rzecz niskopoziomow. C++ jest za jzykiem wysokiego poziomu i szczyci si niezalenoci od platformy sprztowej. Powoduje to, i nie posiada on adnych specjalnych mechanizmw, pozwalajcych odczyta lub zapisywa dane do rejestrw procesora. Zdecydowaa o tym nie tylko przenono, ale i bezpieczestwo - mieszanie w tak zaawansowanych obszarach systemu moe bowiem przynie sporo szkody. Jedynym sposobem na uzyskanie dostpu do rejestrw jest skorzystanie z wstawek asemblerowych, ujmowanych w bloki __asm. Mona o nich przeczyta w MSDN; uywajc ich trzeba jednak mie wiadomo, w co si pakujemy :)
Pami operacyjna
Do sensownego funkcjonowania komputera potrzebne jest miejsce, w ktrym mgby on skadowa kod wykonywanych przez siebie programw (obejmuje to take system operacyjny) oraz przetwarzane przez nie dane. Jest to stosunkowo spora ilo informacji, wic wymaga znacznie wicej miejsca ni to oferuj rejestry procesora. Kady komputer posiada wic osobn pami operacyjn, przeznaczon na ten wanie cel. Nazywamy j czsto angielskim skrtem RAM (ang. random access memory - pami o dostpie bezporednim).
Fotografia 2. Kilka koci RAM typu DIMM (zdjcie pochodzi z serwisu Toms Hardware Guide)
Rzeczywicie jest to najwaniejsza cz tej pamici (sama zwana jest czasem pamici fizyczn), ale na pewno nie jedyna. Obecnie wiele podzespow komputerowych posiada wasne zasoby pamici operacyjnej, przystosowane do wykonywania bardziej specyficznych zada. W szczeglnoci dotyczy to kart graficznych i dwikowych, zoptymalizowanych do pracy z waciwymi im typami danych. Ilo pamici, w jak s wyposaane, systematycznie ronie.
Wskaniki
247
Pami wirtualna
Istnieje jeszcze jedno, przebogate rdo dodatkowej pamici operacyjnej: jest nim dysk twardy komputera, a cilej jego cz zwana plikiem wymiany (ang. swap file) lub plikiem stronnicowania (ang. paging file). Obszar ten suy systemowi operacyjnemu do udawania, i ma pokanie wicej pamici ni posiada w rzeczywistoci. Wanie dlatego tak symulowan pami nazywamy wirtualn. Podobny zabieg jest niewtpliwie konieczny w rodowisku wielozadaniowym, gdzie naraz moe by uruchomionych wiele programw. Chocia w danej chwili pracujemy tylko z jednym, to pozostae mog nadal dziaa w tle - nawet wwczas, gdy czna ilo potrzebnej im pamici znacznie przekracza fizyczne moliwoci komputera. Cen za ponadplanowe miejsce jest naturalnie wydajno. Dysk twardy charakteryzuje si duszym czasem dostpu ni ukady RAM, zatem wykorzystanie go jako pamici operacyjnej musi pocign za sob spowolnienie dziaania systemu. Dzieje si jednak tylko wtedy, gdy uruchamiamy wiele aplikacji naraz. Mechanizm pamici wirtualnej, jako niemal niezbdny do dziaania kadego nowoczesnego systemu operacyjnego, funkcjonuje zazwyczaj bardzo dobrze. Mona jednak poprawi jego osigi, odpowiednio ustawiajc pewne opcje pliku wymiany. Przede wszystkim warto umieci go na nieuywanej zwykle partycji (Linux tworzy nawet sam odpowiedni partycj) i ustali stay rozmiar na mniej wicej dwukrotno iloci posiadanej pamici fizycznej.
Pami trwaa
Przydatno komputerw nie wykraczaaby wiele poza zastosowania kalkulatorw, gdyby swego czasu nie wynaleziono sposobu na trwae zachowywanie informacji midzy kolejnymi uruchomieniami maszyny. Tak narodziy si dyskietki, dyski twarde, zapisywalne pyty CD, przenone noniki dugopisowe i inne media, suce do dugotrwaego magazynowania danych. Spord nich na najwicej uwagi zasuguj dyski twarde, jako e obecnie s niezbdnym elementem kadego komputera. Zwane s czasem pamici trwa (z wyjanionych wyej wzgldw) albo masow (z powodu ich duej pojemnoci). Moliwo zapisania duego zbioru informacji jest aczkolwiek okupiona lamazarnoci dziaania. Odczytywanie i zapisywanie danych na dyskach magnetycznych trwa bowiem zdecydowanie duej ni odwoanie do komrki pamici operacyjnej. Ich wykorzystanie ogranicza si wic z reguy do jednorazowego wczytywania duych zestaww danych (na przykad caych plikw) do pamici operacyjnej, poczynienia dowolnej iloci zmian oraz powtrnego, trwaego zapisania. Wszelkie operacje np. na otwartych dokumentach s wic w zasadzie dokonywane na ich kopiach, rezydujcych wewntrz pamici operacyjnej. Nie zajmowalimy si jeszcze odczytem i zapisem informacji z plikw na dysku przy pomocy kodu C++. Nie martw si jednak, gdy ostatecznie poznamy nawet wicej ni jeden sposb na dokonanie tego. Pierwszy zdarzy si przy okazji omawiania strumieni, bdcych czci Biblioteki Standardowej C++.
248
Podstawy programowania
Adresowanie pamici
Wygodnie jest wyobraa sobie pami operacyjn jako co w rodzaju wielkiej tablicy bajtw. W takiej strukturze kady element (zmiemy go komrk) powinien da si jednoznacznie identyfikowa poprzez swj indeks. I tutaj rzeczywicie tak jest - numer danego bajta w pamici nazywamy jego adresem. W ten sposb dochodzimy te do pojcia wskanika: Wskanik (ang. pointer) jest adresem pojedynczej komrki pamici operacyjnej. Jest to wic w istocie liczba, interpretowana jako unikalny indeks danego miejsca w pamici. Specjalne znaczenie ma tu jedynie warto zero, interpretowana jako wskanik pusty (ang. null pointer), czyli nieodnoszcy si do adnej konkretnej komrki pamici. Wskaniki su wic jako cz do okrelonych miejsc w pamici operacyjnej; poprzez nie moemy odwoywa si do tyche miejsc. Bdziemy rwnie potrafili pobiera wskaniki na zmienne oraz funkcje, zdefiniowane we wasnych aplikacjach, i wykonywa przy ich pomocy rne wspaniae rzeczy :) Zanim jednak zajmiemy si bliej samymi wskanikami w jzyku C++, powimy nieco uwagi na to, w jaki sposb systemy operacyjne zajmuj si organizacj i systematyzacj pamici operacyjnej - czyli jej adresowaniem. Pomoe nam to lepiej zrozumie dziaanie wskanikw.
Schemat 31. Segmentowe adresowanie pamici. Adres zaznaczonej komrki zapisywano zwykle jako 012A:0007, a wic oddzielajc dwukropkiem numer segmentu i offset (oba zapisane w systemie szesnastkowym). Do ich przechowywania potrzebne byy dwie liczby 16-bitowe.
Moe nie wydaje si to wielk niedogodnoci, ale naprawd ni byo. Przede wszystkim niemoliwe byo operowanie na danych o rozmiarze wikszym ni owe 64 kB (a wic chociaby na dugich napisach). Chodzi te o fakt, i to programista musia martwi si o rozmieszczenie kodu oraz danych pisanego programu w pamici operacyjnej. Czas pokaza, e obowizek ten z powodzeniem mona przerzuci na kompilator - co zreszt wkrtce stao si moliwe.
Wskaniki
249
Schemat 32. Idea paskiego modelu pamici. Adresy skadaj si tu tylko z offsetw, przechowywanych jako liczby 32-bitowe. Mog one odnosi si do jakiegokolwiek rzeczywistego rodzaju pamici, na przykad do takich jak na ilustracji.
W Windows dodatkowo kady proces (program) posiada swoj wasn przestrze adresow, niedostpn dla innych. Wymiana danych moe wic zachodzi jedynie poprzez dedykowane do tego mechanizmy. Bdziemy o nich mwi, gdy ju przejdziemy do programowania aplikacji okienkowych. Przy takim modelu pamici porwnanie jej do ogromnej, jednowymiarowej tablicy staje si najzupeniej suszne. Wskaniki mona sobie wtedy cakiem dobrze wyobraa jako indeksy tej tablicy.
Stos i sterta
Na koniec wspomnimy sobie o dwch wanych dla programistw rejonach pamici operacyjnych, a wic wanie o stosie oraz stercie.
250
Podstawy programowania
Na stosie egzystuj wszystkie zmienne zadeklarowane jawnie w kodzie (szczeglne te lokalne w funkcjach), jest on take uywany do przekazywania parametrw do funkcji. Faktycznie wic mona by w ogle nie wiedzie o jego istnieniu. Czasem jednak objawia si ono w do nieprzyjemny sposb: poprzez bd przepenienia stosu (ang. stack overflow). Wystpuje on zwykle wtedy, gdy nastpi zbyt wiele wywoa funkcji.
O stercie
Reszta pamici operacyjnej nosi oryginaln nazw sterty. Sterta (ang. heap) to caa pami dostpna dla programu i mogca by mu przydzielona do wykorzystania. Czytajc oba opisy (stosu i sterty) pewnie trudno jest wychwyci midzy nimi jakie rnice, jednak w rzeczywistoci s one cakiem spore. Przede wszystkim, rozmiar stosu jest ustalany raz na zawsze podczas kompilacji programu i nie zmienia si w trakcie jego dziaania. Wszelkie dane, jakie s na nim przechowywane, musz wic mie stay rozmiar - jak na przykad skalarne zmienne, struktury czy te statyczne tablice. Kontrol pamici sterty zajmuje si natomiast sam programista i dlatego moe przyzna swojej aplikacji odpowiedni jej ilo w danej chwili, podczas dziaania programu. Jest to bardzo dobre rozwizanie, kiedy konieczne jest przetwarzanie zbiorw informacji o zmiennym rozmiarze. Terminy stos i sterta maj w programowaniu jeszcze jedno znaczenie. Tak mianowicie nazywaj si dwie czsto wykorzystywane struktury danych. Omwimy je przy okazji poznawania Biblioteki Standardowej C++. *** Na tym zakoczymy ten krtki wykad o samej pamici operacyjnej. Cz tych wiadomoci bya niektrym pewnie doskonale znana, ale chyba kady mia okazj dowiedzie si czego nowego :) Wiedza ta bdzie nam teraz szczeglnie przydatna, gdy rozpoczynamy wreszcie zasadnicz cz tego rozdziau, czyli omwienie wskanikw w jzyku C++: najpierw na zmienne, a potem wskanikw na funkcje.
Wskaniki na zmienne
Trudno zliczy, ile razy stosowalimy zmienne w swoich programach. Takie statystyki nie maj zreszt zbytniego sensu - programowanie bez uycia zmiennych jest przecie tym samym, co prowadzenie samochodu bez korzystania z kierownicy ;D Wiele razy przypominaem te, e zmienne rezyduj w pamici operacyjnej. Mechanizm wskanikw na nie jest wic zupenie logiczn konsekwencj tego zjawiska. W tym podrozdziale zajmiemy si wanie takimi wskanikami.
Wskaniki
251
sowy, kompilator musi zna odpowied na pytanie: Jakiego rodzaju jest zmienna, na ktr pokazuje dany wskanik?. Dziki temu potrafi zachowywa kontrol nad typami danych w podobny sposb, w jaki czyni to w stosunku do zwykych zmiennych. Obejmuje to take rzutowanie midzy wskanikami, o ktrym te sobie powiemy. Wiedzc o tym, spjrzmy teraz na ten elementarny przykad deklaracji oraz uycia wskanika: // deklaracja zmiennej typu int oraz wskanika na zmienne tego typu int nZmienna = 10; int* pnWskaznik; // nasz wskanik na zmienne typu int // przypisanie adresu zmiennej do naszego wskanika i uycie go do // wywietlenia jej wartoci w konsoli pnWskaznik = &nZmienna; // pnWskaznik odnosi si teraz do nZmienna std::cout << *pnWskaznik; // otrzymamy 10, czyli warto zmiennej Dobra wiadomo jest taka, i mimo prostoty ilustruje on wikszo zagadnie zwizanych ze wskanikami na zmiennej. Nieco gorsz jest pewnie to, e owa prostota moe dla niektrych nie by wcale taka prosta :) Naturalnie, wyjanimy sobie po kolei, co dzieje si w powyszym kodzie (chocia komentarze mwi ju cakiem sporo). Oczywicie najpierw mamy deklaracj zmiennej (z inicjalizacj), lecz nas interesuje bardziej sposb zadeklarowania wskanika, czyli: int* pnWskaznik; Poprzez dodanie gwiazdki (*) do nazwy typu int informujemy kompilator, e oto nie ma ju do czynienia ze zwyk zmienn liczbow, ale ze wskanikiem przeznaczonym do przechowywania adresu takiej zmiennej. pWskaznik jest wic wskanikiem na zmienne typu int, lub, krcej, wskanikiem na (typ) int. A zatem mamy ju zmienn, mamy i wskanik. Przydaoby si zmusi je teraz do wsppracy: niech pWskaznik zacznie odnosi si do naszej zmiennej! Aby tak byo, musimy pobra jej adres i przypisa go do wskanika - o tak: pnWskaznik = &nZmienna; Zastosowany tutaj operator & suy wanie w tym celu - do uzyskania adresu miejsca w pamici, gdzie egzystuje zmienna. Potem rzecz jasna zostaje on zapisany w pnWskaznik; odtd wskazuje on wic na zmienn nZmienna. Na koniec widzimy jeszcze, e za porednictwem wskanika moemy dosta si do zmiennej i uy jej w ten sam sposb, jaki znalimy dotychczas, choby do wypisania jej wartoci w oknie konsoli: std::cout << *pnWskaznik; Jak z pewnoci przypuszczasz, operator * nie dokonuje tutaj mnoenia, lecz podejmuje warto zmiennej, z ktr poczony zosta pnWskaznik; nazywamy to dereferencj wskanika. W jej wyniku otrzymujemy na ekranie liczb, ktr oryginalnie przypisalimy do zmiennej nZmienna. Bez zastosowania wspomnianego operatora zobaczylimy warto wskanika (a wic adres komrki w pamici), nie za warto zmiennej, na ktr on pokazuje. To oczywicie wielka rnica.
252
Podstawy programowania
Zaprezentowana prbka kodu faktycznie realizuje zatem zadanie wywietlenia wartoci zmiennej nZmienna w icie okrny sposb. Zamiast bezporedniego przesania jej do strumienia wyjcia posugujemy si w tym celu dodatkowym porednikiem w postaci wskanika. Samo w sobie moe to budzi wtpliwoci co do sensownoci korzystania ze wskanikw. Pomylmy jednak, e majc wskanik moemy umoliwi dostp do danej zmiennej z jakiegokolwiek miejsca programu - na przykad z funkcji, do ktrej przekaemy go jako parametr (w kocu to tylko liczba!). Potrafimy wtedy zaprogramowa kad czynno (algorytm) i zapewni jej wykonanie w stosunku do dowolnej iloci zmiennych, piszc odpowiedni kod tylko raz. Wicej przekonania do wskanikw na zmiennej nabierzesz wwczas, gdy poznasz je bliej - i temu wanie zadaniu powicimy teraz uwag.
Deklaracje wskanikw
Stwierdzilimy, e wskaniki mog z powodzeniem odnosi si do zmiennych - albo oglnie mwic, do danych w programie. Czyni to poprzez przechowywanie numeru odpowiedniej komrki w pamici, a zatem pewnej wartoci. Sprawia to, e wskaniki s w rzeczy samej take zmiennymi. Wskaniki w C++ to zmienne nalece do specjalnych typw wskanikowych. Taki typ atwo pozna po obecnoci przynajmniej jednej gwiazdki w jego nazwie. Jest nim wic choby int* - typ zmiennej pWskaznik z poprzedniego przykadu. Zawiera on jednoczenie informacj, na jaki rodzaj danych bdzie nasz wskanik pokazywa - tutaj jest to int. Typ wskanikowy jest wic typem pochodnym, zdefiniowanym na podstawie jednego z ju wczeniej istniejcych. To definiowanie moe si odbywa ad hoc, podczas deklarowania konkretnej zmiennej (wskanika) - tak byo w naszym przykadzie i tak te postpuje si najczciej. Dozwolone (i przydatne) jest aczkolwiek stworzenie aliasw na typy wskanikowe poprzez instrukcj typedef; standardowe nagwki systemu Windows zawieraj na przykad wiele takich nazw. Deklarowanie wskanikw jest zatem niczym innym, jak tylko wprowadzeniem do kodu nowych zmiennych - tyle tylko, i maj one swoiste przeznaczenie, inne ni reszta ich licznych wspbraci. Czynno ich deklarowania, a take same typy wskanikowe zasuguj przeto na szersze omwienie.
Wskaniki
253
jeeli uywaj oni innego sposobu deklarowania wskanikw ni nasz. Dlatego te wielokrotnie prbowano ustali jaki jeden, suszny wariant w tej materii i w zasadzie nigdy si to nie udao! Podobnie rzecz ma si take z umieszczaniem nawiasw klamrowych po instrukcjach if, else oraz nagwkach ptli. Jeli wic chodzi o dwa ostatnie sposoby, to generalnie prawie nikt nich nie uywa i raczej nie jest to niespodziank. Nieuywanie spacji czyni instrukcj mao czyteln, za ich obecno po obu stronach znaku * nieodparcie przywodzi na myl mnoenie, a nie deklaracj zmiennej. Co do dwch pierwszych metod, to w kwestii ich uywania panuje niczym niezmcona dowolno Powanie! W kodach, jakie spotkasz, na pewno bdziesz mia okazj zobaczy obie te skadnie. Argumenty stojce za ich wykorzystaniem s niemal tak samo silne w przypadku kadej z nich - tak przynajmniej twierdz ich zwolennicy. Temu problemowi powicony jest nawet fragment FAQ autora jzyka C++. Zauwaye by moe, i w tym kursie uywam pierwszej konwencji i bd si tego konsekwentnie trzyma. Nie chc jednak nikomu jej narzuca; najlepiej bdzie, jeli sam wypracujesz sobie odpowiadajcy ci zwyczaj i, co najwaniejsze, bdziesz go konsekwentnie przestrzega. Nie ma bowiem nic gorszego ni niespjny kod. Z opisywanym problemem wie si jeszcze jeden dylemat, powstajcy gdy chcemy zadeklarowa kilka zmiennych - na przykad tak: int* a, b; Czy w ten sposb otrzymamy dwa wskaniki (zmienne typu int*)? Pozostawiam to zainteresowanym do samodzielnego sprawdzenia89. Odpowied nie jest taka oczywista, jak by si to wydawao na pierwszy rzut oka, zatem stosowanie takiej konstrukcji pogarsza czytelno kodu i moe by przyczyn bdw. Czuje si wic w obowizku przestrzec przed ni: Nie prbuj deklarowa kilku wskanikw w jednej instrukcji, oddzielajc je przecinkami. Trzeba niestety przyzna, e jzyk C++ zawiera w sobie jeszcze kilka podobnych niejasnoci. Bd zwraca na nie uwag w odpowiednim czasie i miejscu.
Wskaniki do staych
Wskaniki maj w C++ pewn, do oryginaln cech. Mianowicie, nierzadko aplikuje si do nich modyfikator const, a mimo to cay czas moemy je nazywa zmiennymi. Dodatkowo, w modyfikator moe by do zastosowany a na dwa rne sposoby. Pierwszy z nich zakada poprzedzenie nim caej deklaracji wskanika, co wyglda mniej wicej tak: const int* pnWskaznik; const, jak wiemy, zmienia nam zmienn w sta. Tutaj mamy jednak do czynienia ze wskanikiem na zmienn, zatem dziaanie modyfikatora powoduje jego zmian we wskanik na sta :)
89
254
Podstawy programowania
Wskanik na sta (ang. pointer to constant) pokazuje na warto, ktra moe by poprzez ten wskanik jedynie odczytywana. Przypatrzmy si, jak wskanik na sta moe by wykorzystany w przykadowym kodzie: // deklaracja zmiennej i wskanika do staej float fZmienna = 3.141592; const float* pfWskaznik; // zwizanie zmiennej ze wskanikiem pfWskaznik = &fZmienna; // pokazanie wartoci zmiennej poprzez wskanik std::cout << *pfWskaznik; Przykad ten jest podobny do poprzedniego: za porednictwem wskanika odczytujemy tu warto zmiennej. Dozwolne jest zatem, aby w wskanik by wskanikiem na sta jako taki wic go deklarujemy: const float* pfWskaznik; Ronica, jak czyni modyfikator const, ujawni si przy prbie zapisania wartoci do zmiennej, na ktr pokazuje wskanik: *pfWskaznik = 1.0; // BD! pfWskaznik pokazuje na sta warto
Kompilator nie pozwoli na to. Decydujc si na zadeklarowanie wskanika na sta (tutaj typu const float*) uznalimy bowiem, e bdziemy tylko odczytywa warto, do ktrej si on odnosi. Zapisywanie jest oczywicie pogwaceniem tej zasady. Powysza linijka byaby rzecz jasna poprawna, gdyby pfWskaznik by zwykym wskanikiem typu float*. Jeeli wskanik na sta jest dodatkowo wskanikiem na obiekt, to na jego rzecz moliwe jest wywoanie jedynie staych metod. Nie modyfikuj one bowiem pl obiektu. Wskanik na sta umoliwia wic zabezpieczenie przed niepodan modyfikacj wartoci, na ktr wskazuje. Z tego wzgledu jest dosy czsto wykorzystywany w praktyce, chociaby przy przekazywaniu parametrw do funkcji.
Stae wskaniki
Druga moliwo uycia const powoduje nieco inny efekt. Odmienne jest wwczas take umiejscowienie modyfikatora w deklaracji wskanika: float* const pfWskaznik; Takie ustawienie powoduje mianowicie zadeklarowanie staego wskanika zamiast wskanika na sta. Stay wskanik (ang. const(ant) pointer) jest nieruchomy, na zawsze przywizany do jednego adresu pamici. Ten jeden jedyny i niezmienny adres moemy okreli tylko podczas inicjalizacji wskanika: float fA;
Wskaniki
float* const pfWskaznik = &fA; Wszelkie pniejsze prby zwizania wskanika z inn komrk pamici (czyli inn zmienn) skocz si niepowodzeniem: float fB; pfWskaznik = &fB; // BD! pfWskaznik jest staym wskanikiem
255
Zadeklarowanie staego wskanika jest bowiem umow z kompilatorem, na mocy ktrej zobowizujemy si nie zmienia adresu, do ktrego tene wskanik pokazuje. Pole zastosowa staych wskanikw jest, przyznam szczerze, raczej wskie. Mimo to mielimy ju okazj korzysta z tego rodzaju wskanikw - i to niejednokrotnie. Gdzie? Ot staym wskanikiem jest this, ktry, jak pamitamy, pokazuje wewntrz metod klasy na aktualny jej obiekt. Nie ogranicza on w aden sposb dostpu do tego obiektu, jednak nie pozwala na zmian samego wskazania; jest wic trwale zwizany z tym obiektem. Typem wskanika this wewntrz metod klasy klasa jest wic klasa* const. W przypadku staych metod wskanik this nie pozwala take na modyfikacj pl obiektu, a zatem wskazuje na sta. Jego typem jest wtedy const klasa* const, czyli mikst obu rodzajw staoci wskanika.
Czy jest jaki prosty sposb na zapamitanie, ktra deklaracja odpowiada jakiemu rodzajowi wskanikw? No c, moe nie jest to banalne, ale w pewien sposb zawsze mona sobie pomc. Przede wszystkim patrzmy na fraz bezporednio za modyfikatorem const. Dla staych wskanikw (przypominam, e to te, ktre zawsze wskazuj na to samo miejsce w pamici) deklaracja wyglda tak: typ* const wskanik; Bezporednio po sowie const mamy wic nazw wskanika, co razem daje const wskanik. W wolnym tumaczeniu znaczy to oczywicie stay wskanik :)
256
Podstawy programowania
W przypadku wskanikw na stae forma deklaracji przedstawia si nastpujco: const typ* wskanik; Uywamy tu const w ten sam sposb, w jaki ze zmiennych czynimy stae. W tym przypadku mamy rzecz jasna do czynienia ze wskanikiem na zmienn, a poniewa const przemienia nam zmienn w sta, wic ostatecznie otrzymujemy wskanik na sta. Potwierdzenia tego moemy szuka w tabelce.
Niezbdne operatory
Na wszelkich zmiennych mona w C++ wykonywa jakie operacje i wskaniki nie s w tym wzgldnie adnym wyjtkiem. Posiadaj nawet wasne instrumentarium specjalnych operatorw, dokonujcych na nich pewnych szczeglnych dziaa. To na nich wanie skupimy si teraz.
Wskaniki
257
Argumentem operatora jest naturalnie wskanik, przechowujcy adres miejsca w pamici, do ktrego chcemy si dosta. W wyniku dziaania tego operatora otrzymujemy moliwo odczytania oraz ewentualnie zapisania tam jakiej wartoci. Typ tej wartoci musi si jednak zgadza z typem wskanika: jeeli u nas by to unsigned*, to po dereferencji zostanie typ unsigned, akceptujcy tylko dodatnie liczby cakowite. Podobnie z wyraenia *puWskaznik moemy skorzysta jedynie tam, gdzie dozwolone s tego rodzaju wartoci. Wyraenie *pWskaznik jest tu tak zwan l-wartoci (ang. l-value). Nazwa bierze si std, i taka warto moe wystpowa po lewej (ang. left) stronie operatora przypisania. Typowymi l-wartociami s wic zmienne, a w oglnoci s to wszystkie wyraenia, za ktrymi kryj si konkretne miejsca w pamici operacyjnej i ktre nie zostay opatrzone modyfikatorem const. Dla odrnienia, r-warto (ang. r-value) jest dopuszczalna tylko po prawej (ang. right) stronie operatora przypisania. Ta grupa obejmuje oczywicie wszystkie l-wartoci, a take liczby, znaki i ich acuchy (tzw. stae dosowne) oraz wyniki oblicze z uyciem wszelkiego rodzaju operatorw (wykorzystujcych tymczasowe obiekty). Pamitajmy, e zapisanie danych do komrki pokazywanej przez wskanik jest moliwe tylko wtedy, gdy nie jest on wskanikiem do staej. Natura operatorw & i * sprawia, e najlepiej rozpatrywa je cznie. Powiedzielimy sobie nawet, e ich funkcjonowanie jest sobie wzajemnie przeciwstawne. Ilustruje to dobrze poniszy diagram:
Warto rwnie wiedzie, e pobranie adresu zmiennej oraz dereferencja wskanika s moliwe zawsze, niezalenie od typu teje zmiennej czy te wskanika. Dopiero inne zwizane z tym operacje, takie jak zachowanie adresu w zmiennej wskanikowej lub zapisanie wartoci w miejscu, do ktrego odwouje si wskanik, moe napotyka ograniczenia zwizane z typami zmiennej i/lub stosowanego wskanika.
Wyuskiwanie skadnikw
Trzeci operator wskanikowy jest nam ju znany od wprowadzenia OOPu. Operator wyuskania -> (strzaka) suy do wybierania skadnikw obiektu, na ktry wskazuje wskanik. Pod pojciem obiektu kryje si tu zarwno instancja klasy, jak i typu strukturalnego lub unii. Poniewa znamy ju doskonale t konstrukcj, na prostym przykadzie przeledzimy jedynie zwizek tego operatora z omwionymi przed chwil & i *. Zamy wic, e mamy tak oto klas:
258
Podstawy programowania
class CFoo { public: int Metoda() const { return 1; } }; Tworzc dynamicznie jej instancj przy uyciu wskanika, moemy wywoa skadowe metody: // stworzenie obiektu CFoo* pFoo = new CFoo; // wywoanie metody std::cout << pFoo->Metoda(); pFoo jest tu wskanikiem, takim samym jak te, z ktrych korzystalimy dotd; wskazuje na typ zoony - obiekt. Wykorzystujc operator -> potrafimy dosta si do tego obiektu i wywoa jego metod, co te niejednokrotnie czynilimy w przeszoci. Zwrmy jednakowo uwag, e ten sam efekt osignlibymy dokonujc dereferencji naszego wskanika i stosujc drugi z operatorw wyuskania - kropk: // inna metoda wywoania metody Metoda() ;D (*pFoo).Metoda(); // zniszczenie obiektu delete pFoo; Nawiasy pozwalaj nie przejmowa si tym, ktry z operatorw: * czy . ma wyszy priorytet. Ich wykorzystywanie jest wic zawsze wskazane, o czym zreszt nie raz wspominam :) Analogicznie, mona instancjowa obiekt poprzez zmienn obiektow i mimo to uywa operatora -> celem dostpu do jego skadowych: // zmienna obiektowa CFoo Foo; // obie ponisze linijki robi to samo std::cout << Foo.Metoda(); std::cout << (&Foo)->Metoda(); Tym razem bowiem pobieramy adres obiektu, czyli wskanik na niego, i aplikujemy do wskanikowy operator wyuskania ->. Widzimy zatem wyranie, e oba operatory wyuskania maj charakter mocno umowny i teoretycznie mog by stosowane zamiennie. W praktyce jednak korzysta si zawsze z kropki dla zmiennych obiektowych oraz strzaki dla wskanikw, i to z bardzo prostego powodu: wymuszenie zaakceptowania drugiego z operatorw wie si przecie z dodatkow czynnoci pobrania adresu albo dereferencji. cznie zatem uywamy wtedy dwch operatorw zamiast jednego, a to z pewnoci moe odbi si na wydajnoci kodu.
Wskaniki
zreszt jak to czasem bywa dla zwykych zmiennych. W takich przypadkach z pomoc przychodz nam rne metody konwersji typw wskanikowych, jakie oferuje C++.
259
Ustalamy t drog, i nasz wskanik nie bdzie zwizany z adnym konkretnym typem zmiennych. Nic nie wiadomo zatem o komrkach pamici, do ktrych si on odnosi mog one zawiera dowolne dane. Brak informacji o typie upoledza jednak podstawowe waciwoci wskanika. Nie mogc okreli rodzaju danych, na ktre pokazuje wskanik, kompilator nie moe pozwoli na dostp do nich. Powoduje to, e: Niedozwolone jest dokonanie dereferencji oglnego wskanika typu void*. C bowiem otrzymalibymy w jej wyniku? Jakiego typu byoby wyraenie *pWskaznik? void? Nie jest to przecie aden konkretny typ danych. Susznie wic dereferencja wskanika typu void* jest niemoliwa. Uomno takich wskanikw nie jest zbytni zacht do ich stosowania. Czym wic zasuyy sobie na tytu paragrafu im powiconego? Ot maj one jedn szczegln i przydatn cech, zwizan z brakiem wiadomoci o typie. Mianowicie: Wskanik typu void* moe przechowywa dowolny adres z pamici operacyjnej. Moliwe jest zatem przypisanie mu wartoci kadego innego wskanika (z wyjtkiem wskanikw na stae). Poprawny jest na przykad taki oto kod: int nZmienna; void* pWskaznik = &nZmienna; // &nZmienna jest zasadniczo typu int*
Fakt, e wskanik typu void* to tylko sam adres, bez dodatkowych informacji o typie, przeznaczonych dla kompilatora, sprawia, e owe informacje s tracone w momencie przypisania. Wskazywanym w pamici danym nie dzieje si naturalnie adna krzywda, jedynie my tracimy moliwo odwoywania si do nich poprzez dereferencj. Czy przypadkiem czego nam to nie przypomina? W miar podobna sytuacja miaa przecie okazj zainstnie przy okazji programowania obiektowego i polimorfizmu.
260
Podstawy programowania
Wskanik do obiektu klasu pochodnej moglimy bowiem przypisa do wskanika na obiekt klasy bazowej i uywa go potem tak samo, jak kadego innego wskanika na obiekt tej klasy. Tutaj typ void* jest czym rodzaju typu bazowego dla wszystkich innych typw wskanikowych. Moliwe jest zatem przypisywanie ich wskanikw zmiennym typu void*. Wwczas tracimy wprawdzie wiedz o pierwotnym typie wskanika, ale zachowujemy to, co najwaniejsze: adres przechowywany przez wskanik
Wskaniki
261
rodzaju danych jako zupenego innego. Porednictwo typu void* w niskopoziomowych konwersjach midzy wskanikami staje si wtedy kopotliwe. Z tego powodu (a take z potrzeby cakowitego zastpienia rzutowania w stylu C) wprowadzono do C++ kolejny operator rzutowania - reinterpret_cast/ Potrafi on rzutowa dowolny typ wskanikowy na dowolny inny typ wskanikowy i nie tylko. Konwersje przy uyciu tego operatora prawie zawsze nie s wic bezpieczne i powinny by stosowane wycznie wtedy, gdy zaley nam na mechanicznej zmianie (bit po bicie) jednego typu danych w inny. Jeeli chodzi o przykady, to chyba jedynym bezpiecznym zastosowaniem reinterpret_cast jest zapisanie adresu pamici ze wskanika do zwykej zmiennej liczbowej: int* pnWskaznik; unsigned uAdres = reinterpret_cast<unsigned>(pnWskaznik); W innych przypadkach stosowanie tego operatora powinno by wyjtkowo ostrone i oszczdne. Kompletnych informacji o reinterpret_cast dostarcza oczywicie MSDN. Jest tam take ciekawy artyku, wyjaniajcy dogbnie rnice midzy tym operatorem, a zwykym rzutowaniem static_cast. Istnieje jeszcze jeden, czwarty operator rzutowania const_cast. Jego zastosowanie jest bardzo wskie i ogranicza si do usuwania modyfikatora const z opatrzonych nim typw danych. Mona wic uy go, aby zmieni stay wskanik lub wskanik do staej w zwyky. Blisze informacje na temat tego operatora mona naturalnie znale we wiadomym rdle :)
Wskaniki i tablice
Tradycyjnie wskanikw uywa si do operacji na tablicach. Celowo pisz tu tradycyjnie, gdy prawie wszystkie te operacje mona wykona take bez uycia wskanikw, wic korzystanie z nich w C++ nie jest tak popularne jak w jego generacyjnym poprzedniku. Poniewa jednak czasem bdziemy zmuszeni korzysta z kodu wywodzcego si z czasw C (na przykad z Windows API), wiedza o zastosowaniu wskanikw w stosunku do tablic moe by przydatna. Obejmuje ona take zagadnienia acuchw znakw w stylu C, ktrym powicimy osobny paragraf. Ju sysz gosy oburzenia: Przecie miae zajmowa si nauczaniem C++, a nie wywlekaniem jego rnic w stosunku do swego poprzednika!. Rzeczywicie, to prawda. Wskaniki s to dziedzin jzyka, ktra najczciej zmusza nas do podry w przeszo. Wbrew pozorom nie jest to jednak przeszo zbyt odlega, skoro z powodzeniem wpywa na teraniejszo. Z waciwoci wskanikw i tablic bdziesz bowiem korzysta znacznie czciej ni sporadycznie.
262
Podstawy programowania
Nie s wic porozrzucane po caej dostpnej pamici (czyli pofragmentowane), ale grzecznie zgrupowane w jeden pakiet.
Dziki temu kompilator nie musi sobie przechowywa adresw kadego z elementw tablicy, aby programista mg si do nich odwoywa. Wystarczy tylko jeden: adres pocztku tablicy, jej zerowego elementu. W kodzie mona go atwo uzyska w ten sposb: // tablica i wskanik int aTablica[5]; int* pnTablica; // pobranie wskanika na zerowy element tablicy pnTablica = &aTablica[0]; Napisaem, e jest to take adres pocztku samej tablicy, czyli w gruncie rzeczy warto kluczowa dla caego agregatu. Dlatego reprezentuje go rwnie nazwa tablicy: // inny sposb pobrania wskanika na zerowy element (pocztek) tablicy pnTablica = aTablica; Wynika std, i: Nazwa tablicy jest take staym wskanikiem do jej zerowego elementu (pocztku). Staym - bo jego adres jest nadany raz na zawsze przez kompilator i nie moe by zmieniany w programie.
Wskanik w ruchu
Posiadajc wskanik do jednego z elementw tablicy, moemy z atwoci dosta si do pozostaych - wykorzystujc fakt, i tablica jest cigym obszarem pamici. Mona mianowicie odpowiednio przesun nasz wskanik, np.: pnTablica += 3; Po tej operacji bdzie on pokazywa na 3 elementy dalej ni dotychczas. Poniewa na pocztku wskazywa na pocztek tablicy (zerowy element), wic teraz zacznie odnosi si do jej trzeciego elementu. To ciekawe zjawisko. Wskanik jest przecie adresem, liczb, zatem dodanie do niego jakiej liczby powinno skutkowa odpowiednim zwikszeniem przechowywanej wartoci. Poniewa kolejne adresy w pamici s numerami bajtw, wic pnTablica powinien, zdawaoby si, przechowywa adres trzeciego bajta, liczc od pocztku tablicy. Tak jednak nie jest, gdy kompilator podczas dokonywania arytmetyki na wskanikach korzysta take z informacji o ich typie. Skoki spowodowane dodawaniem liczb cakowitych nastpuj w odstpach bajtowych rwnych wielokrotnociom rozmiaru
Wskaniki
263
zmiennej, na jak wskazuje wskanik. W naszym przypadku pnTablica przesuwa si wic o 3*sizeof(int) bajtw, a nie o 3 bajty! Obecnie wskazuje zatem na trzeci element tablicy aTablica. Dokonujc dereferencji wskanika, moemy odwoa si do tego elementu: // obie ponisze linijki s rwnowane *pnTablica = 0; aTablica[3] = 0; Wreszcie, dozwolony jest take trzeci sposb: *(aTablica + 3) = 0; Uywamy w nim wskanikowych waciwoci nazwy tablicy. Wyraenie aTablica + 3 odnosi si zatem do jej trzeciego elementu. Jego dereferencja pozwala przypisa temu elementowi jak warto. Wydao si wic, e do i-tego elementu tablicy mona odwoa si na dwa rne sposoby: *(tablica + i) tablica[i] W praktyce kompilator sam stosuje tylko pierwszy. Wprowadzenie drugiego miao oczywicie gboki sens: jest on zwyczajnie prostszy, nie tylko w zapisie, ale i w zrozumieniu. Nie wymaga te adnej wiedzy o wskanikach, a ponadto daje wiksz elastyczno przy definiowaniu wasnych typw danych. Nie naley jednak zapomina, e oba sposoby s tak samo podatne na bd przekroczenia indeksw, ktry wystpuje, gdy i wykracza poza przedzia <0; rozmiar_tablicy - 1>.
264
Podstawy programowania
Od razu spotka nas tutaj pewna niespodzianka. O ile bowiem C++ posiada wygodny typ std::string, sucy do przechowywania napisw, to C w ogle takiego typu nie posiada! Zwyczajnie nie istnieje aden specjalny typ danych, sucy reprezentacji tekstu. Zamiast niego stosowanie jest inne podejcie do problemu. Napis jest to cig znakw, a wic uporzdkowany zbir kodw ANSI, opisujcych te znaki. Dla pojedynczego znaku istnieje za typ char, zatem ich cig moe by przedstawiany jako odpowiednia tablica. acuch znakw w stylu C to jednowymiarowa tablica elementw typu char. Rni si ona jednak on innych tablic. S one przeznaczone gwnie do pracy nad ich pojedynczymi elementami, natomiast acuch znakw jest czciej przetwarzany w caoci, ni znak po znaku. Sprawia to, e dozwolone s na przykad takie (w gruncie rzeczy trywialne!) operacje: char szNapis[256] = "To jest jaki tekst"; Manipulujemy w nich wicej ni jednym elementem tablicy naraz. Zauwamy jeszcze, e przypisywany cig jest krtszy ni rozmiar tablicy (256). Aby zaznaczy, gdzie si on koczy, kompilator dodaje zawsze jeszcze jeden, specjalny znak o kodzie 0, na samym kocu napisu. Z powodu tej waciwoci acuchy znakw w stylu C s czsto nazywane napisami zakoczonymi zerem (ang. null-terminated strings). Dlaczego jednak ten sposb postpowania z tekstem jest zy (zosta przecie zastpiony przez typ std::string)? Pierwsz przyczyn s problemy ze zmienn dugoci napisw. Tekst jest kopotliwym rodzajem danych, ktry moe zajmowa bardzo rn ilo pamici, zalenie od liczby znakw. Rozsdnym rozwizaniem jest oczywicie przydzielanie mu dokadnie tylu bajtw, ilu wymaga; do tego potrzebujemy jednak mechanizmw zarzdzania pamici w czasie dziaania programu (poznamy je zreszt w tym rozdziale). Mona te statycznie rezerwowa wicej miejsca, ni to jest potrzebne - tak zrobiem choby w poprzednim skrawku przykadowego kodu. Wada tego rozwizania jest oczywista: spora cz pamici zwyczajnie si marnuje. Drug niedogodnoci s utrudnienia w dokonywaniu najprostszych w zasadzie operacji na tak potraktowanych napisach. Chodzi tu na przykad o konkatenacj; wiedzc, jak proste jest to dla napisw typu std::string, pewnie bez wahania napisalibymy co w tym rodzaju: char szImie[] = "Max"; char szNazwisko[] = "Planck"; char szImieINazwisko[] = szImie + " " + szNazwisko; Visual C++ zareagowaby za takim oto bdem:
error C2110: '+': cannot add two pointers
// BD!
Miaby w nim cakowit suszno. Rzeczywicie, prbujemy tutaj doda do siebie dwa wskaniki, co jest niedozwolne i pozbawione sensu. Gdzie s jednak te wskaniki? To przede wszystkim szImie i szNazwisko - jako nazwy tablic s przecie wskanikami do swych zerowych elementw. Rwnie spacja " " jest przez kompilator traktowana jako wskanik, podobnie zreszt jak wszystkie napisy wpisane w kodzie explicit. Porwnywanie takich napisw poprzez operator == jest wic niepoprawne!
Wskaniki
265
czenie napisw w stulu C jest naturalnie moliwe, wymaga jednak uycia specjalnych funkcji w rodzaju strcat(). Inne funkcje s przeznaczone choby do przypisywania napisw (str[n]cpy()) czy pobierania ich dugoci (strlen()). Nietrudno si domyle, e korzystanie z nich nie naley do rzeczy przyjemnych :) Na cae szczcie ominie nas ta rozkosz. Standardowy typ std::string zawiera bowiem wszystko, co jest niezbdne do programowej obsugi acuchw znakw. Co wicej, zapewnia on take kompatybilnoc z dawnymi rozwizaniami. Metoda c_str() (skrt od C string), bo o ni tutaj chodzi, zwraca wskanik typu const char*, ktrego mona uy wszdzie tam, gdzie wymagany jest napis w stylu C. Nie musimy przy tym martwi si o pniejsze zwolnienie zajmowanej przez nasz tekst pamici - zadba oto sama Biblioteka Standardowa. Przykadem wykorzystania tego rozwizania moe by wywietlenie okna komunikatu przy pomocy funkcji MessageBox() z Windows API: #include <string> #include <windows.h> std::string strKomunikat = "Przykadowy komunikat"; strKomunikat += "."; MessageBox (NULL, strKomunikat.c_str(), "Komunikat", MB_OK); O samej funkcji MessageBox() powiemy sobie wszystko, gdy ju przejdziemy do programowania aplikacji okienkowych. Powyszy kod zadziaa jednak take w programie konsolowym. Drugi oraz trzeci parametr tej funkcji powinien by acuchem znakw w stylu C. Moemy wic skorzysta z metody c_str() dla zmiennej strKomunikat, by uczyni zado temu wymaganiu. W sumie wic nie przeszkadza ono zupenie w normalnym korzystaniu z dobrodziejstw standardowego typu std::string.
266
}
Podstawy programowania
Ta prosta funkcja dzielenia cakowitego zwraca dwa rezultaty. Pierwszy to zasadniczy iloraz - jest on oddawany w tradycyjny sposb poprzez return. Natomiast reszta z dzielenia jest przekazywana poprzez stay wskanik pReszta, ktry funkcja otrzymuje jako parametr. Dokonuje jego dereferencji i zapisuje dan warto w miejscu, na ktre on wskazuje. Jeeli pamitamy o tym, skorzystanie z powyszej funkcji jest raczej proste i przedstawia si mniej wicej tak: // Division - dzielenie przy uyciu wskanika przekazywanego do funkcji void main() { // (pominiemy pobranie dzielnej i dzielnika od uytkownika) // obliczenie rezultatu int nIloraz, nReszta; nIloraz = Podziel(nDzielna, nDzielnik, &nReszta); // wywietlenie rezultatu std::cout << std::endl; std::cout << nDzielna << " / " <<nDzielnik << " = " << nIloraz << " r " << nReszta; getch();
Jako trzeci parametr w wywoaniu funkcji Podziel(): nIloraz = Podziel(nDzielna, nDzielnik, &nReszta); przekazujemy adres zmiennej (uzyskany oczywicie poprzez operator &). W niej te znajdziemy potem dan reszt i wywietlimy j w oknie konsoli:
W podobny sposb dziaa wiele funkcji z Windows API czy DirectX. Zalet tego rozwizania jest take moliwo oddzielenia zasadniczego wyniku funkcji (zwracanego przez wskanik) od ewentualnej informacji o bdzie czy te sukcesie jego uzyskania (przekazywanego w tradycyjny sposb). Oczywicie nic nie stoi na przeszkodzie, aby t drog zwraca wicej ni jeden dodatkowy rezultat funkcji. Jeli jednak ich liczba jest znaczna, lepiej zczy je w struktur ni deklarowa po kilkanacie parametrw w nagwku funkcji.
Wskaniki
267
Kiedy za wywoujemy funkcj z parametrami, wwczas kompilator dokonuje ich caociowego kopiowania - tak, e w ciele funkcji mamy do czynienia z duplikatami rzeczywistych parametrw aktualnych funkcji. Mwilimy zreszt we waciwym czasie, i parametry peni w funkcji rol dodatkowych zmiennych lokalnych. Aby to zilustrowa, wemy tak oto banaln funkcj: int Dodaj(int nA, int nB) { nA += nB; return nA; } Jak wida, dokonujemy w niej modyfikacji jednego z parametrw. Kiedy jednak wywoamy niniejsz funkcj w sposb podobny do tego: int nLiczba1 = 1, nLiczba2 = 2; std::cout << Dodaj(nLiczba1, nLiczba2); std::cout << nLiczba1; // nadal nLiczba1 == 1 ! zobaczymy, e podana jej zmienna pozostaje nietknita. Funkcja otrzymaa bowiem tylko jej warto, ktra zostaa w tym celu skopiowana. Trzeba jednak przyzna, e wikszo funkcji z zaoenia nie modyfikuje swoich parametrw, a jedynie odczytuje z nich wartoci. W takim przypadku jest im wic wszystko jedno, czy odwouj si do faktycznie istniejcych zmiennych, czy te do ich kopii, istniejcych tylko podczas dziaania funkcji. Jednak nam, programistom, nie jest wszystko jedno. Stworzenie kopii zmiennych wymaga bowiem dodatkowego czasu - na przydzielenie odpowiedniej iloci pamici i zapisanie w niej podanej wartoci. Naturalnie, w przypadku typw liczbowych jest to pomijalnie may interwa, ale dla wikszych obiektw (chociaby acuchw znakw) moe sta si znaczcy. A przecie wcale nie musi tak by! Moliwe jest zlikwidowanie koniecznoci tworzenia duplikatw zmiennych dla wywoywanych funkcji: wystarczy tylko zamiast wartoci przekazywa odwoania do nich, czyli wskaniki! Skopiowanie czterech bajtw bdzie na pewno znacznie szybsze ni przemieszczanie iloci danych liczonej na przykad w dziesitkach kilobajtw. Zobaczmy wic, jak mona przyspieszy dziaanie funkcji operujcych na duych obiektach. Posu si tu przykadem na wyszukiwanie pozycji jednego cigu znakw wewntrz innego: #include <string> // funkcja przeszukuje drugi napis w poszukiwaniu pierwszego; // gdy go znajdzie, zwraca indeks pierwszego pasujcego znaku, // w przeciwnym wypadku warto -1 int Wyszukaj (const std::string* pstrSzukany, const std::string* pstrPrzeszukiwany) { // przeszukujemy nasz napis for (unsigned i = 0; i <= pstrPrzeszukiwany->length() - pstrSzukany->length(); ++i) { // porwnujemy kolejne wycinki napisu (o odpowiedniej dugoci) // z poszukiwanym acuchem. Metoda std::string::substr() suy // do pobierania wycinka napisu if (pstrPrzeszukiwany->substr(i, pstrSzukany->length()) == *pstrSzukany) // jeeli wycinek zgadza si, to zwracamy jego indeks return i;
268
} // w razie niepowodzenia zwracamy -1 return -1;
Podstawy programowania
Przeszukiwany tekst moe by bardzo dugi - edytory pozwalaj na przykad na poszukiwanie wybranej frazy wewntrz caego dokumentu, liczcego nieraz wiele kilobajtw. Nie jest to jednak problemem: dziki temu, e funkcja operuje na nim poprzez wskanik, pozostaje on cay czas na swoim miejscu w pamici i nie jest kopiowany. Zysk na wydajno aplikacji moe by wtedy znaczny. W zamian jednake dowiadczamy pewnej niedogodnoci, zwizanej ze skadni dziaa na wskanikach. Aby odwoa si do przekazanego napisu, musimy kadorazowo dokonywa jego dereferencji; take wywoywanie metod wymaga innego operatora ni kropka, do ktrej przyzwyczailimy si, operujc na napisach. Ale i na to jest rada. Na koniec podrozdziau poznamy bowiem referencje, ktre zachowuj cechy wskanikw przy jednoczesnym umoliwieniu stosowania zwykej skadni, waciwej zmiennym.
Wskaniki
int* pnLiczba;
269
Chwilowo nie pokazuje on na adne sensowne dane. Moglibymy oczywicie zczy go z jak zmienn zadeklarowan w kodzie (poprzez operator &), lecz nie o to nam teraz chodzi. Chcemy sobie sami takow zmienn stworzy - uywamy do tego operatora new (nowy) oraz nazwy typu tworzonej zmiennej: pnLiczba = new int; Wynikiem dziaania tego operatora jest adres, pod ktrym widnieje w pamici nasza wieo stworzona, nowiutka zmienna. Umieszczamy go zatem w przygotowanym wskaniku - odtd bdzie on suy nam do manipulowania wykreowan zmienn. C takiego rni j innych, deklarowanych w kodzie? Ano cakiem sporo rzeczy: nie ma ona nazwy, poprzez ktr moglibymy si do niej odwywa. Wszelka komunikacja z ni musi zatem odbywa si za porednictwem wskanika, w ktrym zapisalimy adres zmiennej. czasu istnienia zmiennej nie kontroluje kompilator, ale sam programista. Inaczej mwic, nasza zmienna istnieje a do momentu jej zwolnienia (poprzez operator delete, ktry omwimy za chwil). Wynika std rwnie, e dla takiej zmiennej nie ma sensu pojcie zasigu. pocztkowa warto zmiennej jest przypadkowa. Zaley bowiem od tego, co poprzednio znajdowao si w tym miejscu pamici, ktre teraz system operacyjny odda do dyspozycji naszego programu. Poza tymi aspektami, moemy na tak stworzonej zmiennej wykonywa te same operacje, co na wszystkich innych zmiennych tego typu. Dereferujc pokazujcy na wskanik, otrzymujemy peen dostp do niej: *pnLiczba = 100; *pnLiczba += rand(); std::cout << *pnLiczba; // itp. Oczywicie nasze moliwoci nie ograniczaj si tylko do typw liczbowych czy podstawowych. Przeciwnie, za pomoc new moemy alokowa pami dla dowolnych rodzajw zmiennych - take tych definiowanych przez nas samych. Widzimy wic, e to bardzo potne narzdzie.
270
delete pnLiczba;
Podstawy programowania
Naley mie wiadomo, e delete niczego nie modyfikuje w samym wskaniku, zatem nadal pokazuje on na ten sam obszar pamici. Teraz jednak nasz program nie jest ju jego wacicielem, dlatego te aby unikn omykowego odwoania si do nieswojego rejonu pamici, wypadaoby wyzerowa nasz wskanik: pnLiczba = NULL; Warto NULL to po prostu zero, za zerowy adres nie istnieje. pnLiczba staje si wic wskanikiem pustym, niepokazujcym na adn konkretn komrk pamici. Gdybymy teraz (omykowo) sprbowali ponownie zastosowa wobec niego operator delete, wtedy instrukcja ta zostaaby po prostu zignorowana. Jeeli jednak wskanik nadal pokazywaby na ju zwolniony obszar pamici, wwczas bez wtpienia wystpiby bd ochrony pamici (ang. access violation). Zatem pamitaj, aby dla bezpieczestwa zerowa wskanik po zwolnieniu dynamicznej zmiennej, na ktr on wskazywa.
Dynamiczne tablice
Alokacja pamici dla pojedynczej zmiennej jest wprawdzie poprawna i klarowna, ale raczej mao efektowna. Trudno wwczas powiedzie, e faktycznie operujemy na zbiorze danych o niejednostajnej wielkoci, skoro owa niestao objawia si jedynie obecnoci lub nieobecnoci jednej zmiennej! O wiele bardziej interesuj s dynamiczne tablice - takie, ktrych rozmiar jest ustalany w czasie dziaania aplikacji. Mog one przechowywa rn ilo elementw, wic nadaj si do mnstwa wspaniaych celw :) Zobaczymy teraz, jak obsugiwa takie tablice.
Wskaniki
271
Tablice jednowymiarowe
Najprociej sprawa wyglda z takimi tablicami, ktrych elementy s indeksowane jedn liczb, czyli po prostu z tablicami jednowymiarowymi. Popatrzmy zatem, jak odbywa si ich alokacja i zwalnianie. Tradycyjnie ju zaczynamy od odpowiedniego wskanika. Jego typ bdzie determinowa rodzaj danych, jakie moemy przechowywa w naszej tablicy: float* pfTablica; Alokacja pamici dla niej take przebiega w dziwnie znajomy sposb. Jedyn rnic w stosunku do poprzedniego paragrafu jest oczywista konieczno podania wielkoci tablicy: pfTablica = new float [1024]; Podajemy j w nawiasach klamrowych, za nazw typu pojedynczego elementu. Z powodu obecnoci tych nawiasw, wystpujcy tutaj operator jest czsto okrelony jako new[]. Ma to szczeglny sens, jeeli porwnamy go z operatorem zwalniania tablicy, ktry zobaczymy za momencik. Zwamy jeszcze, e rozmiar naszej tablicy jest dosy spory. By moe wobec dzisiejszych pojemnoci RAMu brzmi to zabawnie, ale zawsze przecie istnieje potencjalna moliwo, e zabraknie dla nas tego yciodajnego zasobu, jakim jest pami operacyjna. I na takie sytuacje powinnimy by przygotowani - tym bardziej, e poczynienie odpowiednich krokw nie jest trudne. W przypadku braku pamici operator new zwrci nam pusty wskanik; jak pamitamy, nie odnosi si on do adnej komrki, wic moe by uyty jako warto kontrolna (spotkalimy si ju z tym przy okazji rzutowania dynamic_cast). Wypadaoby zatem sprawdzi, czy nie natrafilimy na tak nieprzyjemn sytuacj i zareagowa na ni odpowiednio: if (pfTablica == NULL) // moe by te if (!pfTablica) std::cout << "Niestety, zabraklo pamieci!"; Moemy zmieni to zachowanie i sprawi, eby w razie niepowodzenia alokacji pamici bya wywoywana nasza wasna funkcja. Po szczegy moesz zajrze do opisu funkcji set_new_handler() w MSDN. Jeeli jednak wszystko poszo dobrze - a tak chyba bdzie najczciej :) - moemy uywa naszej tablicy w identyczny sposb, jak tych alokowanych statycznie. Powiedzmy, e wypenimy j treci przy pomocy nastpujcej ptli: for (unsigned i = 0; i < 1024; ++i) pfTablica[i] = i * 0.01; Wida, e dostp do poszczeglnych elementw odbywa si tutaj tak samo, jak dla tablic o staym rozmiarze. A waciwie, eby by cisym, to raczej tablice o staym rozmiarze zachowuj si podobnie, gdy w obu przypadkach mamy do czynienia z jednym i tym samym mechanizmem - wskanikami. Naley jeszcze pamita, aby zachowa gdzie rozmiar alokowanej tablicy, eby mc na przykad przetwarza j przy pomocy ptli for, podobnej do powyszej. Na koniec trzeba oczywicie zwolni pami, ktra przeznaczylimy na tablic. Za jej usunicie odpowiada operator delete[]:
272
delete[] pfTablica;
Podstawy programowania
Musimy koniecznie uwaa, aby nie pomyli go z podobnym operatorem delete. Tamten suy do zwalniania wycznie pojedyncznych zmiennych, za jedynie niniejszy moe by uyty do usunicia tablicy. Nierespektowanie tej reguy moe prowadzi do bardzo nieprzyjemnych bdw! Zatem do zwalniania tablic korzystaj tylko z operatora delete[]! atwo zapamita t zasad, jeeli przypomnimy sobie, i do alokowania tablicy posuya nam instrukcja new[]. Jej usunicie musi wic rwnie odbywa si przy pomocy operatora z nawiasami kwadratowymi.
Opakowanie w klas
Jeli czsto korzystamy z dynamicznych tablic, warto stworzy dla odpowiedni klas, ktra uatwi nam to zadanie. Nie jest to specjalnie trudne. My stworzymy tutaj przykadow klas jednowymiarowej tablicy elementw typu int. Zacznijmy moe od jej prywatnych pl. Oprcz oczywistego wskanika na wewntrzn tablic klasa powinna by wyposaona take w zmienn, w ktrej zapamitamy rozmiar utworzonej tablicy. Uwolnimy wtedy uytkownika od koniecznoci zapisywania jej we wasnym zakresie. Metody musz zapewni dostp do elementw tablicy, a wic pobieranie wartoci o okrelonym indeksie oraz zapisywanie nowych liczb w okrelonych elementach tablicy. Przy okazji moemy te kontrolowa indeksy i zapobiega ich przekroczeniu, co znowu zapewni nam dozgonn wdziczno programisty-klienta naszej klasy ;) Definicja takiej tablicy moe wic przedstawia si nastpujco: class CIntArray { // domylny rozmiar tablicy static const unsigned DOMYSLNY_ROZMIAR = 5; private: // wskanik na waciw tablic oraz jej rozmiar int* m_pnTablica; unsigned m_uRozmiar; public: // konstruktory CIntArray() // domylny { m_uRozmiar = DOMYSLNY_ROZMIAR; m_pnTablica = new int [m_uRozmiar]; } CIntArray(unsigned uRozmiar) // z podaniem rozmiaru tablicy { m_uRozmiar = uRozmiar; m_pnTablica = new int [m_uRozmiar]; } // destruktor ~CIntArray() { delete[] m_pnTablica; }
// ------------------------------------------------------------// pobieranie i ustawianie elementw tablicy int Pobierz(unsigned uIndeks) const { if (uIndeks < m_uRozmiar) return m_pnTablica[uIndeks]; else return 0; } bool Ustaw(unsigned uIndeks, int nWartosc)
Wskaniki
{ if (uIndeks >= m_uRozmiar) return false; m_pnTablica[uIndeks] = uWartosc; return true; // inne unsigned Rozmiar() const { return m_uRozmiar; }
273
};
S w niej wszystkie detale, o jakich wspomniaem wczeniej. Dwa konstruktory maj na celu zaalokowanie pamici na nasz tablic; jeden z nich jest domylny i ustawia okrelon z gry wielko (wpisan jako staa DOMYSLNY_ROZMIAR), drugi za pozwala poda j jako parametr. Destruktor natomiast dba o zwolnienie tak przydzielonej pamici. W tego typu klasach metoda ta jest wic szczeglnie przydatna. Pozostae funkcje skadowe zapewniaj intuicyjny dostp do elementw tablicy, zabezpieczajc przy okazji przed bdem przekroczenia indeksw. W takiej sytuacji Pobierz() zwraca warto zero, za Ustaw() - false, informujc o zainstniaym niepowodzeniu. Skorzystanie z tej gotowej klasy nie jest chyba trudne, gdy jej definicja niemal dokumentuje si sama. Popatrzmy aczkolwiek na nastpujcy przykad: #include <cstdlib> #include <ctime> srand (static_cast<unsigned>(time(NULL))); CIntArray aTablica(rand()); for (unsigned i = 0; i < aTablica.Rozmiar(); ++i) aTablica.Ustaw (i, rand()); Jak wida, generujemy w nim losow ilo losowych liczb :) Nieodmiennie te uywamy do tego ptli for, nieodzownej przy pracy z tablicami. Zdefiniowana przed momentem klasa jest wic cakiem przydatna, posiada jednak trzy zasadnicze wady: raz ustalony rozmiar tablicy nie moe ju ulega zmianie. Jego modyfikacja wymaga stworzenia nowej tablicy dostp do poszczeglnych elementw odbywa si za pomoc mao wygodnych metod zamiast zwyczajowych nawiasw kwadratowych typem przechowywanych elementw moe by jedynie int Na dwa ostatnie mankamenty znajdziemy rad, gdy ju nauczymy si przecia operatory oraz korzysta z szablonw klas w jzyku C++. Niemono zmiany rozmiaru tablicy moemy jednak usun ju teraz. Dodajmy wic jeszcze jedn metod za to odpowiedzialn: class CIntArray { // (reszt wycito) public: bool ZmienRozmiar(unsigned);
};
Wykona ona alokacj nowego obszaru pamici i przekopiuje do niego ju istniejc cz tablicy. Nastpne zwolni j, za caa klasa bdzie odtd operowaa na nowym fragmencie pamici. Brzmi to dosy tajemniczo, ale w gruncie rzeczy jest bardzo proste:
274
Podstawy programowania
#include <memory.h> bool CIntArray::ZmienRozmiar(unsigned uNowyRozmiar) { // sprawdzamy, czy nowy rozmiar jest wikszy od starego if (!(uNowyRozmiar > m_uRozmiar)) return false; // alokujemy now tablic int* pnNowaTablica = new int [uNowyRozmiar]; // kopiujemy do star tablic i zwalniamy j memcpy (pnNowaTablica, m_pnTablica, m_uRozmiar * sizeof(int)); delete[] m_pnTablica; // "podczepiamy" now tablic do klasy i zapamitujemy jej rozmiar m_pnTablica = pnNowaTablica; m_uRozmiar = uNowyRozmiar; // zwracamy pozytywny rezultat return true;
Wyjanienia wymaga chyba tylko funkcja memcpy(). Oto jej prototyp (zawarty w nagwku memory.h, ktry doczamy): void* memcpy(void* dest, const void* src, size_t count); Zgodnie z nazw (ang. memory copy - kopiuj pami), funkcja ta suy do kopiowania danych z jednego obszaru pamici do drugiego. Podajemy jej miejsce docelowe i rdowe kopiowania oraz ilo bajtw, jaka ma by powielona. Wanie ze wzgldu na bajtowe wymagania funkcji memcpy() uywamy operatora sizeof, by pobra wielko typu int i pomnoy go przez rozmiar (liczb elementw) naszej tablicy. W ten sposb otrzymamy wielko zajmowanego przez ni rejonu pamici w bajtach i moemy go przekaza jako trzeci parametr dla funkcji kopiujcej. Pena dokumentacja funkcji memcpy() jest oczywicie dostpna w MSDN. Po rozszerzeniu nowa tablica bdzie zawieraa wszystkie elementy pochodzce ze starej oraz nowy obszar, moliwy do natychmiastowego wykorzystania.
Tablice wielowymiarowe
Uelastycznienie wielkoci jest w C++ moliwe take dla tablic o wikszej liczbie wymiarw. Jak to zwykle w tym jzyku bywa, wszystko odbywa si analogicznie i intuicyjnie :D Przypomnijmy, e tablice wielowymiarowe to takie tablice, ktrych elementami s inne tablice. Wiedzc za, i mechanizm tablic jest w C++ zarzdzany poprzez wskaniki, dochodzimy do wniosku, e: Dynamiczna tablica n-wymiarowa skada si ze wskanikw do tablic (n-1)-wymiarowych. Dla przykadu, tablica o dwch wymiarach jest tak naprawd jednowymiarowym wektorem wskanikw, z ktrych kady pokazuje dopiero na jednowymiarow tablic waciwych elementw.
Wskaniki
275
Aby wic obsugiwa tak tablic, musimy uy do osobliwej konstrukcji programistycznej - wskanika na wskanik. Nie jest to jednak takie dziwne. Wskanik to przecie te zmienna, a wic rezyduje pod jakim adresem w pamici. Ten adres moe by przechowywany przez kolejny wskanik. Deklaracja czego takiego nie jest trudna: int** ppnTablica; Wystarczy doda po prostu kolejn gwiazdk do nazwy typu, na ktry ostatecznie pokazuje nasz wskanik. Jak taki wskanik ma si do dynamicznych, dwuwymiarowych tablic? Ilustrujc nim opis podany wczeniej, otrzymamy schemat podobny do tego:
Schemat 35. Dynamiczna tablica dwuwymiarowa jest tablic wskanikw do tablic jednowymiarowych
Skoro wic wiemy ju, do czego zmierzamy, pora osign cel. Alokacja dwywumiarowej tablicy musi odbywa si dwuetapowo: najpierw przygotowujemy pami pod tablic wskanikw do jej wierszy. Potem natomiast przydzielamy pami kademu z tych wierszy - tak, e w sumie otrzymujemy tyle elementw, ile chcielimy. Po przeoeniu na kod C++ algorytm wyglda w ten sposb: // Alokacja tablicy 3 na 4 // najpierw tworzymy tablic wskanikw do kolejnych wierszy ppnTablica = new int* [3]; // nastpnie alokujemy te wiersze for (unsigned i = 0; i < 3; ++i) ppnTablica[i] = new int [4];
276
Przeanalizuj go dokadnie. Zwr uwag szczeglnie na linijk: ppnTablica[i] = new int [4];
Podstawy programowania
Za pomoc wyraenia ppnTablica[i] odwoujemy si tu do i-tego wiersza naszej tablicy - a cilej mwic, do wskanika na niego. Przydzielamy mu nastpnie adres zaalokowanego fragmentu pamici, ktry bdzie peni rol owego wiersza. Robimy tak po kolei ze wszystkimi wierszami tablicy. Uytkowanie tak stworzonej tablicy dwuwymiarowej nie powinno nastrcza trudnoci. Odbywa si ono bowiem identycznie, jak w przypadku statycznych macierzy. Najczstsz konstrukcj jest tu znowu zagniedona ptla for: for (unsigned i = 0; i < 3; ++i) for (unsigned j = 0; j < 4; ++j) ppnTablica[i][j] = i - j; Co za ze zwalnianiem tablicy? Ot przeprowadzamy je w sposb dokadnie przeciwny do jej alokacji. Zaczynamy od uwolnienia poszczeglnych wierszy, a nastpnie pozbywamy si take samej tablicy wskanikw do nich. Wyglda to mniej wicej tak: // zwalniamy wiersze for (unsigned i = 0; i < 3; ++i) delete[] ppnTablica[i]; // zwalniamy tablic wskanikw do nich delete[] ppnTablica; Przedstawion tu kolejno naley zawsze bezwgldnie zachowywa. Gdybymy bowiem najpierw pozbyli si wskanikw do wierszy tablicy, wtedy nijak nie moglibymy zwolni samych wierszy! Usuwanie tablicy od tyu chroni za przed tak ewentualnoci. Znajc technik alokacji tablicy dwuwymiarowej, moemy atwo rozszerzy j na wiksz liczb wymiarw. Popatrzmy tylko na kod odpowiedni dla trjwymiarowej tablicy: /* Dynamiczna tablica trjwymiarowa, 5 na 6 na 7 elementw */ // wskanik do niej ("trzeciego stopnia"!) int*** p3nTablica; /* alokacja */ // tworzymy tablic wskanikw do 5 kolejnych "paszczyzn" tablicy p3nTablica = new int** [5]; // przydzielamy dla nich pami for (unsigned i = 0; i < 5; ++i) { // alokujemy tablic na wskaniki do wierszy p3nTablica[i] = new int* [6]; // wreszcie, dla przydzielamy pami dla waciwych elementw for (unsigned j = 0; j < 6; ++j) p3nTablica[i][j] = new int [7];
Wskaniki
277
/* uycie */ // wypeniamy tabelk jak treci for (unsigned i = 0; i < 5; ++i) for (unsigned j = 0; j < 6; ++j) for (unsigned k = 0; k < 7; ++k) p3nTablica[i][j][k] = i + j + k; /* zwolnienie */ // zwalniamy kolejne "paszczyzny" for (unsigned i = 0; i < 5; ++i) { // zaczynamy jednak od zwolnienia wierszy for (unsigned j = 0; j < 6; ++j) delete[] p3nTablica[i][j]; // usuwamy "paszczyzn" delete[] p3nTablica[i];
// na koniec pozbywamy si wskanikw do "paszczyzn" delete[] p3nTablica; Wida niestety, e z kadym kolejnym wymiarem kod odpowiedzialny za alokacj oraz zwalnianie tablicy staje si coraz bardziej skomplikowany. Na szczcie jednak dynamiczne tablice o wikszej liczbie wymiarw s bardzo rzadko wykorzystywane w praktyce.
Referencje
Naocznie przekonae si, e domena zastosowa wskanikw jest niezwykle szeroka. Jeeli nawet nie dayby w danym programie jakich niespotykanych moliwoci, to na pewno za ich pomoc mona poczyni spore optymalizacje w kodzie i przyspieszy jego dziaanie. Za popraw wydajnoci trzeba jednak zapaci wygod: odwoywanie si do obiektw poprzez wskaniki wymaga bowiem ich dereferencji. Wprowadza ona nieco zamieszania do kodu i wymaga powicenia mu wikszej uwagi. C, zawsze co za co, prawda? Ot nieprawda :) Twrcy C++ wyposayli bowiem swj jzyk w mechanizm referencji, ktry czy zalety wskanikw z normaln skadni zmiennych. Zatem i wilk jest syty, i owca caa. Referencje (ang. references) to zmienne wskazujce na adresy miejsc w pamici, ale pozwalajce uywa zwyczajnej skadni przy odwoywaniu si do tyche miejsc. Mona je traktowa jako pewien szczeglny rodzaj wskanikw, ale stworzony dla czystej wygody programisty i poprawy wygldu pisanego przeze kodu. Referencje s aczkolwiek niezbdne przy przecianiu operatorw (o tym powiemy sobie niedugo), jednak swoje zastosowania mog znale niemal wszdzie. Przy takiej rekomendacji trudno nie oprze si chci ich poznania, nieprawda? ;) Tym wanie zagadnieniem zajmiemy si wic teraz.
278
Podstawy programowania
Typy referencyjne
Podobnie jak wskaniki wprowadziy nam pojcie typw wskanikowych, tak i referencje dodaj do naszego sownika analogiczny termin typw referencyjnych. W przeciwiestwie jednak do wskanikw, dla kadego normalnego typu istniej jedynie dwa odpowiadajce mu typy referencyjne. Dlaczego tak jest, dowiesz si za chwil. Na razie przypatrzmy si deklaracjom przykadowych referencji.
Deklarowanie referencji
Referencje odnosz si do zmiennych, zatem najpierw przydaoby si jak zmienn posiada. Niech bdzie to co w tym rodzaju: short nZmienna; Odpowiednia referencja, wskazujca na t zmienn, bdzia natomiast zadeklarowana w ten oto sposb: short& nReferencja = nZmienna; Koczcy nazw typu znak & jest wyrnikiem, ktry mwi nam i kompilatorowi, e mamy do czynienia wanie z referencj. Inicjalizujemy j od razu tak, aeby wskazywaa na nasz zmienn nZmienna. Zauwamy, e nie uywamy do tego adnego dodatkowego operatora! Posugujc si referencj moliwe jest teraz zwyczajne odwoywanie si do zmiennej, do ktrej si ona odnosi. Wyglda to wic bardzo zachcajco - na przykad: nReferencja = 1; // przypisanie wartoci zmiennej nZmienna std::cout << nReferencja; // wywietlenie wartoci zmiennej nZmienna Wszystkie operacje, jakie tu wykonujemy, odbywaj si na zmiennej nZmienna, chocia wyglda, jakby to nReferencja bya jej celem. Ona jednak tylko w nich poredniczy, tak samo jak czyni to wskaniki. Referencja nie wymaga jednak skorzystania z operatora * (zwanego notabene operatorem dereferencji) celem dostania si do miejsca pamici, na ktre sama wskazuje. Ten wanie fakt (midzy innymi) rni j od wskanika.
Wskaniki
279
W C++ wystpuj wycznie stae referencje. Po koniecznej inicjalizacji nie mog ju by zmieniane. To jest wanie powd, dla ktrego istniej tylko dwa warianty typw referencyjnych. O ile wic w przypadku wskanikw atrybut const mg wystpowa (lub nie) w dwch rnych miejscach deklaracji, o tyle dla referencji jego drugi wystp jest niejako domylny. Nie istnieje zatem adna niestaa referencja. Przypisanie zmiennej do referencji moe wic si odbywa tylko podczas jej inicjalizacji. Jak widzielimy, dzieje si to prawie tak samo, jak przy staych wskanikach - naturalnie z wyczeniem braku operatora &, np.: float fLiczba; float& fRef = fLiczba; Czy fakt ten jest jak niezmiernie istotn wad referencji? miem twierdzi, e ani troch! Tak naprawd prawie nigdy nie uywa si mechanizmu referencji w odniesieniu do zwykych zmiennych. Ich prawdziwa uyteczno ujawnia si bowiem dopiero w poczeniu z funkcjami. Zobaczmy wic, dlaczego s wwczas takie wspaniae ;D
Referencje i funkcje
Chyba jedynym miejscem, gdzie rzeczywicie uywa si referencji, s nagwki funkcji (prototypy). Dotyczy to zarwno parametrw, jak i wartoci przez te funkcje zwracanych. Referencje daj bowiem cakiem znaczce optymalizacje w szybkoci dziaania kodu, i to w zasadzie za darmo. Nie wymagaj adnego dodatkowego wysiku poza ich uyciem w miejsce zwykych typw. Brzmi to bardzo kuszco, zatem zobaczmy te wymienite rozwizania w akcji.
280
Podstawy programowania
// porwnujemy kolejne wycinki napisu if (strPrzeszukiwany.substr(i, strSzukany.length()) == strSzukany) // jeeli wycinek zgadza si, to zwracamy jego indeks return i;
Obecnie nie wida tu najmniejszych oznak silenia si na jakkolwiek optymalizacj, a mimo jest ona taka sama jak w wersji wskanikowej. Powodem jest forma nagwka funkcji: int Wyszukaj (const std::string& strSzukany, const std::string& strPrzeszukiwany) Oba jej parametry s tutaj referencjami do staych napisw, a wic nie s kopiowane w inne miejsca pamici wycznie na potrzeby funkcji. A jednak, chocia faktycznie funkcja otrzymuje tylko ich adresy, moemy operowa na tych parametrach zupenie tak samo, jakbymy dostali cae obiekty poprzez ich wartoci. Mamy wic zarwno wygodn skadni, jak i dobr wydajno tak napisanej funkcji. Zatrzymajmy si jeszcze przez chwil przy modyfikatorach const w obu parametrach funkcji. Obydwa napisy nie w jej ciele w aden sposb zmieniane (bo i nie powinny), zatem logiczne jest zadeklarowanie ich jako referencji do staych. W praktyce tylko takie referencje stosuje si jako parametry funkcji; jeeli bowiem naley zwrci jak warto poprzez parametr, wtedy lepiej dla zaznaczenia tego faktu uy odpowiedniego wskanika.
Zwracanie referencji
Na podobnej zasadzie, na jakiej funkcje mog pobiera referencje poprzez swoje parametry, mog te je zwraca na zewntrz. Uzasadnienie dla tego zjawiska jest rwnie takie samo, czyli zaoszczdzenie niepotrzebnego kopiowania wartoci. Najprotszym przykadem moe by ciekawe rozwizanie problemu metod dostpowych tak jak poniej: class CFoo { private: unsigned m_uPole; public: unsigned& Pole() { return m_uPole; } }; Poniewa metoda Pole() zwraca referencj, moemy uywa jej niemal tak samo, jak zwyczajnej zmiennej: CFoo Foo; Foo.Pole() = 10; std::cout << Foo.Pole(); Oczywicie kwestia, czy takie rozwizanie jest w danym przypadku podane, jest mocno indywidualna. Zawsze naley rozway, czy nie lepiej zastosowa tradycyjnego wariantu metod dostpowych - szczeglnie, jeeli chcemy zachowywa kontrol nad wartociami przypisywanymi polom.
Wskaniki
281
Z praktycznego punktu widzenia zwracanie referencji nie jest wic zbytnio przydatn moliwoci. Wspominam jednak o niej, gdy stanie si ona niezbdna przy okazji przeadowywania operatorw - zagadnienia, ktrym zajmiemy si w jednym z przyszych rozdziaw. *** Tym drobnym wybiegniciem w przyszo zakoczymy nasze spotkania ze wskanikami na zmienne. Jeeli miae jakiekolwiek wtpliwoci co do uytecznoci tego elementu jzyka C++, to chyba do tego momentu zostay one cakiem rozwiane. Najlepiej jednak przekonasz si o przydatnoci mechanizmw wskanikw i referencji, kiedy sam bdziesz mia okazj korzysta z nich w swoich wasnych aplikacjach. Przypuszczam take, e owe okazje nie bd wcale odosobnionymi przypadkami, ale sta praktyk programistyczn. Oprcz wskanikw na zmienne jzyk C++ oferuje rwnie inn ciekaw konstrukcj, jak s wskaniki na funkcje. Nie od rzeczy bdzie wic zapoznanie si z nimi, co te pilnie uczynimy.
Wskaniki do funkcji
Mylc o tym, co jest przechowywane w pamici operacyjnej, zwykle wyobraamy sobie rne dane programu: zmienne, tablice, struktury itp. One stanowi informacje reprezentowane w komrkach pamici, na ktrych aplikacja wykonuje swoje dziaania. Caa pami operacyjna jest wic usiana danymi kadego z aktualnie pracujcych programw. Hmm Czy aby na pewno o czym nie zapomnielimy? A co z samymi programami?! Kod aplikacji jest przecie pewn porcj binarnych danych, zatem i ona musi si gdzie podzia. Przez wikszo czasu egzystuje wprawdzie na dysku twardym w postaci pliku (zwykle o rozszerzeniu EXE), ale dla potrzeb wykonywania kodu jest to z pewnoci zbyt wolne medium. Gdyby system operacyjny co rusz siga do pliku w czasie dziaania programu, wtedy na pewno wszelkie czynnoci cignyby si niczym toffi i przyprawiay zniecierpliwionego uytkownika o bia gorczk. Co wic zrobi z tym fantem? Rozsdnym wyjciem jest umieszczenie w pamici operacyjnej take kodu dziaajcej aplikacji. Dostp do nich jest wwczas wystarczajco szybki, aby programy mogy dziaa w normalnym tempie i bez przeszkd wykonywa swoje zadania. Pami RAM jest przecie stosunkowo wydajna, wielokrotnie bardziej ni nawet najszybsze dyski twarde. Tak wic podczas uruchamiania programu jego kod jest umieszczany wewntrz pamici operacyjnej. Kady podprogram, kada funkcja, a nawet kada instrukcja otrzymuj wtedy swj unikalny adres, zupenie jak zmienne. Maszynowy kod binarny jest bowiem take swoistego rodzaju danymi. Z tych danych korzysta system operacyjny (glwnie poprzez procesor), wykonujc kolejne instrukcje aplikacji. Wiedza o tym, jaka komenda ma by za chwil uruchomiona, jest przechowywana wanie w postaci jej adresu - czyli po prostu wskanika. Nam zwykle nie jest potrzebna a tak dokadna lokalizacja jakiego wycinka kodu w naszej aplikacji, szczeglnie jeeli programujemy w jzyku wysokiego poziomu, ktrym jest z pewnoci C++. Trudno jednak pogardzi moliwoci uzyskania adresu funkcji w programie, jeli przy pomocy tego adresu (oraz kilku dodatkowych informacji, o czym za chwil) mona ow funkcj swobodnie wywoywa. C++ oferuje wic mechanizm wskanikw do funkcji, ktry udostpnia taki wanie potencja. Wskanik do funkcji (ang. pointer to function) to w C++ zmienna, ktra przechowuje adres, pod jakim istnieje w pamici operacyjnej dana funkcja.
282
Podstawy programowania
Wiem, e pocztkowo moe by ci trudno uwiadomi sobie, w jaki sposb kod programu jest reprezentowany w pamici i jak wobec tego dziaaj wskaniki na funkcje. Dokadnie wyjanienie tego faktu wykracza daleko poza ramy tego rozdziau, kursu czy nawet programowania w C++ jako takiego (oraz, przyznam szczerze, czciowo take mojej wiedzy :D). Dotyka to ju bowiem niskopoziomowych aspektw dziaania aplikacji. Niemniej postaram si przystpnie wyjani przynajmniej te zagadnienia, ktre bd nam potrzebne do sprawnego posugiwania si wskanikami do funkcji. Zanim to si stanie, moesz myle o nich jako o swoistych czach do funkcji, podobnych w swych zaoeniach do skrtw, jakie w systemie Windows mona tworzy w odniesieniu do aplikacji. Tutaj natomiast mamy do czynienia z pewnego rodzaju skrtami do pojedynczych funkcji; przy ich pomocy moemy je bowiem wywoywa niemal w ten sam sposb, jak to czynimy bezporednio. Omawianie wskanikw do funkcji zaczniemy nieco od tyu, czyli od bytw na ktre one wskazuj - a wic od funkcji wanie. Przypomnimy sobie, c takiego charakteryzuje funkcj oraz powiemy sobie, jakie jej cechy bd szczeglne istotne w kontekcie wskanikw. Potem rzecz jasna zajmiemy si uywaniem wskanikw do funkcji w naszych wasnych programach, poczynajc od deklaracji a po wywoywanie funkcji za ich porednictwem. Na koniec uwiadomimy sobie take kilka zastosowa tej ciekawej konstrukcji programistycznej.
Wskaniki
283
Specjaln rol peni tutaj typ void (pustka), ktry jest synonimem niczego. Nie mona wprawdzie stworzy zmiennych nalecych do tego typu, jednak moliwe jest uczynienie go typem zwracanym przez funkcj. Taka funkcj bdzie zatem zwraca nic, czyli po prostu nic nie zwraca; mona j wic nazwa procedur.
Konwencja wywoania
Troch trudno w to uwierzy, ale podanie (zdawaoby si) wszystkiego, co mona powiedzie o danej funkcji: jej parametrw, wartoci przeze zwracanej, nawet nazwy nie wystarczy kompilatorowi do jej poprawnego wywoania. Bdzie on aczkolwiek wiedzia, co musi zrobi, ale nikt mu nie powie, jak ma to zrobi. C to znaczy? Celem wyjanienia porwnajmy ca sytuacj do telefonowania. Gdy mianowicie chcemy zadzwoni pod konkretny numer telefonu, mamy wiele moliwych drg uczynienia tego. Moemy zwyczajnie pj do drugiego pokoju, podnie suchawk stacjonarnego aparatu i wystuka odpowiedni numer. Moemy te siegnc po telefon komrkowy i uy go, wybierajc na przykad waciw pozycj z jego ksiki adresowej. Teoretycznie moemy te wybra si do najbliszej budki telefonicznej i skorzysta z zainstalowanego tam aparatu. Wreszcie, moliwe jest wykorzystanie modemu umieszczonego w komputerze i odpowiedniego oprogramowania albo te dowolnej formy dostpu do globalnej sieci oraz protokou VoIP (Voice over Internet Protocol). Technicznych moliwoci mamy wic mnstwo i zazwyczaj wybieramy t, ktra jest nam w aktualnej chwili najwygodniejsza. Zwykle te osoba po drugiej stronie linii nie odczuwa przy tym adnej rnicy.
284
Podstawy programowania
Podobnie rzecz ma si z wywoywaniem funkcji. Znajc jej miejsce docelowe (adres funkcji w pamici) oraz ewentualne dane do przekazania jej w parametrach, moliwe jest zastosowanie kilku drg osignicia celu. Nazywamy je konwencjami wywoania funkcji. Konwencja wywoania (ang. calling convention) to okrelony sposb wywoywania funkcji, precyzujcy przede wszystkim kolejno przekazywania jej parametrw. Dziwisz si zapewne, dlaczego dopiero teraz mwimy o tym aspekcie funkcji, skoro jasno wida, i jest on nieodzowny dla ich dziaania. Przyczyna jest prosta. Wszystkie funkcje, jakie samodzielnie wpiszemy do kodu i dla ktrych nie okrelimy konwencji wywoania, posiadaj domylny jej wariant, waciwy dla jzyka C++. Jeeli za chodzi o funkcje biblioteczne, to ich prototypy zawarte w plikach naglwkowych zawieraj informacje o uywanej konwencji. Pamitajmy, e korzysta z nich gwnie sam kompilator, gdy w C++ wywoanie funkcji wyglda skadniowo zawsze tak samo, niezalenie od jej konwencji. Jeeli jednak uywamy funkcji do innych celw ni tylko prostego przywoywania (a wic stosujemy choby wskaniki na funkcje), wtedy wiedza o konwencjach wywoania staje si potrzebna take i dla nas.
Wskaniki
285
90
286
Podstawy programowania
Nazwa funkcji
To zadziwiajce, e chyba najwaniejsza dla programisty cecha funkcji, czyli jej nazwa, jest niemal zupenie nieistotna dla dziaajcej aplikacji! Jak ju bowiem mwiem, widzi ona swoje funkcje wycznie poprzez ich adresy w pamici i przy pomocy tych adresw ewentualnie wywouje owe funkcje. Mona dywagowa, czy to dowd na cakowity brak skrzyowania midzy drogami czowieka i maszyny, ale fakt pozostaje faktem, za jego przyczyna jest prozaicznie pragmatyczna. Chodzi tu po prostu o wydajno: skoro funkcje programu s podczas jego uruchamiania umieszczane w pamici operacyjnej (mona adnie powiedzie: mapowane), to dlaczego system operacyjny nie miaby uywa wygenerowanych przy okazji adresw, by w razie potrzeby rzeczone funkcje wywoywa? To przecie proste i szybkie rozwizanie, naturalne dla komputera i niewymagajce adnego wysiku ze strony programisty. A zatem jest ono po prostu dobre :)
Parametry funkcji
Ogromna wikszo funkcji nie moe oby si bez dodatkowych danych, przekazywanych im przy wywoywaniu. Pierwsze strukturalne jzyki programowania nie oferoway adnego wspomagania w tym zakresie i skazyway na korzystanie wycznie ze zmiennych globalnych. Bardziej nowoczesne produkty pozwalaj jednak na deklaracj parametrw funkcji, co te niejednokrotnie czynimy w praktyce. Aby wywoa funkcj z parametrami, kompilator musi zna ich liczb oraz typ kadego z nich. Informacje te podajemy w prototypie funkcji, za w jej kodzie zwykle nadajemy take nazwy poszczeglnym parametrom, by mc z nich pniej korzysta. Parametry peni rol zmiennych lokalnych w bloku funkcji - z t jednak rnic, e ich pocztkowe wartoci pochodz z zewntrz, od kodu wywoujcego funkcj. Na tym wszake kocz si wszelkie odstpstwa, poniewa parametrw moemy uywa identycznie, jak gdyby byo one zwykymi zmiennymi odpowiednich typw. Po zakoczeniu wykonywania funkcji s one niszczone, nie pozostawiajc adnego ladu po ewentualnych operacjach, ktre mogy by na nich dokonywane kodzie funkcji. Wnioskujemy std, e: Parametry funkcji s w C++ przekazywane przez wartoci. Regua ta dotyczy wszystkich typw parametrw, mimo e w przypadku wskanikw oraz referencji jest ona pozornie amania. To jednak tylko zudzenie. W rzeczywistoci take i tutaj do funkcji s przekazywane wycznie wartoci - tyle tylko, e owymi
Wskaniki
287
wartociami s tu adresy odpowiednich komrek w pamici. Za ich porednictwem moemy wic uzyska dostp do rzeczonych komrek, zawierajcych na przykad jakie zmienne. Gdy dodatkowo korzystamy z referencji, wtedy nie wymaga to nawet specjalnej skadni. Trzeba by jednak wiadomym, e zjawiska te dotycz samej natury wskanikw czy te referencji, nie za parametrw funkcji! Dla nich bowiem zawsze obowizuje przytoczona wyej zasada przekazywania poprzez warto.
288
Podstawy programowania
Ponownie, tak samo jak w przypadku wskanikw na zmienne, moglibymy wywoa nasz funkcj bezporednio. Pamitasz jednake o korzyciach, jakie daje wykorzystanie wskanikw - wikszo z nich dotyczy take wskanikw do funkcji. Ich uycie jest wic czsto bardzo przydatne. Omwmy zatem po kolei wszystkie aspekty wykorzystania wskanikw do funkcji w C++.
Od funkcji do wskanika na ni
Deklaracja wskanika do funkcji jest w C++ do nietypow czynnoci. Nie przypomina bowiem znanej nam doskonale deklaracji w postaci: typ_zmiennej nazwa_zmiennej; Zamiast tego nazwa wskanika jest niejako wtrcona w typ funkcji, co w pierwszej chwili moe by nieco mylce. atwo jednak mona zrozumie tak form deklaracji, jeeli porwnamy j z prototypem funkcji, np.: float Funkcja(int);
91 Posiada te domyln w C++ konwencj wywoania, czyli cdecl. Pniej zobaczymy przykady wskanikw do funkcji, wykorzystujcych inne konwencje.
Wskaniki
289
Ot odpowiadajcy mu wskanik, ktry mgby pokazywa na zadeklarowan wyej funkcj Funkcja(), zostanie wprowadzony do kodu w ten sposb: float (*pfnWskaznik)(int); Nietrudno zauway rnic: zamiast nazwy funkcji, czyli Funkcja, mamy tutaj fraz (*pfnWskaznik), gdzie pfnWskaznik jest oczywicie nazw zadeklarowanego wanie wskanika. Moe on pokazywa na funkcje przyjmujce jeden parametr typu int oraz zwracajce wynik w postaci liczby typu float. Oglnie zatem, dla kadej funkcji o tak wygldajcym prototypie: zwracany_typ nazwa_funkcji([parametry]); deklaracja odpowiadajcego jej wskanika jest bardzo podobna: zwracany_typ (*nazwa_wskanika)([parametry]); Ogranicza si wic do niemal mechanicznej zmiany cile okrelonego fragmentu kodu. Deklaracja wskanika na funkcj o domylnej konwencji wywoania wyglda tak, jak jej prototyp, w ktrym nazwa_funkcji zostaa zastpiona przez (*nazwa_wskanika). Ta prosta zasada sprawdza si w 99 procentach przypadkw i bdziesz z niej stale korzysta we wszystkich programach wykorzystujcych mechanizm wskanikw do funkcji. Trzeba jeszcze podkreli znaczenie nawiasw w deklaracji wskanikw do funkcji. Maj one tutaj niebagateln rol skadniow, gdy ich brak cakowicie zmienia sens caej deklaracji. Gdybymy wic opucili je: void *pfnWskaznik(); // a co to jest?
caa instrukcja zostaaby zinterpretowana jako: void* pfnWskaznik(); // to prototyp funkcji, a nie wskanik na ni!
i zamiast wskanika do funkcji otrzymalibymy funkcj zwracajc wskanik. Jest to oczywicie cakowicie niezgodne z nasz intencj. Pamitaj zatem o poprawnym umieszczaniu nawiasw w deklaracjach wskanikw do funkcji.
Specjalna konwencja
Opisanego powyej sposobu tworzenia deklaracji nie mona niestety uy do wskanikw do funkcji, ktre stosuj inn konwencj wywoania ni domylna (czyli cdecl) i zawieraj odpowiednie sowo kluczowe w swoim naglwku czy te prototypie. W Visual C++ tymi sowami s __cdecl, __stdcall oraz __fastcall. Przykad funkcji podpadajcej pod te warunki moe by nastpujcy: float __fastcall Dodaj(float fA, float fB) { return fA + fB; }
Dodatkowe sowo midzy zwracanym_typem oraz nazw_funkcji cakowicie psuje nam schemat deklaracji wskanikw. Wynik jego zastosowania zostaby bowiem odrzucony przez kompilator:
290
Podstawy programowania
// BD!
Dzieje si tak, poniewa gdy widzi on najpierw nazw typu (float), a potem specyfikator konwencji wywoania (__fastcall), bezdyskusyjne interpretuje ca linijk jako deklaracj funkcji. Nastpujc potem niespodziewan sekwencj (*pfnWskaznik) traktuje wic jako bd skadniowy. By go unikn, musimy rozcign nawiasy, w ktrych umieszczamy nazw wskanika do funkcji i wzi pod ich skrzyda take okrelenie konwencji wywoania. Dziki temu kompilator napotka otwierajcy nawias zaraz po nazwie zwracanego typu (float) i zinterpretuje cao jako deklaracj wskanika do funkcji. Wyglda ona tak: float (__fastcall *pfnWskaznik)(float, float); // OK
Ten, zdawaoby si, szczeg moe niekiedy stan oci w gardle w czasie kompilacji programu. Wypadaoby wic o nim pamita.
Waciwy wskanik, mogcy pokazywa na t funkcj, deklarujemy w ten oto (teraz ju, mam nadziej, oczywisty) sposb:
Wskaniki
int (*pfnWskaznik)();
291
Jak kady wskanik, zaraz po zadeklarowaniu nie pokazuje on na nic konkretnego - w tym przypadku na adn konkretn funkcj. Musimy dopiero przypisa mu adres naszej przygotowanej funkcji PobierzLiczbe(). Czynimy to wic w nastpujcej zaraz linijce kodu: pfnWskaznik = &PobierzLiczbe; Zwrmy uwag, e nazwa funkcji PobierzLiczbe() wystpuje tutaj bez, wydawaoby si - nieodcznych, nawiasw okrgych. Ich pojawienie si oznaczaoby bowiem wywoanie tej funkcji, a my przecie tego nie chcemy (przynajmniej na razie). Pragniemy tylko pobra jej adres w pamici, by mc jednoczenie przypisa go do swojego wskanika. Wykorzystujemy do tego znany ju operator &. Ale niespodzianka! w operator tak naprawd nie jest konieczny. Ten sam efekt osigniemy rwnie i bez niego: pfnWskaznik = PobierzLiczbe; Po prostu ju sam brak nawiasw okrgych (), wyrniajcych wywoanie funkcji, jest wystarczajca wskazwk mwic kompilatorowi, i chcemy pobra adres funkcji o danej nazwie, nie za - wywoywa j. Dodatkowy operator, chocia dozwolony, nie jest wic niezbdny - wystarczy sama nazwa funkcji. Czy nie mamy w zwizku z tym uczucia deja vu? Identyczn sytuacj mielimy przecie przy tablicach i wskanikach na nie. A zatem zasada, ktr tam poznalimy, w poprawionej formie stosuje si rwnie do funkcji: Nazwa funkcji jest take wskanikiem do niej. Nie musimy wic korzysta z operatora &, by pobra adres funkcji. W tym miejscu mamy ju wskanik pfnWskaznik pokazujcy na nasz funkcj PobierzLiczbe(). Ostatnim aktem bdzie wywoanie jej za porednictwem tego wskanika, co czynimy poniszym wierszem kodu: std::cout << (*pfnWskaznik)(); Liczb otrzyman z funkcji wypisujemy na ekranie, ale najpierw wywoujemy sam funkcj, korzystajc midzy innymi z nastpnego znajomego operatora - dereferencji, czyli *. Po raz kolejny jednak nie jest to niezbdne! Wywoanie funkcji przy pomocy wskanika mona z rwnym powodzeniem zapisa te w takiej formie: std::cout << pfnWskaznik(); Jest to druga konsekwencja faktu, i funkcja jest reprezentowana w kodzie poprzez swj wskanik. Taki sam fenomen obserwowalimy i dla tablic.
292
Podstawy programowania
funkcje maj nieodmiennie t sam charakterystyk (pobieraj liczb rzeczywist i tak te liczb zwracaj w wyniku). Moemy wic zaimplementowa odpowiedni algorytm (tutaj jest to algorytm bisekcji92) w sposb oglny - posugujc si wskanikami do funkcji. Przykadowy program wykorzystujcy t technik moe przedstawia si nastpujco: // Zeros - szukanie miejsc zerowych funkcji // granica toleracji const double EPSILON = 0.0001; // rozpieto badanego przedziau const double PRZEDZIAL = 100; // wspczynniki funkcji f(x) = k * log_a(x - p) + q double g_fK, g_fA, g_fP, g_fQ; // ---------------------------------------------------------------------// badana funkcja double f(double x) { return g_fK * (log(x - g_fP) / log(g_fA)) + g_fQ; } // algorytm szukajcy miejsca zerowego danej funkcji w danym przedziale bool SzukajMiejscaZerowego(double fX1, double fX2, // przedzia double (*pfnF)(double), // funkcja double* pfZero) // wynik { // najpierw badamy koce podanego przedziau if (fabs(pfnF(fX1)) < EPSILON) { *pfZero = fX1; return true; } else if (fabs(pfnF(fX2)) < EPSILON) { *pfZero = fX2; return true; } // // // if dalej sprawdzamy, czy funkcja na kocach obu przedziaw przyjmuje wartoci rnych znakw jeeli nie, to nie ma miejsc zerowych ((pfnF(fX1)) * (pfnF(fX2)) > 0) return false;
// nastpnie dzielimy przedzia na p i sprawdzamy, czy w ten sposb // nie otrzymalimy pierwiastka double fXp = (fX1 + fX2) / 2; if (fabs(pfnF(fXp)) < EPSILON) { *pfZero = fXp; return true; } // jeli otrzymany przedzia jest wystarczajco may, to rozwizaniem // jest jego punkt rodkowy if (fabs(fX2 - fX1) < EPSILON)
92 Oprcz niego popularna jest rwnie metoda Newtona, ale wymaga ona znajomoci rwnie pierwszej pochodnej funkcji.
Wskaniki
{ }
293
// jezeli nadal nic z tego, to wybieramy t powk przedziau, // w ktrej zmienia si znak funkcji if ((pfnF(fX1)) * (pfnF(fXp)) < 0) fX2 = fXp; else fX1 = fXp; // przeszukujemy ten przedzia tym samym algorytmem return SzukajMiejscaZerowego(fX1, fX2, pfnF, pfZero);
// ---------------------------------------------------------------------// funkcja main() void main() { // (pomijam pobranie wspczynnikw k, a, p i q dla funkcji) /* znalezienie i wywietlenie miejsca zerowego */ // zmienna na owo miejsce double fZero; // szukamy miejsca i je wywietlamy std::cout << std::endl; if (SzukajMiejscaZerowego(g_fP > -PRZEDZIAL ? g_fP : -PRZEDZIAL, PRZEDZIAL, f, &fZero)) std::cout << "f(x) = 0 <=> x = " << fZero << std::endl; else std::cout << "Nie znaleziono miejsca zerowego." << std::endl; // czekamy na dowolny klawisz getch();
f ( x) = k log a ( x p) + q
Najpierw zadaje wic uytkownikowi pytania co do wartoci wspczynnikw k, a, p i q w tym rwnaniu, a nastpnie pogr si w obliczeniach, by ostatecznie wywietli wynik. Niniejszy program jest przykadem zastosowania wskanikw na funkcje, a nie rozwizywania rwna. Jeli chcemy wyliczy miejsce zerowe powyszej funkcji, to znacznie lepiej bdzie po prostu przeksztaci j, wyznaczajc x:
q x = exp a + p k
294
Podstawy programowania
Oczywicie w niniejszym programie najbardziej interesujca bdzie dla nas funkcja SzukajMiejscaZerowego() - gwnie dlatego, e wykorzystany w niej zosta mechanizm wskanikw na funkcje. Ewentualnie moesz te zainteresowa si samym algorytmem; jego dziaanie cakiem dobrze opisuj obfite komentarze :) Gdzie jest wic w sawetny wskanik do funkcji? Znale go moemy w nagwku SzukajMiejscaZerowego(): bool SzukajMiejscaZerowego(double fX1, double fX2, double (*pfnF)(double), double* pfZero) To nie pomyka - wskanik do funkcji (biorcej jeden parametr double i zwracajcej take typ double) jest tutaj argumentem innej funkcji. Nie ma ku temu adnych przeciwwskaza, moe poza do dziwnym wygldem nagwka takiej funkcji. W naszym przypadku, gdzie funkcja jest swego rodzaju danymi, na ktorych wykonujemy operacje (szukanie miejsca zerowego), takie zastosowanie wskanika do funkcji jest jak najbardziej uzasadnione. Pierwsze dwa parametry funkcji poszukujcej s natomiast liczbami okrelajcymi przedzia poszukiwa pierwiastka. Ostatni parametr to z kolei wskanik na zmienn typu double, poprzez ktr zwrcony zostanie ewentualny wynik. Ewentualny, gdy o powodzeniu lub niepowodzeniu zadania informuje regularny rezultat funkcji, bdcy typu bool. Nasz funkcj szukajc wywoujemy w programie w nastpujcy sposb: double fZero; if (SzukajMiejscaZerowego(g_fP > -PRZEDZIAL ? g_fP : -PRZEDZIAL, PRZEDZIAL, f, &fZero)) std::cout << "f(x) = 0 <=> x = " << fZero << std::endl; else std::cout << "Nie znaleziono miejsca zerowego." << std::endl; Przekazujemy jej tutaj a dwa wskaniki jako ostatnie parametry. Trzeci to, jak wiemy, wskanik na funkcj - w tej roli wystpuje tutaj adres funkcji f(), ktr badamy w poszukiwaniu miejsc zerowych. Aby przekaza jej adres, piszemy po prostu jej nazw bez nawiasw okrgych - tak jak si tego nauczylimy niedawno. Czwarty parametr to z kolei zwyky wskanik na zmienn typu double i do tej roli wystawiamy adres specjalnie przygotowanej zmiennej. Po zakoczonej powodzeniem operacji poszukiwania wywietlamy jej warto poprzez strumie wyjcia. Jeeli za chodzi o dwa pierwsze parametry, to okrelaj one obszar poszukiwa, wyznaczony gwnie poprzez sta PRZEDZIAL. Dolna granica musi by dodatkowo
Wskaniki
przycita z dziedzin funkcji - std te operator warunkowy ?: i porwnanie granicy przedziau ze wspczynnikiem p.
295
Powiedzmy sobie jeszcze wyranie, jaka jest praktyczna korzy z zastosowania wskanikw do funkcji w tym programie, bo moe nie jest ona zbytnio widoczna. Ot majc wpisany algorytm poszukiwa miejsca zerowego w oglnej wersji, dziaajcy na wskanikach do funkcji zamiast bezporednio na funkcjach, moemy stosowa go do tylu rnych funkcji, ile tylko sobie zayczymy. Nie wymaga to wicej wysiku ni jedynie zdefiniowania nowej funkcji do zbadania i przekazania wskanika do niej jako parametru do SzukajMiejscaZerowego(). Uzyskujemy w ten sposb wiksz elastyczno programu.
Zastosowania
Poprawa elastycznoci nie jest jednak jedynym, ani nawet najwaniejszym zastosowaniem wskanikw do funkcji. Tak naprawd stosuje si je glwnie w technice programistycznej znanej jako funkcje zwrotne (ang. callback functions). Do powiedzie, e opieraj si na niej wszystkie nowoczesne systemy operacyjne, z Windows na czele. Umoliwia ona bowiem informowanie programw o zdarzeniach zachodzcych w systemie (wywoanych na przykad przez uytkownika, jak kliknicie myszk) i odpowiedniego reagowania na nie. Obecnie jest to najczstsza forma pisania aplikacji, zwana programowaniem sterowanym zdarzeniami. Kiedy rozpoczniemy tworzenie aplikacji dla Windows, take bdziemy z niej nieustannie korzysta. *** I tak zakoczylimy nasze spotkanie ze wskanikami do funkcji. Nie s one moe tak czsto wykorzystywane i przydatne jak wskaniki na zmienne, ale, jak moge przeczyta, jeszcze wiele razy usyszysz o nich i wykorzystasz je w przyszoci. Warto wic byo dobrze pozna ich skadni (fakt, jest nieco zagmatwana) oraz sposoby uycia.
Podsumowanie
Wskaniki s czsto uwaane za jedn z natrudniejszych koncepcji programistycznych w ogle. Wielu cakiem dobrych koderw ma niekiedy wiksze lub mniejsze kopoty w ich stosowaniu. Celowo nie wspomniaem o tych opiniach, aby mg najpierw samodzielnie przekona si o tym, czy zagadnienie to jest faktycznie takie skomplikowane. Dooyem przy tym wszelkich stara, by uczyni je chocia troch prostszym do zrozumienia. Jednoczenie chciaem jednak, aby zawarty tu opis wskanikw by jak najbardziej dokadny i szczegowy. Wiem, e pogodzenie tych dwch de jest prawie niemoliwe, ale mam nadziej, e wypracowaem w tym rozdziale w miar rozsdny kompromis. Zaczem wic od przedstawienia garci przydatnych informacji na temat samej pamici operacyjnej komputera. Podejrzewam, e wikszo czytelnikw nawet i bez tego bya wystarczajco obeznana z tematem, ale przypomnie i uzupenie nigdy do :) Przy okazji wprowadzilimy sobie samo pojcie wskanika. Dalej zajlimy si wskanikami na zmienne, ich deklarowaniem i wykorzystaniem: do wspomagania pracy z tablicami, przekazywania parametrw do funkcji czy wreszcie dynamicznej alokacji pamici. Poznalimy te referencje. Podrozdzia o wskanikach na funkcje skada si natomiast z poszerzenia wiadomoci o samych funkcjach oraz wyczerpujcego opisu stosowania wskanikw na nie.
296
Podstawy programowania
Nieniejszy rozdzia jest jednoczenie ostatnim z czci 1, stanowicej podstawowy kurs C++. Po nim przejdziemy (wreszcie ;D) do bardziej zaawansowanych zagadnie jzyka, Biblioteki Standardowej, a pniej Windows API i DirectX, a wreszcie do programowania gier. A zatem pierwszy duy krok ju za nami, lecz nadal szykujemy si do wielkiego skoku :)
Pytania i zadania
Tradycji musi sta si zado: oto wiea porcja pyta dotyczcych treci tego rozdziau oraz wicze do samodzielnego rozwizania.
Pytania
Jakie s trzy rodzaje pamici wykorzystywanej przez komputer? Na czym polega paski model adresowania pamici operacyjnej? Czym jest wskanik? Co to jest stos i sterta? W jaki sposb deklarujemy w C++ wskaniki na zmienne? Jak dziaaj operatory pobrania adresu i dereferencji? Czym rzni si wskanik typu void* od innych? Dlaczego acuchy znakw w stylu C nazywamy napisami zakoczonymi zerem? Dlaczego uywanie wskanikw lub referencji jako parametrw funkcji moe poprawi wydajno programu? 10. W jaki sposb dynamicznie alokujemy zmienne, a w jaki tablice? 11. Co to jest wyciek pamici? 12. Czym rni si referencje od wskanikw na zmienne? 13. Jakie podstawowe konwencje wywoywania funkcji s obecnie w uyciu? 14. (Trudne) Czy funkcja moe nie uywa adnej konwencji wywoania? 15. Jakie s trzy cechy wyznaczajce typ funkcji i jednoczenie typ wskanika na ni? 16. Jak zadeklarowa wskanik do funkcji o znanym prototypie? 1. 2. 3. 4. 5. 6. 7. 8. 9.
wiczenia
1. Przejrzyj przykadowe kody z poprzednich rozdziaw i znajd instrukcje, wykorzystujce wskaniki lub operatory wskanikowe. 2. Zmodyfikuj nieco metod ZmienRozmiar() klasy CIntArray. Niech pozwala ona take na zmniejszenie rozmiaru tablicy. 3. Sprbuj napisa podobn klas dla tablicy dwuwymiarowej. (Trudne) Niech przechowuje ona elementy w cigym obszarze pamici - tak, jak robi to kompilator ze statycznymi tablicami dwuwymiarowymi. 4. Zadeklaruj wskanik do funkcji: 1) pobierajcej jeden parametr typu int i zwracajcej wynik typu float 2) biorcej dwa parametry typu double i zwracajcej acuch std::string 3) pobierajcej trzy parametry: jeden typu int, drugi typu __int64, a trzeci typu std::string i zwracajcej wskanik na typ int 4) (Trudniejsze) przyjmujcej jako parametr picioelementow tablic liczb typu unsigned i nic niezwracajc 5) (Trudne) zwracajcej warto typu float i przyjmujcej jako parametr wskanik do funkcji biorcej dwa parametry typu int i nic niezwracajcej 6) (Trudne) pobierajcej tablic picioelementow typu short i zwracajcej jedn liczb typu int 7) (Bardzo trudne) biorcej dwa parametry: jeden typu char, a drugi typu int, i zwracajcej tablic 10 elementw typu double Wskazwka: to nie tylko trudne, ale i podchwytliwe :)
Wskaniki
297
8) (Ekstremalne) przyjmujcej jeden parametr typu std::string oraz zwracajcej w wyniku wskanik do funkcji przyjmujcej dwa parametry typu float i zwracajcej wynik typu bool 5. Okrel typy parametrw oraz typ wartoci zwracanej przez funkcje, na ktre moe pokazywa wskanik o takiej deklaracji: a) int (*pfnWskaznik)(int); b) float* (*pfnWskaznik)(const std::string&); c) bool (*pfnWskaznik)(void* const, int**, char); d) const unsigned* const (*pfnWskaznik)(void); e) (Trudne) void (*pfnWskaznik)(int (*)(bool), const char*); f) (Trudne) int (*pfnWskaznik)(char[5], tm&); g) (Bardzo trudne) float (*pfnWskaznik(short, long, bool))(int, int);
2
Z AAWANSOWANE C++
1
PREPROCESOR
Gdy si nie wie, co si robi, to dziej si takie rzeczy, e si nie wie, co si dzieje ;-).
znana prawda programistyczna
Poznawanie bardziej zaawansowanych cech jzyka C++ zaczniemy od czego, co pochodzi jeszcze z czasw jego poprzednika, czyli C. Podobnie jak wskaniki, preprocesor nie pojawi si wraz z dwoma plusami w nazwie jzyka i programowaniem zorientowanym obiektowo, lecz by obecny od jego samych pocztkw. W przypadku wskanikw trzeba jednak powiedzie, e s one take i teraz niezbdne do efektywnego i poprawnego konstruowania aplikacji. Natomiast o proceprocesorze niewielu ma tak pochlebne zdanie: wedug sporej czci programistw, sta si on prawie zupenie niepotrzebny wraz z wprowadzeniem do C++ takich elementw jak funkcje inline oraz szablony. Poza tym uwaa si powszechnie, e czste i intensywne uywanie tego narzdzia pogarsza czytelno kodu. W tym rozdziale bd musia odpowiedzie jako na te opinie. Nie da si ukry, e niektre z nich s suszne: rzeczywicie, era wietnoci preprocesora jest ju dawno za nami. Zgadza si, nadmierne i nieuzasadnione wykorzystywanie tego mechanizmu moe przynie wicej szkody ni poytku. Tym bardziej jednak powiniene wiedzie jak najwicej na temat tego elementu jzyka, aby mc stosowa go poprawnie. Od korzystania z niego nie mona bowiem uciec. Cho moe nie zdawae sobie z tego sprawy, lecz korzystae z niego w kadym napisanym dotd programie w C++! Wspomnij sobie choby dyrektyw #include Dotd jednak zadowalae si lakonicznym stwierdzeniem, i tak po prostu trzeba. Lektur tego rozdziau masz szans to zmieni. Teraz bowiem omwimy sobie zagadnienie preprocesora w caoci, od pocztku do koca i od rodka :)
Pomocnik kompilatora
Rozpocz wypadaoby od przedstawienia gwnego bohatera naszej opowieci. Czym jest wic preprocesor? Preprocesor to specjalny mechanizm jzyka, ktry przetwarza tekst programu jeszcze przed jego kompilacj. To jakby przedsionek waciwego procesu kompilacji programu. Preprocesor przygotowuje kod tak, aby kompilator mg go skompilowa zgodnie z yczeniem programisty. Bardzo czsto uwalnia on te od koniecznoci powtarzania czsto wystpujcych i potrzebnych fragmentw kodu, jak na przykad deklaracji funkcji. Kiedy wiemy ju mniej wicej, czym jest preprocesor, przyjrzymy si wykonywanej przez niego pracy. Dowiemy si po prostu, co on robi.
302
Zaawansowane C++
Gdzie on jest?
Obecno w procesie budowania aplikacji nie jest taka oczywista. Cakiem dua liczba jzykw radzi sobie, nie posiadajc w ogle narzdzia tego typu. Rwnie cel jego istnienia wydaje si niezbyt klarowny: dlaczego kod naszych programw miaby wymaga przed kompilacj jakich przerbek? T drug wtpliwo wyjani kolejne podrozdziay, opisujce moliwoci i polecenia preprocesora. Obecnie za okrelimy sobie jego miejsce w procesie tworzenia wynikowego programu.
Przy takim modelu kompilacji zawarto kadego moduu musi wystarcza do jego samodzielnej kompilacji, niezalenej od innych moduw. W przypadku jzykw z rodziny C oznacza to, e kady modu musi zawiera deklaracje uywanych funkcji oraz definicje klas, ktrych obiekty tworzy i z ktrych korzysta. Gdyby zadanie doczania tych wszystkich deklaracji spoczywao na programicie, to byoby to dla niego niezmiernie uciliwe. Pliki z kodem zostay ponadto rozdte do nieprzyzwoitych rozmiarw, a i tak wikszo zawartych we informacji przydawayby si tylko przez chwil. Przez t chwil, ktr zajmuje kompilacja moduu.
Preprocesor
303
Nic wic dziwnego, e aby zapobiec podobnym irracjonalnym wymaganiom wprowadzono mechanizm preprocesora.
Dodajemy preprocesor
Ujawni si nam pierwszy cel istnienia preprocesora: w jzyku C(++) suy on do czenia w jedn cao moduw kodu wraz z deklaracjami, ktre s niezbdne do dziaania tego kodu. A skd brane s te deklaracje? Oczywicie - z plikw nagwkowych. Zawieraj one przecie prototypy funkcji i definicje klas, z jakich mona korzysta, jeeli doczy si dany nagwek do swojego moduu. Jednak kompilator nic nie wie o plikach nagwkowych. On tylko oczekuje, e zostan mu podane pliki z kodem rdowym, do ktrego bd si zaliczay take deklaracje pewnych zewntrznych elementw - nieobecnych w danym module. Kompilator potrzebuje tylko ich okrelenia z wierzchu, bez wnikania w implementacj, gdy ta moe znajdowa si w innych moduach lub nawet innych bibliotekach i staje si wana dopiero przy linkowaniu. Nie jest ju ona spraw kompilatora - on da tylko tych informacji, ktre s mu potrzebne do kompilacji. Niezbdne deklaracje powinny si znale na pocztku kadego moduu. Trudno jednak oczekiwa, ebymy wpisywali je rcznie w kadym module, ktry ich wymaga. Byoby to niezmiernie uciliwe, wic wymylono w tym celu pliki nagwkowe i preprocesor. Jego zadaniem jest tutaj poczenie napisanych przez nas moduw oraz plikw nagwkowych w pliki z kodem, ktre mog mog by bez przeszkd przetworzone przez kompilator.
304
Zaawansowane C++
Skd preprocesor wie, jak ma to zrobi? Ot, mwimy o tym wyranie, stosujc dyrektyw #include. W miejscu jej pojawienia si zostaje po prostu wstawiona tre odpowiedniego pliku nagwkowego. Wczanie nagwkw nie jest jednak jedynym dziaaniem podejmowanym przez preprocesor. Gdyby tak byo, to przecie nie powicalibymy mu caego rozdziau :) Jest wrcz przeciwnie: doczanie plikw to tylko jedna z czynnoci, jak moemy zleci temu mechanizmowi - jedna z wielu czynnoci Wszystkie zadania preprocesora s rnorodne, ale maj te kilka cech wsplnych. Przyjrzyjmy si im w tym momencie.
Dziaanie preprocesora
Komendy, jakie wydajemy preprocesorowi, rni si od normalnych instrukcji jzyka programowania. Take sposb, w jaki preprocesor traktuje kod rdowy, jest zupenie inny.
Dyrektywy
Polecenie dla preprocesora nazywamy jego dyrektyw (ang. directive). Jest to specjalna linijka kodu rdowego, rozpoczynajca si od znaku # (hash), zwanego potkiem93: # Na nim te moe si zakoczy - wtedy mamy do czynienia z dyrektyw pust. Jest ona ignorowana przez preprocesor i nie wykonuje adnych czynnoci. Bardziej praktyczne s inne dyrektywy, ktrych nazwy piszemy zaraz za znakiem #. Nie oddzielamy ich zwykle adnymi spacjami (cho mona to robi), wic w praktyce potek staje si czci ich nazw. Mwi si wic o instrukcjach #include, #define, #pragma i innych, gdy w takiej formie zapisujemy je w kodzie. Dalsza cz dyrektywy zaley ju od jej rodzaju. Rne parametry dyrektyw poznamy, gdy zajmiemy si szczegowo kad z nich.
Bez rednika
Jest bardzo wane, aby zapamita, e: Dyrektywy preprocesora kocz si zawsze przejciem do nastpnego wiersza. Innymi sowy, jeeli preprocesor napotka w swojej dyrektywie na znak koca linijki (nie wida go w kodzie, ale jest on dodawany po kadym wciniciu Enter), to uznaje go take za koniec dyrektywy. Nie ma potrzeby wpisywania rednika na zakoczenie instrukcji. Wicej nawet: nie powinno si go wpisywa! Zostanie on bowiem uznany za cz dyrektywy, co w zalenoci od jej rodzaju moe powodowa rne niepodane efekty. Kocz si one zwykle bdami kompilacji. Zapamitaj zatem zalecenie: Nie kocz dyrektyw preprocesora rednikiem. Nie s to przecie instrukcje jzyka programowania, lecz polecenia dla moduu wspomagajcego kompilator.
93 Przed hashem mog znajdowa si wycznie tzw. biae znaki, czyli spacje lub tabulatory. Zwykle nie znajduje si nic.
Preprocesor
Mona natomiast koczy dyrektyw komentarzem, opisujcym jej dziaanie. Kiedy wiele kompilatorw miao z tym kopoty, ale obecnie wszystkie liczce si produkty potrafi radzi sobie z komentarzami na kocu dyrektyw preprocesora.
305
Twrca jzyka C++, Bjarne Stroustrup, wprowadzi do niego sekwencje trjznakowe z powodu swojej klawiatury. W wielu duskich ukadach klawiszy zamiast przydatnych symboli z prawej kolumny tabeli widniay bowiem znaki typu , czy . Aby umoliwi swoim rodakom programowanie w stworzonym jzyku, Stroustrup zdecydowa si na ten zabieg. Dzisiaj obecno trjznakw nie jest taka wana, bo powszechnie wystpuj na caym wiecie klawiatury typu Sholesa, ktre zawieraj potrzebne w C++ znaki. Moglibymy wic o nich zapomnie, ale
Aby zapisa liczb w systemie szesnastkowym, naley j poprzedzi sekwencj 0x lub 0X. Tak wic 0xFF to dziesitnie 255.
94
306
Zaawansowane C++
No wanie, jest pewien problem. Z niewiadomych przyczyn jest czsto tak, e nieuywana funkcja prdzej czy pniej daje o sobie zna niczym przeterminowana konserwa. Prawie zawsze te nie jest to zbyt przyjemne. Kopot polega na tym, e jedna z sekwencji - ??! - moe by uyta w sytuacji wcale odmiennej od zaoonego zastpowania znaku |. Popatrzmy na ten kod: std::cout << "Co mowisz??!"; Nie wypisze on wcale stanowczej proby o powtrzenie wypowiedzi, lecz napis "Co mowisz|". Trjznak ??! zosta bowiem zastpiony przez |. Mona tego unikn, stosujc jedn z tzw. sekwencji ucieczki (unikowych, ang. escape sequences) zamiast znakw zapytania. Poprawiony kod bdzie wyglda tak: std::cout << "Co mowisz\?\?!"; Podobn niespodziank moemy te sobie sprawi, gdy podczas wpisywania trzech znakw zapytania za wczenie zwolnimy klawisz Shift. Powstanie nam wtedy co takiego: std::cout << "Co??/"; Taka sytuacja jest znacznie perfidniejsza, bowiem trjznak ??/ zostanie zastpiony przez pojedynczy znak \ (backslash). Doprowadzi to do powstania niekompletnego napisu "Co\". Niekompletnego, bo wystpuje tu sekwencja unikowa \", zastpujca cudzysw. Znak cudzysowu, ktry tu widzimy, nie bdzie wcale oznacza koca napisu, lecz jego cz. Kompilator bdzie za oczekiwa, e waciwy cudzysw koczcy znajduje si gdzie dalej, w tej samej linijce kodu. Nie napotka go oczywicie, a to oznacza dla nas kopoty Musimy wic pamita, aby bacznie przyglda si kademu wystpieniu dwch znakw zapytania w kodzie C++. Takie skamieniae okazy nawet po wielu latach mog dotkliwie ksa nieostronego programist.
Preprocesor
307
Makra
Makro (ang. macro) jest instrukcj dla preprocesora, pozwalajc dokonywa zastpienia pewnego wyraenia innym. Dziaa ona troch jak funkcja Znajd i zamie w edytorach tekstu, z tym e proces zamiany dokonuje si wycznie przed kompilacj i nie jest trway. Pliki z kodem rdowym nie s fizycznie modyfikowane, lecz tylko zmieniona ich posta trafia do kompilatora. Makra w C++ (zwane aczkolwiek czciej makrami C) potrafi by te nieco bardziej wyrafinowane i dokonywa zoonych, sparametryzowanych operacji zamiany tekstu. Takie makra przypominaj funkcje i zajmiemy si nimi nieco dalej. Definicja makra odbywa si przy pomocy dyrektywy #define: #define odwoanie tekst Najoglniej mwic, daje to taki efekt, i kade wystpienie odwoania w kodzie programu powoduje jego zastpienie przez tekst. Szczegy tego procesu zale od tego, czy nasze makro jest proste - udajce sta - czy moe bardziej skomplikowane udajce funkcj. Osobno zajmiemy si kadym z tych dwch przypadkw. Do pary z #define mamy jeszcze dyrektyw #undef: #undef odwoanie Anuluje ona poprzedni definicj makra, pozwalajc na przykad na jego ponowne zdefiniowanie. Makro w swej aktualnej postaci jest wic dostpne od miejsca zdefiniowania do wystpienia #undef lub koca pliku.
Proste makra
W prostej postaci dyrektywa #define wyglda tak: #define wyraz [zastpczy_cig_znakw] Powoduje ona, e w pliku wysanym do kompilacji kade samodzielne95 wystpienie wyrazu zostanie zastpione przez podany zastpczy_cig_znakw. Mwimy o tym, e makro zostanie rozwinite. W wyrazie mog wystpi tylko znaki dozwolone w nazwach jzyka C++, a wic litery, cyfry i znak podkrelenia. Nie moe on zawiera spacji ani innych biaych znakw, gdy w przeciwnym razie jego cz zostanie zinterpretowana jako tre makra (zastpczy_cig_znakw), a nie jako jego nazwa. Tre makra, czyli zastpczy_cig_znakw, moe natomiast zawiera biae znaki. Moe take nie zawiera znakw - nie tylko biaych, ale w ogle adnych. Wtedy kade wystapienie wyrazu zostanie usunite przez preprocesor z pliku rdowego.
308
int aTablica[SIEDEM]; for (unsigned i = 0; i < SIEDEM; ++i) std::cout << aTablica[i] << std::endl; std::cout << "Wypisalem SIEDEM elementow tablicy";
Zaawansowane C++
Nie moemy tego wprawdzie zobaczy, ale uwierzmy (lub sprawdmy empirycznie poprzez kompilacj), e preprocesor zamieni powyszy kod na co takiego: std::cout << 7 << "elementow tablicy" << std::endl;; int aTablica[7]; for (unsigned i = 0; i < 7; ++i) std::cout << aTablica[i] << std::endl; std::cout << "Wypisalem SIEDEM elementow tablicy"; Zauwamy koniecznie, e: Preprocesor nie dokonuje zastpowania nazw makr wewntrz napisw. Jest to uzasadnione, bo wewntrz acucha nazwa moe wystpowa w zupenie innym znaczeniu. Zwykle wic nie chcemy, aby zostaa ona zastpiona przez rozwinicie makra. Jeeli jednak yczymy sobie tego, musimy potraktowa makro jak zmienn, czyli na przykad tak: std::cout << "Wypisalem " << SIEDEM << " elementow tablicy"; Poza acuchami znakw makro jest bowiem wystawione na dziaanie preprocesora. Zgodnie z przyjt powszechnie konwencj, nazwy makr piszemy wielkimi literami. Nie jest to rzecz jasna obowizkowe, ale poprawia czytelno kodu.
Jest to przydatne, jeli w kodzie funkcji mamy wiele miejsc, ktre mog wymaga jej zakoczenia. Kadorazowe rczne wpisywanie tego kodu byoby wic uciliwe, za z pomoc makra staje si proste. Przypomnijmy jeszcze, jak to dziaa. Jeeli mamy tak oto funkcj: void Funkcja() { // ... if // if // if // (!DrugaFunkcja()) ZAKONCZ; ... (!TrzeciaFunkcja()) ZAKONCZ; ... (CosSieStalo()) ZAKONCZ; ...
Preprocesor
309
to preprocesor zamieni j na co takiego: void Funkcja() { // ... if // if // if // (!DrugaFunkcja()) { g_nZmienna = 0; return; }; ... (!TrzeciaFunkcja()) { g_nZmienna = 0; return; }; ... (CosSieStalo()) { g_nZmienna = 0; return; }; ...
Wyodrbnienie kodu w postaci makra ma t zalet, e jeli nazwa zmiennej g_nZmienna zmieni si (;D), to modyfikacj poczynimy tylko w jednym miejscu - w definicji makra. Spjrzmy jeszcze, i tre makra ujem w nawiasy klamrowe. Gdybym tego nie zrobi, to otrzymalibymy kod typu: if (!DrugaFunkcja()) g_nZmienna = 0; return;; Nie wida tego wyranie, ale kodem wykonywanym w razie prawdziwoci warunku if jest tu tylko wyzerowanie zmiennej. Instrukcja return zostanie wykonana niezalenie od okolicznoci, bo znajduje si poza blokiem warunkowym. Przyzwoity kompilator powie nam o tym, bo obecno takiej zgubionej instrukcji powoduje zbdno caego dalszego kodu funkcji. Nie zawsze jednak korzystamy z makr zawierajcych return, zatem: Zawsze umieszczajmy tre makr w nawiasach. Jak si niedugo przekonamy, ta stanowcza sugestia dotyczy te makr typu staych (jak SIEDEM z pierwszego przykadu), lecz w ich przypadku chodzi o nawiasy okrge. Wtpliwoci moe budzi nadmiar rednikw w powyszych przykadach. Poniewa jednak nie poprzedzaj ich adne instrukcje, wic dodatkowe redniki zostan zignorowane przez kompilator. Akurat w tej sytuacji nie jest to problemem
W kilku linijkach
Piszc makra zastpujce cae poacie kodu, moemy je podzieli na kilka linijek. W tym celu korzystamy ze znaku \ (backslash), np. w ten sposb: #define WYPISZ_TABLICE for (unsigned i = 0; i < 10; ++i) { std::cout << i << "-ty element"; std::cout << nTab[i] << std::endl; } \ \ \ \
Pamitajmy, e to konieczne tylko dla dyrektyw preprocesora. W przypadku zwykych instrukcji wiemy doskonale, e ich podzia na linie jest cakowicie dowolny.
310
#define PROMIEN 10 #define OBWOD_KOLA (2 * PI * PROMIEN)
Zaawansowane C++
Mwic cilej, to makra mog korzysta ze wszystkich informacji dostpnych w czasie kompilacji programu, a wic np. operatora sizeof, typw wyliczeniowych lub staych.
Zasig
Brak zasigu jest szczeglnie dotkliwy. Makra maj wprawdzie zakres obowizywania, wyznaczany przez dyrektywy #define i #undef (wzgldnie koniec pliku), ale absolutnie nie jest to tosame pojcia. Makro zdefiniowane - jak si zdaje - wewntrz funkcji: void Funkcja() { #define STALA 1500.100900 } nie jest wcale dostpne tylko wewntrz niej. Z rwnym powodzeniem moemy z niego korzysta take w kodzie nastpujcym dalej. Wszystko dlatego, e preprocesor nie zdaje sobie w ogle sprawy z istnienia takiego czego jak funkcje czy bloki kodu, a ju na pewno nie zasig zmiennych. Nie jest zatem dziwne, e jego makra nie posiadaj zasigu.
Preprocesor
311
makro - pod wzgldem zerowego wykorzystania pamici. Jednoczenie zachowa te podane cechy zmiennej. Mamy wic dwie pieczenie na jednym ogniu, a makra mog si spali ze wstydu ;)
Typ
Makra nie maj te typw. Jak to?!, odpowiesz. A czy 67 jest napisem, albo czy "klawiatura" jest liczb? A przecie i te, i podobne wyraenia mog by treci makr! Faktycznie wyraenia te maj swoje typy i mog by interpretowane tylko w zgodzie z nimi. Ale jakie s to typy? 67 moe by przecie rwnie dobrze uznana za warto int, jak i BYTE, unsigned, nawet float. Z kolei napis jest formalnie typu const char[], ale przecie moemy go przypisa do obiektu std::string. Poprzez wystpowanie niejawnych konwersji (powiemy sobie o nich w nastpnym rozdziale) sytuacja z typami nie jest wic taka prosta. A makra dodatkowo j komplikuj, bo nie pozwalaj na ustalenie typu staej. Nasze 67 mogo by przecie docelowo typu float, ale staa zdefiniowana jako: #define STALA 67 zostanie bez przeszkd przyjta dla kadego typu liczbowego. O to nam chyba nie chodzio?! Z tym problemem mona sobie aczkolwiek poradzi, nie uciekajc od #define. Pierwszym wyjciem jest jawne rzutowanie: #define (float) 67 Chyba nieco lepsze jest dodanie do liczby odpowiedniej kocwki, umoliwiajcej inn interpretacj jej typu. Stosujc te kocwki moemy zmieni typ wyraenia wpisanego w kodzie. Oto jak zmienia si typ liczby 67, gdy dodamy jej rne sufiksy (nie s to wszystkie moliwoci): liczba 67 67u 67.0 67.0f typ int unsigned int double float
Przewaga staych const zwizana z typami objawia si najpeniej, gdy chodzi o tablice. Nie ma bowiem adnych przeciwskaza, aby zadeklarowa sobie tablic wartoci staych: const int STALE = { 1, 2, 3, 4 }; a potem odwoywa si do jej poszczeglnych elementw. Podobne dziaanie jest cakowicie niemoliwe dla makr.
Efekty skadniowe
Z wartociami staymi definiowanymi jako makra zwizane te s pewne nieoczekiwane i trudne do przewidzenia efekty skadniowe. Powoduje je fakt, i dziaanie preprocesora jest operacj na zwykym tekcie, a kod przecie zwykym tekstem nie jest
rednik
Podkrelaem na pocztku, e dyrektyw preprocesora, w tym i #define, nie naley koczy rednikiem. Ale co by si stao, gdyby nie zastosowa si do tego zalecenia? Sprawdmy. Zdefiniujmy na przykad takie oto makro:
312
Zaawansowane C++
// uwaga, rednik!
Niby rnica jest niewielka, ale zaraz zobaczymy jak bardzo jest ona znaczca. Uyjmy teraz naszego makra, w jakim wyraeniu: int nZmienna = 2 * DZIESIEC; Dziaa? Tak Preprocesor zamienia DZIESIEC na 10;, co w sumie daje: int nZmienna = 2 * 10;; Dodatkowy rednik, jaki tu wystpuje, nie sprawia kopotw, lecz atwo moe je wywoa. Wystarczy choby przestawi kolejno czynnikw lub rozbudowa wyraenie - na przykad umieci w nim wywoanie funkcji: int nZmienna = abs(2 * DZIESIEC); I tu zaczynaj si kopoty. Preprocesor wyprodukuje z powyszego wiersza kod: int nZmienna = abs(2 * 10;); // ups!
ktry z pewnoci zostanie odrzucony przez kady kompilator. Susznie jednak stwierdzisz, e takie czy podobne bdy (np. uycie DZIESIEC jako rozmiaru tablicy) s stosunkowo proste do wykrycia. Lecz przy uywaniu makr nie zawsze tak jest: zaraz zobaczysz, e nietrudno dopuci si pomyek niewpywajcych na kompilacj, ale wypywajcych na powierzchni ju w gotowym programie.
std::cout << "Gestosc zaludnienia wynosi: " << LUDNOSC / POLE; Powinien on wydrukowa liczb 50, prawda? No c, zobaczmy czy tak bdzie naprawd. Wyraenie LUDNOSC / POLE zostanie rozwinite przez preprocesor do: LUDNOSC / SZEROKOSC * WYSOKOSC czyli w konsekwencji do dziaa na liczbach: 10000 / 10 * 20 a to daje w wyniku: 1000 * 20 czyli ostatecznie: 20000 // ??? Co jest nie tak!
Hmm Pidziesit a dwadziecia tysicy to raczej dua rnica, znajdmy wic bd. Nie jest to trudne - tkwi on ju w pierwszym kroku rozwijania makra:
Preprocesor
313
LUDNOSC / SZEROKOSC * WYSOKOSC Zgodnie z reguami kolejnociami dziaa, zwanych w programowaniu priorytetami operatorw, wpierw wykonywane jest tu dzielenie. To bd - przecie najpierw powinnimy oblicza warto powierzchni, czyli iloczynu SZEROKOSC * WYSOKOSC. Naleaoby zatem obj go w nawiasy, i to najlepiej ju przy definicji makra POLE: #define POLE (SZEROKOSC * WYSOKOSC) Cakiem nietrudno o tym zapomnie. Jeszcze atwiej przeoczy fakt, e i SZEROKOSC, i WYSOKOSC mog by take zoonymi wyraeniami, wic rwnie i one powinny posiada wasn par nawiasw. Moe nie by wiadome, czy w ich definicjach takie nawiasy wystpuj, zatem przydaoby si wprowadzi je powyej Mamy wic cakiem sporo niewiadomych podczas korzystania ze staych-makr. A przecie wcale nie musimy rozstrzyga takich dylematw - zastosujmy po prostu stae bdce obiektami const: const const const const int int int int SZEROKOSC = 10; WYSOKOSC = 20; POLE = SZEROKOSC * WYSOKOSC; LUDNOSC = 10000;
std::cout << "Gestosc zaludnienia wynosi: " << LUDNOSC / POLE; Teraz wszystko bdzie dobrze. Poniewa to inteligentny kompilator zajmuje si takimi staymi (traktujc je jak niezmienne zmienne), warto wyraenia LUDNOSC / POLE jest obliczana waciwie.
Odpowied: czterdzieci dwa. Brzmi to zupenie nonsensownie, zwaywszy e 69 to przecie 54. A jednak to prawda - aby si o tym przekona, popatrz na poniszy program: // FortyTwo - odpowied na najwaniejsze pytanie Wszechwiata #include <iostream> #include <conio.h> #define SZESC #define DZIEWIEC 1 + 5 8 + 1
314
Zaawansowane C++
int main() { std::cout << "Szesc razy dziewiec rowna sie " << SZESC * DZIEWIEC; getch(); } return 0;
Czyby wic bya to faktycznie tak magiczna liczba, i specjalnie dla niej naginane s zasady matematyki? Niestety, wyjanienie jest bardziej prozaiczne. Spjrzmy tylko na wyraenie SZESC * DZIEWIEC. Jest ono rozwijane do postaci: 1 + 5 * 8 + 1 Tutaj za, zgodnie z wanymi od pocztku do koca Wszechwiata reguami arytmetyki, pierwszym obliczanym dziaaniem jest mnoenie. Ostatecznie wic mamy 1 + 40 + 1, czyli istotnie 42. Nie musimy jednak wierzy temu prostego wytumaczeniu. Czy nie lepiej sdzi, e nasz poczciwy preprocesor ma dostp do rozwiza niewyjanionych od wiekw zagadek Uniwersum?
Numer wiersza
Makro __LINE__ zostaje przez preprocesor zamienione na numer wiersza w aktualnie przetwarzanym pliku rdowym. Wiersze licz si od 1 i obejmuj take dyrektywy oraz puste linijki. Zatem w poniszym programie: #include <iostream> #include <conio.h> int main() { std::cout << "Wypisanie tekstu w wierszu " << __LINE__ << std::endl; return 0; }
Preprocesor
315
liczb pokazan na ekranie bdzie 6. Mona te zauway, e sam kompilator posuguje si t nazw, gdy pokazuje nam komunikat o bdzie podczas nieudanej kompilacji programu.
Dyrektywa #line
Informacje podawane przez __LINE__ i __FILE__ moemy zmieni, umieszczajc te makra w innych miejscach (plikach?). Ale moliwe jest te oszukanie preprocesora za pomoc dyrektywy #line: #line wiersz ["plik"] Gdy z niej skorzystamy, to preprocesor uzna, e umieszczona ona zostaa w linijce o numerze wiersz. Jeeli podamy te nazw pliku, to wtedy take oryginalna nazwa moduu zostanie uniewaniona przez t podan. Oczywicie nie fizycznie: sam plik pozostanie nietknity, a tylko preprocesor bdzie myla, e zajmuje si innym plikiem ni w rzeczywistoci. Osobicie nie sdz, aby wiadome oszukiwanie miao tu jaki gbszy sens. (Nad)uywajc dyrektywy #line moemy atwo straci orientacj nawet w programie, ktry obficie drukuje informacje o sprawiajcych problemy miejscach w kodzie.
Data i czas
Innym rodzajem informacji, jakie mona wkompilowa do wynikowego programu, jest data i czas jego zbudowania, ewentualnie modyfikacji kodu. Su do tego dyrektywy __DATE__, __TIME__ oraz __TIMESTAMP__. Zwrmy jeszcze uwag, e polecenia te absolutnie nie su do pobierania biecego czasu systemowego. S one tylko zamieniane na dosowne stae, ktre w niezmienionej postaci s przechowywane w gotowym programie i np. wywietlane wraz z informacj o wersji. Natomiast do uzyskania aktualnego czasu uywamy znanych funkcji time(), localtime(), itp. z pliku nagwkowego ctime.
Czas kompilacji
Chcc zachowa w programie dat i godzin jego kompilacji, stosujemy dyrektywy odpowiednio: __DATE__ oraz __TIME__. Preprocesor zamienia je na dat w formacie Mmm dd yy i na czas w formacie hh:mm:ss. Obie te wartoci s literaami znakowymi, a wic ujte w cudzysowy. Przykadowo, gdybym w chwili pisania tych sw skompilowa ponisz linijk kodu: std::cout << "Kompilacja wykonana w dniu " << __DATE__ << << " o godzinie " << __TIME__ << std::endl;
316
Zaawansowane C++
to w programie zapisana zostaaby data "Jul 14 2004" i czas "18:30:51". Uruchamiajc program za minut, p godziny czy za dziesi lat ujrzabym t sam dat i ten sam czas, poniewa byyby one wpisane na stae w pliku EXE. Z tego powodu data i czas kompilacji mog by uyte jako prymitywny sposb podawania wersji programu.
Typ kompilatora
Jest jeszcze jedno makro, zdefiniowane zawsze w kompilatorach jzyka C++. To __cplusplus. Nie ma ono adnej wartoci, gdy liczy si sama jego obecno. Pozwala ona na wykorzystanie tzw. kompilacji warunkowej, ktr poznamy za jaki czas, do rozrniania kodu w C i w C++. Dla nas, nieuywajcych wczeniej jzyka C, makro to nie jest wic zbyt praktycze, ale w czasie migracji starszego kodu do nowego jzyka okazywao si bardzo przydatne. Poza tym wiele kompilatorw C++ potrafi udawa kompilatory jego poprzednika w celu budowania wykonywalnych wersji starych aplikacji. Jeli wczylibymy tak opcj w naszym ulubionym kompilatorze, wtedy makro __cplusplus nie byoby definiowane przed rozpoczciem pracy preprocesora.
Inne nazwy
Powysze nazwy s zdefiniowane w kadym kompilatorze cho troch zgodnym ze standardem C++. Wiele z nich definiuje jeszcze inne: przykadowo, Visual C++ udostpnia makra __FUNCTION__ i __FUNCSIG__, ktre wewntrz blokw funkcji s zmieniane w ich nazwy i sygnatury (nagwki). Ponadto, kompilatory pracujce w rodowisku Windows definiuj te nazwy w rodzaju _WIN32 czy _WIN64, pozwalajce okreli bitowo platform tego systemu. Po inne predefiniowane makra preprocesora musisz zajrze do dokumentacji swojego kompilatora. Jeli uywasz Visual C++, to bdzie ni oczywicie MSDN.
Makra parametryzowane
Bardziej zaawansowany rodzaj makr to makra parametryzowane, czyli makrodefinicje. Z wygldu przypomniaj one nieco funkcje, cho funkcjami nie s. To po prostu nieco bardziej wyrafinowe polecenia dla preprocesora, instruujce go, jak powinien zamienia jeden tekst kodu w inny.
Preprocesor
317
Nie wydaje si to szczeglnie skomplikowane, jednak wok makrodefinicji naroso mnstwo mitw i faszywych stereotypw. Chyba aden inny element jzyka C++ nie wzbudza tylu kontrowersji co do jego prawidowego uycia, a wrd nich przewaaj opinie bardzo skrajne. Mwi one, ze makrodefinicje s cakowicie przestarzae i nie powinny by w ogle stosowane, gdy z powodzeniem zastpuj je inne elementy jzyka. Jak kade radykalne sdy, nie s to zdania suszne. To prawda jednak, e obecnie pole zastosowa makrodefinicji (i makr w ogle) zawyo si znacznie. Nie jest to aczkolwiek wystarczajcym powodem, aeby usprawiedliwia nim nieznajomo tej wanej czci jzyka. Zobaczmy zatem, co jest przyczyn tego caego zamieszania.
W ten sposb zdefiniowalimy makro SQR(), posiadajce jeden parametr - nazwalimy go tu x. Treci makra jest natomiast wyraenie ((x) * (x)). Jak ono dziaa? Ot, jeli preprocesor napotka w programie na wywoanie: SQR(cokolwiek) to zamieni je na wyraenie: ((cokolwiek) * (cokolwiek)) Tym cokolwiek moe by teoretycznie dowolny tekst (przypominam do znudzenia, e preprocesor operuje na tekcie programu), ale sensowne jest tam wycznie podanie wartoci liczbowej96. Wszelkie eksperymentowanie np. z acuchami znakw skoczy si komunikatem o bdzie skadniowym albo niedozwolonym uyciu operatora *. Powiedzmy jeszcze, dlaczego sowo wywoanie wziem w cudzysw, cho pewnie domylasz si tego. Tak, makro nie jest adn funkcj, wic jego uycie nie oznacza przejcia do innej czci programu. Makrodefinicja jest tylko poleceniem na preprocesora, mwicym mu, w jaki sposb zmieni to wywoaniopodobne wyraenie SQR(x) na inny fragment kodu, wykorzystujcy symbol x. W tym przypadku jest to iloczyn dwch zmiennych x, czyli kwadrat podanego wyraenia. A jak wyglda to makro w akcji? Bardzo prosto: int nLiczba; std::cout << "Podaj liczb: "; std::cin >> nLiczba; std::cout << "Kwadrat liczby " << nLiczba << " to " << SQR(nLiczba); Uycie makra w postaci SQR(nLiczba) zostanie tu zamienione na ((nLiczba) * (nLiczba)), zatem w wyniku rzeczywicie dostaniemy kwadrat podanej liczby.
96
Lub oglnie: kadego typu danych, dla ktrego zdefiniowalimy (lub zdefiniowa kompilator) dziaanie operatora *. O (prze)definiowaniu znacze operatorw mwi nastpny rozdzia.
318
Zaawansowane C++
Kilka przykadw
Dla utrwalenia przyjrzyjmy si jeszcze innym przykadom makrodefinicji.
Wzory matematyczne
Proste podniesienie do kwadratu to nie jedyne dziaanie, jakie moemy wykona poprzez makro. Prawie kady prosty wzr daje si zapisa w postaci odpowiedniej makrodefinicji spjrzmy: #define CB(x) ((x) * (x) * (x)) #define SUM_1_n(n) ((n) * ((n) + 1) / 2) #define POLE(a) SQR(a) Moemy tu zauway kilka faktw na temat parametryzowanych makr: mog one korzysta z ju zdefiniowanych makr (parametryzowanych lub nie) oraz wszelkich innych informacji dostpnych w czasie kompilacji - jak choby obiektw const moliwe jest zdefiniowanie makra z wicej ni jednym parametrem. Wtedy jednak dla bezpieczestwa lepiej nie stawia spacji po przecinku, gdy niektre kompilatory uznaj kady biay znak za koniec nazwy i rozpoczcie treci makra. W nazwach typu POLE(a,b) i podobnych nie wpisujmy wic adnych biaych znakw Jeli chodzi o atwo zauwaalne, intensywne uycie nawiasw w powyszych definicjach, to wyjani si ono za par chwil. Sdz jednak, e pamitajc o dowiadczeniach z makrami-staymi, domylasz si ich roli
Skracanie zapisu
Podobnie jak makra bez parametrw, makrodefinicje mog przyda si do skracania czsto uywanych fragmentw kodu. Oferuj one jeszcze moliwo oglnego zdefiniowania takiego fragmentu, bez wyranego podania niektrych nazw np. zmiennych, ktre mog si zmienia w zalenoci od miejsca uycia makra. A oto potencjalnie uyteczny przykad: #define DELETE(p) { delete (p); (p) = NULL; }
Makro DELETE() jest przeznaczone do usuwania obiektu, na ktry wskazuje wskanik p. Dodatkowo jeszcze dokonuje ono zerowania wskanika - dziki temu bdzie mona uchroni si przed omykowym odwoaniem do zniszczonego obiektu. Zerowy wskanik mona bowiem atwo wykry za pomoc odpowiedniego warunku if. Jeszcze jeden przykad: #define CLAMP(x, a, b) { if ((x) <= (a)) (x) = (a); if ((x) >= (b)) (x) = (b); }
To makro pozwala z kolei upewni si, e zmienna (liczbowa) podstawiona za x bdzie zawiera si w przedziale <a; b>. Jego normalne uycie w formie: CLAMP(nZmienna, 1, 10) zostanie rozwinite do kodu: { if ((nZmienna) <= (1)) (nZmienna) = (1); if ((nZmienna) >= (10)) (nZmienna) = (10); }
Preprocesor
319
po wykonaniu ktrego bdziemy pewni, e nZmienna zawiera warto rwn co najmniej 1 i co najwyej 10. Przypominam o nawiasach klamrowych w definicjach makr. Jak sdz pamitasz, e chroni one przed nieprawidow interpretacj kodu makra w jednolinijkowych instrukcjach if oraz ptlach.
Operatory preprocesora
W definicjach makr moemy korzysta z kilku operatorw, niedozwolonych nigdzie indziej. To specjalne operatory preprocesora, ktre za chwil zobaczymy przy pracy.
Sklejacz
Sklejacz (ang. token-pasting operator) jest te czsto nazywany operatorem czenia (ang. merging operator). Obie nazwy s adekwatne do dziaania, jakie ten operator wykonuje. W kodzie makr jest on reprezentowany przez dwa znaki potka (hash) - ##. Sklejacz czy ze sob dwa identyfikatory, czyli nazwy, w jeden nowy identyfikator. Najlepiej przeledzi to dziaanie na przykadzie: #define FOO foo##bar
Wystpienie FOO w programie zostanie przez preprocesor zamienione na zczenie nazw foo i bar. Bdzie to wic foobar. Operator czcy przydaje si te w makrodefinicjach, poniewa potrafi dziaa na ich argumentach. Spjrzmy na takie oto przydatne makro: #define UNICODE(text) L##text
Jego wywoanie z jakkolwiek dosown sta napisow spowoduje jej interpretacj jako acuch znakw Unicode. Przykadowo: UNICODE("Wlaz kotek na potek i spad") zmieni si na: L"Wlaz kotek na potek i spad" czyli napis zostanie zinterpretowany jako skadajcy si z 16-bitowych, szerokich znakw.
Operator acuchujcy
Drugim z operatorw preprocesora jest operator acuchujcy (ang. stringizing operator). Symbolizuje go jeden znak potka (hash) - #, za dziaanie polega na ujciu w podwjne cudzysowy ("") nazwy, ktr owym potkiem poprzedzimy. Popatrzmy na takie makro: #define STR(string) #string
Dziaa ono w prosty sposb. Jeli podamy mu jakkolwiek nazw czegokolwiek, np. tak: STR(jakas_zmienna) to w wyniku rozwinicia zostanie ona zastpiona przez napis ujty w cudzysowy: "jakas_zmienna"
320
Zaawansowane C++
Podana nazwa moe skada z kilku wyrazw - take zawierajcych znaki specjalne, jak cudzysw czy ukonik: STR("To jest tekst w cudzyslowach") Zostan one wtedy zastpione odpowiednimi sekwencjami ucieczki, tak e powyszy tekst zostanie zakodowany w programie w sposb dosowny: "\"To jest tekst w cudzyslowach\"" W programie wynikowym zobaczylibymy wic napis:
"To jest tekst w cudzysowach"
Byby on wic identycznie taki sam, jak argument makra STR(). Visual C++ posiada jeszcze operator znakujcy (ang. charazing operator), ktremu odpowiada symbol #@. Operator ten powoduje ujcie podanej nazwy w apostrofy.
Niebezpieczestwa makr
Niech wielu programistw do uywania makr nie jest bezpodstawna. Te konstrukcje jzykowe kryj w sobie bowiem kilka puapek, ktrych umiejscowienie naley zna. Dziki temu mona je omija - same te puapki, albo nawet makra w caoci. Zobaczmy wic, na co trzeba zwrci uwag przy korzystaniu z makrodefinicji.
Preprocesor
321
Aby dociec rozwizania, rozpiszmy druga linijk tak, jak robi to preprocesor: std::cout << ((nZmienna++) * (nZmienna++)) << std::endl; Wida wyranie, e nZmienna jest tu inkrementowana dwukrotnie. Pierwsza postinkrementacja zwraca wprawdzie wyniku 7, ale po niej nZmienna ma ju warto 8, zatem druga inkrementacja zwrci w wyniku wanie 8. Obliczymy wic iloczyn 78, czyli 56. Ale to nie wszystko. Druga inkrementacja zwikszy jeszcze warto 8 o jeden, zatem nZmienna bdzie miaa ostatecznie warto 9. Obie te niespodziewane liczby ujrzymy na wyjciu programu. Jaki z tego wniosek? Ano taki, e wyraenia podane jako argumenty makr s obliczane tyle razy, ile razy wystpuj w ich definicjach. Przyznasz, e to co najmniej nieoczekiwane zachowanie
Priorytety operatorw
Pora na akt trzeci dramatu. Obiecaem wczeniej, e wyjani, dlaczego tak gsto stawiam nawiasy w definicjach makr. Jeli uwanie czytae sekcj o makrach-staych, to najprawdopodobniej ju si tego domylasz. Wytumaczmy to jednak wyranie. Najlepiej bdzie przekona o roli nawiasw na przykadzie, w ktrym ich nie ma: #define SUMA(a,b,c) a + b + c
Uyjemy teraz makra SUMA() w takim oto kodzie: std::cout << 4 * SUMA(1, 2, 3); Jak liczb wydrukuje nam program? Oczywicie 24 Zaraz, czy aby na pewno? Kompilacja i uruchomienie koczy si przecie rezultatem:
9
Co si zatem stao? Ponownie winne jest wyraenie wykorzystujce makra. Preprocesor rozwinie je przecie do postaci: 4 * 1 + 2 + 3
322
Zaawansowane C++
co wedle wszelkich prawide rachunku na liczbach (i pierwszestwa operatorw w C++) kae najpierw wykona mnoenie 4 * 1, a dopiero potem reszt dodawania. Wynik jest wic zupenie nieoczekiwany. Jak si te zdylimy wczeniej przekona, podobn rol jak nawiasy okrge w makrach-wyraeniach peni nawiasy klamrowe w makrach zastpujcych cae instrukcje.
Zalety makrodefinicji
Z lektury poprzedniego paragrafu wynika wic, e stosowanie makrodefinicji wymaga ostronoci zarwno w ich definiowaniu (nawiasy!), jak i pniejszych uyciu (przekazywanie prostych wyrae). Co za zyskujemy w zamian, jeli zdecydujemy na stosowanie makr?
Efektywno
Na kadym kroku wyranie podkrelam, jak dziaaj makrodefinicje. To nie s funkcje, ktre program wywouje, lecz dosowny kod, ktry zostanie wstawiony w miejsce uycia przez preprocesor. Co z tego wynika? Ot z pozoru jest to bardzo wyrana zaleta. Brak koniecznoci skoku w inne miejsce programu - do funkcji - oznacza, e nie trzeba wykonywa wszelkich czynnoci z tym zwizanych. Nie trzeba zatem angaowa pamici stosu, by zachowa aktualny punkt wykonania oraz przekaza parametry. Nie trzeba te szuka w pamici operacyjnej miejsca, gdzie rezyduje funkcja i przeskakiwa do niego. Wreszcie, po skoczonym wykonaniu funkcji nie trzeba zdejmowa ze stosu adresu powrotnego i przy jego pomocy wraca do miejsca wywoania.
Funkcje inline
A jednak te zalety nie s wcale argumentem przewaajcym na korzy makr. Wszystko dlatego, e C++ umoliwia skorzystanie z nich take w odniesieniu do zwykych funkcji. Tworzymy w ten sposb funkcje rozwijane w miejscu wywoania - albo krtko: funkcje inline. S t funkcje pen gb i dlatego zupenie nie dotycz ich problemy zwizane z wielokrotnym obliczaniem wartoci parametrw czy priorytetami operatorw. Dziaaj one po prostu tak, jakbymy si tego spodziewali po normalnych funkcjach, a ponadto posiadaj te zalety makrodefinicji. Funkcje inline nie s wic faktycznie wywoywane podczas dziaania programu, lecz ich kod zostaje wstawiony (rozwinity) w miejscu wywoania podczas kompilacji programu. Dzieje si to zupenie bez ingerencji programisty w sposb wywoywania funkcji. Jedyne, co musi on zrobi, to poinformowa kompilator, ktre funkcje maj by rozwijane. Czyni to, przenoszc ich definicje do pliku nagwkowego (to wane!97) i opatrujc przydomkiem inline, np.: inline int Sqr(int a) { return a * a; }
Wspaniale!, moesz krzykn, Odtd wszystkie funkcje bd deklarowa jako inline! Chwileczk, nie tdy droga. Musisz by wiadom, e wstawianie kodu duych funkcji w miejsce kadego ich wywoania powodowaoby rozdcie kodu do sporych rozmiarw. Duy rozmiar mgby nawet spowolni wykonanie programu, zajmujcego nadzwyczajnie duo miejscu w pamici operacyjnej. Na funkcjach inline mona si wic polizgn.
97 Jest tak, gdy pena definicja funkcji inline (a nie tylko prototyp) musi by znana w miejscu wywoania funkcji - tak, aby jej tre moga by wstawiona bezporednio do kodu w tym miejscu.
Preprocesor
323
Lepiej zatem nie opatrywa modyfikatorem inline adnych funkcji, ktre maj wicej ni kilka linijek. Na pewno te nie powinny to by funkcje zawierajce w swym ciele ptle czy inne rozbudowane konstrukcje jzykowe (typu switch lub wielopoziomowych instrukcji if). Mio jest jednak wiedzie, e obecne kompilatory s po naszej stronie, jesli chodzi o funkcje inline. Dobry kompilator potrafi bowiem zrobi analiz zyskw i strat z zastosowania inline do konkretnej funkcji: jeli stwierdzi, e w danym przypadku rozwijanie urgaoby szybkoci programu, nie przeprowadzi go. Dla prostych funkcji (dla ktrych inline ma najwikszy sens) kompilatory zawsze jednak ulegaj naszym daniom. W Visual C++ jest dodatkowe sowo kluczowe __forceinline. Jego uycie zamiast inline sprawia, e kompilator na pewno rozwinie dan funkcj w miejscu wywoania, ignorujc ewentualne uszczerbki na wydajnoci. VC++ ma te kilka dyrektyw #pragma, ktre kontroluj rozwijanie funkcji inline - moesz o nich przeczyta w dokumentacji MSDN. Warto te wiedzie, e metody klas definiowane wewntrz blokw class (lub struct i union) s automatycznie inline. Nie musimy opatrywa ich adnym przydomkiem. Jest to szczeglnie korzystne dla metod dostpowych do pl klasy.
atwo to wyjani. Preprocesor zamieni po prostu kade uycie makra na odpowiedni iloczyn, zapisany w sposb dosowny w kodzie wysanym do kompilatora. Ten za potraktuje te wyraenia jak kade inne. Gdybymy chcieli podobny efekt uzyska przy pomocy funkcji inline, to zapewne pierwszym pomysem byoby napisanie kilku(nastu?) przecionych wersji funkcji. To jednak nie jest konieczne: C++ potrafi bowiem stosowa w kontekcie normalnych funkcji take i t cech makra, jak jest niezaleno od typu. Poznamy bowiem wkrtce mechanizm szablonw, ktry pozwala na takie wanie zachowanie.
324
Zaawansowane C++
{ return a * a; }
Powyszy szablon funkcji (tak to si nazywa) moe by stosowany dla kadego typu liczbowego, a nawet wicej - dla kadego typu obsugujcego operator *. Posiada przy tym te same zalety co zwyke funkcje i funkcje inline, a pozbawiony jest typowych dla makr kopotw z wielokrotnym obliczaniem argumentw i nawiasami. W jednym z przyszych rozdziaw poznamy dokadnie mechanizm szablonw w C++, ktry pozwala robi tak wspaniae rzeczy bardzo maym kosztem.
Zastosowania makr
Czytelnicy chccy znale uzasadnienie dla wykorzystania makr, mog si poczu zawiedzeni. Wyliczyem bowiem wiele ich wad, a wszystkie zalety okazyway si w kocu zaletami pozornymi. Takie wraenie jest w duej czci prawdziwe, lecz nie znaczy to, e makrach naley cakiem zapomnie. Przeciwnie, naley tylko wiedzie, gdzie, kiedy i jak z nich korzysta.
Znanych jest wiele podobnych i przydatnych sztuczek, szczeglnie z wykorzystanie operatorw preprocesora - # i ##. By moe niektre z nich sam odkryjesz lub znajdziesz w innych rdach.
Preprocesor
325
326
Zaawansowane C++
skorzysta musimy wprowadzi do kodu dodatkowe informacje. Zostan one wykorzystane w kompilacji warunkowej. Kontrolowanie kompilacji moe wic da duo korzyci. Warto zatem zobaczy, w jaki sposb to si odbywa.
Puste makra
Wprowadzajc makra napomknem, e podawanie ich treci nie jest obowizkowe. Mwic dosownie, preprocesor uzna za cakowicie poprawn definicj: #define MAKRO Jeli MAKRO wystpi dalej w pliku kompilowanym, to zostanie po prostu usunite. Nie bdzie on zatem zbyt przydatne, jeli chodzi o operacje na tekcie programu. To jednak nie jest teraz istotne. Wane jest samo zdefiniowanie tego makra. Poniewa zrobilimy to, preprocesor bdzie wiedzia, e taki symbol zosta mu podany i zapamita go. Pozwala nam to na zastosowanie kompilacji warunkowej. Przypomnijmy jeszcze, e moemy odwoa definicj makra dyrektyw #undef.
Dyrektywa #ifdef
Najprostsz i jedn z czciej uywanych dyrektyw kompilacji warunkowej jest #ifdef: #ifdef makro instrukcje #endif Jej nazwa to skrt od angielskiego if defined, czyli jeli zdefiniowane. Dyrektywa #ifdef powoduje wic kompilacje kodu instrukcji, jeli zdefiniowane jest makro. instrukcje mog by wielolinijkowe; koczy je dyrektywa #endif. #ifdef pozwala na czasowe wyczenie lub wczenie okrelonego kodu. Typowym zastosowaniem tej dyrektywy jest pomoc w usuwaniu bdw, czyli debuggowaniu. Moemy obj ni na przykad kod, ktry drukuje parametry przekazane do jakiej funkcji: void Funkcja(int nParametr1, int nParametr2, float { #ifdef DEBUG std::cout << "Parametr 1: " << nParametr1 std::cout << "Parametr 2: " << nParametr2 std::cout << "Parametr 3: " << fParametr3 #endif } // (kod funkcji) fParametr3) << std::endl; << std::endl; << std::endl;
Preprocesor
327
Kod ten zostanie skompilowany tylko wtedy, jeli wczeniej zdefiniujemy makro DEBUG: #define DEBUG Tre makra nie ma znaczenia, bo liczy si sam fakt jego zdefiniowania. Moemy wic pozostawi j pust. Po zakoczeniu testowania usuniemy lub wykomentujemy t definicj, a linijki drukujce parametry nie zostan wczone do programu. Jeli uyjemy #ifdef (lub innych dyrektyw warunkowych) wiksz liczb razy, to oszczdzimy mnstwo czasu, bo nie bdziemy musieli przeszukiwa programu i oddzielnie komentowa kadej porcji diagnostycznego kodu. W wielu kompilatorach moemy wybra tryb kompilacji, jak np. Debug (testowa) i Release (wydaniowa) w Visual C++. Rni sie one stopniem optymalizacji i bezpieczestwa, a take zdefiniowanymi makrami. W trybie Debug kompilator Microsoftu sam definiuje makro _DEBUG, ktrego obecno moemy testowa.
Dyrektywa #ifndef
Przeciwnie do #ifdef dziaa druga dyrektywa - #ifndef: #ifndef makro instrukcje #endif Ta opozycja polega na tym, e instrukcje ujte w #ifndef/#endif zostan skompilowane tylko wtedy, gdy makro nie jest zdefiniowane. #ifndef znaczy if not defined, czyli wanie jeeli nie zdefiniowane. Nawizujc do kolejnego przykadu, moemy uy #ifndef w stosunku do kodu, ktry ma si kompilowa wycznie w wersjach wydaniowych. Moe to by choby wywietlanie ekranu powitalnego (ang. splash screen). Jego widok przy setnym, testowym uruchamianiu programu moe by bowiem naprawd denerwujcy.
Dyrektywa #else
Do spki z obiema dyrektywami #ifdef i #ifndef (a take z #if, opisan w nastpnym paragrafie) wchodzi polecenie #else. Jak mona si domyle, pozwala ono na wybr dwch wariantw kodu: jednego, ktry jest kompilowany w razie zdefiniowania (#ifdef) lub niezdefiniowania (#ifndef) makra oraz drugiego - w przeciwnych sytuacjach: #if[n]def makro instrukcje_1 #else instrukcje_2 #endif Zastosowaniem dla tej dyrektywy moe by na przykad system raportowania bdw. W trybie testowania mona chcie zrzutu caej pamici programu, jeli wystpi w nim jaki powany bd. W wersjach wydaniowych i tak nie monaby byo nic z krytycznym bdem zrobi, wic nie powinno si zmusza (zdenerwowanego przecie) klienta do czekania na tak wyczerpujc operacj. Wystarczy wtedy zapis wartoci najwaniejszych zmiennych. Zwrmy uwag, e dyrektywa #else suy w tym przypadku wycznie naszej wygodzie. Rwnie dobrze poradzilibycie sobie bez niej, piszc najpierw warunek z #ifdef (#ifndef), a potem z #ifndef (#ifdef).
328
Zaawansowane C++
Konstruowanie warunkw
Co moe by warunkiem? W oglnoci wszystko, co znane jest preprocesorowi w momencie napotkania dyrektywy #if. S to wic: wartoci dosownych staych liczbowych, podane bezporednio w kodzie jako liczby, np. -8, 42 czy 0xFF wartoci makr-staych, zdefiniowane wczeniej dyrektyw #define wyraenia z operatorem defined A co z reszt staych wartoci, np. obiektami const? Ot one nie mog (albo raczej nie powinny) by skadnikami warunkw #if. Jest tak, poniewa obiekty te nale do kompilatora, a nie do preprocesora. Ten nie ma o nich pojcia, gdy zna tylko swoje makra #define. To jedyny przypadek, gdy maj one przewag na staymi const. Podobnie rzecz ma si z operatorem sizeof, ktry jest wprawdzie operatorem czasu kompilacji, ale nie jest operatorem preprocesora. Gdyby #if rozpoznawao warunki z uyciem staych const i operatora sizeof, nie mogoby ju by obsugiwane przez preprocesor. Musisz bowiem pamita, e dla preprocesora istniej tylko jego dyrektywy, za cay tekst midzy nimi moe by czymkolwiek (cho dla nas jest akurat kodem). Chcc zmusi preprocesor do obsugi obiektw const i operatora sizeof naleaoby w istocie obarczy go zadaniami kompilatora.
Operator defined
Operator defined suy do sprawdzenia, czy dane makro zostao zdefiniowane. Warunek: #if defined(makro) jest wic rwnowany z: #ifdef makro Natomiast dla #ifndef alternatyw jest:
Preprocesor
#if !defined(makro) Przewaga operatora defined na #if[n]def polega na tym, i operator ten moe wystpowa w zoonych wyraeniach, bdcych warunkami w dyrektywie #if.
329
Zoone warunki
#if jest podobna do if take pod tym wzgldem, i pozwala na stosowanie operatorw relacyjnych i logicznych w swoich warunkach. Nie zmienia to aczkolwiek faktu, e wszystkie argumenty tych operatorw musz by znane w trakcie pracy preprocesora - a wic nalee do trzech grup, ktre podaem we wstpie do paragrafu. Ta moliwo dyrektywy #if pozwala na warunkow kompilacj kodu zalen od kilku warunkw, na przykad: #define MAJOR_VERSION #define MINOR_VERSION 4 6
#if ((MAJOR_VERSION == 4) && (MINOR_VERSION >= 2)) || (MAJOR_VERSION > 4) std::cout << "Ten kod skompiluje si tylko w wersji 4.2 lub nowszej"; #endif Mog w nich wystapi porwnania makr-staych, liczb wpisanych dosownie oraz wyrae z operatorem defined. Wszystkie te czci mona natomiast czy znanymi operatorami logicznymi: !, && i ||.
Zagniedanie dyrektyw
Wewntrz kodu zawartego midzy #if[[n]def] oraz #else i midzy #else i #endif mog si znale kolejne dyrektywy kompilacji warunkowej. Dziaa to w podobny sposb, jak zagniedone instrukcje if w blokach kodu innych instrukcji if. Spjrzmy na ten przykad98: #define WINDOWS #define WIN_NT #define PLATFORM #define WIN_VER 1 1 WINDOWS WIN_NT
#if PLATFORM == WINDOWS #if WIN_VER == WIN_NT std::cout << "Program kompilowany na Windows z serii NT"; #else std::cout << "Program kompilowany na Windows 9x lub ME"; #endif #else std::cout << "Nieznana platforma (DOS? Linux?)"; #endif
To tylko przykad ilustrujcy kompilacj warunkow. Prawdziwa kontrola wersji systemu Windows, na ktrej kompilujemy program, wymaga doczenia pliku windows.h i kontrolowania makr o nieco innych nazwach i wartociach
98
330
Zaawansowane C++
Jeli zagniedamy w sobie dyrektywy preprocesora, to stosujmy wcici podobne do instrukcji w normalnym kodzie. Nie wiedzie czemu niektre IDE (np. Visual C++) domylnie wyrwnuj dyrektywy preprocesora w jednym pionie; wyczmy im t niepraktyczn opcj. W Visual Studio .NET wybierzmy pozycj w menu Tools|Options, za w pojawiajcym si oknie dialogowym przejdmy do zakadki Text Editor|C/C++|Tabs i ustawmy opcj Indenting na Block.
Dyrektywa #elif
Czasem dwa warianty to za mao. Jeli chcemy wybra kilka moliwych drg kompilacji, to naley zastosowa dyrektyw #elif. Jej nazwa to skrt od else if, co mwi wszystko na temat roli tej dyrektywy. Ponownie zerknijmy na przykadowy kod: #define #define #define #define WINDOWS LINUX OS_2 QNX 1 2 3 4 WINDOWS
#define PLATFORM
#if PLATFORM == WINDOWS std::cout << "Kod kompilowany w systemie Windows"; #elif PLATFORM == LINUX std::cout << "Program budowany w systemie Linux"; #elif PLATFORM == OS_2 std::cout << "Kompilacja na platformie systemu OS/2"; #elif PLATFORM == QNX std::cout << "Skompilowano w systemie QNX"; #endif Do takich warunkw pewnie znacznie lepsza byaby dyrektywa typu #switch, lecz niestety preprocesor jest nie posiada. Dyrektywa #elif, podobnie jak #else, moe by take doczepiona do warunkw #ifdef i #ifndef. Pamitajmy jednak, e po niej musi nastpi wyraenie logiczne, a nie tylko nazwa makra.
Dyrektywa #error
Ostatni z dyrektyw warunkowej kompilacji jest #error: #error "komunikat" Gdy preprocesor spotka j na swojej drodze, wtedy jest to dla niego sygnaem, i tok kompilacji schodzi na ze tory i powinien zosta przerwany. Czyni to wic, a po takim niespodziewanym zakoczeniu widzimy w oknie bdw komunikat, jaki podalimy w dyrektywie #error (nie musi on koniecznie by ujty w cudzysowy, ale to dobry zwyczaj). Dla ilustracji tego polecenia uzupenimy pitrowy warunek #if z poprzedniego paragrafu: #if PLATFORM == WINDOWS std::cout << "Kod kompilowany w systemie Windows"; #elif PLATFORM == LINUX std::cout << "Program budowany w systemie Linux"; // ...
Preprocesor
#else #error "Nieznany system operacyjny, kompilacja przerwana!" #endif
331
Jeeli nie zdefiniujemy makra PLATFORM lub bdzie miao inn warto ni podane stae WINDOWS, LINUX, itd., to preprocesor zareaguje odpowiednim bdem. W Visual C++ .NET wyglda on tak:
fatal error C1189: #error : "Nieznany system operacyjny, kompilacja przerwana!"
Jak wida jest to bd fatalny, ktry zawsze powoduje przerwanie kompilacji programu. *** W ten oto sposb zakoczylimy omawianie dyrektyw preprocesora, sucych kontroli procesu kompilacji programu. Obok makr jest to najwaniejszy aspekt zastosowania mechanizmu wstpnego przetwarzania kodu. Te dwa tematy nie s aczkolwiek peni moliwoci preprocesora. Teraz poznamy jeszcze kilka dyrektyw oglnego przeznaczenia - nie mniej wanych ni te dotychczasowe.
Reszta dobroci
Pozostae dyrektywy preprocesora s take bardzo istotne. Jedna z nich jest na tyle kluczowa, e widzisz j w kadym programie napisanym w C++.
Doczanie plikw
T dyrektyw jest oczywicie #include. Ju przynajmniej dwa razy przygldalimy si jej bliej, lecz teraz czas na wyjanienie wszystkiego.
Z nawiasami ostrymi
Model z nawiasami ostrymi (tworzonymi poprzez znak mniejszoci i wikszoci): #include <nazwa_pliku>
332
Zaawansowane C++
stosowalimy od samego pocztku nauki C++. Nieprzypadkowo: pliki, jakie doczamy w ten sposb, s po prostu niezbdne do wykorzystania niektrych elementw jzyka, Biblioteki Standardowej oraz innych bibliotek (Windows API, DirectX, itd.). Gdy preprocesor widzi dyrektyw #include w powyszej postaci, to zaczyna szuka podanego pliku w jednym z wewntrznych katalogw kompilatora, gdzie znajduj si pliki doczane (ang. include files). Takich katalogw jest zwykle kilka, wic preprocesor przeszukuje ich list; foldery te zawieraj m.in. nagwki Biblioteki Standardowej C++ (string, vector, list, ctime, cmath, ), starszej Biblioteki Standardowej C (time.h, math.h, 99), a czsto take nagwki innych zainstalowanych bibliotek. Chcc przejrze lub zmodyfikowa list katalogw z plikami doczanymi w Visual C++ .NET, musimy wybra z menu Tools pozycj Options. Dalej przechodzimy do zakadki Projects|VC++ Directories, a na licie rozwijalnej Show directories for: wybieramy Include files.
Z cudzysowami
Drugi typ instrukcji #include wyglda nastpujco: #include "nazwa_pliku" Z nimtake zdylimy si ju spotka - stosowalimy go do wczania wasnych plikw nagwkowych do swoich moduw. Ten wariant #include dziaa w sposb nieco bardziej kompleksowy ni poprzedni. Wpierw bowiem przeszukuje on biecy katalog - tzn. ten katalog, w ktrym umieszczono plik zawierajcy dyrektyw #include. Jeli tam nie znajdzie podanego pliku, wwczas zaczyna zachowywa si tak, jak #include z nawiasami ostrymi. Przeglda wic zawarto katalogw z listy folderw plikw doczanych.
Ktry wybra?
Dwa rodzaje jednej dyrektywy to cakiem sporo. Ktr wybra w konkretnej sytuacji?
99 Te nagwki sa niezalecane, naley stosowa ich odpowiedniki bez rozszerzenia .h i literk c na pocztku. Zamiast np. math.h uywamy wic cmath.
Preprocesor
333
zapowiadajce zmiennych oraz definicje klas (a czsto take definicje szablonw, ale o tym pniej). Pliki te maj zwykle rozszerzenie .h, .hh, .hxx lub .hpp.
cieki wzgldne
W obu wersjach #include moemy wykorzystywa tzw. cieki wzgldne (ang. relative paths), cho prawdziwie przydatne s one tylko w dyrektywie z cudzysowami. cieki wzgldne pozwalaj docza pliki znajdujce si w innym katalogu ni biecy100: w podkatalogach lub w nadkatalogu czy te w innych katalogach tego samego poziomu. Oto kilka przykadw: #include "gui\buttons.h" #include "..\base.h" #include "..\common\pointers.hpp" // 1 // 2 // 3
Dyrektywa 1 powoduje doczenie pliku buttons.h z podkatalogu gui. Kolejne uycie #include doczy nam plik base.h z katalogu nadrzdnego wzgldem obecnego. Z kolei ostatnia dyrektywa powoduje wpierw wyjcie z aktualnego katalogu (..), nastpnie wejcie do podkatalogu common, pobranie ze zawartoci pliku pointers.hpp i wstawienie w miejsce linijki 3. Jak wida, w #include mona wykorzysta te same zasady tworzenia cieek wzgldnych, jakie obowizuj w caym systemie operacyjnych101.
Tradycyjne rozwizanie
Rozwizanie problemu znanym jeszcze z C jest zastosowanie kompilacji warunkowej. Musimy po prostu obja cay plik nagwkowy (nazwijmy go plik.h) w dyrektywy #ifndef-#endif: #ifndef _PLIK__H_ #define _PLIK__H_ // (caa tre pliku nagwkowego) #endif Uyte tu makro (_PLIK__H_) powinno by najlepiej spreparowane w jaki sposb z nazwy i rozszerzenia pliku - a jeli trzeba, take i ze cieki do niego.
Biecy - to znaczy ten katalog, gdzie znajduje sie plik z dyrektyw #include "...". Jako separatora moemy uy slasha lub backslasha. Slash ma t zalet, e dziaa take w systemach unixowych - jeli oczywicie dla kogo jest to zalet
101
100
334
Zaawansowane C++
Jak to dziaa? Ot dyrektywa #ifndef przepuci tylko jedno wstawienie treci pliku. Przy powtrnej prbie makro _PLIK__H_ bedzie ju zdefiniowane, wic caa zawarto pliku zostanie wyczona z kompilacji.
Pomaga kompilator
Zaprezentowany wyej sposb ma przynajmniej kilka wad: wymaga wymylania nazwy dla makra kontrolnego, co przy duych projektach, gdzie atwo wystpuj nagwki o tych samych nazwach, staje si kopotliwe. Sytuacja wyglda jeszcze gorzej w przypadku bibliotek pisanych przez nas: tam makra powinni mie w nazwie take okrelenie biblioteki, aby nie prowokowa potencjalnych konfliktw z innymi zasobami kodu umieszczona na kocu pliku dyrektywa #endif moe by atwo przeoczona i omykowo skasowana. Nietrudno te napisa jaki kod poza klamr #ifndef#else - on nie bdzie ju objty ochron sztuczka wymaga a trzech linii kodu, w tym jednej umieszczonej na samym kocu pliku Mnie osobicie rozwizanie to wydaje si po prostu nieeleganckie - zwaszcza, e coraz wicej kompilatorw oferuje inny sposb. Jest nim umieszczenie gdziekolwiek w pliku dyrektywy: #pragma once Jest to wprawdzie polecenie zalene od kompilatora, ale obsugiwane przez wszystkie liczce si narzdzia (w tym take Visual C++ .NET oraz kompilator GCC z Dev-C++). Jest te cakiem prawdopodobne, e taka metoda rozwizania problemu wielokrotnego doczania znajdzie si w kocu w standardzie C++.
Dyrektywa #pragma
Do wydawania tego typu polece suy dyrektywa #pragma: #pragma polecenie To, czy dane polecenie zostanie faktycznie wzite pod uwage podczas kompilacji, zaley od posiadanego przez nas kompilatora. Preprocesor zachowuje si jednak bardzo porzdnie: jeli stwierdzi, e dana komenda jest nieznana kompilatorowi, wwczas caa dyrektywa zostanie po prostu zignorowana. Niektre troskliwe kompilatory wywietlaj ostrzeenie o tym fakcie. Po opis polece, jakie s dostpne w dyrektywie #pragma, musisz uda si do dokumentacji swojego kompilatora.
Preprocesor
335
Nie omwimy ich wszystkich, gdy nie jest to podrcznik VC++, a poza tym wiele z nich dotyczy sprawa bardzo niskopoziomowych. Przypatrzymy si aczkolwiek tym, ktre mog by przydatne przecitnemu programicie. Opisy wszystkich parametrw dyrektywy #pragma w Visual C++ .NET moesz rzecz jasna znale w dokumentacji MSDN. Wybrane parametry podzieliem na kilka grup.
Komunikaty kompilacji
Pierwsza trjka parametrw #pragma pozwala na wywietlanie pewnych informacji podczas procesu kompilacji programu. W przeciwiestwie do #error, polcenia nie powoduje jednak przerwania tego procesu, lecz tylko peni funkcj powiadamiajc np. o pewnych decyzjach podjtych w czasie kompilacji warunkowej. Przyjrzyjmy si tym komendom.
message
Skadnia polecenia message jest nastpujca: #pragma message("komunikat") Gdy preprocesor napotka powysz linijk kodu, to wywietli w oknie komunikatw kompilatora (tam, gdzie zwykle podawane s bdy) wpisany tutaj komunikat. Jego wypisanie nie spowoduje jednak przerwania procesu kompilacji, co rni #pragma message od dyrektywy #error. Przykadowym uyciem tego polecenie moe by pitrowy #if podobny do tego z jakim mielimy do czynienia w poprzednim podrozdziale: #define #define #define #define KEYBOARD MOUSE TRACKBALL JOYSTICK 1 2 3 4
#define INPUT_DEVICE KEYBOARD #if (INPUT_DEVICE == KEYBOARD) #pragma message("Wkompilowuje obsluge klawiatury") #elif (INPUT_DEVICE == MOUSE) #pragma message("Domylsne urzadzenie: mysz") #elif (INPUT_DEVICE == TRACKBALL) #pragma message("Sterowanie trackballem") #elif (INPUTDEVICE == JOYSTICK) #pragma message("Obsluga joysticka") #else #error "Nierozpoznane urzadzenie wejsciowe!" #endif Teraz, w zalenie od wartoci makra INPUT_DEVICE w polu komunikatw kompilatora zobaczymy na przykad:
Sterowanie trackballem
W parametrze message moemy te stosowa makra, np.: #pragma message("Kompiluje modul " __FILE__ ", ktory byl ostatnio " \
336
"zmodyfikowany: " __TIMESTAMP__)
Zaawansowane C++
W ten sposb zobaczymy oprcz nazwy kompilowanego pliku take dat i czas jego ostatniej modyfikacji.
deprecated
Nieco inne zastosowanie ma parametr deprecated, lecz take suy do pokazywania komunikatw dla programisty podczas kompilacji. Oto jego skadnia: #pragma deprecated(nazwa_1 [, nazwa_2, ...]) deprecated znaczy dosownie potpiony i jest to troch zbyt teatralna, ale adekwatna nazwa dla tego parametru dyrektywy #pragma. deprecated pozwala na wskazanie, ktre nazwy w programie (funkcji, zmiennych, klas, itp.) s przestarzae i nie powinny by uywane. Jeeli zostan one wykorzystane w kodzie, wwczas kompilator wygeneruje ostrzeenie. Spjrzmy na ten przykad: // ta funkcja jest przestarzaa void Funkcja() { std::cout << "Mam juz dluga, biala brode..."; } #pragma deprecated(Funkcja) int main() { Funkcja(); }
// spowoduje ostrzeenie
Zauwamy, e dyrektyw #pragma deprecated umieszczamy po definicji przestarzaego symbolu. W przeciwnym razie sama definicja spowodowaaby wygenerowanie ostrzeenia. Innym sposobem oznaczenia symbolu jako przestarzay jest poprzedzenie jego deklaracji fraz __declspec(deprecated). Moemy te oznacza makra jako przestarzae, lecz aby unikn ich rozwinicia w dyrektywie #pragma, naley ujmowa nazwy makr w cudzysowy.
warning
Ten parametr nie generuje wprawdzie adnych komunikatw, ale pozwala na sprawowanie kontroli nad tym, jakie ostrzeenia s generowae przez kompilator. Oto skadnia dyrektywy #pragma warning: #pragma warning(specyfikator_1: numer_1_1 [numer_1_2 ...] \ [; specyfikator_2: numer_2_1 [numer_2_2 ...]]) Wyglda ona do skomplikowanie, ale w praktyce stosuje si tylko jeden specyfikator na kade uycie dyrektywy, wic waciwa posta staje si prostsza.
Preprocesor
337
Co dokadnie robi #pragma warning? Ot pozwala ona zmieni sposb traktowania przez kompilator ostrzee o podanych numerach. Podejmowane dziaania okrela dokadnie specyfikator: specyfikator disable once default error 1 2 3 4 znaczenie Powoduje wyczenie raportowania podanych numerw ostrzee. Sytuacje, w ktrych powinny wystpi, zostan po prostu zignorowane, a programista nie bdzie o nich powiadamiany. Sprawia, e podane ostrzeenia bd wywietlane tylko raz, przy pierwszym wystpieniu powodujcych je sytuacji. Przywraca sposb obsugi ostrzee do trybu domylnego. Sprawia, e podane ostrzeenia bd traktowane jako bedy. Ich wystpienie spowoduje wic przerwanie kompilacji. Zmienia tzw. poziom ostrzeenia (ang. warning level). Generalnie wyszy poziom oznacza mniejsz dolegliwo i niebezpieczestwo ostrzeenia. Przesunicie danego ostrzeenia do okrelonego poziomu powoduje, e jego interpretacja (wywietlanie, przerwanie kompilacji, itd.) zalee bdzie od ustawie kompilatora dla danego poziomu ostrzee. Za ustawienia te nie odpowiada jednak #pragma warning.
Tabela 15. Specyfikatory kontroli ostrzee dyrektywy #pragma warning w Visual C++ .NET
Skd natomiast wzi numer ostrzeenia? Jest on podawany w komunikacie kompilatora - jest to liczba poprzedzona liter C, np.:
warning C4101: 'nZmienna' : unreferenced local variable
Do #pragma warning podajemy numer ju bez tej litery. Chcc wic wyczy powysze ostrzeenie, stosujemy dyrektyw: #pragma warning(disable: 4101) Pamitajmy, e stosuje si on do wszystkich instrukcji po swoim wystpieniu - podobnie jak wszystkie inne dyrektywy preprocesora. Uwaga: jakkolwiek wyczanie ostrzee jest czasem konieczne, nie naley z tym przesadza. Przede wszystkim nie wyczajmy wszystkich pojawiajcych si ostrzee jak leci, lecz wpierw przyjrzyjmy si, jakie kod je powoduje. Kade uycie #pragma warning(disable: numer) powinno by bowiem dokadnie przemylane.
Funkcje inline
Z poznanymi w tym rozdziale funkcjami inline jest zwizanych kilka parametrw dyrektywy #pragma. Zobaczmy je.
auto_inline
#pragma auto_inline ma bardzo prost posta: #pragma auto_inline([on/off]) Parametr ten kontroluje automatyczne rozwijanie krtkich funkcji przez kompilator. Ze wzgldw optymalizacyjnych niektre funkcje mog by bowiem traktowane jako inline nawet wtedy,gdy nie s zadeklarowane z przydomkiem inline. Jeli z jakich powodw nie chcemy aby tak byo, moemy to wyczy: #pragma auto_inline(off)
338
Zaawansowane C++
Wszystkie nastpujce dalej funkcje na pewno nie bd rozwijane w miejscu wywoania chyba e sami tego sobie yczymy, deklarujc je jako inline. Typowo #pragma auto_inline stosujemy dla pojedynczej funkcji w ten sposb: #pragma auto_inline(off) void Funkcja(/* ... */) { // ... } #pragma auto_inline(on) Jeeli nie podamy w dyrektywie ani on, ani off, to stan auto_inline zostanie zamieniony na przeciwny (z on na off lub odwrotnie).
inline_recursion
Ta komenda jest take przecznikiem: #pragma inline_recursion([on/off]) Kontroluje ona rozwijanie wywoa rekurencyjnych (ang. recursive calls) w funkcjach typu inline. Rekurencj (ang. recurrency) nazywamy zjawisko, kiedy jaka funkcja wywouje sam siebie - oczywicie nie zawsze, lecz w zalenoci od spenienia jakich warunkw. Wywoania rekurencyjne s prostym sposobem na tworzenie pewnych algorytmw - szczeglnie takich, ktre operuj na rekurencyjnych strukturach danych, jak drzewa. Rekurencja moe by bezporednia - gdy funkcja sama wywouje siebie - lub porednia - jeli robi to inna funkcja, wywoana wczeniej przez t nasz. Rekurencyjne mog by take funkcje inline. W takim wypadku kompilator domylnie rozwija tylko ich pierwsze wywoanie; dalsze wywoania rekurencyjne s ju dokonywane w sposb waciwy dla normalnych funkcji. Mona to zmieni, powodujc rozwijanie take dalszych przywoa rekurencyjnych (w ograniczonym zakresie oczywicie) - naley wprowadzi do kodu dyrektyw: #pragma inline_recursion(on) atwo si domysli, e inline_recursion jest domylnie ustawiona na off.
inline_depth
Z poprzednim poleceniem zwizane jest take to - dyrektywa #pragma inline_depth: #pragma inline_depth(gboko) gboko moe tu by sta cakowit z zakresu od zera do 255. Liczba ta precyzuje, jak gboko kompilator ma rozwija rekurencyjne wywoania funkcji inline. Naturalnie, wartoc ta ma jakiekolwiek znaczenie tylko wtedy, gdy ustawimy inline_recursion na on. Ponadto warto 255 oznacza rozwijanie rekurencji bez ogranicze (z wyjtkiem rzecz jasna zasobw dostpnych dla kompilatora). Domylnie rozwijanych jest osiem rekurencyjnych wywoa inline. Pamitajmy, e przesada z t wartoci moe do atwo doprowadzai do rozrostu kody wynikowego zwaszcza, jeli przesadzamy te z obdzielaniem funkcji modyfikatorami inline (a szczeglnie __forceinline).
Preprocesor
339
Inne
Oto dwie ostatnie komendy #pragma w Visual C++, jednak wcale nie s one najmniej wane. Jakby to powiedzieli Anglicy, one s last but not least :) Przyjrzymy si im.
comment
To polecenie umoliwa zapisanie pewnych informacji w wynikowym pliku EXE: #pragma comment(typ_komentarza [, "komentarz"]) Umieszczone tak komentarze nie su naturalnie tylko do dekoracji (cho niektre do tego te :D), lecz moga nie take dane wane dla kompilatora czy linkera. Wszystko zaley od frazy typ_komentarza. Oto dopuszczalne moliwoci: typ komentarza znaczenie Umieszcza w skompilowanym pliku tekstowy komentarz, ktry linker w niezmienionej postaci przenosi do konsolidowanego pliku EXE. Napis ten nie jest adowany do pamici podczas uruchamiania programu, niemniej istnieje w pliku wykonywalnym i mona go odczyta specjalnymi aplikacjami. Wstawia do skompilowanego pliku podany komentarz, jednak linker ignoruje go i nie pojawia si on w wynikowym EXEku. Istnieje natomiast w skompilowanym pliku .obj. Dodaje do skompilowanego modulu informacj o wersji kompilatora. Nie pojawia si ona wynikowym pliku wykonywalnym z programem. Przy stosowaniu tego typu, nie naley podawa adnego komentarza, bo w przeciwnym razie kompilator uraczy nas ostrzeeniem. Ten typ pozwala na podanie nazwy pliku statycznej biblioteki (ang. static library), ktra bdzie linkowana razem ze skompilowanymi moduami naszej aplikacji. Linkowanie dodatkowych bibliotek jest czsto potrzebne, aby skorzysta z niestandardowego kodu, np. Windows API, DirectX i innych. Tak moemy poda dodatkowe opcje dla linkera, niezalenie od tych podanych w ustawieniach projektu.
exestr
user
compiler
lib
linker
Tabela 16. Typy komentarzy w dyrektywie #pragma comment w Visual C++ .NET
Spord tych moliwoci najczciej stosowane s lib i linker, poniewa pozwalaj zarzdza procesem linkowania. Oprcz tego exestr umoliwia zostawienie w pliku EXE dodatkowego tekstu informacyjnego, np.: #pragma comment(exestr, "Skompilowano: " __DATE__ __TIME__) Jak wida na zaczonym obrazku, w takim tekcie mona stosowa te makra.
once
Na ostatku przypomnimy sobie pierwsze poznane polecenie #pragma - once: #pragma once Wiemy ju doskonale, jakie jest dziaanie dyrektywy #pragma once. Ot powoduje ona, e zawierajcy j plik bedzie wczany tylko raz podczas przegldania kodu przez preprocesor. Kade sukcesywne wystpienie dyrektywy #include z tyme plikiem zostanie zignorowane.
340
Zaawansowane C++
Dyrektywa #pragma once jest obecnie obsugiwana przez bardzo wiele kompilatorw nie tylko przez Visual C++. Istnieje wic niemaa szansa, e niedugo podobna dyrektywa stanie si czci standardu C++. Na pewno jednak nie bdzie to #pragma once, gdy wszystkie szczegy dyrektyw #pragma s z zaoenia przynalene konkretnemu kompilatorowi, a nie jzykowi C++ w ogle. Jeli sam miabym optowa za jak konkretn, ustandaryzowan propozycj skadniow dla tego rozwizania, to chyba najlepsze byoby po prostu #once. *** I t sugesti dla Komitetu Standaryzacyjnego C++ zakoczylimy omawianie preprocesora i jego dyrektyw :)
Podsumowanie
Ten rozdzia by powicony rzadko spotykanej w jzykach programowania waciwoci C++, jak jest preprocesor. Moge z niego dowiedzie si wszystkiego na temat roli tego wanego mechanizmu w procesie budowania programu oraz pozna jego dyrektywy. Pozwoli ci to na sterowanie procesem kompilacji wasnego kodu. W tym rozdziale staraem si te w jak najbardziej obiektywny sposb przedstawi makra i makrodefinicje, gdy na ich temat wygasza si czsto wiele bdnych opinii. Chciaem wic uwiadomi ci, e chocia wikszo dawnych zastosowa makr zostaa ju wyparta przez inne konstrukcje jzyka, to makra s nadal przydatne w skracaniu zapisu czesto wystpujcych fragmentw kodu oraz przede wszystkim - w kompilacji warunkowej. Istnieje te wiele sposobw na wykorzystanie makr, ktre nosz znamiona trikw - by moe natrafisz na takowe podczas lektury innych kursw, ksiek i dokumentacji. Warto by wtedy pamita, e w stosowaniu makr, jak i we wszystkim w programowaniu, naley zawsze umie znale rwnowag midzy efektownoci a efektywnoci kodowania. Preprocesor oraz omwione wczeniej wskaniki byy naszym ostatnim spotkaniem z krain starego C w obrbie krlestwa C++. Kolejne trzy rozdziay skupiaj si na zaawansowanych cechach tego ostatniego: programowaniu obiektowym (ze szczeglnym uwzgldnieniem przeciania operatorw), wyjtkach oraz szablonach. Wpierw zobaczymy usprawnienia OOPu, jakie oferuje nam jzyk C++.
Pytania i zadania
Moesz uwaa, e preprocesor jest reliktem przeszoci, ale nie uchroni ci to od wykonania obowizkowej pracy domowej! ;))
Pytania
1. 2. 3. 4. 5. 6. 7. 8. Czym jest preprocesor? Kiedy wkracza do akcji i jak dziaa? Na czym polega mechanizm rozwijania i zastpowania makr? Jakie dwa rodzaje makr mona wyrni? Na jakie problemy mona natrafi, jeeli sprbuje si zastosowa makra zamiast bardziej odpowiednich, innych konstrukcji jzyka C++? Jakie dwa zastosowania makr pozostaj nadal aktualne? Jakie wyraenia moe zawiera warunek kompilacji dyrektyw #if i #elif? Czym rni si dwa warianty dyrektywy #include? Jak rol peni dyrektywa #pragma?
Preprocesor
341
wiczenia
1. Opracuj (klasyczne ju) makro wyznaczajce wiksz z dwch podanych wartoci. 2. (Trudniejsze) Odszukaj definicj klasy CIntArray z rozdziau o wskanikach i przy pomocy preprocesora przerb j tak, aby mona by z niej korzysta dla dowolnego typu danych. 3. Otwrz kod aplikacji rozwizujcej rwnania kwadratowe, ktr (mam nadziej) napisae w rozdziale 1.4. Dodaj do niej kod pomocniczy, wywietlajcy warto delta dla podanego rwnania; niech kompiluje si on tylko wtedy, gdy zdefiniowana zostanie nazwa DEBUG. 4. (Trudne) Skonstruuj warunek kontrolowanej kompilacji, ktry pozwoli na wykrycie platform 16-, 32- i 64-bitowych. Wskazwka: wykorzystaj charakterystyk typu int
2
ZAAWANSOWANA OBIEKTOWO
Nuda jest wrogiem programistw.
Bjarne Stroustrup
C++ jest zasuonym czonkiem licznej obecnie rodziny jzykw obiektowych. Oferuje on wszystkie koniecznie mechanizmy, suce praktycznej realizacji idei programowania zorientowanego obiektowo. Poznalimy je w dwch rozdziaach poprzedniej czci kursu. Midzy C++ a innymi jzykami OOP wystpuj jednak pewne rnice. Nasz jzyk ma wiele specyficznych dla siebie moliwoci, ktre maj za zadanie uatwienie ycia programicie. Czsto te przyczyniaj si do powstania obiektywnie lepszych programw. W tym rozdziale poznamy t wanie stron OOPu w C++. Przedstawione tu zagadnienia, cho w zasadzie niezbdne do wystarczajcej znajomoci jzyka, s w duej czci przydatnymi udogonieniami. Nie niezbdnymi, lecz wielce interesujcymi i praktycznymi. Poznanie ich sprawi, e nasze obiektowe programy bd wygodne w konstruowaniu i pniejszej modyfikacji. Programowanie stanie si po prostu atwiejsze i przyjemniejsze a to chyba bdzie bardzo znaczcym osigniciem. Zobaczmy wic, jakie wyjtkowe konstrukcje OOP oferuje nam C++.
O przyjani
W czasie pierwszych spotka z programowaniem obiektowym wspominaem do czsto o jego zaletach, wymieniajc wrd nich podzia kodu na drobne i atwe to zarzdzania kawaki. Tymi fragmentami (take pod wzgldem koncepcyjnym) s oczywicie klasy. Plusem, jaki niesie za soba stosowanie klas, jest wyodrbnienie kodu i danych w obiekty zajmujce si konkretnymi zadaniami i reprezentujcymi konkretne obiekty. Instancje klas wsppracuj ze sob i dziki temu wypeniaj zadania aplikacji. Tak to wyglda przynajmniej w teorii :) Atutem klas jest niezaleno, zwana fachowo hermetyzacj lub enkapsulacj. Objawia si ona tym, i dana klasa posiada pewien zestaw pl i metod, z ktrym tylko wybrane s dostpne dla wiata zewntrznego. Jej wewntrzne sprawy s cakowicie chronione; su ku temu specyfikatory dostpu, jak private i protected. Opatrzone nimi skadowe s w zasadzie cakiem odseparowane od wiata zewntrznego, bo ten jest dla nich potencjalnie grony. Upubliczniajc swoje pole klasa naraaaby przecie swoje dane na przypadkowe lub celowe, ale zawsze niepodane modyfikacje. To tak jakby wyj z domu i zostawi drzwi niezamknite na klucz: nie jest to wpradzie bezporednie zaproszenie dla zodzieja, ale taka okazja moe go uczyni - w myl znanego powiedzenia. Ale przecie nie wszyscy s li - kady ma przynajmniej kilku przyjaci. Przyjaciel jest to osoba, na ktr mona liczy; o ktrej wiemy, e nie zrobi nam nic zego. Wikszo ludzi uwaa, e przyja jest w yciu bardzo wana - i nie musz nas do tego
344
Zaawansowane C++
przekonywa adni socjologowie. Wszyscy wiemy to dobrze z wasnego, yciowego dowiadczenia. No dobrze, ale co to ma wsplnego z programowaniem? Ot bardzo wiele, zwaszcza z programowaniem obiektowym. Mianowicie, klasa take moe mie przyjaci: mog by nimi globalne funkcje, metody innych klas, a take inne klasy w caoci. C to jednak znaczy, e klasa ma jakiego przyjaciela? Wyjanijmy wic, e: Przyjaciel (ang. friend) danej klasy ma dostp do jej wszystkich skadnikw - take tych chronionych, a nawet prywatnych. Jeeli zatem klasa posiada przyjaciela, to oznacza to, e daa mu klucze (dostp) do swojego mieszkania (niepublicznych skadowych). Przyjaciel klasy ma do nich prawie takie samo prawo, jak metody teje klasy. Pewne drobne rnice wyjanimy sobie przy okazji osobnego omwienia zaprzyjanionych funkcji i klas. Dowiedzmy si teraz, jak zaprzyjani z klas jaki inny element programu. Jest oczywicie i jak zwykle bardzo proste ;) Naley bowiem umieci w definicji klasy tzw. deklaracj przyjani (ang. friend declaration): friend deklaracja_przyjaciela; Sowem kluczowym friend poprzedzamy w niej deklaracj_przyjaciela. T deklaracj moe by: prototyp funkcji globalnej prototyp metody ze zdefiniowanej wczeniej klasy nazwa zadeklarowanej wczeniej klasy Oto najprostszy i niezbyt mdry przykad: class CFoo { private: std::string m_strBardzoOsobistyNapis; public: // konstruktor CFoo() { m_strBardzoOsobistyNapis = "Kocham C++!"; } // deklaracja przyjani z funkcj friend void Wypisz(CFoo*);
};
// zaprzyjaniona funkcja void Wypisz(CFoo* pFoo) { std::cout << pFoo->m_strBardzoOsobistyNapis; } Zaprzyjaniony byt - w tym przypadku funkcja - ma tu peen dostp do prywatnego pola klasy CFoo. Moe wic wypisa jego zawarto dla kadego obiektu tej klasy, jaki zostanie mu podany. Deklaracja przyjani w tym przykadzie wydaje si by umieszczona w sekcji public klasy CFoo. Tak jednak nie jest, gdy:
Zaawansowana obiektowo
Deklaracja przyjani moe by umieszczona w kadym miejscu definicji klasy i zawsze ma to samo znaczenie.
345
Jest wic obojtne, gdzie si ona pojawi. Zwykle piszemy j albo na pocztku, albo na kocu klasy, wyrniajc na przykad zmniejszonym wciciem. Pokazujemy w ten sposb, e nie podlega ona specyfikatorom dostpu. Nie ma wic czego takiego jak publiczna deklaracja przyjani lub prywatna deklaracja przyjani. Przyjaciel pozostaje przyjacielem niezalenie od tego, czy si nim chwalimy, czy nie. Skoro teraz wiemy ju z grubsza, czym s przyjaciele klas, omwimy sobie osobno zaprzyjanianie funkcji globalnych oraz innych klas i ich metod.
Funkcje zaprzyjanione
Najpierw zobaczymy, jak zaprzyjani klas z funkcj - tak, aby funkcja miaa dostp do niepublicznych skadnikw z danej klasy.
};
// zaprzyjaniona funkcja bool PrzecinajaSie(CCircle& Okrag1, CCircle& Okrag2) { // obliczamy odlego midzy rodkami float fRoznicaX = Okrag2.m_ptSrodek.x - Okrag1.m_ptSrodek.x; float fRoznicaY = Okrag2.m_ptSrodek.y - Okrag1.m_ptSrodek.y; float fOdleglosc = sqrt(fRoznicaX*fRoznicaX + fRoznicaY*fRoznicaY);
346
Zaawansowane C++
// odlego ta musi by mniejsza od sumy promieni, ale wiksza // od ich bezwzgldnej rnicy return (fOdleglosc < Okrag1.m_fPromien + Okrag2.m_fPromien && fOdleglosc > abs(Okrag1.m_fPromien - Okrag2.m_fPromien);
Bardzo dobrze wida tu ide przyjani: funkcja PrzecinajaSie() ma dostp do skadowych m_ptSrodek oraz m_fPromien z obiektw klasy CCircle - mimo e s prywatne pola klasy. CCircle deklaruje jednak przyja z funkcj PrzecinajaSie(), a zatem udostpnia jej swoje osobiste dane. Zauwamy jeszcze, e w deklaracji przyjani podajemy cay prototyp funkcji, a nie tylko jej nazw. Moliwe jest bowiem zdefiniowanie kilku funkcji o tej nazwie, np. tak: bool PrzecinajaSie(CCircle&, CCircle&); bool PrzecinajaSie(CRectangle&, CRectangle&); bool PrzecinajaSie(CPolygon&, CPolygon&); // itd. (wraz z ewentualnymi kombinacjami krzyowymi) Klasa bdzie jednak przyjania si tylko z t funkcj, ktrej deklaracj zamiecimy po sowie friend. Zapamitajmy po prostu, e: Jedna zwyka deklaracja przyjani oznacza przyja z jedn funkcj.
Zaawansowana obiektowo
Pamitaj zatem, i: Funkcje zaprzyjanione z klas nie s jej skadnikami. Nie posiadaj dostpu do wskanika this tej klasy, gdy nie s jej metodami.
347
W praktyce wic naley jako poda takiej funkcji obiekt klasy, ktra si z ni przyjani. Zobaczylimy w poprzednim przykadzie, e prawie zawsze odbywa si to poprzez parametry. Referencja do obiektu klasy CCircle bya parametrem zaprzyjanionej z ni funkcji PrzecinajaSie(). Tylko posiadajc dostp do obiektu klasy, ktra si z ni przyjani, funkcja zaprzyjaniona moe odnie jak korzy ze swojego uprzywilejowanego statusu.
// gdzie dalej definicja funkcji... pocztkowy prototyp funkcji PrzecinajaSie(), umieszczony przed definicj CCircle, nie jest koniecznie wymagany. Bez niego kompilator skorzysta po prostu z deklaracji przyjani jak z normalnej deklaracji funkcji. Deklaracja przyjani z funkcj moe by jednoczenie deklaracj samej funkcji. Wczeniejsza wiedza kompilatora o istnieniu zaprzyjanianej funkcji nie jest niezbdna, aby funkcja ta moga zosta zaprzyjaniona.
Dodajemy definicj
Najbardziej zaskakujce jest jednak to, e deklarujc przyja z jak funkcj moemy t funkcj jednoczenie zdefiniowa! Nic nie stoi na przeszkodzie, aby po zakoczeniu deklaracji nie stawia rednika, lecz otworzy nawias klamrowy i wpisa tre funkcji: class CVector2D { private: float m_fX, m_fY;
348
public: CVector2D(float fX = 0.0f, float fY = 0.0f) { m_fX = fX; m_fY = fY; }
Zaawansowane C++
};
// zaprzyjaniona funkcja dodajca dwa wektory friend CVector2D Dodaj(CVector2D& v1, CVector2D& v2) { return CVector2D(v1.m_fX + v2.m_fX, v1.m_fY + v2.m_fY); }
Nie zapominajmy, e nawet wwczas funkcja zaprzyjaniona nie jest metod klasy pomimo tego, e jej umieszczenie wewntrz definicji klasy sprawia takie wraenie. W tym przypadku funkcja Dodaj() jest nadal funkcj globaln - wywoujemy j bez pomocy adnego obiektu, cho oczywicie przekazujemy jej obiekty CVector2D w parametrach i taki te obiekt otrzymujemy z powrotem: CVector2D vSuma = Dodaj(CVector2D(1.0f, 2.0f), CVector2D(0.0f, -1.0f)); Umieszczenie definicji funkcji zaprzyjanionej w bloku definicji klasy ma jednak pewien skutek. Ot funkcja staje si wtedy funkcj inline, czyli jest rozwijana w miejscu swego wywoania. Przypomina pod tym wzgldem metody klasy, ale jeszcze raz powtarzam, e metod nie jest. Moe najlepiej bdzie, jeli zapamitasz, e: Wszystkie funkcje zdefiniowane wewntrz definicji klasy s automatycznie inline, jednak tylko te bez swka friend s jej metodami. Pozostae s funkcjami globalnymi, lecz zaprzyjanionymi z klas.
Klasy zaprzyjanione
Zaprzyjanianie klas z funkcjami globalnymi wydaje si moe nieco dziwnym rozwizaniem (gdy czciowo amie zalet OOPu - hermetyzacj), ale niejednokrotnie bywa przydatnym mechanizmem. Bardziej obiektowym podejciem jest przyja klas z innymi klasami - jako caociami lub tylko z ich pojedynczymi metodami.
Zaawansowana obiektowo
Tym razem funkcja PrzecinajaSie() staa si skadow klasy CGeometryManager. To bardziej obiektowe rozwizanie - tym bardziej dobre, e nie przeszkadza w zadeklarowaniu przyjani z t funkcj. Teraz jednak klasa z CCircle przyjani si z metod innej klasy - CGeometryManager. Odpowiedni zmian (do naturaln) wida wic w deklaracji przyjani.
349
Przyja z metodami innych klas byaby bardzo podobna do przyjani z funkcjami globalnymi gdyby nie jeden szkopu. Kompilator musi mianowicie zna deklaracj zaprzyjanianej metody (CGeometryManager::PrzecinajaSie()) ju wczeniej. To za wi si z koniecznoci zdefiniowania jej macierzystej klasy (CGeometryManager). Do tego potrzebujemy jednak informacji o klasie CCircle, aby moga ona wystpi jako typ agrumentu metody PrzecinajaSie(). Rozwizaniem jest deklaracja zapowiadajca, w ktre informujemy kompilator, e CCircle jest klas, nie mwiac jednak niczego wicej. Z takimi deklaracjami spotkalimy si ju wczeniej i jeszcze spotkamy si nie raz - szczeglnie w kontekcie przyjani midzyklasowej. Chwileczk! A co z t zaprzyjanian metod, CGeometryManager::PrzecinajaSie()? Czyby miaa ona nie posiada dostpu do wskanika this, mimo e jest funkcj skadow klasy? Odpowied brzmi: i tak, i nie. Wszystko zaley bowiem od tego, o ktry wskanik this nam dokadnie chodzi. Jeeli o ten pochodzcy od CGeometryManager, to wszystko jest w jak najlepszym porzdku: metoda PrzecinajaSie() posiada go oczywicie, zatem ma dostp do skadnikw swojej macierzystej klasy. Jeli natomiast mamy na myli klas CCircle, to faktycznie metoda PrzecinajaSie() nie ma dojcia do wskanika this tej klasy! Zgadza si to cakowicie z faktem, i funkcja zaprzyjaniona nie jest metod klasy, ktra si z ni przyjani - tak wic nie posiada wskanika this tej klasy (tutaj CCircle). Funkcja moe by jednak metod innej klasy (tutaj CGeometryManager), a dostp do jej skadnikw bdzie mie zawsze - takie s przecie podstawowe zaoenia programowania obiektowego.
Przyja z ca klas
Deklarujc przyja jednej klasy z metodami innej klasy, mona pj o krok dalej. Dlaczego na przykad nie powiza przyjani od razu wszystkich metod pewnej klasy z nasz? Oczywicie monaby pracowicie zadeklarowa przyja ze wszystkimi metodami tamtej klasy, ale jest prostsze rozwizanie. Moe zaprzyjani jedn klas z drug. Deklaracja przyjani z ca klas jest nad wyraz prosta: friend class nazwa_zaprzyjanionej_klasy; Zastpuje ona deklaracje przyjani ze wszystkimi metodami klasy o podanej nazwie, wyszczeglnionymi osobno. Taka forma jest poza tym nie tylko krtsza, ale te ma kilka innych zalet. Wpierw jednak spjrzmy na przykad: class CPoint; class CRect { private: // ... public: bool PunktWewnatrz(CPoint&);
};
350
class CPoint { private: float m_fX, m_fY; public: CPoint(float fX = 0.0f, float fY = 0.0f) { m_fX = fX; m_fY = fY; } // deklaracja przyjani z Crect friend class CRect;
Zaawansowane C++
};
Wyznanie przyjani, ktry czyni klasa CPoint, sprawia, e zaprzyjaniona klasa CRect ma peen dostp do jej skadnikw niepublicznych. Metoda CRect::PunktWewnatrz() moe wic odczyta wsprzdne podanego punktu i sprawdzi, czy ley on wewntrz prostokta opisanego przez obiektt klasy CRect. Zauwamy jednoczenie, e klasa CPoint nie ma tutaj podobnego dostpu do prywatnych skadowych CRect. Klasa CRect nie zadeklarowaa bowiem przyjani z klas CPoint. Wynika std bardzo wana zasada: Przyja klas w C++ nie jest automatycznie wzajemna. Jeeli klasa A deklaruje przyja z klas B, to klasa B nie jest od razu take przyjacielem klasy A. Obiekty klasy B maj wic dostp do niepublicznych danych klasy A, lecz nie odwrotnie. Do czsto aczkolwiek yczymy sobie, aby klasy wzajemnie deklaroway sobie przyja. Jest to jak najbardziej moliwe: po prostu w obu klasach musz by deklaracje przyjani: class CBar; class CFoo { friend class CBar; }; class CBar { friend class CFoo; }; Wymaga to zawsze zastosowania deklaracji zapowiadajcej, gdy kompilator musi wiedzie, e dana nazwa jest klas, zanim pozwoli na jej zastosowanie w konstrukcji friend class. Nie musi natomiast zna caej definicji klasy, co byo wymagane dla przyjani z pojedynczymi metodami. Gdyby tak byo, to wzajemna przyja klas nie byaby moliwa. Kompilator zadowala si na szczcie sam informacj CBar jest klas, bez wnikania w szczegy, i przyjmuje deklaracj przyjani z klas, o ktrej w zasadzie nic nie wie. Kompilator nie przyjmie natomiast deklaracji przyjani z pojedyncz metod nieznanej bliej klasy. Sprawia to, e wybircza przyja dwch klas nie jest moliwa, bo wymagaaby niemoliwego: zdefiniowania pierwszej klasy przed definicj drugiej oraz zdefiniowania drugiej przed definicj pierwszej. To oczywicie niemoliwe, a kompilator nie zadowoli si niestety sam deklaracj zapowiadajc - jak to czyni przy deklarowaniu cakowitej przejani (friend class klasa;).
Zaawansowana obiektowo
351
102 Jest tak, gdy stosujemy dziedziczenie publiczne (class pochodna : public bazowa), ale tak robimy niemal zawsze.
352
Zaawansowane C++
Zastosowania
Mwic o zastosowaniach przyjani, musimy rozgraniczy zaprzyjanione klasy i funkcje globalne.
Konstruktory w szczegach
Konstruktory peni w C++ wyjtkowo duo rl. Cho oczywicie najwaniejsza (i w zasadzie jedyn powan) jest inicjalizacja obiektw - instancji klas, to niejako przy okazji mog one dokonywa kilku innych, przydatnych operacji. Wszystkie one wi si z tym gwnym zadaniem. W tym podrozdziale nie bdziemy wic mwi o tym, co robi konstruktor (bo to wiemy), ale jak moe to robi. Innymi sowy, dowiesz si, jak wykorzysta rne rodzaje konstruktorw do wasnych szczytnych celw programistycznych.
Maa powtrka
Najpierw jednak przyda si mae powtrzenie wiedzy, ktra bdzie nam teraz przydatna. Przy okazji moe j troch usystematyzujemy; powinno si te wyjani to, co do tej pory mogo by dla ciebie ewentualnie niejasne. Zaczniemy od przypomnienia konstruktorw, a pniej procesu inicjalizacji.
Zaawansowana obiektowo
353
Konstruktory
Konstruktor jest specjaln metod klasy, wywoywan podczas tworzenia obiektu. Nie jest on, jak si czasem bdnie sdzi, odpowiedzialny za alokacj pamici dla obiektu, lecz tylko za wstpne ustawienie jego pl. Niejako przy okazji moe on aczkolwiek podejmowa te inne czynnoci, jak zwyka metoda klasy.
Cechy konstruktorw
Konstruktory tym jednak rni si od zwykych metod, i: nie posiadaj wartoci zwracanej. Konstruktor nic nie zwraca (bo i komu?), nawet typu pustego, czyli void. Zgoda, mona si spiera, e wynikiem jego dziaania jest obiekt, lecz konstruktor nie jest jedynym mechanizmem, ktry bierze udzia w jego tworzeniu: liczy si jeszcze alokacja pamici. Dlatego te przyjmujemy, e konstruktor nie zwraca wartoci. Wida to zreszt w jego deklaracji nie mog by wywoywane za porednictwem wskanika na funkcje. Przyczyna jest prosta: nie mona pobra adresu konstruktora maj mnstwo ogranicze co do przydomkw w deklaracjach: nie mona ich czyni metodami staymi (const) nie mog by metodami wirtualnymi (virtual), jako e sposb ich wywoywania w warunkach dziedziczenia jest zupenie odmienny od obu typw metod: wirtualnych i niewirtualnych. Wspominaym o tym przy okazji dziedziczenia. nie mog by metodami statycznymi klas (static). Z drugiej strony posiadaj unikaln cech metod statycznych, jak jest moliwo wywoania bez koniecznoci posiadania obiektu macierzystej klasy. Konstruktory maj jednak dostp do wskanika this na tworzony obiekt, czego nie mona powiedzie o zwykych metodach statycznych nie s dziedziczone z klas bazowych do pochodnych Wida wic, e konstruktor to bardzo dziwna metoda: niby zwraca jak warto (tworzony obiekt), ale nie deklarujemy mu wartoci zwracanej; nie moe by wirtualny, ale w pewnym sensie jest; nie moe by statyczny, ale posiada cechy metod statycznych; jest funkcj, ale nie mona pobra jego adresu, itd. To wszystko wydaje si nieco zakrcone, lecz wiemy chyba, e nie przeszkadza to wcale w normalnym uywaniu konstruktorw. Zamiast wic rozstrzsa fakty, czym te metody s, a czym nie, zajmijmy si ich definiowaniem.
Definiowanie
W C++ konstruktor wyrnia si jeszcze tym, e jego nazwa odpowiada nazwie klasy, na rzecz ktrej pracuje. Przykadowa deklaracja konstruktora moe wic wyglda tak: class CFoo { private: int m_nPole; public: CFoo(int nPole) { m_nPole = nPole; }
};
Przecianie
Zwyke metody klasy take mona przecia, ale w przypadku konstruktorw dzieje si to nadzwyczaj czsto. Znowu posuymy si przykadem wektora:
354
Zaawansowane C++
class CVector2D { private: float m_fX, m_fY; public: // konstruktor, trzy sztuki CVector2D() { m_fX = m_fY = 0.0f; } CVector2D(float fDlugosc) { m_fX = m_fY = fDlugosc / sqrt(2); } CVector2D(float fX, float fY) { m_fX = fX; m_fY = fY; }
};
Definiujc przecione konstruktory powinnimy, analogicznie jak w przypadku innych metod oraz zwykych funkcji, wystrzega si niejednoznacznoci. W tym przypadku powstaaby ona, gdyby ostatni wariant zapisa jako: CVector2D(float fX = 0.0f, float fY = 0.0f); Wwczas mgby on by wywoany z jednym argumentem, podobnie jak konstruktor nr 2. Kompilator nie zdecyduje, ktry wariant jest lepszy i zgosi bd.
Konstruktor domylny
Konstruktor domylny (ang. default constructor), zwany te domniemanym, jest to taki konstruktor, ktry moe by wywoany bez podawania parametrw. W klasie powyej jest to wic pierwszy z konstruktorw. Gdybymy jednak ca trjk zastpili jednym: CVector2D(float fX = 0.0f, float fY = 0.0f) { m_fX = fX; m_fY = fY; } to on take byby konstruktorem domylnym. Ilo podanych do niego parametrw moe by bowiem rwna zeru. Wida wic, e konstruktor domylny nie musi by akurat tym, ktry faktycznie nie posiada parametrw w swej deklaracji (tzw. parametrw formalnych). Naturalnie, klasa moe mie tylko jeden konstruktor domylny. W tym przypadku oznacza to, e konstruktor w formie CVector2D(), CVector2D(float fDlugosc = 0.0f) czy jakikolwiek inny tego typu nie jest dopuszczalny. Powstaaby bowiem niejednoznaczno, a kompilator nie wiedziaby, ktr metod powinien wywoywa. Za wygeneroowanie domylnego konstruktora moe te odpowiada sam kompilator. Zrobi to jednak tylko wtedy, gdy sami nie podamy jakiegolwiek innego konstruktora. Z drugiej strony, nasz wasny konstruktor domylny zawsze przesoni ten pochodzcy od kompilatora. W sumie mamy wic trzy moliwe sytuacje: nie podajemy adnego wasnego konstruktora - kompilator automatycznie generuje domylny konstruktor publiczny podajemy wasny konstruktor domylny (jeden i tylko jeden) - jest on uywany podajemy wasne konstruktory, ale aden z nich nie moe by domylny, czyli wywoywany bez parametrw - wwczas klasa nie ma konstruktora domylnego Tak wic tylko w dwch pierwszych sytuacjach klasa posiada domylny konstruktor. Jaka jest jednak korzy z jego obecnoci? Ot jest ona w sumie niewielka: tylko obiekty posiadajce konstruktor domylny mog by elementami tablic. Podkrelam: chodzi o obiekty, nie o wskaniki do nich - te mog by czone w tablice bez wzgldu na konstruktory
Zaawansowana obiektowo
355
tylko klas posiadajc konstruktor domylny mona dziedziczy bez dodatkowych zabiegw przy konstruktorze klasy pochodnej T drug zasad wprowadziem przy okazji dziedziczenia, cho nie wspominaem o owych dodatkowych zabiegach. Bd one treci tego podrozdziau.
Niejawne wywoanie
Niejawne wywoanie (ang. implicit call) wystpuje wtedy, gdy to kompilator wywouje nasz konstruktor. Jest par takich sytuacji: najprostsza: gdy deklarujemy zmienn obiektow, np.: CFoo Foo; w momencie tworzenia obiektu, ktry zawiera w sobie pola bdce zmiennymi obiektowymi innych klas w chwili tworzenia obiektu klasy pochodnej jest wywoywany konstruktor klasy bazowej
Jawne wywoanie
Konstruktor moemy te wywoa jawnie. Mamy wtedy wywoanie niejawne (ang. explicit call), ktre wystpuje np. w takich sytuacjach: przy konstruowaniu obiektu operatorem new przy jawnym wywoaniu konstruktora: nazwa_klasy([parametry]) W tym drugim przypadku mamy tzw. obiekt chwilowy. Zwracalimy taki obiekt, kopiujc go do rezultatu funkcji Dodaj(), prezentujc funkcje zaprzyjanione.
Inicjalizacja
Teraz powiemy sobie wicej o inicjalizacji. Jest to bowiem proces cile zwizany z aspektami konstruktorw, ktre omwimy w tym podrozdziale. Inicjalizacja (ang. initialization) jest to nadanie obiektowi wartoci pocztkowej w chwili jego tworzenia.
Kiedy si odbywa
W naturalny sposb inicjalizacj wiemy z deklaracj zmiennych. Odbywa si ona jednak take w innych sytuacjach. Dwie kolejne zwizane z funkcjami. Ot jest to: przekazanie wartoci poprzez parametr zwrcenie wartoci jako rezultatu funkcji Wreszcie, ostatnia sytuacja zwizana jest inicjalizacj obiektw klas - poznamy j za chwil.
Jak wyglda
Inicjalizacja w oglnoci wyglda mniej wicej tak: typ zmienna = inicjalizator;
356
Zaawansowane C++
Agregaty
Bardziej zozone typy danych moemy inicjalizowa w specjalny sposb, jako tzw. agregaty. Agregatem jest tablica innych agregatw (wzgldnie elementw typw podstawowych) lub obiekt klasy, ktra: nie dziedziczy z adnej klasy bazowej posiada tylko skadniki publiczne (public, ewentualnie bez specyfikatora w przypadku typw struct) nie posiada funkcji wirtualnych nie posiada zadeklarowanego konstruktora Agregaty moemy inicjalizowa w specjalny sposb, podajc wartoci wszystkich ich elementw (pl). Znamy to ju tablic, np.: int aTablica[13] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41 }; Podobnie moe to si odbywa take dla struktur (tudzie klas), speniajcych cztery podane warunki: struct VECTOR3 { float x, y, z; }; VECTOR3 vWektor = { 6.0f, 12.5f, 0.0f }; W przypadku bardziej skomplikowanych, zagniedonych agregatw, bdziemy mieli wicej odpowiednich par nawiasw klamrowych: VECTOR3 aWektory[3] = { { 0.0f, 2.0f, -3.0f }, { -1.0f, 0.0f, 0.0f }, { 8.0f, 6.0f, 4.0f } }; Mona je aczkolwiek opuci i napisa te 9 wartoci jednym cigiem, ale przyznasz chyba, e w tej postaci inicjalizacja wyglda bardziej przejrzycie. Po inicjalizatorze wida przynajmniej, e inicjujemy tablic trj-, a nie dziewicioelementow.
Inicjalizacja konstruktorem
Ostatni sposb to inicjalizacja obiektu jego wasnym konstruktorem - na przykad: std::string strZmienna = "Hmm..."; Tak, to jest jak najbardziej taki wanie przykad. W rzeczywistoci kompilator rozwinie go bowiem do: std::string strZmienna("Hmm...");
Zaawansowana obiektowo
357
gdy w klasie std::string istnieje odpowiedni konstruktor przyjmujcy jeden argument typu napisowego103: string(const char[]); Konstruktor jest tu wic wywoywany niejawnie - jest to tak zwany konstruktor konwertujcy, ktremu przyjrzymy si bliej w tym rozdziale.
Listy inicjalizacyjne
W definicji konstruktora moemy wprowadzi dodatkowy element - tzw. list inicjalizacyjn: nazwa_klasy::nazwa_klasy([parametry]) : lista_inicjalizacyjna { ciao_konstruktora } Lista inicjalizacyjna (ang. initializers list) ustala sposb inicjalizacji obiektw tworzonej klasy. Za pomoc takiej listy moemy zainicjalizowa pola klasy (i nie tylko) jeszcze przed wywoaniem samego konstruktora. Ma to pewne konsekwencje i bywa przydatne w okrelonych sytuacjach.
Inicjalizacja skadowych
Dotychczas dokonywalimy inicjalizacji pl klasy w taki oto sposb: class CVector2D { private: float m_fX, m_fY; public: CVector2D(float fX = 0.0f, float fY = 0.0f) { m_fX = fX; m_fY = fY; }
};
Przy pomocy listy inicjalizacyjnej zrobimy to samo mniej wicej tak: CVector2D(float fX = 0.0f, float fY = 0.0f) : m_fX(fX), m_fY(fY) { } Jaka jest rnica? konstruktor moe u nas by pusty. To najprawdopodobniej sprawi, e kompilator zastosuje wobec niego jak optymalizacj dziaania m_fX(fX) i m_fY(fY) (zwrmy uwag na skadni), maj charakter inicjalizacji pl, podczas gdy przypisania w ciele konstruktora s przypisaniami wanie lista inicjalizacyjna jest wykonywana jeszcze przed wejciem w ciao konstruktora i wykonaniem zawartych tam instrukcji Drugi i trzeci fakt jest bardzo wany, poniewa daj nam one moliwo umieszczania w klasie takich pl, ktre nie moga oby si bez inicjalizacji, a wic:
103 W rzeczywistoci ten konstruktor wyglda znacznie obszerniej, bo w gr wchodz jeszcze szablony z biblioteki STL. Nic jednak nie staoby na przeszkodzie, aby tak to wanie wygldao.
358
staych (pl z przydomkiem const) staych wskanikw (typ* const) referencji obiektw, ktrych klasy nie maj domylnych konstruktorw
Zaawansowane C++
Lista inicjalizacyjna gwarantuje, e zostan one zainicjalizowane we waciwym czasie podczas tworzenia obiektu: class CFoo { private: const float m_fPole; // nie moe by: const float m_fPole = 42; !! public: // konstruktor - inicjalizacja pola CFoo() : m_fPole(42) { /* m_fPole = 42; // te nie moe by - za pno! // m_fPole musi mie warto ju // na samym pocztku wykonywania // konstruktora */ }
};
Mwiem te, e inicjalizacja przy pomocy listy inicjalizacyjnej jest szybsza od przypisa w ciele konstruktora. Powinnimy wic stosowa j, jeeli mamy tak moliwo, a decyzja na ktrej z dwch rozwiza nie robi nam rnicy. Zauwamy te, e zapis na licie inicjalizacyjnej jest po prostu krtszy. W licie inicjalizacyjnej moemy umieszcza nie tylko czyste stae i argumenty konstruktora, lecz take zloone wyraenia - nawet z wywoaniami metod czy funkcji globalnych. Nie ma wic adnych ogranicze w stosunku do przypisania.
Zaawansowana obiektowo
public: CIndirectBase(int nPole1) : m_nPole1(nPole) { }
359
};
class CDirectBase : public CIndirectBase { public: // wywoanie konstruktora klasy bazowej CDirectBase(int nPole1) : CIndirectBase(nPole1) { } }; class CDerived : public CDirectBase { protected: float m_fPole2; public: // wywoanie konstruktora klasy bezporednio bazowej CDerived(int nPole1, float fPole2) : CDirectBase(nPole1), m_fPole2(fPole2) { }
};
Zwrmy uwag szczeglnie na klas CDerived. Jej konstruktor wywouje konstruktor z klasy bazowej bezporedniej - CDirectBase, lecz nie z poredniej - CIndirectBase. Nie ma po prostu takiej potrzeby, gdy za relacje midzy konstruktorami klas CDirectBase i CIndirectBase odpowiada tylko ta ostatnia. Jak zreszt wida, wywouje ona jedyny konstruktor CIndirectBase. Spjrzmy jeszcze na parametry wszystkich konstruktorw. Jak wida, zachowuj one parametry konstruktorw klas bazowych - zapewne dlatego, e same nie potrafi poda dla nich sensownych danych i bd ich da od twrcy obiektu. Uzyskane dane przekazuj jednak do swoich przodkw; powstaje w ten sposb swoista sztafeta, w ktrej dane z konstruktora najniszego poziomu dziedziczenia trafiaj w kocu do klasy bazowej. Po drodze s one przekazywane z rk do rk i ewentualnie zostawiane w polach klas porednich. Wszystko to dzieje si za porednictwem list inicjalizacyjnej. W praktyce ich wykorzystanie eliminuje wic bardzo wiele sytuacji, ktre wymagaj definiowania ciaa konstruktora. Sam si zreszt przekonasz, e cae mnstwo pisanych przez ciebie klas bedzie zawierao puste konstruktory, realizujce swoje funkcje wycznie poprzez listy inicjalizacyjne.
Konstruktory kopiujce
Teraz porozmawiamy sobie o kopiowaniu obiektw, czyli tworzeniu ich koncepcyjnych duplikatw. W C++ mamy na to dwie wydzielone rodzaje metod klas: konstruktory kopiujce, tworzce nowe obiekty na podstawie ju istniejcych przecione operatory przypisania, ktrych zadaniem jest skopiowanie stanu jednego obiektu do drugiego, ju istniejcego Przecianiem operatorw zajmiemy si dalszej czci rozdziau. W tej sekcji przyjrzymy si natomiast konstruktorom kopiujcym.
O kopiowaniu obiektw
Wydawaoby si, e nie ma nic prostszego od skopiowania obiektu. Okazuje si jednak, e czsto nieodzowne s specjalne mechanizmy temu suce Sprawdmy to.
360
Zaawansowane C++
Pole po polu
Gdy mwimy o kopiowaniu obiektw i nie zastanawiamy si nad tym duej, to sdzimy, e to po prostu skopiowanie danych - zawartoci pl - z jednego obszaru pamici do drugiego. Przykadowo, spjrzmy na dwa wektory: CVector2D vWektor1(1.0f, 2.0f, 3.0f); CVector2D vWektor2 = vWektor1; Cakiem susznie oczekujemy, e po wykonaniu kopiowania vWektor1 do vWektor2 oba obiekty bd miay identyczne wartoci pl. W przypadku takich struktur danych jak wektory, jest to zupenie wystarczajce. Dlaczego? Ot wszystkie ich pola s cakowicie odrbnymi zmiennymi - nie maj adnych koneksji z otaczajcym je wiatem. Trudno przecie oczekiwa, eby liczby typu float robiy cokolwiek innego poza przechowywaniem wartoci. Ich proste skopiowanie jest wic waciwym sposobem wykonania kopii wektora - czyli obiektu klasy CVector2D. Samowystarczalne obiekty mog by kopiowane poprzez dosowne przepisanie wartoci swoich pl.
// ------------------------------------------------------------// pobieranie i ustawianie elementw tablicy int Pobierz(unsigned uIndeks) const { if (uIndeks < m_uRozmiar) return m_pnTablica[uIndeks]; else return 0; } bool Ustaw(unsigned uIndeks, int nWartosc) { if (uIndeks >= m_uRozmiar) return false; m_pnTablica[uIndeks] = uWartosc; return true; } // inne
Zaawansowana obiektowo
unsigned Rozmiar() const { return m_uRozmiar; }
361
};
Pytanie brzmi: jak skopiowa tablic typu CIntArray? Niby nic prostszego: CIntArray aTablica1; CIntArray aTablica2 = aTablica1; // hmm... W rzeczywistoci mamy tu bardzo powany bd. Metoda pole po polu zupenie nie sprawdza si w przypadku tej klasy. Problemem jest pole m_pnTablica: jesli skopiujemy ten wskanik, to otrzymamy nic innego, jak tylko kopi wskanika. Bdzie si on odnosi do tego samego obszaru pamici. Zamiast wic dwch fizycznych tablic mamy tylko jedn, a obiekty Tablica1 i Tablica2 to jedynie kopie opakowa dla wskanika na t tablic. Odwoujc si do danych, zapisanych w rzekomo odrbnych tablicach klasy CIntArray, faktycznie bdziemy odnosi si do tych samych elementw! To powany bd, co gorsza niewykrywalny a do momentu wyprodukowania bdnych rezultatw przez program. Co wic trzeba z tym zrobi - domylasz si, e rozwizaniem s tytuowe konstruktory kopiujce. Jeszcze zanim je poznamy, powiniene zapamita: Jeeli obiekt pracuje na jakim zewntrznym zasobie (np. pamici operacyjnej) i posiada do niego odwoanie (np. wskanik), to jego klas koniecznie naley wyposay w konstruktor kopiujcy. Bez niego zostanie bowiem podczas kopiowanie obiektu zostanie skopiowane samo odwoanie do zasobu (czyli wskanik) zamiast stworzenia jego duplikatu (czyli alokacji nowej porcji pamici). Trzeba te wiedzie, e konieczno zdefiniowania konstruktora kopiujcego zwykle automatycznie pociga za sob wymg obecnoci przecionego operatora przypisania.
Konstruktor kopiujcy
Zobaczmy zatem, jak dziaaj te cudowne konstruktory kopiujce. Jednak oprcz zachwycania si nimi poznamy take sposb ich uycia (definiowania) w C++.
362
Zaawansowane C++
W niej pracuje konstruktor kopiujcy, gdy dokonujemy tu inicjalizacji nowego obiektu starym. Konstruktor kopiujcy jest wywoywany w momencie inicjalizacji nowotworzonego obiektu przy pomocy innego obiektu tej samej klasy. Z tego powodu taki konstruktor jest rwnie zwany inicjalizatorem kopiujcym. Zaraz, jak to - przecie nie zdefiniowalimy dotd adnego specjalnego konstruktora! Jak wic mg on by uyty w kodzie powyej? Owszem, to prawda, ale kompilator wykona robot za nas. Jeli nie zdefiniujemy wasnego konstruktora kopiujcego, to klasa zostanie obdarzona jego najprostszym wariantem. Bdzie on wykonywa zwyke kopiowanie wartoci - dla nas cakowicie niewystarczajce. Musimy zatem wiedzie, jak definiowa wasne konstruktory kopiujce.
// a co to jest?...
Czy w trzeciej linijce take zostanie wywoany konstruktor kopiujcy? Ot nie. Nie jest bowiem inicjalizacja (a wtedy przecie pracuje konstruktor kopiujcy), lecz zwyke przypisanie. Nie tworzymy tu nowego obiektu, lecz przypisujemy jeden ju istniejcy obiekt do drugiego istniejcego obiektu. Wobec braku aktu kreacji nie ma tu miejsca dla adnego konstruktora. Zamiast tego kompilator posuguje si operatorem przypisania. Jeeli go przeciymy (a nauczymy si to robi ju w tym rozdziale), zdefiniujemy wasn akcj dla przypisywania obiektw. W przypadku klasy CIntArray jest to niezbdne, bo nawet obecno konstruktora kopiujcego nie spowoduje, e zaprezentowany wyej kod bdzie poprawny. Konstruktorw nie dotyczy przecie przypisanie.
Zaawansowana obiektowo
{ }
363
ciao_konstruktora
Bierze on jeden parametr, bdcy referencj do obiektu swojej macierzystej klasy. Obiekt ten jest podstaw kopiowania - inaczej mwic, jest to ten obiekt, ktrego kopi ma zrobi konstruktor. W inicjalizacji: CIntArray aTablica2 = aTablica1; parametrem konstruktora kopiujcego bdzie wic aTablica1, za tworzonym obiektemkopi Tablica2. Wida to nawet lepiej w rwnowanej linijce: CIntArray aTablica2(aTablica1); Pozostaje jeszcze kwestia swka const w deklaracji parametru konstruktora. Cho teoretycznie jest ona opcjonalna, to w praktyce trudno znale powd na uzasadnienie jej nieobecnoci. Bez niej konstruktor kopiujcy mgby bowiem potencjalnie zmodyfikowa kopiowany obiekt! Innym skutkiem byaby te niemono kopiowania obiektw chwilowych. Zapamitaj wic: Parametr konstruktora kopiujcego praktycznie zawsze musi by sta referencj.
Po dodaniu tego prostego kodu tworzenie tablicy na podstawie innej, ju istniejcej: CIntArray aTablica2 = aTablica1; jest ju cakowicie poprawne.
Konwersje
Trzecim i ostatnim aspektem konstruktorw, jakim si tu zajmiemy, bedzie ich wykorzystanie do konwersji typw. Temat ten jest jednak nieco szerszy ni wykorzystanie samych tylko konstruktorw, wic omwimy go sobie w caoci.
364
Zaawansowane C++
Konwersje niejawne (ang. implicit conversions) mog nam uatwi programowanie - jak wikszo rzeczy w C++ :) W tym przypadku pozwalaj na przykad uchroni si od koniecznoci definiowania wielu przecionych funkcji. Najlepsz ilustracj bdzie tu odpowiedni przykad. Akurat tak si dziwnie skada, e podrczniki programowania podaj tu najczciej jak klas zoonych liczb. Nie warto naruszac tej dobrej tradycji - zatem spjrzmy na tak oto klas liczby wymiernej: class CRational { private: // licznik i mianownik int m_nLicznik; int m_nMianownik; public: // konstruktor CRational(int nLicznik, int nMianownik) : m_nLicznik(nLicznik), m_nMianownik(nMianownik) { } // ------------------------------------------------------------// metody dostpowe int Licznik() const { return m_nLicznik; } void Licznik(int nLicznik) { m_nLicznik = nLicznik; } int Mianownik() const { return m_nMianownik; } void Mianownik(int nMianownik) { m_nMianownik = (nMianownik != 0 ? nMianownik : 1); }
};
Napiszemy teraz funkcj mnoc przez siebie dwie takie liczby (czyli dwa uamki). Jeli nie spalimy na lekcjach matematyki w szkole podstawowej, to bdzie ona wygldaa chociaby tak: CRational Pomnoz(const CRational& Liczba1, const CRational& Liczba2) { return CRational(Liczba1.Licznik() * Liczba2.Licznik(), Liczba1.Mianownik() * Liczba2.Mianownik()); } Moemy teraz uywa naszej funkcji na przykad w ten sposb: CRational Raz(1, 2), Dwa(2, 3); CRational Wynik = Pomnoz(Raz, Dwa); Niestety, jest pewna niedogodno. Nie moemy zastosowa np. takiego wywoania: CRational Wynik = Pomnoz(Raz, 5); Drugi argument nie moe by bowiem typu int, lecz musi by obiektem typu CRational. To niezbyt dobrze: wiemy przecie, e 5 (i kada liczba cakowita) jest take liczb wymiern. My to wiemy, ale kompilator nie. W tej sekcji poznamy zatem sposoby na informowanie go o takich faktach - czyli wanie niejawne konwersje.
Zaawansowana obiektowo
365
Fachowo mwimy, e chcemy zdefiniowa sposb konwersji typu int na typ CRational. Wanie o takich konwersjach bdziemy mwi w niniejszym paragrafie. Poznamy dwa sposoby na realizacj automatycznej zamiany typw w C++.
Konstruktory konwertujce
Pierwszym z nich jest tytuowy konstruktor konwertujcy.
W ten sposb za jednym zamachem mamy normalny konstruktor, jak te konwertujcy. Ba, mona pj nawet jeszcze dalej: CRational(int nLicznik = 0, int nMianownik = 1) : m_nLicznik(nLicznik), m_nMianownik(nMianownik) { }
Ten konstruktor moe by wywoany bez parametrw, z jednym lub dwoma. Jest on wic jednoczenie domylny i konwertujcy. Duy efekt maym kosztem. Konstruktor konwertujcy nie musi koniecznie definiowa konwersji z typu podstawowego. Moe wykorzystywa dowolny typ. Popatrzmy na to: class CComplex { private: // cz rzeczywista i urojona float m_fRe; float m_fIm; public:
366
Zaawansowane C++
// zwyky konstruktor (ktry jest rwnie domylny // oraz konwertujcy z float do CComplex) CComplex(float fRe = 0, float fIm = 0) : m_fRe(fRe), m_fIm(fIm) { } // konstruktor konwertujcy z CRational do CComplex CComplex(const CRational& Wymierna) : m_fRe(Wymierna.Licznik() / (float) Wymierna.Mianownik()), m_fIm(0) { } // ------------------------------------------------------------// metody dostpowe float Re() const void Re(float fRe) float Im() const void Im(float fIm) { { { { return m_fRe; } m_fRe = fRe; } return m_fIm; } m_fIm = fIm; }
};
Klasa CComplex posiada zdefiniowane konstruktory konwertujce zarwno z float, jak i CRational. Poza tym, e odpowiada to oczywistemu faktowi, i liczby rzeczywiste i wymierne s take zespolone, pozwala to na napisanie takiej funkcji: CComplex Dodaj(const CComplex& Liczba1, const CComplex& Liczba2) { return CComplex(Liczba1.Re() + Liczba2.Re(), Liczba2.Im() + Liczba2.Im()); } oraz wywoywanie jej zarwno z parametrami typu CComplex, jaki CRational i float: CComplex Wynik; Wynik = Dodaj(CComplex(1, 5), 4); Wynik = Dodaj(CRational(10, 3), CRational(1, 3)); Wynik = Dodaj(1, 2); // itd. Mona zapyta: Czy konstruktor konwertujcy z float do CComplex jest konieczny? Przecie jest ju jeden, z float do CRational, i drugi - z CRational do CComplex. Oba robi w sumie to, co trzeba! Tak, to byaby prawda. W sumie jednak jest to bardzo gboko ukryte. Istot niejawnych konwersji jest wanie to, e s niejawne: programista nie musi si o nie martwi. Z drugiej strony oznacza to, e pewien kod jest wykonywany za plecami kodera. Przy jednej niedosownej zamianie nie jest to raczej problemem, ale przy wikszej ich liczbie trudno byoby zorientowa si, co tak naprawd jest zamieniane w co. Oprcz tego jest jeszcze bardziej prozaiczny powd: gdyby pozwala na wielokrotne konwersje, kompilator musiaby sprawdza mnstwo potencjalnych drg konwersji. Znacznie wyduyoby to czas kompilacji. Nie jest wic dziwne, e: Kompilator C++ dokonuje zawsze co najwyej jednej niejawnej konwersji zdefiniowanej przez programist. Nie jest przy tym wane, czy do konwersji stosujemy konstruktory czy te operatory konwersji, ktre poznamy w nastpnym akapicie.
Zaawansowana obiektowo
367
Swko explicit
Dowiedzielimy si, e kady jednoargumentowy konstruktor definiuje konwersj typu swojego parametru do typu klasy konstruktora. W ten sposb moemy okrela, jak kompilator ma zamieni jaki typ (na przykad wbudowany lub inn klas) w typ naszych obiektw. atwo przeoczy fakt, e t drog jednoargumentowy konstruktor (ktry jest w sumie konstruktorem jak kady inny) nabiera nowego znaczenia. Ju nie tylko inicjalizuje obiekt swej klasy, ale i podaje sposb konwersji. Dotd mwilimy, e to dobrze. Nie zawsze jednak tak jest. Czasem piszemy w klasie jednoparametrowy konstruktor wcale nie po to, aby ustali jakkolwiek konwersj. Nierzadko bowiem tego wymaga logika naszej klasy. Spjrzmy chociaby na konstruktor z CIntArray: CIntArray(unsigned uRozmiar) : m_uRozmiar(uRozmiar); m_pnTablica(new int [m_uRozmiar])
{ }
Przyjmuje on parametr typu int - rozmiar tablicy. Niestety (tak, niestety!) jest tutaj take konstruktorem konwertujcym z typu int na typ CIntArray. Z tego powodu zupenie poprawne staje si bezsensowne przypisanie104 w rodzaju: CIntArray aTablica; aTablica = 10; // Oj! Tworzymy 10-elementow tablic!
W powyszym kodzie tworzona jest tablica o odpowiedniej liczbie elementw i przypisywana zmiennej Tablica. Na pewno nie moemy na to pozwoli - takie przypisanie to przecie ewidentny bd, ktry powinien zosta wykryty przez kompilator. Jednak musimy mu o tym powiedzie i w tym celu posugujemy si swkiem explicit (jawny): explicit CIntArray(unsigned uRozmiar) : m_uRozmiar(uRozmiar); m_pnTablica(new int [m_uRozmiar])
{ }
Gdy opatrzymy nim deklaracj konstruktora jednoargumentowanego, bdzie to znakiem, i nie chcemy, aby wykonywa on niejawn konwersj. Po zastosowaniu tego manewru sporny kod nie bdzie si ju kompilowa. I bardzo dobrze. Jeeli potrzebujesz konstruktora jednoparametrowego, ktry bdzie dziaa wycznie jako zwyky (a nie te jako konwertujcy), umie w jego deklaracji sowo kluczowe explicit. Jak wiemy konstruktor konwertujcy moe mie wicej argumentw, jeli ma te parametry opcjonalne. Do takich konstruktorw rwnie mona stosowa explicit, jeli jest to konieczne.
Operatory konwersji
Teraz poznamy drugi sposb konwersji typw - funkcje (operatory) konwertujce.
104
A take podobna do niego inicjalizacja oraz kade uycie liczby int w miejsce tablicy CIntArray.
368
Stwarzamy sobie problem
Zaawansowane C++
Zostawmy wysz matematyk liczb zespolonych w klasie CComplex i zajmijmy si klas CRational. Jak wiemy, reprezentowane przez ni liczby wymierne s take liczbami rzeczywistymi. Byoby zatem dobrze, abymy mogli przekazywa je w tych miejscach, gdzie wymagane s liczby zmiennoprzecinkowe, np.: float abs(float x); float sqrt(float x); // itd. Niestety, nie jest to moliwe. Obecnie musimy sami dzieli licznik przez mianownik, aby otrzyma liczb typu float z typu CRational. Dlaczego jednak kompilator nie miaby tutaj pomc? Zdefiniujmy niejawn konwersj z typu CRational do float! W tym momencie napotkamy powany problem. Konwersja do typu CRational bya jak najbardziej moliwa poprzez konstruktor, natomiast zamiana z typu CRational na float nie moe by ju tak zrealizowana. Nie moemy przecie doda konstruktora konwertujcego do klasy float, bo jest to elementarny typ podstawowy. Zreszt, nawet jeli nasz docelowy typ byby klas, to nie zawsze byoby to moliwe. Konieczna byaby bowiem modyfikacja definicji tej klasy, a to jest moliwe tylko dla naszych wasnych klas. Tak wic konstruktory konwertujce na niewiele nam si zdadz. Potrzebujemy innego sposobu
Zaawansowana obiektowo
369
Wszystko zaley wic od tego, ktry z typw - rdowy, docelowy - jest klas, do ktrej definicji mamy dostp: jeeli jestemy w posiadaniu definicji klasy docelowej, to moemy zastosowa konstruktor konwertujcy jeli mamy dostp do klasy rdowej, moliwe jest zastosowanie operatora konwersji W przypadku gdy oba warunki s spenione (tzn. chcemy wykona konwersj z wasnorcznie napisanej klasy do innej wasnej klasy), wybr sposobu jest w duej mierze dowolny. Trzeba jednak pamita, e: konstruktory nie s dziedziczone, wic w jeli chcemy napisac konwersj typu do klasy pochodnej, potrzebujemy osobnego konstruktora w tej klasie konstruktory nie s metodami wirtualnymi, w przeciwiestwie do operatorw konwersji argument konstruktora konwertujcego musi mie typ cile dopasowany do zadeklarowanego W sumie wic wnioski z tego s takie (czytaj: przechodzimy do sedna :D): chcc wykona konwersj typu podstawowego (lub klasy bibliotecznej) do typu wasnej klasy, stosujemy konstruktor konwertujcy chcc dokona konwersji typu wasnej klasy do typu podstawowego (lub klasy bibliotecznej), wykorzystujemy operator konwersji definiujc konwersj midzy dwoma wasnymi klasami moemy wybra, kierujc si innymi przesankami, jak np. wpywem dziedziczenia na konwersje czy nawet kolejnoci definicji obu klas w pliku nagwkowym *** Zbiorem dobrych rad odnonie stosowania rnych typw konwersji zakoczylimy omawianie zaawansowanych aspektw konstruktorw w C++.
370
Zaawansowane C++
Przecianie operatorw
W tym podrozdziale przyjrzymy si unikalnej dla C++, a jednoczenie wspaniaej technice przeciania operatorw. To jedno z najwikszych osigni tego jzyka w zakresie uatwiania programowania i uczynienia go przyjemniejszym. Zanim jednak poznamy t cudowno, czas na krtk dygresj :) Jak ju wielokrotnie wspomniaem, C++ jest czonkiem bardzo licznej dzisiaj rodziny jzykw obiektowych. Takie jzyki charakteryzuje moliwo tworzenia wasnych typw danych - klas zawierajcych w sobie (kapsukujcych) pewne dane (pola) oraz pewne dziaania (metody). Na tym polega OOP. aden jzyk programowania nie moe si jednak oby bez mniej lub bardziej rozbudowanego wachlarza typw podstawowych. W C++ mamy ich mnstwo, z czego wikszo jest spadkiem po jego poprzedniku, jzyku C. Z jednej strony mamy wic typy wbudowane (w C++: int, float, unsigned, itd.), a drugiej typy definiowane przez uytkownika (struktury, klasy, unie). W jakim stopniu s one do siebie podobne? Pomylisz: Gupie pytanie! One przecie wcale nie s do siebie podobne. Typw podstawowych uywamy przeciez inaczej ni klas, i na odwrt. Nie ma mowy o jakim wikszym podobiestwie - moe poza tym, e dla wszystkich typw moemy deklarowa zmienne i parametry funkcji No i moe jeszcze wystpuj podobne konwersje Jeeli faktycznie tak pomylae, to nie bdziesz zdziwiony, e twrcy wielu jzykw obiektowych take przyjli tak strategi. W jzykach Java, Object Pascal (Delphi), Visual Basic, PHP i jeszcze wielu innych, typy definiowane przez uytkownika (klasy) s jakby wydzielon czci jzyka. Maj niewiele punktw wsplnych z typami wbudowanymi, poza tymi naprawd niezbdnymi, ktre sam wyliczye. Jednak wcale nie musi tak by i C++ jest tego najlepszym przykadem. Autorzy tego jzyka (z Bjarne Stroustrupem na czele) dyli bowiem do tego, aby definiowane przez programist typy byy funkcjonalnie jak najbardziej zblione do typw wbudowanych. Ju sam fakt, e moemy tworzy obiekty na dwa sposoby - jak normalne zmienne oraz poprzez new - dobrze o tym wiadczy. Moliwo zdefiniowania konstruktorw kopiujcych i konwersji wiadczy o tym jeszcze bardziej. Ale ukoronowaniem tych wysikw jest obecno w C++ mechanizmu przeciania operatorw. Czy wic jest ten wspaniay mechanizm? Przecianie operatorw (ang. operator overloading), zwane te ich przeadowaniem, polega na nadawaniu operatorom nowych znacze - tak, aby mogy by one wykorzystane w stosunku do obiektw zdefiniowanych klas. Polega to wic na napisaniu takiego kodu, ktry sprawi, e wyraenia w rodzaju: a = b + c a /= d if (b == c) { /* ... */ } bd poprawne nie tylko wtedy, gdy a, b, c i d bd zmiennymi, nalecymi do typw wbudowanych. Po przecieniu operatorw (tutaj: +, =, /= i ==) dla okrelonych klas bdzie mona pisa takie wyraenia: zawierajce operatory i obiekty naszych klas. W ten sposb zdefiniowane przez nas klasy nie bd si rniy praktycznie niczym od typw wbudowanych.
Zaawansowana obiektowo
Dlaczego to jest takie cudowne? By si o tym przekona, przypomnijmy sobie zdefiniowan ongi klas liczb wymiernych - CRational. Napisalimy sobie wtedy funkcj, ktra zajmowaa si ich mnoeniem. Uywalimy jej w ten sposb: CRational Liczba1(1, 2), Liczba2(5, 1), Wynik; // 1/2, czyli p :) // 5 // zmienna na wynik
371
Wynik = Pomnoz(Liczba1, Liczba2); Nie wygldao to zachwycajco, szczeglnie jeli uwiadomimy sobie, e dla typw wbudowanych ostatnia linijka mogaby prezentowa si tak: Wynik = Liczba1 * Liczba2; Nie do, e krcej, to jeszcze adniej Czemu my tak nie moemy?! Ale tak, wanie moemy! Przecianie operatorw pozwala nam na to! Znajc t technik, moemy zdefiniowac nowe znaczenie dla operator mnoenia, czyli *. Nauczymy go pracy z liczbami wymiernymi - obiektami naszej klasy CRational - i od tego momentu pokazane wyej mnoenie bdzie dla nich poprawne! Co wicej, bdzie dziaao zgodnie z naszymi oczekiwaniami: tak, jak funkcja Pomnoz(). Czy to nie pikne? Na takie wspaniaoci pozwala nam przecianie operatorw. Na co wic jeszcze czekamy - zobaczmy, jak to si robi! Hola, nie tak prdko! Jak sama nazwa wskazuje, technika ta dotyczy operatorw, a dokadniej: wyposaania ich w nowe znaczenia. Zanim si za to zabierzemy, warto byoby zna przedmiot naszych manipulacji. Powinnimy zatem przyjrze si operatorom w C++: ich rodzajom, wbudowanej funkcjonalnoci oraz innym waciwociom. I to wanie zrobimy najpierw. Tylko nie narzekaj :P
Cechy operatorw
Obok sw kluczowych i typw, operatory s podstawowymi elementami kadego jzyka programowania wysokiego poziomu. Przypomnijmy sobie, czym jest operator. Operator to jeden lub kilka znakw (zazwyczaj niebdcych literami), ktre maj specjalne znaczenie w jzyku programowania. Dotychczas uywalimy bardzo wielu operatorw - niemal wszystkich, jakie wystpuj w C++ - ale dotd nie zajlimy si nimi caociowo. Poznae wprawdzie takie pojcia jak operatory unarne, binarne, priorytety, jednak teraz bdzie zasadne ich powtrzenie. Zbierzmy wic tutaj wszystkie cechy operatorw wystpujcych w C++.
Liczba argumentw
Operator sam w sobie nie moe wykonywa adnej czynnoci (to rni go od funkcji), gdy potrzebuje jakich parametrw. W tym przypadku mwimy zwykle o argumentach operatora - operandach. Operatory dziel si z grubsza na dwie due grupy, jeeli chodzi o liczb swoich argumentw. S to operatory jedno- i dwuargumentowe. W C++ mamy jeszcze operator warunkowy ?:, uznawany za ternarny (trjargumentowy), ale jest on wyjtkiem, ktrym nie naley zaprzta sobie gowy.
372
Zaawansowane C++
Operatory jednoargumentowe
Te operatory fachowo nazywa si unarnymi (ang. unary operators). Stanowi one cakiem liczn rodzin, ktra charakteryzuje si jednym: kady jej czonek wymaga do dziaania jednego argumentu. Std nazwa tego rodzaju operatorw. Najbardziej znanym operatorem unarnym (nawet dla tych, ktrzy nie maj pojcia o programowaniu!) jest zwyky minus. Formalnie nazywa si go operatorem negacji albo zmiany znaku, a dziaa on w ten sposb, e zmienia jaka liczb na liczb do niej przeciwn: int nA = 5; int nB = -nA; // nB ma warto -5 (a nA nadal 5)
Podobnie dziaaj operatory ! i ~, z tym e operuj one (odpowiednio): na wyraeniach logicznych i na cigach bitw. Istniej te operatory jednoargumentowane o zupenie innej funkcjonalnoci; wszystkie je przypomnimy sobie w nastpnej sekcji.
Operatory dwuargumentowe
Jak sama nazwa wskazuje, te operatory przyjmuj po dwa argumenty. Nazywamy je binarnymi (ang. binary operators). Nie ma to nic wsplnego z binarn reprezentacj danych, lecz po prostu z iloci operandw. Typowymi operatorami dwuargumentowymi s operatory arytmetyczne, czyli popularne znaki dziaa: int nA = 8, nB = -2, nC; nC = nA + nB; nC = nA - nB; nC = nA * nB; nC = nA / nB; // // // // 6 10 -16 -4
Mamy te operatory logiczne oraz bitowe, Warto wspomnie (o czym bdziemy jeszcze bardzo szeroko mwi), e przypisanie (=) to take operator dwuargumentowy, do specyficzny zreszt.
Priorytet
Operatory mog wystpowa w zoonych wyraeniach, a ich argumenty mog pokrywa si. Oto prosty przykad: int nA = nB * 4 + 18 / nC - nD % 3; Zapewne wiesz, e w takiej sytuacji kompilator kieruje si priorytetami operatorw (ang. operators precedence), aby rozstrzygn problem. Owe priorytety to nic innego, jak swoista kolejno dziaa. Rni si ona od tej znanej z matematyki tylko tym, e w C++ mamy take inne operatory ni arytmetyczne. Dla znakw +, -, *, /, % priorytety s aczkolwiek dokadnie takie, jakich nauczylimy si w szkole. Wyraenia zawierajce te operatory moemy wic pisa bez pomocy nawiasw. Jeeli jednak s one skomplikowane, albo uywamy w nich take innych rodzajw operatorw, wwczas konieczne naley pomaga sobie nawiasami. Lepiej przecie postawi po kilka znakw wicej ni co chwila siga do stosownej tabelki pierwszestwa.
Zaawansowana obiektowo
373
czno
Gdy w wyraeniu pojawi si obok siebie kilka operatorw tego samego rodzaju, maj one oczywicie ten sam priorytet. Trzeba jednak nadal rozstrzygn, w jakiej kolejnoci dziaania bd wykonywane. Tutaj pomaga czno operatorw (ang. operators associativity). Okrela ona, od ktrej strony bd obliczane wyraenia (lub ich fragmenty) z ssiedztwem operatorw o tych samych priorytetach. Mamy dwa rodzaje cznoci: czno lewostronna (ang. left-to-right associativity), ktra rozpoczyna obliczenia od lewej strony i wykorzystuje czstkowe wyniki jako lewostronne argumenty dla kolejnych operatorw czno prawostronna (ang. right-to-left associativity) - tutaj obliczenia s wykonywane, poczynajc od prawej strony. Czciowe wyniki s nastpnie uywane jako prawostronne argumenty kolejnych operatorw Najlepiej zilustrowa to na przykadzie. Jeeli mamy takie oto wyraenie: nA + nB + nC + nD + nE + nF + nG + nH to oczywicie priorytety wszystkich operatorw s te same. Zaczyna dominowa czno, ktra w przypadku operatorw arytmetycznych (oraz im podobnych, jak bitowe, logiczne i relacyjne) jest lewostronna. To naturalne, po przecie takie obliczenia rwnie przeprowadzalibymy od lewej do prawej. Kompilator bdzie wic oblicza powysze wyraenie w ten sposb: ((((((nA + nB) + nC) + nD) + nE) + nF) + nG) + nH Zauwamy, e akurat w przypadku plusa czno nie ma znaczenia, bo dodawanie jest przecie przemienne. Gdyby jednak chodzio o odejmowanie czy dzielenie, wwczas byoby to bardzo wane. czno prawostronna dotyczy na przykad operatora przypisania: nA = nB = nC = nD = nE = nF Innymi sowy, powysze wyraenie zostanie potraktowane tak: nA = (nB = (nC = (nD = (nE = nF)))) Oznacza to, e kompilator wykona najpierw skrajnie prawe przypisanie, a zwrcon przez to wyraenie warto (rwn wartoci przypisywanej) wykorzysta w kolejnym przypisaniu, i tak dalej. W sumie wic wszystkie zmienne bd potem rwne zmiennej nF.
Operatory w C++
Jzyk C++ posiada cae multum rnych operatorw. Pod tym wzgldem jest chyba rekordzist wrd wszystkich jzykw programowania. wiadczy to zarwno o jego wielkich moliwociach, jak i sporej elastycznoci. Co ciekawe, dotd praktycznie nie ma jednoznacznej definicji operatora w tym jzyku, a w wielu rdach mona znale nieco rnice si midzy sob zestawy operatorw. S to jednak gwnie niuanse, ktrych rozstrzyganie dla przecitnego programisty nie jest wcale istotne.
374
Zaawansowane C++
W tej sekcji powtrzymy sobie i uzupenimy wiadomoci na temat wszystkich operatorw C++ - przynajmniej tych, co do ktrych nie ma wtpliwoci, e faktycznie s operatorami. Podzielimy je sobie na kilka kategorii.
Operatory arytmetyczne
Ju na samym pocztku zetknlimy si z operatorami arytmetycznymi. Nic dziwnego, to przecie najprostszy i najbardziej naturalny rodzaj operatorw. Znaj go wszyscy absolwenci przedszkola.
Inkrementacja i dekrementacja
Specyficzne dla C++ s operatory inkrementacji i dekrementacji. W odrnieniu od wikszoci operatorw, modyfikuj one swj argument. Dokadniej mwic, dodaj one (inkrementacja) lub odejmuj (dekrementacja) jedynk do/od swego operandu. Operatorem inkrementacji jest ++, za dekrementacji --. Oto przykad: int nX = 9; ++nX; --nX; // teraz nX == 10 // teraz znowu nX == 9
Powyszy kod mona te zapisa jako: nX++; nY++; Jeeli ignorujemy warto zwracan przez te operatory, to uycie ktrejkolwiek wersji (zwanej, jak wiesz, prein/dekrementacj oraz postin/dekrementacj) nie sprawa rnicy - przynajmniej dla typw podstawowych. Gdy natomiast zapisujemy gdzie zwracan warto, to powinnimy pamita o rnicy midzy znaczeniem operatorw w obu miejscach (na pocztku i na kocu zmiennej). Mwilimy ju o tym, ale przypomn jeszcze raz: Prein/dekrementacja zwraca warto ju zwikszon (zmniejszon) o 1. Postin/dekrementacja zwraca oryginaln warto. Wariant postfiksowy jest generalnie bardziej kosztowny, poniewa wymaga przygotowania tymczasowego obiektu, w ktrym zostanie zachowana pierwotna warto w celu jej pniejszego zwrotu. Dla typw podstawowych to kwestia kilku bajtw, ale dla klas zdefiniowanych przez uytkownika (ktre mog przecia oba operatory - czym si rzecz jasna zajmiemy za momencik) moe to by spora rnica.
Zaawansowana obiektowo
375
Operatory bitowe
Przedstawione wyej operatory arytmetyczne dziaaj na liczbach na zasadach, do jakich przyzwyczaia nas matematyka. Nie ma w tym przypadku znaczenia, e operacje przeprowadzane s na komputerze. Nie ma te znaczenia wewntrzna reprezentacja liczb. Jak wiemy, komputery przechowuj dane w postaci cigw zer i jedynek, zwanych bitami. Pojedyncze bity mog przechowywa tylko elementarn informacj - 0 (bit ustawiony) lub 1 (bit nieustawiony). Aby przedstawia bardziej zoone dane - choby liczby - naley bity czy ze sob. Powstaj w ten sposb wektory bitowe, cigi bitw (ang. bitsets) lub sowa (ang. words). S po prostu sekwencje zer i jedynek. Do operacji na wektorach bitw C++ posiada sze operatorw. Obecnie nie s one tak czsto uywane jak na przykad w czasach C, ale nadal s bardzo przydatne. Omwi je tu pokrtce. O wiele obszerniejsze omwienie tych operatorw, wraz z zastosowaniami, znajdziesz w Dodatku C, Manipulacje bitami.
Operacje logiczno-bitowe
Cztery operatory: ~, &, | i ^ wykonuj na bitach operacje zblione do logicznych, gdzie bit ustawiony (1) odgrywa rol wyraenia prawdziwego, za nieustawiony (0) faszywego. Oto te operatory: negacja bitowa (operator ~) zmienia w caym cigu (zwykle liczbie) wszystkie bity na przeciwne. Ustawione zmieniaj si na nieustawione i odwrotnie koniunkcja bitowa (operator &) porwnuje ze sob odpowiadajce bity dwch sw: tam, gdzie napotka na dwie jedynki, wypisuje do wyniku take jedynk; w przeciwnym wypadku zero
376
Zaawansowane C++
alternatywa bitowa (operator |) rwnie dziaa na dwch sowach. Porwnujc ich kolejne bity, zwraca w bicie wyniku zero, jeeli stwierdzi w operandach dwa nieustawione bity oraz jedynk w przeciwnym wypadku bitowa rnica symetryczna (operator ^) porwnuje bity sw i zwraca 1, jeeli s rne i 0, gdy s sobie rwne
Operator ~ jest jednoargumentowy (unarny), za pozostae dwa s binarne - i wcale nie dlatego, e pracuj w systemie dwjkowym :)
Przesunicie bitowe
Mamy te dwa operatory przesunicia bitowego (ang. bitwise shift). Jest to: przesunicie w lewo (operator <<). Przesuwa on bity w lew stron sowa o podan liczb miejsc przesunicie w prawo (operator >>) dziaa analogicznie, tylko e przesuwa bity w praw stron sowa Z obu operatorw korzystamy podobnie, tj. w ten sposb: sowo << ile_miejsc sowo >> ile_miejsc Oto kilka przykadw - dla uproszczenia z liczbami zapisanymi binarnie (niestety, w C++ nie mona tego zrobi):
00010010 << 3 1111000 >> 4 00111100 << 5 // 10010000 // 00001111 // 10000000
Jak wida, bity ktre wyjedaj w wyniku przesunicia poza granic sowa s tracone. Pustki s natomiast wypeniane zerami.
Operatory strumieniowe
Czytajc ten akapit na pewno pomylae: Jakie operatory bitowe?! Przecie to s strzaki, ktrych uywamy razem ze strumieniami wejcia i wyjcia! Tak, to rwnie prawda - ale to tylko jedna jej strona. Faktem jest, e << i >> to przede wszystkim operatory przesunicia bitowego. Nie przeszkadza to jednak, aby miay one take inne znaczenie - co wicej, maj je one tylko w odniesieniu do strumieni. W sumie wic peni one w C++ a dwie funkcje. Czy domylasz si, dlaczego? Ale tak, wanie tak - operatory te zostay przecione przez twrcw Biblioteki Standardowej C++. Posiadaj one dodatkow funkcjonalno, ktra pozwala na ich uywanie razem z obiektami cout i cin105. W odniesieniu do samych liczb nadal jednak s one operatorami przesunicia bitowego. Nieco wicej informacji o tych operatorach otrzymasz przy okazji omawiania strumieni STL. Tam te nauczysz si przecia je dla swoich wasnych klas - tak, aby ich obiekty mona byo zapisywa do strumieni i odczytywa z nich w identyczny sposb, jak typy wbudowane.
Operatory porwnania
Bardzo wanym rodzaje operatorw s operatory porwnania, czyli znaki: < (mniejszy), > (wikszy), <= (mniejszy lub rwny), >= (wikszy lub rwny), == (rwny) oraz != (rny).
105 Rwnie clog, cerr oraz wszystkimi innymi obiektami, wywodzcymi si od klas istream i ostream oraz ich pochodnych. Po wicej informacji odsyam do rozdziau o strumieniach Biblioteki Standardowej.
Zaawansowana obiektowo
377
O tych operatorach wiemy w zasadzie wszystko, bo uywamy ich nieustannie. O tym, jak dziaaj, powiedzielimy sobie zreszt bardzo wczenie. Zwrc jeszcze tylko uwag, aby nie myli operatora rwnoci (==) z operatorem przypisania (=). Omykowe uycie tego drugiego w miejsce pierwszego nie zostanie bowiem oprotestowane przez kompilator (co najwyej wygeneruje on ostrzeenie). Dlaczego tak jest - wyjani przy okazji operatrw przypisania.
Operatory logiczne
Te operatory su do czenia wyrae logicznych (true lub false) w zoone warunki. Takie warunki moemy potem wykorzysta z instrukcjach if oraz ptlach, co zreszt niejednokrotnie robilimy. W C++ mamy trzy operatory logiczne, bdce odpowiednikami pewnych operatorw bitowych. Rnica polega jednak na tym, e operatory logiczne dziaaj na wartociach liczb (lub wyrae logicznych: faszywe oznacza 0, za prawdziwe - 1) za bitowe - na wartociach bitw. Oto te trzy operatory: negacja (zaprzeczenie, operator !) powoduje zamian prawdy (1) na fasz (0) koniunkcja (iloczyn logiczny, operator &&) dwch wyrae zwraca prawd tylko wwczas, gdy oba jej argumenty s prawdziwe alternatywa (suma logiczna, operator ||) jest prawdziwa, gdy cho jeden z jej argumentw jest prawdziwy (rny od zera) Warto zapamita, e w wyraeniach zawierajcych operatory && i || wykonywanych jest tylko tyle oblicze, ile jest koniecznych do zdeterminowania wartoci warunkowych. Przykadowo, w poniszym kodzie: int nZmienna; std::cin >> nZmienna; if (nZmienna >= 1 && nZmienna <= 10)
{ /* ... */ }
jeeli stwierdzona zostanie falszywo pierwszej czci koniunkcji (nZmienna >= 1), to druga nie bdzie ju sprawdzana i cay warunek uznany zostanie za faszywy. Podobnie dzieje si przy alternatywie, ktrej pierwszy argument jest prawdziwy - wwczas cae wyraenie rwnie reprezentuje prawd. Argumenty operatorw logicznych s wic zawsze obliczane od lewej do prawej. Wrd operatorw nie ma rnicy symetrycznej, zwanej alternatyw wykluczajc (ang. XOR - eXclusive OR). Mona j jednak atwo uzyska, wykorzystujc tosamo:
a b (a b)
co w przeoeniu na C++ wyglda tak: if (!(a == b)) { /* ... */ } // a i b to wyraenia logiczne
Operatory przypisania
Kolejn grup stanowi operatory przypisania. C++ ma ich kilkanacie, cho wiemy, e tak naprawd tylko jeden jest do szczcia potrzebny. Pozostae stworzono dla wygody programisty, jak zreszt wiele mechanizmw w C++. Popatrzmy wic na operatory przypisania.
378
Zaawansowane C++
L-warto i r-warto
Zauwamy, e odwrotne przypisanie: 7 = nX; // le!
jest niepoprawne. Nie moemy nic przypisa do sidemki, bo ona nie zajmuje adnej komrki w pamici - w przeciwiestwie do zmiennej, jak np. nX. Zarwno 7, jak i nX, s jednak poprawnymi wyraeniami jzyka C++. Widzimy aczkolwiek, e rni si pod wzgldem wsppracy z przypisaniem. nX moe by celem przypisania, za 7 - nie. Mwimy, e nX jest l-wartoci, za 7 - r-wartoci lub p-wartoci. L-warto (ang. l-value) jest wyraeniem mogcym wystpi po lewej stronie operatora przypisania - std ich nazwa. R-warto (ang. r-value), po polsku zwana p-wartoci, moe wystpi tylko po prawej stronie operatora przypisania. Zauwamy, e nic nie stoi na przeszkodzie, aby nX pojawio si po prawej stronie operatora przypisania: int nY; nY = nX; Jest tak, poniewa: Kada l-warto jest jednoczenie r-wartoci (p-wartoci) - lecz nie odwrotnie! Domylasz si pewnie, e w C++ kade wyraenie jest r-wartoci, poniewa reprezentuje jakie dane. L-wartociami s natomiast te wyraenia, ktre: odpowiadaj komrkom pamici operacyjnej nie s oznaczone jako stae (const) Najbardziej typowymi rodzajami l-wartoci s wic: zmienne wszystkich typw niezadeklarowane jako const wskaniki do powyszych zmiennych, wobec ktrych stosujemy operator dereferencji, czyli gwiazdk (*) niestae referencje do tyche zmiennych elementy niestaych tablic niestae pola klas, struktur i unii, ktre podpadaj pod jeden z powyszych punktw i nie wystpuj w ciele staych metod106
106
Zaawansowana obiektowo
R-wartoci to oczywicie te, jak i wszystkie inne wyraenia.
379
Rezultat przypisania
Wyraeniem jest take samo przypisanie, gdy samo w sobie reprezentuje pewn warto: std::cout << (nX = 5); Ta linijka kodu wyprodukuje rezultat:
5
co pozwala nam uglni, i: Rezultatem przypisania jest przypisywana warto. Ten fakt powoduje, e w C++ moliwe s, niespotykane w innych jzykach, wielokrotne przypisania: nA = nB = nC = nD = nE; Poniewa operator(y) przypisania maj czno prawostronn, wic ten wiersz zostanie obliczony jako: nA = (nB = (nC = (nD = nE))); Innymi sowy, nE zostanie przypisane do nD. Nastpnie rezultat tego przypisania (czyli nE, bo to byo przypisywane) zostanie przypisany do nC. To take wyprodukuje rezultat i to ten sam, nE - ktry zostanie przypisany nB. To przypisanie rwnie zwrci ten sam wynik, ktry zostanie wreszcie umieszczony w nA. W ten wic sposb wszystkie zmienne bd miay ostatecznie t sam warto, co nE. T technik moemy wykona tyle przypisa naraz, ile tylko sobie yczymy.
380
Zaawansowane C++
bdziemy wiadomie wykonywa przypisania w podobnych sytuacjach, musimy pamita, e: Naley zwraca baczn uwag na kade przypisanie wystpujce w warunku instrukcji if lub ptli. Moe to by bowiem niedosze porwnanie. Zaleca si, aby opatrywa stosownym komentarzem kade zamierzone uycie przypisania w tych newralgicznych miejscach. Dziki temu unikniemy nieporozumie z kompilatorem, innymi programistami i samym sob!
Rozwinicie wziem w cudzysw, poniewa nie jest tak, e jaki mechanizm w rodzaju makrodefinicji zamienia te skrcone wyraenia do ich penych form. O nie, one s kompilowane w tej postaci. Ma to taki skutek, e wyraenie po lewej stronie operatora jest obliczane jeden raz. W wersji rozwinitej byoby natomiast obliczane dwa razy. Podobna zasada obowizuje te w operatorach pre/postin/dekrementacji. Jest to te realizacja bardziej fundamentalnej reguy, ktra mwi, e skadniki kadego wyraenia s obliczane tylko raz.
Operatory wskanikowe
Wskaniki byy ongi kluczow cech jzyka C, a i w C++ nie straciy wiele ze swojego znaczenia. Do ich obsugi mamy w naszym ulubionym jzyku trzy operatory.
Pobranie adresu
Jednoargumentowy operator & suy do pobrania adresu obiektu, przy ktrym stoi. Oto przykad: int nZmienna; int* pnWskaznik = &nZmienna; Argument tego operatora musi by l-wartoci. To raczej oczywiste, bo przecie musi ona rezydowa w jakim miejscu pamici. Inaczej niemoliwe byoby pobranie adresu tego miejsca. Typowo operandem dla & jest zmienna lub funkcja.
Zaawansowana obiektowo
381
Dereferencja
Najprostszym i najczciej stosowanym sposobem jest dereferencja: int nZmienna; int* pnWskaznik = &nZmienna; *pnWskaznik = 42; Odpowiada za ni jednoargumentowy operator *, zwany operatorem dereferencji lub adresowania poredniego. Pozwala on na dostp do miejsca w pamici, ktremu odpowiada wskanik. Operator ten wykorzystuje ponadto typ wskanika, co gwarantuje, e odczytana zostanie waciwa ilo bajtw. Dla int* bdzie to sizeof(int), zatem *pnWskaznik reprezetuje u nas liczb cakowit. To, czy *wskanik jest l-wartoci, czy nie, zaley od staoci wskanika. Jeeli jest to stay wskanik (const typ*), wwczas nie moemy modyfikowa pokazywanej przeze pamici. Mamy wic do czynienia z r-wartoci. W pozostaych przypadkach mamy lwarto.
Indeksowanie
Jeeli wskanik pokazuje na tablic, to moemy dosta si do jej kolejnych elementw za pomoc operatora indeksowania (ang. subscript operator) - nawiasw kwadratowych []. Oto zupenie banalny przykad: std::string aBajka[3]; aBajka[0] = "Dawno, dawno temu, ..."; aBajka[1] = "w odleglej galaktyce..."; aBajka[2] = "zylo sobie siedmiu kransoludkow..."; Jeeli zapytasz A gdzie tu wskanik?, to najpierw udam, e tego nie syszaem i pozwol ci na chwil zastanowienia. A jeli nadal bdziesz si upiera, e adnego wskanika tu nie ma, to bd zmuszony naoy na ciebie wyrok powtrnego przeczytania rozdziau o wskanikach. Chyba tego nie chcesz? ;-) Wskanikiem jest tu oczywicie aBajka - jaka nazwa tablicy wskazuje na jej pierwszy element. W zasadzie wic mona dokona jego dereferencji i dosta si do tego elementu: *aBajka = "Dawno, dawno temu, ..."; Przesuwajc wskanik przy pomocy dodawania mona te dosta si do pozostaej czci tablicy: *(aBajka + 1) = "w odleglej galaktyce..."; *(aBajka + 2) = "zylo sobie siedmiu kransoludkow..."; Taki zapis jest jednak do kopotliwy w interpretacji - cho koniecznie trzeba go zna (przydaje si przy iteratorach STL). C++ ma wygodniejszy sposb dostepu do elementw tablicy o danym indeksie - jest to wanie operator indeksowania.
382
Na koniec musz jeszcze przypomnie, e wyraenie: tablica[i] odpowiada (i-1)-emu elementowi tablicy. A to dlatego, e: W C++ elementy tablic (oraz acuchw znakw) liczymy od zera. Skoro ju tak si powtarzam, to przypomn jeszcze, e:
Zaawansowane C++
W n-elementowej tablicy nie istnieje element o indeksie n. Prba odwoania si do niego spowoduje bd ochrony pamici. Zasada ta nie dotyczy aczkolwiek acuchw znakw, gdzie n-ty element to zawsze znak o kodzie 0 ('\0'). Jest to zaszo zakonserwowana w czasach C, ktra przetrwaa do dzi.
Operatory pamici
Mamy w C++ kilka operatorw zajmujcych si pamici. Jedne su do jej alokacji, drugie do zwalniania, a jeszcze inne do pobierania rozmiaru typw i obiektw.
Alokacja pamici
Alokacja pamici to przydzielenie jej okrelonej iloci dla programu, by ten mg j wykorzysta do wasnych celw. Pozwala to dynamicznie tworzy zmienne i tablice.
new
new jest przeznaczony do dynamicznego tworzenia zmiennych. Obiekty stworzone przy pomocy tego operatora s tworzone na stercie, a nie na stosie, zatem nie znikaj po opuszczeniu swego zakresu. Tak naprawd to w ogle nie stosuje si do nich pojcie zasigu. Tworzenie obiektw poprzez new jest banalnie proste: float pfZmienna = new float; Oczywicie nie ma zbyt wielkiego sensu tworzenie zmiennych typw podstawowych czy nawet prostych klas. Jeeli jednak mamy do czynienia z duymi obiektami, ktre musz istnie przez duszy czas i by dostpne w wielu miejscach programu, wtedy musimy tworzy je dynamicznie poprzez new. W przypadku kreowania obiektw klas, new dba o prawidowe wywoanie konstrukturw, wic nie trzeba si tym martwi.
new[]
Wersj operatora new, ktra suy do alokowania tablic, nazywam new[], aby w ten sposb podkreli jej zwizek z delete[]. new[] potrafi alokowa tablice dynamiczne po podanym rozmiarze. Aby uy tej moliwoci po nazwie docelowego typu okrelamy wymiary podanej tablicy, np.: float** matMacierz4x4 = new float [4][4];
Zaawansowana obiektowo
383
W wyniku dostajemy odpowiedni wskanik lub ewentualnie wskanik do wskanika (do wskanika do wskanika itd. - zalenie od liczby wymiarw), ktry moemy zachowa w zmiennej okrelonego typu. Do powstaej tablicy odwoujemy si tak samo, jak do tablic statycznych: for (unsigned i = 0; i < 4; ++i) for (unsigned j = 0; j < 4; ++j) matMacierz4x4[i][j] = (i == j ? 1.0f : 0.0f); Dynamiczna tablica istnieje jednak na stercie, wic tak samo jak wszystkie obiekty tworzone w czasie dziaania programu nie podlega reguom zasigu.
Zwalnianie pamici
Pami zaalokowana przy pomocy new i new[] musi zosta zwolniona przy pomocy odpowiadajcych im operatorw delete i delete[]. Wiesz doskonale, e w przeciwnym razie dojdzie do gronego bdu wycieku pamici.
delete
Za pomoc delete niszczymy pami zaalokowan przez new. Dla operatora tego naley poda wskanik na tene blok pamici, np.: delete pfZmienna; delete zapewnia wywoanie destruktora klasy, jeeli takowy jest konieczny. Destruktor taki moe by wizany wczenie (jak zwyka metoda) lub pno (jak metoda wirtualna) ten drugi sposb jest zalecany, jeeli chcemy korzysta z dobrodziejstw polimorfizmu.
delete[]
Analogicznie, delete[] suy do zwalniania dynamicznych tablic. Nie musimy podawa rozmiaru takiej tablicy, gdy j niszczymy - wystarczy tylko wskanik: delete[] matMacierz4x4; Koniecznie pamitajmy, aby nie myli obu postaci operatora delete[] - w szczeglnoci nie mona stosowa delete do zwalniania pamici przydzielonej przez new[].
Operator sizeof
sizeof pozwala na pobranie rozmiaru obiektu lub typu: int nZmienna; if (sizeof(nZmienna) != sizeof(int)) std::cout << "Chyba mamy zepsuty kompilator :D"; Jest to operator czasu kompilacji, wic nie moe korzysta z informacji uzyskanych w czasie dziaania programu. W szczeglnoci, nie moe pobra rozmiaru dynamicznej tablicy - nawet mimo takich prob: int* pnTablica = new int [5]; std::cout << sizeof(pnTablica); std::cout << sizeof(*pnTablica); // to samo co sizeof(int*) // to samo co sizeof(int)
Taki rozmiar trzeba po prostu zapisa gdzie po alokacji tablicy. sizeof zwraca warto nalec do predefiniownego typu size_t. Zwykle jest to liczba bez znaku lub bardzo dua liczba ze znakiem.
384
Ciekawostka: operator __alignof
Zaawansowane C++
W Visual C++ istnieje jeszcze podobny do sizeof operator __alignof. Uywamy go w ten sam sposb, podajc mu zmienn lub typ. W wyniku zwraca on tzw. wyrwnanie (ang. alignment) danego typu danych. Jest to liczba, ktra okrela sposb organizacji pamici dla danego typu danych. Przykadowo, jeeli wyrwnywanie wynosi 8, to znaczy to, i obiekty tego typu s wyrwnane w pamici do wielokrotnoci omiu bajtw (ich adresy s wielokrotnoci omiu). Wyrwnanie sprawia rzecz jasna, e dane zajmuj w pamici nieco wicej miejsca ni faktycznie mogyby. Zyskujemy jednak szybciej, poniewa porcje pamici wyrwnane do cakowitych potg dwjki (a takie jest zawsze wyrwnanie) s przetwarzane szybciej. Wyrwnanie mona kontrolowa poprzez __declspec(align(liczba)). Np. ponisza struktura: __declspec(align(16)) struct FOO { int nA, nB; }; bdzie tworzy zmienne zajmujce w pamici fragmenty po 16 bajtw, cho jej faktyczny rozmiar jest dwa razy mniejszy107. Polecajc wyrwnywanie do 1 bajta okrelimy praktyczny jego brak: #define PACKED __declspec(align(1)) Typy danych opatrzone tak deklaracj bd wic ciasno upakowane w pamici. Moe to da pewn jej oszczdno, ale zazwyczaj spadek prdkoci dostpu do danych nie jest tego wart.
Operatory typw
Istniej jzyki programowania, ktre cakiem dobrze radz sobie bez posiadania cile zarysowanych typw danych. C++ do nich nie naley: w nim typ jest spraw bardzo wan, a do pracy z nim oddelegowano kilka specjalnych operatorw.
Operatory rzutowania
Rzutowanie jest zmian typu wartoci, czyli jej konwersj. Mamy par operatorw, ktre zajmuj si tym zadaniem i robi to w rny sposb. Wrd nich s tak zwane cztery nowe operatory, o skadni: okrelenie_cast<typ_docelowy>(wyraenie) To wanie one s zalecane do uywania we wszystkich sytuacjach, wymagajcych rzutowania. C++ zachowuje aczkolwiek take star form rzutowania, znan z C.
static_cast
Ten operator moe by wykorzystywany do wikszoci konwersji, jakie zdarza si przeprowadza w C++. Nie oznacza to jednak, e pozwala on na wszystko: Poprawno rzutowania static_cast jest sprawdzana w czasie kompilacji programu. static_cast mona uywa np. do: konwersji midzy typami numerycznymi rzutowania liczby na typ wyliczeniowy (enum)
107
Zaawansowana obiektowo
rzutowania wskanikw do klas zwizanych relacj dziedziczenia
385
Jeeli chodzi o ostatnie zastosowanie, to naley pamita, e tylko konwersja wskanika na obiekt klasy pochodnej do wskanika na obiekt klasy bazowej jest zawsze bezpieczna. W odwrotnym przypadku trzeba by pewnym co do wykonalnoci rzutowania, aby nie narobi sobie kopotw. Tak pewno mona uzyska na przykad za pomoc sposobu z metodami wirtualnymi, ktry zaprezentowaem w rozdziale 1.7, lub poprzez operator typeid. Inn moliwoci jest te uycie operatora dynamic_cast.
dynamic_cast
Przy pomocy dynamic_cast mona rzutowa wskaniki i referencje do obiektw w d hierarchii dziedziczenia. Oznacza to, e mona zamieni odwoanie do obiektu klasy bazowej na odwoanie do obiektu klasy pochodnej. Wyglda to np. tak: class CFoo class CBar : public CFoo { /* ... */ }; { /* ... */ };
Taka zamiana nie zawsze jest moliwa, bo przecie dany wskanik (referencja) niekoniecznie musi pokazywa na obiekt danej klasy pochodnej. Operacja jest jednak bezpieczna, poniewa: Poprawno rzutowania dynamic_cast jest sprawdzana w czasie dziaania programu. Wiemy doskonale, w jaki sposb pozna rezultat tego sprawdzania. dynamic_cast zwraca po prostu NULL (wskanik pusty, zero), jeeli rzutowanie nie mogo zosta wykonane. Naley to zawsze skontrolowa: if (!pBar) { // OK - pBar faktycznie pokazuje na obiekt klasy CBar } Dla skrcenia zapisu mona wykorzysta warto zwracan operatora przypisania: if (pBar = dynamic_cast<CBar*>(pFoo)) { // rzutowanie powiodo si } Znak = jest tu oczywicie zamierzony. Warunek bdzie mia bowiem warto rwn rezultatowi rzutowania, zatem bdzie prawdziwy tylko wtedy, gdy si ono powiedzie. Zwrcony wskanik bdzie wtedy rny od zera.
reinterpret_cast
reinterpret_cast moe suy do dowolnych konwersji midzy wskanikami, a take do rzutowania wskanikw na typy liczbowe i odwrotnie. Wachlarz moliwoci jest wic szeroki, niestety: Poprawno rzutowania reinterpret_cast nie jest sprawdzana.
386
Zaawansowane C++
atwo wic moe doj do niebezpiecznych konwersji. Ten operator powinien by uywany tylko jako ostatnia deska ratunku - jeeli inne zawiod, a my jestemy przekonani o wzgldnym bezpieczestwie planowanej zamiany. Wykorzystanie tego operatora generalnie jednak powinno by bardzo rzadkie. reintepret_cast moemy potencjalnie uy np. do uzyskania dostpu do pojedynczych bitw w zmiennej o wikszej ich iloci: unsigned __int32 u32Zmienna; unsigned __int8* pu8Bajty; // liczba 32-bitowa // wskanik na liczby 8-bitowe (bajty)
// zamieniamy wskanik do 4 bajtowej zmiennej na wskanik do // 4-elementowej tablicy bajtw pu8Bajty = reinterpret_cast<unsigned __int8*>(&u32Zmienna); // wywietlamy kolejne bajty zmiennej u32Zmienna for (unsigned i = 0; i < 4; ++i) std::cout << "Bajt nr " << i << ": " << pu8Bajty[i] << std::endl; Wida wic, e najlepiej sprawdza si w operacjach niskopoziomowych. Tutaj monaby oczywicie uy przesunicia bitowego, ale tablica wyglda z pewnoci przejrzyciej.
const_cast
Ostatni z nowych operatorw rzutowania ma do ograniczone zastosowanie: const_cast suy do usuwania przydomkw const i volatile z opatrzonych nimi wskanikw do zmiennych. Obecno tego operatora suy chyba tylko temu, aby moliwe byo cakowite zastpienie sposobw rzutowania znanych z C. Jego praktyczne uycie naley do sporadycznych sytuacji.
Rzutowanie w stylu C
C++ zachowuje stare sposoby rzutowania typw. Jednym z nich jest rzutowanie nazywane, cakiem adekwatnie, rzutowaniem w stylu C (ang. C-style cast): (typ) wyraenie Ta skadnia konwersji jest nadal czsto uywana, gdy jest po prostu krtsza. Naley jednak wiedzie, e nie odrnia ona rnych sposobw rzutowania i w zalenoci od typu i wyraenia moe si zachowywa jak static_cast, reinterpret_cast lub const_cast.
Rzutowanie funkcyjne
Inn skadni ma rzutowanie funkcyjne (ang. function-style cast): typ(wyraenie) Przypomina ona wywoanie funkcji, cho oczywicie adna funkcja nie jest tu wywoywana. Ten rodzaj rzutowania dziaa tak samo jak rzutowanie w stylu C, aczkolwiek nie mona w nim stosowa co niektrych nazw typw. Nie mona na przykad wykona: int*(&fZmienna)
Zaawansowana obiektowo
387
i to z do prozaicznego powodu. Po prostu gwiazdka i nawias otwierajcy wystpujce obok siebie zostan potraktowane jako bd skadniowy. W tej sytuacji mona sobie ewetualnie pomc odpowiednim typedefem.
Operator typeid
typeid suy pobrania informacji o typie podanego wyraenia podczas dziaania programu. Jest to tzw. RTTI, czyli informacja o typie czasu wykonania (ang. RunTime Type Information). Przygotowanie do wykorzystania tego operatora objemuje wczenie RTTI (co dla Visual C++ opisaem w rozdziae 1.7) oraz doczenie standardowego nagwka typeinfo: #include <typeinfo> Potem moemy ju stosowa typeid np. tak: class CFoo class CBar : public CFoo { /* ... */ }; { /* ... */ };
int nZmienna; CFoo* pFoo = new CBar; std::cout << typeid(nZmienna).name(); std::cout << typeid(pFoo).name(); std::cout << typeid(*pFoo).name();
Jak wida, operator ten jest leniwy i jeli tylko moe, bdzie korzysta z informacji dostpnych w czasie kompilacji programu. Aeby wic pozna np. typ polimorficznego obiektu, na ktry pokazujemy wskanikiem, trzeba uy derefrencji
Wyuskanie z obiektu
Majc zmienn obiektow, do jej skadnikw odwoujemy si poprzez operator kropki (.), np. tak: struct FOO FOO Foo; Foo.x = 10; W podobny dziaa operator .*, ktry suy aczkolwiek do wyowienia skadnika poprzez wskanik do niego: int FOO::*p2mnSkladnik = &FOO::x; Foo.*p2mnSkladnik = 42; Wskaniki na skadowe s przedmiotem nastpnego podrozdziau. { int x; };
388
Zaawansowane C++
Wyuskanie ze wskanika
Gdy mamy wskanik na obiekt, wwczas zamiast kropki uywamy innego operatora wyuskania - strzaki (->): FOO* pFoo = new FOO; pFoo->x = 16; Tutaj take mamy odpowiednik, sucy do wybierania skadowych za porednictwem wskanika na nie: pFoo->*p2mnSkladnik += 80; W powyszej linijce mamy dwa wskaniki, stojce po obydwu stronach operatora ->*. O pierwszym rodzaju powiedzielimy sobie na samym pocztku programowania obiektowego - to po prostu zwyczajny wskanik na obiekt. Drugi to natomiast wskanik do skadowej klasy - o tym typie wskanikw pisze wicej nastpny podrozdzia.
Operator zasigu
Ten operator, nazywany te operatorem rozwikania zakresu (ang. scope resolution operator) suy w C++ do rozrniania nazw, ktre rezyduj w rnych zakresach. Znamy dwa podstawowe zastosowania tego operatora: dostp do przesonitych zmiennych globalnych dostp do skadowych klasy Oglnie, operatora tego uywamy, aby dosta si do identyfikatora zagniedoneego wewntrz nazwanych zakresw: zakres_poziom1::[zakres_poziom2::[zakres_poziom3::[...]]]nazwa Nazwy zakresw odpowiadaj m.in. strukturom, klasom i uniom. Przykadowo, FOO z poprzedniego akapitu byo nazw zakresu - oprcz tego, rzecz jasna, take nazw struktury. Przy pomocy operatora :: mona odnie si do jej zawartoci. Zakresy mona te tworzy poprzez tzw. przestrzenie nazw (ang. namespaces). Jest to bardzo dobre narzdzie, suce organizacji kodu i zapobiegajce konfliktom oznacze. Opisuje je rozdzia Sztuka organizacji kodu. Do tej pory cay czas korzystalimy z pewnej szczeglnej przestrzeni nazw - std. Pamitasz doskonale, e przy niej take uywalimy operatora zakresu.
Pozostae operatory
Ostatnie trzy operatory trudno zakwalifikowa do jakiej konkretnej grupy, wic zebraem je tutaj.
Nawiasy okrge
Nawiasy () to do oczywisty operator. W C++ suy on gwnie do: grupowania wyrae w celu ich obliczania w pierwszej kolejnoci deklarowania funkcji i wskanikw na nie wywoywania funkcji rzutowania Brak nawiasw moe by przyczyn bdnego (innego ni przewidywane) obliczania wyrae, a take nieprawidowej interpretacji niektrych deklaracji (np. funkcji i wskanikw na nie). Obfite stawianie nawiasw jest szczeglnie wane w makrodefinicjach.
Zaawansowana obiektowo
Z kolei nadmiar nawiasw jeszcze nikomu nie zaszkodzi :)
389
Operator warunkowy
Operator ?: jest nazywamy ternarnym, czyli trjargumentowym. Jako jedyny bierze bowiem trzy dane: warunek ? wynik_dla_prawdy : wynik_dla_faszu Umiejtne uycie tego operatora skraca kod i pozwala unikn niepotrzebnych instrukcji if. Co ciekawe, moe on by take uyty w deklaracjach, np. pl w klasach. Wtedy jednak wszystkie jego operandy musz by staymi.
Przecinek
Przecinek (ang. comma) to operator o najniszym priorytecie. Oprcz tego, e oddziela on argumenty funkcji, moe te wystpowa samodzielnie, np.: (nX + 17, 26, rand() % 5, nY) W takim wyraeniu operandy s obliczane od lewej do prawej, natomiast wynikiem jest warto ostatniego wyraenia. Tutaj wic bdzie to nY. Przecinek przydaje si, gdy chcemy wykona pewn dodatkow czynno w trakcie wyliczania jakiej wartoci. Przykadowo, spjrzmy na tak ptl odczytujc znaki: char chZnak; while (chZnak = ReadChar(), chZnak != ' ') { // zrb co ze znakiem, ktry nie jest spacj } ReadChar() jest funkcj, ktra pobiera nastpny znak (np. z pliku). Sama ptla ma za wykonywa si a do napotkania spacji. Zanim jednak mona sprawdzi, czy dany znak jest spacj, trzeba go odczyta. Robimy to w warunku ptli, posugujc si przecinkiem. Bez niego trzebaby najprawdopodobniej zmieni ca ptl na do, co spowodowaoby konieczno powtrzenia kodu wywoujcego ReadChar(). Inne wyjcie to uycie ptli nieskoczonej. C++ pozwala jednak osign ten sam efekt na kilka sposobw, spord ktrych wybieramy ten najbardziej nam pasujcy.
Funkcje operatorowe
Pomylmy: co waciwie robi kompilator, gdy natrafi w wyraeniu na jaki operator? Czy tylko sobie znanymi sposobami oblicza on docelow warto, czy moe jednak jest w tym jaka zasada? Ot tak. Dziaanie operatora definiuje pewna funkcja, zwana funkcj operatorow (ang. operator function). Istnieje wiele takich funkcji, ktre s wbudowane w kompilator i dziaaj na typach podstawowych. Dodawanie, odejmowanie i inne predefiniowane dziaania na liczbach s dostpne bez adnych stara z naszej strony. Kiedy natomiast chcemy przeciy jaki operatory, to oznacza to konieczno napisania wasnej funkcji dla nich. Zwyczajnie, trzeba poda jej argumenty oraz warto zwracan i
390
Zaawansowane C++
wypeni kodem. Nie ma w tym adnej magii. Za chwil zreszt przekonasz si, jak to dziaa.
Przeadowywa moemy te i tylko te operatory. W wikszoci ksiek i kursw za chwil nastpiaby podobna (acz znacznie krtsza) lista operatorw, ktrych przecia nie mona. Z dowiadczenia wiem jednak, e rodzi to niewyobraaln iloc nieporozumie, spowodowan nieprecyzyjnym okreleniem, co jest operatorem, a co nie. Dlatego te nie podaj adnej takiej tabelki - zapamitaj po prostu, e przecia mona wycznie te operatory, ktre wymieniem wyej.
Zaawansowana obiektowo
391
Musz jednak poda kilka wyjanie odnonie tej tabelki: operatory: +, -, *, & mona przecia zarwno w wersji jedno-, jak i dwuargumentowej operatory inkrementacji (++) i dekrementacji (--) przeciamy oddzielnie dla wersji prefiksowej i postfiksowej przecienie new i delete powoduje take zdefiniowanie ich dziaania dla wersji tablicowych (new[] i delete[]) operatory () i [] to nawiasy: okrge (grupowanie wyrae) i kwadratowe (indeksowanie, wybr elementw tablicy) operatory -> i ->* maj predefiniowane dziaanie dla wskanikw na obiekty jego nie moemy zmieni. Moemy natomiast zdefiniowia ich dziaanie dla samych obiektw lub referencji do nich (domylnie takiego dziaania w ogle nie ma)
Pozostae sprawy
Warto jeszcze powiedzie o pewnych naturalnych sprawach: przynajmniej jeden argument przecianego operatora musi by innego typu ni wbudowane. To naturalne: operatory przeciamy na rzecz wasnych typw (klas), bo dziaania na typach podstawowych s wyaczn domen kompilatora. Nie wtrcamy si w nie funkcja operatorowa nie moe posiada parametrw domylnych przecienia nie kumuluj si, tzn. jeeli na przykad przeciymy operatory + oraz =, nie bdzie to oznaczao automatycznego zdefiniowania operatora +=. Kade nowe znaczenie dla operatora musimy poda sami
392
Zaawansowane C++
Na te dwa przypadki popatrzymy sobie, definiujc operator mnoenia (dwuargumentowy *) dla klasy CRational, znanej z poprzednich podrozdziaw. Chcemy sprawi, aby jej obiekty mona byo mnoy przez inne liczby wymierne, np. tak: CRational JednaPiata(1, 5), TrzyCzwarte(3, 4); CRational Wynik = JednaPiata * TrzyCzwarte; To bdzie spore udogodnienie, wic zobaczmy, jak mozna to zrobi.
Zaawansowana obiektowo
393
Problem przemiennoci
Nasz entuzjazm szybko moe jednak osabn. jeeli zechcemy wyprbowa przemienno tak zdefiniowanego mnoenia. Nie bdzie przeszkd dla dwch liczb wymiernych: CRational Wynik = TrzySiodme * DwieTrzecie; albo dla pary cakowita-wymierna kompilator zaprotestuje: CRational Calosc = 2 * Polowa; // bd!
Dlaczego tak si dzieje? Ponowny rzut oka na jawne wywoanie operator*() pomoe rozwika problem: TrzySiodme.operator*(DwieTrzecie) 2.operator*(Polowa) // OK // ???
Wyranie wida przyczyn. Dla dwjki nie mona wywoa funkcji operator*(), bo taka funkcja nie istnieje dla typu int - on przecie nie jest nawet klas. Nic wic dziwnego, e uycie operatora zdefiniowanego jako metoda nie powiedzie si. Zaraz - a co z niejawn konwersj? Dlaczego ona nie zadziaaa? Faktycznie, monaby przypuszcza, e konstruktor konwertujcy moe zamieni 2 na obiekt klasy CRational i uczyni wyraenie poprawnym: CRational(2).operator*(Polowa) To jest nieprawda. Powodem jest to, i: Niejawne konwersje nie dziaaj przy wyuskiwaniu skadnikw obiektu. Kompilator nie rozwinie wic problematycznego wyraenia do powyszej postaci i zgosi bd. // OK
394
Zaawansowane C++
W tej formie oba argumenty operatora s normalnymi parametrami funkcji operator*(). Ma wic ona teraz dwa wyrane parametry, wobec ktrych moe zaj niejawna konwersja. W tym przypadku 2 faktycznie bdzie wic interpretowane jako CRational(2), zatem mnoenie powiedzie si bez przeszkd. To spostrzeenie mona uoglni: Globalna funkcja operatorowa pozwala kompilatorowi na dokonywanie niejawnych konwersji wobec wszystkich argumentw operatora. Jest to prosty sposb na definiowanie przemiennych dziaa na obiektach rnych typw, midzy ktrymi istniej okrelenia konwersji.
};
Zaawansowana obiektowo
395
Nie jest to przypadek. Operatory przeciamy bowiem najczeciej dla tego typu klas, zwanych narzdziowymi. Wektory, macierze i inne przydatne obiekty matematyczne s wanie idealnymi kandydatami na klasy z przeadowanymi operatorami. Pokazane tu przecienia nie bd jednak tylko sztuk dla samej sztuki. Wspomniane obiekty bd nam bowiem niezbdne z programowaniu grafiki przy uyciu DirectX. A e przy okazji ilustruj t ciekaw technik programistyczn, jak jest przecianie operatorw, tym lepiej dla nas :) Spjrzmy zatem, jakie ciekawe operatory moemy przedefiniowa na potrzeby tego typu klas.
108
396
CVector2D operator+() const { return CVector2D(+m_fX, +m_fY); } CVector2D operator-() const { return CVector2D(-m_fY, -m_fY); }
Zaawansowane C++
};
Co do drugiego operatora, to chyba nie ma adnych wtpliwoci. Natomiast przeadowywanie plusa moe wydawa si wrcz mieszne. To jednak cakowicie uzasadniona praktyka: jeli operator ten dziaa dla typw wbudowanych, to powinien take funkcjononowa dla naszego wektora. Aczkolwiek tre metody operator+() to faktycznie przykad-analogia do operator-(): rozsdniej byoby po prostu zwrci *this (czyli kopi wektora) ni tworzy nowy obiekt. Obie metody umieszczamy bezporednio w definicji klasy, bo s one na tyle krtkie, eby zasugiwa na atrybut inline.
Inkrementacja i dekrementacja
To, co przed chwil powiedziaem o operatorach jednoargumentowych, nie stosuje si do operatorw inkrementacji (++) i dekrementacji (--). cile mwic, nie stosuje si w caoci. Mamy tu bowiem dwie odmienne kwestie. Pierwsz z nich jest to, i oba te operatory nie s ju tak grzeczne i nie pozostawiaj swojego argumentu w stanie nienaruszonym. Potrzebny jest im wic dostp do obiektu, ktry zezwalaby na jego modyfikacj. Trudno oczekiwa, aby wszystkie funkcje miay do tego prawo, zatem operator++() i operator--() powinny by co najmniej zaprzyjanione z klas. A najlepiej, eby byy po prostu jej metodami: klasa klasa::operator++(); // lub operator--()
Druga sprawa jest nieco innej natury. Wiemy bowiem, e inkrementacja i dekrementacja wystpuje w dwch wersjach: przedrostkowej i przyrostkowej. Z zaprezentowanej wyej skadni wynika jednak, e moemy przeadowa tylko jedn z nich. Czy tak? Bynajmniej. Powysza forma jest prototypem funkcji operatorowej dla preinkrementacji, czyli dla przedrostkowego wariantu operatora. Nie znaczy to jednak, e wersji postfiksowej nie mona przeciy. Przeciwnie, jest to jak najbardziej moliwe w ten oto sposb: klasa klasa::operator++(int); // lub operator--(int)
Nie jest on zbyt elegancki i ma wszelkie znamiona triku, ale na co trzeba byo si zdecydowa Dodatkowy argument typu int jest tu niczym innym, jak rodkiem do rozrnienia obu typw in/dekrementacji. Nie peni on poza tym adnej roli, a ju na pewno nie trzeba go podawa podczas stosowania postfiksowego operatora ++ (--). Jest on nadal jednoargumentowy, a dodatkowy parametr jest tylko mao satysfakcjonujcym wyjciem z sytuacji. W pocztakach C++ tego nie byo, gdy po prostu niemoliwe byo przecianie przyrostkowych operatorw inkrementacji (dekrementacji). Pniej jednak stao si to dopuszczalne - opucimy ju jednak zason milczenia na sposb, w jaki to zrealizowano. Tak samo jak w przypadku wszystkich operatorw zaleca si, aby zachowanie obu wersji ++ i -- byo spjne z typami podstawowymi. Jeli wic przeciamy prefiksowy operator++() lub (i) operator--(), to w wyniku powinien on zwraca obiekt ju po dokonaniu zaoonej operacji zwikszenia o 1. Dla spokoju sumienia lepiej te przeciy obie wersje tych operatorw. Nie jest to uciliwe, bo moemy korzysta z ju napisanych funkcji. Oto przykad dla CVector2D:
Zaawansowana obiektowo
397
// preinkrementacja CVector2D CVector2D::operator++() // postinkrementacja CVector2D CVector2D::operator++(int) { CVector2D vWynik = *this; ++(*this); return vWynik; }
// (dekrementacja przebiega analogicznie) Spostrzemy, e nic nie stoi na przeszkodzie, aby w postinkrementacji uy operatora preinkrementacji: ++(*this); Przy okazji mona dostrzec wyranie, dlaczego wariant prefiskowy jest wydajniejszy. W odmianie przyrostkowej trzeba przecie ponie koszt stworzenia tymczasowego obiektu, aby go potem zwrci jako rezultat.
398
Zaawansowane C++
Czy bdzie to trudne? Myl, e ani troch. Zacznijmy od dodawania i odejmowania: class CVector2D { // (pomijamy szczegy) // dodawanie friend CVector2D operator+(const CVector2D& vWektor1, const CVector2D& vWektor2) { return CVector2D(vWektor1.m_fX + vWektor2.m_fX, vWektor1.m_fY + vWektor2.m_fY); } }; // (analogicznie definiujemy odejmowanie: operator-())
Zastosowaem tu funkcj zaprzyjanion - przypominam przy okazji, e nie jest to metoda klasy CVector2D, cho pewnie na to wyglda. Umieszczenie jej wewntrz bloku klasy to po prostu zaakcentowanie faktu, e funkcja niejako naley do definicji wektora - nie tej stricte programistycznej, ale matematycznej. Oprcz tego pozwala nam to na zgrupowanie wszystkich funkcji zwizanych z wektorem w jednym miejscu, no i na czerpanie zalet wydajnociowych, bo przecie operator+() jest tu funkcj inline. Kolejny punkt programu to mnoenie i dzielenie przez liczb. Tutaj opaca si zdefiniowa je jako metody klasy: class CVector2D { // (pomijamy szczegy) public: // (tu te) // mnoenie wektor * liczba CVector2D operator*(float fLiczba) const { return CVector2D(m_fX * fLiczba, m_fY * fLiczba); } }; // (analogicznie definiujemy dzielenie: operator/())
Dlaczego? Ano dlatego, e pierwszy argument ma by naszym wektorem, zatem odpowiada nam fakt, i bdzie to this. Drugi operand deklarujemy jako liczb typu float. Ale chwileczk Przecie mnoenie jest przemienne! W naszej wersji operatora * liczba moe jednak sta tylko po prawej stronie! Ha, a nie mwiem! operator*() jako metoda jest niepoprawny - trzeba zdefiniowa go jako funkcj globaln! Hola, nie tak szybko. Faktycznie, powysza funkcja nie wystarczy, ale to nie znaczy, e mamy j od razu wyrzuca. Przy zastosowaniu funkcji globalnych musielibymy przecie take napisa ich dwie sztuki: CVector2D operator*(const CVector2D& vWektor, float fLiczba); CVector2D operator*(float fLiczba, const CVector2D& vWektor);
Zaawansowana obiektowo
W kadym wic przypadku jeden operator*() nie wystarczy109. Musimy doda jego kolejn wersj: class CVector2D { // (pomijamy szczegy)
399
};
// mnoenie liczba * wektor friend CVector2D operator*(float fLiczba, const CVector2D& vWektor) { return vWektor * fLiczba; }
Korzystamy w niej z uprzednio zdefiniowanej. Kwestia, czy naley poprzedni wersj operatora take zamieni na zwyk funkcj zaprzyjanion, jest otwarta. Jeeli razi ci niekonsekwencja (jeden wariant jako metoda, drugi jako zwyka funkcja), moesz to zrobi. Na koniec dokonamy trzeciej definicji operator*(). Tym razem jednak bdzie to operator mnoenia dwch wektorw - czyli iloczynu skalarnego (ang. dot product). Przypomnijmy, e takie dziaanie jest po prostu sum iloczynw odpowiadajcych sobie wsprzdnych wektora. Jego wynikiem jest wic pojedyncza liczba. Poniewa operator bdzie dziaa na dwch obiektach CVector2D, decyzja co do sposobu jego zapisania nie ma znaczenia. Aby pozosta w zgodzie z tym ustalonym dla operatorw dodawania i mnoenia, niech bdzie to funkcja zaprzyjaniona: class CVector2D { // (pomijamy szczegy) // iloczyn skalarny friend float operator*(const const { return vWektor1.m_fX * + vWektor1.m_fY } CVector2D& vWektor1, CVector2D& vWektor2) vWektor2.m_fX, * vWektor2.m_fY;
};
Operatory przypisania
Teraz porozmawiamy sobie o pewnym wyjtkowym operatorze. Jest on unikalny pod wieloma wzgldami; mowa o operatorze przypisania (ang. assignment operator) tudzie podstawienia. Do czsto nie potrzebujemy nawet jego wyranego zdefiniowania. Kompilator dla kadej klasy generuje bowiem taki operator, o domylnym dziaaniu. Taki automatyczny operator dokonuje przypisania skadnik po skadniku - tak wic po jego zastosowaniu przypisywane obiekty s sobie rwne na poziomie wartoci pl110. Taka sytuacja nam czsto odpowiada - przykadowo, dla naszej klasy CVector2D bdzie to idealne rozwizanie. Niekiedy jednak nie jest to dobre wyjcie - za chwil zobaczymy, dlaczego. Powiedzmy jeszcze tylko, e domylny operator przypisania nie jest tworzony przez kompilator, jeeli klasa:
109 Pomijam tu zupenie fakt, e za chwil funkcj t zdefiniujemy po raz trzeci - tym razem jako iloczyn skalarny dwch wektorw. 110 W tym kopiowanie pole po polu wykorzystywane s aczkolwiek indywidualne operatory przypisania od klas, ktre instancjujemy w postaci pl. Nie zawsze wic obiekty takie faktycznie s sobie doskonale rwne.
400
Zaawansowane C++
ma skadnik bdcy sta (const typ) lub staym wskanikiem (typ* const) posiada skadnik bdcy referencj istnieje prywatny (private) operator przypisania: w klasie bazowej w klasie, ktrej obiekt jest skadnikiem naszej klasy
Nawet jeli aden z powyszych punktw nie dotyczy naszej klasy, domylne dziaanie operatora przypisania moe nam nie odpowiada. Wtedy naley go zdefiniowa samemu w ten oto sposb: klasa& klasa::operator=(const klasa&); Jest to najczstsza forma wystpowania tego operatora, umoliwiajca kontrol przypisywania obiektw tego samego typu co macierzysta klasa. Moliwe jest aczkolwiek przypisywanie dowolnego typu - czasami jest to przydatne. Jest jednak co, na co musimy zwrci uwag w pierwszej kolejnoci: Operatory przypisania (zarwno prosty, jak i te zoone) musz by zdefiniowane jako niestatyczna funkcja skadowa klasy, na ktrej pracuj. Wida to z zaprezentowanej deklaracji. Nie wida z niej jednak, e: Przeciony operator przypisania nie jest dziedziczony. Dlaczego - o tym mwiem przy okazji wprowadzania samego dziedziczenia. OK, wystarczy tej teorii. Czas zobaczy definiowanie tego opratora w praktyce. Wspomniaem ju, e dla klasy CVector2D w zupenoci wystarczy operator tworzony przez kompilator. Mamy jednak inn klas, dla ktrej jest to wrcz niedopuszczalne rozwizanie. To CIntArray, nasza tablica liczb. Dlaczego nie moemy skorzysta dla z niej z przypisania skadnik po skadniku? Z bardzo prostego powodu: spowoduje to przecie skopiowanie wskanikw na tablice, a nie samych tablic. Zauwamy, e z tego samego powodu napisalimy dla CIntArray konstruktor kopiujcy. To nie przypadek. Jeeli klasa musi mie konstruktor kopiujcy, to najprawdopodobniej potrzebuje take wasnego operatora przypisania (i na odwrt). Zajmijmy si wic napisaniem tego operatora. Aby to uczyni, pomylmy, co powinno si sta w takim przypisaniu: CIntArray aTablica1(7), aTablica2(8); aTablica1 = aTablica2; Po jego dokonaniu obie tablice musza zawiera te same elementy, lecz jednoczenie by niezalene - modyfikacja jednej nie moe pociga za sob zmiany zawartoci drugiej. Operator przypisania musi wic: zniszczy tablic w obiekcie aTablica1 zaalokowa w tym obiekcie tyle pamici, aby pomieci zawarto aTablica2 skopiowa j tam Te trzy kroki s charakterystyczne dla wikszoci implementacji operatora przypisania. Dziel one kod funkcji operatorowej na dwie czci: cz destruktorow, odpowiedzialn za zniszczenie zawartoci obiektu, ktry jest celem przypisania
Zaawansowana obiektowo
cz konstruktorow, zajmujc si kopiowaniem Nie mona jednak ograniczy go do prostego wywoania destruktora, a potem konstruktora kopiujcego - choby z tego wzgldu, e tego drugiego nie da si tak po prostu wywoa.
401
Dobrze, teraz to ju naprawd zaczniemy co kodowa :) Napiszemy operator przypisania dla klasy CIntArray: CIntArray& CIntArray::operator=(const CIntArray& aTablica) { // usuwamy nasz tablic delete[] m_pnTablica; // alokujemy tyle pamici, aby pomieci przypisywan tablic m_uRozmiar = aTablica.m_uRozmiar; m_pnTablica = new int [m_uRozmiar]; // kopiujemy tablic memcpy (m_pnTablica, aTablica.m_pnTablica, m_uRozmiar * sizeof(int)); // zwracamy wynik return *this;
Nie jest on chyba niespodziank - mamy tu wszystko, o czym mwilimy wczeniej. Tak wic na pocztku zwalniamy tablic w obiekcie, bdcym celem przypisania. Pniej alokujemy now - na tyle du, aby zmieci przypisywany obiekt. Wreszcie dokonujemy kopiowania. I pewnie jeszcze tylko jedna sprawa zaprzta twoj uwag: dlaczego funkcja zwraca w wyniku *this? Nie jest trudno odpowiedzie na to pytanie. Po prostu realizujemy tutaj konwencj znan z typw podstawowych, mwic o rezultacie przypisania, Pozwala to te na dokonywanie wielokrotnych przypisa, np. takich: CIntArray aTablica1(4), aTablica2(5), aTablica3(6); aTablica1 = aTablica2 = aTablica3; Powyszy kod bedzie dziaa identycznie, jak dla typw podstawowych. Wszystkie tablice stan si wic kopiami obiektu aTablica3. Aby to osign, wystarczy trzyma si prostej zasady: Operator przypisania powinien zwraca referencj do *this. Wydawaoby si, e teraz wszystko jest ju absolutnie w porzdku, jeeli chodzi o przypisywanie obiektw klasy CIntArray. Niestety, znowu zawodzi nas czujno. Popatrzmy na taki oto kod: CIntArray aTablica; aTablica = aTablica; // co si stanie z tablic?
By moe przypisywanie obiektu do niego samego jest dziwne, ale jednak kompilator dopuszcza je dla typw podstawowych, gdy jest dla nich nieszkodliwe. Nie mona tego samego powiedzie o naszej klasie i jej operatorze przypisania. Wywoanie funkcji operator=() spowoduje bowiem usunicie wewntrznej tablicy w obu obiektach (bo s one przecie jednym i tym samym bytem), a nastpnie prb
402
Zaawansowane C++
skopiowania tej usunitej tablicy do nowej! Bdziemy mogli mwi o szczciu, jeli spowoduje to tylko bd access violation i awaryjne zakoczenie programu Przed tak ewentualnoci musimy si wic zabezpieczy. Nie jest to trudne i ogranicza si do prostego sprawdzenia, czy nie mamy do czynienia z przypisywaniem obiektu do jego samego. Robimy to tak: klasa& klasa::operator=(const klasa& obiekt) { if (&obiekt == this) return *this; // (reszta instrukcji) } return *this;
albo tak: klasa& klasa::operator=(const klasa& obiekt) { if (&obiekt != this) { // (reszta instrukcji) } } return *this;
W instrukcji if porwnujemy wskaniki: adres przypisywanego obiektu oraz this. W ten wyapujemy ich ewentualn identyczno i zapobiegamy katastrofie.
Operator indeksowania
Skoro jestemy ju przy naszej tablicy, warto zaj si operatorem o wybitnie tablicowym charakterze. Mwi oczywicie o nawiasach kwadratowych [], czyli operatorze indeksowania (ang. subscript operator). Operator ten definiujemy zwykle w taki oto sposb: typ_wartoci& klasa::operator[](typ_klucza); Znowu widzimy, e jest to metoda klasy i po raz kolejny nie jest to przypadkiem: Operator indeksowania musi by zdefiniowany jako niestatyczna metoda klasy. To ju drugi operator, ktrego dotyczy taki wymg. Podpada pod niego jeszcze nastpna dwjka, ktrej przecianie omwimy za chwil. Najpierw zajmijmy si operatorem indeksowania. Przede wszystkim chciaby pewnie wiedzie, jak on dziaa. Nie jest to trudne; jeeli przeciymy ten operator, to wyraenie w formie: obiekt[klucz] zostanie przez kompilator zinterpretowane jako wywoanie w postaci: obiekt.operator[](klucz)
Zaawansowana obiektowo
403
Do funkcji operatorowej poprzez parametr trafia wic klucz, czyli warto, jak podajemy w nawiasach kwadratowych. Co ciekawe, nie musi to by wcale warto typu int, ani nawet warto liczbowa - rwnie dobrze sprawdza si tu cakiem dowolny typ danych, nawet napisy. Pozwala to tworzy klasy tzw. tablic asocjacyjnych, znanych na przykad z jzyka PHP111. Poniewa wspomniaem ju o tablicach, zajmijmy si t, ktra sami kiedy napisalimy i cigle udoskonalamy. Nie da si ukry, e CIntArray wiele zyska na przecieniu operatora []. Jeeli zrobimy to umiejtnie, bdzie mona uywac go tak samo, jak czynimy to w stosunku do zwykych tablic jzyka C++. Aby jednak to zrobi, musimy zwrci uwag na pewien szczeglny fakt. W stosunku do typw wbudowanych operator [] jest mianowicie bardzo elastyczny: w szczeglnoci pozwala on zarwno na odczyt, jak i modyfikacj elementw tablicy: int aTablica[10] aTablica[7] = 100; std::cout << aTablica[7]; // zapis // odczyt
Wyraenie z operatorem [] moe sta zarwno po lewej, jak i po prawej stronie znaku przypisania. T cech wypadaoby zachowa we wasnej jego wersji - znaczy to, e: Operator indeksowania powinien w wyniku zwraca l-warto. Gwarantuje to, e jego uycie bdzie zgodne z tym dla typw podstawowych. Zaakcentowaem ten wymg, piszc w skadni operatora referencj jako typ zwracanej wartoci. To wanie spowoduje podane zachowanie. Jeeli nie moemy sobie pozwoli sobie na zwracanie l-wartoci, to powinnimy raczej cakowicie zrezygnowa z przeadowania operatora [] i poprzesta na metodach dostpowych - takich jak Pobierz() i Ustaw() w klasie CIntArray. Zabierzmy si teraz do pracy: napiszemy przecion wersj operatora indeksowania dla klasy CIntArray. Dziki temu bdziemy mogli manipulowa elementami tablicy w taki sam sposb, jaki znamy dla normalnych tablic. To bdzie cakiem spory krok naprzd. Osignicie tego nie jest przy tym trudne - wrcz przeciwnie, u nas bdzie niezwykle proste: int& CIntArray::operator[](unsigned uIndeks) { return m_pnTablica[uIndeks]; } To wszystko! Zwrcenie referencji do elementu w prawidziwej, wewntrznej tablicy pozwoli na niczym nieskrpowany dostp do jej zawartoci. Teraz moemy w wygodny sposb odczytywa i zapisywa liczby w naszej tablicy: CIntArray aTablica(4); aTablica[0] aTablica[1] aTablica[2] aTablica[3] = = = = 1; 4; 9; 16;
for (unsigned i = 0; i < aTablica.Rozmiar(); ++i) std::cout << aTablica[i] << ", ";
111 Zazwyczaj lepszym rozwizaniem jest skorzystanie z mapy STL, czyli klasy std::map. Omwimy j, kiedy przejdziemy do opisu klas pojemnikowych Biblioteki Standardowej.
404
Zaawansowane C++
Obecnie jest ju ona funkcjonalnie identyczna z tablic typu int[]. Moemy jednak zacz czerpa take pewne korzyci z napisania tej klasy. Skoro juz przeciamy operator [], to zadbajmy, aby wykonywa po drodze jak poyteczn czynno - na przykad sprawdza poprawno danego indeksu: int& CIntArray::operator[](unsigned uIndeks) { return m_pnTablica[uIndeks < m_uRozmiar ? uIndeks : m_uRozmiar-1]; } Przy takiej wersji funkcji nie grozi nam ju bd przekroczenia zakresu (ang. subscript out of range). W razie podania nieprawidowego numeru elementu, funkcja zwrci po prostu odwoanie do ostatniej liczby w tablicy. Nie jest to najlepsze rozwizanie, ale przynajmniej zabezpiecza przed bdem czasu wykonania. Znacznie lepszym wyjciem jest rzucenie wyjtku, ktry poinformuje wywoujcego o zainstaniaym problemie. O wyjtkach porozmawiamy sobie w nastpnym rozdziale.
Operatory wyuskania
C++ pozwala na przeadowanie dwch operatorw wyuskania: -> oraz ->*. Nie jest to czsta praktyka, a jeli nawet jest stosowana, to przecianiu podlega zwykle tylko pierwszy z tych operatorw. Moesz wic pomin ten akapit, jeeli nie wydaje ci si konieczna znajomo sposobu przeadowywania operatorw wyuskania.
Operator ->
Operator -> kojarzy nam si z wybieraniem skadnika poprzez wskanik do obiektu. Wyglda to np. tak: CFoo* pFoo = new CFoo; pFoo->Metoda(); delete pFoo; Jeeli jednak sprbowalimy uy tego operatora w stosunku do samego obiektu (lub referencji do niego): CFoo Foo; Foo->Metoda(); // !!!
to bez wtpienia otrzymalibymy komunikat o bdzie. Domylnie nie jest bowiem moliwe uycie operatora -> w stosunku do samych obiektw. Jest on aplikowalny tylko do wskanikw. Ale w C++ nawet ta elazna moe zosta nagita. Moliwe jest bowiem nadanie operatorowi -> znaczenia i dopuszczenie do jego uywania razem ze zmiennymi obiektowymi. Aby to uczyni, trzeba oczywicie przeciy ten operator. Czynimy to tak oto funkcj: jaka_klasa* klasa::operator->(); Nie wyglda ona na skomplikowan ale znowu jest to metoda klasy! Tak wic: Operator wyuskania -> musi by niestatyczn funkcj skadow klasy. Powiedzmy sobie teraz, jak on dziaa. Nie jest przecie wcale takie oczywiste - choby z tego wzgldu, e z niewiadomych na razie powodw operator zadowala si zaledwie jednym argumentem (Jest on rzecz jasna przekazywany poprzez wskanik this)
Zaawansowana obiektowo
A oto i odpowied. Kiedy przeciymy operator ->, wyraenie w formie: obiekt->skadnik zostanie zmienione na: (obiekt.operator->())->skadnik
405
Mamy tu ju jawne wywoanie operator->(), ale nadal pojawia si strzaka w swej normalnej postaci. Ot jest to konieczne; w tym kodzie -> stojcy tu przy skadniku jest ju bowiem zwykym operatorem wyuskania ->. Zwykym - to znaczy takim, ktry oczekuje wskanika po swojej lewej stronie - a nie obiektu, jak operator przeciony. Wynika z tego wyraenie: obiekt.operator->() musi reprezentowa wskanik, aby cao dziaaa poprawnie. Dlatego te funkcja operator->() zwraca w wyniku typ wskanikowy. Jednoczenie nie interesuje si ona tym, co stoi po prawej stronie strzaki - to jest ju bowiem spraw tego normalnego, wbudowanego w kompilator operatora ->. Podsumowujc, mona powiedzie, e: Funkcja operator->() dokonuje raczej zamiany obiektu na wskanik ni faktycznego przedefiniowania znaczenia operatora ->. Godne uwagi jest to, e wskanik zwracany przez t funkcje wcale nie musi by wskanikiem na obiekt jej macierzystej klasy. Moe to by wskanik na dowoln klas, co zreszt obrazuje skadnia funkcji. Zastanawiasz si pewnie: A po co mi przecianie tego operatora? Moe po to, aby do skadnikw obiektu odnosi si nie tylko kropk (.), ale i strzak (->)? Odradzam przecianie operatora w tym celu, bo to raczej ukryje bdy w kodzie ni uatwi programowanie. Operator -> moemy jednak przeciy i bdzie to przydatne przy pisaniu klas tzw. inteligentnych wskanikw. Inteligentny wskanik (ang. smart pointer) to klasa bdca opakowaniem dla normalnych wskanikw i zapewniajca wobec nich dodatkowe, inteligentne zachowanie. Rodzajw tych inteligentnych zachowa jest doprawdy mnstwo. Moe to by kontrola odwoa do wskanika - zarwno w prostej formie zliczania, jak i zaawansowanej komunikacji z mechanizmem zajmujcym si usuwaniem nieuywanych obiektw (odmiecaczem, ang. garbage collector). Innym zastosowaniem moe by ochrona przed wyciekami pamici spowodowanymi nagym opuszczeniem zakresu. My napiszemy sobie najprostsz wersj takiego wskanika. Bdzie on przechowywa odwoanie do obiektu CFoo, ktre przekaemy mu w konstruktorze, i zwalnia je w swoim destruktorze. Oto kod klasy wskanika: class CFooSmartPtr { private: // opakowywany, waciwy wskanik CFoo* m_pWskaznik;
406
Zaawansowane C++
public: // konstruktor i destruktor CFooSmartPtr(CFoo* pFoo) : m_pWskaznik(pFoo) { } ~CFooSmartPtr() { if (m_pWskaznik) delete m_pWskaznik; } // ------------------------------------------------------------// operator dereferencji CFoo& operator*() { return *m_pWskaznik; } // operator wyuskania CFoo* operator->() { return m_pWskaznik }
};
Ta klasa jest ubosz wersj std::auto_ptr z Biblioteki Standardowej. Suy ona do bezpiecznego obchodzenia si z pamici w sytuacjach zwizanych z wyjtkami. Omwimy j sobie w nastpnym rozdziale (wrcimy tam zreszt take i do powyszej klasy). Co nam daje taki wskanik? Jeeli go uyjemy, to zapobiegnie on wyciekowi pamici, ktry moe zosta spowodowany przez nage opuszczenie zakresu (np. w wyniku wyjtku - patrz nastpny rozdzia). Jednoczenie nie umniejszamy sobie w aden sposb wygody kodowania - nadal moemy korzysta ze skadni, do ktrej si przyzwyczailimy: CFooSmartPtr pFoo = new CFoo; // wywoanie metody na dwa sposoby pFoo->Metoda(); // naprawd: (pFoo.operator->())->Metoda() (*pFoo).Metoda(); // naprawd: (pFoo.operator*()).Metoda() Prosz tylko nie sdzi, e odtd powinnimy uywa tylko takich sprytnych wskanikw. O nie, one nie s panaceum na wszystko i maj cakiem konkretne zastosowania. Nie naley ich traktowac jako zoty rodek - szczeglnie jako rodek przeciwko zapomnialskiemu niezwalnianiu zaalokowanej pamici.
Zaawansowana obiektowo
W pierwszym przypadku skadnia przecienia wyglda mniej wicej tak: typ_pola& klasa::operator->*(typ_pola klasa::*); typ_pola& operator->*(klasa&, typ_pola klasa::*); Jest chyba do logiczne, e typ docelowego pola oraz typ zwracany przez funkcj operatorow musi si zgadza. Do podobnie jest dla metod:
407
zwracany_typ klasa::operator->*(zwracany_typ (klasa::*)([parametry])); zwracany_typ operator->*(klasa&, zwracany_typ (klasa::*)([parametry])); Tutaj funkcja musi zwraca ten sam typ, co metoda klasy, na ktrej wskanik przyjmujemy. Jak wyglda przecianie w praktyce? Spjrzmy na przykad na tak oto klas: class CFoo { public: int nPole1, nPole2; // ------------------------------------------------------------// operator ->* int& operator->*(int CFoo::*) { return nPole1; }
};
Po takim redefiniowaniu operatora, wszystkie wskaniki na skadowe typu int w klasie CFoo bd prowadziy tylko i wycznie do pola nPole1.
408
Zaawansowane C++
Oznacza to rwnie, e moliwe jest zdefiniowanie wielu wersji przecionego operatora (). Musz one jednak by rozrnialne w tym sam sposb, jak przeadowane funkcje. Powinny wic posiada inn liczb, kolejno i/lub typy parametrw. Do czego moe nam przyda si taka potga i elastyczno? Moliwoci jest bardzo wiele, moe do nich nalee np. wybr elementu tablicy wielowymiarowej. Do ciekawszych zastosowa naley jednak tworzenie tzw. obiektw funkcyjnych (ang. function objects) - funktorw. Funktory s to obiekty przypominajce zwyke funkcje, jednak rni si tym, i mog posiada stan. Maj go, poniewa w rzeczywistoci s to klasy, ktre zawieraj jakie publiczne pola, za skadni wywoania funkcji uzyskuj za pomoc przecienia operatora (). Oto prosty przykad - funktor obliczajcy redni arytmetyczn z podanych liczb i aktualizujcy wynik z kadym kolejnym wywoaniem: class CAverageFunctor { private: // aktualny wynik double m_fSrednia; // ilo wywoa unsigned m_uIloscLiczb; public: // konstruktor CAverageFunctor() : m_fSrednia(0.0), m_uIloscLiczb(0) { } // ------------------------------------------------------------// funkcja resetujca stan funktora void Reset() { m_fSrednia = m_uIloscLiczb = 0; } // ------------------------------------------------------------// operator wywoania funkcji - oblicza redni double operator()(double fLiczba) { // liczymy now redni, uwzgldniajc dodan liczb // oraz aktualizujemy zmienn przechowuj ilo liczb // wszystko w jednym wyraeniu - za to kochamy C++ ;D m_fSrednia = ((m_fSrednia * m_uIloscLiczb) + fLiczba) / m_uIloscLiczb++); // zwracamy now redni return m_fSrednia;
};
Uycie tego obiektu wyglda tak: CAverageFunctor Srednia; Srednia(4); Srednia(18.5); Srednia(-6); Srednia(42); Srednia.Reset(); Srednia(56); // // // // // rednia z 4 rednia z 4 i 18.5 rednia z 4, 18.5 i -6 rednia z 4, 18.5, -6 i 42 zresetowanie funktora, warto przepada
// rednia z 56
Zaawansowana obiektowo
Srednia(90); Srednia(4 * atan(1)); std::cout << Srednia(13); // rednia z 56 i 90 // rednia z 56, 90 i pi // wywietlenie redniej z 56, 90, pi i 13
409
Naturalnie, matematycy zapaliby si za gow widzc taki algorytm obliczania redniej. Bardzo skutecznie prowadzi on bowiem to kumulowania bdw zwizanych z niedokadnym zapisem liczb w komputerze. Jest to jednak cakiem dobra ilustracja koncepcji funkctora. W Bibliotece Standardowej mamy cakiem sporo klas funktorw, z ktrymi bdziesz mg si wkrtce zapozna.
410
Zaawansowane C++
Z kolei funkcja dla operatora delete potrzebuje tylko parametru, bdcego wskanikiem. Jest to rzecz jasna wskanik do obszaru pamici, ktry ma by zwolniony. W zamian funkcja zwraca void, czyli nic. Oczywiste. Mniej oczywista jest opcjonalna fraza klasa::. Owszem, sugeruje ona, e obie funkcje mog by metodami klasy lub funkcjami globalnymi. W przeciwiestwie do pozostaych operatorw ma to jednak znaczenie: new i delete jako metody maj bowiem inne znaczenie ni new i delete - funkcje globalne. Mamy mianowicie moliwo lokalnego przecienia obydwu operatorw, jak rwnie zdefiniowania ich nowych, globalnych wersji. Omwimy sobie oba te przypadki.
// delete void operator delete(void* pWskaznik) { // informacja std::cout << "Zwalniamy wskaznik " << pWskaznik; // usuwamy pami ::delete pWskaznik;
};
Kiedy teraz sprbujemy stworzy dynamicznie obiekt klasy CFoo: CFoo* pFoo = new CFoo; to odbdzie si to z jednoczesnym powiadomieniem o tym fakcie przy pomocy strumienia wyjcia. Analogicznie bdzie w przypadku usunicia: delete pFoo;
Zaawansowana obiektowo
Nadal jednak moemy skorzysta z normalnych wersji new i delete - wystarczy poprzedzi ich nazwy operatorem zakresu: CFoo* pFoo = ::new CFoo; // ... ::delete pFoo;
411
Tak te robimy w ciele naszych funkcji operatorowych. Mamy dziki temu pewno, e wywoujemy standardowe operatory i nie wpadamy w puapk nieskoczonej rekurencji. W przypadku lokalnych operatorw nie jest to bynajmniej konieczne, ale warto tak czyni dla zaznaczenia faktu korzystania z wbudowanych ich wersji.
Globalna redefinicja
new i delete moemy te przeadowa w sposb caociowy i globalny. Zastpimy w ten sposb wbudowane sposoby alokacji pamici dla kadego uycia tych operatorw. Wyjtkiem bdzie tylko jawne poprzedzenie ich operatorem zakresu, ::. Jak dokona takiego fundamentalnego przecienia? Bardzo podobnie, jak to robilimy w trybie lokalnym. Tym razem nasze funkcje operator new() i operator delete() bda po prostu funkcjami globalnymi: // new void* operator new(size_t cbRozmiar) { // informacja na konsoli std::cout << "Alokujemy " << cbRozmiar << " bajtow"; // alokujemy pami i zwracamy wskanik return ::new char [cbRozmiar];
// delete void operator delete(void* pWskaznik) { // informacja std::cout << "Zwalniamy wskaznik " << pWskaznik; // usuwamy pami ::delete pWskaznik;
Ponownie peni one u nas wycznie funkcj monitorujc, ale to oczywicie nie jest jedyna moliwo. Wszystko zaley od potrzeb i fantazji. Koniecznie zwrmy jeszcze uwag na sposb, w jaki w tych przecianych funkcjach odwoujemy si do oryginalnych operatorw new i delete. Uywamy ich w formie ::new i ::delete, aby omykowo nie uy wasnych wersji ktre przecie wanie piszemy! Gdybymy tak nie robili, spowodowaoby to wpadnicie w niekoczcy si cig wywoa rekurencyjnych. Pamitajmy zatem, e: Jeli w treci przecionych, globalnych operatorw new i delete musimy skorzysta z ich standardowej wersji, koniecznie naley uy formy ::new i ::delete. Z domylnych wersji operatorw pamici moemy te korzysta wiadomie nawet po ich przecieniu: int* pnZmienna1 = new int; int* pnZmienna2 = ::new int; // przeciaona wersja // oryginalna wersja
412
Zaawansowane C++
Naturalnie, trzeba wtedy zdawa sobie spraw z tego przecienia i na wasne yczenie uy operatora ::. To gwarantuje nam, e nikt inny, jak tylko kompilator bdzie zajmowa si zarzdzaniem pamici. Nie wpadajmy jednak w paranoj. Jeeli korzystamy z kodu, w ktrym zaimplementowano inny sposb nadzorowania pamici, to nie naley bez wyranego powodu z niego rezygnowa. W kocu po to kto (moe ty?) pisa w mechanizm, eby by on wykorzystywany w praktyce, a nie z premedytacj omijany. Cay czas mniej lub bardziej subtelnie sugeruj, e operatory new i delete naley przecia razem. Nie jest to jednak formalny wymg jzyka C++ i jego kompilatorw. Zwykle jednak tak wanie trzeba czyni, aby wszystko dziaao poprawnie - zwaszcza, jeli stosujemy inny ni domylny sposb alokacji pamici.
Operatory konwersji
Na koniec przypomn jeszcze o pewnym mechanizmie, ktry w zasadzie nie zalicza si do operatorw, ale uywa podobnej skadni i dlatego take nazywamy go operatorami. Rzecz jasna s to operatory konwersji. Skadnia takich operatorw to po prostu: klasa::operator typ(); Jak doskonale pamitamy, celem funkcji tego typu jest zmiana obiektu klasy do danego typu. Przy jej pomocy kompilator moe dokonywa niejawnych konwersji. Innym (lecz nie zawsze stosowalnym) sposobem na osignicie podobnych efektw jest konstruktor konwertujcy. O obu tych drogach mwilimy sobie wczeniej.
agodnie mwic: nie jest to zbyt oczywiste, prawda? Pamitaj zatem, eby symbole operatorw odpowiaday ich naturalnym znaczeniom, a nie tworzyy uciliwe dla programisty rebusy.
Zaawansowana obiektowo
413
414
Zaawansowane C++
W nastpnym podrozdziale, dla odmiany, zapoznamy si ze znacznie mniej przydatn technik ;)) Chodzi o wskaniki do skadnikw klasy. Mimo tej mao zachcajcej zapowiedzi, zapraszam do przeczytania tego podrozdziau.
Wskanik na obiekt
To ju znamy. Wiemy te, e moemy tworzy take wskaniki do obiektw swoich wasnych klas: class CFoo { public: int nSkladnik; }; CFoo Foo; CFoo* pFoo = &Foo; Przy pomocy takich wskanikw moemy odnosi si do skadnikw obiektu. W tym przypadku moemy na przykad zmodyfikowa pole nSkladnik:
Zaawansowana obiektowo
pFoo->nSkladnik = 76; Sprawi to rzecz jasna, e zmieni si pole nSkladnik w obiekcie Foo - jego adres ma bowiem wskanik pFoo. Wypisanie wartoci pola tego obiektu: std::cout << Foo.nSkladnik;
415
uwiadomi wic nam, e ma ono warto 76. Ustawilimy j bowiem za porednictwem wskanika. To te ju znamy dobrze.
416
VECTOR3 Wektor;
Zaawansowane C++
Nastpnie moemy te pobra adres jej pola - ktrej ze wsprzdnych: float* pfX = &Wektor.x;
i tak oto uzyskuje adres czwartego elementu tablicy (o indeksie 3). Spjrzmy na dodawane wyraenie:
3 * sizeof(int)
Okrela ono przesunicie (ang. offset) elementu tablicy o indeksie 3 wzgldem jej pocztku. Znajc t warto kompilator oraz adres pierwszego elementu tablicy, kompilator moe wyliczy pozycj w pamici dla elementu numer 3. Dlaczego jednak o tym mwi? Ot bardzo podobna operacja zachodzi przy odwoywaniu si do pola w obiekcie klasy (struktury). Kiedy bowiem odnosimy si jakiego pola w ten oto sposb: Wektor.y to po pierwsze, kompilator zamienia to wyraenie tak, aby posugiwa si wskanikami, bo to jest jego mow ojczyst: (&Wektor)->y Nastpnie stosuje on ten sam mechanizm, co dla elementw tablic. Oblicza wic adres pola (tutaj y) wedug schematu:
&Wektor + offset_pola_y
Zaawansowana obiektowo
417
W tym przypadku sprawa nie jest aczkolwiek taka prosta, bo definicja klasy moe zawiera pola wielu rnych typw o rnych rozmiarach. Offset nie bdzie wic mg by wyliczany tak, jak to si dzieje dla elementu tablicy. On musi by znany ju wczeniej Skd? Z definicji klasy! Okrelajc nasz klas w ten sposb: struct VECTOR3 { float x, y, z; }; zdefiniowalimy nie tylko jej skadniki, ale te kolejno pl w pamici. Oczywicie nie musimy podawa dokadnych liczb, precyzujcych pooenie np. pola z wzgldem obiektu klasy VECTOR3. Tym zajmie si ju sam kompilator: przeanalizuje ca definicj i dla kadego pola wyliczy sobie oraz zapisze gdzie odpowiednie przesunicie. I t wanie liczb nazywamy wskanikiem na pole klasy: Wskanik na pole klasy jest okreleniem miejsca w pamici, jakie zajmuje pole danej klasy, wzgldem pocztku obiektu w pamici. W przeciwniestwie do zwykego wskanika nie jest to wic liczba bezwzgldna. Nie mwi nam, e tu-i-tu znajduje si takie-a-takie pole. Ona tylko informuje, o ile bajtw naley si przesun, poczynajc od adresu obiektu, a znale w pamici konkretne pole w tym obiekcie. Moe jeszcze lepiej zrozumiesz to na przykadzie kodu. Jeeli stworzymy sobie obiekt (statycznie, dynamicznie - niewane) - na przykad obiekt naszego wektora: VECTOR3* pWektor = new VECTOR3; i pobierzemy adres jego pola - na przykad adres pola y w tym obiekcie: int* pnY = &pWektor->y; to rnica wartoci obu wskanikw (adresw) - na obiekt i na jego pole:
pnY - pWektor
bedzie niczym innym, jak wanie offsetem tego pola, czyli jego miejscem w definicji klasy! To jest ten rodzaj wskanikw C++, jakim si chcemy tutaj zaj.
Pobieranie wskanika
Zauwamy, e offset pola jest wartoci globaln dla caej klasy. Kady bowiem obiekt ma tak samo rozmieszczone w pamici pola. Nie jest tak, e wrd kilku obiektw naszej klasy VECTOR3 jeden ma pola uoone w kolejnoci x, y, z, drugi - y, z, x, trzeci - z, y, x, itp. O nie, tak nie jest: wszystkie pola s poukadane dokadnie w takiej kolejnoci, jak ustalilimy w definicji klasy, a ich umiejscowienie jest dla kadego obiektu identyczne. Uzyskanie offsetu danego pola, czyli wskanika na pole klasy, moe wic odbywa si bez koniecznoci posiadania obiektu. Wystarczy tylko poda, o jak klas i o jakie pole nam chodzi, np.: &VECTOR3::y Powysze wyraenie zwrci nam wskanik na pole y w klasie VECTOR3. Powtarzam jeszcze raz (aby dobrze to zrozumia), i bdzie to ilo bajtw, o jak naley si
418
Zaawansowane C++
przesun poczynajc od adresu jakiego obiektu klasy VECTOR3, aby natrafi na pole y tego obiektu. Jeeli jest to dla ciebie zbyt trudne, to moesz mysle o tym wskaniku jako o indeksie pola y w klasie VECTOR3.
Zaawansowana obiektowo
p2mfWspolrzedna = &VECTOR3::z;
419
Warunkiem jest jednak, aby pole byo publiczne. W przeciwnym wypadku wyraenie klasa::pole byloby nielegalne (poza klas) i nie monaby zastosowa wobec niego operatora &.
Uycie wskanika
Wskanik na pole klasy jest adresem wzgldnym, offsetem. Aby skorzysta z niego praktycznie, musimy posiada jaki obiekt; kompilator bdzie dziki temu wiedzia, gdzie si dany obiekt zaczyna w pamici. Posiadajc dodatkowo offset pola w definicj klasy, bdziemy mogli odwoywa si do tego pola w tym konkretnym obiekcie. A zatem do dziea. Stwrzmy sobie obiekt naszej klasy: VECTOR3 Wektor; Potem zadeklarujmy wskanik na i ustawmy go na jedno z trzech pl klasy VECTOR3: float VECTOR3::*p2mfPole = &VECTOR3::x; Teraz przy pomocy tego wskanika moemy odwoac si do tego pola w naszym obiekcie. Jak? O tak: Wektor.*p2mfPole = 12; // wpisanie liczby do pola obiektu Wektor, // na ktre pokazuje wskanik p2mfPole
Caa zabawa polega tu na tym, e p2mfPole moe pokazywa na dowolne z trzech pl klasy VECTOR3 - x, y lub z. Przy pomocy wskanika moemy jednak do kadego z nich odwoywa si w ten sam sposb. Co nam to daje? Mniej wicej to samo, co w przypadku zwykych wskanikw. Wskanik na pole klasy moemy przekaza i wykorzysta gdzie indziej. W tym przypadku potrzebujemy aczkolwiek jeszcze jednej danej: obiektu naszej klasy, w kontekcie ktrego uyjemy wskanika. Moe czas na jaki konkretny przykad. Wyobramy sobie funkcj, ktra zeruje jedn wsprzdn tablicy wektorw. Teraz moemy j napisa: void WyzerujWspolrzedna(VECTOR3 aTablica[], unsigned uRozmiar, float VECTOR3::*p2mfWspolrzedna) { for (unsigned i = 0; i < uRozmiar; ++i) aTablica[i].*p2mfWspolrzedna = 0; } W zalenoci od tego, jak j wywoamy: VECTOR3 aWektory[50]; WyzerujWspolrzedna (aWektory, 50, &VECTOR3::x); WyzerujWspolrzedna (aWektory, 50, &VECTOR3::y); WyzerujWspolrzedna (aWektory, 50, &VECTOR3::z); spowoduje ona wyzerowanie rnych wsprzdnych wektorw w podanej tablicy. Wskanik na pole klasy moemy te wykorzysta, gdy na samym obiekcie operujemy take przy pomocy wskanika (tym razem zwykego, na obiekt). Stosujemy wtedy aczkolwiek inn skadni:
420
Zaawansowane C++
// deklaracja i inicjalizacja obu wskanikw - na obiekt i pole klasy VECTOR3* pWektor = new VECTOR3; float VECTOR3::p2mfPole = &VECTOR3::z; // zapisanie wartoci do pola z obiektu *pWektor przy pomocy wskanikw pWektor->*p2mfPole = 42; Jak wida, w kontekcie wskanikw na skadowe operatory .* i ->* s dokadnymi odpowiednikami operatorw wyuskania . i ->. Tych drugim uywamy jednak wtedy, gdy odwoujemy si do skadnikw obiektu poprzez ich nazwy, natomiast tych pierwszych jeli posugujemy si wskanikami do skadowych. Operator ->*, podobnie jak ->, moe by przeciony. Z kolei .*, tak samo jak . - nie.
Jednak nie tylko funkcje globalne mog by wskazywane przez takie wskaniki. Wskaniki do zwykych funkcji potrafi te pokazywa na statyczne metody klas. Nietrudno to wyjani. Takie metody to tak naprawd funkcje globalne o nieco zmienionym zasigu i notacji wywoania. Najwaniejsze, e nie posiadaj one ukrytego parametru - wskanika this - poniewa ich wywoanie nie wymaga obecnoci adnego obiektu klasy. Nie korzystaj one wic z konwencji wywoania thiscall (waciwej metodom niestatycznym), a zatem moemy zadeklarowa zwyke wskaniki, ktre bd na pokazywa. Warunkiem jest jednak to, aby metoda statyczna bya zadeklarowana jako public. W przeciwnym razie wyraenie nazwa_klasy::nazwa_metody nie bdzie legalne. Podobne uwagi mona poczyni dla statycznych pl, na ktre mona pokazywa przy pomocy zwykych wskanikw na zmienne.
Zaawansowana obiektowo
421
Deklaracja wskanika
Spjrzmy lepiej na jaki przykad. Wemy tak oto klas: class CNumber { private:
113 Zauwamy, e deklaracja metody wyjta z klasy i umieszczona poza ni automatycznie stanie si funkcj globaln. Nie trzeba dokonywa adnych zmian w jej prototypie, polegajcych np. na usuniciu sowa thiscall. Takiego sowa kluczowego po prostu nie ma: C++ odrnia metody od zwykych funkcji wycznie po miejscu ich zadeklarowania.
422
float m_fLiczba;
Zaawansowane C++
public: // konstruktor CNumber(float m_fLiczba = 0.0f) : m_fLiczba(fLiczba) { } // ------------------------------------------------------------// kilka metod float Dodaj(float x) float Odejmij(float x) float Pomnoz(float x) float Podziel(float x) { { { { return return return return (m_fLiczba (m_fLiczba (m_fLiczba (m_fLiczba += -= *= /= x); x); x); x); } } } }
};
Nie jest ona moe zbyt mdra - nie ma przecionych operatorw i w ogle wykonuje do dziwn czynno enkapsulacji typu podstawowego - ale dla naszych celw bdzie wystarczajca. Zwrmy uwag na jej cztery metody: wszystkie bior argument typu float i tak liczb zwracaj. Jeeli chcielibymy zadeklarowa wskanik, mogcy pokazywa na te metody, to robimy to w ten sposb114: float (CNumber::*p2mfnMetoda)(float); Wskanik p2mfnMetoda moe pokazywa na kad z tych czterech metod, tj.: float float float float CNumber::Dodaj(float x); CNumber::Odejmij(float x); CNumber::Pomnoz(float x); CNumber::Podziel(float x);
Mona std cakiem atwo wywnioskowa ogln skadni deklaracji takiego wskanika. A wic, dla metody klasy o nagwku: zwracany_typ nazwa_klasy::nazwa_metody([parametry]) deklaracja odpowiadajcego jej wskanika wyglda tak: zwracany_typ (nazwa_klasy::*nazwa_wskanika)([parametry]); Deklaracja wskanika na metod klasy wyglda tak, jak nagwek tej metody, w ktrym fraza nazwa_klasy::nazwa_metody zostaa zastpiona przez sekwencj (nazwa_klasy::*nazwa_wskanika). Na kocu deklaracji stawiamy oczywicie rednik. Sposb jest wic bardzo podobny jak przy zwykych wskanikach na funkcje. Ponownie te istotne staj si nawiasy. Gdybymy bowiem je opucili w deklaracji p2mfnMetoda, otrzymalibymy: float CNumber::*p2mfnMetoda(float); co zostanie zinterpretowane jako: float CNumber::* p2mfnMetoda(float);
114
Zaawansowana obiektowo
423
czyli funkcja biorca jeden argument float i zwracajca wskanik do pl typu float w klasie CNumber. Zatem znowu - zamiast wskanika na funkcj otrzymujemy funkcj zwracajc wskanik. Dla wskanikw na metody klas nie ma problemu z umieszczenia sowa kluczowego konwencji wywoania, bo wszystkie metody klas uywaj domylnej i jedynie susznej w ich przypadku konwencji thiscall. Nie ma moliwoci jej zmiany (mam nadziej, e jest oczywiste, dlaczego).
Uycie wskanika
Czas wreszcie na akcj. Zobaczmy, jak mona wywoa metod pokazywan przez wskanik: CNumber Liczba = 42; std::cout << (Liczba.*p2mfnMetoda)(2); Potrzebujemy naturalnie jakiego obiektu klasy CNumber, aby na jego rzecz wywoa metod. Tworzymy go wic; dalej znowu korzystamy z operatora .*, wywoujc przy jego pomocy metod klasy CNumber dla naszego obiektu - przekazujemy jej jednoczenie parametr 2. Poniewa po naszej zabawie z przypisywaniem p2mfnMetoda pokazywa na metod Odejmij(), na ekranie zobaczylibymy:
40
Zwracam jeszcze uwag na nawiasy w wywoaniu metody. Tutaj s one konieczne (w przeciwiestwie do zwykych wskanikw na funkcje) - bez nich kompilator uzna linijk za bdn. Domylasz si, e jeli posiadalibymy tylko wskanik na obiekt, to do wywoania jego metody posuylibymy si operatorem ->*. Identycznie jak przy wskanikach na pola klasy.
424
Zaawansowane C++
Tutaj musz ci nieco zmartwi. Wskaniki na skadowe klasy s w praktyce bardzo rzadko uywane, bo w zasadzie trudno znale dla nich jakie uyteczne zastosowanie. To chyba najdobitniejszy przykad jzykowego wodotrysku - na szczcie C++ nie posiada zbyt wiele takich nadmiarowych udziwnie. Sprbujemy jednak znale dla nich jakie zastosowanie Okazuje si, e jest to moliwe. Wskanikw tych moemy bowiem uy do symulowania innego rodzaju wskanikw - nieobecnych niestety w C++, ale za to bardzo przydatnych. Jakie to wskaniki? Spjrz na ponisz tabel. Grupuje ona wszystkie znane (i nieznane ;D) w programowaniu strukturalnym i obiektowym rodzaje wskanikw, wraz z ich nazwami w C++: rodzaj wskanika cel wskanika dane kod obiektowe strukturalne na skadowe statyczne na skadowe niestatyczne w klasach w obiektach wskaniki do pl wskaniki do klasy zmiennych wskaniki do BRAK metod klasy
Wynika z niej, e znamy ju wszystkie rodzaje wskanikw, jakie posiada w swoim arsenale C++. A co z tymi brakujcymi? Czym one s? Ot s to takie wskaniki, ktre potrafi pokazywa na konkretn metod w konkretnym obiekcie. Podobnie jak wskaniki do pl obiektu, s one samodzielne. Ich uycie nie wymaga wic adnych dodatkowych informacji: dokonujc zwyczajnej dereferencji takiego wskanika, wywoywalibymy okrelon metod w odniesieniu do okrelonego obiektu. Zupenie tak, jak dla zwykych wskanikw do funkcji - tyle tylko, e tutaj nie wywoujemy funkcji globaln, lecz metod obiektu. No dobrze, nie mamy tego rodzaju wskanikw Ale co z tego? Na pewno s one rwnie uyteczne, jak te co poznalimy niedawno! Ot wrcz przeciwnie! Tego rodzaju wskaniki s niezwykle przydatne! Pozwalaj one bowiem na implementacj funkcji zwrotnych (ang. callback functions) z zachowaniem penej obiektowoci programu. C to s - te funkcje callback? S to takie funkcje, ktrych adresy przekazujemy komu, aby ten kto mg je dla nas wywoa w odpowiednim momencie. Ten odpowiedni moment to na przykad zajcie jakiego zdarzenia, na ktre oczekujemy (wcinicie klawisza, wybicie pnocy na zegarze, itp.) albo chociaby wystpienie bdu. W kadej tego typu sytuacji nasz program moe by o tym natychmiast poinformowany. Bez funkcji zwrotnych musiaby zwykle dokonywa mozolnego odpytywania ktosia, aby dowiedzie si, czy dana okoliczno wystpia. To mao efektywne rozwizanie. Funkcje callback s lepsze. Jednak w C++ tylko funkcje globalne lub statyczne metody klas mog by takimi funkcjami. Powd jest prosty: jedynie na takie metody moemy pokazywa samodzielnymi wskanikami. A to jest zupenie niezadowolajce w programowaniu obiektowym. Zmusza to przecie do pisania kodu poza klasami programu. W dodatku trzeba jako zapewni sensown komunikacj midzy tym kodem-outsiderem, a obiektow reszt programu. W sumie mamy mnstwo kopotw. Wymylono rzecz jasna pewien sposb na obejcie tego problemu, polegajcy na wykorzystaniu metod wirtualnych, dziedziczenia i polimorfizmu. Nie jest to jednak idealne rozwizanie - przynajmniej nie w C++.
Zaawansowana obiektowo
Powiedziaem jednak, e nasze wieo poznane wskaniki mog pomc w poradzeniu sobie z tym problemem. Zobaczmy jak to zrobi.
425
Bardzo, ale to bardzo odradzam czytanie tych dwch punktw przy pierwszym kontakcie z tekstem (to zreszt dotyczy prawie wszystkich Ciekawostek). Sprawa jest wprawdzie bardzo ciekawa i niezwykle przydatna, lecz jej zawio moe ci szybko odstrczy od wskanikw klasowych - albo nawet od programowania obiektowego, co by byo znacznie gorsz katastrof.
426
drugi to wskanik na metod klasy, ktra ma by wywoywana
Zaawansowane C++
Chcc stworzy nasz wskanik, musimy wic poczy te dwie dane. Zrbmy to! Najpierw zdefiniujmy sobie jak klas, na ktrej metody bdziemy pokazywa: class CFoo { public: void Metoda(int nParam) { std::cout << "Wywolano z " << nParam; } }; Dalej - dodajmy obiekt, ktry bdzie bra udzia w wywoaniu: CFoo Foo; Przypomnijmy wreszcie, e chcemy zrobi taki wskanik, ktrego uycie zastapi nam wywoanie: Foo.Metoda(); Potrzebujemy do tego wspomnianych dwch rodzajw wskanikw: wskanika na obiekty klasy CFoo wskanika na metody klasy CFoo biorce int i niezwracajce wartoci Poczymy oba te wskaniki w jedn struktur, dodajc przy okazji pomocnicze funkcje jak konstruktor oraz operator(): struct METHODPOINTER { // rzeczone oba wskaniki CFoo* pObject; void (CFoo::*p2mfnMethod)(int);
// ------------------------------------------------------------------// konstruktor METHODPOINTER(CFoo* pObj, void (CFoo::*p2mfn)(int)) : pObject(pObj), p2mfnMethod(p2mfn) // operator wywoania funkcji void operator() (int nParam) { (pObject->*p2mfnMethod(nParam); }
{ }
};
Teraz moemy ju pokaza takim wskanikiem na metod naszego obiektu. Podajemy po prostu zarwno wskanik na obiekt, jak i na metod klasy: METHODPOINTER p2ofnMetoda(&Foo, &CFoo::Metoda); To wprawdzie pewna niedogodno (nie moemy poda po prostu Foo.Metoda, lecz musimy pamita nazw klasy), ale i tak jest to cakiem dobre rozwizanie. Nasz metod moemy bowiem wywoa w najprostszy moliwy sposb: p2ofnMetoda (69); // to samo co Foo.Liczba (69);
Zaawansowana obiektowo
Jest to aczkolwiek rozwizanie dla szczeglnego przypadku. A jak wyglda to w przypadku oglnym? Mniej wicej w ten sposb: struct WSKANIK { // wskaniki klasa* pObject; zwracany_typ (klasa::*p2mfnMethod)([parametry_formalne]);
427
// ------------------------------------------------------------------// konstruktor WSKANIK(klasa* pObj, zwracany_typ (klasa::*p2mfn)([parametry_formalne])) : pObject(pObj), p2mfnMethod(p2mfn) { } // operator wywoania funkcji zwracany_typ operator() ([parametry_formalne]) { [return] (pObject->*p2mfnMethod([parametry_aktualne]); } }; Niestety, preprocesor na niewiele nam si przyda w tym przypadku. Tego rodzaju struktury musiaby wpisywa do kodu samodzielnie.
Mona do niej doda wirtualny destruktor czy inne wsplne dla wszystkich klas skadowe, jednak to nie jest tutaj wane. Grunt, eby taka klasa bya obecna. Teraz sprecyzujmy problem. Zamy, e mamy kilka innych klas, zawierajcych metody o waciwej dla nas sygnaturze: class CFoo : public IObject { public: float Funkcja(int x) };
{ return x * 0.75f; }
428
Zaawansowane C++
{ return x * 1.42f; }
Zauwamy z IObject. Czego chcemy? Ot poszukujemy sposobu na zaimplementowanie wskanika, ktry bdzie pokazywa na metod Funkcja() zarwno w obiektach klasy CFoo, jak i CBar. Nawet wicej - chcemy takiego wskanika, ktry pokae nam na dowoln metod biorc int i zwracaj float w dowolnym obiekcie dowolnej klasy w naszym programie. Mwiem ju, e w praktyce ta dowolna klasa musi dziedziczy po IObject. C wic zrobi? Moe znowu signiemy po dwa wskaniki - jeden na obiekt, a drugi na metod klasy? Punkt dla ciebie. Faktycznie, tak wanie zrobimy. Posta naszego wskanika nie rni si wic zbytnio od tej z poprzedniego punktu: struct METHODPOINTER { // rzeczone oba wskaniki IObject* pObject; float (IObject::*p2mfnMethod)(int);
// ------------------------------------------------------------------// konstruktor METHODPOINTER(IObject* pObj, float (IObject::*p2mfn)(int)) : pObject(pObj), p2mfnMethod(p2mfn) { } // operator wywoania funkcji float operator() (int x) { return (pObject->*p2mfnMethod(x); }
};
Chwileczk Deklarujemy tutaj wskanik na metody klasy IObject, biorce int i zwracajce float Ale przecie IObject nie ma takich metod - ba, u nas nie ma nawet adnych metod! Takim wskanikiem nie pokaemy wic na adn metod! Bingo, kolejny punkt za uwan lektur :) Rzeczywicie, taki wskanik wydaje si bezuyteczny. Pamitajmy jednak, e w sumie chcemy pokazywa na metod obiektu, a nie na metod klasy. Za nasze obiekty bd pochodzi od klasy IObject, bo ich wasne klasy po IObject dziedzicz. W sumie wic wskanikiem na metod klasy bazowej bdziemy pokazywa na metod klasy pochodnej. To jest poprawne - za chwil wyjani bliej, dlaczego. Najpierw sprbujmy uy naszego wskanika. Stwrzmy wic obiekt ktrej z klas: CBar* pBar = new CBar; i ustawmy nasz wskanik na metod Funkcja() w tym obiekcie - tak, jak to robilimy dotd: METHODPOINTER p2ofnMetoda(pBar, &CBar::Funkcja); I jak? Mamy przykr niespodziank. Kady szanujcy si kompilator C++ najpewniej odrzuci t linijk, widzc niezgodno typw. Jak niezgodno?
Zaawansowana obiektowo
429
Pierwszy parametr jest absolutnie w porzdku. To znana i lubiana konwersja wskanika na obiekt klasy pochodnej (CBar*) do wskanika na obiekt klasy bazowej (IObject*). Brak zastrzee nikogo nie dziwi - przecie na tym opiera si cay polimorfizm. To drugi parametr sprawia problem. Kompilator nie zezwala na zamian typu: float (CBar::*)(int) na typ: float (IObject::*)(int) Innymi sowy, nie pozwala na konwersj wskanik na metod klasy pochodnej do wskanika na metod klasy bazowej. Jest to uzasadnione: wskanik na metod (oglnie: na skadow) moe by bowiem poprawny w klasie pochodnej, natomiast nie zawsze bdzie poprawny w klasie bazowej. Obiekt klasy bazowej moe by przecie mniejszy, nie zawiera pewnych elementw, wprowadzonych w modszym pokoleniu. W takim wypadku wskanik bdzie strzela w prni, co skoczy si bdem ochrony pamici116. Tak mogoby by, jednak u nas tak nie bdzie. Naszego wskanika na metod uyjemy przecie tylko i wyacznie do wywoania metody obiektu pBar. Klasa obiektu oraz klasa wskanika w tym przypadku zgadzaj si, s identyczne - to CBar. Nie ma adnego ryzyka. Kompilator bynajmniej o tym nie wie i nie naley go wcale za to wini. Musimy sobie po prostu pomc rzutowaniem: METHODPOINTER p2ofnMetoda(pBar, static_cast<float (IObject::*)(int)> (&CBar::Funkcja)); Wiem, e wyglda to okropnie, ale przecie nic nie stoi na przeszkodzie, aby uly sobie odpowiednim makrem. Zreszt, liczy si efekt. Teraz moemy wywoa metod pBar->Funkcja() w ten prosty sposb: p2ofnMetoda (42); // to samo co pBar->Funkcja (42);
Jest te zupenie moliwe, aby pokaza naszym wskanikiem na analogiczn metod w obiekcie klasy CFoo: CFoo Foo; p2ofnMetoda.pObject = &Foo; p2ofnMetoda.p2mfnMethod = static_cast<float (IObject::*)(int)> (&CFoo::Funkcja)); p2ofnMetoda (14); // to samo co Foo.Funkcja (14)
Zmieniajc ustawienie wskanika musimy jednak pamita, by: Klasy docelowego obiektu oraz docelowej metody musz by identyczne. Inaczej ryzykujemy bad ochrony pamici.
Konwersja w drug stron (ze wskanika na skadow klasy bazowej do wskanika na skadow klasy pochodnej) jest z kolei zawsze moliwa. Jest tak dlatego, e klasa pochodna nie moe usun adnego skadnika klasy bazowej, lecz co najwyej rozszerzy ich zbir. Wskanik bdzie wic zawsze poprawny.
116
430
Zaawansowane C++
Zaprezentowane rozwizanie moe nie jest szczeglnie eleganckie, ale wystarczajce. Nie zmienia to jednak faktu, e wbudowana obsuga wskanikw na metody obiektw w C++ byaby wielce podana. Nieco lepsz implementacj wskanikw tego rodzaju, korzystajc m.in. z szablonw, moesz znale w moim artykule Wskanik na metod obiektu. *** Czy masz ju do? :) Myl, e tak. Wskaniki na skadowe klas (czy te obiektw) to nie jest najatwiejsza cz OOPu w C++ - miem twierdzi, e wrcz przeciwnie. Mamy j ju jednak za sob. Jeeli aczkolwiek chciaby si dowiedzie na ten temat nieco wicej (take o zwykych wskanikach na funkcje), to polecam wietn witryn The Function Pointer Tutorials. W ten sposb poznalimy te ca ofert narzdzi jzyka C++ w zakresie programowania obiektowego. Moemy sobie pogratulowa.
Podsumowanie
Ten dugi rozdzia by powicony kilku specyficznym dla C++ zagadnieniom programowania obiektowego. Zdecydowana wikszo z nich ma na celu poprawienie wygody, czasem efektywnoci i naturalnoci kodowania. C wic zdylimy omwi? Na pocztek poznalimy zagadnienie przyjani midzy klasami a funkcjami i innymi klasami. Zobaczye, e jest to prosty sposb na zezwolenie pewnym cile okrelonym fragmentom kodu na dostp do niepublicznych skadowych jakiej klasy. Dalej przyjrzelimy si bliej konstruktorom klas. Poznalimy ich listy inicjalizacyjne, rol w kopiowaniu obiektw oraz niejawnych konwersjach midzy typami. Nastpnie dowiedzielimy si (prawie) wszystkiego na temat bardzo przydatnego udogodnienia programistycznego: przeciania operatorw. Przy okazji powtrzylimy sobie wiadomoci na temat wszystkich operatorw jzyka C++. Wreszcie, odwaniejsi spord czytelnikw zapoznali si take ze specyficznym rodzajem wskanikw: wskanikami na skadniki klasy. Nastpny rozdzia bdzie natomiast powicony niezwykle istotnemu mechanizmowi wyjtkw.
Pytania i zadania
By moe zaprezentowane w tym rozdziale techniki su tylko wygodzie programisty, ale nie zwalnia to kodera z ich dokadnej znajomoci. Odpowiedz wic na powysze pytania i wykonaj wiczenia.
Pytania
1. Jakie specjalne uprawnienia ma przyjaciel klasy? Co moe by takim przyjacielem? 2. W jaki sposb deklarujemy zaprzyjanion funkcj? 3. Co oznacza deklaracja przyjani z klas? 4. Jak mona sprawi, aby dwie klasy przyjaniy si z wzajemnoci? 5. Co to jest konstruktor domylny? Jakie s korzyci klasy z jego posiadania?
Zaawansowana obiektowo
431
6. Czym jest inicjalizacja? Kiedy i jak przebiega? 7. Do czego suy lista inicjalizacyjna konstruktora? 8. Kiedy konieczny jest konstruktor kopiujcy? 9. W jaki sposb moemy definiowa niejawne konwersje? 10. Co powoduje sowo kluczowe explicit w deklaracji konstruktora? 11. Kiedy konstruktor konwertujcy jest jednoczenie domylnym? 12. Wymie podstawowe cechy operatorw w jzyku programowania. 13. Jakie rodzaje operatorw posiada C++? 14. Na czym polega przecienie operatora? 15. Jaki status mog posiada funkcje operatorowe? Czym si one rni? 16. Jak mona skorzysta z niejawnych konwersji, piszc przecione wersje operatorw binarnych? 17. Ktre operatory mog by przeciane wycznie jako niestatyczne metody klas? 18. Kiedy konieczne jest zdefiniowanie wasnego operatora przypisania? 19. Ile argumentw ma operator wywoania funkcji? 20. O czym naley pamita, przeciajc operatory? 21. O czym informuje wskanik do skadowej klasy? 22. Jakim wskanikiem pokazujemy na pole w obiekcie, a jakim na pole w klasie? 23. Czy zwykym wskanikiem do funkcji moemy pokaza na metod obiektu?
wiczenia
1. Zdefiniuj dwie klasy, ktre bd ze sob wzajemnie zaprzyjanione. 2. Przejrzyj definicje klas z poprzednich rozdziaw i popatrz na ich konstruktory. W ktrych przypadkach monaby uy w nich list inicjalizacyjnych? 3. Do klas CRational i CComplex dodaj operatory niejawnych konwersji na typ bool. Co dziki temu zyskae? 4. (Trudniejsze) Wzboga wspomniane klasy take o operatory dodawania, odejmowania i dzielenia (tylko CRational) oraz o odpowiadajce im operatory zoonego przypisania i in/dekrementacji. 5. Napisz funktor obliczajcy najwiksz z podawanych mu liczb typu float. Niech stosuje on ten sam interfejs i sposb dziaania, co klasa CAverageFunctor.
3
WYJTKI
Dowiadczenie - to nazwa, jak nadajemy naszym bdom.
Oscar Wilde
Programici nie s nieomylni. O tym wiedz wszyscy, a najlepiej oni sami. W kocu to gwnie do nich naley codzienna walka z wikszymi i mniejszymi bdami, wkradajcymi si do kodu rdowego. Dobrze, jeli s to tylko usterki skadniowe w rodzaju braku potrzebnego rednika albo domykajcego nawiasu. Wtedy sam kompilator daje o nich zna. Nieco gorzej jest, gdy mamy do czynienia z bdami objawiajcymi si dopiero podczas dziaania programu. Moe to spowodowa nawet produkowanie nieprawidowych wynikw przez nasz aplikacj (bdy logiczne). Wszystkie tego rodzaju sytuacj maj jdna cech wspln. Mona bowiem (i naley) im zapobiega: moliwe i podane jest takie poprawienie kodu, aby bdy tego typu nie pojawiay si. Aplikacja bdzie wtedy dziaaa poprawnie Ale czy na pewno? Czy twrca aplikacji moe przewidzie wszystkie sytuacje, w jakich znajdzie si jego program? Nawet jeli jego kod jest cakowicie poprawny i wolny od bdw, to czy gwarantuje to jego poprawne dziaanie za kadym razem? Gdyby odpowied na chocia jedno z tych pyta brzmiaa Tak, to programici pewnie rwaliby sobie z gw o poow mniej wosw ni obecnie. Niestety, nikt o zdrowym rozsdku nie moe obieca, e jego kod bdzie zawsze dziaa zgodnie z oczekiwaniami. Naturalnie, jeeli jest on napisany dobrze, to w wikszoci przypadkw tak wanie bdzie. Od kadej reguy zawsze jednak mog wystpi wyjtki W tym rozdziale bdziemy mwi wanie o takich wyjtkach - albo raczej o sytuacjach wyjtkowych. Poznamy moliwoci C++ w zakresie obsugi takich niecodziennych zdarze i oglne metody radzenia sobie z nimi.
434
Zaawansowane C++
Dopuszczalne sposoby
Do cakiem dobrych metod informowania o niespodziewanych sytuacjach naley zwracanie jakiej specjalnej wartoci - indykatora. Wywoujcy dan funkcj moe wtedy sprawdzi, czy bd wystpi, kontrolujc rezultaty zwrcone przez podprogram.
Funkcja ta wykorzystuje iteracyjn metod Newtona do obliczania pierwiastka, ale to nie jest dla nas zbyt wane, bowiem dotyczy zwykej sytuacji. My natomiast mwimy o sytuacjach niezwykych. Co ni bdzie dla naszej funkcji? Na pewno bdzie to podanie jej liczby ujemnej. Dopki pozostajemy na gruncie prostej matematyki, jest to dla nas bdna warto - nie mona wycign pierwiastka kwadratowego z liczby mniejszej od zera. Nie mona jednak wykluczy, e nasza funkcja otrzyma kiedy liczb ujemn. Bdzie to bd, sytuacja wyjtkowa - i trzeba bdzie na ni zareagowa. cile mwic, trzeba bdzie poinformowa o niej wywoujcego funkcj.
Wyjtki
435
Specjalny rezultat
Jak mona to zrobi? Prostym sposobem jest zwrcenie specjalnej wartoci. Niech bdzie to warto, ktra w normalnych warunkach nie ma prawa by zwrcona. W tym przypadku powinna to by taka liczba, ktrej prawidowe zwrcenie przez Pierwiastek() nie powinno mie miejsca. Jaka to liczba? Oczywicie - dowolna liczba ujemna. Powiedzmy, e np. -1: if (x < 0) return -1;
Po dodaniu tego sprawdzenia funkcja bdzie ju odporna na sytuacje z nieprawidowym argumentem. Wywoujcy j bdzie musia natomiast sprawdza, czy rezultat funkcji nie jest przypadkiem informacj o bdzie - np. w ten sposb: float fLiczba; float fPierwiastek; if ((fPierwiastek = Pierwiastek(fLiczba)) < 0) std::cout << "Nieprawidlowa liczba"; else std::cout << "Pierwiastek z " << fLiczba << " to " << fPierwiastek; Jak wida, przy wykorzystaniu wartoci zwracanej operatora przypisania nie jest to szczeglnie uciliwe.
Tutaj take moliwe jest podanie nieprawidowych argumentw: wystarczy, eby cho jeden z nich by ujemny lub aby podstawa logarytmu (a) bya rwna jeden. Nie warto polega na reakcji funkcji bibliotecznej log() w razie zaistnienia takiej sytuacji; lepiej samemu co na to poradzi. No wanie - ale co? Moemy oczywicie skontrolowa poprawno argumentw funkcji: if (a < 0 || a == 1.0f || x < 0) /* bd, ale jak o nim powiedzie?... */ ale nie bardzo wiadomo, jak specjaln warto naleaoby zwrci. W zakresie typu float nie ma bowiem adnej wolnej liczby, poniewa poprawny wynik logarytmu moe by kad liczb rzeczywist. Ostatecznie mona zwrci zero, ktry to wynik zachodzi normalnie tylko dla x rwnego 1. Wwczas jednak sprawdzanie potencjalnego bdu byoby bardzo niewygodne: // sprawdzamy, czy rezultat jest rwny zero, a argument rny od jeden; // jeeli tak, to bd if (((fWynik = LogA(fPodstawa, fLiczba)) == 0.0f) && fLiczba != 1.0f) std::cout << "Zly argument funkcji";
436
Zaawansowane C++
else std::cout << "Logarytm o podst. " << fPodstawa << " z " << fLiczba << " wynosi " << fWynik; To chyba przesdza fakt, i czenie informacji o bdzie z waciwym wynikiem nie jest dobrym pomysem.
Wykorzystanie wskanikw
Nasza funkcja, oprcz normalnych argumentw, moe przyjmowa jeden wskanik. Za jego porednictwem przekazana zostanie dodatkowa warto. Moe to by informacja o bdzie, ale czciej (i wygodniej) umieszcza si tam waciwy rezultat funkcji. Jak to wyglda? Oto przykad. Funkcja StrToUInt() dokonuje zamiany liczby naturalnej zapisanej jako cig znakw (np. "21433") na typ unsigned: #include <cmath> bool StrToUInt(const std::string& strLiczba, unsigned* puWynik) { // sprawdzamy, czy podany napis w ogle zawiera znaki if (strLiczba.empty()) return false; /* dokonujemy konwersji */ // zmienna na wynik unsigned uWynik = 0; // przelatujemy po kolejnych znakach, sprawdzajc czy s to cyfry for (unsigned i = 0; i < strLiczba.length(); ++i) if (strLiczba[i] > '0' && strLiczba[i] < '9') { // OK - cyfra; mnoymy aktualny wynik przez 10 // i dodajemy t cyfr uWynik *= 10; uWynik += strLiczba[i] - '0'; } else // jeeli znak nie jest cyfr, to koczymy niepowodzeniem return false; // w przypadku sukcesu przepisujemy wynik i zwracamy true *puWynik = uWynik; return true;
Wyjtki
437
Nie jest ona moe najszybsza, jako e wykorzystuje najprostszy, naturalny algorytm konwersji. Nam jednak chodzi o co innego: o sposb, w jaki funkcja zwraca rezultat i informacj o ewentualnym bdzie. Jak mona zauway, typem zwracanym przez funkcj jest bool. Nie jest to wic zasadniczy wynik, lecz tylko znacznik powodzenia lub niepowodzenia dziaa. Zasadniczy rezultat to kwestia ostatniego parametru funkcji: naley tam przekaza wskanik na zmienn, ktra otrzyma wynikow liczb. Brzmi to moe nieco skomplikowanie, ale w praktyce korzystanie z tak napisanej funkcji jest bardzo proste: std::string strLiczba; unsigned uLiczba; if (StrToUInt(strLiczba, &uLiczba)) std::cout << strLiczba << " razy dwa == " << uLiczba * 2; else std::cout << strLiczba << " - nieprawidlowa liczba"; Moesz si spiera: Ale przecie tutaj mamy wybitnego kandydata na poczenie rezultatu z informacj o bdzie! Wystarczy zmieni zwracany typ na int - wtedy wszystkie wartoci ujemne mogyby informowa o bdzie! Chyba jednak sam widzisz, jak to rozwizanie byoby nacigane. Nie do, e uylibymy nieadekwatnego typu danych (ktry ma mniejszy zakres interesujcych nas liczb dodatnich ni unsigned), to jeszcze ograniczylibymy moliwo przyszej rozbudowy funkcji. Zamy na przykad, e na bazie StrToUInt() chcesz napisa funkcj StrToInt(): bool StrToInt(const std::string& strLiczba, int* pnWynik); Nie jest to trudne, jeeli wykorzystujemy zaprezentowan tu technik informacji o bdach. Gdybymy jednak poprzestali na czeniu rezultatu z informacj o bedzie, wwczas byoby to problemem. Oto stracilibymy przecie ca ujemn powk typu int, bo ona teraz take musiaaby by przeznaczona na poprawne wartoci. Dla wprawy w oglnym programowaniu moesz napisa funkcj StrToInt(). Jest to raczej proste: wystarczy doda sprawdzanie znaku minus na pocztku liczby i nieco zmodyfikowa ptl for. Wida wic, e mimo pozornego zwikszenia poziomu komplikacji, ten sposb informowania o bedach jest lepszy. Nic dziwnego, e stosuj go zarwno funkcje Windows API, jak i interfejsu DirectX.
Uycie struktury
Dla nieobytych ze wskanikami (mam nadziej, e do nich nie naleysz) sposb zaprezentowany wyej moe si wydawa dziwny. Istnieje te nieco inna metoda na odseparowanie waciwego rezultatu od informacji o bdzie. Ot parametry funkcji pozostawiamy bez zmian, natomiast inny bdzie typ zwracany przez ni. W miejsce pojedynczej wartoci (jak poprzednio: unsigned) uyjemy struktury: struct RESULT { unsigned uWynik; bool bBlad;
438
}; Zmodyfikowany prototyp bdzie wic wyglda tak: RESULT StrToUInt(const std::string& strLiczba); Myl, e nietrudno zgadn, jakie zmiany zajd w treci funkcji.
Zaawansowane C++
Wywoanie tak spreparowanej funkcji nie odbiega od wywoania funkcji z wymieszanym rezultatem. Musi ono wyglda co najmniej tak: RESULT Wynik = StrToUInt(strLiczba); if (Wynik.bBlad) /* bd */ Mona te uy warunku: if ((Wynik = StrToUInt(strLiczba)).bBlad) ktry wyglda pewnie dziwnie, ale jest skadniowo poprawny, bo przecie wynikiem przypisania jest zmienna typu RESULT. Tak czy inaczej, nie jest to zbyt pocigajca droga. Jest jeszcze gorzej, jeli uwiadomimy sobie, e dla kadego moliwego typu rezultatu naleaoby definiowa odrbn struktur. Poza tym prototyp funkcji staje si mniej czytelny, jako e typ jej waciwego rezultatu (unsigned) ju w nim nie wystpuje. 117 Dlatego te o wiele lepiej uywa metody z dodatkowym parametrem wskanikowym.
Wywoanie zwrotne
Idea wywoania zwrotnego (ang. callback) jest nieskomplikowana. Jeeli w pisanej przez nas funkcji zachodzi sytuacja wyjtkowa, wywoujemy inn funkcj pomocniczn. Taka funkcja moe peni rol ratunkow i sprbowa naprawi okolicznoci, ktre doprowadziy do powstania problemu - jak np. bdne argumenty dla naszej funkcji. W ostatecznoci moe to by tylko sposb na powiadomienie o nienaprawialnej sytuacji wyjtkowej.
Uwaga o wygodnictwie
Zaleta wywoania zwrotnego uwidacznia si w powyszym opisie. Przy jego pomocy nie jestemy skazani na bierne przyjcie do wiadomoci wystpienia bdu; przy odrobinie dobrej woli mona postara si go naprawi. Nie zawsze jest to jednak moliwe. Mona wprawdzie poprawi nieprawidowy parametr, przekazany do funkcji, ale ju nic nie zaradzimy chociaby na brak pamici.
117
Wykorzystanie szablonw zlikwidowaoby obie te niedogodnoci, ale czy naprawd s one tego warte?
Wyjtki
439
Poza tym, technika callback z gry czyni pesymistyczne zaloenie, e sytuacje wyjtkowe bd trafiay si na tyle czsto, e konieczny staje si mechanizm wywoa zwrotnych. Jego stosowanie nie zawsze jest wspmierne do problemu, czasem jest to zwyczajne strzelanie z armaty do komara. Przykadowo, w funkcji Pierwiastek() spokojnie moemy sobie pozwoli na inne sposoby informowania o bdach - nawet w obliczu faktu, e naprawienie nieprawidowego argumentu byoby przecie moliwe. Funkcja ta nie jest bowiem na tyle kosztowna, aby opacao si chroni j przed niespodziewanym zakoczeniem. Dlaczego jednak wywoanie zwrotne jest taki cikim rodkiem? Ot wymaga ono specjalnych przygotowa. Od strony programisty-klienta obejmuj one przede wszystkim napisania odpowiednich funkcji zwrotnych. Od strony piszcego kod biblioteczny wymagaj natomiast gruntowego obmylenia mechanizmu takich funkcji zwrotnych: tak, aby nie mnoy ich ponad miar, a jednoczenie zapewni dla siebie pewn wygod i uniwersalno.
Uwaga o logice
Funkcje callback s te bardzo kopotliwe z punktu widzenia logiki programu i jego konstrukcji. Zakadaj bowiem, by kod niszego poziomu - jak funkcje biblioteczne w rodzaju wspomnianej Pierwiastek() lub StrToUInt() - wywoyway kod wyszego poziomu, zwizany bezporednio z dziaaniem samej aplikacji. amie to naturaln hierarchi warstw kodu i burzy porzdek jego wykonywania.
Zakoczenie programu
Wyjtkowy bd moe spowodowa jeszcze jedn moliw akcj: natychmiastowe zakoczenie dziaania programu. Brzmi to bardzo drastycznie i takie jest w istocie. Naprawd trudno wskaza sytuacj, w ktrej byoby konieczne przerwanie wykonywania aplikacji - zwaszcza niepoprzedzone adnym ostrzeeniem czy zapytaniem do uytkownika. Chyba tylko krytyczne braki pamici lub niezbdnych plikw mog by tego czciowym usprawiedliwieniem. Na pewno jednak fatalnym pomysem jest stosowanie tego rozwizania dla kadej sytuacji wyjtkowej. I chyba nawet nie musz mwi, dlaczego
Wyjtki
Takie s tradycyjne sposobu obsugi sytuacji wyjtkowych. Byy one przydatne przez wiele lat i nadal nie straciy nic ze swojej uytecznoci. Nie myl wic, e mechanizm, ktry zaraz poka, moe je cakowicie zastpi. Tym mechanizmem s wyjtki (ang. exceptions). Skojarzenie tej nazwy z sytuacjami wyjtkowymi jest jak najbardziej wskazane. Wyjtki su wanie do obsugi niecodzienych, niewystpujcych w normalnym toku programu wypadkw. Spjrzmy wic, jak moe si to odbywa w C++.
440
Zaawansowane C++
Blok try-catch
Obsuga sytuacji wyjtkowych zawiera si wewntrz blokw try i catch. Wygldaj one na przykad tak: try {
ryzykowne_instrukcje } catch (...) { kod_obsugi_wyjtkw } ryzykowne_instrukcje zawarte wewntrz bloku try s kodem, ktry poddawany jest pewnej specjalnej ochronie na wypadek wystpienia wyjtku. Na czym ta ochrona polega - bdziemy mwi w nastpnym podrozdziale. Na razie zapamitaj, e w bloku try umieszczamy kod, ktrego wykonanie moe spowodowa sytuacj wyjtkow, np. wywoania funkcji bibliotecznych. Jeeli tak istotnie si stanie, to wwczas sterowanie przenosi si do bloku catch. Instrukcja catch apie wystpujce wyjtki i pozwala przeprowadzi ustalone dziaania w reakcji na nie.
Wyjtki
441
Instrukcja throw
Kiedy wiadomo, e wystpia sytuacja wyjtkowa? Ot musi ona zosta zasygnalizowana przy pomocy instrukcji throw: throw obiekt; Wystpienie tej instrukcji powoduje natychmiastowe przerwanie normalnego toku wykonywania programu. Sterowanie przenosi si wtedy do najbliszego pasujcego bloku catch. Rzucony obiekt peni natomiast funkcj informujc. Moe to by warto dowolnego typu - rwnie bdca obiektem zdefiniowanej przez nas klasy, co jest szczeglnie przydatne. obiekt zostaje wyrzucony poza blok try; mona to porwna do pilota katapultujcego si z samolotu, ktry niechybnie ulegnie katastrofie. Wystpienie throw jest bowiem sygnaem takiej katastrofy - sytuacji wyjtkowej.
Wdrwka wyjtku
Zaraz za blokiem try nastpuje najczciej odpowiednia instrukcja catch, ktra zapie obiekt wyjtku. Wykona potem odpowiednie czynnoci, zawarte w swym bloku, a nastpnie program rozpocznie wykonywanie dalszych instrukcji, zaraz za blokiem catch. Jeli jednak wyjtek nie zostanie przechwycony, to moe on opuci sw macierzyst funkcj i dotrze do tej, ktr j wywoaa. Jeli i tam nie znajdzie odpowiadajcego bloku catch, to wyjdzie jeszcze bardziej na powierzchni. W przypadku gdy i tam nie bdzie pasujcej instrukcji catch, bdzie wyskakiwa jeszcze wyej, i tak dalej. Proces ten nazywamy odwijaniem stosu (ang. stack unwinding) i trwa on dopki jaka instrukcja catch nie zapie leccego wyjtku. W skrajnym (i nieprawidowym) przypadku, odwijanie moe zakoczy si przerwaniem dziaania programu - mwimy wtedy, e wystpi niezapany wyjtek (ang. uncaught exception).
Zarwno o odwijaniu stosu, jak i o apaniu i niezapaniu wyjtkw bdziemy szerzej mwi w przyszym podrozdziale.
throw a return
Instrukcja throw jest troch podobna do instrukcji return, ktrej uywamy do zakoczenia funkcji i zwrcenia jej rezultatu. Istniej jednak wane rnice: return powoduje zawsze przerwanie tylko jednej funkcji i powrt do miejsca, z ktrego j wywoano. throw moe natomiast wcale nie przerywa wykonywania
442
Zaawansowane C++
funkcji (jeeli znajdzie w niej pasujc instrukcj catch), lecz rwnie dobrze moe przerwa dziaanie wielu funkcji, a nawet caego programu w przypadku return moliwe jest rzucenie obiektu nalecego tylko do jednego, cile okrelonego typu. Tym typem jest typ zwracany przez funkcj, okrelany w jej deklaracji. throw moe natomiast wyrzuca obiekt dowolnego typu, zalenie od potrzeb return jest normalnym sposobem powrotu z funkcji, ktry stosujemy we wszystkich typowych sytuacjach. throw jest za uywany w sytuacjach wyjtkowych; nie powinno si uywa go jako zamiennika dla return, bo przeznaczenie obu tych instrukcji jest inne
Wida wic, e mimo pozornego podobiestwa instrukcje te s zupenie rne. return jest typow instrukcj jzyka programowania, bez ktrej tworzenie programw byoby niemoliwe. throw jest z kolei czci wikszej caloci - mechanizmu obsugi wyjtkw bdcym po prostu specjalnym mechanizmem radzenia sobie z sytuacjami kryzysowymi. Mimo jej przydatnoci, stosowanie tej techniki nie jest obowizkowe. Skoro jednak mamy wybiera midzy uywaniem a nieuywaniem wyjtkw (a takich wyborw bdziesz dokonywa czsto), naley wiedzie o wyjtkach co wicej. Dlatego te kontynuujemy zajmowanie si tym tematem.
Waciwy chwyt
W poprzednich akapitach kilkakrotnie uywaem sformuowania pasujcy blok catch oraz odpowiednia instrukcja catch. C one znacz? Jedn z zalet mechanizmu wyjtkw jest to, e instrukcja throw moe wyrzuca obiekty dowolnego typu. Ponisze wiersze s wic cakowicie poprawne: throw throw throw throw 42u; "Straszny blad!"; CException("Wystapil wyjatek", __FILE__, __LINE__); 17.5;
Te cztery instrukcje throw rzucaj (odpowiednio) obiekty typw unsigned, const char[], zdefiniowanej przez uytkownika klasy CException oraz double. Wszystkie one s zapewne cennymi informacjami o bdach, ktre naleaoby odczyta w bloku catch. Niewykluczone przecie, e nawet najmniejsza pomoc z miejsca katastrofy moe by dla nas przydatna. Dlatego te w mechanizmie wyjtkw przewidziano sposb nie tylko na oddanie sterowania do bloku catch, ale te na przesanie tam jednego obiektu. Jest to oczywicie ten obiekt, ktry podajemy instrukcji throw. catch otrzymuje natomiast jego lokaln kopi - w podobny sposb, w jaki funkcje otrzymuj kopie przekazanych im parametrw. Aby jednak tak si stao, blok catch musi zadeklarowa, z jakiego typu obiektami chce pracowa: catch (typ obiekt) { kod } W ten sposb bedzie mia dostp do kadego zapanego obiektu wyjtku, ktry naley do podanego typu. Da mu to moliwo wykorzystania go - chociaby po to, aby wywietli uytkownikowi zawarte w nim informacje:
Wyjtki
try {
443
srand (static_cast<unsigned>(time(NULL))) // losujemy rzucony wyjtek switch (rand() % 4) { case 0: throw "Wyjatek tekstowy"; case 1: throw 1.5f; case 2: throw -12; case 3: throw (void*) NULL; }
} catch (int nZlapany) { std::cout << "Zlapalem wyjatek liczbowy z wartoscia " << nZlapany; } Komunikaty o bdach powinny by w zasadzie kierowane do strumienia cerr, a nie cout. Tutaj jednak, dla zachowania prostoty, bd posugiwa si standardowym strumieniem wyjcia. O pozostaych dwch rodzajach strumieni wyjciowych pomwimy w rozdziale o strumieniach STL. W tym kawaku kodu blok catch zapie liczb typu int - jeeli takowa zostanie wyrzucona przez instrukcj throw. Przechwyci j w postaci lokalnej zmiennej nZlapany, aby potem wywietli jej warto w konsoli. A co z pozostaymi wyjtkami? Nie mamy instrukcji catch, ktre by je apay. Wobec tego zostan one wyrzucone ze swej macierzystej funkcji i bd wdroway t ciek a do natrafienia pasujcych blokw catch. Jeeli ich nie znajd, spowoduj zakoczenie programu. Powinnimy zatem zapewni obsug take i tych wyjtkw. Robimy w taki sposb, i dopisujemy po prostu brakujce bloki catch: catch (const { std::cout } catch (float { std::cout } catch (void* { std::cout } char szNapis[]) << szNapis; fLiczba) << "Zlapano liczbe: " << fLiczba; pWskaznik) << "Wpadl wskaznik " << pWskaznik;
Blokw catch, nazywanych procedurami obsugi wyjtkw (ang. exception handlers), moe by dowolna ilo. Wszystko zaley od tego, ile typw wyjtkw zamierzamy przechwytywa.
444
Dopasowywanie typu obiektu wyjtku
Zamy wic, e mamy tak oto sekwencj try-catch: try {
Zaawansowane C++
W bloku try rzucamy jako wyjtek liczb 90. Poniewa nie podajemy jej adnych przyrostkw, kompilator uznaje, i jest to warto typu int. Nasz obiekt wyjtku jest wic obiektem typu int, ktry leci na spotkanie swego losu. Gdzie si zakoczy jego droga? Wszystko zaley od tego, ktry z trzech blokw catch przechwyci ten wyjtek. Wszystkie one s do tego zdolne: typ int pasuje bowiem zarwno do typu float, jak i double (no i oczywicie int). Mwic pasuje, mam tu na myli dokadnie taki sam mechanizm, jaki jest uruchamiany przy wywoywaniu funkcji z parametrami. Majc bowiem trzy funkcje: void Funkcja1(float); void Funkcja2(int); void Funkcja3(double); kadej z nich moemy przekaza warto typu int. Naturalnie, jest on najbardziej zgodna z Funkcja2(), ale pozostae te si do tego nadaj. W ich przypadku zadziaaj po prostu wbudowane, niejawne konwersje: kompilator zamieni liczb na int na typ float lub double. A jednak to tylko cz prawdy. Zgodno typu wyjtku z typem zadeklarowanym w bloku catch to tylko jedno z kryterium wyboru - w dodatku wcale nie najwaniejsze! Ot najpierw w gr wchodzi kolejno instrukcji catch. Kompilator przeglda je w takim samym porzdku, w jakim wystpuj w kodzie, i dla kadej z nich wykonuje test dopasowania argumentu. Jeli stwierdzi jakkolwiek zgodno (niekoniecznie najlepsz moliw), ignoruje wszystkie pozostae bloki catch i wybiera ten pierwszy pasujcy. Co to znaczy w praktyce? Spjrzmy na nasz przykad. Mamy obiekt typu int, ktry zostanie kolejno skonfrontowany z typami trzech blokw catch: float, int i double. Wobec przedstawionych wyej zasad, ktry z nich zostanie wybrany? Odpowied nie jest trudna. Ju pierwsze dopasowanie int do float zakoczy si sukcesem. Nie bdzie ono wprawdzie najlepsze (wymaga bdzie niejawnej konwersji), ale, jak podkresliem, kompilator poprzestanie wanie na nim. Porzdek blokw catch wemie po prostu gr nad ich zgodnoci. Pamitaj wic zasad dopasowywania typu obiektu rzuconego do wariantw catch: Typy w blokach catch s sprawdzane wedle ich kolejnoci w kodzie, a wybierana jest pierwsza pasujca moliwo. Przy dopasowywaniu brane s pod uwag wszystkie niejawne konwersje. Szczeglnie natomiast we sobie do serca, i: Kolejno blokw catch czsto ma znaczenie.
Wyjtki
445
Mimo e z pozoru przypominaj one funkcje, funkcjami nie s. Obowizuj w nich wic inne zasady wyboru waciwego wariantu.
Szczegy przodem
Jak w takim razie naley ustawia procedury obsugi wyjtkw, aby dziaay one zgodnie z naszymi yczeniami? Popatrzmy wpierw na taki przykad: try {
// ... throw 16u; // ... throw -87; // ... throw 9.242f; // ... throw 3.14157;
Pytanie powinno tutaj brzmie: co jest le na tym obrazku? Domylasz si, e chodzi o kolejno blokw catch. Sprawdmy. W bloku try rzucamy jeden z czterech wyjtkw - typu unsigned, int, float oraz double. Co si z nimi dzieje? Oczywicie trafiaj do odpowiednich blobkw catch czy aby na pewno? Niezupenie. Wszystkie te liczby zostan bowiem od razu dopasowane do pierwszego wariantu z parametrem double. Typ double swobodnie potrafi pomieci wszystkie cztery typy liczbowe, zatem wszystkie cztery wyjtkie trafi wycznie do pierwszego bloku catch! Pozostae trzy s w zasadzie zbdne! Kolejno procedur obsugi jest zatem nieprawidowa. Poprawnie powinny by one uoone w ten sposb: catch catch catch catch (unsigned uLiczba) (int nLiczba) (float fLiczba) (double fLiczba) { { { { /* /* /* /* ... ... ... ... */ */ */ */ } } } }
To gwarantuje, e wszystkie wyjtki trafi do tych blokw catch, ktre im dokadnie odpowiadaj. Korzystamy tu z faktu, e: typ unsigned w pierwszym bloku przyjmie tylko wyjtki typu unsigned typ int w drugim bloku mgby przej zarwno liczby typu unsigned, jak i int. Te pierwsz s jednak przechwycane przez poprzedni blok, zatem tutaj trafiaj wycznie wyjtki faktycznego typu int typ float moe przyj typy unsigned, int i float. Pierwsze dwa s ju jednak obsuone, wic ten blok catch dostaje tylko prawdziwe liczby zmiennoprzecinkowe pojedynczej precyzji typ double pasuje do kadej liczby, ale tutaj blok catch z tym typem dostanie jedynie te wyjtki, ktre s faktycznie typu double. Pozostae liczby zostan przechwycone przez poprzednie warianty Midzy typami unsigned, int, float i double zachodzi tu po prosta relacja polegajca na tym, e kady z nich jest szczeglnym przypadkiem nastpnego:
446
Zaawansowane C++
Najbardziej szczeglny jest typ unsigned i dlatego on wystpuje na pocztku. Dalej mamy ju coraz bardziej oglne typy liczbowe. Taka zasada konstrurowania sekwencji blokw catch jest poprawna w kadym przypadku, nie tylko dla typw liczbowych, Umieszczajc kilka blokw catch jeden po drugim, zadbaj o to, aby wystpoway one w porzdku rosncej oglnoci. Niech najpierw pojawi si bloki o najbardziej wyspecjalizowanych typach, a dopiero potem typy coraz bardziej oglne. Moesz krci nosem na takie niecise sformulowania. Bo i co to znaczy, e dany typ jest oglniejszy ni inny? W gr wchodz tu niejawne konwersje - jak wiemy, kompilator stosuje je przy dopasowywaniu w blokach catch. Mona zatem powiedzie, e: Typ A jest oglniejszy od typu B, jeeli istnieje niejawna konwersja z B do A, niepowodujca utraty danych. W tym sensie double jest oglniejszy od kadego z typw: unsigned, int i float, poniewa w kadym przypadku istniej niejawne konwersje standardowe, zamieniajce te typy na double. To zreszt zgodne ze zdrowym rozsdkiem i wiedz matematyczn, ktra mwi, nam e liczby naturalne i cakowite s take liczbami rzeczywistymi. Innym rodzajem konwersji, ktry bdzie nas interesowa w tym rozdziale, jest zamiana odwoania do obiektu klasy pochodnej na odwoanie do obiektu klasy bazowej. Uyjemy jej do budowy hierarchii klas dla wyjtkw.
ryzykowne_instrukcje_wewntrzne } catch (typ_wewntrzny_1 obiekt_wewntrzny_1) { wewntrzne_instrukcje_obsugi_1 } catch (typ_wewntrzny_2 obiekt_wewntrzny_2) { wewntrzne_instrukcje_obsugi_2 } // ... ryzykowne_instrukcje_zewntrzne } catch (typ_zewntrzny_1 obiekt_zewntrzny_1) { zewntrzne_instrukcje_obsugi_1
try {
Wyjtki
} catch (typ_zewntrzny_1 obiekt_zewntrzny_2) { zewntrzne_instrukcje_obsugi_2 } // ... dalsze_instrukcje
447
Mimo pozornego skomplikowania jej funkcjonowanie jest intuicyjne. Jeeli podczas wykonywania ryzykownych_instrukcji_wewntrznych rzucony zostanie wyjtek, to wpierw bdzie on apany przez wewntrzne bloki catch. Dopiero gdy one przepuszcz wyjtek, do pracy wezm si bloki zewntrzne. Jeeli natomiast ktry z zestaww catch (wewntrzny lub zewntrzny) wykona swoje zadanie, to program bdzie kontynuowa od nastpnych linijek po tym zestawie. Tak wic w przypadku, gdy wyjtek zapie wewntrzny zestaw, wykonywane bd ryzykowne_instrukcje_zewntrzne; jeli zewntrzny - dalsze_instrukcje. No a jeli aden wyjtek nie wystpi? Wtedy wykonaj si wszystkie instrukcje poza blokami catch, czyli: ryzykowne_instrukcje_wewntrzne, ryzykowne_instrukcje_zewntrzne i wreszcie dalsze_instrukcje. Takie dosowne zagniedanie blokw try-catch jest w zasadzie rzadkie. Czciej wewntrzny blok wystpuje w funkcji, ktrej wywoanie mamy w zewntrznym bloku. Oto przykad: void FunkcjaBiblioteczna() { try { // ... } catch (typ obiekt) { // ... } // ... } void ZwyklaFunkcja() { try { FunkcjaBiblioteczna(); // ... } catch (typ obiekt) { // ... } } Takie rozwizanie ma prost zalet: FunkcjaBiblioteczna() moe zapa i obsuy te wyjtki, z ktrymi sama sobie poradzi. Jeeli nie potrzeba angaowa w to wywoujcego, jest to dua zaleta. Cz wyjtkw najprawdopodobniej jednak opuci funkcj - tylko tymi bdzie musia zaj si wywoujcy. Wewntrzne sprawy wywoywanej funkcji (take wyjtki) pozostan jej wewntrznymi sprawami. Oglnie mona powiedzie, e:
448
Zaawansowane C++
Wyjtki powinny by apane w jak najbliszym od ich rzucenia miejscu, w ktrym moliwe jest ich obsuenie. O tej wanej zasadzie powiemy sobie jeszcze przy okazji uwag o wykorzystaniu wyjtkw.
Zapanie i odrzucenie
Przy zagniedaniu blokw try (niewane, czy z porednictwem funkcji, czy nie) moe wystpi czsta w praktyce sytuacja. Moliwe jest mianowicie, e po zapaniu wyjtku przez bardziej wewntrzny catch nie potrafimy podj wszystkich akcji, jakie byyby dla niego konieczne. Przykadowo, moemy tutaj jedynie zarejestrowa go w dzienniku bdw; bardziej uyteczn reakcj powinien zaj si kto wyej. Moglibymy pomin wtedy ten wewntrzny catch, ale jednoczenie pozbawilibymy si moliwoci wczesnego zarejestrowania bdu. Lepiej wic pozostawi go na miejscu, a po zakoczeniu zapisywania informacji o wyjtku wyrzuci go ponownie. Robimy to instrukcj throw bez adnych parametrw: throw; Ta instrukcja powoduje ponowne rzucenie tego samego obiektu wyjtku. Teraz jednak bd mogy zaj si nim bardziej zewntrzne bloki catch. Bd one pewnie bardziej kompetentne ni nasze siy szybkiego reagowania.
// instrukcje } catch (...) { // obsuga wyjtkw } Uniwersalno tego specjalnego rodzaju catch polega na tym, i pasuj do niego wszystkie obiekty wyjtkw. Jeeli kompilator, transportujc wyjtek, natrafi na catch(...), to bezwarunkowo wybierze wanie ten wariant, nie ogldajc si na adne inne. catch(...) jest wic wszystkoerny: pochania dowolne typy wyjtkw. Pochania to zreszt dobre sowo. Wewntrz bloku catch(...) nie mamy mianowicie adnych informacji o obiekcie wyjtku. Nie tylko o jego wartoci, ani nawet o jego typie. Wiemy jedynie, e jaki wyjtek wystpi - i skromn t wiedz musimy si zadowoli. Po co nam wobec tego taki dziwny blokcatch? Jest on przydatny tam, gdzie moemy jako wykorzysta samo powiadomienie o wyjtku, nie znajc jednak jego typu ani wartoci. Wewntrz catch(...) moemy jedynie podja pewne domylne dziaania. Moemy na przykad dokona maego zrzutu pamici (ang. memory dump), zapisujc w bezpiecznym miejscu wartoci zmiennych na wypadek zakoczenia programu. Moemy te w jaki sposb przygotowa si do waciwej obsugi bdw. Cokolwiek zrobimy, na koniec powinnimy przekaza wyjtek dalej, czyli uy konstrukcji: throw;
Wyjtki
449
Jeeli tego nie zrobimy, to catch(...) zdusi w zarodku wszelkie wyjtki, nie pozwalajc na to, by dotary one dalej. *** Na tym kocz si podstawowe informacje o mechanizmie wyjtkw. To jednak nie wszystkie aspekty tej techniki. Musimy sobie jeszcze porozmawia o tym, co dzieje si midzy rzuceniem wyjtku poprzez throw i jego zapaniem przy pomocy catch. Porozmawiamy zatem o odwijaniu stosu.
Odwijanie stosu
Odwijanie stosu (ang. stack unwinding) jest procesem cile zwizanym z wyjtkami. Jakkolwiek sama jego istota jest raczej prosta, musimy wiedze, jakie ma on konsekwencje w pisanym przez nas kodzie.
Wychodzenie na wierzch
Na czym jednak polega samo odwijanie? Ot mona opisa je w skrcie jako wychodzenie punktu wykonania ze wszystkich blokw kodu. Co to znaczy, najlepiej wyjani na przykadzie. Zamy, e mamy tak oto sytuacj: try {
} catch { // ... }
for (/* ... */) { switch (/* ... */) { case 1: if (/* ... */) { // ... throw obiekt; } } }
Instrukcja throw wystpuje to wewntrz 4 zagniedonych w sobie blokw: try, for, switch i if. My oczywicie wiemy, e najwaniejszy jest ten pierwszy, bo zaraz za nim wystpuje procedura obsugi wyjtku - catch. Co si dzieje z wykonywaniem programu, gdy nastpuje sytuacja wyjtkowa? Ot nie skacze on od razu do odpowiedniej instrukcji catch. Byoby to moe najszybsze z
450
Zaawansowane C++
punktu widzenia wydajnoci, ale jednoczenie cakowicie niedopuszczalne. Dlaczego tak jest - o tym powiemy sobie w nastpnym paragrafie. Jak wic postpuje kompilator? Rozpoczyna to sawetne odwijanie stosu, ktremu powicony jest cay ten podrozdzia. Dziaa to mniej wicej tak, jakby dla kadego bloku, w ktrym si aktualnie znajdujemy, zadziaaa instrukcja break. Powoduje to wyjcie z danego bloku. Po kadej takiej operacji jest poza tym sprawdzana obecno nastpujcego dalej bloku catch. Jeeli takowy jest obecny, i pasuje on do typu obiektu wyjtku, to wykonywana jest procedura obsugi wyjtku w nim zawarta. Proste i skuteczne :) Zobaczmy to na naszym przykadzie. Instrukcja throw znajduje si tu przede wszystkim wewntrz bloku if - i to on bdzie w pierwszej kolejnoci odwinity. Potem nie zostanie znaleziony blok catch, zatem opuszczone zostan take bloki switch, for i wreszcie try. Dopiero w tym ostatnim przypadku natrafimy na szukan procedur obsugi, ktra zostanie wykonana. Warto pamita, e - cho nie wida tego na przykadzie - odwijanie moe te dotyczy funkcji. Jeeli zajdzie konieczno odwinicia jej bloku, to sterowanie wraca do wywoujcego funkcj.
warto
zastosowanie
oglne programowanie
Wszystkie te trzy wasnoci trzech instrukcji s bardzo wane i koniecznie musisz o nich pamita. Nie bdzie to chyba dla ciebie problemem, skoro dwie z omawianych instrukcji znasz doskonale, a o wszystkich aspektach trzeciej porozmawiamy sobie jeszcze cakiem obszernie.
Wyjtki
451
Specyfikacja wyjtkw
Aby jednak mona byo to uczyni, naley wiedzie, jakiego typu wyjtki funkcja moe wyrzuca na zewntrz. Dziki temu moemy opakowa jej przywoanie w blok try i doda za nim odpowiednie instrukcje catch, chwytajce waciwe obiekty. Skd mamy uzyska t tak potrzebn wiedz? Wydawaoby si, e nic prostszego. Wystarczy przejrze kod funkcji, znale wszystkie instrukcje throw i okreli typ obiektw, jakie one rzucaj. Nastpnie naley odrzuci te, ktre s obsugiwane w samej funkcji i zaj si tylko wyjtkami, ktre z niej uciekaj. Ale to tylko teoria i ma ona jedn powan sabostk. Wymaga przecie dostpu do kodu rdowego funkcji, a ten nie musi by wcale osigalny. Wiele bibliotek jest dostarczanych w formie skompilowanej, zatem nie ma szans na ujrzenie ich wntrza. Mimo to ich funkcjom nikt cakowicie nie zabroni rzucania wyjtkw. Dlatego naleao jako rozwiza ten problem. Uzupeniono wic deklaracje funkcji o dodatkow informacj - specyfikacj wyjtkw. Specyfikacja albo wyszczeglnienie wyjtkw (ang. exceptions specification) mwi nam, czy dana funkcja wyrzuca z siebie jakie nieobsuone obiekty wyjtkw, a jeli tak, to informuje take o ich typach. Takie wyszczeglnienie jest czci deklaracji funkcji - umieszczamy je na jej kocu, np.: void Znajdz(int* aTablica, int nLiczba) throw(void*); Po licie parametrw (oraz ewentualnych dopiskach typu const w przypadku metod klasy) piszemy po prostu sowo throw. Dalej umieszczamy w nawiasie list typw wyjtkw, ktre bd opuszczay funkcj i ktrych zapanie bdzie naleao do obowizkw wywoujcego. Oddzielamy je przecinkami. Ta lista typw jest nieobowizkowa, podobnie zreszt jak caa fraza throw(). S to jednak dwa szczeglne przypadki - wygldaj one tak: void Stepuj(); void Spiewaj() throw(); Brak specyfikacji oznacza tyle, i dana funkcja moe rzuca na zewntrz wyjtki dowolnego typu. Natomiast podanie throw bez okrelenia typw wyjtkw informuje, e funkcja w ogle nie wyrzuca wyjtkw na zewntrz. Widzc tak zadeklarowan funkcj moemy wic mie pewno, e jej wywoania nie trzeba umieszcza w bloku try i martwi si o obsug wyjtkw przez catch. Specyfikacja wyjtkw jest czci deklaracji funkcji, zatem bdzie ona wystpowa np. w pliku nagwkowym zewntrznej biblioteki. Jest to bowiem niezbdna informacja, potrzebna do korzystania z funkcji - podobnie jak jej nazwa czy parametry. Kiedy jednak tamte wiadomoci podpowiadaj, w jaki sposb wywoywa funkcj, wyszczeglnienie throw() mwi nam, jakie wyjtki musimy przy okazji tego wywoania obsugiwa. Warto te podkreli, e mimo swej obecnoci w deklaracji funkcji, specyfikacja wyjtkw nie naley do typu funkcji. Do niego nadal zaliczamy wycznie list parametrw oraz typ wartoci zwracanej. Na pokazane wyej funkcje Stepuj() i Spiewaj() mona wic pokazywa tym samym wskanikiem.
452
Zaawansowane C++
Niestety, ycie i programowanie uczy nas, e niektre obietnice mog by tylko obiecankami. Zamy na przykad, e w nowej wersji biblioteki, z ktrej pochodzi funkcja, dokonano pewnych zmian. Teraz rzucany jest jeszcze jeden, nowy typ wyjtkw, ktrego obsuga spada na wywoujcego. Zapomniano jednak zmieni deklaracj funkcji - wyglda ona nadal np. tak: bool RobCos() throw(std::string); Obiecywanym typem wyjtkw jest tu tylko i wycznie std::string. Przypumy jednak, e w wyniku poczynionych zmian funkcja moe teraz rzuca take liczby typu int - typu, ktrego nazwa nie wystpuje w specyfikacji wyjtkw. Co si wtedy stanie? Czy wystpi bd? Powiedzmy. Jednak to nie kompilator nam o nim powie. Nie zrobi tego nawet linker. Ot: O rzuceniu przez funkcj niezadeklarowanego wyjtku dowiemy si dopiero w czasie dziaania programu. Wyglda to tak, i program wywoa wtedy specjaln funkcj unexpected() (niespodziewany). Jest to funkcja biblioteczna, uruchamiana w reakcji na niedozwolony wyjtek. Co robi ta funkcja? Ot wywouje ona drug funkcj, terminate() (przerwij). O niej bdziemy jeszcze rozmawia przy okazji niezapanych wyjtkw. Na razie zapamitaj, e funkcja ta po prostu koczy dziaanie programu w mao porzdny sposb. Wyrzucenie przez funkcj niezadeklarowanego wyjtku koczy si awaryjnym przerwaniem dziaania programu. Spytasz pewnie: Dlaczego tak drastycznie? Taka reakcja jest jednak uzasadniona, gdy do czynienia ze zwyczajnym oszustwem. Oto kto (twrca funkcji) deklaruje, e bdzie ona wystrzeliwa z siebie wycznie okrelone typy wyjtkw. My posusznie podporzdkowujemy si tej obietnicy: ujmujemy wywoanie funkcji w blok try i piszemy odpowiednie bloki catch. Wszystko robimy zgodnie ze specyfikacj throw(). Tymczasem zostajemy oszukani. Obietnica zostaa zamana: funkcja rzuca nam wyjtek, ktrego si zupenie nie spodziewalimy. Nie mamy wic kodu jego obsugi albo nawet gorzej: mamy go, ale nie tam gdzie trzeba. W kadym przypadku jest to sytuacja nie do przyjcia i stanowi wystarczajc podstaw do zakoczenia dziaania programu. To domylne moemy aczkolwiek zmieni. Nie zaleca si wprawdzie, aby mimo niespodziewanego wyjtku praca programu bya kontynuowana. Jeeli jednak napiszemy wasn wersj funkcji unexpected(), bdziemy mogli odrni dwie sytuacje: niezapany wyjtek - czyli taki wyjtek, ktrego nie schwyci aden blok catch nieprawidowy wyjtek - taki, ktry nie powinien si wydosta z funkcji Rnica jest bardzo wana, bowiem w tym drugim przypadku nie jestemy winni zaistniaemu problemu. Dokadniej mwic, nie jest winny kod wywoujcy funkcj przyczyna tkwi w samej funkcji, a zawini jej twrca. Jego obietnice dotyczce wyjtkw okazay si obietnicami bez pokrycia. Rozdzielenie tych dwch sytuacji pozwoli nam uchroni si przed poprawianiem kodu, ktry by moe wcale tego nie wymaga. Z powodu niezadeklarowanego wyjtku nie ma bowiem potrzeby dokonywania zmian w kodzie wywoujcym funkcj. Pniej bd one oczywicie konieczne; pniej - to znaczy wtedy, gdy powiadomimy twrc funkcj o jego niekompetencji, a ten z pokor naprawi swj bd.
Wyjtki
453
Jak zatem moemy zmieni domyln funkcj unexpected()? Czynimy to wywoujc inn funkcj - set_unexpected(): unexpected_handler set_unexpected(unexpected_handler pfnFunction); Tym, ktry ta funkcja przyjmuje i zwraca, to unexpected_handler. Jest to alias ta wskanik do funkcji: takiej, ktra nie bierze adnych parametrw i nie zwraca adnej wartoci. Poprawn wersj funkcji unexpected() moe wic by np. taka funkcja: void MyUnexpected() { std::cout << "--- UWAGA: niespodziewany wyjtek ---" << std::endl; exit (1); } Po przekazaniu jej do set_unexpected(): set_unexpected (MyUnexpected); bdziemy otrzymywali stosown informacj w przypadku wyrzucenia niedozwolonego wyjtku przez jakkolwiek funkcj programu.
Niezapany wyjtek
Przekonalimy si, e proces odwijania stosu moe doprowadzi do przerwania dziaania funkcji i poznalimy tego konsekwencje. Nieprawidowe sygnalizowanie lub obsuga wyjtkw mog nam jednak sprawi jeszcze jedn niespodziank. Odwijanie moe si mianowicie zakoczy niepowodzeniem, jeli aden pasujcy blok catch nie zostanie znaleziony. Mwimy wtedy, e wystpi nieobsuony wyjtek. Co nastpuje w takim wypadku? Ot program wywouje wtedy funkcj terminate(). Jej nazwa wskazuje, e powoduje ona przerwanie programu. Faktycznie funkcja ta wywouje inn funkcj - abort() (przesta). Ona za powoduje brutalne i nieznoszce adnych kompromisw przerwanie dziaania programu. Po jej wywoaniu moemy w oknie konsoli ujrze komunikat:
Abnormal program termination
Taki te napis bdzie poegnaniem z programem, w ktrym wystpi niezapany wyjtek. Moemy to jednak zmieni, piszc wasn wersj funkcji terminate(). Do ustawienia nowej wersji suy funkcja set_terminate(). Jest ona bardzo podobna do analogicznej funkcji set_unexpected(): terminate_handler set_terminate(terminate_handler pfnFunction); Wystpujcy tu alias terminate_handler jest take wskanikiem na funkcj, ktra nic nie bierze i nie zwraca. W parametrze set_terminate() podajemy wic wskanik do nowej funkcji terminate(), a w zamian otrzymujemy wskanik do starej - zupenie jak w set_unexpected(). Oto przykadowa funkcja zastpcza: void MyTerminate() { std::cout << "--- UWAGA: blad mechanizmu wyjatkow ---" << std::endl;
454
exit (1);
Zaawansowane C++
Wypisywany przez nas komunikat jest tak oglny (nie brzmi np. "niezlapany wyjatek"), poniewa terminate() jest wywoywana take w nieco innych sytuacjach, ni niezapany wyjtek. Powiemy sobie o nich we waciwym czasie. Zastosowana tutaj, jak w i MyUnexpected() funkcja exit() suy do normalnego (a nie awaryjnego) zamknicie programu. Podajemy jej tzw. kod wyjcia (ang. exit code) zwyczajowo zero oznacza wykonanie bez bdw, inna warto to nieprawidowe dziaanie aplikacji (tak jak u nas).
Porzdki
Odwijanie stosu jest w praktyce bardziej zoonym procesem ni to si wydaje. Oprcz przetransportowania obiektu wyjtku do stosownego bloku catch kompilator musi bowiem zadba o to, aby reszta programu nie doznaa przy okazji jakich obrae. O co chodzi? O tym porozmawiamy sobie w tym paragrafie.
Wyjtki
Ot nie - jest to dozwolone, ale w sumie nie o tym chcemy mwi :) Musimy sobie powiedzie, co rozumiemy poprzez obsug wyjtku dokonywan przez kompilator.
455
Dla nas obsug wyjtku jest kod w bloku catch. Aby jednak mg on by wykonany, obiekt wyjtku oraz punkt sterowania programu musz tam trafi. Tym zajmuje si kompilator - to jest wanie jego obsuga wyjtku: dostarczenie go do bloku catch. Dalej nic go ju nie obchodzi: kod z bloku catch jest traktowany jako normalne instrukcje, bowiem sam kompilator uznaje ju, e z chwil rozpoczcia ich wykonywania jego praca zostaa wykonana. Wyjtek zosta przyniesiony i to si liczy. Tak wic: Obsuga wyjtku dokonywana przez kompilator polega na jego dostarczeniu go do odpowiedniego bloku catch przy jednoczesnym odwiniciu stosu. Teraz ju wiemy, na czym polega zastrzeenie podane na pocztku. Nie moemy rzuci nastpnego wyjtku w chwili, gdy kompilator zajmuje si jeszcze transportem poprzedniego. Inaczej mwic, midzy wykonaniem instrukcji throw a obsug wyjtku w bloku catch nie moe wystapi nastpna instrukcja throw.
Strefy bezwyjtkowe
No dobrze, ale waciwie co z tego? Przecie po rzuceniu jednego wyjtku wszystkim zajmuje si ju kompilator. Jak wic moglibymy rzuci kolejny wyjtek, zanim ten pierwszy dotrze do bloku catch? Faktycznie, tak mogoby si wydawa. W rzeczywistoci istniej a dwa miejsca, z ktrych mona rzuci drugi wyjtek. Jeli chodzi o pierwsze, to pewnie si go domylasz, jeeli uwanie czytae opis procesu odwijania stosu i zwizanego z nim niszczenia obiektw lokalnych. Powiedziaem tam, e przebiega ono w identyczny sposb, jak normalnie. Pami jest zawsze zwalniania, a w przypadku obiektw klas wywoywane s destruktory. Bingo! Destruktory s wanie tymi procedurami, ktre s wywoywane podczas obsugi wyjtku dokonywanej przez kompilator. A zatem nie moemy wyrzuca z nich adnych wyjtkw, poniewa moe zdarzy, e dany destruktor jest wywoywany podczas odwijania stosu. Nie rzucaj wyjtkw z destruktorw. Druga sytuacja jest bardziej specyficzna. Wiemy, e mechanizm wyjtkw pozwala na rzucanie obiektw dowolnego typu. Nale do nich take obiekty klas, ktre sami sobie zdefiniujemy. Definiowanie takich specjalnych klas wyjtkw to zreszt bardzo podana i rozsdna praktyka. Pomwimy sobie jeszcze o niej. Jednak niezalenie od tego, jakiego rodzaju obiekty rzucamy, kompilator z kadym postpuje tak samo. Podczas transportu wyjtku do catch czyni on przynajmniej jedn kopi obiektu rzucanego. W przypadku typw podstawowych nie jest to aden problem, ale dla klas wykorzystywane s normalne sposoby ich kopiowania. Znaczy to, e moe zosta uyty konstruktor kopiujcy - nasz wasny. Mamy wic drugie (i na szczcie ostatnie) potencjalne miejsce, skd mona rzuci nowy wyjtek w trakcie obsugi starego. Pamitajmy wic o ostrzeeniu: Nie rzucajmy nowych wyjtkw z konstruktorw kopiujcych klas, ktrych obiekty rzucamy jako wyjtki. Z tych dwch miejsc (wszystkie destruktory i konstruktory kopiujce obiektw rzucanych) nie powinnimy rzuca adnych wyjtkw. W przeciwnym wypadku kompilator uzna to za bardzo powany bd. Zaraz si przekonamy, jak powany
456
Zaawansowane C++
Biblioteka Standardowa udostpnia prost funkcj uncaught_exception(). Zwraca ona true, jeeli kompilator jest w trakcie obsugi wyjtku. Mona jej uy, jeli koniecznie musimy rzuci wyjtek w destruktorze; oczywicie powinnimy to zrobi tylko wtedy, gdy funkcja zwrci false. Prototyp tej funkcji znajduje si w pliku nagwkowym exception w przestrzeni nazw std.
Skutki wypadku
Co si stanie, jeeli zignorujemy ktry z zakazw podanych wyej i rzucimy nowy wyjtek w trakcie obsugi innego? Bdzie to wtedy bardzo powana sytuacja. Oznacza ona bdzie, e kompilator nie jest w stanie poprawnie przeprowadzi obsugi wyjtku. Inaczej mwic, mechanizm wyjtkw zawiedzie - tyle e bdzie to rzecz jasna nasza wina. Co moe wwczas zrobi kompilator? Niewiele. Jedyne, co wtedy czyni, to wywoanie funkcji terminate(). Skutkiem jest wic nieprzewidziane zakoczenie programu. Naturalnie, zmiana funkcji terminate() (poprzez set_terminate()) sprawi, e zamiast domylnej bdzie wywoywana nasza procedura. Piszc j powinnimy pamita, e funkcja terminate() jest wywoywana w dwch sytuacjach: gdy wyjtek nie zosta zapany przez aden blok catch gdy zosta rzucony nowy wyjtek w trakcie obsugi poprzedniego Obie s sytuacjami krytycznymi. Zatem niezalenie od tego, jakie dodatkowe akcje bdziemy podejmowa w naszej funkcji, zawsze musimy na koniec zamkn nasz program. W aplikacjach konsolowych mona uczyni to poprzez exit().
Wyjtki
delete pFoo; // zwolnienie obiektu-zasobu
457
Midzy stworzeniem a zniszczeniem obiektu moe jednak zaj sporo zdarze. W szczeglnoci: moliwe jest rzucenie wyjtku. Co si wtedy stanie? Wydawa by si mogo, e obiekt zostanie zniszczony, bo przecie tak byo zawsze Bd! Obiekt, na ktry wskazuje pFoo nie zostanie zwolniony z prostego powodu: nie jest on obiektem lokalnym, rezydujcym na stosie, lecz tworzonym dynamicznie na stercie. Sami wydajemy polecenie jego utworzenia (new), wic rwnie sami musimy go potem usun (poprzez delete). Zostanie natomiast zniszczony wskanik na niego (zmienna pFoo), bo jest to zmienna lokalna - co aczkolwiek nie jest dla nas adn korzyci. Moesz zapyta: A w czym problem? Skoro pami naley zwolni, to zrbmy to przed rzuceniem wyjtku - o tak: try {
CFoo* pFoo = new CFoo; // ... if (warunek_rzucenia_wyjtku) { delete pFoo; throw wyjtek; } // ...
delete pFoo; } catch (typ obiekt) { // ... } To powinno rozwiza problem. Taki sposb to jednak oznaka skrajnego i niestety nieuzasadnionego optymizmu. Bo kto nam zagwarantuje, e wyjtki, ktre mog nam przeszkadza, bd rzucane wycznie przez nas? Moemy przecie wywoa jak zewntrzn funkcj, ktra sama bdzie wyrzucaa wyjtki - nie pytajc nas o zgod i nie baczc na nasz zaalokowan pami, o ktrej przecie nic nie wie! To te nie katastrofa, odpowiesz, Moemy przecie wykry rzucenie wyjtku i w odpowiedzi zwolni pami: try {
CFoo* pFoo = new CFoo; // ... try { // wywoanie funkcji potencjalnie rzucajcej wyjtki FunkcjaKtoraMozeWyrzucicWyjatek(); } catch (...) { // niszczymy obiekt delete pFoo; // rzucamy dalej otrzymany wyjtek
458
throw; } // ... delete pFoo; } catch (typ obiekt) { // ... }
Zaawansowane C++
Blok catch(...) zapie nam wszystkie wyjtki, a my w jego wntrzu zwolnimy pami i rzucimy je dalej poprzez throw;. Wszystko proste, czy nie? Brawo, twoja pomysowo jest cakiem dua. Ju widz te dziesitki wywoa funkcji bibliotecznych, zamknitych w ich wasne bloki try-catch(...), ktre dbaj o zwalnianie pamici Jak sdzisz, na ile eleganckie, efektywne (zarwno pod wzgldem czasu wykonania jak i zakodowania) i atwe w konserwacji jest takie rozwizanie? Jeeli zastanowisz si nad tym cho troch dusz chwil, to zauwaysz, e to bardzo ze wyjcie. Jego stosowanie (podobnie zreszt jak delete przed throw) jest wiadectwem koszmarnego stylu programowania. Pomylmy tylko, e wymaga to wielokrotnego napisania instrukcji delete - powoduje to, e kod staje si bardzo nieczytelny: na pierwszy rzut oka mona pomyle, e kilka(nacie) razy usuwany jest obiekt, ktry tworzymy tylko raz. Poza tym obecno tego samego kodu w wielu miejscach znakomicie utrudnia jego zmian. By moe teraz pomylae o preprocesorze i jego makrach Jeli naprawd chciaby go zastosowa, to bardzo prosz. Potem jednak nie narzekaj, e wyprodukowae kod, ktry stanowi zagadk dla jasnowidza. Teraz moesz si oburzy: No to co naley zrobi?! Przecie nie moemy dopuci do powstawania wyciekw pamici czy niezamykania plikw! Moe naley po prostu zrezygnowa z tak nieprzyjaznego narzdzia, jak wyjtki? C, moemy nie lubi wyjtkw (szczeglnie w tej chwili), ale nigdy od nich nie uciekniemy. Jeeli sami nie bdziemy ich stosowa, to uyje ich kto inny, ktrego kodu my bdziemy potrzebowali. Na wyjtki nie powinnimy si wic obraa, lecz sprbowa je zrozumie. Rozwizanie problemu zasobw, ktre zaproponowalimy wyej, jest ze, poniewa prbuje wtrci si w automatyczny proces odwijania stosu ze swoim rcznym zwalnianiem zasobw (tutaj pamici). Nie tdy droga; naley raczej zastosowa tak metod, ktra pozwoli nam czerpa korzyci z automatyki wyjtkw. Teraz poznamy waciwy sposb dokonania tego. Problem z niezwolnionymi zasobami wystpuje we wszystkich jzykach, w ktrych funkcjonuj wyjtki. Trzeba jednak przyzna, e w wikszoci z nich poradzono sobie z nim znacznie lepiej ni w C++. Przykadowo, Java i Object Pascal posiadaj moliwo zdefiniowania dodatkowego (obok catch) bloku finally (nareszcie). W nim zostaje umieszczany kod wykonywany zawsze - niezalenie od tego, czy wyjtek w try wystpi, czy te nie. Jest to wic idealne miejsce na instrukcje zwalniajce zasoby, pozyskane w bloku try. Mamy bowiem gwarancj, i zostan one poprawnie oddane niezalenie od okolicznoci.
Opakowywanie
Pomys jest do prosty. Jak wiemy, podczas odwijania stosu niszczone s wszystkie obiekty lokalne. W przypadku, gdy s to obiekty naszych wasnych klas, do pracy ruszaj wtedy destruktory tych klas. Wanie we wntrzu tych destruktorw moemy umieci kod zwalniajcy przydzielon pami czy jakikolwiek inny zasb.
Wyjtki
459
Wydaje si to podobne do rcznego zwalniania zasobw przed rzuceniem wyjtku lub w blokach catch(...). Jest jednak jedna bardzo wana rnica: nie musimy tutaj wiedzie, w ktrym dokadnie miejscu wystpi wyjtek. Kompilator bowiem i tak wywoa destruktor obiektu - niewane, gdzie i jaki wyjtek zosta rzucony. Skoro jednak mamy uywa destruktorw, to trzeba rzecz jasna zdefiniowa jakie klasy. Potem za naley w bloku try tworzy obiekty tyche klas, by ich destruktory zostay wywoane w przypadku wyrzucenia jakiego wyjtku. Jak to naley uczyni? Kwestia nie jest trudna. Najlepiej jest zrobi tak, aby dla kadego pojedynczego zasobu (jak zaalokawany blok pamici, otwarty plik, itp.) istnia jeden obiekt. W momencie zniszczenia tego obiektu (z powodu rzucenia wyjtku) zostanie wywoany destruktor jego klasy, ktry zwolni zasb (czyli np. usunie pami albo zamknie plik).
Destruktor wskanika?
To bardzo proste, prawda? ;) Ale eby byo jeszcze atwiejsze, spjrzmy na prosty przykad. Zajmiemy si zasobem, ktry najbardziej znamy, czyli pamici operacyjn; oto przykad kodu, ktry moe spowodowa jej wyciek: try {
CFoo* pFoo = new CFoo; // ... throw "Cos sie stalo"; // obiekt niezwolniony, mamy wyciek!
} // (tutaj catch)
Przyczyna jest oczywicie taka, i odwijanie stosu nie usunie obiektu zaalokowanego dynamicznie na stercie. Usunity zostanie rzecz jasna sam wskanik (czyli zmienna pFoo), ale na tym si skoczy. Kompilator nie zajmie si obiektem, na ktry w wskanik pokazuje. Zapytasz: A czemu nie? Przecie mgby to zrobi. Pomyl jednak, e nie musi to by wcale jedyny wskanik pokazujcy na dynamiczny obiekt. W przypadku usunicia obiektu wszystkie pozostae stayby si niewane. Oprcz tego byoby to zamanie zasady, i obiekty stworzone jawnie (poprzez new) musz by take jawnie zniszczone (przez delete). My jednak chcielibymy, aby wraz z kocem ycia wskanika skoczy si take ywot pamici, na ktr on pokazuje. Jak mona to osign? C, gdyby nasz wskanik by obiektem jakiej klasy, wtedy moglibymy napisa instrukcj delete w jej destruktorze. Tak jest jednak nie jest: wskanik to typ wbudowany118, wic nie moemy napisa dla destruktora - podobnie jak nie moemy tego zrobi dla typu int czy float.
Sprytny wskanik
Wskanik musiaby wic by klas Dlaczego nie? Podkrelaem w zeszym rozdziale, e klasy w C++ s tak pomylane, aby mogy one naladowa typy podstawowe. Czemu zatem nie monaby stworzy sobie takiej klasy, ktra dziaaby jak wskanik - typ
118 Wskanik moe wprawdzie pokazywa na typ zdefiniowany przez uytkownika, ale sam zawsze bdzie typem wbudowanym. Jest to przecie zwyka liczba - adres w pamici.
460
Zaawansowane C++
wbudowany? Wtedy mielibymy pen swobod w okreleniu jej destruktora, a take innych metod. Oczywicie, nie my pierwsi wpadlimy na ten pomys. To rozwizanie jest szeroko znane i nosi nazw sprytnych wskanikw (ang. smart pointers). Takie wskaniki s podobne do zwykych, jednak przy okazji oddaj jeszcze pewne dodatkowe przysugi. W naszym przypadku chodzi o dbao o zwolnienie pamici w przypadku wystpienia wyjtku. Sprytny wskanik jest klas. Ma ona jednak odpowiednio przecione operatory - tak, e korzystanie z jej obiektw niczym nie rni si od korzystania z normalnych wskanikw. Popatrzmy na znany z zeszego rozdziau przykad: class CFooSmartPtr { private: // opakowywany, waciwy wskanik CFoo* m_pWskaznik; public: // konstruktor i destruktor CFooSmartPtr(CFoo* pFoo) : m_pWskaznik(pFoo) { } ~CFooSmartPtr() { if (m_pWskaznik) delete m_pWskaznik; } // ------------------------------------------------------------// operator dereferencji CFoo& operator*() { return *m_pWskaznik; } // operator wyuskania CFoo* operator->() { return m_pWskaznik; }
};
Jest to inteligentny wskanik na obiekty klasy CFoo; docelowy typ jest jednak nieistotny, bo rwnie dobrze monaby pokazywa na liczby typu int czy te inne obiekty. Wana jest zasada dziaania - zupenie nieskomplikowana. Klasy CFooSmartPtr uywamy po prostu zamiast typu CFoo*: try {
CFooSmartPtr pFoo = new CFoo; // ... throw "Cos sie stalo"; // niszczony obiekt pFoo i wywoywany destruktor CFooSmartPtr
} // (tutaj catch)
Dziki przecieniu operatorw korzystamy ze sprytnego wskanika dokadnie w ten sam sposb, jak ze zwykego. Poza tym rozwizujemy problem ze zwolnieniem pamici: zajmuje si tym destruktor klasy CFooSmartPtr. Stosuje on operator delete wobec waciwego, wewntrznego wskanika (typu normalnego, czyli CFoo*), usuwajc stworzony dynamicznie obiekt. Robi niezalenie od tego, gdzie i kiedy (i czy) wystpi jakikolwiek wyjtek. Wystarczy, e zostanie zlikwidowany obiekt pFoo, a to pocignie za sob zwolnienie pamici. I o to nam wanie chodzio. Wykorzystalimy mechanizm odwijania stosu do zwolnienia zasobw, ktre normalnie byyby pozostawione same sobie. Nasz problem zosta rozwizany.
Wyjtki
461
Nieco uwag
Aby jednak nie byo a tak bardzo piknie, na koniec paragrafu musz jeszcze troch pogldzi :) Chodzi mianowicie o dwie wane sprawy zwizane ze sprytnymi wskanikami, ktrych uywamy w poczeniu z mechanizmem wyjtkw.
Co ju zrobiono za nas
Metoda opakowywania zasobw moe si wydawa nazbyt praco- i czasochonna, a przede wszystkim wtrna. Stosujc j pewnie szybko zauwayby, e napisane przez ciebie klasy powinny by obecne w niemal kadym programie korzystajcym z wyjtkw. Naturalnie, mog by one dobrym punktem wyjcia dla twojej wasnej biblioteki z przydatnymi kodami, uywanymi w wielu aplikacjach. Niewykluczone, e kiedy bdziesz musia napisa przynajmniej kilka takich klas-opakowa, jeeli zechcesz skorzysta z zasobw innych ni pami operacyjna czy pliki dyskowe. Na razie jednak lepiej chyba sprawdz si narzdzia, ktre otrzymujesz wraz z jzykiem C++ i jego Bibliotek Standardow. Zobaczmy pokrtce, jak one dziaaj; ich dokadny opis znajdziesz w kolejnych rozdziaach, powiconych samej tylko Bibliotece Standardowej.
Klasa std::auto_ptr
Sprytne wskaniki chronice przed wyciekami pamici, powstajcymi przy rzucaniu wyjtkw, s do czsto uywane w praktyce. Samodzielne ich definiowanie byoby wic uciliwe. W C++ mamy wic ju stworzon do tego klas std::auto_ptr. cilej mwic, auto_ptr jest szablonem klasy. Co to dokadnie znaczy, dowiesz si w nastpnym rozdziale. Pki co bdziesz wiedzia, i pozwala to na uywanie auto_ptr w charakterze wskanika do dowolnego typu danych. Nie musimy ju zatem definiowia adnych klas. Aby skorzysta z auto_ptr, trzeba jedynie doczy standardowy plik nagwkowy memory:
462
#include <memory>
Zaawansowane C++
Teraz moemy ju korzysta z tego narzdzia. Z powodzeniem moe ono zastpi nasz pieczoowicie wypracowan klas CFooSmartPtr: try {
std::auto_ptr<CFoo> pFoo(new CFoo); // ... throw "Cos sie stalo"; // przy niszczeniu wskanika auto_ptr zwalniana jest pami
} // (tutaj catch)
Konstrukcja std::auto_ptr<CFoo> pewnie wyglda nieco dziwnie, ale atwo si do niej przyzwyczaisz, gdy ju poznasz szablony. Mona z niej take wydedukowa, e w nawiasach ktowych <> podajemy typ danych, na ktry chcemy pokazywa poprzez auto_ptr - tutaj jest to CFoo. atwo domyli si, e chcc mie wskanik na typ int, piszemy std::auto_ptr<int>, itp. Zwrmy jeszcze uwag, w jaki sposob umieszcza si instrukcj new w deklaracji wskanika. Z pewnych powodw, o ktrych nie warto tu mwi, konstruktor klasy auto_ptr jest opatrzony swkiem explicit. Dlatego te nie mona uy znaku =, lecz trzeba jawnie przekaza parametr, bdcy normalnym wskanikiem do zaalokowanego poprzez new obszaru pamici. W sumie wic skadnia deklaracji wskanika auto_ptr wyglda tak: std::auto_ptr<typ> wskanik(new typ[(parametry_konstruktora_typu)]); O zwolnienie pamici nie musimy si martwi. Destruktor auto_ptr usunie j zawsze, niezalenie od tego, czy wyjtek faktycznie wystpi.
// stworzenie strumienia i otwarcie pliku do zapisu std::ofstream Plik("plik.txt", ios::out); // zapisanie czego do pliku Plik << "Co"; // ... throw "Cos sie stalo"; // strumie jest niszczony, a plik zamykany
} // (tutaj catch)
Wyjtki
Plik reprezentowany przez strumie Plik zostanie zawsze zamknity. W kadym przypadku - wystpienia wyjtku lub nie - wywoany bowiem bdzie destruktor klasy ofstream, a on tym si wanie zajmie. Nie trzeba wic martwi si o to. *** Tak zakoczymy omawianie procesu odwijania stosu i jego konsekwencji. Teraz zobaczysz, jak w praktyce powinno si korzysta z mechanizmu wyjtkw w C++.
463
Wykorzystanie wyjtkw
Dwa poprzednie podrozdziay mwiy o tym, czym s wyjtki i jak dziaa ten mechanizm w C++. W zasadzie na tym monaby poprzesta, ale taki opis na pewno nie bdzie wystarczajcy. Jak kady element jzyka, take i wyjtki naley uywa we waciwy sposb; korzystaniu z wyjtkw w praktyce zostanie wic powicony ten podrozdzia.
Wyjtki w praktyce
Zanim z pieni na ustach zabierzemy si do wykorzystywania wyjtkw, musimy sobie odpowiedzie na jedno fundamentalne pytanie: czy tego potrzebujemy? Takie postawienie sprawy jest pewnie zaskakujce - dotd wszystkie poznawane przez nas elementy C++ byy waciwie niezbdne do efektywnego stosowania tego jzyka. Czy z wyjtkami jest inaczej? Przyjrzyjmy si sprawie bliej Moe powiedzmy sobie o dwch podstawowych sytuacjach, kiedy wyjtkw nie powinnimy stosowa. W zasadzie mona je zamkn w jedno stwierdzenie: Nie powinno si wykorzystywa wyjtkw tam, gdzie z powodzeniem wystarczaj inne techniki sygnalizowania i obsugi bdw. Oznacza to, e: nie powinnimy na si dodawa wyjtkw do istniejcego programu. Jeeli po przetestowaniu dziaa on dobrze i efektywnie bez wyjtkw, nie ma adnego powodu, aby wprowadza do kodu ten mechanizm dla tworzonych od nowa, lecz krtkich programw wyjtki mog by zbyt potnym narzdziem. Wysiek woony w jego zaprogramowanie (jak si zaraz przekonamy - wcale niemay) nie musi si opaca. Co oznacza pojcie krtki program, to ju kady musi sobie odpowiedzie sam; zwykle uwaa si, e krtkie s te aplikacje, ktre nie przekraczaj rozmiarami 1000-2000 linijek kodu Wida wic, e nie kady program musi koniecznie stosowa ten mechanizm. S oczywicie sytuacje, gdy oby si bez niego jest bardzo trudno, jednak naduywanie wyjtkw jest zazwyczaj gorsze ni ich niedostatek. O obu sprawach (korzyciach pyncych z wyjtkw i ich przesadnemu stosowaniu) powiemy sobie jeszcze pniej. Zamy jednak, e zdecydowalimy si wykorzystywa wyjtki. Jak poprawnie zrealizowa te intencje? Jak wikszo rzeczy w programowaniu, nie jest to trudne :) Musimy mianowicie: pomyle, jakie sytuacje wyjtkowe mog wystpi w naszej aplikacji i wyrni wrd nich poszczeglne rodzaje, a nawet pewn hierarchi. To pozwoli na stworzenie odpowiednich klas dla obiektw wyjtkw, czym zajmiemy si w pierwszym paragrafie
464
Zaawansowane C++
we waciwy sposb zorganizowa obsug wyjtkw - chodzi gwnie o rozmieszczenie blokw try i catch. Ta kwestia bdzie przedmiotem drugiego paragrafu
Potem moemy ju tylko mie nadziej, e nasza ciko wykonana praca nigdy nie bdzie potrzebna. Najlepiej przecie byoby, aby sytuacje wyjtkowe nie zdarzay si, a nasze programy dziaay zawsze zgodnie z zamierzeniami C, praca programisty nie jest usana rami, wic tak nigdy nie bdzie. Nauczmy si wic poprawnie reagowa na wszelkiego typu nieprzewidziane zdarzenia, jakie mog si przytrafi naszym aplikacjom.
Definiujemy klas
Co wic powinien zawiera taki obiekt? Najwaniejsze jest ustalenie rodzaju bdu oraz miejsca jego wystpienia w kodzie. Typowym zestawem danych dla wyjtku moe by zatem: nazwa pliku z kodem i numer wiersza, w ktrym rzucono wyjtek. Do tego mona doda jeszcze dat kompilacji programu, aby rozrni jego poszczeglne wersje dane identyfikacyjne bdu - w najprostszej wersji tekstowy komunikat Nasza klasa wyjtku mogaby wic wyglda tak: #include <string> class CException { private: // dane wyjtku std::string m_strNazwaPliku; unsigned m_uLinijka; std::string m_strKomunikat; public: // konstruktor CException(const std::string& strNazwaPliku, unsigned uLinijka, const std::string& strKomunikat) : m_strNazwaPliku(strNazwaPliku), m_uLinijka(uLinijka), m_strKomunikat(strKomunikat)
{ }
// ------------------------------------------------------------// metody dostpowe std::string NazwaPliku() const unsigned Linijka() const std::string Komunikat() const { return m_strNazwaPliku; } { return m_uLinijka; } { return m_strKomunikat; }
};
Do obszerny konstruktor pozwala na podanie wszystkich danych za jednym zamachem, w instrukcji throw:
Wyjtki
465
throw CException(__FILE__, __LINE__, "Cos sie stalo"); Dla wygody mona sobie nawet zdefiniowa odpowiednie makro, jako e __FILE__ i __LINE__ pojawi si w kadej instrukcji rzucenia wyjtku. Jest to szczeglnie przydatne, jeeli do wyjtku doczymy jeszcze inne informacje pochodzce predefiniowanych symboli preprocesora. Take konstruktor klasy moe dokonywa zbierania jakich informacji od programu. Mog to by np. zrzuty pamici (ang. memory dumps), czyli obrazy zawartoci kluczowych miejsc pamici operacyjnej. Takie zaawansowane techniki s aczkolwiek przydatne tylko w naprawd duych programach. Po zapaniu takiego obiektu moemy pokaza zwizane z nim dane - na przykad tak: catch (CException& Wyjatek) { std::cout << " Wystapil wyjatek " << std::endl; std::cout << "---------------------------" << std::endl; std::cout << "Komunikat:\t" << Wyjatek.Komunikat() << std::endl; std::cout << "Plik:\t" << Wyjatek.NazwaPliku() << std::endl; std::cout << "Wiersz kodu:\t" << Wyjatek.Linijka() << std::endl;
Hierarchia wyjtkw
Pojedyncza klasa wyjtku rzadko jest jednak wystarczajca. Wad takiego skromnego rozwizania jest to, e ze wzgldu na charakter danych o sytuacji wyjtkowej, jakie zawiera obiekt, ograniczamy sobie moliwo obsugi wyjtku. W naszym przypadku trudno jest podj jakiekolwiek dziaania poza wywietleniem komunikatu i zamkniciem programu. Dla zwikszenia pola manewru monaby doda do klasy jakie pola typu wyliczeniowego, okrelajce bliej rodzaj bdu; wwczas w bloku catch pojawiaby si pewnie jaka instrukcja switch. Jest aczkolwiek praktyczniejsze i bardziej elastyczne wyjcie: moemy uy dziedziczenia. Okazuje si, e rozsdne jest stworzenie hierarchii sytuacji wyjtkw i odpowiadajcej jej hierarchii klas wyjtkw. Opiera si to na spostrzeeniu, e moliwe bdy moemy najczciej w pewien sposb sklasyfikowa. Przykadowo, monaby wyrni wyjtki zwizane z pamici, z plikami dyskowymi i obliczeniami matematycznymi: wrd tych pierwszych mielibymy np. brak pamici (ang. out of memory) i bd ochrony (ang. access violation); dostp do pliku moe by niemoliwy chociaby z powodu jego braku albo nieobecnoci dysku w napdzie; dziaania na liczbach mog wreszcie doprowadzi do dzielenia przez zero lub wycigania pierwiastka z liczby ujemnej. Taki ukad, oprcz moliwoci rozrnienia poszczeglnych typw wyjtkw, ma jeszcze jedn zalet. Mona bowiem dla kadego typu zakodowa specyficzny dla niego sposb obsugi, stosujc do tego metody wirtualne - np. w ten sposb: // klasa bazowa class IException { public: // wywietl informacje o wyjtku
466
virtual void Wyswietl();
Zaawansowane C++
};
// ---------------------------------------------------------------------// wyjtek zwizany z pamici class CMemoryException : public IException { public: // dziaania specyficzne dla tego rodzaju wyjtku virtual void Wyswietl(); }; // wyjtek zwizany z class CFilesException { public: // dziaania virtual void }; plikami : public IException specyficzne dla tego rodzaju wyjtku Wyswietl();
Pamitajmy jednak, e nadmierne rozbudowywanie hierarchii te nie ma zbytniego sensu. Nie wydaje si na przykad suszne wyrnianie osobnych klas dla wyjtkw dzielenia przez zero, pierwiastka kwadratowego z liczy ujemnej oraz podniesienia zera do potgi zerowej. Jest bowiem wielce prawdopodobne, e jedyna rnica midzy tymi sytuacjami bdzie polegaa na treci wywietlanego komunikatu. W takich przypadkach zdecydowanie wystarczy pojedyncza klasa.
Kod warstwowy
Jednym z podstawowych powodw, dla ktrych wprowadzono wyjtki w C++, bya konieczno zapewnienia jakiego sensownego sposobu reakcji na bdy w programach o skomplikowanym kodzie. Kady wikszy (i dobrze napisany) program ma bowiem skonno do rozwarstwiania kodu. Nie jest to bynajmniej niepodane zjawisko, wrcz przeciwnie. Polega ono na tym, e w aplikacji moemy wyrni fragmenty wyszego i niszczego poziomu. Te pierwsze odpowiadaj za ca logik aplikacji, w tym za jej komunikacj z uytkownikiem; te drugie wykonuj bardziej wewntrzne czynnoci, takie jak na przykad zarzdzanie pamici operacyjn czy dostp do plikw na dysku. Taki podzia jest korzystny, poniewa uatwia konserwacj programu, a take wykorzystywanie pewnych fragmentw kodu (zwaszcza tych niskopoziomowych) w kolejnych projektach. Funkcje odpowiedzialne za pewne proste czynnoci, jak wspomniany dostp do plikw nie musz nic wiedzie o tym, kto je wywouje - waciwie to nawet nie powinny. Innymi sowy: Kod niszego poziomu powinien by zazwyczaj niezaleny od kodu wyszego poziomu.
Wyjtki
467
Dobre wyporodkowanie
Ich stosowanie jest szczeglnie wskazane wanie wtedy, gdy nasz kod ma kilka logicznych warstw, co zreszt powinno zdarza si jak najczciej. Wwczas odnosimy jedn zasadnicz korzy: nie musimy martwi si o sposb, w jaki informacja o bdzie dotrze z pokadw gbinowych programu, gdzie wystpia, na grne pitra, gdzie mogaby zosta waciwie obsuona. Naszym problemem jest jednak co innego. O ile zazwyczaj dokadnie wiadomo, gdzie wyjtek naley rzuci (wiadomo - tam gdzie co si nie powiodo), o tyle trudno moe sprawi wybranie waciwego miejsca na jego zapanie: jeeli bdzie ono za nisko, wtedy najprawdopodobniej nie bdzie moliwe podjcie adnych rozsdnych dziaa w reakcji na wyjtek. Przykadowo, wymieniona funkcja otwierajca plik nie powinna sama apa wyjtku, ktry rzuci, bo bdzie wobec niego bezradna. Skoro przecie rzucia ten wyjtek, jest to wanie znak, i nie radzi sobie z powstaa sytuacj i oddaje inicjatyw komu bardziej kompetentnemu z drugiej strony, umieszczenie blokw catch za wysoko powoduje zbyt due zamieszanie w funkcjonowaniu programu. Powoduje to, e punkt wykonania przeskakuje o cae kilometry, niespodziewanie przerywajc wszystko znajdujce si po drodze zdania. Nie naley bowiem zapomina, e po rzuceniu wyjtku nie ma ju powrotu - dalsze wykonywanie zostanie co najwyej podjte po wykonaniu bloku catch, ktry ten wyjtek. Cakowitym absurdem jest wic np. ujcie caej zawartoci funkcji main() w blok try i obsuga wszystkich wyjtkw w nastpujcym dalej bloku catch. Nietrudno przecie domyli si, e takie rozwizanie spowoduje zakoczenie programu po kadym wystpieniu wyjtku Pytanie brzmi wic: jak osign rozsdny kompromis? Trzeba pogodzi ze sob dwie racje: konieczno sensownej obsugi wyjtku konieczno przywrcenia programu do normalnego stanu Naley wic apa wyjtek w takim miejscu, w ktrym ju moliwe jest jego obsuenie, ale jednoczenie po jego zakoczeniu program powinien nadal mc podja podj w miar normaln prac. Przykad? Jeeli uytkownik wybierze opcj otwarcia pliku, ale potem poda nieistniejc nazw, program powinien po prostu poinformowa o tym i ponownie zapyta o nazw
468
Zaawansowane C++
pliku. Nie moe natomiast zmusza uytkownika do ponownego wybrania opcji otwarcia pliku. A ju na pewno nie moe niespodziewanie koczy swojej pracy - to byoby wrcz skandaliczne.
... (CMemoryException& Wyjatek) ... (CFilesException& Wyjatek) ... (IException& Wyjatek) ...
Instrukcje chwytajce bardziej wyspecjalizowane wyjtki - CMemoryException i CFilesException - umieszczamy na samej grze. Dopiero niej zajmujemy si pozostaymi wyjtkami, chwytajc obiekty typu bazowego IException. Gdybymy czynili to na pocztku, zapalibymy absolutnie wszystkie swoje wyjtki - nie dajc sobie szansy na rozrnienie bdw pamici od wyjtkw plikowych lub innych.
Oczywicie wynika ona std, e obiekt klasy pochodnej jest jednoczenie obiektem klasy bazowej. Albo te std, e zawsze istnieje niejawna konwersja z klasy pochodnej na klasy bazowej - jakkolwiek to wyrazimy, bdzie poprawnie.
119
Wyjtki
469
Wida wic po raz kolejny, e waciwe uporzdkowanie blokw catch ma niebagatelne znaczenie.
Lepiej referencj
We wszystkich przytoczonych ostatnio kodach apaem wyjatki poprzez referencje do nich, a nie poprzez same obiekty. Zbywalimy to dotd milczeniem, ale czas ten fakt wyjani. Przyczyna jest waciwie cakiem prosta. Referencje s, jak pamitamy, zakamuflowanymi wskanikami: faktycznie rni si od wskanikw tylko drobnymi szczegami, jak choby skadni. Zachowuj jednak ich jedn cenn waciwo obiektow: pozwalaj na stosowanie polimorfizmu metod wirtualnych. To doskonalne znane nam zjawisko jest wic moliwe do wykorzystania take przy obsudze wyjtkw. Oto przykad: try {
// ... } catch (IException& Wyjatek) { // wywoanie metody wirtualnej, pno wizanej Wyjatek.Wyswietl(); } Metoda wirtualna Wyswietl() jest tu pno wizana, zatem to, ktry jej wariant - z klasy podstawowej czy pochodnej - zostanie wywoany, decyduje si podczas dziaania programu. Jest to wic inny sposb na swoiste rozrnienie typu wyjtku i podjcie dziaa celem jego obsugi.
Uwagi oglne
Na sam koniec podziel si jeszcze garci uwag oglnych dotyczcych wyjtkw. Przede wszystkim zastanowimy si nad korzyciami z uywania tego mechanizmu oraz sytuacjami, gdzie czsto jest on naduywany.
470
Zaawansowane C++
Do tej grupy monaby prbowa zaliczy te destruktory, ale jak przecie, z destruktorw nie mona rzuca wyjtkw. Dziki temu, e wyjtki nie opieraj si na normalnym sposobie wywoywania i powrotu z funkcji, mog by uywane take i w tych specjalnych funkcjach.
Uproszczenie kodu
Jakkolwiek dziwnie to zabrzmi, wyjtki umoliwiaj te znaczne uproszczenie kodu i uczynienie go przejrzystszym. Jest tak, gdy pozwalaj one przenie sekwencje odpowiedzialne za obsug bdw do osobnych blokw, z dala od waciwych instrukcji. W normalnym kodzie procedury wygldaj mniej wicej tak: zrb co sprawd, czy si udao zrb co innego sprawd, czy si udao zrb jeszcze co sprawd, czy nie byo bdw itd. Wyrnione tu sprawdzenia bdw s realizowane zwykle przy pomocy instrukcji if lub switch. Przy ich uyciu kod staje si wic pltanin instrukcji warunkowych, raczej trudnych do czytania. Gdy za uywamy wyjtkw, to obsuga bdw przenosi si na koniec algorytmu: zrb co zrb co innego zrb jeszcze co itd. obsu ewentualne niepowodzenia Oczywicie dla tych, ktrzy nie dbaj o porzdek w kodzie, jest to aden argument, ale ty si chyba do nich nie zaliczasz?
Wyjtki
471
Naduywanie wyjtkw
Czytajc o zaletach wyjtkw, nie mona wpa w bezkrytyczny zachwyt nad nimi. One nie s ani obowizkow technik programistyczn, ani te nie s lekarstwem na bdy w programach, ani nawet nie s pasujcym absolutnie wszdzie rozwizaniem. Wyjtkw atwo mona naduy i dlatego chc si przed tym przestrzec.
Rzucanie wyjtku w razie nieznalezienia elementu tablicy to gruba przesada. Pomylmy tylko, e kod wykorzystujcy t funkcj musiaby wyglda mniej wicej tak: // szukamy liczby nZmienna w tablicy aTablicaLiczb try { unsigned uIndeks = Szukaj(aTablicaLiczb, nZmienna); // zrb co ze znalezion liczb... } catch (CError& Wyjatek) { std::cout << Wyjatek.Komunikat() << std::endl; } Moe i ma on swj urok, ale chyba lepiej skorzysta z mniej urokliwej, ale na pewno prostszej instrukcji if, porwnujcej po prostu rezultat funkcji Szukaj() z jak ustalon sta (np. -1), oznaczajc niepowodzenie szukania. Pozwoli to na wydorbnienie sytuacji faktycznie wyjtkowych od tych, ktre zdarzaj si w normalnym toku dziaania programu. Nieobecno liczby w tablicy naley zwykle do tej drugiej grupy i nie jest wcale krytyczna dla funkcjonowania aplikacji - ergo: nie wymaga zastosowania wyjtkw.
472
Zaawansowane C++
*** Praktyczne wykorzystanie wyjtkw to sztuka, jak zreszt cae programowanie. Najlepszym nauczycielem bdzie tu dowiadczenie, ale jeli zawarto tego podrozdziau pomoe ci cho troch, to jego cel bd mg uwaa za osignity.
Podsumowanie
Ten rozdzia omawia mechanizm wyjtkw w jzyku C++. Rozpocz si od przedstawienia kilku popularnych sposobw radzenia sobie z bdami, jakie moga wystapi w trakcie dziaania programu. Pniej poznae same wyjtki oraz podstawowe informacje o nich. Dalej zajlimy si zagadnieniem odwijania stosu i jego konsekwencji, by wreszcie nauczy si wykorzystywa wyjtki w praktyce.
Pytania i zadania
Rozdzia koczymy tradycyjn porcj pyta i wicze.
Pytania
1. Kiedy moemy mwi, i mamy do czynienia z sytuacj wyjtkow? 2. Dlaczego specjalny rezultat funkcji nie zawsze jest dobr metod informowania o bdzie? 3. Czy rni si throw od return? 4. Dlaczego kolejno blokw catch jest wana? 5. Jaka jest rola bloku catch(...)? 6. Czym jest specyfikacja wyjtkw? Co dzieje si, jeeli zostanie ona naruszona? 7. Ktre obiekty s niszczone podczas odwijania stosu? 8. W jakich funkcjach nie naley rzuca wyjtkw? 9. W jaki sposb moemy zapewni zwolnienie zasobw w przypadku wystpienia wyjtku? 10. Dlaczego warto definiowa wasne klasy dla obiektw wyjtkw?
wiczenia
1. Zastanw si, jakie informacje powinien zawiera dobry obiekt wyjtku. Ktre z tych danych dostarcza nam sam kompilator, a ktre trzeba zapewni sobie samemu? 2. (Trudne) Mechanizm wyjtkw zosta pomylany do obsugi bdw w trakcie dziaania programu. To jednak nie s jego jedyne moliwe zastosowanie; pomyl, do czego potencjalnie przydatne mog by jeszcze wyjtki - a szczeglnie towarzyszcy im proces odwijania stosu
4
SZABLONY
Gdy co si nie udaje, mwimy, e to by tylko eksperyment.
Robert Penn Warren
Nieuchronnie, wielkimi krokami, zbliamy si do koca kursu C++. Przed tob jeszcze tylko jedno, ostatnie i arcywane zagadnienie: tytuowe szablony. Ten element jzyka, jak chyba aden inny, wzbudza wrd wielu programistw rne niezdrowe emocje i kontrowersje; porwna je mona tylko z reakcjami na preprocesor. Nie s to aczkolwiek reakcje skrajnie negatywne: przeciwnie, szablony powszechnie uwaa si za jeden z najwikszych atutw jzyka C++. Problemem jest jednak to, i obecne ich moliwoci (mimo e ju teraz ogromne) s niezadowalajce dla biegych programistw. Dlatego te wanie szablony s t czci C++, ktra najszybciej podlega ewolucji. Trzeba jednak uwiadomi sobie, e od odgrnie narzuconego pomysu Komitetu Standaryzacyjnego do implementacji stosownej funkcji w kompilatorach wiedzie bardzo daleka droga. Skutek jest taki, e na palcach jednej rki mona policzy kompilatory, ktre w peni odpowiadaj tym zaleceniom i oferuje szablony cakowicie zgodne ze standardem. Jest to zadziwiajce, zwaywszy e sama idea szablonw liczy ju sobie kilkanacie (!) lat. Mam jednak take pocieszajc wiadomo. Ot mona krci nosem i narzeka, e kompilator, ktrego uywamy, nie jest w peni na czasie, lecz dla wikszoci programistw nie bdzie to miao wielkiego znaczenia. Oczywicie, najlepiej jest uywa zawsze najnowszych wersji narzdzi programistycznych; nie oznacza to wszake, e starsze ich wersje nie nadaj si do niczego. Skoro ju o tym mwi, to przydaoby si wspomnie, jak wyglda obsuga szablonw w naszym ulubionym kompilatorze, czyli Visual C++. I tu czeka nas raczej mia niespodzianka. Przede wszystkim warto wiedzie, e jego aktualna wersja, zawarta w pakiecie Microsoft Visual Studio .NET 2003, jest absolutnie zgodna z aktualnym standardem jzyka C++ - naturalnie, take pod wzgldem obsugi szablonw. Jeeli natomiast chodzi o starsz wersj Visual Studio .NET (nazywan teraz czsto .NET 2001), to tutaj sprawa take przedstawia si nie najgorzej. W codziennym, ani nawet nieco bardziej egzotycznym programowaniu nie odczujemy bowiem adnego niedostatku w obsudze szablonw przez ten kompilator. Niestety, podobnie dobrych wiadomoci nie mam dla uytkownikw Visual C++ 6. To leciwe ju rodowisko moe szybko okaza si niewystarczajce. Warto wic zaopatrzy w jego nowsz wersj. W kadym jednak przypadku, niezalenie od posiadanego kompilatora, znajomo szablonw jest niezbdna. Wpisay si one w praktyk programistyczn na tyle silnie, e obecnie mao ktry program moe si bez nich obej. Poza tym przekonasz si wkrtce na wasnej skrze, e stosowanie szablonw zdecydowanie uatwia typowe czynnoci koderskie i sprawia, e tworzony kod staje si znacznie bardziej uniwersalny i elastyczny. Najlepszym przykadem tego jest Biblioteka Standardowa jzyka C++, z ktrej fragmentw miae ju okazj korzysta. Zabierzmy si zatem do poznawania szablonw - na pewno tego nie poaujesz :D
474
Zaawansowane C++
Podstawy
Na pocztek przedstawi ci, czym w ogle s szablony i pokae kilka przykadw na ich zastosowanie. Bardziej zaawansowanymi zagadnieniami zajmiemy si bowiem w nastpnym podrozdziale. Na razie czas na krtkie wprowadzenie.
Idea szablonw
Mgbym teraz podwin rkami, poprosi ci o uwag i kawaek po kawaku wyjania, czym s te cae szablony. Na to rwnie przyjdzie pora, ale najpierw lepiej chyba odkry, do czego mog nam si te dziwne twory przyda. Dziki temu moe atwiej przyjdzie ci ich zrozumienie, a potem znajdowanie dla zastosowa i wreszcie polubienie ich! Tak, szablony naprawd mona polubi - za robot, ktrej nam oszczedzaj; nam: ciko przecie pracujcym programistom ;-) Zobacz zatem, jakie fundamentalne problemy pomog ci niedugo rozwizywa te nieocenione konstrukcje
Szablony
return (fLiczba1 > fLiczba2 ? fLiczba1 : fLiczba2);
475
Takich wersji musiaoby by jednak bardzo wiele: za kadym kolejnym typem, dla ktrego chcielibymy stosowa max(), musiaaby i odrbna funkcja. Ich definiowanie byoby uciliwe i nudne, a podczas wykonywania tej nucej czynnoci trudno byoby nie zwtpi, czy jest to aby na pewno suszne rozwizanie
Moliwe rozwizania
Ale jakie mamy wyjcie?, spytasz pewnie. C, mona sobie jako radzi
Wykorzystanie preprocesora
Ogln funkcj max() (i podobne) moemy zasymulowa przy uyciu parametryzowanych makr: #define MAX(a,b) ((a) > (b) ? (a) : (b))
Sdz jednak, e pamitasz wady takich makrodefinicji. Nawiasy wok a i b likwiduj wprawdzie problem pierwszestwa operatorw, ale nie zabezpiecz przed podwjnym obliczaniem wyrae. Wiesz przecie, e preprocesor dziaa na kodzie tak jak na tekcie, zatem np. wyraenie w rodzaju: MAX(10, rand()) nie zwrci nam wcale liczby pseudolosowej rwnej co najmniej 10. Zostanie ono bowiem rozwinite do: ((10) > (rand()) ? 10 : (rand())) Funkcja rand() bdzie wic obliczana dwukrotnie, z kadym razem dajc oczywicie inny wynik - bo takie jest jej przeznaczenie. Makro MAX() nie bdzie wic zawsze dziaao poprawnie.
476
};
Zaawansowane C++
Bdziemy musieli si jednak zmaga z niedogodnociami wskanikw void* - przede wszystkim z utrat informacji o rzeczywistym typie danych: CPtrArray Tablica(5); // alokacja pamici dla elementu (!) Tablica[2] = new int; // przypisanie - nieszczeglnie adne... *(static_cast<int*>(Tablica[2])) = 10; Kadorazowe rzutowanie na waciwy typ elementw (tutaj int) na pewno nie bdzie naleao do przyjenoci. Poza tym trzeba bdzie pamita o zwolnieniu pamici zaalokowanej dla poszczeglnych elementw. W przypadku maych obiektw, jak liczby, nie ma to adnego sensu Zatem nie! To na pewno nie jest zadowalajce wyjcie!
Kompilator to potrafi
Ale nie! Moemy ten wzorzec - ten szablon (ang. template) - wpisa do kodu, tworzc ogln funkcj max(). Trzeba to jedynie zrobi w odpowiedni sposb - tak, aby kompilator wiedzia, z czym ma do czynienia. Zobaczmy wic, jak mona tego dokona.
Skadnia szablonu
A zatem: chcc zdefiniowa wzorzec funkcji max(), musimy napisa go w ten oto sposb sposb: template <typename TYP> TYP max(TYP Parametr1, TYP Parametr2) { return (Parametr1 > Parametr2 ? Parametr1 : Parametr2); }
Szablony
Dopki nie wyjanimy sobie dokadnie kwestii umieszczania szablonw w plikach rdowych, zapamitaj, aby wpisywa je w caoci w plikach nagwkowych.
477
W ten sposb tworzymy szablon funkcji (ang. function template) Zobaczmy, co si na niego skada. Zauwaye zapewne najpierw zupenie now cz nagwka funkcji: template <typename TYP> Jest ona obowizkowa dla kadego rodzaju szablonw, nie tylko funkcji. Sowo kluczowe template (szablon) mwi bowiem kompilatorowi, e nie ma tu do czynienia ze zwykym kodem, lecz wanie z szablonem. Dalej nastpuje, ujta w nawiasy ostre, lista parametrw szablonu. W tym przypadku mamy tylko jeden taki parametr: sowo typename (nazwa typu) informuje, e jest nim typ. Okazuje si bowiem, e parametrami szablonu mog by take normalne wartoci, podobne do argumentw funkcji - nimi te si zajmiemy, ale pniej. Na razie mamy tu jeden parametr szablonu bdcy typem o jake opisowej nazwie TYP. Potem przychodzi ju normalna definicja funkcji - z jedn drobn rnic. Jak wida, uywamy w niej nazwy TYP zamiast waciwego typu danych (czyli int, double, itd.). Stosujemy go jednak w tych samych miejscach, czyli jako typ wartoci zwracanej oraz typ obu przyjmowanych parametrw funkcji. Tre szablonu odpowiada wic wzorcowi z poprzedniego akapitu. Rnica jest jednak taka, e o ile tamten kod by niezrozumiay dla kompilatora, o tyle ten szablon jest jak najbardziej poprawny i, co najwaniejsze, dziaa zgodnie z oczekiwaniami. Nasza funkcja max() potrafi ju bowiem operowa na dowolnym typie argumentw: int nMax = max(-1, 2); unsigned uMax = max(10u, 65u); float fMax = max(-12.4, 67); // TYP = int // TYP = unsigned // TYP = double (!)
Najciekawsze jest to, i to funkcja na podstawie swych argumentw sama zgaduje, jaki typ danych ma by wstawiony w miejsce symbolicznej nazwy TYP. To wanie jedna z zalet szablonw funkcji: uywamy ich zwykle tak samo, jak normalnych funkcji, a jednoczenie zyskujemy zadziwiajc uniwersalno. Popatrzmy jeszcze na ogln skadni szablonu w C++: template <parametry_szablonu> kod Jak wspomniaem, swko template jest tu obowizkowe, bo dziki nim niemu kompilator wie, e ma do czynienia z szablonem. parametry_szablonu to najczciej symboliczne oznaczenia nieznanych z gry typw danych; oznaczenia te s wykorzystywane w nastpujcym dalej kodzie. Na temat obu tych kluczowych czci szablonu powiemy sobie jeszcze mnstwo rzeczy.
Co moe by szablonem
Wpierw ustalmy, do jakiego rodzaju kodu w C++ moemy doczepi fraz template<...>, czynic j szablonem. Generalnie mamy dwa rodzaje szablonw: szablony funkcji - s to wic taki funkcje, ktre mog dziaa w odniesieniu do dowolnego typu danych. Zazwyczaj kompilator potrafi bezbdnie ustali, jaki typ jest waciwy w konkretnym wywoaniu (por. przykad zastosowania szablonu max() z poprzedniego punktu)
478
Zaawansowane C++
szablony klas - czyli klasy, potrafice operowa na danych dowolnego typu. W tym przypadku musimy zwykle poda ten waciwy typ; zobaczymy to wszystko nieco dalej
Wkrtce aczkolwiek okazao si, e bardzo podane s take inne rodzaje szablonw gwnie po to, aby uatwi prac z szablonami klas. My jednak zajmiemy si zwaszcza tymi dwoma rodzajami szablonw. Wpierw wic poznasz nieco bliej szablony funkcji, a potem zobaczysz take szablony klas.
Szablony funkcji
Szablon funkcji moemy wyobrazi sobie jako: oglny algorytm, ktry dziaa poprawnie dla danych rnego typu zesp funkcji, zawierajc odrbne wersje funkcji dla poszczeglnych typw Oba te podejcia s cakiem suszne, aczkolwiek jedno z nich bardziej odpowiada rzeczywistoci. Ot: Szablon funkcji reprezentuje zestaw (rodzin) funkcji, dziaajcych dla dowolnej liczby typw danych. Zasada stojca za szablonami jest taka, e kompilator sam dokonuje po prostu tego, co mgby zrobi programista, nudzc si przy tym niezmiernie. Na podstawie szablonu funkcji generowane s wic jej konkretne egzemplarze (specjalizacje, bdce przecionymi funkcjami), operujce ju na rzeczywistych typach danych. Potem s one wywoywane w trakcie dziaania programu. Proces ten nazywamy konkretyzacj (ang. instantiation) i zachodzi on dla wszelkiego rodzaju szablonw. Zanim aczkolwiek moe do niego doj, szablno trzeba zdefiniowa. Zobaczmy wic, jak definiuje si szablony funkcji.
Szablony
479
Stosowalno definicji
Mona zapyta: Czy powyszy szablon moe dziaa tylko dla wbudowanych typw liczbowych? Czy poradziby sobie np. z wyznaczeniem wartoci bezwzgldnej z liczby wymiernej, czyli obiektu zdefiniowanej ongi klasy CRational? Aby zdecydowa o tym i o podobnych sprawach, musimy odpowiedzie na inne pytanie:
Czy to, co robimy w treci szablonu funkcji, da si wykona po podstawieniu danego typu w miejsce parametru szablonu?
U nas wic typ danych, wystpujcy na razie pod oznaczeniem TYP, musi udostpnia: operator porwnania >=, pozwalajcy na konfrontacj obiektu z zerem operator negacji -, sucy tutaj do uzyskania liczby przeciwnej do danej publiczny konstruktor kopiujcy, umoliwiajcy zwrot wyniku funkcji Pod wszystkie te wymagania podpadaj rzecz jasna wbudowane typy liczbowe. Jeli za wyposaylibymy klas CRational we dwa wspomniane operatory, to take jej obiekty mogyby by argumentami funkcji Abs()! Wynika std, e: Szablon funkcji moe by stosowany dla tych typw danych, dla ktrych poprawne s wszystkie operacje, dokonywane na obiektach tyche typw w treci szablonu. atwo mona wic stwierdzi, e np. dla typu std::string ten szablon byby niedozwolony. Klasa std::string nie udostpnia bowiem operatora negacji, ani te nie pozwala na porwnywanie swych obiektw z liczbami cakowitymi.
480
Zaawansowane C++
i dla klas nie jest, jak sdz, adn niespodziank. To samo jednak moemy uczyni take w stosunku do podstawowych typw danych. W C++ s wic cakowicie poprawne wyraenia typu int(), float(), bool() czy unsigned(). Co waniejsze w wyniku daj one zero odpowiedniego typu - czyli dziaaj tak, jakbymy napisali (odpowiednio): 0, 0.0f, false i 0u. Inicjalizacja zerowa gwarantuje wic wspprac naszego szablonu z typami podstawowymi, poniewa wyraenie TYP() da w kadym przypadku potrzebny nam tutaj obiekt zerowy. Niewane, czy bdzie chodzio o typ podstawowy C++, czy te klas zdefiniowan przez programist.
Parametry tej funkcji to Parametr1 i Parametr2. Obydwa nale one do typu oznaczonego po prostu jako TYP. w TYP mgby by klas, aliasem zdefiniowanym poprzez typedef, wyliczeniem enum, itd. Tutaj jednak TYP jest parametrem szablonu: deklarujemy go w nawiasach ostrych po sowie template przy pomocy typename. Fakt, e TYP parametrw funkcji jest parametrem szablonu ma dalekosine i dobroczynne konsekwencje. Powoduje to mianowicie, i moe on by wydedukowany z argumentw wywoania funkcji: // (byo ju do przykadw wywoywania max(), wic jeden wystarczy :D) std::cout << max(42, 69); Nie musimy w powyszej linijce wyranie okrela, e szablon max() ma by tu uyty do wygenerowania funkcji pracujcej na argumentach typu int. Ten typ zostanie po prostu wzity z argumentw wywoania (ktre s typu int wanie). To jedna z wielkich zalet szablonw funkcji. Moliwe jest aczkolwiek jawne okrelenie typu, czyli parametru szablonu. O tym powiemy sobie w nastpnym paragrafie.
Szablony
481
Podobnie jak parametry funkcji, parametry szablonu zawarte w nawiasach ostrych take o oddzielamy przecinkami. Moe ich by dowolna ilo; tutaj mamy dwa parametry szablonu, ktre bezporednio przedkadaj si na dwa parametry funkcji. Nowa wersja funkcji max() potrafi wic porwnywa wartoci rnych typw - o ile oczywicie istnieje odpowiedni operator >. Oto przykad wykorzystania tego szablonu: int float nMax = max(-18, 42u); fMax = max(9.5f, 34); fMax = max(6.78, 80); // TYP1 = int, TYP2 = unsigned // TYP1 = float, TYP2 = int // TYP1 = double, TYP2 = int
W ostatnim wywoaniu wartoci zwrcon przez max() bdzie 80.0 typu double. Jej przypisanie do mniej pojemnego typu float spowoduje zapewne ostrzeenie kompilatora. Jak wida, argumenty funkcji nie musz by tu konwertowane do wsplnego typu, jak to si dziao przy jednoparametrowym szablonie. W sumie jednak midzy oboma szablonami nie ma wielkiej rznicy funkcjonalnej; podaem tu jedynie przykad na to, e szablon funkcji moe mie wicej parametrw ni jeden. Z powyszym szablonem jest jednak pewien do istotny kopot. Chodzi mianowicie o typ wartoci zwracanej. Wpisaem w nim wprawdzie TYP1, ale to nie ma adnego uzasadnienia, gdy rwnie dobry (a raczej niedobry) byy TYP2. Problemem jest to, i na etapie kompilacji nie wiemy rzecz jasna, jakie wartoci zostan przekazane do funkcji. Nie wiemy wobec tego, jaki powinien by typ wartoci zwracanej. W takiej sytuacji naleaoby uy typu oglniejszego, bardziej pojemnego: dla int i float byby to zatem float, i tak dalej (przypomnij sobie z poprzedniego rozdziau, kiedy jaki typ jest oglniejszy od drugiego). Niestety, poniewa z samego zaoenia szablonw funkcji nie wiemy, dla jakich faktycznych typw bdzie on uyty, nie moemy nijak okreli, ktry z tej dwjki bdzie pojemniejszy. W zasadzie wic nie wiemy, jaki powinien by typ wartoci zwracanej! Rozsdne rozwizanie tego problemu nie ley niestety w zakresie moliwoci programisty. Potrzebny jest tutaj jaki nowy mechanizm jezyka; zwykle mwi si w tym kontekcie o operatorze typeof (typ czego). Miaby on zwraca nazw typu z podanego mu (staego) wyraenia. Nazwa ta mogaby by potem uyta tak, jak kada inna nazwa typu - a wic na przykad w charakterze rodzaju wartoci zwracanej przez funkcj. Obecnie istniej kompilatory, ktre oferuj operator typeof, ale oficjalny standard C++ pki co nic o nim nie mwi.
482
Wyjtkowy przypadek
Zaawansowane C++
Twoja nauka C++ opiera si midzy innymi na serii narzuconych przypuszcze, zatem teraz przypumy, e chcemy rozszerzy nieco funkcjonalno szablonu funkcji max(). Zalmy mianowicie, e chcemy uczyni j wadn do wsppracy nie tylko z liczbami, ale te z tak oto klas wektora: #include <cmath> struct VECTOR2 { // wsprzdne tego wektora double x, y; // ------------------------------------------------------------------// metoda liczca dugo wektora double Dlugosc() const { return sqrt(x * x + y * y); } }; // (reszta jest rednio potrzebna, zatem pomijamy)
Naturalnie, monaby wyposay j w odpowiedni operator>(). My jednak chcemy zdefiniowa specjalizowan wersj szablonu funkcji max(). Czynimy to w taki oto sposb: template<> VECTOR2 max(VECTOR2 vWektor1, VECTOR2 vWektor2) { // porwujemy dugoci wektorw; w przypadku rwnoci zwracamy 1-szy return (vWektor1.Dlugosc() >= vWektor2.Dlugosc() ? vWektor1 : vWektor2); } Waciwie to mona powiedzie, e funkcja ta nie rni si prawie niczym od normalnej funkcji max() (nieszablonowej). Dlatego te wane jest opatrzenie jej fraz template<> (z pustymi nawiasami ostrymi), bo dziki temu kompilator moe uzna nasza definicj za specjalizacj szablonu funkcji max(). Co do nagwka funkcji, to jest to ten sam naglwek, co w oryginalnym szablonie - z t tylko rnic, e TYP zostao zamienione na nazw rzeczywistego typu, czyli VECTOR2. Ze wzgldu na t jednoznaczno specjalizacja nie wymaga adnych dalszych zabiegw. W sumie jednak mona (i zaleca si) bezporednie podanie typu, dla ktrego specjalizujemy szablon: template<> VECTOR2 max<VECTOR2>(VECTOR2 vWektor1, VECTOR2 vWektor2) Dziwn fraz max<VECTOR2> mona tu z powodzeniem traktowa jako nazw funkcji specjalizacji szablonu max() dla typu VECTOR2. W takiej zreszt roli poznamy podobne konstrukcje, gdy zajmiemy si dokadniej uyciem funkcji szablonowych.
Szablony
otrzymamy w wyniku takiego porwnania? Naturalnie, dostaniemy ten wskanik, ktrego adres jest mniejszy. Zapytam wprost: i co nam z tego? Lepiej chyba byoby, aby porwnanie dokonywane byo raczej na obiektach, do ktrych te wskaniki si odnosz. Wtedy mielibymy bardziej sensowny wynik i np. z dwch wskanikw typu int* dostalibymy ten, ktry odnosi si do wikszej liczby.
483
Takie dziaanie szablonu funkcji max() w odniesieniu do wskanikw - przy zachowaniu jego normalnego dziaania dla pozostaych typw danych - nie jest moliwe do osignicia przy pomocy zwykej specjalizacji, zaprezentowanej w poprzednim punkcie. Trzebaby bowiem zdefiniowa osobne wersje dla wszystkich typw wskanikw (int*, CRational*, float*, ), jakich chcielibymy uywa. Cakowicie przekrela to sens szablonw, ktre przecie opieraj si wanie na tym, e to sam kompilator generuje ich wyspecjalizowane wersje w zalenoci od potrzeb. Tutaj trzeba by uy mechanizmu specjalizacji czciowej, znanego bardziej z szablonw klas. Oznacza on ni mniej, ni wicej, jak tylko zdefiniowanie innej wersji szablonu dla caej grupy typw (parametrw szablonu). W tym przypadku ta grup s typy wskanikowe, a szablon funkcji max() wygldaby dla nich tak: template <typename TYP> TYP* max<TYP*>(TYP* pWskaznik1, TYP* pWskaznik2) { return (*pWskaznik1 > *pWskaznik2 ? pWskaznik1 : pWskaznik2); } Nazwa specjalizowanej funkcji, czyli max<TYP*>, gdzie TYP jest parametrem szablonu, wskazuje jednoznacznie, i chodzi nam o wersj funkcji przeznaczon dla wskanikw. Naturalnie, typ wartoci zwracanej i parametrw funkcji musi by rwnie taki sam. Kiedy zostanie uyty ten bardziej wyspecjalizowany szablon? Ot wtedy, gdy jako parametry funkcji max() zostan przekazane jakie wskaniki, np.: int nLiczba1 = 10, nLiczba2 = 98; int* pnLiczba1 = &nLiczba1; int* pnLiczba2 = &nLiczba2; std::cout << *(max(pnLiczba1, pnLiczba2)); // szablon max<TYP*>(), // gdzie TYP = int
W tym wic przypadku wywietlan liczb bdzie zawsze 98, bo liczy si bd tutaj faktyczne wartoci, a nie rozmieszczenie zmiennych w pamici (a wic nie adresy, na ktre pokazuj wskaniki). Czciowe specjalizacje szablonw funkcji nie wygldaj moe na zbytnio skomplikowane. Moe ci jednak zaskoczy to, i to jeden z najbardziej zaawansowanych aspektw szablonw - tak bardzo, e pki co Standard C++ o nim nie wspomina (!), a tylko nieliczne kompilatory obsuguj go. Pki co jest to wic bardzo rzadko uywana technika i dlatego na razie naley j traktowa jako ciekawostk.
484
Zaawansowane C++
max(12, 56) max() jest tu szablonem funkcji, ktrego parametr (typ) jest stosowany w charakterze typu obu parametrw funkcji, jak rwnie zwracanej przez ni warto. Nie podajemy jednak tego typu dosownie; to wanie wielka zaleta szablonw funkcji, gdy waciwy typ - parametr szablonu, tutaj int - moe by wydedukowany z jej wywoania. O tym, jak to si dzieje, mwi nastpny akapit. Aby jednak zrozumie istot szablonw funkcji, musimy cho z grubsza wiedzie, jak kompilator traktuje takie wywoania jak powysze. Generalnie nie jest trudne. Jak wspomniaem wczeniej, szablony w C++ s implementowane w ten sposb, i podczas kompilacji tworzony jest ich waciwy (nieszablonowy) kod dla kadego typu, dla ktrego uywamy danego szablonu. Proces ten nazywamy konkretyzacj (ang. instantiation) a poszczeglne egzemplarze szablonw - specjalizacjami (ang. specialization albo instance). Tak wic kompilator musi sobie wytworzy odpowiednie specjalizacje, ktre bd wykorzystywane w miejscach uycia szablonu. W przykadzie powyej szablon funkcji max() posuy do wygenerowania jej konkretnej wersji: funkcji max() dla parametru szablonu rwnego int. Dopiero ta konkretna wersja - specjalizacja - bdzie skompilowana w normalny sposb, do normalnego kodu maszynowego. W ten sposb zarwno funkcje, jak te klasy szablonowe zachowuj niemal wszystkie cechy zwykych funkcji i klas. To, jak szablon funkcji zostanie skonkretyzowany w danym przypadku, zaley wycznie od sposobu jego uycia w kodzie. Przyjrzyjmy si wic sposobom na wywoywanie funkcji szablonowych.
Szablony
Nie jest to adne pustosowie, bowiem ma to konkretne konsekwencje. Nazwa max<unsigned> dziaa mianowicie tak samo, jak kada inna nazwa funkcji. W szczeglnoci, moemy jej uy do pobrania adresu funkcji szablonowej: unsigned (*pfnUIntMax)(unsigned, unsigned) = max<unsigned>;
485
Zauwa rnic: nie moemy pobra adresu szablonu (czyli max), bo ten nie istnieje w pamici podczas dziaania programu. Jest on tylko instrukcj dla kompilatora (podobnie jak makra s instrukcjami dla preprocesora), mwic mu, jak ma wygenerowa prawdziwe, specjalizowane funkcje. max<unsigned> jest tak wanie wyspecjalizowan funkcj i ona ju istnieje w pamici, bowiem jest kompilowana do kodu maszynowego tak, jak normalna funkcja. Moemy zatem pobra jej adres.
Jak to dziaa
A zatem, skd kompilator wie, dla jakich parametrw ma skonkretyzowa szablon funkcji? Innymi sowy, skd bierze on waciwy typ dla funkcji szablonowej? C, nie jest to bardzo skomplikowane: Parametry szablonu funkcji s dedukowane w oparciu o parametry jej wywoania oraz niejawne konwersje. Przeledmy to na przykadzie wywoania szablonu funkcji: template <typename TYP> TYP max(TYP Parametr1, TYP Parametr2); w kilku formach: max(67, 76) max(5.6, 6.5f) max(8.7f, 9.0f) max("Hello", std::string("world")) // // // // 1 2 3 4
Pierwszy przykad jest jak sdze prosty. Obie liczby s tu typu int, zatem uyt tu funkcj max<int>. Nie ma adnych watpliwoci. Dalej jest ciekawiej. Parametry drugiego wywoania funkcji s typu double i float. Mamy jednak jeden parametr szablonu (TYP), ktry musi przyj t sam warto w wywoaniu funkcji. Co zatem zrobi kompilator? Wykorzysta on to, e midzy float i double istnieje niejawna konwersja i wybierze typ double jako oglniejszy. Uytym wariantem bdzie wic max<double>. Kolejny przykad to nic nowego :) Oba argumenty s tu typu float (skutek przyrostka f), zatem wykorzystan funkcj bdzie max<float>. Ostatnia, czwarta linijka jest zdecydowanie najciekawsza. Napisy "Hello" i "world" maj z pewnoci ten sam typ - const char[]. Niemniej, drugi parametr jest typu std::string, bowiem jawnie tworzymy obiekt tej klasy przy uyciu konstruktora. Wobec takiego obrotu sprawy kompilator musi pogodzi go z const char[]. Robi to, poniewa
486
Zaawansowane C++
istnieje niejawna konwersja ancucha typu C na std::string. Szablon funkcji zostanie wic skonkretyzowany do max<std::string>120. Oglny wniosek z tych przykadw jest taki, e jeli jeden parametr szablonu musi by dopasowany na podstawie kilku rnych typw parametrw funkcji, to kompilator prbuje zastosowa niejawne konwersje celem sprowadzenia ich do jakiego jednego typu oglnego. Dopiero jeeli ta prba si nie powiedzie, sygnalizowany jest bd. W zasadzie to trzeba powiedzie: jeeli ta prba si nie powiedzie i nie ma adnych innych moliwych dopasowa. Moliwe bowiem, e istniej inne szablony, ktrych parametry pozwalaj na problematyczne dopasowanie. Przykadowo, wywoanie max(18, "tekst") nie mogoby by dopasowane do jednoparametrowego szablonu max(), ale bez problemu przypasowane zostaoby do szablonu dwuparametrowego max(), podanego jaki czas temu (i poniej). Ten dopuszczaby przecie rne typy argumentw. Regua mwica, i pierwsze niepowodzenie dopasowywania parametrw szablonu nie jest bdem, funkcjonuje pod skrtem SFINAE (ang. Substitution Failure Is Not An Error poraka podstawiania nie jest bdem).
120
Porwnywanie dwch napisw moe si wydawa dziwne, ale jest ono poprawne. Klasa std::string posiada operator >, dokonujcy porwnania tekstw pod wzgldem ich dugoci oraz przechowywanych we znakw (ich kolejnoci alfabetycznej).
Szablony
Istnieje aczkolwiek sposb na to. Naley przesun parametr ZWROT na pocztek listy parametrw szablonu: template <typename ZWROT, typename TYP1, typename TYP2> ZWROT max(TYP1 Parametr1, TYP2 Parametr2); Teraz pozostae dwa typy mog by odgadnite z parametrw funkcji. Tego szablonu max() bdziemy wic mogli uywa, podajc tylko typ wartoci zwracanej: max<float>(17, 67f); Wynika std prosty wniosek:
487
Dedukcja parametrw szablonu nastpuje od koca (od prawej strony). Te parametry, ktre mog by wzite z wywoania funkcji, powinny zatem znajdowa si na kocu listy.
Szablony klas
Szablony funkcji mog przedstawia si wcale zachcajco, jednak o wiele wiksz zalet C++ s szablony klas. Ponownie, moemy je traktowa jako: swego rodzaju oglne klasy (zwane czasem metaklasami), definiujce zachowanie si obiektw w odniesieniu do dowolnych typw danych zesp klas, delegujcych odrbne klasy do obsugi rnych typw Po raz kolejny te to drugie podejcie jest bardziej poprawne. Szablon klasy reprezentuje zestaw (rodzin) klas, mogcych wsppracowa z rnymi typami danych. Konieczno istnienia szablonw klas bezporednio wynika z faktu, e C++ jest jzykiem zorientowanym obiektowo. Do potrzeb programowania strukturalnego z pewnoci wystarczyyby szablony funkcji; kiedy jednak chcemy w peni korzysta z dobrodziejstw OOPu i cieszy si elastycznoci szablonw, naturalnym jest uycie szablonw klas. Z bardziej praktycznego punktu widzenia szablony klas s znacznie przydatniejsze i czciej stosowane ni szablony funkcji. Typowym ich zastosowaniem s klasy pojemnikowe, czyli znane i lubiane struktury danych - a one obok algorytmw, s wedug klasykw informatyki podstawowymi skadnikami programw. Niemniej przez lata istnienia szablony klas dorobiy si take wielu cakiem niespodziewanych zastosowa. Szablony klas intensywnie wykorzystuje Biblioteka Standardowa jzyka C++, a take niezwykle popularna biblioteka Boost. Niezalenie od tego, czy twj kontakt z tymi rodzajami szablonw bdzie si ogranicza wycznie do pojemnikw w rodzaju wektorw lub kolejek, czy te wymylisz dla nich znacznie wicej zastosowa, powiniene dobrze pozna ten element jzyka C++. I te temu wanie suy niniejsza sekcja.
488
Zaawansowane C++
{ }
// ------------------------------------------------------------// pobieranie i ustawianie elementw tablicy int Pobierz(unsigned uIndeks) const { if (uIndeks < m_uRozmiar) return m_pnTablica[uIndeks]; else return 0; } bool Ustaw(unsigned uIndeks, int nWartosc) { if (uIndeks >= m_uRozmiar) return false; m_pnTablica[uIndeks] = uWartosc; return true; } // inne unsigned Rozmiar() const { return m_uRozmiar; }
// ------------------------------------------------------------// operator indeksowania int& operator[](unsigned uIndeks) { return m_pnTablica[uIndeks]; } // operator przypisania (duszy, wic nie w definicji) CIntArray& operator=(const CIntArray&);
};
Szablony
Przerbmy j zatem na szablon.
489
Definiujemy szablon
Jak wic zdefiniowa szablon klasy w C++? Patrzc na ogln skadni szablonu mona by nawet domyli si tego, lecz spjrzmy na poniszy - pusty na razie - przykad: template <typename TYP> class TArray { // ... }; Jest to szkielet definicji szablonu klasy TArray, czyli tablicy dynamicznej na elementy dowolnego typu121. Wida tu znane ju czci: przede wszystkim, fraza template <typename TYP> identyfikuje konstrukcj jako szablon i deklaruje parametry tego szablonu. Tutaj mamy jeden parametr - bdzie nim rzecz jasna typ elementw tablicy. Dalej mamy waciwie zwyk definicj klasy i w zasadzie jedyn dobrze widoczn rnic jest to, e wewntrz niej moemy uy nazwy TYP - parametru szablonu. U nas bdzie on peni identyczn rol jak int w CIntArray, zatem pena wersja szablonu TArray bdzie wygldaa nastpujco: template <typename TYP> class TArray { // domylny rozmiar tablicy static const unsigned DOMYSLNY_ROZMIAR = 5; private: // wskanik na waciw tablic oraz jej rozmiar TYP* m_pTablica; unsigned m_uRozmiar; public: // konstruktory explicit TArray(unsigned uRozmiar = DOMYSLNY_ROZMIAR) : m_uRozmiar(uRozmiar), m_pTablica(new TYP [m_uRozmiar]) { } TArray(const TArray&); // destruktor ~TArray() { delete[] m_pTablica; } // ------------------------------------------------------------// pobieranie i ustawianie elementw tablicy TYP Pobierz(unsigned uIndeks) const { if (uIndeks < m_uRozmiar) return m_pTablica[uIndeks]; else return TYP(); } bool Ustaw(unsigned uIndeks, TYP Wartosc) { if (uIndeks >= m_uRozmiar) return false; m_pTablica[uIndeks] = Wartosc; return true; } // inne unsigned Rozmiar() const { return m_uRozmiar; }
// ------------------------------------------------------------121
490
Zaawansowane C++
// operator indeksowania TYP& operator[](unsigned uIndeks) { return m_pTablica[uIndeks]; } // operator przypisania (duszy, wic nie w definicji) TArray& operator=(const TArray&);
};
Moesz by nawet zaskoczony, e byo to takie proste. Faktycznie, uczynienie klasy CIntArray szablonem ograniczao si do zastpienia nazwy int, uytej jako typ elementw tablicy, nazw parametru szablonu - TYP. Pamitaj jednak, e nigdy nie powinno si bezmylnie dokonywa takiego zastpowania; int mg by przecie choby typem licznika ptli for (for (int i = ...)) i w takiej sytuacji zastpienie go przez parametr szablonu nie miaoby adnego sensu. Nie zapominaj wic, e jak zwykle podczas programowania naley myle nad tym, co robimy. Naturalnie, gdy ju opanujesz szablony klas (co, jak sdz, stanie si niedugo), dojdziesz do wniosku, e wygodniej jest od razu definiowia waciwy szablon ni wychodzi od specjalizowanej klasy i czyni j ogln.
I znowu moemy mie dja vu: kod zaczynamy ponownie sekwencj template <...>. atwo to jednak uzasadni: mamy tu bowiem do czynienia z szablonem, w ktrym uywamy przecie jego parametru TYP. Koniecznie wic musimy uyc wspomnianej sekwencji po to, aby: kompilator wiedzia, e ma do czynienia z szablonem, a nie zwykym kodem moliwe byo uycie nazw parametrw szablonu (tutaj mamy jeden - TYP) w jego wntrzu Kady kawaek szablonu trzeba zatem zacz od owego template <...>, aby te dwa warunki byy spenione. Jest to moe i uciliwe, lecz niestety konieczne. Idmy dalej - zostajc jednak nadal w pierwszym wierszu kodu. Jest on nader interesujcy z tego wzgldu, e a trzykrotnie wystpuje w nim nazwa naszego szablonu, TArray - na dodatek ma ona tutaj trzy rne znaczenia. Przenalizujmy je:
Szablony
491
w pierwszym przypadku jest to wyraz TArray<TYP>. Jak pamitamy z szablonw funkcji, takie konstrukcje oznaczaj zazwyczaj konkretne egzemplarze szablonu specjalizacje. W tym jednak wypadku podajemy tu parametr TYP, a nie jaki szczeglny typ danych. W sumie cay ten zwrot peni funkcj nazwy typu klasy; potraktuj to po prostu jako obowizkow cz nagwka, wystpujc zawsze przed operatorem :: w implementacji metod. Podobnie byo np. z CIntArray, gdy chodzio o zwyke metody zwykych klas. Zapamitaj zatem, e: Sekwencja nazwa_szablonu<typ> peni rol nazwy typu klasy tam, gdzie jest to konieczne. drugi raz uywamy TArray w charakterze nazwy metody - konstruktora. Moe to nie by nieco mylce, bo przecie piszc konstruktory normalnych klas po obu stronach operatora zasigu podawalimy t sam nazw. Musisz wic zapamita, e: Konstruktory i destruktory w szablonach klas maj nazwy odpowiadajce nazwom ich macierzystych szablonw i niczemu wicej, tzn. nie zawieraj parametrw w nawiasach ostrych. trzeci raz TArray jest uyta jako cz typu parametru konstruktora kopiujcego const TArray&. By moe zabyniesz tu kompetencj i krzykniesz, e to niepoprawne i e jeli chodzi nam o nazw typu klasy szablonowej, to powinnimy wstawi TArray<TYP>, bo samo TArray to tylko nazwa szablonu. Odpowiem jednak, e posunicie to jest rwnie poprawne; mamy tu do czynienia z tak zwan nazw wtrcon. Polega to na tym, i: Sama nazwa szablonu moe by stosowana wewntrz niego w tych miejscach, gdzie wymagany jest typ klasy szablonowej. Moemy wic posuy si ni do skrtowego deklarowania pl, zmiennych czy parametrw funkcji bez potrzeby pisania nawiasw ostrych i nazw parametrw szablonu. Wobec nagwka tak cikiego kalibru reszta tej funkcji nie przedstawia si chyba bardzo skomplikowanie? :) W rzeczywistoci to niemal dokadna kopia treci oryginalnego konstruktora kopiujcego - z tym, e typ int elementw CIntArray zastpuje tutaj nieznany z gry TYP - parametr szablonu. W podobny sposb naleaoby jeszcze zaimplementowa operator przypisania. Sdz, e nie sprawioby ci problemu samodzielne wykonanie tego zadania.
Korzystanie z tablicy
Gdy mamy ju definiowany szablon klasy, chcielibymy zapewne skorzysta z niego. Sprbujmy wic stworzy sobie obiekt tablicy; poniewa przez cay zajmowalimy si tablic int-w, to teraz niech bdzie to tablica napisw: TArray<std::string> aNapisy(3); Jak doskonale wiemy, to co widnieje po lewej stronie jest typem deklarowanej zmiennej. W tym przypadku jest to wic TArray<std::string> - specjalizowana wersja naszego szablonu klas. Uywamy w niej skadni, do ktrej, jak sdz, zaczynasz si ju przyzwyczaja. Po nazwie szablonu (TArray) wpisujemy wic par nawiasw ostrych, a w niej warto parametru szablonu (typ std::string). U nas parametr ten okrela jednoczenie typ elementw tablicy - powysza linijka tworzy wic trjelementow tablic acuchw znakw. Cakiem podobnie wyglda tworzenie tablicy ze zmiennych innych typw, np.:
492
Zaawansowane C++
// 7-el. tablica z liczbami float // zestaw omiu flag bool-owskich // tablica wskanikw na obiekty
Zwrmy uwag, e parametr(y) szablonu - tutaj: typ elementw tablicy - musimy poda zawsze. Nie ma moliwoci wydedukowania go, bo i skd? Nie jest to przecie funkcja, ktrej przekazujemy parametry, lecz obiekt klasy, ktry tworzymy. Postpowanie z tak tablic nie rni si niczym od posugiwania si klas CIntArray, a wic porednio - rwnie zwykymi tablicami w C++. W szablonach C++ obowizuj po prostu te same mechanizmy, co w zwykych klasach: dziaaj przecione operatory, niejawne konwersje i reszta tych nietuzinkowych moliwoci OOPu. Korzystanie z szablonw klas jest wic nie tylko efektywne i elastycznie, ale i intuicyjne: // wypenienie tablicy aNapisy[0] = "raz"; aNapisy[1] = "dwa"; aNapisy[2] = "trzy"; // pokazanie zawartoci tablicy for (unsigned i = 0; i < aNapisy.Rozmiar(); ++i) std::cout << aNapisy[i] << std::endl; Przyznasz chyba teraz, e szablony klas przedstawiaj si wyjtkowo zachcajco? Dowiedzmy si zatem wicej o tych konstrukcjach.
Szablony
double fWynik = 0.0; for (unsigned i = 0; i < Rozmiar(); ++i) fWynik += (*this)[i] * aWektor[i]; // zwracamy wynik return fWynik;
493
W samym akcie dziedziczenia, jak i w implementacji klasy pochodnej, nie ma adnych niespodzianek. Uywamy po prostu TArray<double> tak, jak kadej innej nazwy klasy i moemy korzysta z jej publicznych i chronionych skadnikw. Naley oczywicie pamita, e w tej klasie typ double wystpuje tam, gdzie w szablonie TArray pojawia si parametr szablonu - TYP. Dotyczy to chociaby rezultatu operatora [], ktry jest wanie liczb typu double: fWynik += (*this)[i] * aWektor[i]; Myl aczkolwiek, e fakt ten jest intuicyjny i dziedziczenie specjalizowanych klas szablonowych nie bdzie ci sprawia kopotu.
494
if (!(uNowyRozmiar > m_uRozmiar)) return false; // alokujemy now tablic TYP* pNowaTablica = new TYP [uNowyRozmiar];
Zaawansowane C++
// kopiujemy do star tablic i zwalniamy j memcpy (pnNowaTablica, m_pTablica, m_uRozmiar * sizeof(TYP)); delete[] m_pTablica; // "podczepiamy" now tablic do klasy i zapamitujemy jej rozmiar m_pTablica = pNowaTablica; m_uRozmiar = uNowyRozmiar; // zwracamy pozytywny rezultat return true;
Widzimy wic, e dziedziczenie szablonu klasy nie jest wcale trudne. W jego wyniku powstaje po prostu nowy szablon klas.
Aliasy typedef
Cech wyrniajc szablony jest to, i operuj one na typach danych w podobny sposb, jak inny kod na samych danych. Naturalnie, wszystkie te operacje s przeprowadzane w czasie kompilacji programu, a ich wiksz czci jest konkretyzacja tworzenie specjalizowanych wersji funkcji i klas na podstawie ich szablonw. Proces ten sprawia jednoczenie, e niektre przewidywalne i, zdawaoby si, znajome konstrukcje jzykowe nabieraj nowych cech. Naley do nich choby instrukcja typedef; w oryginale suy ona wycznie do tworzenia alternatywnych nazw dla typw np. tak: typedef void* PTR; Nie jest to adna rewolucja w programowaniu, co zreszt podkrelaem, prezentujc t instrukcj. Ciekawie zaczyna si robi dopiero wtedy, jeli uwiadomimy sobie, e aliasowanym typem moe by parametr szablonu! Ale skd on pochodzi? Oczywicie - z szablonu klasy. Jeeli bowiem umiecimy typedef wewntrz definicji takiego szablonu, to moemy w niej wykorzysta parametryzowany typ. Oto najprostszy przykad: template <typename TYP> class TArray { public: // alias na parametr szablonu typedef TYP ELEMENT; }; // (reszta niewana)
Szablony
495
Instrukcja typedef pozwala nam wprowadzenie czego w rodzaju skadowej klasy reprezentujcej typ. Naturalnie, jest to tylko skadowa w sensie przenonym, niemniej nazwa ELEMENT zachowuje si wewntrz klasy i poza ni jako penoprawny typ danych rwnowany parametrowi szablonu, TYP. Przydatno takiego aliasu moe si aczkolwiek wydawa wtpliwa, bo przecie atwiej i krcej jest pisa nazw typu float ni TArray<float>::ELEMENT. typedef wewntrz szablonu klasy (lub nawet oglnie - w odniesieniu do szablonw) ma jednak znacznie sensowniejsze zastosowania, gdy wsppracuje ze soba wiele takich szablonw. Koronnym przykadem jest Biblioteka Standardowa C++, gdzie w ten sposb cakiem mona zyska dostp m.in. do tzw. iteratorw, wspomagajcym prac ze strukturami danych.
Deklaracje przyjani
Czciej spotykanym elementem w zwykych klasach s deklaracje przyjani. Naturalnie, w szablonach klas nie moglo ich zabrakn. Moemy tutaj rwnie deklarowa przyjanie z funkcjami i klasami. Dodatkowo moliwe jest (obsuguj to nowsze kompilatory) uczynienie deklaracji przyjani szablonow. Oto przykad: template <typename T> class TBar { /* ... */ };
template <typename T> class TFoo { // deklaracja przyjani z szablonem klasy TBar template <typename U> friend class TBar<U>; }; Taka deklaracja sprawia, e wszystkie specjalizacje szablonu TBar bd zaprzyjanione ze wszystkimi specjalizacjami szablonu TFoo. TFoo<int> bdzie wic miaa dostp do niepublicznych skadowych TBar<double>, TBar<unsigned>, TBar<std::string> i wszystkich innych specjalizacji szablonu TBar. Zauwamy, e nie jest to rwnowaznaczne z zastosowaniem deklaracji: friend class TBar<T>; Ona spowoduje tylko, e zaprzyjanione zostan te egzemplarze szablonw TBar i TFoo, ktre konkretyzowano z tym samym parametrem T. TBar<float> bdzie wic zaprzyjaniony z TFoo<float>, ale np. z TFoo<short> czy z jakkolwiek inn specjalizacj TFoo ju nie.
122 Dostpna aczkolwiek tylko w niektrych kompilatorach (np. w Visual C++ .NET 2003), podobnie jak szablony deklaracji przyjani.
496
// ... aFloaty1 = aFloaty2; aFloaty2 = aInty;
Zaawansowane C++
// OK, przypisujemy tablic tego samego typu // BD! TArray<int> niezgodne z TArray<float>
Drugie przypisanie tablicy int-w do tablicy float-w nie jest dopuszczalne. To niedobrze, poniewa, logicznie rzecz ujmujc, powinno to by jak najbardziej moliwe. Kopiowanie mogoby si przecie odby poprzez przepisanie poszczeglnych liczb elementw tablicy aInty. Konwersja z int do float jest bowiem jak cakowicie poprawna i nie powoduje adnych szkodliwych efektw. Kompilator jednak tego nie wie, gdy w szablonie TArray zdefiniowalimy operator przypisania wycznie dla tablic tego samego typu. Musielibymy wic doda kolejn jego wersj - tym razem uniwersaln, szablonow. Dziki temu w razie potrzeby mona by jej uy w takich wanie przypisaniach. Jak to zrobi? Spjrzmy: template <typename T> class TArray { public: // szablonowy operator przypisania template <typename U> TArray<T>& operator=(const TArray<U>&); }; // (reszta niewana)
Mamy wic tutaj znowu zagniedon deklaracj szablonu. Druga fraza template <...> jest nam potrzebna, aby uniezaleni od typu operator przypisania - uniezaleni nie tylko w sensie oglnym (jak to ma miejsce w caym szablonie TArray), ale te w znaczeniu moliwej innoci parametru tego szablonu (U) od parametru T macierzystego szablonu TArray. Zatem przykadowo: jeeli zastosujemy przypisanie tablicy TArray<int> do TArray<float>, to T przyjmie warto float, za U - int. Wszystko jasne? To teraz czas na smakowity deser. Powyszy szablon metody trzeba jeszcze zaimplementowa. No i jak to zrobi? C, nic prostszego. Napiszmy wic t funkcj. Zaczynamy oczywicie od template <...>: template <typename T> W ten sposb niejako otwieramy pierwszy z szablonw - czyli TArray. Ale to jeszcze nie wszystko: mamy przecie w nim kolejny szablon - operator przypisania. Co z tym pocz? Ale tak, potrzebujemy drugiej frazy template <...>: template <typename T> template <typename U> // od szablonu klasy TArray // od szablonu operatora przypisania
licznie to wyglda, no ale to nadal nie wszystko. Dalej jednak jest ju, jak sdz, prosto. Piszemy bowiem zwyky nagwek metody, posikujc si prototypem z definicji klasy. A zatem: template <typename T> template <typename U> TArray<T>& TArray<T>::operator=(const TArray<U>& aTablica) { // ... }
Szablony
497
Stosuj tu takie dziwne formatowanie kodu, aby unaoczni ci jego najwaniejsze elementy. W normalnej praktyce moesz rzecz jasna skondensowa go bardziej, piszc np. obie klauzule template <...> w jednym wierszu i nie wcinajc kodu metody. Wreszcie, czas na ciao funkcji - to chyba najprostsza cz. Robimy podobnie, jak w normalnym operatorze przypisania: najpierw niszczymy wasn tablic obiektu, tworzymy now dla przypisywanej tablicy i kopiujemy jej tre: template <typename T> template <typename U> TArray<T>& TArray<T>::operator=(const TArray<U>& aTablica) { // niszczymy wasn tablic delete[] m_pTablica; // tworzymy now, o odpowiednim rozmiarze m_uRozmiar = aTablica.Rozmiar(); m_pTablica = new T [m_uRozmiar]; // przepisujemy zawarto tablicy przy pomocy ptli for (unsigned i = 0; i < m_uRozmiar; ++i) m_pTablica = aTablica[i]; // zwracamy referencj do wasnego obiektu return *this;
Niespodzianek raczej brak - moe z wyjtkiem ptli uytej do kopiowania zawartoci. Nie posugujemy si tutaj memcpy() z prostego powodu: chcemy, aby przy przepisywaniu elementw zadziaay niejawne konwersje. Dokonuj si one oczywicie w linijce: m_pTablica = aTablica[i]; To wanie ona sprawi, e w razie niedozwolonego przypisywania tablic (np. TArray<std::string> do TArray<double>) kompilacja nie powiedzie si. Natomiast we wszystkich innych przypadkach, jeli istniej niejawne konwersje midzy elementami tablicy, wszystko bdzie w porzdku. Do penego szczcia naleaoby jeszcze w podobny sposb zdefiniowa konstruktor konwertujcy (albo kopiujcy - zaley jak na to patrze), bdcy rwnie szablonem metody. To oczywicie zadanie dla ciebie :)
Tworzenie obiektw
Najbardziej oczywistym sposobem korzystania z szablonu klasy jest tworzenie obiektw bazujcych na specjalizacji tego szablonu.
498
TArray<long> aLongi;
Zaawansowane C++
long jest tu parametrem szablonu TArray. Jednoczenie cay wyraz TArray<long> jest typem zmiennej aLongi. Analogia ze zwykych typw danych dziaa wic tak samo dla klas szablonowych. Docierajc do tego miejsca pewnie przypomniae ju sobie o wskaniku std::auto_ptr z poprzedniego rozdziau. Patrzc na instrukcj jego tworzenia nietrudno wycign wniosek: auto_ptr jest rwnie szablonem klasy. Parametrem tego szablonu jest za typ, na ktry wskanik pokazuje. Przy okazji tego banalnego punktu zwrc jeszcze uwage na pewien fakt skadniowy. Przypumy wic, e zapragniemy stworzy przy uyciu naszego szablonu tablic dwuwymiarow. Pamitajc o tym, e w C++ tablice wielowymiarowe s obsugiwane jako tablice tablic, wyprodukujemy zapewne co w tym rodzaju: TArray<TArray<int>> aInty2D; // no i co tu jest le?...
Koncepcyjnie wszystko jest tutaj w porzdku: TArray<int> jest po prostu parametrem szablonu, czyli okrela tym elementw tablicy - mamy wic tablic tablic elementw typu int. Nieoczekiwanie jednak kompilator wykazuje si tu kompletn ignoracj i zupenym brakiem ogady: problematyczne staj si bowiem dwa zamykajce nawiasy ostre, umieszczone obok siebie. S one interpretowane jako uwaga operator przesunicia bitowego w prawo! Wiem, e to brzmi idiotycznie, bo przecie w tym kontekcie operator ten jest zupenie niemoliwy do zastosowania. Musz wic przeprosi ci za wikszo nierozgarnitych kompilatorw, ktre w tym kontekcie interpretuj sekwencj >> jako operator bitowy123. No dobrze, ale co z tym fantem zrobi? Ot rozwizanie jest nadzwyczaj proste: trzeba oddzieli oba znaki, aby nie mogo ju dochodzi do nieporozumie na linii kompilatorprogramista: TArray<TArray<int> > aInty2D; // i teraz jest OK
Moe wyglda to nieadnie, ale pki co naley tak wanie pisa. Zapamitaj wic, e: W miejsach, gdy w kodzie uywajcym szablonw maj wystpi obok siebie dwa ostre nawiasy zamykajce (>>), naley wstawi midzy nimi spacj (> >), by nie pozwoli na ich interpretacj jako operatora przesunicia. O tym i o podobnych lapsusach jzykowych napomkn wicej w stosownym czasie.
Szablony
499
podanymi parametrami nie reprezentuj klas istniejcych w kodzie programu. S one tylko instrukcjami dla kompilatora, mwicymi mu, by wykona dwie czynnoci: odnalaz wskazany szablon klas (TArray, TDynamicArray ) i sprawdzi, czy podane mu parametry s poprawne wykona jego konkretyzacj, czyli wygenerowa odpowiednie klasy szablonowe Waciwe klasy s wic tworzone dopiero w czasie kompilacji - dziaa to na nieco podobnej zasadzie, jak rozwijanie makr preprocesora, cho jest oczywicie znacznie bardziej zaawansowane. Najwaniejsze dla nas, programistw nie s jednak szczegy tego procesu, lecz jedna cecha kompilatora - bardzo dla nas korzystna. A chodzi o to, e kompilator jest leniwy (ang. lazy)! Jego lenistwo polega na tym, e wykonuje on wycznie tyle pracy, ile jest konieczne do poprawnej kompilacji - i nic ponadto. W przypadku szablonw klas znaczy to po prostu tyle, e: Konkretyzacji podlegaj tylko te skadowe klasy, ktre s faktycznie uywane. Ten bardzo przyjemny dla nas fakt najlepiej zrozumie, jeeli przez chwil wczujemy si w rol leniwego kompilatora. Przypumy, e widzi on tak deklaracj: TArray<CFoo> aFoos; Naturalnie, odszukuje on szablon TArray; przypumy, e stwierdza przy tym, i dla typu CFoo nie by on jeszcze konkretyzowany. Innymi sowy, nie posiada definicji klasy szablonowej dla tablicy elementw typu CFoo. Musi wic j stworzy. C wic robi? Ot w pocie czoa generuje on dla siebie kod w mniej wicej takiej postaci124:
class TArray<CFoo> { static const unsigned DOMYSLNY_ROZMIAR = 5; private: CFoo* m_pTablica; unsigned m_uRozmiar; public: explicit TArray(unsigned uRozmiar = DOMYSLNY_ROZMIAR) : m_uRozmiar(uRozmiar), m_pTablica(new CFoo [m_uRozmiar]) }; {}
Chwila! A gdzie s wszystkie pozostae metody?! Moesz si zaniepokoi, ale poczekaj chwil Powiedzmy, e oto dalej spotykamy instrukcj: aFoos[0] = CFoo("Foooo!"); Co wtedy? Wracamy mianowicie do wygenerowanej przed chwil definicji, a kompilator j modyfikuje i teraz wyglda ona tak:
class TArray<CFoo> { static const unsigned DOMYSLNY_ROZMIAR = 5; private: CFoo* m_pTablica; unsigned m_uRozmiar;
124
500
Zaawansowane C++
public: explicit TArray(unsigned uRozmiar = DOMYSLNY_ROZMIAR) : m_uRozmiar(uRozmiar), m_pTablica(new CFoo [m_uRozmiar]) CFoo& operator[](unsigned uIndeks) }; { return m_pTablica[uIndeks]; } {}
Wreszcie kompilator stwierdza, e wyszed poza zasig zmiennej aFoos. Co wtedy dzieje si z nasz klas? Spjrzmy na ni:
class TArray<CFoo> { static const unsigned DOMYSLNY_ROZMIAR = 5; private: CFoo* m_pTablica; unsigned m_uRozmiar; public: explicit TArray(unsigned uRozmiar = DOMYSLNY_ROZMIAR) : m_uRozmiar(uRozmiar), m_pTablica(new CFoo [m_uRozmiar]) ~TArray() { delete m_pTablica; } CFoo& operator[](unsigned uIndeks) }; { return m_pTablica[uIndeks]; } {}
Czy ju rozumiesz? Przypuszczam, e tak. Zaakcentujmy jednak to wane stwierdzenie: Kompilator konkretyzuje wycznie te metody klasy szablonowej, ktre s uywane. Korzy z tego faktu jest chyba oczywista: generowanie tylko potrzebnego kodu sprawia, e w ostatecznym rozrachunku jest go mniej. Programy s wic mniejsze, a przez to take szybciej dziaaj. I to wszystko dziki lenistwu kompilatora! Czy wic nadal mona podziela pogld, e ta cecha charakteru jest tylko przywar? :)
Sama jej tre do szczeglnie odkrywczych nie naley, a przeznaczenie jest, zdaje si, oczywiste. Spjrzmy raczej na nagwek, bo to on sprawia, e mwimy o tym szablonie w kategoriach wsppracy z szablonem klas TArray. Oto bowiem parametr szablonu TYP
Szablony
501
uywany jest jako parametr od TArray (midzy innymi). Dziki temu mamy wic ogln funkcj do pracy z dowolnym rodzajem tablicy. Taka wsppraca pomidzy szablonami klas i szablonami funkcji jest naturalna. Gdziekolwiek bowiem umiecimy fraz template <...>, powoduje ona uniezalenienie kodu od konkretnego typu danych. A jeli chcemy t niezaleno zachowa, to nieuknione jest tworzenie kolejnych szablonw. W ten sposb skonstruowanych jest mnstwo bibliotek jzyka C++, z Bibliotek Standardow na czele.
502
Zaawansowane C++
unsigned Rozmiar() const { return static_cast<unsigned>(m_strTablica.length()); } bool ZmienRozmiar(unsigned); // ------------------------------------------------------------// operator indeksowania char& operator[](unsigned uIndeks) { return m_strTablica[i]; } // operator rzutowania na typ std::string operator std::string() const { return m_strTablica; }
};
C mona o niej powiedzie? Naturalnie, rozpoczynamy j, jak kad specjalizacj szablonu, od frazy template<>. Nastpnie musimy jawnie poda parametry szablonu (char), czyli nazw klasy szablonowej (TArray<char>). Wymg ten istnieje, bo definicja tej klasy moe by zupenie rna od definicji oryginalnego szablonu! Popatrzmy choby na nasz specjalizacj. Nie uywamy ju w niej tablicy dynamicznej inicjowanej podczas wywoania konstruktora. Zamiast tego mamy obiekt klasy std::string, ktremu w czasie tworzenia tablicy kaemy przechowywa podan liczb znakw. Fakt, e sami nie alokujemy pamici sprawia te, e i sami nie musimy jej zwalnia: napis m_strTablica usunie si sam - zatem destruktor jest ju niepotrzebny. Poza tym nie ma raczej wielu niespodzianek. Do najciekawszych naley pewnie operator konwersji na typ std::string - dziki niemu tablica TArray<char> moe by uywana tam, gdzie konieczny jest acuch znakw C++. Dodanie tej niejawnej konwersji byo gwnym powodem tworzenia wasnej specjalizacji; jak wida, zaoony cel zosta osignity atwo i szybko. Pozostaje jeszcze do zrobienia implementacja metody ZmienRozmiar(), ktr umiecimy poza blokiem klasy. Kod wyglda moe tak: bool TArray<char>::ZmienRozmiar(unsigned uNowyRozmiar) { try { // metoda resize() klasy std::string zmienia dugo napisu m_strTablica.resize (uNowyRozmiar, '\0'); } catch (std::length_error&) { // w razie niepowodzenia zmiany rozmiaru zwracamy false return false; } // gdy wszystko si uda, zwracamy true return true;
Od razu zwrmy uwag na brak klauzuli template<>. Nie ma jej, bowiem tutaj nie mamy do czynienia ze specjalizacj szablonu ZmienRozmiar(). Metoda ta jest po prostu zwyk funkcj klasy TArray<char> - podobnie byo zreszt w oryginalnym szablonie TArray. Implementujemy j wic jako normaln metod. Nie ma tu zatem znaczenia fakt, e metoda ta jest czci specjalizacji szablonu klasy. Najlepiej jest po prostu zapamita, e dany szablon specjalizujemy raz i to wystarczy; gdybymy take tutaj sprbowali doda template<>, to przecie byoby tak, jakbymy ponownie chcieli
Szablony
503
sprecyzowa fragment czego (metod), co ju zostao precyzyjnie okrelone jako cao (klasa). Co do treci metody, to uywamy tutaj funkcji std::string::resize() do zmiany rozmiaru napisyu. Funkcja ta moe rzuci wyjtek w przypadku niepowodzenia. My ten wyjtek przerabiamy na rezultat funkcji: false, jeli wystpi, i true, gdy wszystko si uda.
Jak to zwykle w specjalizacjach, zaczynamy od template<>. Dalej widzimy natomiast normaln w zasadzie definicj destruktora. To, i jest ona specjalizacj metody dla TArray z parametrem int* rozpoznajemy rzecz jasna po nagwku - a dokadniej, po nazwie klasy: TArray<int*>. Reszta nie jest chyba zaskoczeniem. W destruktorze TArray<int*> wpierw wic przechodzimy po caej tablicy, stosujc operator delete dla kadego jej elementu (wskanika). W ten sposb zwalniamy bloki pamici (zmienne dynamiczne), na ktre pokazuj wskaniki. Z kolei po skoczonej robocie pozbywamy si take samej tablicy dokadnie tak, jak to czynilimy w szablonie TArray.
504
Zaawansowane C++
Szablony
unsigned m_uRozmiarY; public: // konstruktor i destruktor explicit TArray(unsigned uRozmiarX = DOMYSLNY_ROZMIAR, unsigned uRozmiarY = DOMYSLNY_ROZMIAR) : m_uRozmiarX(uRozmiarX), m_uRozmiar(uRozmiarY), m_pTablica(new TYP [uRozmiarX * uRozmiarY]) { } ~TArray() { delete[] m_pTablica; }
505
// ------------------------------------------------------------// metody zwracajce wymiary tablicy unsigned RozmiarX() const { return m_uRozmiarX; } unsigned RozmiarY() const { return m_uRozmiarY; } // ------------------------------------------------------------// operator () do wybierania elementw tablicy TYP& operator()(unsigned uX, unsigned uY) { return m_pTablica[uY * m_uRozmiarX + uX]; } }; // (pomijam konstruktor kopiujcy i operator przypisania}
Tak naprawd to w opisywanej sytuacji specjalizacja czciowa niekoniecznie moe by uznawana za najlepsze rozwizanie. Do logiczne jest bowiem zdefiniowanie sobie zupenie nowego szablonu, np. TArray2D i wykorzystywanie go zamiast misternej konstrukcji TArray<TArray<...> >. Poniewa jednak masz tutaj przede wszystkim pozna zagadnienie specjalizacji czciowej, wycz na chwil swj nazbyt czuy wykrywacz naciganych rozwiza i w spokoju kontynuuj lektur :D Rozpoczyna si ona od sekwencji template <typename TYP> (a nie template<>), co moe budzi zaskoczenie. W rzeczywistoci jest to logiczne i niezbdne: to prawda, e mamy do czynienia ze specjalizacj szablonu, jednak jest to specjalizacja czciowa, zatem nie okrelamy explicit wszystkich jego parametrw. Nadal wic posugujemy si faktycznym szablonem - choby w tym sensie, e typ elementw tablicy pozostaje nieznany z gry i musi podlega parametryzacji jako TYP. Klauzula template <typename TYP> jest zatem niezbdna - podobnie zreszt jak we wszystkich przypadkach, gdy tworzymy kod niezaleny od konkretnego typu danych. Tutaj klauzula ta wyglda tak samo, jak w oryginalnym szablnoie TArray. Warto jednak wiedzie, e nie musi wcale tak by. Przykadowo, jeli specjalizowalibymy szablon o dwch parametrach, wwczas fraza template <...> mogaby zawiera tylko jeden parametr. Drugi musiaby by wtedy narzucony odgrnie w specjalizacji. Kompilator wie jednak, e nie jest to taki zwyczajny szablon podstawowy. Dalej bowiem okrelamy dokadnie, o jakie przypadki uycia TArray nam chodzi. S to wic te sytuacje, gdy klasa parametryzowana nazw TYP (TArray<TYP>) sama staje si parametrem szablonu TArray, tworzc swego rodzaju zagniedenie (tablic tablic) TArray<TArray<TYP> >. O tym wiadczy pierwsza linijka naszej definicji, czyli: template <typename TYP> class TArray<TArray<TYP> >
Sam blok klasy wynika bezporednio z tego, e programujemy tablic dwuwymiarow zamiast jednowymiarowej. Mamy wic dwa pola okrelajce jej rozmiar - liczb wierszy i ilo kolumn. Wymiary te podajemy w nowym, dwuparametrowym konstruktorze:
506
Zaawansowane C++
explicit TArray(unsigned uRozmiarX = DOMYSLNY_ROZMIAR, unsigned uRozmiarY = DOMYSLNY_ROZMIAR) : m_uRozmiarX(uRozmiarX), m_uRozmiar(uRozmiarY), m_pTablica(new TYP [uRozmiarX * uRozmiarY])
{ }
Ten za dokonuje alokacji pojedynczego bloku pamici na ca tablic - a o to nam przecie chodzio. Wielko tego bloku jest rzecz jasna na tyle dua, aby pomieci wszystkie elementy - rwna si ona iloczynowi wymiarw tablicy (bo np. tablica 47 ma w sumie 28 elementw, itp.). Niestety, fakt i jest to tablica dwuwymiarowa, uniemoliwia przecienie w prosty sposb operatora [] celem uzyskania dostpu do poszczeglnych elementw tablicy. Zamiast tego stosujemy wic inny rodzaj nawiasw - okrge. Te bowiem pozwalaj na podanie dowolnej liczby argumentw (indeksw); my potrzebujemy naturalnie dwch: TYP& operator()(unsigned uX, unsigned uY) { return m_pTablica[uY * m_uRozmiarX + uX]; } Uywamy ich potem, aby zwrci element o danych indeksach. Wewntrzna m_pTablica jest aczkolwiek ciga i jednowymiarowa (bo ma zajmowa pojedynczy blok pamici), dlatego konieczne jest przeliczenie indeksw. Zajmuje si tym formuka uY * m_uRozmiar + uX, sprawiajc jednoczenie, e elementy tablicy s ukadane w pamici wierszami. Przypadkowo zgadza si to ze sposobem, jaki stosuje kompilator jzyka C++. Na koniec popatrzmy jeszcze na sposb uycia tej (czciowo) specjalizowanej wersji szablonu TArray. Oto przykad kodu, ktry z niej korzysta: TArray<TArray<double> > aMacierz4x4(4, 4); // dostp do elementw tablicy for (unsigned i = 0; i < aMacierz4x4.RozmiarX(); ++i) for (unsigned j = 0; j < aMacierz4x4.RozmiarY(); ++j) aMacierz4x4(i, j) = i + j; Tak wic dziki specjalizacji czciowej klasa TArray<TArray<double> > i inne tego rodzaju mog dziaa poprawnie, co nie bylo moliwe, gdy obecna bya jedynie podstawowa wersja szablonu TArray.
Typowy typ
Zanim jednak popatrzymy na sam technik, popatrzmy na taki oto szablon: // para template <typename TYP1, typename TYP2> struct TPair { // elementy pary TYP1 Pierwszy; TYP2 Drugi; // ------------------------------------------------------------------// konstruktor TPair(const TYP1& e1, const TYP2& e2) : Pierwszy(e1), Drugi(e2) { }
Szablony
};
507
Reprezentuje on par wartoci rnych typw. Taka struktura moe si wydawa lekko dziwaczna, ale zapewniam, e znajduje ona swoje zastosowania w rznych nieprzewidzianych momentach :) Zreszt nie o zastosowania tutaj chodzi, lecz o parametey szablonu. A mamy tutaj dwa takie parametry: typy obu obiektw. Uycie naszej klasy wyglda wic moe chociaby tak: TPair<int, int> TPair<std::string, int> TPair<float, int> Dzielnik(42, 84); Slownie("dwanacie", 12); Polowa(2.5f, 5);
Przypumy teraz, e w naszym programie czsto zdarza si nam, i jeden z obiektw w parze naley do jakiego znanego z gry typu. W kodzie powyej na przykad kada z tych par ma jeden element typu int. Chcc zaoszczdzi sobie koniecznoci pisania tego podczas deklarowania zmiennych, moemy uczyni int argumentem domylnym: template <typename TYP1, typename TYP2 = int> struct TPair { // ... }; Piszc w ten sposb sprawiamy, e w razie niepodania wartoci dla drugiego parametru szablonu, ma on oznacza typ int: TPair<CFoo> TPair<double> TPair<int> Wielkosc(CFoo(), sizeof(CFoo)); Pierwiastek(sqrt(2), 2); DwaRaz(12, 6); // TPair<CFoo, int> // TPair<double, int> // TPair<int, int>
Okrelajc parametr domylny pamitajmy jednak, e: Parametr szablonu moe mie warto domyln tylko wtedy, gdy znajduje si na kocu listy lub gdy wszystkie parametry za nim te maj warto domyln. Niepoprawny jest zatem szablon: template <typename TYP1 = int, typename TYP2> struct TPair; // LE!
Nic aczkolwiek nie stoi na przeszkodzie, aby poda wartoci domylne dla wszystkich parametrw: template <typename TYP1 = std::string, typename TYP2 = int> struct TPair; // OK
Uywajc takiego szablonu, nie musimy ju podawa adnych typw, aczkolwiek naley zachowa nawiasy ktowe: TPair<> Opcja("Ilo plikw", 200); // TPair<std::string, int>
Obecnie domylne argumenty mona podawa wycznie dla szablonw klas. Jest to jednak pozostao po wczesnych wersjach C++, niemajca adnego uzasadnienia, wic jest cakiem prawdopodobne, e ograniczenie to zostanie wkrtce usunite ze Standardu. Co wicej, sporo kompilatorw ju teraz pozwala na podawanie domylnych argumentw szablonw funkcji.
508
Skorzystanie z poprzedniego parametru
Zaawansowane C++
Dobierajc parametr domylny szablonu, moemy te skorzysta z poprzedniego. Oto przykad dla naszej pary: template <typename TYP1, typename TYP2 = TYP1> struct TPair; Przy takim postawieniu sprawy i podaniu jednego parametru szablonu bdziemy mieli pary identycznych obiektw: TPair<int> TPair<std::string> TPair<double> DwaDo(8, 256); Tlumaczenie("tablica", "array"); DwieWazneStale(3.14, 2.71);
Mona jeszcze zauway, e identyczny efekt osignlibymy przy pomocy czciowej specjalizacji szablonu TPair dla tych samych argumentw: template <typename TYP> struct TPair<TYP, TYP> { // elementy pary TYP Pierwszy; TYP Drugi; // ------------------------------------------------------------------// konstruktor TPair(const TYP& e1, const TYP& e2) : Pierwszy(e1), Drugi(e2) { }
};
Domylne argumenty maj jednak t oczywist zalet, e nie zmuszaj do praktycznego dublowania definicji klasy (tak jak powyej). W tym konkretnym przypadku s one znacznie lepszym wyborem. Jeeli jednak posta szablonu dla pewnej klasy parametrw ma si znaczco rni, wwczas dosownie napisana specjalizacja jest najczciej konieczna. *** Na tym koczymy prezentacj szablonw funkcji oraz klas. To aczkolwiek nie jest jeszcze koniec naszych zmaga z szablonami w ogle. Jest bowiem jeszcze kilka rzeczy oglniejszych, o ktrych naley koniecznie wspomnie. Przejdmy wic do kolejnego podrozdziau na temat szablonw.
Wicej informacji
Po zasadniczym wprowadzeniu w tematyk szablonw zajmiemy si nieco szczegowiej kilkoma ich aspektami. Najpierw wic przestudiujemy parametry szablonw, potem za zwrcimy uwag na pewne problemy, jakie moga wynikn podczas stosowania tego elementu jzyka. Najwicej uwagi powicimy tutaj sprawie organizacji kodu szablonw w plikach nagwkowych i moduach, gdy jest to jedna z kluczowych kwestii. Zatem poznajmy szablony troch bliej.
Parametry szablonw
Dowiedziae si na samym pocztku, e kady szablon rozpoczyna si od obowizkowej frazy w postaci:
Szablony
509
[export] template <parametry> O nieobowizkowym sowie kluczowym export powiemy w nastpnej sekcji, w paragrafie omawiajcym tzw. model separacji. Nazywamy j klauzul parametryzacji (ang. parametrization clause). Peni ona w kodzie dwojak funkcj: informuje ona kompilator, e nastpujcy dalej kod jest szablonem. Dziki temu kompilator wie, e nie powinien dla przeprowadza normalnej kompilacji, lecz potraktowa w sposb specjalny - czyli podda konkretyzacji klauzula zawiera te deklaracje parametrw szablonu, ktre s w nim uywane Wanie tymi deklaracjami oraz rodzajami i uyciem parametrw szablonu zajmiemy si obecnie. Na pocztek warto wic wiedzie, e parametry szablonw dzielimy na trzy rodzaje: parametry bdce typami parametry bdce staymi znanymi w czasie kompilacji (tzw. parametry pozatypowe) szablony parametrw Dotychczas w naszych szablonach niepodzielnie krloway parametry bdce typami. Nadal bowiem s to najczciej wykorzystywane parametry szablonw; dotd mwi si nawet, e szablony i kod niezaleny od typu danych to jedno i to samo. My jednak nie moemy pozwoli sobie na ignoracj w zakresie ich parametrw. Dlatego te teraz omwimy dokadnie wszystkie rodzaje parametrw szablonw.
Typy
Parametry szablonw bdce typami stanowi najwiksz si szablonw, przyczyn ich powstania, niespotykanej popularnoci i przydatnoci. Nic wic dziwnego, e pierwsze poznane przez nas przykady szablonw korzystay wanie z parametryzowania typw. Nabrae wic cakiem sporej wprawy w ich stosowaniu, a teraz poznasz kryjc si za tym fasad teorii ;)
510
Zaawansowane C++
Niemniej powyszy sposb moe ci z pocztku pomc, jeli dotd nie rozumiae idei parametru szablonu bdcego typem.
Stae
Cenn waciwoci szablonw jest moliwo uycia w nich innego rodzaju parametrw ni tylko typy. S to tak zwane parametry pozatypowe (ang. non-type parameters), a dokadniej mwic: stae.
125
Szablony
511
Jak susznie zauwaye, szablon ten zawiera dwa parametry. Pierwszy z nich to typ elementw tablicy, deklarowany w znany sposb poprzez typename. Natomiast drugi parametr jest wanie przedmiotem naszego zainteresowania. Stosujemy w nim typ unsigned, wobec czego bdzie on sta tego wanie typu. Popatrzmy najlepiej na sposb uycia tego szablonu: TStaticArray<int, 10> a10Intow; // 10-elementowa tablica typu int TStaticArray<float, 20> a20Floatow; // 20 liczb typu float TStaticArray< TStaticArray<double, 5>, 8> a8x5Double; // tablica 85 liczb typu double Podobnie jak w przypadku parametrw bdcych typami moesz sobie wyobrazi, e kompilator konkretyzuje szablon, definiujc warto N jako sta. Klasa TStaticArray<float, 10> odpowiada wic mniej wicej zapisowi w takiej postaci: typedef float T; const unsigned N = 10; class TStaticArray { private: T m_aTablica[N]; }; // ...
Wynika z niego przede wszystkim to, i: Parametry pozatypowe szablonw s traktowane wewntrz nich jako stae.
512
Zaawansowane C++
Oznacza to przede wszystkim, e musz by one wywoywane z wartocami, ktre s obliczalne podczas kompilacji. Wszystkie pokazane powyej konkretyzacje s wic poprawne, bo 10, 20, 5 i 8 s rzecz jasna staymi dosownymi, a wic znanymi w czasie kompilacji. Nie byoby natomiast dozwolone uycie szablonu jako TStaticArray<typ, zmienna>, gdzie zmienna niezadeklarowana zostaa z przydomkiem const.
Wida tutaj, e parametr pozatypowy moe by z rwnym powodzeniem uyty zarwno w nagwku funkcji (typ const TStaticArray<T, N>&), jak i w jej wntrzu (warunek zakoczenia ptli for).
Teoretycznie powinno by to jak najbardziej moliwe. Pierwszym 10 elementw tablicy a20Intow mogoby by przecie zastpione zawartoci zmiennej a10Intow. Nie ma zatem przeciwwskaza. Niestety, kompilator odrzuci taki kod, mwic, i nie znalaz adnego pasujcego operatora przypisania ani niejawnej konwersji. I bdzie to szczera prawda! Musimy bowiem pamita, e: Szablony klas konkretyzowane innym zestawem parametrw s zupenie odmiennymi typami. Nic wic dziwnego, e TStaticArray<int, 10> i TStaticArray<int, 20> s traktowane jako odrbne klasy, niezwizane ze sob (obie te nazwy, wraz z zawartoci nawiasw ktowych, s bowiem nazwami typw, o czym przypominam po raz ktry). W takim wypadku domylnie generowany operator przypisania zawodzi. Warto wic pamita o powyszej zasadzie. No ale skoro mamy ju taki problem, to przydaoby si go rozwiza. Odpowiednim wyjciem jest wasny operator przypisania zdefiniowany jako szablon skadowej: template <typename T, unsigned N> class TStaticArray {
Szablony
// ... public: // operator przypisania jednej tablicy do drugiej template <typename T2, unsigned N2> TStaticArray& operator=(const TStaticArray<T2, N2>& aTablica) { // kontrola przypisania zwrotnego if (&aTablica != this) { // sprawdzenie rozmiarw if (N2 > N) throw "Za duza tablica"; // przepisanie tablicy for (unsigned i = 0; i < N2; ++i) (*this)[i] = aTablica[i];
513
} }
return *this;
};
Moe i wyglda on nieco makabrycznie, ale w gruncie rzeczy dziaa na identycznej zasadzie jak kady rozsdny operator przypisania. Zauwamy, e parametryzacji podlega w nim nie tylko rozmiar rdwej tablicy (N2), ale te typ jej elementw (T2). To, czy przypisanie faktycznie jest moliwe, zaley od tego, czy powiedzie si kompilacja instrukcji: (*this)[i] = aTablica[i]; A tak bdzie oczywicie tylko wtedy, gdy istnieje niejawna konwersja z typu T2 do T.
514
Zaawansowane C++
Gorzej wyglda sprawa z uyciem takiego szablonu. Ot nie moemy przekaza mu wskanika ani na obiekt chwilowy, ani na obiekt lokalny, ani nawet na obiekt o zasigu moduowym. Nie jest wic poprawny np. taki kod: int nZmienna; TClass<&nZmienna> Obiekt; // LE! Wskanik na obiekt lokalny
Wyjanienie jest tu proste. Wszystkie takie obiekty maj po prostu zbyt may zakres, ktry nie pokrywa si z widocznoci konkretyzacji szablonu. Aby tak byo, obiekt, na ktry wskanik podajemy, musiaby by globalny (czony zewntrznie): extern int g_nZmienna = 42; // ... TClass<&g_Zmienna> Cos;
// OK
Z identycznych powodw nie mona do szablonw przekazywa acuchw znakw: template <const char[] S> class TStringer TStringer<"Hmm..."> Napisowiec; { /* ... */ }; // NIE!
acuch "Hmm..." jest tu bowiem obiektem chwilowym, zatem szybko przestaby istnie. Typ TStringer<"Hmm..."> musiaby natomiast egzystowa i by potencjalnie dostpnym w caym programie. To oczywicie wzajemnie si wyklucza.
Inne restrykcje
Oprcz powyszych obostrze s jeszcze dwa inne. Po pierwsze, w charakterze parametrw szablonu nie mona uywa obiektw wasnych klas. Ponisze szablony s wic niepoprawne: template <CFoo F> class TMetaFoo template <std::string S> class TStringTemplate { /* ... */ }; { /* ... */ };
Poza tym, w charakterze parametrw pozatypowych teoretrycznie niedozwolone s wartoci zmiennoprzecinkowe: template <float F> class TCalc { /* ... */ };
Mwi teoretycznie, gdy wiele kompilatorw pozwala na ich uycie. Nie ma bowiem ku temu adnych technicznych przeciwwskaza (w odrnieniu od pozostaych ogranicze parametrw pozatypowych). Niemniej, w Standardzie C++ nadal zakorzenione jest to przestarzae ustalenie. Zapewne jednak tylko kwesti czasu jest jego usunicie.
Szablony parametrw
Ostatnim rodzajem parametrw s tzw. szablony parametrw szablonw (ang. template templates parameters). Pod t dziwnie brzmic nazw kryje si moliwo przekazania jako parametru nie konkretnego typu, ale uprzednio zdefiniowanego szablonu. Poniewa zapewnie nie brzmi to zbyt jasno, najrozsdniej bdzie doj do sedna sprawy przy pomocy odpowiedniego przykadu.
Idc za potrzeb
A wic Swego czasu stworzylimy sobie szablon oglnej klasy TArray. Okazuje si jednak, e niekiedy moe by on niewystarczajcy. Chocia dobrze nadaje si do samej czynnoci przechowywania wartoci, nie pomylelimy o adnych mechanizmach operowania na tyche wartociach.
Szablony
515
Z drugiej strony, nie ma sensu zmiany dobrze dziaajcego kodu w co, co nie zawsze bdzie nam przyadtne. Takie czynnoci jak dodawnie, odejmowanie czy mnoenie tablic maj bowiem sens tylko w przypadku wektorw liczb. Lepiej wic zdefiniowa sobie nowy szablon do takich celw: template <typename T> class TNumericArray { private: // wewntrzna tablica TArray<T> m_aTablica; public: // ... // jakie operatory... // (np. indeksowania) TNumericArray operator+(const TNumericArray& aTablica) { TNumericArray Wynik(*this); for (unsigned i = 0; i < Wynik.Rozmiar(); ++i) Wynik[i] += aTablica[i]; } }; return Wynik;
// (itp.)
W sumie nic specjalnego nie moemy powiedzie o tym szablonie klasy TNumericArray. Jak si pewnie domylasz, to si za chwil zmieni :)
Domylnie byby to nadal szablon TArray, niemniej przy takim szablonie TNumericArray monaby w miar atwo deklarowa zarwno due, jak i mae tablice: TNumericArray<int> TNumericArray<float, TOptimizedArray<float> > TNumericArray<double, TSuperFastArray<double> > aMalaTablica(50); aDuzaTablica(1000); aGigaTablica(250000);
516
Zaawansowane C++
W tym przykadzie zakadamy oczywicie, e TOptimizedArray i TSuperFastArray s jakimi uprzednio zdefiniowanymi szablonami tablic efektywniejszych od TArray. W uzasadnionych przypadkach duej liczby elementw ich uycie jest wic pewnie podane, co te czynimy.
Drobna niedogodno
Powysze rozwizanie ma jednak pewien drobny mankament skadniowy. Nietrudno mianowicie zauway, e dwa razy piszemy w nim typ elementw tablic - float i double. Pierwszy raz jest on podawany szablonowi TNumericArray, a drugi raz - szablonowi wewntrznej tablicy. W sumie powoduje to zbytni rozwleko nazwy caego typu TNumericArray<...>, a na dodatek ujawnia osawiony problem nawiasw ostrych. Wydaje si przy tym, e informacj o typie podajemy o jeden raz za duo; w kocu zamiast deklaracji: TNumericArray<float, TOptimizedArray<float> > TNumericArray<double, TSuperFastArray<double> > rwnie dobrze mogoby si sprawdza co w tym rodzaju: TNumericArray<float, TOptimizedArray> TNumericArray<double, TSuperFastArray> aDuzaTablica(1000); aGigaTablica(250000); aDuzaTablica(1000); aGigaTablica(250000);
Problem jednak w tym, e parametry szablonu TNumericArray - TOptimizedArray i TSuperFastArray nie s zwykymi typami danych (klasami), wic nie pasuj do deklaracji typename TAB. One same s szablonami klas, zdefiniowanymi zapewne kodem podobnym do tego: template <typename T> class TOptimizedArray template <typename T> class TSuperFastArray { /* ... */ }; { /* ... */ };
Mona wic powiedzie, e wystpuje to swoista niezgodno typw midzy pojciami typ i szablon klasy. Czy zatem nasz pomys skrcenia sobie zapisu trzeba odrzuci?
Szablony
517
Posugujemy si tu dwa razy sowem kluczowym template. Pierwsze uycie jest ju powszechnie znane; drugie wystpuje w licie parametrw szablonu TNumericArray i o nie nam teraz chodzi. Przy jego pomocy deklarujemy bowiem szablon parametru. Skadnia: template <typename> class TAB oznacza tutaj, e do parametru TAB pasuj wszystkie szablony klas (template <...> class), ktre maj dokadnie jeden parametr bdcy typem (typename126). W przypadku niepodania adnego szablonu, zostanie wykorzystany domylny - TArray. Teraz, gdy nazwa TAB jest ju nie klas, lecz jej szablonem, uywamy jej tak jak szablonu. Deklaracja pola wewntrznej tablicy wyglda wic nastpujco: TAB<T> m_aTablica; Jako parametr dla TAB podajemy T, czyli pierwszy parametr naszego szablonu TNumericArray. W sumie jednak monaby uy dowolnego typu (take podanego dosownie, np. int), bo TAB zachowuje si tutaj tak samo, jak penoprawny szablon klasy. Naturalnie, teraz poprawne staj si propozycje deklaracji zmiennych z poprzedniego akapitu: TNumericArray<float, TOptimizedArray> TNumericArray<double, TSuperFastArray> aDuzaTablica(1000); aGigaTablica(250000);
Na ile przydatne s szablony parametrw szablonw (zwane te czasem metaszablonami - ang. metatemplates) musisz si waciwie przekona sam. Jest to jedna z tych cech jzyka, dla ktrych trudno od razu znale jakie oszaamiajce zastosowanie, ale jednoczenie moe okaza si przydatna w pewnych szczeglnych sytuacjach.
Problemy z szablonami
Szablony s rzeczywicie jednym z najwikszych osigni jzyka C++. Jednak, jak to jest z wikszoci zaawansowanych technik, ich stosowanie moe za soba pociga pewne problemy. Nie, nie chodzi mi tu wcale o to, e szablony s trudne do nauczenia, cho pewnie masz takie nieodparte wraenie ;) Chciabym raczej porozmawia o kilku puapkach czyhajcych na programist (szczeglnie pocztkujcego), ktry zechce uywa szablonw. Dziki temu by moe atwiej unikniesz mniej lub bardziej powanych problemw z tymi konstrukcjami jzykowymi. Zobaczmy wic, co moe stan nam na drodze
126 Nie podajemy nazwy parametru szablonu TAB, bo nie ma takiej potrzeby. Nazwa ta nie jest nam po prostu do niczego potrzebna.
518
Zaawansowane C++
Na kompilatorze spoczywa mnstwo trudnych zda, jeli chodzi o kod wykorzystujcy szablony. Dlatego te niekiedy potrzebuje on wsparcia ze strony programisty, ktre uatwioby mu intepretacj kodu rdowego. O takich wanie uatwieniach dla kompilatora traktuje niniejszy paragraf.
Nawiasy ostre
Niejednego nowicjusza w uywaniu szablonw zjad smok o nazwie problem nawiasw ostrych. Nietrudno przecie wyprodukowa taki kod, wierzc w jego poprawno: typedef TArray<TArray<double>> MATRIX; // oj!
Ta wiara zostaje jednak doc szybko podkopana. Coraz czciej wprawdzie zdarza si, e kompilator poprawnie rozpoznaje znaki >> jako zamykajce nawiasy ostre. Niemniej, nadal moe to jeszcze powodowa bd lub co najmniej ostrzeenie. Poprawna wersja kodu, dziaajca w kadej sytuacji, to oczywicie: typedef TArray<TArray<double> > MATRIX; // OK
Dodatkowa spacja wyglda tu rzecz jasna bardzo nieadnie, ale pki co jest konieczna. Wcale niewykluczone jednak, e za jaki czas take pierwsza wersja instrukcji typedef bdzie musiaa by uznana za poprawn.
Nieoczywisty przykad
Mona susznie sdzi, e w powyszym przykadzie rozpoznanie sekwencji >> jako pary nawiasw zamykajcych (a nie operatora przesunicia w prawo) nie jest zadaniem ponad siy kompilatora. Pamitajmy aczkolwiek, e nie zawsze jest to takie oczywiste. Spjrzmy choby na tak deklaracj: TStaticArray<int, 16>>2> aInty; // chyba prosimy si o kopoty...
Dla czytajcego (i piszcego) kod czowieka jest cakiem wyrane widoczne, e drugim parametrem szablonu TStaticArray jest tu 16>>2 (czyli 64). Kompilator uczulony na problem nawiasw ostrych zinterpretuje aczkolwiek ponisz linijk jako: TStaticArray<int, 16> >2> aInty; // ojej!
W sumie wic nie bardzo wiadomo, co jest lepsze. Waciwie jednak wyraenia podobne do powyszego s raczej rzadkie i prawd mwic nie powinny by w ogle stosowane. Gdyby zachodzia taka konieczno, najlepiej posuy si pomocniczymi nawiasami okrgymi: TStaticArray<int, (16>>2)> aInty; // OK
Wniosek z tego jest jeden: kiedy chodzi o nawiasy ostre i szablony, lepiej by wyrozumiaym dla kompilatora i w odpowiednich miejscach pomc mu w zrozumieniu, o co nam tak naprawd chodzi.
Szablony
519
Przyczyn jest po czci sposb, w jaki kompilatory C++ dokonuj analizy kodu. Dokadne omwienie tego procesu jest skomplikowane i niepotrzebne, wic je sobie darujemy. Interesujc nas czynnoci jest aczkolwiek jeden z pierwszych etapw przetwarzania - tak zwana tokenizacja (ang. tokenization). Tokenizacja polega na tym, i kompilator, analizujc kod znak po znaku, wyrnia w nim elementy leksykalne jzyka - tokeny. Do tokenw nale gwnie identyfikatory (nazwy zmiennych, funkcji, typw, itp.) oraz operatory. Kompilator wpierw dokonuje ich analizy (parsowania) i tworzy list takich tokenw. Sk polega na tym, e C++ jest jzykiem kontekstowym (ang. context-sensitive language). Oznacza to, e identyczne sekwencje znakw mog w nim znaczy zupenie co innego w zalenoci od kontekstu. Przykadowo, fraza a*b moe by zarwno mnoeniem zmiennej a przez zmienn b, jak te deklaracj wskanika na typ a o nazwie b. Wszystko zaley od znaczenia nazw a i b. W przypadku operatorw mamy natomiast jeszcze jedn zasad, zwan zasad maksymalnego dopasowania (ang. maximum match rule). Mwi ona, e naley zawsze prbowa uj jak najwicej znakw w jeden token. Te dwie cechy C++ (kontekstowo i maksymalne dopasowanie) daj w efekcie zaprezentowane wczeniej problemy z nawiasami ostrymi. Problem jest bowiem w tym, i zalenie od kontekstu i ssiedztwa znaki < i > mog by interpretowane jako: operatory wikszoci i mniejszoci operatory przesunicia bitowego nawiasy ostre Nie ma to wikszego znaczenia, jeli nie wystpuj one w bliskim ssiedztwie. W przeciwnym razie zaczynaj si powane kopoty - jak choby tutaj: TSomething<32>>4 > FOO> CosTam; // no i?...
Najlogiczniej wic byoby unika takich ryzykownych konstrukcji lub opatrywa je dodatkowymi znakami (spacjami, nawiasami okrgymi), ktre umoliwi kompilatorowi jednoznaczn interpretacj.
Nazwy zalene
Problem nawiasw ostrych jest w zasadzie kwesti wycznie skadniow, spowodowan faktem wyboru takiego a nie innego rodzaju nawiasw do wsppracy z szablonami. Jednak jeli nawet sprawy te zostayby kiedy rozwizane (co jest mao prawdopodobne, zwaywszy, e pitego rodzaju nawiasw jeszcze nie wymylono :D), to i tak kod szablonw w pewnych sytuacjach bdzie kopotliwy dla kompilatora. O co dokadnie chodzi? Ot trzeba wiedzie, e szablony s tak naprawd kompilowane dwukrotnie (albo raczej w dwch etapach): najpierw s one analizowane pod ktem ewentualnych bdw skadniowych i jzykowych w swej czystej (nieskonkretyzowanej) postaci. Na tym etapie kompilator nie ma informacji np. o typach danych, do ktrych odnosz symboliczne oznaczenia parametrw szablonw (T, TYP, itd.) pniej produkty konkretyzacji s sprawdzane pod ktem swej poprawnoci w cakiem normalny ju sposb, zbliony do analizy zwykego kodu C++ Nie byoby w tym nic niepokojcego gdyby nie fakt, e w pewnych sytuacjach kompilator moe nie by wystarczajco kompetentny, by wykona faz pierwsz. Moe si bowiem okaza, e do jej przeprowadzania wymagane s informacje, ktre mona uzyska dopiero po konketyzacji, czyli w fazie drugiej.
520
Zaawansowane C++
Pewnie w tej chwili nie bardzo moesz sobie wyobrazi, o jakie informacje moe tutaj chodzi. Powiem wic, e sprawa dotyczy gwnie waciwej interpretacji tzw. nazw zalenych. Nazwa zalena (ang. dependent name) to kada nazwa uyta wewntrz szablonu, powizana w jaki sposb z jego parametrami. Fakt, e nazwy takie s powizane z parametrami szablonu, sprawia, e ich znaczenie moe by rne w zalenoci od parametrw tego szablonu. Te wszystkie engimatyczne stwierdzenia stan si bardziej jasne, gdy przyjrzymy si konkretnym przykadom problemw i sposobom na ich rozwizanie.
Mona si zdziwi, czemu parametrem szablonu jest tu typ tablicy (czyli np. TArray<int>), a nie typ jej elementw (int). Dziki temu funkcja jest jednak bardziej uniwersalna i niekoniecznie musiy wsppracowa wycznie z tablicami TArray. Przeciwnie, moe dziaa dla kadej klasy tablic (a wic np. dla TOptimizedArray i TSuperFastArray z paragrafiu o metaszablonach), ktra ma: operator indeksowania metod Rozmiar() alias ELEMENT na typ elementw tablicy Niestety, ten ostatni punkt jest wanie problemem. cilej mwic, to fraza TAB::ELEMENT stanowi kopot - ELEMENT jest tu bowiem nazw zalen. My jestemy tu wicie przekonani, e reprezentuje ona typ (int dla TArray<int>, itd.), jednak kompilator nie moe bra takich informacji znikd. On faktycznie musi to wiedzie, aby mg uzna m.in. deklaracj: TAB::ELEMENT Wynik; za poprawn. A skd ma si tego dowiedzie? Nie ma ku temu adnej moliwoci na etapie analizy samego szablonu. Dopiero konkretyzacja, gdy TAB jest zastpowane prawdziwym typem danych, daje mu tak moliwo. Tyle e aby w ogle mogo doj do konkretyzacji, szablon musi najpierw przej test poprawnoci. Mwic wprost: aby skontrolowa bezbdno szablonu kompilator musi najpierw skontrolowa bezbdno szablonu :D Dochodzimy zatem do bdnego koa. A wyjcie z niego jest jedno. Musimy w jaki sposb da do zrozumienia kompilatorowi, e TAB::ELEMENT jest typem, a nie statycznym polem - bo taka jest wanie druga
Szablony
moliwa interpretacja tej konstrukcji. Czynimy to poprzedzajc problematyczn fraz swkiem typename: typename TAB::ELEMENT Wynik;
521
Deklaracja nieco nam si rozwleka, ale w przy korzystaniu z szablonw jest to ju chyba regu :) W kadym razie teraz nie bdzie ju problemw ze zmienn Wynik; do penej satysfakcji naley jeszcze podobny zabieg zastosowa wobec typu zwracanego przez funkcj: template <class TAB> typename TAB::ELEMENT Najwiekszy(const TAB& aTablica) Podobnie naley postpi z kadym wystpieniem TAB::ELEMENT w tym szablonie. Powiem nawet wicej, formuujc ogln zasad: Naley poprzedza sowem typename kad nazw zalen, ktra ma by interpretowana jako typ danych. Stosujc si do niej, nie bdziemy wprawia w kompilatora w zakopotanie i oszczdzimy sobie dziwnie wygldajcych komunikatw o bdach.
522
Zaawansowane C++
Wiem, e wyglda to jak skryowanie trolla z goblinem, ale mwimy teraz o naprawd specyficznym szczegliku, ktrego uycie jest bardzo rzadkie. Powyszy kod wyglaby pewnie przejrzyciej, gdyby usun z niego wyrazy typename i template: // UWAGA: ten kod NIE JEST poprawny! // wywoanie jako statycznej metody bez obiektu TFoo<T>::TBar<T>::Baz(); // utworzenie lokalnego obiektu i wywoanie metody TFoo<T>::TBar<T> Bar; Bar.Baz<T>(); // utworzenie dynamicznego obiektu i wywoanie metody TFoo<T>::TBar<T>* pBar; pBar = new TFoo<T>::TBar<T>; pBar->Baz<T>(); delete pBar; Tym samym jednak pozbawiamy kompilator informacji potrzebnych do skompilowania szablonu. Rol typename znamy, wic zajmijmy si dodatkowymi uyciami template. Ot tutaj template (a waciwie ::template, .template i ->template) suy do poinformowania, e nastpujca dalej nazwa zalena jest szablonem. Patrzc na definicj TFoo wiemy to oczywicie, jednak kompilator nie dowie si tego a do chwili konkretyzacji. Dla niego nazwy TBar i Baz mog by rwnie dobrze skadowymi statycznymi, za nastpujce dalej znaki < i > - operatorami relacji. Musimy wic wyprowadzi go bdu. Stosuj kontrukcje ::template, .template i ->template zamiast samych operatorw ::, . i -> w tych miejscach, gdzie podana dalej nazwa zalena jest szablonem. Stosowalno tych konstrukcji jest wic ograniczona i zawa si do przypadkw zagniedonych szablonw. W codziennej i nawet troch bardziej niecodziennej praktyce programistycznej mona si bez nich obej, aczkolwiek warto o nich wiedzie, by mc je zastosowa w tych nielicznych sytuacjach ujawniajcej si niewiedzy kompilatora.
Model wczania
Najwczeniejszym i do dzi najpopularniejszym sposobem zarzdzania szablonami jest model wczania (ang. inclusion model). Jest on jednoczenie cakiem prosty w stosowaniu i czsto wystarczajcy. Przyjrzyjmy mu si.
Szablony
523
Zwyky kod
Wpierw jednak przypomnimy sobie, jak naley radzi sobie z kodem C++ bez szablonw. Ot, jak wiemy, wyrniamy w nim pliki nagwkowe oraz moduy kodu. I tak: pliki nagwkowe s opatrzone rozszerzeniami .h, .hh, .hpp, lub .hxx i zawieraj deklaracje wspuytkowanych czci kodu. Nale do nich: prototypy funkcji deklaracje zapowiadajce zmiennych globalnych (opatrzone sowem extern) definicje wasnych typw danych i aliasw, wprowadzane sowami typedef, enum, struct, union i class implementacje funkcji inline moduy kodu s z kolei wyrzniane rozszerzeniami .c, .cc, .cpp lub .cxx i przechowuj definicje (tudzie implementacje) zadeklarowanych w nagwkach elementw programu. S to wic: instrukcje funkcji globalnych oraz metod klas deklaracje zmiennych globalnych (bez extern) i statycznych pl klas Ten system, spity dyrektywami #include, dziaa wymienicie, oddzielajc to, co jest wane do stosowania kodu od technicznych szczegw jego implementacji. Co si jednak dzieje, gdy na scen wkraczaj szablony?
524
std::cin >> fLiczba1; std::cin >> fLiczba2;
Zaawansowane C++
std::cout << "Wieksza jest liczba " << max(fLiczba1, fLiczba2); getch();
Pieczoowicie wykonujc te proste w gruncie rzeczy czynnoci, mamy prawo czu si zaskoczeni efektami. Prba wygenerowania gotowego programu skoczy si bowiem komunikatem linkera zblionym do poniszego:
error LNK2019: unresolved external symbol "double __cdecl max(double,double)" (?max@@YANNN@Z) referenced in function _main
Wynika z niego klarownie, e funkcja max() w wersji skonkretyzowanej dla double nie istnieje! Jak to wyjani? Wytumaczenie jest w miar proste. Zwr uwag, e doczenie pliku max.hpp wcza do main.cpp jedynie deklaracj szablonu, a nie jego definicj. Nie majc definicji kompilator nie moe natomiast skonkretyzowa szablonu dla parametru double. Wobec tego czyni on zaoenie, e funkcja max<double>() zostaa wygenerowana gdzie indziej. Nie ma w tym nic zdronego - ten sam mechanizm dziaa przecie dla zwykych funkcji, ktre s deklarowane (prototypowane) w pliku nagwkowym, a implementowane w innym module. Niestety, w tym przypadku jest to zaoenie bdne: konkretyzacja nie zostanie bowiem przeprowadzona z powodu wspomnianego braku informacji (definicji szablonu). Ostatecznie wic powstaje zewntrzne dowizanie do specjalizacji szablonu max() dla parametru double - specjalizacji, ktra nie istnieje! Ten fakt nie umknie ju uwadze linkera, czego skutkiem jest zaprezentowany wyej bd i poraka konsolidacji.
Szablony
525
W sumie mona wic powiedzie, e model wczania jest zadowolajcym sposobem zarzdzania kodem szablonw. Nie jest to jednak wystarczajcy argument za tym, aby nie przyjrze si take innym modelom :)
Konkretyzacja jawna
W bdnym przykadzie programu z szablonem max() problem polega na tym, e kompilator nie mia okazji do waciwego skonkretyzowania szablonu. Model wczania umoliwia mu to w sposb automatyczny. Istnieje aczkolwiek inna metoda na rozwizanie tego problemu. Moemy mianowicie zastosowa model konkretyzacji jawnej (ang. explicit instantiation) i przej kontrol nad procesem rozwijania szablonw. Zobaczmy zatem, jak mona to zrobi.
526
Zaawansowane C++
umoliwie dokadnego okrelenia miejsca (moduu kodu), w ktrym egzemplarz szablonu (specjalizacja) zostanie utworzony
W wikszoci przypadkw te argumenty nie s jednak wystarczajce, aby mogy przeway na rzecz wykorzystania modelu konkretyzacji jawnej w praktyce. Podobnie bowiem jak w przypadku modelu wczania, rozrost programu powoduje take wyduenie czasu przeznaczonego na konkretyzacj. Rnica tkwi jednake w tym, e w tym pierwszym modelu ca prac zajmuje si kompilator, ktry i tak nie ma nic ciekawszego do roboty, natomiast konkretyzacja jawna zrzuca ten obowizek na barki wiecznie zapracowanego programisty. W sumie wic ten model organizacji szablonw trudno uzna za praktyczny i wygodny. By moe sprawdziby si niele w maych programach, ale tam mona sobie przecie tym bardziej pozwoli na znacznie wygodniejszy model wczania.
Model separacji
Lekarstwem na bolczki modelu wczania ma by mechanizm eksportowania szablonw. Technika ta, nazywana rwnie modelem separacji, jest czci samego jzyka C++ i teoretycznie jest to wanie ten sposb zarzdzania kodem szablonw, ktry ma by preferowany. Przynajmniej tako rzecze Standard C++. Tym niemniej ju od razu powiadomi, e w miar poprawna obsuga tego modelu jest dostpna dopiero w Visual Studio .NET 2003. Wypadaoby zatem pozna bliej to natywne rozwizanie samego jzyka.
Szablony eksportowane
Idea tego modelu jest generalnie bardzo prosta: zachowany zostaje naturalny porzdek oddzielania deklaracji/definicji od implementacji. W pliku nagwkowym umieszczamy wic wycznie deklaracje (prototypy) szablonw funkcji oraz definicje szablonw klas. Postepujemy zatem tak, jak prbowalimy czyni na samym pocztku - dopki linker nie sprowadzi nas na ziemi zmiana polega jedynie na tym, e deklaracj szablonu w pliku nagwkowym opatrujemy sowem kluczowym export Stosujc te dwie wskazwki do naszego bdnego przykadu TemplatesTryout, naleaoby jedynie zmodyfikowa plik max.hpp. Zmiana ta jest zreszt niemal kosmetyczna: // max.hpp // prototyp szablonu max() jako szablon eksportowany export template <typename T> T max(T, T); Jak si wydaje, dodanie sowa export przed deklaracj szablonu zaatwia spraw. W rzeczywistoci sowo to powinno si znale przed kadym uyciem klauzuli template <...>. export ma jednak t przyjemn waciwo, e po jednokrotnym jego zastosowaniu w obrbie danego pliku z kodem wszystkie dalsze szablony otrzymuj ten przydomek niejawnie. A dziki temu, e w pliku max.cpp znajduje si odpowiednia dyrektywa #include: // max.cpp #include "max.hpp"
Szablony
// (dalej implementacja szablonu max())
527
rwnie kod szablonu funkcji max() dostaje modyfikator export w prezencie od pliku nagwkowego max.hpp. Jeli wic zdecydujemy si pisa kod szablonw w identyczny sposb, jak zwyky kod C++, to nasza troska o waciw kompilacj szablonw powinna ogranicza si do dodawania sowa kluczowego export przed deklaracjami template <...> w plikach nagwkowych. Przynajmniej teoretycznie tak wanie powinno by
528
Zaawansowane C++
Pomys jest prosty. Naley tak zmodyfikowa plik nagwkowy z deklaracj szablonu (u nas max.hpp), by w razie potrzeby zawiera on rwnie jego definicj - czyli wcza j z moduu kodu (max.cpp). Oto propozycja takiej modyfikacji: // max.hpp // zabezpieczenie przed wielokrotnym doczaniem - wane! #pragma once // w zalenoci od tego, czy zdefiniowano makro USE_EXPORT, // wprowadzamy do programu sowo kluczowe export #ifdef USE_EXPORT #define EXPORT export #else #define EXPORT #endif // deklaracja szablonu EXPORT template <typename T> T max(T, T); // jeeli nie uywamy modelu separacji, to potrzebujemy take // definicji szablonu. Wczamy j wic #ifndef USE_EXPORT #include "max.cpp" #endif Decyzja co do uywanego modelu ogranicza si tu bdzie do zdefiniowania lub niezdefiniowania makra USE_EXPORT przed doczeniem pliku max.hpp: // uywanie modelu separacji; bez #define bdzie to model wczania #define USE_EXPORT #include "max.hpp" Trzeba jeszcze pamita, aby w tym pliku nagwkowym przynajmniej pierwsz deklaracj szablonu (a najlepiej wszystkie) opatrzy nazw makra EXPORT. W zalenoci od wybranego modelu bdzie ono bowiem rozwinite do sowa export lub do pustego cigu, co w wyniku da nam zastosowanie wybranego modelu. Opisana sztuczka opiera si, w przypadku uycia modelu wczania, o sprzenie zwrotne dyrektyw #include: max.hpp docza bowiem max.cpp, za max.cpp prbuje doczy max.hpp. Trzeba rzecz jasna zadba o to, by ta druga prba nie zakoczya si powodzeniem, stosujc jedno z zabezpiecze przeciw wielokrotnemu doczaniu. Tutaj uyem #pragma once, cho metoda z unikalnym makrem oraz #ifndef/#endif rwnie zdaaby egzamin. *** I tak oto zakoczylimy drugi podrozdzia powicony opisowi szablonw w C++. W zasadzie moesz uzna ten moment za koniec teorii tego skomplikowanego zagadnienia. Chocia wic zajmowalimy si ju sprawami bardziej praktycznymi (jak choby modelem organizacji kodu), to dopiero w nastpnym podrozdziale poznasz prawdziwe zastosowania szablonw. Zacznie si wic robi bardzo ciekawie, jako e dopiero w konkretnych metodach na wykorzystanie szablonw wida prawdziw potg tego skadnika C++. Pora zatem j ujarzmi!
Szablony
529
Zastosowania szablonw
Jeszcze w pocztkach tego rozdziau powiedziaem, do czego su szablony w jzyku C++. Przypominam: stosujemy je gwnie tam, gdzie chcemy uniezaleni kod programu od konkretnego typu danych. To oglnikowe stwierdzenie jest z pewnoci pomocne, ale mao konkretne. Na pewno bdziesz bardziej zadowolony, jeeli ujrzysz jakie precyzyjniej okrelone zastosowania dla szablonw. I to jest wanie treci tego podrozdziau. Pomwimy sobie wic o niektrych sytuacjach, gdy skorzystanie z szablonw uatwia lub wrcz umoliwia wykonanie wanych programistycznych zada.
Zastpienie makrodefinicji
Gdyby to bya bajka, to zaczoby si tak: dawno, dawno temu w krlestwie Elastycznych Programw niepodzielnie rzdzia okrutna kasta Makrodefinicji. Do czsto utrudniaa ona ycie mieszkacom, powodujc wiksze lub mniejsze yciowe uciliwoci. Na szczcie pewnego dnia na pomoc przybyli dzielni rycerze Szablonw, ktrzy obalili tyranw i zapewnili krlestwu szczliwe ycie pod rzdami nowych, askawych wadcw. I wszyscy yli dugo i szczliwie. To tyle, jeli chodzi o otoczk baniow, bo teraz naleaoby wrci do rzeczywistego zagadnienia. Jaki czas temu mielimy okazj pozna dyrektywy preprocesora, zwracajc przy tym szczegln uwag na makra. Makra imitujce funkcje byy kiedy jedynym sposobem na tworzenie kodu niezwizanego z adnym typem danych. Teraz za mamy ju szablony. Czy s one lepsze?
Wida par podobiestw, ale i mnstwo rnic. Przede wszystkim interesuje nas to, w jaki sposb makra i szablony osigaj niezaleno od typu danych - parametrw. W sumie wiemy to dobrze: w szablonach wystpuj parametry bdce typami (jak u nas T), nieodpowiadajce jednak adnemu konkretnemu typowi danych. Poprzez konkretyzacj tworzone s potem specjalizowane egzemplarze funkcji, dziaajce dla cile okrelonych ju rodzajw zmiennych makra w ogle nie posuguj si pojciem typ danych. Ich istota polega na zwykej zamianie jednego tekstu (wywoania makra) w inny tekst (rozwinicie makra). Dopiero to rozwinicie jest przedmiotem zainteresowania kompilatora, ktry wedle swoich regu - jak choby poprawnego uycia operatorw - uzna je za poprawne bd nie Mamy wic dwa rne podejcia i zapewne ju wiesz lub domylasz si, e nie s one rwnowane ani nawet rwnie dobre. Naley wic odpowiedzie na proste pytanie - co jest lepsze?
530
Zaawansowane C++
Pojedynek na szczycie
W tym celu sprbujmy uy obu zaprezentowanych wyej konstrukcji, poddajc je swoistym prbom: // bdziemy potrzebowali kilku zmiennych int nA = 42; float fB = 12.0f; // i startujemy... std::cout << max(34, 56) << " | " << std::cout << max(nA, fB) << " | " << std::cout << max(nA++, fB) << " | " << MAX(34, 56) << std::endl; // 1 MAX(nA, fB) << std::endl; // 2 MAX(nA++, fB) << std::endl; // 3
Czy obie konstrukcje przejd je z powodzeniem? C, odpowied jest niestety przeczca. Tylko pierwsza linijka nie wymaga adnych uwag ani analizy. W tym przypadku nie ma po prostu adnych wtpliwoci: obie wartoci do porwnania s jednoznacznymi staymi tych samych typw. Wszystko wic pjdzie gadko. Jednak dalej zaczynaj si ju kopoty
I wszystko byoby w porzdku, gdyby nie jeden drobny niuans, w zasadzie niedostrzegalny na pierwszy rzut oka. Jak to zwykle bywa w niejasnych sytuacjach, chodzi o wydajno. Zwrmy uwag, e parametry funkcji max() s tu przekazywane poprzez warto. Potencjalnie wic moe to prowadzi do dwch niepotrzebnych kopiowa, wykonywanych podczas wywoywania funkcji w skompilowanym programie. Oczywicie, ma to znaczenie tylko dla duych obiektw, lecz kto powiedzia, e nie moglibymy chcie uy tej funkcji na przykad do 1000-elementowej tablicy? Powiesz pewnie, e jest to na to rada. Wystarczy skorzysta z wynalazku C++ znanego pod nazw referencji. Przypomnijmy, e referencje, czyli ukryte wskaniki, nie powoduj przekazania do funkcji samego obiektu, lecz tylko jego adresu. Ich zalet jest za to, e nie zmuszaj do korzystania z kopotliwej w gruncie rzeczy skadni wskanikw. Pamitajc o tym, ochoczo przerabiamy nasz szablon na wersj korzystajc z referencji: template <typename T> T max(const T& a, const T& b) { return (a > b ? a : b); }
Szablony
531
W ten sposb niechccy pozbawilimy kompilator wanej moliwoci: uywania niejawnych konwersji. W momencie, gdy chcemy przekaza do funkcji nie obiekt, a referencj do niego, kompilator staje si po prostu lepy na ten mechanizm jzyka. atwo to zreszt wyjani: istot referencji jest odwoywanie si do istniejcego obiektu bez kopiowania, za istniejcy obiekt ma swj typ, ktrego zmieni nie mona. Wic co zrobi? Najlepiej po prostu pogodzi si z tym strasznym marnotrawstem, ktre i tak nie jest szczeglnie wielkie, a przez dobry kompilator moe by nawet z niezym skutkiem minimalizowane. Naturalnie, mona prbowac kombinowa dalej - chociaby doda drugi parametr szablonu. Tyle e wtedy pozostanie nierozstrzygalny wybr, ktry z nich uczyni typem wartoci zwracanej. Naturalnie, mona ten typ doda jako kolejny, trzeci ju parametr szablonu i kaza go podawa wywoujcemu. Wreszcie, mona nawet uy jednego z kilku do pokrtnych (koncepcyjnie i skadniowo) sposobw na obejcie problemu - ale chyba nie zmartwisz si tym, e ci ich tutaj oszczdz. Nadmierna komplikacja jest tu bowiem wysoce niewskazana; zaangaowane rodki bd zwyczajnie niewspmierne do zyskw.
Wynik
Ostatecznie moemy uzna remis obu rozwiza, aczkolwiek z lekkim wskazaniem na makrodefinicje. Z wyjtkiem fanatykw wydajnoci nie ma jednak bodaj nikogo, kto uwaaby nieefektywne dziaanie szablonw za wielki bd. A tym, ktrzy rzeczywicie tak uwaaj, pozostaje chyba tylko przerzucenie si na jzyk asemblera :)
532
Jak zadziaa makro
Zaawansowane C++
A teraz czas na analiz makrodefinicji i jej uycia w formie MAX(nA++, fB). Pamitajc, jak dziaa preprocesor, susznie mona wywnioskowa, e zamieni on wywoanie makra na takie oto wyraenie: ((nA++) > (fB) ? (nA++) : (fB)) Wszystko jest zatem w porzdku? Nie cakiem. Wrcz przeciwnie. Mamy problem. Powany problem. A jego przyczyn jest obecno instrukcji nA++ dwukrotnie. Fakt ten sprawi mianowicie, e zmienna nA zostanie dwa razy zwikszona o 1! Ostatecznie warunek powyej zwrci bdny wynik - rnicy si od waciwego o ow problematyczn jedynk. Jeli pamitasz dokadnie rozdzia o preprocesorze, takie zachowanie nie powinno by dla ciebie zaskoczeniem. Ju wtedy zaprezentowaem przykad tego problemu i ostrzegem przed stosowaniem makrodefinicji w charakterze funkcji.
Wynik
C mona wicej powiedzie? Bdny rezultat uycia makra sprawia, e makrodefinicje nie tylko przegrywaj, ale waciwie zostaj zdyskwalifikowane jako narzedzia tworzenia kodu niezalenego od typu. Bezapelacyjnie wygrywaj szablony!
Konkluzje
Wniosek jest waciwie jeden: Naley uywa szablonw funkcji zamiast makr, ktre maj udawa funkcje. Makrodefinicje w rodzaju MAX(), MIN() czy innych tego rodzaju nie maj ju wic waciwie racji bytu. Zastpiy je cakowicie szablony funkcji, oferujce nie tylko te same rezultaty (przy zastosowaniu inline - rwnie wydajnociowe), ale te jedn konieczn cech, ktrej makrom brak - poprawno. Szablony s po prostu bardziej inteligentne, jako e odpowiada za nie przemylnie skonstruowany kompilator, a nie jego uomny pomocnik - preprocesor. Jak si te miae okazj przekona w tym rozdziale, moliwoci szablonw funkcji s nieporwnywalnie wiksze od tych dawanych przez makrodefinicje. Nie znaczy to oczywicie, e makra zostay cakowicie zastpione przez szablony. Nadal bowiem znajduj one zastosowanie tam, gdzie chcemy dokonywa operacji na kodzie jak na zwykym tekcie - a wic na przykad do wstawiania kilku czsto wystepujcych instrukcji, ktrych nie moemy wyodrbni w postaci funkcji. Niemniej naley podkrela (co robi po raz n-ty), e makra nie su do imitacji funkcji, gdy same funkcje (lub ich szablony) doskonale radz sobie ze wszystkimi zadaniami, jakie chcielibymy im powierzy. Naocznie to zreszt zobaczylimy.
Struktury danych
Szablony funkcji maj wic swoje wane zastosowanie. Waciwie jednak to szablony klas s uyteczne w znacznie wikszym stopniu. Wykorzystujemy je bowiem w celu implementacji w programach tzw. struktur danych.
Szablony
533
Jak gosi stare programistyczne rwnanie, obok algorytmw to struktury danych s gwnymi skadnikami programw127. Jak wskazuje nazwa tego pojcia, su one do przemylanej organizacji informacji przetwarzanych przez aplikacj. Zazwyczaj te struktury danych cile wsppracuj z algorytmami programu. Z najprostszymi strukturami danych zapoznae si ju cakiem dawno temu. Typowym przykadem moe by zwyka, jednowymiarowa tablica; inny to np. struktura jzyka C++ (definiowana poprzez struct), zwana czasem rekordem. To jednak tylko wierzchoek gry lodowej. Wrd wielu struktur danych wikszo jest o wiele bardziej wyspecjalizowana i funkcjonalna. C jednak ma to wsplnego z szablonami? Ot bardzo wiele. Dziki mechanizmowi parametryzowanych typw (czyli szablonw klas) implementacja przernych struktur danych w C++ jest prosta. Przynajmniej jest ona prosta w tym sensie, e nie nastrcza kopotw zwizanych z nieokrelonymi typami danych. Szablony zaatwiaj za nas t spraw, dziki czemu owe struktury mog by uniwersalne. Prawdopodobnie wanie to zastosowanie byo jednym z gwnych powodw, dla ktrego w ogle wprowadzono do jzyka C++ narzdzia szablonw. Nam pozostaje si tylko z tego cieszy no, monaby jeszcze przyjrze si sprawie nieco bliej :) Zrbmy wic to. W tej sekcji porozmawiamy sobie zatem o tym, jak szablony pomogaj w tworzeniu struktur danych w programach. Naturalnie, temat ten jest niezwykle szeroki i dlatego nie bdziemy w niego wnika dokadnie. Niemniej bdzie to dobra rozgrzewka przez poznawaniem Biblioteki Standardowej, ktra szeroko uywa szablonw do implementacji struktur danych. Omwimy wic sobie dwie najprostsze kategorie takich struktur: krotki i kontenery (pojemniki).
Krotki
Krotk (ang. tuple, nie myli ze stokrotk ;)) nazywamy poczenie kilku wartoci rnych typw w jedn cao. C++, podobnie jak wiele innych jzykw programowania umoliwia na zrealizowanie takiej koncepcji przy uyciu struktury, zawierajcej dwa, trzy, cztery lub wiksz liczb pl dowolnych typw. Tutaj jednak chcemy zobaczy w akcji szablony, zatem stworzymy nieco bardziej elastyczne rozwizanie.
Przykad pary
Najprotsz krotk jest oczywicie pojedyncza warto :) Poniewa jednak w jej przypadku do szczcia wystarcza normalna zmienna, zajmijmy si raczej zespoem dwch wartoci. Zwiemy go par (ang. pair) lub duetem (ang. duo).
Definicja szablonu
Majc w pamici fakt, i chcemy otrzyma par dwch wartoci dwch rnych typw, wyprodukujemy zapewne szablon podobny do poniszego: template <typename T1, typename T2> struct TPair { T1 Pierwszy; // warto pierwszego pola T2 Drugi; // warto drugiego pola };
127 To rwnanie to Algorytmy + struktury danych = programy, bedce jednoczenie tytuem synnej ksiki Niklausa Wirtha.
534
Zaawansowane C++
Zastosowa takiej prostej struktury jest cae mnstwo. Przy jej uyciu moemy na przykad w atwy sposb stosowa technik informowania o bdach przy pomocy rezultatu funkcji. Oto przykad: TPair<bool, T> Wynik = Funkcja(); // funkcja zwraca par wartoci if (Wynik.Pierwszy) { // wykonanie funkcji powiodo si; jej waciwy rezultat to // Wynik.Drugi } Wynik jako zesp dwch wartoci pozwala na oddzielenie waciwego rezultatu od danych bdu. Jednoczenie nie zatracamy informacji o typie wartoci zwracanej przez funkcj - tutaj ukrywa si on za T i jest widoczny w prototypie funkcji.
Pomocna funkcja
Do wygodnego uywania pary przydaby si sposb na jej atwie utworzenie. Na razie bowiem Funkcja() musiaaby wykonywa np. taki kod: TPair<bool, int> Wynik; Wynik.Pierwszy = true; Wynik.Drugi = 42; return Wynik; // // // // obiekt wyniku informacja o ewentualnym bdzie zasadniczy rezultat zwracamy to wszystko
Sytuacj moemy poprawi, dodajc konstruktor(y): template <typename T1, typename T2> struct TPair { T1 Pierwszy; // warto pierwszego pola T2 Drugi; // warto drugiego pola // ------------------------------------------------------------------// konstruktory TPair() : Pierwszy(), Drugi() TPair(const T1& Wartosc1, const T2& Wartosc2) : Pierwszy(Wartosc1), Drugi(Wartosc2) { } { }
};
W zasadzie to s one niezbdne - inaczej nie monaby tworzy par z obiektw, ktrych klasy nie maj domylnych konstruktorw. Tak czy owak, skracamy ju zapis do skromnego: return TPair<bool, int>(true, 42); Nadal jednak mona troch ponarzeka. Kompilator nie jest na przykad na tyle inteligentny, aby wydedukowa parametry szablonu TPair z argumentw konstruktora. To jednak mona atwo uzyska, jako e umiejtno takiej dedukcji jest nieodczn cech szablonw funkcji. Moemy zatem stworzy sobie pomocn funkcj Para(), tworzc duet: template <typename T1, typename T2> inline TPair<T1, T2> Para(const T1& Wartosc1, const T2& Wartosc2) { return TPair<T1, T2>(Wartosc1, Wartosc2); } To wreszcie pozwoli na stosowanie krtkiej i przemylanej formy tworzenia pary:
Szablony
return Para(true, 42); Przydomek inline zabezpiecza natomiast przed niewybaczalnym uszczerbkiem na wydajnoci spowodowanym poredni drog kreacji obiektu.
535
Dalsze usprawnienia
Moemy dalej usprawnia szablon TPair - tak, aby wygoda korzystania z niego nie ustpowaa niczym przyjemnoci uytkowania typw wbudowanych. Dodamy mu wic: operator przypisania konstruktor kopiujcy Ale po co?, moesz spyta. Przecie w tym przypadku wersje tworzone przez kompilator pasuj jak ula. Owszem, masz racj. Mona je jednak poprawi, definiujc obie metody jako szablony: template <typename T1, typename T2> struct TPair { T1 Pierwszy; // warto pierwszego pola T2 Drugi; // warto drugiego pola // ------------------------------------------------------------------// konstruktory (zwyke i kopiujco-konwertujcy) TPair() : Pierwszy(), Drugi() { } TPair(const T1& Wartosc1, const T2& Wartosc2) : Pierwszy(Wartosc1), Drugi(Wartosc2) { } template <typename U1, typename U2> TPair(const TPair<U1, U2>& Para) : Pierwszy(Para.Pierwszy), Drugi(Para.Drugi) { } // ------------------------------------------------------------------// operator przypisania template <typename U1, typename U2> operator=(const TPair<U1, U2>& Para) { Pierwszy = Para.Pierwszy; Drugi = Para.Drugi; return *this; }
};
W ten sposb pieczemy dwa befsztyki na jednym ogniu. Nasze metody peni bowiem nie tylko rol kopiujc, ale i rol konwertujc. Pary staj si wic kompatybilne wzgldem niejawnym konwersji swoich skadnikw; zatem np. para TPair<int, int> bdzie moga by od teraz bez problemw przypisana do pary TPair<float, double>, itd. Konieczne konwersje bd dokonywane podczas inicjalizacji (konstruktor) lub przypisywania (operator =) pl. Do peni funkcjonalnoci brakuje jeszcze moliwoci porwnywania par. To za osigamy, definiujc operatory == i !=. Take tutaj moe zaj konieczno konfrontowania duetw o rnych typach pl, zatem ponownie naley uy szablonu: // operator rwnoci template <typename T1, typename inline bool operator==(const const { return (Para1.Pierwszy && Para1.Drugi T2, typename U1, typename U2> TPair<T1, T2>& Para1, TPair<U1, U2>& Para2) == Para2.Pierwszy == Para2.Drugi);
536
} // operator nierwnoci template <typename T1, typename inline bool operator!=(const const { return (Para1.Pierwszy || Para1.Drugi }
Zaawansowane C++
T2, typename U1, typename U2> TPair<T1, T2>& Para1, TPair<U1, U2>& Para2) != Para2.Pierwszy != Para2.Drugi);
Troch makabrycznie na pierwszy rzut oka moe wyglda szablon z czterema parametrami. Powd jego wystpienia jest jednak banalny: potrzebujemy po prostu parametryzacji typw dla obu porwnywanych par. W sumie wic mog wystpi cztery typy pl, co adnie przedstawiaj deklaracje parametrw funkcji. O tym, czy typy te bd ze sob wspgray, zdecyduj ju porwnywania w ciele funkcji operatorowych. Naturalnie, w przypadku braku identycznoci lub niejawnych konwersji, kompilacji problematycznego uycia operatora nie powiedzie si. Stworzony szablon TPair wraz z oprzyrzdowaniem w postaci pomocniczej funkcji i przecionych operatorw jest bardzo podobny do klasy std::pair z Biblioteki Standardowej.
// ------------------------------------------------------------------// konstruktory (zwyke i kopiujco-konwertujcy) TTriplet() : Pierwszy(), Drugi(), Trzeci() { } TTriplet(const T1& Wartosc1, const T2& Wartosc2, const T3& Wartosc3) : Pierwszy(Wartosc1), Drugi(Wartosc2), Trzeci(Wartosc3) { } template <typename U1, typename U2, typename U3> TTriplet(const TTriplet<U1, U2, U3>& Trojka) : Pierwszy(Trojka.Pierwszy), Drugi(Trojka.Drugi), Trzeci(Trojka.Trzeci) { } // ------------------------------------------------------------------// operator przypisania template <typename U1, typename U2, typename U3> operator=(const TTriplet<U1, U2, U3>& Trojka) { Pierwszy = Trojka.Pierwszy;
Szablony
Drugi = Trojka.Drugi; Trzeci = Trojka.Trzeci; } return *this;
537
};
// operator rwnoci template <typename T1, typename T2, typename T3, typename U1, typename U2, typename U3> inline bool operator==(const TTriplet<T1, T2, T3>& Trojka1, const TTriplet<U1, U2, U3>& Trojka2) { return (Trojka1.Pierwszy == Trojka2.Pierwszy && Trojka1.Drugi == Trojka2.Drugi && Trojka1.Trzeci == Trojka2.Trzeci); } // operator nierwnoci template <typename T1, typename T2, typename T3, typename U1, typename U2, typename U3> inline bool operator==(const TTriplet<T1, T2, T3>& Trojka1, const TTriplet<U1, U2, U3>& Trojka2) { return (Trojka1.Pierwszy != Trojka2.Pierwszy || Trojka1.Drugi != Trojka2.Drugi || Trojka1.Trzeci != Trojka2.Trzeci); } // ---------------------------------------------------------------------// wygodna funkcja tworzca trojk template <typename T1, typename T2, typename T3> inline TTriplet<T1, T2, T3> Trojka(const T1& Wartosc1, const T2& Wartosc2, const T3& Wartosc3) { return TTriplet<T1, T2, T3>(Wartosc1, Wartosc2, Wartosc3); } Wyglda on lekko strasznie, ale te pokazuje wyranie, e szablony w C++ to naprawd potne narzedzie. Pomyl, czy w ogle sensowne byoby implementowanie krotek bez nich? Wysze krotki wygodnie jest programowa w sposb rekurencyjny, wykorzystujc jedynie szablon pary. Przy takim podejciu trjka np. typu TTriplet<int, float, std::string> jest przechowywana jako typ TPair<int, TPair<float, std::string> > - czyli par, ktrej elementem jest kolejna para. Analogicznie wyglda to dalej. Takie podejcie, w poczeniu z kilkoma innymi, maksymalnie wykrconymi technikami, daje moliwo tworzenia krotek dowolnego rzdu. Takie rozwizanie jest czci znanej biblioteki Boost.
Pojemniki
Nadesza pora, by pozna gwny powd wprowadzenia do C++ mechanizmu szablonw. S nim mianowicie klasy kontenerowe.
538
Zaawansowane C++
Kontenery albo pojemniki (ang. containers) to specjalne struktury danych przeznaczone do zarzdzania kolekcjami obiektw tego samego typu w okrelony sposb. Poniewa definicja ta jest bardzo oglna, mamy mnstwo rodzajw kontenerw. Spora ich cz zostaa zaimplementowana w Bibliotece Standardowej, a o wszystkich mwi dowolna ksika o algorytmach i strukturach danych. Nie bdziemy tutaj omawia kadego rodzaju pojemnika, lecz skoncentrujemy si jedynie na tym, w jaki sposb szablony pomagaj im w prawidowym funkcjonowaniu. Zobaczymy wic najdoniolejsze zastosowanie szablonw w programowaniu.
Szablony
// konstruktor TStack() : m_uRozmiar(0)
539
{ }
// ------------------------------------------------------------// odoenie elementu na stos void Push(const T& Element) { if (m_uRozmiar == N) throw "TStack::Push() - stos jest peen"; m_aStos[m_uRozmiar] = Element; ++m_uRozmiar; // dodanie elementu // zwiksz. licznika
// pobranie elementu ze szczytu stosu T Pop() { if (m_uRozmiar == 0) throw "TStack::Pop() - stos jest pusty"; // zwrcenie elementu i zmniejszenie licznika return m_aStos[--m_uRozmiar];
};
Jest to waciwie najprostsza moliwa wersja stosu. Dwa parametry szablonu okrelaj w niej typ przechowywanych elementw oraz maksymaln ich liczb. Drugi oczywicie nie jest konieczny - atwo wyobrazi sobie (i napisa) stos, ktry uywa dynamicznej tablicy i dostosowuje si do liczby odoonych elementw. Co do metod, to ich garnitur jest rwnie skromny. Metoda Push() powoduje odoenie na stos podanej wartoci, za Pop() - pobranie jej i zwrcenie w wyniku. To absolutne minimum; czsto dodaje si do tego jeszcze funkcj Top() (szczyt), ktra zwraca element lecy na grze bez zdejmowania go ze stosu. Klas mona te usprawnia dalej: dodajc szablonowy kostruktor kopiujcy i operator przypisania, metody zwracajce aktualny rozmiar stosu (liczb odoonych elementw) i inne dodatki. Monaby nawet zmieni wewntrzny mechanizm funkcjonowania klasy i zaprzc do pracy szablon TArray - dziki temu maksymalny rozmiar stosu mgby by ustalany dynamicznie. Zawsze jednak istota dziaania pojemnika bdzie taka sama.
Korzystanie z szablonu
Spoytkowanie tak napisanego stosu nie jest trudne. Oto najbanalniejszy z banalnych przykadw: // deklaracja obiektu stosu, zawierajcego maksymalnie 5 liczb typu int TStack<int, 5> Stos; // odoenie paru liczb na stos Stos.Push (12); Stos.Push (23); Stos.Push (34); // podjcie i wywietlenie odoonych liczb for (unsigned i = 0; i < 3; ++i) std::cout << Stos.Pop() << std::endl; W jego rezultacie zobaczylibymy wypisanie liczb:
540
Zaawansowane C++
34 23 12
Wida zatem wyranie, e metoda Pop() powoduje zwrcenie elementw stosu w kolejnoci przeciwnej do ich odkadania poprzez Push(). Na tym wanie opiera si idea stosu. Stos ma w programowaniu rozliczne zastosowania: poczwszy od rekurencyjnego przeszukiwania hierarchicznych baz danych (jak chociaby katalogi na dysku twardym) po rysowanie trjwymiarowych modeli w grach komputerowych. Obok zwykej tablicy, jest to chyba najczciej wykorzystywany pojemnik.
Programowanie oglne
Szablony, a szczeglnie ich uycie do implementacji kontenerw, stay si podstaw idei tak zwanego programowania oglnego (ang. general programming). Trudno precyzyjnie j wyrazi i zdefiniowa, ale mona j rozumie jako poszukiwanie jak najbardziej abstrakcyjnych i oglnych rozwiza w postaci algorytmw i struktur danych. Rozwizania powstae w zgodzie z t ide s wic niesychanie elastyczne. Dobrym przykadem s wanie kontenery. Istnieje wiele ich rodzajw, poczwszy od prostych tablic jednowymiarowych po zoone struktury, jak np. drzewa. Dla kadego pojemnika logiczne jest jednak przeprowadzanie pewnych typowych operacji, jak na przykad wyszukiwanie okrelonego elementu. Operacje te nazywami algorytmami. Logiczne byoby zaprogramowanie algorytmw jako metod klas kontenerowych. Rozwizanie to ma jednak wad: poniewa kady pojemnik jest zorganizowany inaczej, naleaoby dla kadego z nich zapisa osobn wersj algorytmu. Problem ten rozwizano poprzez dodanie abstrakcyjnego pojcia iteratora - obiektu, ktry suy do przegldania kontenera. Iterator ukrywa wszelkie szczegy zwizane z konkretnym pojemnikiem, przez co algorytm oparty na wykorzystaniu iteratorw moe by napisany raz i wykorzystywany wielokrotnie w odniesieniu do dowolnych kontenerw. Ten zmylny pomys sta si podstaw stworzenia Standardowej Biblioteki Szablonw (ang. Standard Template Library - STL). Jest to gwna cz Biblioteki Standardowej jzyka C++ i zawiera wiele szablonw podstawowych struktur danych. S one wsparte algorytmami, iteratorami i innymi pomocniczymi pojciami, dziki ktrym STL jest nie tylko bogata funkcjonalnie, ale i efektywna oraz elastyczna. To jedno z bardziej uytecznych narzdzi jzyka C++ i jednoczenie najwaniejsze zastosowanie szablonw.
Podsumowanie
Ten rozdzia koczy kurs jzyka C++. Na ostatku zapoznae si z jego najbardziej zaawansowanym mechanizmem - szablonami. Wpierw wic zobaczye sytuacje, w ktrych cisa kontrola typw w C++ jest powodem problemw. Chwil pniej otrzymae te do rki lekarstwo, czyli wanie szablony. Przeszlimy potem do dokadnego omwienia ich dwch rodzajw: szablonw funkcji i szablonw klas. W sposb oglniejszy zajlimy si nimi w nastpnym podrozdziale. Poznae zatem trzy rodzaje parametrw szablonw, ktre daj im razem bardzo potne waciwoci. Zaraz jednak uwiadomiem ci take problemy zwizane z szablonami: poczwszy od koniecznoci udzielania podpowiedzi dla kompilatora co do znaczenia niektrych nazw, a koczc na kwestii organizacji kodu szablonw w plikach rdowych.
Szablony
541
W trzecim podrozdziale przyjrzelimy si natomiast najbardziej typowym zastosowaniom szablonw - czyli dowiedzielimy si, jak zdobyta wiedza moe si przyda w praktyce.
Pytania i zadania
Teraz czeka ci jeszcze tylko odpowied na kilka sprawdzajcych wiedz pyta i wykonanie zada. Powodzenia!
Pytania
1. Co to znaczy, e C++ jest jzykiem o cisej kontroli typw? 2. W jaki sposb mona stworzy oglne funkcje, dziaajce dla wielu typw danych? 3. Jakie s sposoby na implementacj oglnych klas pojemnikowych bez uycia szablonw? 4. Jak definiujemy szablon? 5. Jakie rodzaje szablonw s dostpne w C++? 6. Czym jest specjalizacja szablonu? Czym si rni specjalizacja czciowa od penej? 7. Skd kompilator bierze wartoci (nazwy typw) dla parametrw szablonw funkcji? 8. Ktre parametry szablonu funkcji mog by wydedukowane z jej wywoania? 9. Co dzieje si, gdy uywamy szablonu funkcji lub klasy? Jakie zadania spoczywaj wwczas na kompilatorze? 10. Jakie trzy rodzaje parametrw moe posiada szablon klasy? 11. Jaka jest rola sowa kluczowego typename? Gdzie i dlaczego jest ono konieczne? 12. Na czym polega model wczania? 13. Ktry sposb organizacji kodu szablonw najbardziej przypomina tradycyjn metod podziau kodu w C++? 14. Dlaczego nie naley uywa makrodefinicji w celu imitowania szablonw funkcji? 15. Czym jest krotka? 16. Co rozumiemy pod pojciem pojemnika lub kontenera?
wiczenia
1. Napisz szablon funkcji Suma(), obliczajcy sum wartoci elementw podanej tablicy TArray. 2. (Trudniejsze) Zdefiniuj szablon klas tablicy wskanikw o nazwie TPtrArray, dziedziczcy z TArray. Szablon ten powinien przyjmowa jeden parametr, bdcy typem, na ktry pokazuj elementy tablicy. 3. (Bardzo trudne) Dodaj do specjalizacji TArray<TArray<TYP> > przeciony operator [], ktry bdzie dziaa w ten sam sposb, jak dla zwykych wielowymiarowych tablic jzyka C++. Wskazwka: operator ten bdzie wobec tablicy uywany dwukrotnie. Pomyl wic, jak warto (obiekt tymczasowy) powinno zwraca jego pierwsze uycie, aby drugie zwrcio w wyniku dany element tablicy. 4. (Trudniejsze) Opracuj i zaimplementuj algorytm dokonujcy przedstawiania liczby naturalnej w systemie rzymskim. Wskazwka: wykorzystaj tablic przegldow par: litera rzymska plus odpowiadajca jej liczba dziesitna. 5. Napisz szablon TQueue, podobny do TStack, lecz implementujcy pojemnik zwany kolejk. Kolejk dziaa w ten sposb, i elementy s dodawane do jej pierwszego koca, natomiast pobierane s z drugiego - tak samo, jak obsugiwane s osoby stojce w kolejce w sklepie czy banku. Podobnie jak w przypadku stosu, moesz okreli jej maksymalny rozmiar jako parametr szablonu.
I
I NNE
INDEKS
#
# (operator) 319 ## (operator) 319 #define (dyrektywa) 307 a const 310 makrodefinicje 317 #elif (dyrektywa) 330 #else (dyrektywa) 327 #endif (dyrektywa) 326 #error (dyrektywa) 330 #if (dyrektywa) 328 #ifdef (dyrektywa) 326 #ifndef (dyrektywa) 327 #include (dyrektywa) 35, 331 wielokrotne doczanie 333 z cudzysowami 332 z nawiasami ostrymi 331 #line (dyrektywa) 315 #pragma (dyrektywa) 334 auto_inline 337 comment 339 deprecated 336 inline_depth 338 inline_recursion 338 message 335 once 339 warning 336 #undef (dyrektywa) 307
A
abort() (funkcja) 453 abs() (funkcja) 94 agregaty inicjalizacja 356 algorytm 20 aliasy typw 81 alternatywa bitowa 376 logiczna 106, 377 alternatywa wykluczajca 377 aplikacje konsolowe 31 okienkowe 31 auto_ptr (klasa) 461
B
BASIC 23 bool (typ) 108 Boost (biblioteka) 487 break (instrukcja) 57, 65
C
callback 295, 438 case (instrukcja) 57 catch (sowo kluczowe) 440 dopasowywanie bloku do wyjtku 444 kolejno blokw 444, 468 stosowanie 442 uniwersalny blok catch 448 cdecl (konwencja wywoania) 285 ceil() (funkcja) 94 cerr (obiekt) 443 char (typ) 79 cigi znakw Patrz acuchy znakw cin (obiekt) 40 class (sowo kluczowe) 167 na licie parametrw szablonu 510 const (modyfikator) 41, 76 w odniesieniu do metod 175 w odniesieniu do wskanikw 255 const_cast (operator) 261, 386 continue (instrukcja) 66 cos() (funkcja) 91 cout (obiekt) 34
_
__alignof (operator) 384 __asm (instrukcja) 246 __cdecl (modyfikator) 285 __cplusplus (makro) 316 __DATE__ (makro) 315 __declspec (modyfikator) align 384 deprecated 336 property 175 __fastcall (modyfikator) 285 __FILE__ (makro) 315 __int16 (typ) 79 __int32 (typ) 79 __int64 (typ) 79 __int8 (typ) 79 __LINE__ (makro) 314 __stdcall (modyfikator) 285 __TIME__ (makro) 315 __TIMESTAMP__ (makro) 316
D
default (instrukcja) 57 defined (operator) 328
546
deklaracje funkcji 145 zapowiadajce 157 zmiennych 39 delegaci 427 delete (operator) 383 niszczenie obiektw 186 przecianie 409 zwalnianie pamici 269 delete[] (operator) 271, 383 przecianie 409 Delphi 24 dereferencja 256, 381 destruktory 177 a dziedziczenie 203 a wyjtki 455 wirtualne 209 Dev-C++ 27 do (instrukcja) 59 double (modyfikator) 80 double (typ) 80 dynamic_cast (operator) 217, 385 dyrektywy preprocesora 304 dziedziczenie 193 jednokrotne 198 klas szablonowych 492 pojedyncze 198 skadnia w C++ 197 szablonw klas 493 wielokrotne 202 wartoci zwracane 49 zwrotne 295, 424 funkcje zaprzyjanione 345 definiowanie wewntrz klasy 347 deklaracje 345 funktory 408
Inne
G
getch() (funkcja) 35
H
hermetyzacja 173
I
IDE 27 if (instrukcja) 51 indeksowanie 381 inicjalizacja 355 agregatw 356 lista 357 poprzez konstruktor 356 skadowych klasy 357 typw podstawowych 356 zerowa 479 inline (modyfikator) 322 instrukcje sterujce ptle 58 warunkowe 51 int (typ) 78 inteligentne wskaniki 405, 460 interfejs uytkownika 141 inynieria oprogramowania 232 iteratory 540
E
else (instrukcja) 53 endl (manipulator) 34 enkapsulacja 229 enum (sowo kluczowe) 125 exit() (funkcja) 454, 456 exp() (funkcja) 89 explicit (sowo kluczowe) 367 export (sowo kluczowe) 526 extern (modyfikator) 157
J
Java 25 jzyk kontekstowy 519 jzyk programowania 22 niskiego poziomu 162 wysokiego poziomu 162
F
fastcall (konwencja wywoania) 285 float (typ) 80 floor() (funkcja) 94 fmod() (funkcja) 95 for (instrukcja) 63 free() (funkcja) 270 friend (sowo kluczowe) 344 funkcja 36 funkcje cechy charakterystyczne 282 inline 322 operatorowe 389 parametry 47 prototypy 145 przecianie 94 skadnia 50
K
klasy 166, 169 abstrakcyjne 211 bazowe 193 definicje klas 170 implemetacja 178 pochodne 193 klasy aprzyjanione deklarowanie 349 klasy wyjtkw 464 uycie dziedziczenia 465 klasy zaprzyjanione 349
Indeks
kod wyjcia 454 komentarze 33 kompilacja warunkowa 325 kompilator 22 koniunkcja bitowa 375 logiczna 106, 377 konkatencja 102 konkretyzacja 478, 484, 499 jawna 525 konsola 31 konstruktory 176 a dziedziczenie 202 cechy 353 definiowanie 353 domylne 204 konwertujce 365 kopiujce 361, 362 kontenery 538 konwencja wywoania 284 konwersje 364 poprzez konstruktor 365 poprzez operator 368 typy oglne i szczeglne 446 krokowy tryb 37 krotki 533 krotno zwizku klas 238 metaszablony 517 metody 165 czysto wirtualne 210 deklarowanie 168 implementacja 168 prototypy 175 stae 175 statyczne 226 wirtualne 205 metody zaprzyjanione 348 deklarowanie 348 model separacji 526 wsppraca z modelem wczania 527 model wczania 522 modyfikatory 77
547
N
nawiasy klamrowe 125 kwadratowe 104 okrge 388 ostre 518 nazwy dekorowane 286 przesanianie 73 wtrcone 491 zalene 520 negacja bitowa 375 logiczna 106, 377 new (operator) 382 alokacja pamici 269 przecianie 409 tworzenie obiektw 184 new[] (operator) 271, 382 przecianie 409 notacja wgierska 40
L
liczby pseudolosowe 92 linker 23 lista inicjalizacyjna 357 inicjalizacja skadowych 357 wywoywanie konstruktorw bazowych 358 log() (funkcja) 89 log10() (funkcja) 89 long (modyfikator) 79 long (typ) 80 l-warto 257, 378
O
obiekty 164 funkcyjne 408 jako wskaniki 184 jako zmienne 180, 182 konkretne 228 narzdziowe 228 tworzenie 168 zasadnicze 228 obsuga bdw 434 oddzielenie rezultatu 436 wywoanie zwrotne 438 zakoczenie programu 439 zwracanie specjalnej wartoci 435 odwijanie stosu 441, 449 ofstream (klasa) 463 OOP 161 operatory 43, 371 arytmetyczne 43, 374 binarne 96, 372 bitowe 375
acuchy znakw 98 inicjalizacja 101 czenie 102 pobieranie znaku 103 w stylu C 264
M
main() (funkcja) 33 makrodefinicje 316 a szablony funkcji 323, 529 definiowanie 318 niebezpieczestwa 320 operatory 319 rola nawiasw 321 malloc() (funkcja) 270
548
cechy 371 dekrementacji 45, 374 dereferencji 256 inkrementacji 45, 96, 374 konwersji 368 logiczne 105, 377 czno 373 pobrania adresu 256 porwnania 105, 376 pracujce z pamici 382 priorytety 44, 372 przecianie 370 przypisania 377 rwnoci a przypisania 55 rzutowania 384 strumieniowe 376 ternarny 389 unarne 95, 372 warunkowy 110 wskanikowe 256, 380 wyuskania 129, 185, 257, 387 zasigu 74
Inne
protected (specyfikator dostpu) 196 (specyfikator dziedziczenia) 198 prototypy funkcji 145 przecianie funkcji 94 przecianie operatorw 370 binarnych 397 inkrementacji i dekrementacji 396 oglna skadnia 390 poprzez funkcj globaln 393 poprzez funkcj skadow klasy 392 poprzez zaprzyjanion funkcj globaln 394 przypisania 399 unarnych 395 wskazwki 412 wywoania funkcji 407 zarzdzania pamici 409 przecinek 389 przepenienie stosu 250 przesanianie nazw 73 przestrzenie nazw 388 przesunicie bitowe 376 przyja 344 cechy 351 deklaracje 344 zastosowania 352 pseudokod 22 public (specyfikator dostpu) 171, 196 (specyfikator dziedziczenia) 198 punkt wykonania 37 p-warto 378
P
pami masowa 247 pami operacyjna 246 dynamiczna alokacja 268 paski model 249 pami wirtualna 247 parametry funkcji 47, 480 szablonu 477, 480 parametry szablonw pozatypowe 510 szablony parametrw szablonw 514 typy 509 Pascal 24 pascal (konwencja wywoania) 285 ptla 58 nieskoczona 155, 379 PHP 25 plik wymiany 247 pliki nagwkowe 142 paski model pamici 249 pojemniki 538 pola 165 statyczne 226 polimorfizm 211 pow() (funkcja) 88 pne wizanie 207 preprocesor 303 dyrektywy 304 private (specyfikator dostpu) 171, 196 (specyfikator dziedziczenia) 198 procedura 36 programowanie obiektowe 161, 191 oglne 540 strukturalne 159 projekt 30
R
rand() (funkcja) 61, 91 referencje 277 deklarowanie 278 jako parametry funkcji 279 register (modyfikator) 245 reinterpret_cast (operator) 261, 385 rejestry procesora 244 rekurencja 338 return (instrukcja) 50 rnica symetryczna bitowa 376 logiczna 377 RTTI 219, 387 r-warto 257, 378 rzutowanie 83 funkcyjne 386 operatory 384 w d hierarchii klas 217 w stylu C 85, 386
S
segmenty pamici operacyjnej 248 sekwencje ucieczki 306
Indeks
set_terminate() (funkcja) 453 set_unexpected() (funkcja) 453 SFINAE 486 short (modyfikator) 79 short (typ) 80 signed (modyfikator) 77 sin() (funkcja) 91 singletony 224 size_t (typ) 83 sizeof (operator) 82, 383 specjalizacje szablonw 484 specyfikacja wyjtkw 451 specyfikatory dostpu do skadowych 171 dziedziczenia 198 sqrt() (funkcja) 88 srand() (funkcja) 61, 92 staa 41 stae 76 deklarowanie 41 jako parametry szablonw 510 static (modyfikator) 75 static_cast (operator) 87, 384 std (przestrze nazw) 35 stdcall (konwencja wywoania) 285 sterta obszar pamici 250 STL 540 stos obszar pamici 249 struktura danych 538 string (klasa) 100 length() (metoda) 104 strings Patrz acuchy znakw struct (sowo kluczowe) rnica wobec class 171 struktury 128 definiowanie 128, 131 inicjalizacja 130 strumie wejcia 40 wyjcia 34 switch (instrukcja) 56 system() (funkcja) 153 sytuacje wyjtkowe 433 szablony 476 eksportowane 526 organizacja kodu 522 problem nawiasw ostrych 498, 518 rodzaje 477 skadnia 477 specjalizacje 484 zastosowania 529 szablony funkcji 478 a makrodefinicje 529 dedukcja parametrw 485 definiowanie 478 specjalizacje 481 wywoywanie 484 zakres stosowalnoci 479 szablony klas 487 definiowanie 489 deklaracje przyjani 495 domylne parametry 506 i dziedziczenie 492 i struktury danych 532 implementacja metod 490 konkretyzacja 499 specjalizacja czciowa 504 specjalizacja metody 503 specjalizacja szablonu 501 szablony metod 495 wsppraca z szablonami funkcji 500 wykorzystanie 491, 497
549
rodowisko programistyczne 27
T
tablice 113 deklarowanie 113 dynamiczne 270 i wskaniki 261 inicjalizacja 115 wielowymiarowe 120 tan() (funkcja) 91 template (sowo kluczowe) 477, 482 terminate() (funkcja) 452, 453 this (sowo kluczowe) 179 thiscall (konwencja wywoania) 285 throw (instrukcja) 440 ponowne rzucenie wyjtku 448 rnice wzgldem break 450 rnice wzgldem return 441, 450 rzucanie wyjtku 441 throw() (deklaracja) 451 time() (funkcja) 92 tokenizacja 519 tokeny 519 trjznakowe sekwencje 305 try (sowo kluczowe) 440 zagniedanie blokw 446 tryb krokowy 37 type_info (struktura) 220 typedef (instrukcja) 81 typeid (operator) 220, 387 typename (sowo kluczowe) na licie parametrw szablonu 477, 510 przy nazwach zalenych 521 typy polimorficzne 212 typy strukturalne Patrz struktury typy wyliczeniowe 123 definiowanie 125 zastosowania 127
U
uncaught_exception() (funkcja) 456 unexpected() (funkcja) 452 unie 136 union (sowo kluczowe) 136 unsigned (modyfikator) 77
550
unsigned (typ) 80 arkana obsugi 466 apanie 442 naduywanie 471 odrzucanie 448 rzucanie 441 specyfikacja 451 wasne klasy dla nich 464 wykorzystanie 463 wykonania punkt 37 wyrwnanie danych w pamici 384
Inne
V
virtual (sowo kluczowe) oznaczenie metody wirtualnej 205 Visual Basic 23 void* (typ) 259
W
wcin (obiekt) 100 wcout (obiekt) 100 wczesne wizanie 207 while (instrukcja) 59, 60 wskaniki 248 i tablice 261 puste 248 wskaniki do funkcji 281 deklarowanie 289 jako argumenty innych funkcji 294 typy 287 wskaniki do skadowych 414 deklaracja wskanika na metod klasy 422 deklarowanie wskanika na pole klasy 418 uycie wskanika na metod klasy 423 uycie wskanika na pole klasy 419 wskaniki do zmiennych deklarowanie 252 przekazywanie do funkcji 266 spr o gwiazdk 252 stae wskaniki 254 wskaniki do staych 254 wstring (klasa) 100 wyjtki 439 a zarzdzanie zasobami 456
Z
zasig globalny 72 lokalny 70 moduowy 72 zasig zmiennych 69 zmienna 39 zmienne deklaracje zapowiadajce 157 deklarowanie 39 lokalne 71 modyfikatory 75 podstawowe typy 40 statyczne 75 typy 76 zasig (zakres) 69 zwizek agregacji 236 asocjacji 237 dwukierunkowy 239 dziedziczenia 235 generalizacji-specjalizacji 235 jednokierunkowy 239 przyporzdkowania 237 zawierania si 236
Zezwala si na kopiowanie i rozpowszechnianie wiernych kopii niniejszego dokumentu licencyjnego, jednak bez prawa wprowadzania zmian
0. Preambua
Celem niniejszej licencji jest zagwarantowanie wolnego dostpu do podrcznika, treci ksiki i wszelkiej dokumentacji w formie pisanej oraz zapewnienie kademu uytkownikowi swobody kopiowania i rozpowszechniania wyej wymienionych, z dokonywaniem modyfikacji lub bez, zarwno w celach komercyjnych, jak i nie komercyjnych. Ponad to Licencja ta pozwala przyzna zasugi autorowi i wydawcy przy jednoczesnym ich zwolnieniu z odpowiedzialnoci za modyfikacje dokonywane przez innych. Niniejsza Licencja zastrzega te, e wszelkie prace powstae na podstawie tego dokumentu musz nosi cech wolnego dostpu w tym samym sensie co produkt oryginalny. Licencja stanowi uzupenienie Powszechnej Licencji Publicznej GNU (GNU General Public License), ktra jest licencj dotyczc wolnego oprogramowania. Niniejsza Licencja zostaa opracowana z zamiarem zastosowania jej do podrcznikw do wolnego oprogramowania, poniewa wolne oprogramowanie wymaga wolnej dokumentacji: wolny program powinien by rozpowszechniany z podrcznikami, ktrych dotycz te same prawa, ktre wi si z oprogramowaniem. Licencja ta nie ogranicza si jednak do podrcznikw oprogramowania. Mona j stosowa do rnych dokumentw tekstowych, bez wzgldu na ich przedmiot oraz niezalenie od tego, czy zostay opublikowane w postaci ksiki drukowanej. Stosowanie tej Licencji zalecane jest gwnie w przypadku prac, ktrych celem jest instrukta lub pomoc podrczna.
1. Zastosowanie i definicje
Niniejsza Licencja stosuje si do podrcznikw i innych prac, na ktrych umieszczona jest pochodzca od waciciela praw autorskich informacja, e dana praca moe by rozpowszechniana wycznie na warunkach niniejszej Licencji. Uywane poniej sowo "Dokument" odnosi si bdzie do wszelkich tego typu publikacji. Ich odbiorcy nazywani bd licencjobiorcami. "Zmodyfikowana wersja" Dokumentu oznacza wszelkie prace zawierajce Dokument lub jego cz w postaci dosownej bd zmodyfikowanej i/lub przeoonej na inny jzyk. "Sekcj drugorzdn" nazywa si dodatek opatrzony odrbnym tytuem lub sekcj pocztkow Dokumentu, ktra dotyczy wycznie zwizku wydawcw lub autorw
552
Inne
Dokumentu z ogln tematyk Dokumentu (lub zagadnieniami z ni zwizanymi) i nie zawiera adnych treci bezporednio zwizanych z ogln tematyk (na przykad, jeeli Dokument stanowi w czci podrcznik matematyki, Sekcja drugorzdna nie moe wyjania zagadnie matematycznych). Wyej wyjaniany zwizek moe si natomiast wyraa w aspektach historycznym, prawnym, komercyjnym, filozoficznym, etycznym lub politycznym. "Sekcje niezmienne" to takie Sekcje drugorzdne, ktrych tytuy s ustalone jako tytuy Sekcji niezmiennych w nocie informujcej, e Dokument zosta opublikowany na warunkach Licencji. "Tre okadki" to pewne krtkie fragmenty tekstu, ktre w nocie informujcej, e Dokument zosta opublikowany na warunkach Licencji, s opisywane jako "do umieszczenia na przedniej okadce" lub "do umieszczenia na tylnej okadce". "Jawna" kopia Dokumentu oznacza kopi czyteln dla komputera, zapisan w formacie, ktrego specyfikacja jest publicznie dostpna. Zawarto tej kopii moe by ogldana i edytowana bezporednio za pomoc typowego edytora tekstu lub (w przypadku obrazw zoonych z pikseli) za pomoc typowego programu graficznego lub (w przypadku rysunkw) za pomoc oglnie dostpnego edytora rysunkw. Ponadto kopia ta stanowi odpowiednie dane wejciowe dla programw formatujcych tekst lub dla programw konwertujcych do rnych formatw odpowiednich dla programw formatujcych tekst. Kopia speniajca powysze warunki, w ktrej jednak zostay wstawione znaczniki majce na celu utrudnienie dalszych modyfikacji przez czytelnikw, nie jest Jawna. Kopi, ktra nie jest "Jawna", nazywa si "Niejawn". Przykadowe formaty kopii Jawnych to: czysty tekst ASCII bez znacznikw, format wejciowy Texinfo, format wejciowy LaTeX, SGML lub XML wykorzystujce publicznie dostpne DTD, standardowy prosty HTML przeznaczony do rcznej modyfikacji. Formaty niejawne to na przykad PostScript, PDF, formaty wasne, ktre mog by odczytywane i edytowane jedynie przez wasne edytory tekstu, SGML lub XML, dla ktrych DTD i/lub narzdzia przetwarzajce nie s oglnie dostpne, oraz HTML wygenerowany maszynowo przez niektre procesory tekstu jedynie w celu uzyskania danych wynikowych. "Strona tytuowa" oznacza, w przypadku ksiki drukowanej, sam stron tytuow oraz kolejne strony zawierajce informacje, ktre zgodnie z t Licencj musz pojawi si na stronie tytuowej. W przypadku prac w formatach nieposiadajcych strony tytuowej "Strona tytuowa" oznacza tekst pojawiajcy si najbliej tytuu pracy, poprzedzajcy pocztek tekstu gwnego.
2. Kopiowanie dosowne
Licencjobiorca moe kopiowa i rozprowadza Dokument komercyjnie lub niekomercyjnie, w dowolnej postaci, pod warunkiem zamieszczenia na kadej kopii Dokumentu treci Licencji, informacji o prawie autorskim oraz noty mwicej, e do Dokumentu ma zastosowanie niniejsza Licencja, a take pod warunkiem nie umieszczania adnych dodatkowych ogranicze, ktre nie wynikaj z Licencji. Licencjobiorca nie ma prawa uywa adnych technicznych metod pomiarowych utrudniajcych lub kontrolujcych czytanie lub dalsze kopiowanie utworzonych i rozpowszechnianych przez siebie kopii. Moe jednak pobiera opaty za udostpnianie kopii. W przypadku dystrybucji duej liczby kopii Licencjobiorca jest zobowizany przestrzega warunkw wymienionych w punkcie 3. Licencjobiorca moe take wypoycza kopie na warunkach opisanych powyej, a take wystawia je publicznie.
553
3. Kopiowanie ilociowe
Jeeli Licencjobiorca publikuje drukowane kopie Dokumentu w liczbie wikszej ni 100, a licencja Dokumentu wymaga umieszczenia Treci okadki, naley doczy kopie okadek, ktre zawieraj ca wyran i czyteln Tre okadki: tre przedniej okadki, na przedniej okadce, a tre tylnej okadki, na tylnej okadce. Obie okadki musz te jasno i czytelnie informowa o Licencjobiorcy jako wydawcy tych kopii. Okadka przednia musi przedstawia peny tytu; wszystkie sowa musz by rwnie dobrze widoczne i czytelne. Licencjobiorca moe na okadkach umieszcza take inne informacje dodatkowe. Kopiowanie ze zmianami ograniczonymi do okadek, dopki nie narusza tytuu Dokumentu i spenia opisane warunki, moe by traktowane pod innymi wzgldami jako kopiowanie dosowne. Jeeli napisy wymagane na ktrej z okadek s zbyt obszerne, by mogy pozosta czytelne po ich umieszczeniu, Licencjobiorca powinien umieci ich pocztek(tak ilo, jaka wydaje si rozsdna) na rzeczywistej okadce, a pozosta cz na ssiednich stronach. W przypadku publikowania lub rozpowszechniania Niejawnych kopii Dokumentu w liczbie wikszej ni 100, Licencjobiorca zobowizany jest albo doczy do kadej z nich Jawn kopi czyteln dla komputera, albo wymieni w lub przy kadej kopii Niejawnej publicznie dostpn w sieci komputerowej lokalizacj penej kopii Jawnej Dokumentu, bez adnych informacji dodanych -- lokalizacj, do ktrej kady uytkownik sieci miaby bezpatny anonimowy dostp za pomoc standardowych publicznych protokow sieciowych. W przypadku drugim Licencjobiorca musi podj odpowiednie rodki ostronoci, by wymieniona kopia Jawna pozostaa dostpna we wskazanej lokalizacji przynajmniej przez rok od momentu rozpowszechnienia ostatniej kopii Niejawnej (bezporedniego lub przez agentw albo sprzedawcw) danego wydania. Zaleca si, cho nie wymaga, aby przed rozpoczciem rozpowszechniania duej liczby kopii Dokumentu, Licencjobiorca skontaktowa si z jego autorami celem uzyskania uaktualnionej wersji Dokumentu.
4. Modyfikacje
Licencjobiorca moe kopiowa i rozpowszechnia Zmodyfikowan wersj Dokumentu na zasadach wymienionych powyej w punkcie 2 i 3 pod warunkiem cisego przestrzegania niniejszej Licencji. Zmodyfikowana wersja peni wtedy rol Dokumentu, a wic Licencja dotyczca modyfikacji i rozpowszechniania Zmodyfikowanej wersji przenoszona jest na kadego, kto posiada jej kopi. Ponadto Licencjobiorca musi w stosunku do Zmodyfikowanej wersji speni nastpujce wymogi: A. Uy na Stronie tytuowej (i na okadkach, o ile istniej) tytuu innego ni tytu Dokumentu i innego ni tytuy poprzednich wersji (ktre, o ile istniay, powinny zosta wymienione w Dokumencie, w sekcji Historia). Tytuu jednej z ostatnich wersji Licencjobiorca moe uy, jeeli jej wydawca wyrazi na to zgod. B. Wymieni na Stronie tytuowej, jako autorw, jedn lub kilka osb albo jednostek odpowiedzialnych za autorstwo modyfikacji Zmodyfikowanej wersji, a take przynajmniej piciu spord pierwotnych autorw Dokumentu (wszystkich, jeli byo ich mniej ni piciu). C. Umieci na Stronie tytuowej nazw wydawcy Zmodyfikowanej wersji. D. Zachowa wszelkie noty o prawach autorskich zawarte w Dokumencie. E. Doda odpowiedni not o prawach autorskich dotyczcych modyfikacji obok innych not o prawach autorskich.
554
Inne
F. Bezporednio po notach o prawach autorskich, zamieci not licencyjn zezwalajc na publiczne uytkowanie Zmodyfikowanej wersji na zasadach niniejszej Licencji w postaci podanej w Zaczniku poniej. G. Zachowa w nocie licencyjnej pen list Sekcji niezmiennych i wymaganych Treci okadki podanych w nocie licencyjnej Dokumentu. H. Doczy niezmienion kopi niniejszej Licencji. I. Zachowa sekcj zatytuowan "Historia" oraz jej tytu i doda do niej informacj dotyczc przynajmniej tytuu, roku publikacji, nowych autorw i wydawcy Zmodyfikowanej wersji zgodnie z danymi zamieszczonymi na Stronie tytuowej. Jeeli w Dokumencie nie istnieje sekcja pod tytuem "Historia", naley j utworzy, podajc tytu, rok, autorw i wydawc Dokumentu zgodnie z danymi zamieszczonymi na stronie tytuowej, a nastpnie dodajc informacj dotyczc Zmodyfikowanej wersji, jak opisano w poprzednim zdaniu. J. Zachowa wymienion w Dokumencie (jeli taka istniaa) informacj o lokalizacji sieciowej, publicznie dostpnej Jawnej kopii Dokumentu, a take o podanych w Dokumencie lokalizacjach sieciowych poprzednich wersji, na ktrych zosta on oparty. Informacje te mog si znajdowa w sekcji "Historia". Zezwala si na pominicie lokalizacji sieciowej prac, ktre zostay wydane przynajmniej cztery lata przed samym Dokumentem, a take tych, ktrych pierwotny wydawca wyraa na to zgod. K. W kadej sekcji zatytuowanej "Podzikowania" lub "Dedykacje" zachowa tytu i tre, oddajc rwnie ton kadego z podzikowa i dedykacji. L. Zachowa wszelkie Sekcje niezmienne Dokumentu w niezmienionej postaci (dotyczy zarwno treci, jak i tytuu). Numery sekcji i rwnowane im oznaczenia nie s traktowane jako nalece do tytuw sekcji. M. Usun wszelkie sekcje zatytuowane "Adnotacje". Nie musz one by zaczane w Zmodyfikowanej wersji. N. Nie nadawa adnej z istniejcych sekcji tytuu "Adnotacje" ani tytuu pokrywajcego si z jakkolwiek Sekcj niezmienn. Jeeli Zmodyfikowana wersja zawiera nowe sekcje pocztkowe lub dodatki stanowice Sekcje drugorzdne i nie zawierajce materiau skopiowanego z Dokumentu, Licencjobiorca moe je lub ich cz oznaczy jako sekcje niezmienne. W tym celu musi on doda ich tytuy do listy Sekcji niezmiennych zawartej w nocie licencyjnej Zmodyfikowanej wersji. Tytuy te musz by rne od tytuw pozostaych sekcji. Licencjobiorca moe doda sekcj "Adnotacje", pod warunkiem, e nie zawiera ona adnych treci innych ni adnotacje dotyczce Zmodyfikowanej wersji -- mog to by na przykad stwierdzenia o recenzji koleeskiej albo o akceptacji tekstu przez organizacj jako autorytatywnej definicji standardu. Na kocu listy Treci okadki w Zmodyfikowanej wersji, Licencjobiorca moe doda fragment "do umieszczenia na przedniej okadce" o dugoci nie przekraczajcej piciu sw, a take fragment o dugoci do 25 sw "do umieszczenia na tylnej okadce". Przez kad jednostk (lub na mocy ustale przez ni poczynionych) moe zosta dodany tylko jeden fragment z przeznaczeniem na przedni okadk i jeden z przeznaczeniem na tyln. Jeeli Dokument zawiera ju tre okadki dla danej okadki, dodan uprzednio przez Licencjobiorc lub w ramach ustale z jednostk, w imieniu ktrej dziaa Licencjobiorca, nowa tre okadki nie moe zosta dodana. Dopuszcza si jednak zastpienie poprzedniej treci okadki now pod warunkiem wyranej zgody poprzedniego wydawcy, od ktrego stara tre pochodzi. Niniejsza Licencja nie oznacza, i autor (autorzy) i wydawca (wydawcy) wyraaj zgod na publiczne uywanie ich nazwisk w celu zapewnienia autorytetu jakiejkolwiek Zmodyfikowanej wersji.
555
5. czenie dokumentw
Licencjobiorca moe czy Dokument z innymi dokumentami wydanymi na warunkach niniejszej Licencji, na warunkach podanych dla wersji zmodyfikowanych w czci 4 powyej, jednak tylko wtedy, gdy w poczeniu zostan zawarte wszystkie Sekcje niezmienne wszystkich oryginalnych dokumentw w postaci niezmodyfikowanej i gdy bd one wymienione jako Sekcje niezmienne poczenia w jego nocie licencyjnej. Poczenie wymaga tylko jednej kopii niniejszej Licencji, a kilka identycznych Sekcji niezmiennych moe zosta zastpionych jedn. Jeeli istnieje kilka Sekcji niezmiennych o tym samym tytule, ale rnej zawartoci, Licencjobiorca jest zobowizany uczyni tytu kadej z nich unikalnym poprzez dodanie na jego kocu, w nawiasach, nazwy oryginalnego autora lub wydawcy danej sekcji, o ile jest znany, lub unikalnego numeru. Podobne poprawki wymagane s w tytuach sekcji na licie Sekcji niezmiennych w nocie licencyjnej poczenia. W poczeniu Licencjobiorca musi zawrze wszystkie sekcje zatytuowane "Historia" z dokumentw oryginalnych, tworzc jedn sekcj "Historia". Podobnie ma postpi z sekcjami "Podzikowania" i "Dedykacje". Wszystkie sekcje zatytuowane "Adnotacje" naley usun.
6. Zbiory dokumentw
Licencjobiorca moe utworzy zbir skadajcy si z Dokumentu i innych dokumentw wydanych zgodnie z niniejsz Licencj i zastpi poszczeglne kopie Licencji pochodzce z tych dokumentw jedn kopi doczon do zbioru, pod warunkiem zachowania zasad Licencji dotyczcych kopii dosownych we wszelkich innych aspektach kadego z dokumentw. Z takiego zbioru Licencjobiorca moe wyodrbni pojedynczy dokument i rozpowszechnia go niezalenie na zasadach niniejszej Licencji, pod warunkiem zamieszczenia w wyodrbnionym dokumencie kopii niniejszej Licencji oraz zachowania zasad Licencji we wszystkich aspektach dotyczcych dosownej kopii tego dokumentu.
556
Inne
8. Tumaczenia
Tumaczenie jest uznawane za rodzaj modyfikacji, a wic Licencjobiorca moe rozpowszechnia tumaczenia Dokumentu na zasadach wymienionych w punkcie 4. Zastpienie Sekcji niezmiennych ich tumaczeniem wymaga specjalnej zgody wacicieli prawa autorskiego. Dopuszcza si jednak zamieszczanie tumacze wybranych lub wszystkich Sekcji niezmiennych obok ich wersji oryginalnych. Podanie tumaczenia niniejszej Licencji moliwe jest pod warunkiem zamieszczenia take jej oryginalnej wersji angielskiej. W przypadku niezgodnoci pomidzy zamieszczonym tumaczeniem a oryginaln wersj angielsk niniejszej Licencji moc prawn ma oryginalna wersja angielska.
9. Wyganicie
Poza przypadkami jednoznacznie dopuszczonymi na warunkach niniejszej Licencji nie zezwala si Licencjobiorcy na kopiowanie, modyfikowanie, czy rozpowszechnianie Dokumentu ani te na cedowanie praw licencyjnych. We wszystkich pozostaych wypadkach kada prba kopiowania, modyfikowania lub rozpowszechniania Dokumentu albo cedowania praw licencyjnych jest niewana i powoduje automatyczne wyganicie praw, ktre licencjobiorca naby z tytuu Licencji. Niemniej jednak w odniesieniu do stron, ktre ju otrzymay od Licencjobiorcy kopie albo prawa w ramach niniejszej Licencji, licencje nie zostan anulowane, dopki strony te w peni si do nich stosuj.
557
Jeli nie zamieszczasz Sekcji Niezmiennych, napisz "nie zawiera Sekcji Niezmiennych" zamiast spisu sekcji niezmiennych. Jeli nie umieszczasz Teksu na Przedniej Okadce wpisz "bez Tekstu na Okadce" w miejsce "wraz z Tekstem na Przedniej Okadce LISTA", analogicznie postp z "Tekstem na Tylnej Okadce" Jeli w twoim dokumencie zawarte s nieszablonowe przykady kodu programu, zalecamy aby take uwolni te przykady wybierajc licencj wolnego oprogramowania, tak jak Powszechna Licencja Publiczna GNU, w celu zapewnienia moliwoci ich uycia w wolnym oprogramowaniu.