You are on page 1of 558

Karol Kuczmarski (Xion)

Od zera do gier kodera


megatutorial

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.

Avocado Software http://avocado.risp.pl

Game Design PL http://warsztat.pac.pl

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

Z czego skada si program? ______________________________________ 29


C++, pierwsze starcie ________________________________________________________ Bliskie spotkanie z kompilatorem ______________________________________________ Rodzaje aplikacji___________________________________________________________ Pierwszy program __________________________________________________________ Kod programu_______________________________________________________________ Komentarze ______________________________________________________________ Funkcja main() ___________________________________________________________ Pisanie tekstu w konsoli _____________________________________________________ Doczanie plikw nagwkowych______________________________________________ Procedury i funkcje ___________________________________________________________ Wasne funkcje ____________________________________________________________ Tryb ledzenia ____________________________________________________________ Przebieg programu _________________________________________________________ Zmienne i stae ______________________________________________________________ Zmienne i ich typy _________________________________________________________ Strumie wejcia __________________________________________________________ Stae ____________________________________________________________________ Operatory arytmetyczne_______________________________________________________ Umiemy liczy! ____________________________________________________________ Rodzaje operatorw arytmetycznych ___________________________________________ Priorytety operatorw_______________________________________________________ Tajemnicze znaki __________________________________________________________ Podsumowanie ______________________________________________________________ Pytania i zadania __________________________________________________________ Pytania ________________________________________________________________ wiczenia ______________________________________________________________ 29 29 31 32 33 33 33 34 35 36 36 37 38 39 39 40 41 42 42 43 44 44 45 45 46 46

Dziaanie programu _____________________________________________ 47


Funkcje nieco bliej __________________________________________________________ Parametry funkcji __________________________________________________________ Warto zwracana przez funkcj ______________________________________________ Skadnia funkcji ___________________________________________________________ Sterowanie warunkowe _______________________________________________________ Instrukcja warunkowa if ____________________________________________________ Fraza else _____________________________________________________________ Bardziej zoony przykad__________________________________________________ Instrukcja wyboru switch ___________________________________________________ Ptle ______________________________________________________________________ Ptle warunkowe do i while__________________________________________________ Ptla do _______________________________________________________________ Ptla while_____________________________________________________________ 47 47 49 50 51 51 53 54 56 58 58 58 60

4
Ptla krokowa for _________________________________________________________ Instrukcje break i continue _________________________________________________ Podsumowanie ______________________________________________________________ Pytania i zadania __________________________________________________________ Pytania ________________________________________________________________ wiczenia ______________________________________________________________ 62 65 66 67 67 67

Operacje na zmiennych __________________________________________ 69


Wnikliwy rzut oka na zmienne __________________________________________________ 69 Zasig zmiennych__________________________________________________________ 69 Zasig lokalny __________________________________________________________ 70 Zasig moduowy ________________________________________________________ 72 Przesanianie nazw _______________________________________________________ 73 Modyfikatory zmiennych_____________________________________________________ 74 Zmienne statyczne _______________________________________________________ 75 Stae __________________________________________________________________ 76 Typy zmiennych _____________________________________________________________ 76 Modyfikatory typw liczbowych _______________________________________________ 77 Typy ze znakiem i bez znaku _______________________________________________ 77 Rozmiar typu cakowitego _________________________________________________ 78 Precyzja typu rzeczywistego _______________________________________________ 79 Skrcone nazwy _________________________________________________________ 80 Pomocne konstrukcje _______________________________________________________ 80 Instrukcja typedef_______________________________________________________ 80 Operator sizeof ________________________________________________________ 81 Rzutowanie _______________________________________________________________ 83 Proste rzutowanie________________________________________________________ 84 Operator static_cast ____________________________________________________ 86 Kalkulacje na liczbach_________________________________________________________ 88 Przydatne funkcje__________________________________________________________ 88 Funkcje potgowe _______________________________________________________ 88 Funkcje wykadnicze i logarytmiczne _________________________________________ 89 Funkcje trygonometryczne_________________________________________________ 90 Liczby pseudolosowe _____________________________________________________ 91 Zaokrglanie liczb rzeczywistych ____________________________________________ 93 Inne funkcje ____________________________________________________________ 94 Znane i nieznane operatory __________________________________________________ 95 Dwa rodzaje ____________________________________________________________ 95 Sekrety inkrementacji i dekrementacji _______________________________________ 96 Swko o dzieleniu _______________________________________________________ 97 acuchy znakw ____________________________________________________________ 98 Napisy wedug C++ ________________________________________________________ 99 Typy zmiennych tekstowych ______________________________________________ 100 Manipulowanie acuchami znakw ___________________________________________ 100 Inicjalizacja ___________________________________________________________ 100 czenie napisw _______________________________________________________ 102 Pobieranie pojedynczych znakw ___________________________________________ 103 Wyraenia logiczne __________________________________________________________ 105 Porwnywanie wartoci zmiennych ___________________________________________ 105 Operatory logiczne ________________________________________________________ 105 Koniunkcja ____________________________________________________________ 106 Alternatywa ___________________________________________________________ 106 Negacja ______________________________________________________________ 106 Zestawienie operatorw logicznych _________________________________________ 107 Typ bool________________________________________________________________ 108 Operator warunkowy ______________________________________________________ 109 Podsumowanie _____________________________________________________________ 110 Pytania i zadania _________________________________________________________ 111 Pytania _______________________________________________________________ 111 wiczenia _____________________________________________________________ 111

Zoone zmienne ______________________________________________ 113


Tablice ___________________________________________________________________ 113

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

Co na to C++? _________________________________________________________ 167


Definiowanie klas _____________________________________________________________ 167 Implementacja metod __________________________________________________________ 168 Tworzenie obiektw____________________________________________________________ 168

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? _________________________________________________________________

171 174 175 176 178

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

Wskaniki na obiekty ____________________________________________________ 183


184 184 185 186 187

Podsumowanie _____________________________________________________________ Pytania i zadania _________________________________________________________ Pytania _______________________________________________________________ wiczenia _____________________________________________________________

188 188 188 189

Programowanie obiektowe ______________________________________ 191


Dziedziczenie ______________________________________________________________ O powstawaniu klas drog doboru naturalnego __________________________________ Od prostoty do komplikacji, czyli ewolucja ___________________________________ Z klasy bazowej do pochodnej, czyli dziedzictwo przodkw ______________________ Obiekt o kilku klasach, czyli zmienno gatunkowa_____________________________ Dziedziczenie w C++ ______________________________________________________ Podstawy _____________________________________________________________ 191 192 193 194 195 195 195

Definicja klasy bazowej i specyfikator protected_____________________________________ 195 Definicja klasy pochodnej _______________________________________________________ 197

Dziedziczenie pojedyncze_________________________________________________ 198


Proste przypadki ______________________________________________________________ Sztafeta pokole ______________________________________________________________ Paskie hierarchie _____________________________________________________________ Podsumowanie _______________________________________________________________

198 199 201 202

Dziedziczenie wielokrotne ________________________________________________ 202 Puapki dziedziczenia ____________________________________________________ 202


Co nie jest dziedziczone? _______________________________________________________ 202 Obiekty kompozytowe __________________________________________________________ 203

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

Zaczynamy od zera dosownie____________________________________________ 209


Czysto wirtualne metody________________________________________________________ 210 Abstrakcyjne klasy bazowe ______________________________________________________ 210

Polimorfizm______________________________________________________________ 211 Oglny kod do szczeglnych zastosowa_____________________________________ 212


Sprowadzanie do bazy _________________________________________________________ 212 Klasy wiedz same, co naley robi _______________________________________________ 215

Typy pod kontrol ______________________________________________________ 217


Operator dynamic_cast ________________________________________________________ 217 typeid i informacje o typie podczas dziaania programu _______________________________ 219 Alternatywne rozwizania _______________________________________________________ 221

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

228 228 231 231

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 operacyjna ______________________________________________________ 246


Skd si bierze pami operacyjna? _______________________________________________ 246 Pami wirtualna ______________________________________________________________ 247

Pami trwaa__________________________________________________________ 247 Organizacja pamici operacyjnej _____________________________________________ 247 Adresowanie pamici ____________________________________________________ 248
Epoka niewygodnych segmentw _________________________________________________ 248 Paski model pamici ___________________________________________________________ 249

Stos i sterta ___________________________________________________________ 249


Czym jest stos? _______________________________________________________________ 249 O stercie ____________________________________________________________________ 250

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

Niezbdne operatory ____________________________________________________ 256


Pobieranie adresu i dereferencja __________________________________________________ 256 Wyuskiwanie skadnikw _______________________________________________________ 257

Konwersje typw wskanikowych __________________________________________ 258


Matka wszystkich wskanikw____________________________________________________ 259 Przywracanie do stanu uywalnoci _______________________________________________ 260 Midzy palcami kompilatora _____________________________________________________ 260 Tablice jednowymiarowe w pamici _______________________________________________ Wskanik w ruchu _____________________________________________________________ Tablice wielowymiarowe w pamici ________________________________________________ acuchy znakw w stylu C______________________________________________________

Wskaniki i tablice ______________________________________________________ 261


261 262 263 263

Przekazywanie wskanikw do funkcji _______________________________________ 265

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

Dynamiczne tablice _____________________________________________________ 270

8
Tablice jednowymiarowe ________________________________________________________ 271 Opakowanie w klas ___________________________________________________________ 272 Tablice wielowymiarowe ________________________________________________________ 274

Referencje ______________________________________________________________ 277 Typy referencyjne ______________________________________________________ 278


Deklarowanie referencji_________________________________________________________ 278 Prawo staoci referencji ________________________________________________________ 278

Referencje i funkcje _____________________________________________________ 279


Parametry przekazywane przez referencje __________________________________________ 279 Zwracanie referencji ___________________________________________________________ 280

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

Konwencja wywoania ___________________________________________________ 283 Nazwa funkcji __________________________________________________________ 286


Rozterki kompilatora i linkera ____________________________________________________ 286

Parametry funkcji _______________________________________________________ 286 Uywanie wskanikw do funkcji _____________________________________________ 287 Typy wskanikw do funkcji_______________________________________________ 287
Wasnoci wyrniajce funkcj __________________________________________________ 287 Typ wskanika do funkcji _______________________________________________________ 287

Wskaniki do funkcji w C++ ______________________________________________ 288


Od funkcji do wskanika na ni ___________________________________________________ Specjalna konwencja___________________________________________________________ Skadnia deklaracji wskanika do funkcji ___________________________________________ Wskaniki do funkcji w akcji _____________________________________________________ Przykad wykorzystania wskanikw do funkcji_______________________________________ 288 289 290 290 291

Zastosowania __________________________________________________________ Podsumowanie _____________________________________________________________ Pytania i zadania _________________________________________________________ Pytania _______________________________________________________________ wiczenia _____________________________________________________________

295 295 296 296 296

ZAAWANSOWANE C++ _______________________________ 299


Preprocesor __________________________________________________ 301
Pomocnik kompilatora _______________________________________________________ Gdzie on jest? __________________________________________________________ Zwyczajowy przebieg budowania programu __________________________________ Dodajemy preprocesor ___________________________________________________ Dziaanie preprocesora _____________________________________________________ Dyrektywy ____________________________________________________________ 301 302 302 303 304 304 306 307 307 307

Bez rednika _________________________________________________________________ 304 Ciekawostka: sekwencje trjznakowe ______________________________________________ 305

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 _____________________________________

Pojedynek: makra kontra stae ____________________________________________ 310

310 310 310 311 311 311 312 313

Predefiniowane makra kompilatora _________________________________________ 314


Numer linii i nazwa pliku ________________________________________________________ 314 Numer wiersza _____________________________________________________________ 314

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

Makra parametryzowane ___________________________________________________ 316 Definiowanie parametrycznych makr ________________________________________ 317


Kilka przykadw ______________________________________________________________ Wzory matematyczne ________________________________________________________ Skracanie zapisu____________________________________________________________ Operatory preprocesora ________________________________________________________ Sklejacz __________________________________________________________________ Operator acuchujcy _______________________________________________________ 318 318 318 319 319 319

Niebezpieczestwa makr _________________________________________________ 320


Brak kontroli typw ____________________________________________________________ 320 Parokrotne obliczanie argumentw ________________________________________________ 321 Priorytety operatorw __________________________________________________________ 321 Efektywno _________________________________________________________________ Funkcje inline ______________________________________________________________ Makra kontra funkcje inline ___________________________________________________ Brak kontroli typw ____________________________________________________________ Ciekawostka: funkcje szablonowe ______________________________________________ Nie korzystajmy z makr, lecz z obiektw const ____________________________________ Nie korzystajmy z makr, lecz z (szablonowych) funkcji inline _________________________ Korzystajmy z makr, by zastpowa powtarzajce si fragmenty kodu__________________ Korzystajmy z makr, by skraca sobie zapis ______________________________________ Korzystajmy z makr zgodnie z ich przeznaczeniem _________________________________

Zalety makrodefinicji ____________________________________________________ 322


322 322 323 323 323 324 324 324 324 325

Zastosowania makr _____________________________________________________ 324

Kontrola procesu kompilacji ___________________________________________________ Dyrektywy #ifdef i #ifndef ________________________________________________ Puste makra ___________________________________________________________ Dyrektywa #ifdef ______________________________________________________ Dyrektywa #ifndef _____________________________________________________ Dyrektywa #else _______________________________________________________ Dyrektywa warunkowa #if _________________________________________________ Konstruowanie warunkw ________________________________________________

325 326 326 326 327 327 328 328

Operator defined _____________________________________________________________ 328 Zoone warunki ______________________________________________________________ 329

Skomplikowane warunki kompilacji _________________________________________ 329 Dyrektywa #error ______________________________________________________ Reszta dobroci _____________________________________________________________ Doczanie plikw _________________________________________________________ Dwa warianty #include __________________________________________________
Zagniedanie dyrektyw ________________________________________________________ 329 Dyrektywa #elif______________________________________________________________ 330

330 331 331 331


331 332 332 332 333

Z nawiasami ostrymi ___________________________________________________________ Z cudzysowami_______________________________________________________________ Ktry wybra? ________________________________________________________________ Nasz czy biblioteczny ________________________________________________________ cieki wzgldne____________________________________________________________

Zabezpieczenie przed wielokrotnym doczaniem ______________________________ 333

Tradycyjne rozwizanie _________________________________________________________ 333 Pomaga kompilator ____________________________________________________________ 334

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

Podsumowanie _____________________________________________________________ Pytania i zadania _________________________________________________________ Pytania _______________________________________________________________ wiczenia _____________________________________________________________

340 340 340 341

Zaawansowana obiektowo _____________________________________ 343


O przyjani ________________________________________________________________ Funkcje zaprzyjanione ____________________________________________________ Deklaracja przyjani z funkcj _____________________________________________ Na co jeszcze trzeba zwrci uwag ________________________________________ 343 345 345 346
346 347 347 347

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++ ______________________________________________

348 348 349 351 351

Przyja nie jest automatycznie wzajemna__________________________________________ 351 Przyja nie jest przechodnia ____________________________________________________ 351 Przyja nie jest dziedziczna_____________________________________________________ 351

Zastosowania __________________________________________________________ 352


Wykorzystanie przyjani z funkcj ________________________________________________ 352 Korzyci czerpane z przyjani klas ________________________________________________ 352

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

Inicjalizacja ___________________________________________________________ 355

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 __________________________________________________

357 357 358 359 359

Konstruktor kopiujcy ___________________________________________________ 361

361 362 362 362 363

Konwersje_______________________________________________________________ 363 Sposoby dokonywania konwersji ___________________________________________ 364


Konstruktory konwertujce ______________________________________________________ Konstruktor z jednym obowizkowym parametrem _________________________________ Swko explicit ___________________________________________________________ Operatory konwersji ___________________________________________________________ Stwarzamy sobie problem ____________________________________________________ Definiowanie operatora konwersji ______________________________________________ 365 365 367 367 368 368

Wybr odpowiedniego sposobu ____________________________________________ 369 Przecianie operatorw ______________________________________________________ 370

11
Cechy operatorw ________________________________________________________ 371 Liczba argumentw _____________________________________________________ 371
Operatory jednoargumentowe____________________________________________________ 372 Operatory dwuargumentowe_____________________________________________________ 372

Priorytet ______________________________________________________________ czno ______________________________________________________________ Operatory w C++ _________________________________________________________ Operatory arytmetyczne _________________________________________________

372 373 373 374

Unarne operatory arytmetyczne __________________________________________________ 374 Inkrementacja i dekrementacja ________________________________________________ 374 Binarne operatory arytmetyczne __________________________________________________ 375

Operatory bitowe _______________________________________________________ 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 ___________________________________________________

378 378 379 379 380

Operatory wskanikowe __________________________________________________ 380


Pobranie adresu ______________________________________________________________ Dostp do pamici poprzez wskanik ______________________________________________ Dereferencja _______________________________________________________________ Indeksowanie ______________________________________________________________ Alokacja pamici ______________________________________________________________ new ______________________________________________________________________ new[] ____________________________________________________________________ Zwalnianie pamici ____________________________________________________________ delete ___________________________________________________________________ delete[] _________________________________________________________________ Operator sizeof ______________________________________________________________ Ciekawostka: operator __alignof ______________________________________________ 380 381 381 381 382 382 382 383 383 383 383 384 384 384 385 385 386 386 386 387

Operatory pamici ______________________________________________________ 382

Operatory typw _______________________________________________________ 384


Operatory rzutowania __________________________________________________________ static_cast ______________________________________________________________ dynamic_cast______________________________________________________________ reinterpret_cast __________________________________________________________ const_cast _______________________________________________________________ Rzutowanie w stylu C ________________________________________________________ Rzutowanie funkcyjne________________________________________________________ Operator typeid ______________________________________________________________

Operatory dostpu do skadowych __________________________________________ 387

Wyuskanie z obiektu __________________________________________________________ 387 Wyuskanie ze wskanika _______________________________________________________ 388 Operator zasigu ______________________________________________________________ 388 Nawiasy okrge ______________________________________________________________ 388 Operator warunkowy ___________________________________________________________ 389 Przecinek ____________________________________________________________________ 389

Pozostae operatory _____________________________________________________ 388

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

Sposoby przeciania operatorw __________________________________________ 394


Najczciej stosowane przecienia _______________________________________________ 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

Wskazwki dla pocztkujcego przeciacza __________________________________ 412

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 do pola wewntrz klasy __________________________________________ 415


416 417 418 419

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

Podsumowanie _____________________________________________________________ Pytania i zadania _________________________________________________________ Pytania _______________________________________________________________ wiczenia _____________________________________________________________

430 430 430 431

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

Niezbyt dobre wyjcia ___________________________________________________ 438

Wyjtki _________________________________________________________________ 439 Rzucanie i apanie wyjtkw ______________________________________________ 440


Blok try-catch _______________________________________________________________ Instrukcja throw ______________________________________________________________ Wdrwka wyjtku __________________________________________________________ throw a return ____________________________________________________________ 440 441 441 441

Waciwy chwyt ________________________________________________________ 442


Kolejno blokw catch ________________________________________________________ 443 Dopasowywanie typu obiektu wyjtku ___________________________________________ 444

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

Porzdki ______________________________________________________________ 454

Zarzdzanie zasobami w obliczu wyjtkw _____________________________________ 456 Opakowywanie _________________________________________________________ 458


Destruktor wskanika? ________________________________________________________ Sprytny wskanik _____________________________________________________________ Nieco uwag __________________________________________________________________ Rne typy wskanikw ______________________________________________________ Uywajmy tylko tam, gdzie to konieczne _________________________________________ 459 459 461 461 461

Co ju zrobiono za nas ___________________________________________________ 461


Klasa std::auto_ptr __________________________________________________________ 461 Pliki w Bibliotece Standardowej___________________________________________________ 462

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____________________________________________________________

Organizacja obsugi wyjtkw _____________________________________________ 466


466 466 467 467 468 468 469

Uwagi oglne ____________________________________________________________ 469 Korzyci ze stosowania wyjtkw __________________________________________ 469


Informacja o bdzie w kadej sytuacji _____________________________________________ 469 Uproszczenie kodu ____________________________________________________________ 470 Wzrost niezawodnoci kodu _____________________________________________________ 470 Nie uywajmy ich tam, gdzie wystarcz inne konstrukcje ______________________________ 471 Nie uywajmy wyjtkw na si __________________________________________________ 471

Naduywanie wyjtkw __________________________________________________ 471 Podsumowanie _____________________________________________________________ Pytania i zadania _________________________________________________________ Pytania _______________________________________________________________ wiczenia _____________________________________________________________ 472 472 472 472

Szablony ____________________________________________________ 473


Podstawy _________________________________________________________________ 474 Idea szablonw___________________________________________________________ 474 ciso C++ powodem blu gowy _________________________________________ 474
Dwa typowe problemy__________________________________________________________ Problem 1: te same funkcje dla rnych typw ____________________________________ Problem 2: klasy operujce na dowolnych typach danych ____________________________ Moliwe rozwizania ___________________________________________________________ Wykorzystanie preprocesora __________________________________________________ Uywanie oglnych typw ____________________________________________________ 474 474 475 475 475 475

Szablony jako rozwizanie ________________________________________________ 476

Kod niezaleny od typu _________________________________________________________ 476 Kompilator to potrafi ___________________________________________________________ 476 Skadnia szablonu___________________________________________________________ 476

14
Co moe by szablonem ______________________________________________________ 477

Szablony funkcji __________________________________________________________ 478 Definiowanie szablonu funkcji _____________________________________________ 478


Podstawowa definicja szablonu funkcji _____________________________________________ Stosowalno definicji________________________________________________________ Parametr szablonu uyty w ciele funkcji__________________________________________ Parametr szablonu i parametr funkcji____________________________________________ Kilka parametrw szablonu ___________________________________________________ Specjalizacja szablonu funkcji ____________________________________________________ Wyjtkowy przypadek _______________________________________________________ Ciekawostka: specjalizacja czciowa szablonu funkcji ______________________________ Jawne okrelenie typu __________________________________________________________ Wywoywanie konkretnej wersji funkcji szablonowej ________________________________ Uycie wskanika na funkcj szablonow _________________________________________ Dedukcja typu na podstawie argumentw funkcji_____________________________________ Jak to dziaa _______________________________________________________________ Dedukcja przy wykorzystaniu kilku parametrw szablonu ____________________________ 478 479 479 480 480 481 482 482

Wywoywanie funkcji szablonowej __________________________________________ 483

484 484 484 485 485 486

Szablony klas ____________________________________________________________ 487 Definicja szablonu klas___________________________________________________ 487


Prosty przykad tablicy _________________________________________________________ Definiujemy szablon _________________________________________________________ Implementacja metod poza definicj ____________________________________________ Korzystanie z tablicy_________________________________________________________ Dziedziczenie i szablony klas_____________________________________________________ Dziedziczenie klas szablonowych _______________________________________________ Dziedziczenie szablonw klas __________________________________________________ Deklaracje w szablonach klas ____________________________________________________ Aliasy typedef _____________________________________________________________ Deklaracje przyjani _________________________________________________________ Szablony funkcji skadowych __________________________________________________ Tworzenie obiektw____________________________________________________________ Stwarzamy obiekt klasy szablonowej ____________________________________________ Co si dzieje, gdy tworzymy obiekt szablonu klasy _________________________________ Funkcje operujce na obiektach klas szablonowych ___________________________________ 488 489 490 491 492 492 493 494 494 495 495

Korzystanie z klas szablonowych ___________________________________________ 497

497 497 498 500 501 501 503 504 504 504 506 506 508

Specjalizacje szablonw klas ______________________________________________ 501


Specjalizowanie szablonu klasy___________________________________________________ Wasna klasa specjalizowana __________________________________________________ Specjalizacja metody klasy____________________________________________________ Czciowa specjalizacja szablonu klasy_____________________________________________ Problem natury tablicowej ____________________________________________________ Rozwizanie: przypadek szczeglniejszy, ale nie za bardzo___________________________ Domylne parametry szablonu klasy_______________________________________________ Typowy typ ________________________________________________________________ Skorzystanie z poprzedniego parametru _________________________________________

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

Szablony parametrw ___________________________________________________ 514


Idc za potrzeb ______________________________________________________________ Dodatkowy parametr: typ wewntrznej tablicy ____________________________________ Drobna niedogodno ________________________________________________________ Deklarowanie szablonowych parametrw szablonw __________________________________

Problemy z szablonami_____________________________________________________ 517 Uatwienia dla kompilatora________________________________________________ 517


Nawiasy ostre ________________________________________________________________ Nieoczywisty przykad _______________________________________________________ Ciekawostka: dlaczego tak si dzieje ____________________________________________ Nazwy zalene________________________________________________________________ 518 518 518 519

15
Sowo kluczowe typename ____________________________________________________ 520 Ciekawostka: konstrukcje ::template, .template i ->template ______________________ 521 522 523 523 524 525 525 525 526 526 527 527

Organizacja kodu szablonw ______________________________________________ 522


Model wczania ______________________________________________________________ Zwyky kod ________________________________________________________________ Prbujemy zastosowa szablony _______________________________________________ Rozwizanie - istota modelu wczania __________________________________________ Konkretyzacja jawna ___________________________________________________________ Instrukcje jawnej konkretyzacji ________________________________________________ Wady i zalety konkretyzacji jawnej _____________________________________________ Model separacji _______________________________________________________________ Szablony eksportowane ______________________________________________________ Nie ma ry bez kolcw ______________________________________________________ Wsppraca modelu wczania i separacji_________________________________________

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____________________________________________________________________

529 529 529 530

530 530 531 531 531 531 532 532 532

Struktury danych _________________________________________________________ 532 Krotki ________________________________________________________________ 533


Przykad pary ________________________________________________________________ Definicja szablonu___________________________________________________________ Pomocna funkcja ___________________________________________________________ Dalsze usprawnienia _________________________________________________________ Trjki i wysze krotki __________________________________________________________ 533 533 534 535 536

Pojemniki _____________________________________________________________ 537


Przykad klasy kontenera - stos __________________________________________________ Czym jest stos _____________________________________________________________ Definicja szablonu klasy ______________________________________________________ Korzystanie z szablonu _______________________________________________________ Programowanie oglne _________________________________________________________ 538 538 538 539 540

Podsumowanie _____________________________________________________________ Pytania i zadania _________________________________________________________ Pytania _______________________________________________________________ wiczenia _____________________________________________________________

540 541 541 541

INNE _____________________________________________ 543


Indeks ______________________________________________________ 545 Licencja GNU Wolnej Dokumentacji ________________________________ 551
0. Preambua ______________________________________________________________ 1. Zastosowanie i definicje ____________________________________________________ 2. Kopiowanie dosowne ______________________________________________________ 3. Kopiowanie ilociowe ______________________________________________________ 4. Modyfikacje _____________________________________________________________ 5. czenie dokumentw _____________________________________________________ 6. Zbiory dokumentw _______________________________________________________ 7. Zestawienia z pracami niezalenymi __________________________________________ 8. Tumaczenia _____________________________________________________________ 9. Wyganicie _____________________________________________________________ 10. Przysze wersje Licencji ___________________________________________________ Zacznik: Jak zastosowa t Licencj do swojego dokumentu? _____________________ 551 551 552 553 553 555 555 555 556 556 556 556

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.

Screen 1. Gwnym zadaniem Winampa jest odtwarzanie plikw muzycznych

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 rozmawiamy z komputerem?


Wiemy ju, e programy dziaaj dziki temu, e programici konstruuj dla nich odpowiednie algorytmy. Poznalimy nawet prosty algorytm, ktry by moe jest stosowany jest przez program Notatnik do otwierania plikw tekstowych. Zauwa jednak, e jest on napisany w jzyku naturalnym to znaczy takim, jakim posuguj si ludzie. Chocia jest doskonale zrozumiay dla wszystkich, to ma jedn niezaprzeczaln wad: nie rozumie go komputer! Dla bezmylnej maszyny jest on po prostu zbyt niejednoznaczny i niejasny. Z drugiej strony, ju istniejce programy s przecie doskonale zrozumiae dla komputera i dziaaj bez adnych problemw. Jak to moliwe? Ot pecet te posuguje si pewnego rodzaju jzykiem. Chcc zobaczy prbk jego talentu lingwistycznego, wystarczy podejrze zawarto dowolnego pliku EXE. Co zobaczymy? Cig bezsensownych, chyba nawet losowych liter, liczb i innych znakw. On ma jednak sens, tyle e nam bardzo trudno pozna go w tej postaci. Po prostu jzyk komputera jest dla rwnowagi zupenie niezrozumiay dla nas, ludzi :)

Screen 2. Tak wyglda plik EXE :-)

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

Niektrzy powiedz, e o niezawodno, ale to ju kwestia osobistych priorytetw :)

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 :)

Screen 3. Kod przykadowego projektu w Visual Basicu

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.

Screen 4.Tworzenie aplikacji w Delphi

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

Screen 5. Krzywka w formie apletu Javy

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 :)

Brzemienna w skutkach decyzja


Musimy zatem zdecydowa, ktrego jzyka bdziemy si uczy, aby zrealizowa nasz nadrzdny cel, czyli poznanie tajnikw programowania gier. Sprecyzujmy wic wymagania wobec owego jzyka: programy w nim napisane musz by szybkie w takim wypadku moemy wzi pod uwag jedynie jzyki cakowicie kompilowane do kodu maszynowego musi dobrze wsppracowa z rnorodnymi bibliotekami graficznymi, na przykad DirectX powinien posiada due moliwoci i zapewnia gotowe, czsto uywane rozwizania nie zaszkodzi te, gdy bdzie w miar prosty i przejrzysty :) Jeeli uwzgldnimy wszystkie te warunki, to spord caej mnogoci jzykw programowania (w tym kilku przedstawionych wczeniej) zostaj nam a dwa Delphi oraz C++. Przygldajc si bliej Delphi, moemy zauway, i jest on przeznaczony przede wszystkim do programowania aplikacji uytkowych, ktre pozostaj przecie poza krgiem naszego obecnego zainteresowania :) Na plus mona jednak zaliczy prostot i przejrzysto jzyka oraz jego bardzo du wydajno. Rwnie moliwoci Delphi s cakiem spore. Z kolei C++ zdaje si by bardziej uniwersalny. Dobrze rozumie si z wanymi dla nas bibliotekami graficznymi, jest take bardzo szybki i posiada due moliwoci. Skadnia z kolei jest raczej ekonomiczna i by moe nieco bardziej skomplikowana.

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)

Najbardziej znanym jest JEDI

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 :)

C++, pierwsze starcie


Zanim w ogle zaczniemy programowa, musisz zaopatrzy si w odpowiedni kompilator C++ - wspominaem o tym pod koniec poprzedniego rozdziau, zalecajc jednoczenie uywanie Visual C++. Dlatego te opisy polece IDE czy screeny bd dotyczyy wanie tego narzdzia. Nie uniemoliwia to oczywicie uywania innego rodowiska, lecz w takim wypadku bdziesz w wikszym stopniu zdany na siebie. Ale przecie lubisz wyzwania, prawda? ;)

Bliskie spotkanie z kompilatorem


Pierwsze wyzwanie, jakim jest instalacja rodowiska, powiniene mie ju za sob, wic pozwol sobie optymistycznie zaoy, i faktycznie tak jest :) Swoj drog, instalowanie programw jest czci niezbdnego zestawu umiejtnoci, ktre trzeba posi, by mieni si (rednio)zaawansowanym uytkownikiem komputera. Za kandydat na przyszego programist powinien niewtpliwie posiada pewien (w domyle raczej wikszy ni mniejszy) poziom obeznania w kwestii obsugi peceta. W ostatecznoci mona jednak sign do odpowiednich pomocy naukowych :D rodowisko programistyczne bdzie twoim podstawowym narzdziem pracy, wic musisz je dobrze pozna. Nie jest ono zbyt skomplikowane w obsudze z pewnoci nie zawiera takiego natoku nikomu niepotrzebnych funkcji jak chociaby popularne pakiety biurowe :) Niemniej, z pewnoci przyda ci kilka sw wprowadzenia. W uyciu jest par wersji Visual C++. W tym tutorialu bd opiera si na pakiecie Visual Studio 7 .NET (Visual C++ jest jego czci), ktry rni si nieco od wczeniejszej, do niedawna bardzo popularnej, wersji 6. Dlatego te w miar moliwoci bd stara si wskazywa na najwaniejsze rnice midzy tymi dwoma edycjami IDE.

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:

Screen 7. Okno pocztkowe rodowiska Visual Studio .NET

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

W Visual C++ 6 byy to obszary robocze (ang. workspaces)

Z czego skada si program?

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.

Screen 8. Opcje nowego projektu

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.

Screen 9. Ustawienia aplikacji

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 :)

Z czego skada si program?

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 :)

Pisanie tekstu w konsoli


Mielimy okazj si przekona, e nasz program pokazuje nam komunikat podczas dziaania. Nietrudno dociec, i odpowiada za to ta linijka: std::cout << "Hurra! Napisalem pierwszy program w C++!" << std::endl; std::cout oznacza tak zwany strumie wyjcia. Jego zadaniem jest wywietlanie na ekranie konsoli wszystkiego, co do wylemy a wysya moemy oczywicie tekst. Korzystanie z tego strumienia umoliwia zatem pokazywanie nam w oknie konsoli wszelkiego rodzaju komunikatw i innych informacji. Bdziemy go uywa bardzo czsto, dlatego musisz koniecznie zaznajomi si ze sposobem wysyania do tekstu. A jest to wbrew pozorom bardzo proste, nawet intuicyjne. Spjrz tylko na omawian linijk nasz komunikat jest otoczony czym w rodzaju strzaek wskazujcych std::cout, czyli wanie strumie wyjcia. Wpisujc je (za pomoc znaku mniejszoci), robimy dokadnie to, o co nam chodzi: wysyamy nasz tekst do strumienia wyjcia. Drugim elementem, ktry tam trafia, jest std::endl. Oznacza on ni mniej, ni wicej, jak tylko koniec linijki i przejcie do nastpnej. W przypadku, gdy wywietlamy tylko jedn lini tekstu nie ma takiej potrzeby, ale przy wikszej ich liczbie jest to niezbdne. Wystpujcy przez nazwami cout i endl przedrostek std:: oznacza tzw. przestrze nazw. Taka przestrze to nic innego, jak zbir symboli, ktremu nadajemy nazw.
8

Podobny optymizm jest zazwyczaj grub przesad i moemy sobie na niego pozwoli tylko w najprostszych programach, takich jak niniejszy :)

Z czego skada si program?

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.

Doczanie plikw nagwkowych


Pozostay nam jeszcze dwie pierwsze linijki programu: #include <iostream> #include <conio.h> ktre wcale nie s tak straszne, jak wygldaj na pierwszy rzut oka :) Przede wszystkim zauwamy, e zaczynaj si one od znaku #, czym niewtpliwie rni si od innych instrukcji jzyka C++. S to bowiem specjalne polecenia wykonywane jeszcze przed kompilacj - tak zwane dyrektywy. Przekazuj one rne informacje i komendy, pozwalaj wic sterowa przebiegiem kompilacji programu. Jedn z tych dyrektyw jest wanie #include. Jak sama nazwa wskazuje, suy ona do doczania przy jej pomocy wczamy do naszego programu pliki nagwkowe Ale czym one s i dlaczego ich potrzebujemy? By si o tym przekona, zapomnijmy na chwil o programowaniu i wczujmy si w rol zwykego uytkownika komputera. Gdy instaluje on now gr, zazwyczaj musi rwnie zainstalowa DirectX, jeeli jeszcze go nie ma. To cakowicie naturalne, gdy wikszo gier korzysta z tej biblioteki, wic wymaga jej do dziaania. Rwnie oczywisty jest take fakt, e do uywania edytora tekstu czy przegldarki WWW w pakiet nie jest potrzebny te programy po prostu z niego nie korzystaj. Nasz program nie korzysta ani z DirectX, ani nawet ze standardowych funkcji Windows (bo nie jest aplikacj okienkow). Wykorzystuje natomiast konsol i dlatego potrzebuje odpowiednich mechanizmw do jej obsugi - zapewniaj je wanie pliki nagwkowe. Pliki nagwkowe umoliwiaj korzystanie z pewnych funkcji, technik, bibliotek itp. wszystkim programom, ktre doczaj je do swojego kodu rdowego. W naszym przypadku dyrektywa #include ma za zadanie wczenie do kodu plikw iostream i conio.h. Pierwszy z nich pozwala nam pisa w oknie konsoli za pomoc std::cout, drugi za wywoa funkcj getch(), ktra czeka na dowolny klawisz.

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

Oczywicie moe ona przy tym wykonywa pewne dodatkowe operacje

Z czego skada si program?

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:

Schemat 2. Sposb dziaania przykadowego programu

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

Inne nazwy to take przechodzenie krok po kroku czy ledzenie

Z czego skada si program?

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

Zmienne i ich typy


Na pocztek zauwamy, e program pobiera od nas pewne dane i wykonuje na nich operacje. S to dziaania do trywialne (jak wywietlenie rzeczonych danych w niezmienionej postaci), jednak wymagaj przechowania przez jaki czas uzyskanej porcji informacji. W jzykach programowania su do tego zmienne. Zmienna (ang. variable) to miejsce w pamici operacyjnej, przechowujce pojedyncz warto okrelonego typu. Kada zmienna ma nazw, dziki ktrej mona si do niej odwoywa. Przed pierwszym uyciem zmienn naley zadeklarowa, czyli po prostu poinformowa kompilator, e pod tak a tak nazw kryje si zmienna danego typu. Moe to wyglda choby tak: std::string strImie; W ten sposb zadeklarowalimy w naszym programie zmienn typu std::string o nazwie strImie. W deklaracji piszemy wic najpierw typ zmiennej, a potem jej nazw. Nazwa zmiennej moe zawiera liczby, litery oraz znak podkrelenia w dowolnej kolejnoci. Nie mona jedynie zaczyna jej od liczby. W nazwie zmiennej nie jest take dozwolony znak spacji.

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)

Tabela 1. Podstawowe typy zmiennych w C++

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.

Z czego skada si program?


Jednoczenie znaczy to, i strumie wejcia zawsze pokazuje kursor tam, gdzie skoczylimy pisanie warto o tym pamita.

41

***
Strumienie wejcia i wyjcia stanowi razem nierozczn par mechanizmw, ktre umoliwiaj nam pen swobod komunikacji z uytkownikiem w aplikacjach konsolowych.

Schemat 3. Komunikacja midzy programem konsolowym i uytkownikiem

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;

// :-) // w kocu to te staa! // np. w grze RPG

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.

Z czego skada si program?


Wany jest za to nastpny wiersz: int nWynik = nLiczba1 + nLiczba2;

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.

Rodzaje operatorw arytmetycznych


Znak +, ktrego uylimy w napisanym przed chwil programie, jest jednym z kilkunastu operatorw jzyka C++. Operator to jeden lub kilka znakw (zazwyczaj niebdcych literami), ktre maj specjalne znaczenie w jzyku programowania. Operatory dzielimy na kilka grup; jedn z nich s wanie operatory arytmetyczne, ktre su do wykonywania prostych dziaa na liczbach. Odpowiadaj one podstawowym operacjom matematycznym, dlatego ich poznanie nie powinno nastrcza ci problemw. Przedstawia je ta oto tabelka: operator + * / % opis dodawanie odejmowanie mnoenie dzielenie reszta z dzielenia

Tabela 2. Operatory arytmetyczne w C++

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) *, /, % +, -

Tabela 3. Priorytety operatorw arytmetycznych w C++

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;

Z czego skada si program?


i = i + 1; j = j 1;

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.

Funkcje nieco bliej


Z funkcjami mielimy do czynienia ju wczeniej. Powiedzielimy sobie wtedy, e s to wydzielone fragmenty kodu realizujce jak czynno. Przytoczyem przy tym do banalny przykad funkcji PokazTekst(), wywietlajcej w konsoli ustalony napis. Niewtpliwie by on klarowny i ukazywa wspomniane tam przeznaczenie funkcji (czyli podzia kodu na fragmenty), jednake pomija dwa bardzo wane aspekty z nimi zwizane. Chodzi mianowicie o parametry oraz zwracanie wartoci. Rozszerzaj one uyteczno i moliwoci funkcji tak znacznie, e bez nich w zasadzie trudno wyobrazi sobie skuteczne i efektywne programowanie. W takiej sytuacji nie moemy wszak przej obok nich obojtnie - niezwocznie przystpimy zatem do poznawania tych arcywanych zagadnie :)

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.

Warto zwracana przez funkcj


Spora cz funkcji pisanych przez programistw ma za zadanie obliczenie jakiego wyniku (czsto na podstawie przekazanych im parametrw). Inne z kolei wykonuj operacje, ktre nie zawsze musz si uda (choby usunicie pliku dany plik moe przecie ju nie istnie). W takich przypadkach istnieje wic potrzeba, by funkcja zwrcia jak warto. Niekiedy bdzie to rezultat jej intensywnej pracy, a innym razem jedynie informacja, czy zlecona funkcji czynno zostaa wykonana pomylnie. Zgodnie ze zwyczajem, popatrzymy teraz na odpowiedni program przykadowy13 :) Pyta on uytkownika o dugoci bokw prostokta, wywietlajc w zamian jego pole powierzchni oraz obwd. Czyni to ten kod (jest to fragment funkcji main()): // ReturnValue - funkcje zwracajce warto int nDlugosc1; std::cout << "Podaj dlugosc pierwszego boku: "; std::cin >> nDlugosc1; int nDlugosc2; std::cout << "Podaj dlugosc drugiego boku: "; std::cin >> nDlugosc2; std::cout << "Obwod prostokata: " << Obwod(nDlugosc1, nDlugosc2) << std::endl; std::cout << "Pole prostokata: " << Pole(nDlugosc1, nDlugosc2) << std::endl; getch(); Linijki wywietlajce gotowy wynik zawieraj wywoania dwch funkcji Obwod() i Pole(). Mona je umieci w tym miejscu, gdy zwracaj one wartoci w dodatku te, ktre chcemy zaprezentowa :) Wywietlamy je dokadnie w ten sam sposb jak wartoci zmiennych i wszystkich innych wyrae liczbowych. Caa istota dziaania aplikacji zawiera si wic w tych dwch funkcjach. Prezentuj si one nastpujco: int Obwod(int nBok1, int nBok2) { return 2 * (nBok1 + nBok2); } int Pole(int nBok1, int nBok2) { return nBok1 * nBok2; } C ciekawego moemy o nich rzec? Widoczn rnic w stosunku do dotychczasowych przykadw funkcji, jakie mielimy okazj napotka, jest chociaby zastpienie swka
13

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:

Screen 10. Miernik prostoktw w akcji

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

Zasada ta dotyczy prawie kadego bloku kodu w C++ (z wyjtkiem funkcji)

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.

Bardziej zoony przykad


By cakowicie upewni si, i znamy i rozumiemy t szalenie wan konstrukcj programistyczn, przeanalizujemy ostatni, bardziej skomplikowan ilustracj jej uycia. Bdzie to aplikacja rozwizujca rwnania liniowe tzn. wyraenia postaci:

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.

Screen 13. Program rozwizujcy rwnania liniowe

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! :))

Instrukcja wyboru switch


Instrukcja switch (przecz) jest w pewien sposb podobna do if: jej przeznaczeniem jest take wybr jednego z wariantw kodu podczas dziaania programu. Pomidzy obiema konstrukcjami istniej jednak do znaczne rnice. O ile if podejmuje decyzj na podstawie prawdziwoci lub faszywoci jakiego warunku, o tyle switch bierze pod uwag warto podanego wyraenia. Z tego te powodu moe dokonywa wyboru spord wikszej liczby moliwoci ni li tylko dwch (prawdy lub faszu). Najlepiej wida to na przykadzie: // Switch instrukcja wyboru void main() { // (pomijam tu kod odpowiedzialny za pobranie od uytkownika dwch // liczb robilimy to tyle razy, e nie powiniene mie z tym // kopotw :)) Liczby s zapisane w zmiennych fLiczba1 i fLiczba2) int nOpcja; std::cout << "Wybierz dzialanie:" << std::endl; std::cout << "1. Dodawanie" << std::endl; std::cout << "2. Odejmowanie" << std::endl; std::cout << "3. Mnozenie" << std::endl; std::cout << "4. Dzielenie" << std::endl; std::cout << "0. Wyjscie" << std::endl; std::cout << "Twoj wybor: "; std::cin >> nOpcja; switch (nOpcja) { case 1: std::cout << fLiczba1 << " + " << fLiczba2 << " = << fLiczba1 + fLiczba2; break; case 2: std::cout << fLiczba1 << " - " << fLiczba2 << " = << fLiczba1 - fLiczba2; break; case 3: std::cout << fLiczba1 << " * " << fLiczba2 << " = << fLiczba1 * fLiczba2; break; case 4: if (fLiczba2 == 0.0) std::cout << "Dzielnik nie moze byc zerem!"; else std::cout << fLiczba1 << " / " << fLiczba2 << << fLiczba1 / fLiczba2; break; case 0: std::cout << "Dziekujemy :)"; break; default: std::cout << "Nieznana opcja!"; } } getch();

" " "

" = "

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?

Screen 14. Kalkulator w dziaaniu

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 :)

Ptle warunkowe do i while


Na pocztek poznamy dwie konstrukcje, ktre zwane s ptlami warunkowymi. Miano to okrela cakiem dobrze ich zastosowanie: cige wykonywanie kodu, dopki speniony jest okrelony warunek. Ptla sprawdza go przy kadym swoim cyklu - jeeli stwierdzi jego faszywo, natychmiast koczy dziaanie.

Ptla do
Prosty przykad obrazujcy ten mechanizm prezentuje si nastpujco: // Do pierwsza ptla warunkowa

Dziaanie programu

59

#include <iostream> #include <conio.h> void main() { int nLiczba; do {

std::cout << "Wprowadz liczbe wieksza od 10: "; std::cin >> nLiczba; } while (nLiczba <= 10);

std::cout << "Dziekuje za wspolprace :)"; getch();

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

Screen 15. Nieugity program przeciwko krnbrnemu uytkownikowi :)

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:

Schemat 4. Dziaanie przykadowej ptli do

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;

std::cout << "Celny strzal :) Brawo!" << std::endl; getch();

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.

Screen 16. Wystarczyo tylko 8 prb :)

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.

Ptla krokowa for


Do tej pory spotykalimy si z sytuacjami, w ktrych naleao wykonywa okrelony kod a do spenienia pewnego warunku. Rwnie czsto jednak znamy wymagan ilo obrotw ptli jeszcze przed jej rozpoczciem chcemy j poda w kodzie explicite lub obliczy wczeniej jako warto zmiennej. Co wtedy zrobi? Moemy oczywicie uy odpowiednio spreparowanej ptli while, chociaby w takiej postaci: int nLicznik = 1;

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:

Schemat 5. "Droga" przykadowej ptli for

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

Instrukcje break i continue


Z ptlami zwizane s jeszcze dwie instrukcje pomocnicze. Nierzadko uatwiaj one rozwizywanie pewnych problemw, a czasem wrcz s do tego niezbdne. Mowa tu o tytuowych break i continue. Z instrukcj break (przerwij) spotkalimy si ju przy okazji konstrukcji switch. Korzystalimy z niej, aby zagwarantowa wykonanie kodu odpowiadajcego tylko jednemu wariantowi case. break powodowaa bowiem przerwanie bloku switch i przejcie do nastpnej linijki po nim.

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.

Wnikliwy rzut oka na zmienne


Zmienna to co w rodzaju pojemnika na informacje, mogcego zawiera okrelone dane. Wczeniej dowiedzielimy si, i dla kadej zmiennej musimy okreli typ danych, ktre bdziemy w niej przechowywa, oraz nazw, przez ktr bdziemy j identyfikowa. Okrelenie takie nazywamy deklaracj zmiennej i stosowalimy je niemal w kadym programie przykadowym powinno wic by ci doskonale znane :) Nasze aktualne wiadomoci o zmiennych s mimo tego do skpe i dlatego musimy je niezwocznie poszerzy. Uczynimy to wszake w niniejszym podrozdziale.

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.

Screen 18. Zakres moduowy 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

A take w wikszoci wspczesnych jzykw programowania

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.

Screen 19. Zliczanie wywoa funkcji przy pomocy zmiennej statycznej

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

Dokadniej mwic: od momentu deklaracji do zakoczenia programu

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

Modyfikatory typw liczbowych


Dotychczas w swoich programach mielimy okazj uywa gwnie typu int, reprezentujcego liczb cakowit. Czasem korzystalimy take z float, bdcego typem liczb rzeczywistych. Dwa sposoby przechowywania wartoci liczbowych to, zdawaoby si, bardzo niewiele. Zwaywszy, i spora cz jzykw programowania udostpnia nawet po kilkanacie takich typw, asortyment C++ moe wyglda tutaj wyjtkowo mizernie. Domylasz si zapewne, e jest to tylko zudne wraenie :) Do kadego typu liczbowego w C++ moemy bowiem doczy jeden lub kilka modyfikatorw, ktre istotnie zmieniaj jego wasnoci. Sprbujmy dokadnie przyjrze si temu mechanizmowi.

Typy ze znakiem i bez znaku


Typ liczbowy int moe nam przechowywa zarwno liczby dodatnie, jak i ujemne. Dosy czsto jednak nie potrzebujemy wartoci mniejszych od zera. Przykadowo, ilo punktw w wikszoci gier nigdy nie bdzie ujemna; to samo dotyczy licznikw upywajcego czasu, zmiennych przechowujcych wielko plikw, dugoci odcinkw, rozmiary obrazkw - i tak dalej. Moemy rzecz jasna zwyczajnie zignorowa obecno liczb ujemnych i korzysta jedynie z wartoci dodatnich. Wad tego rozwizania jest marnotrawstwo: tracimy wtedy poow miejsca zajmowanego w pamici przez zmienn. Jeeli na przykad int mgby zawiera liczby od -10 000 do +10 000 (czyli 20 000 moliwych wartoci24), to ograniczylibymy ten przedzia do 0+10 000 (a wic skromnych 10 000 moliwych wartoci). Nie jest to moe karygodna niegospodarno w przypadku jednej zmiennej, ale gdy mwimy o kilku czy kilkunastu tysicach podobnych zmiennych25, ilo zmarnowanej pamici staje si znaczna. Naleaoby zatem powiedzie kompilatorowi, e nie potrzebujemy liczb ujemnych i w zamian za nie chcemy zwikszenia przedziau liczb dodatnich. Czynimy to poprzez dodanie do typu zmiennej int modyfikatora unsigned (nieoznakowany, czyli bez znaku; zawsze dodatni). Deklaracja bdzie wtedy wyglda na przykad tak: unsigned int uZmienna; // przechowuje liczby naturalne

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

Rozmiar typu cakowitego


W poprzednim paragrafie wspominalimy o przedziale dopuszczalnych wartoci zmiennej, ale nie przygldalimy si bliej temu zagadnieniu. Teraz zatrzymamy si na nim troch duej i zajmiemy rozmiarem zmiennych cakowitych. Wiadomo nam doskonale, e pami komputera jest ograniczona, zatem miejsce zajmowane w tej pamici przez kad zmienn jest rwnie limitowane. W przypadku typw liczbowych przejawia si to ograniczonym przedziaem wartoci, ktre mog przyjmowa zmienne nalece do takich typw. Jak duy jest to przedzia? Nie ma uniwersalnej odpowiedzi na to pytanie. Okazuje si bowiem, e rozmiar typu int jest zaleny od kompilatora. Wpyw na t wielko ma porednio system operacyjny oraz procesor komputera. Nasz kompilator (Visual C++ .NET), podobnie jak wszystkie tego typu narzdzia pracujce w systemie Windows 95 i wersjach pniejszych, jest 32-bitowy. Oznacza to midzy innymi, e typ int ma u nas wielko rwn 32 bitom wanie, a wic w przeliczeniu26 4 bajtom. Cztery bajty to cztery znaki (na przykad cyfry) czyby zatem najwikszymi i najmniejszymi moliwymi do zapisania wartociami byy +9999 i -9999? Oczywicie, e nie! Komputer przechowuje liczby w znacznie efektywniejszej postaci dwjkowej. Wykorzystanie kadego bitu sprawia, e granice przedziau wartoci typu int to a 231 nieco ponad dwa miliardy! Wicej informacji na temat sposobu przechowywania danych w pamici operacyjnej moesz znale w Dodatku B, Reprezentacja danych w pamici. Przedzia ten sprawdza si dobrze w wielu zastosowaniach. Czasem jednak jest on zbyt may (tak, to moliwe :D) lub zwyczajnie zbyt duy. Daje si to odczu na przykad przy odczytywaniu plikw, w ktrych kada warto zajmuje obszar o cile okrelonym rozmiarze, nie zawsze rwnym int-owym 4 bajtom (tzw. plikw binarnych).

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

Tabela 4. Typy cakowite w 32-bitowym Visual C++ .NET27

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.

Precyzja typu rzeczywistego


Podobnie jak w przypadku typu cakowitego int, typ rzeczywisty float posiada okrelon rozpito wartoci, ktre mona zapisa w zmiennych o tym typie. Poniewa jednak jego przeznaczeniem jest przechowywanie wartoci uamkowych, pojawia si kwestia precyzji zapisu takich liczb. Szczegowe wyjanienie sposobu, w jaki zmienne rzeczywiste przechowuj wartoci, jest do skomplikowane i dlatego je sobie darujemy28 :) Najwaniejsze s dla nas wynikajce z niego konsekwencje. Ot: Precyzja zapisu liczby w zmiennej typu rzeczywistego maleje wraz ze wzrostem wartoci tej liczby Przykadowo, dua liczba w rodzaju 10000000.0023 zostanie najpewniej zapisana bez czci uamkowej. Natomiast maa warto, jak 1.43525667 bdzie przechowana z du

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

Tabela 5. Typy zmiennoprzecinkowe w C++

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.

Screen 20. sizeof w akcji

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.

Screen 21. Fragment tabeli 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

// 8 (bo 8*8 == 64) // okoo 1.414213562373

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.

Funkcje wykadnicze i logarytmiczne


Najczciej stosowan w matematyce funkcj wykadnicz jest e , niekiedy oznaczana take jako exp(x) . Tak te form ma ona w C++: double exp(double x); Zwraca ona warto staej e37 podniesionej do potgi x. Popatrzmy na kilka przykadw: fX = exp(0); fX = exp(1); fX = exp(2.302585093); // 1 // e // 10.000000
x

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:

Rysunek 1. Definicje funkcji trygonometrycznych dowolnego kta

38

Znanej jako zmiana podstawy logarytmu.

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.

Screen 22. Przykadowy rezultat rzutu kostk

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.

Zaokrglanie liczb rzeczywistych


Gdy poznawalimy rzutowanie typw, podaem jako przykad konwersj wartoci float na int. Wspomniaem te, e zastosowane w tym przypadku zaokrglenie liczby rzeczywistej polega na zwyczajnym odrzuceniu jej czci uamkowej.

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 :)

Znane i nieznane operatory


Dobrze wiemy, e funkcje to nie jedyne rodki suce do manipulacji wartociami liczbowymi. Od pocztku uywalimy do tego przede wszystkim operatorw, ktre odpowiaday doskonale nam znanym podstawowym dziaaniom matematycznym. Nadarza si dobra okazja, aby przypomnie sobie o tych elementach jzyka C++, przy okazji poszerzajc swoje informacje o nich.

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.

Sekrety inkrementacji i dekrementacji


Operatorw ++ i -- uywamy, aby doda do zmiennej lub odj od niej jedynk. Taki zapis jest najkrtszy i najwygodniejszy, a poza tym najszybszy. Uywamy go szczeglnie czsto w ptlach for. Jednak moe by on take czci zoonych wyrae. Ponisze fragmenty kodu s absolutnie poprawne i w dodatku nierzadko spotykane: int nA = 6; int nB = ++nA; int nC = 4; int nD = nC++; Od tej pory bd mwi jedynie o operatorze inkrementacji, jednak wszystkie przedstawione tu wasnoci dotycz take jego dekrementujcego brata. Nasuwa si naturalne pytanie: jakie wartoci bd miay zmienne nA, nB, nC i nD po wykonaniu tych czterech linijek kodu? Jeeli chodzi o nA i nC, to sprawa jest oczywista. Kada z tych zmiennych zostaa jednokrotnie poddana inkrementacji, zatem ich wartoci s o jeden wiksze ni na pocztku. Wynosz odpowiednio 7 i 5. Pozostae zmienne s ju twardszym orzechem do zgryzienia. Skupmy si wic chwilowo na nB. Jej warto na pewno ma co wsplnego z wartoci nA - moe to by albo 6 (liczba przed inkrementacj), albo 7 (ju po inkrementacji). Analogicznie, nD moe by rwna 4 (czyli wartoci nC przed inkrementacj) lub 5 (po inkrementacji). Jak jest w istocie? Sam si przekonaj! Stwrz nowy program, wpisz do jego funkcji main() powysze wiersze kodu i dodaj instrukcje pokazujce wartoci zmiennych C widzimy? Zmienna nB jest rwna 7, a wic zostaa jej przypisana warto nA ju po inkrementacji. Natomiast nD rwna si 4 - tyle, co nC przed inkrementacj. Przyczyn tego faktu jest rzecz jasna rozmieszczenie plusw. Gdy napisalimy je przed inkrementowan zmienn, dostalimy w wyniku warto zwikszon o 1. Kiedy za umiecilimy je za t zmienn, otrzymalimy jeszcze stary rezultat. Jak zatem moglimy si przekona, odpowiednie zapisanie operatorw ++ i -- ma cakiem spore znaczenie.

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.

Napisy wedug C++


Trudno w to uwierzy, ale poprzednik C++ - jzyk C - w ogle nie posiada odrbnego typu zmiennych, mogcego przechowywa napisy. Aby mc operowa danymi tekstowymi, trzeba byo uywa mao porcznych tablic znakw (typu char) i samemu dba o zagadnienia zwizane z przydzielaniem i zwalnianiem pamici. Nam, programistom C++, nic takiego na szczcie nie grozi :) Nasz ulubiony jzyk jest bowiem wyposaony w kilka bardzo przydatnych i atwych w obsudze mechanizmw, ktre udostpniaj moliwo manipulacji tekstem. Rozwizania, o ktrych bdzie mowa poniej, s czci Biblioteki Standardowej jzyka C++. Jako e jest ona dostpna w kadym kompilatorze tego jzyka, sposoby te s najbardziej uniwersalne i przenone, a jednoczenie wydajne. Korzystanie z nich jest take bardzo wygodne i atwe. Oprcz nich istniej rwnie inne metody obsugi acuchw znakw. Na przykad biblioteki MFC i VCL (wspomagajce programowanie w Windows) posiadaj wasne narzdzia, suce temu wanie celowi45. Nawet jeeli skorzystasz kiedy z tych bibliotek, bdziesz mg wci uywa opisanych tutaj mechanizmw standardowych. Aby mc z nich skorzysta, naley przede wszystkim wczy do swojego kodu plik nagwkowy string: #include <string> Po tym zabiegu zyskujemy dostp do caego arsenau rodkw programistycznych, sucych operacjom tekstowym.

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

Typy zmiennych tekstowych


Istniej dwa typy zmiennych tekstowych, ktre rni si rozmiarem pojedynczego znaku. Ujmuje je ponisza tabelka: nazwa std::string std::wstring typ znaku char wchar_t rozmiar znaku 1 bajt 2 bajty zastosowanie tylko znaki ANSI znaki ANSI i Unicode

Tabela 6. Typy acuchw znakw

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.

Manipulowanie acuchami znakw


OK, gdy ju znamy dwa typy zmiennych tekstowych, jakie oferuje C++, czas zobaczy moliwe dziaania, ktre moemy na nich przeprowadza.

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:

Schemat 7. Pobieranie wycinka tekstu ze zmiennej typu std::string

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

Pobieranie pojedynczych znakw


Ostatni przydatn operacj na napisach, jak teraz poznamy, jest uzyskiwanie pojedynczego znaku o ustalonym indeksie. By moe nie zdajesz sobie z tego sprawy, ale ju potrafisz to zrobi. Zamierzony efekt mona bowiem osign, wykorzystujc jeden ze sposobw na inicjalizacj acucha: std::string strNapis = "przykladowy tekst"; std::string strZnak(strNapis, 9, 1); // jednoznakowy fragment od ind. 9 Tak oto uzyskamy dziesity znak (przypominam, indeksy liczymy od zera!) z naszego przykadowego tekstu - czyli 'w'. Przyznasz jednak, e taka metoda jest co najmniej kopotliwa i byoby ciko uywa jej na co dzie. Dobry C++ ma wic w zanadrzu inn konstrukcj, ktr zobaczymy w niniejszym przykadowym programie: // CharCounter - zliczanie znakw #include <string> #include <iostream> #include <conio.h> unsigned ZliczZnaki(std::string strTekst, char chZnak) { unsigned uIlosc = 0; for (unsigned i = 0; i <= strTekst.length() - 1; ++i) { if (strTekst[i] == chZnak) ++uIlosc; } } return uIlosc;

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.

Screen 23. Zliczanie znakw w akcji

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.

Porwnywanie wartoci zmiennych


Wszystkie warunki w jzyku C++ opieraj si na jawnym lub ukrytym porwnywaniu dwch wartoci. Najczciej jest ono realizowane poprzez jeden ze specjalnych operatorw porwnania, zwanych czasem relacyjnymi. Wbrew pozorom nie s one dla nas niczym nowym, poniewa uywalimy ich w zasadzie w kadym programie, w ktrym musielimy sprawdza warto jakiej zmiennej. W poniszej tabelce znajdziesz wic jedynie starych znajomych :) operator == != > >= < <= porwnanie jest prawdziwe, gdy lewy argument jest rwny prawemu lewy argument nie jest rwny prawemu (jest od niego rny) lewy argument ma wiksz warto ni prawy lewy argument ma warto wiksz lub rwn wartoci prawego lewy argument ma mniejsz warto ni prawy lewy argument ma warto mniejsz lub rwn wartoci prawego
Tabela 7. Operatory porwnania w C++

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.

Zestawienie operatorw logicznych


Zasady funkcjonowania operatorw logicznych ujmuje si czsto w tabelki, przedstawiajce ich wartoci dla wszystkich moliwych argumentw. Niekiedy nazywa si je tablicami prawd (ang. truth tables). Nie powinno wic zabrakn ich tutaj, zatem czym prdzej je przedstawiam: a prawda prawda fasz fasz b prawda fasz prawda fasz a && b prawda fasz fasz fasz a || b prawda prawda prawda fasz

a prawda fasz

!a fasz prawda

Tabele 8 i 9. Rezultaty dziaania operatorw koniunkcji, alternatywy oraz negacji

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.

Screen 24. Okrelanie, czy wpisana liczba jest pierwsza

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

Tutaj ograniczymy si tylko do liczb cakowitych i typu int.

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

Przykad wykorzystania tablicy


Wiemy ju, jak teoretycznie wyglda praca z tablicami w jzyku C++, zatem naturaln kolej rzeczy bdzie teraz uwane przygldnicie si odpowiedniemu przykadowi. Ten (spory :)) kawaek kodu wyglda nastpujco: // Lotto - uycie prostej tablicy liczb const unsigned ILOSC_LICZB = 6; const int MAKSYMALNA_LICZBA = 49; void main() { // deklaracja i wyzerowanie tablicy liczb unsigned aLiczby[ILOSC_LICZB]; for (int i = 0; i < ILOSC_LICZB; ++i) aLiczby[i] = 0; // losowanie liczb srand (static_cast<int>(time(NULL))); for (int i = 0; i < ILOSC_LICZB; ) { // wylosowanie liczby aLiczby[i] = rand() % MAKSYMALNA_LICZBA + 1; // sprawdzenie, czy si ona nie powtarza bool bPowtarzaSie = false; for (int j = 0; j < i; ++j) { if (aLiczby[j] == aLiczby[i]) { bPowtarzaSie = true; break; } } // jeeli si nie powtarza, przechodzimy do nastpnej liczby if (!bPowtarzaSie) ++i;

// 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:

Screen 25. Wysyanie kuponw jest od dzisiaj zbdne ;-)

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:

Schemat 8. Wyobraenie tablicy dwuwymiarowej 45

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.

Schemat 9. Inicjalizacja tablicy dwuwymiarowej 45

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 }, }, }, } };

Zostanie on wtedy wywnioskowany z inicjalizatora.

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

Schemat 10. Przedstawienie tablicy dwuwymiarowej jako tablicy tablic

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.

Nowe typy danych


Wachlarz dostpnych w C++ typw wbudowanych jest, jak wiemy, niezwykle bogaty. W poczeniu z moliwoci fuzji wielu pojedynczych zmiennych do postaci wygodnych w uyciu tablic, daje nam to szerokie pole do popisu przy konstruowaniu wasnych sposobw na przechowywanie danych. Nabyte ju dowiadczenie oraz tytu niniejszego podrozdziau sugeruje jednak, i nie jest to wcale kres potencjau uywanego przez nas jzyka. Przeciwnie: C++ oferuje nam moliwo tworzenia swoich wasnych typw zmiennych, odpowiadajcych bardziej konkretnym potrzebom ni zwyke liczby czy napisy. Nie chodzi tu wcale o znan i prost instrukcj typedef, ktra umie jedynie produkowa nowe nazwy dla ju istniejcych typw. Mam bowiem na myli znacznie potniejsze narzdzia, udostpniajce duo wiksze moliwoci w tym zakresie. Czy znaczy to rwnie, e s one trudne do opanowania? Wedug mnie siedzcy tutaj diabe wcale nie jest taki straszny, jakim go maluj ;D Absolutnie wic nie ma si czego ba!

Wyliczania nadszed czas


Pierwszym z owych narzdzi, z ktrymi si zapoznamy, bd typy wyliczeniowe (ang. enumerated types). Ujrzymy ich moliwe zastosowania oraz techniki uytkowania, a rozpoczniemy od przykadu z ycia wzitego :)

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.

Definiowanie typu wyliczeniowego


Nie od rzeczy bdzie teraz przyjrzenie si kawakowi kodu, ktry wprowadza nam nowy typ wyliczeniowy. Oto i jego skadnia: enum nazwa_typu { staa_1 staa_2 staa_3 ... staa_n [ = warto_1 ], [ = warto_2 ], [ = warto_3 ], [ = warto_n ] };

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 ;)

Uycie typu wyliczeniowego


Typy wyliczeniowe zalicza si do typw liczbowych, podobnie jak int czy unsigned. Mimo to nie jest moliwe bezporednie przypisanie do zmiennej takiego typu liczby zapisanej wprost. Kompilator nie przepuci wic instrukcji podobnej do tej: enum DECISION { YES = 1, NO = 0, DONT_KNOW = -1 }; DECISION Decyzja = 0; Zrobi tak nawet pomimo faktu, i 0 odpowiada tutaj jednej ze staych typu DECISION. C++ dba bowiem, aby typw enum uywa zgodnie z ich przeznaczeniem, a nie jako zamiennikw dla zmiennych liczbowych. Powoduje to, e: Do zmiennych wyliczeniowych moemy przypisywa wycznie odpowiadajce im stae. Niemoliwe jest nadanie im zwykych wartoci liczbowych. Jeeli jednak koniecznie potrzebujemy podobnego przypisania (bo np. odczytalimy liczb z pliku lub uzyskalimy j za pomoc jakiej zewntrznej funkcji), moemy salwowa si rzutowaniem przy pomocy static_cast: // zakadamy, e OdczytajWartosc() zwraca liczb typu int lub podobn Decyzja = static_cast<DECISION>(OdczytajWartosc()); Pamitajmy aczkolwiek, eby w zwykych sytuacjach uywa zdefiniowanych staych. Inaczej cakowicie wypaczalibymy ide typw wyliczeniowych.

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 :)

Typy strukturalne i ich definiowanie


Wrmy wic do naszego problemu ksiki adresowej, albo raczej listy kontaktw najlepiej internetowych. Kada jej pozycja mogaby si skada z takich oto trzech elementw: nicka tudzie imienia i nazwiska danej osoby jej adresu e-mail numeru identyfikacyjnego w jakim komunikatorze internetowym Na przechowywanie tyche informacji potrzebujemy zatem dwch acuchw znakw (po jednym na nick i adres) oraz jednej liczby cakowitej. Znamy oczywicie odpowiadajce tym rodzajom danych typy zmiennych w C++: s to rzecz jasna std::string oraz int. Moemy wic uy ich do utworzenia nowego, zoonego typu, reprezentujcego w caoci pojedynczy kontakt: struct CONTACT { std::string strNick; std::string strEmail; int nNumerIM; }; W ten wanie sposb zdefiniowalimy typ strukturalny. Typy strukturalne (zwane te w skrcie strukturami52) to zestawy kilku zmiennych, nalecych do innych typw, z ktrych kada posiada swoj wasn i unikaln nazw. Owe podzmienne nazywamy polami struktury. Nasz nowonarodzony typ strukturalny skada si zatem z trzech pl, za kade z nich przechowuje jedynie elementarn informacj. Zestawione razem reprezentuj jednak zoon dan o jakiej osobie.
Zazwyczaj strukturami nazywamy ju konkretne zmienne; u nas byyby to wic rzeczywiste dane kontaktowe jakiej osoby (czyli zmienne nalece do zdefiniowanego wanie typu CONTACT). Czasem jednak poj typ strukturalny i struktura uywa si zamiennie, a ich szczegowe znaczenie zaley od kontekstu.
52

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 :)

Schemat 11. Obrazowy model tablicy struktur

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 :)

Odrobina formalizmu - nie zaszkodzi!


Przyszed wanie czas na uporzdkowanie i usystematyzowanie posiadanych informacji o strukturach. Najwikszym zainteresowaniem obdarzymy przeto reguy skadniowe jzyka, towarzyszce ich wykorzystaniu. Mimo tak gronego wstpu nie opuszczaj niniejszego paragrafu, bo taka absencja z pewnoci nie wyjdzie ci na dobre :) Typ strukturalny definiujemy, uywajc sowa kluczowego struct (ang. structure struktura). Skadnia takiej definicji wyglda nastpujco: struct 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; }; Kolejne wiersze wewntrz niej udzco przypominaj deklaracje zmiennych i tak te mona je traktowa. Pola struktury s przecie zawartymi w niej podzmiennymi. Cao tej listy pl ujmujemy oczywicie w stosowne do C++ nawiasy klamrowe. Pamitajmy, aby za kocowym nawiasem koniecznie umieci rednik. Pomimo zblionego wygldu definicja typu strukturalnego nie jest przecie funkcj i dlatego nie mona zapomina o tym dodatkowym znaku.

Przykad wykorzystania struktury


To prawda, e uywanie struktur dotyczy najczciej do zoonych zbiorw danych. Tym bardziej wydawaoby si, i trudno o jaki nietrywialny przykad zastosowania tego mechanizmu jzykowego w prostym programie. Jest to jednak tylko cz prawdy. Struktury wystpuj bowiem bardzo czsto zarwno w standardowej bibliotece C++, jak i w innych, czsto uywanych kodach - Windows API

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

-100 fizyczny emocjonalny intelektualny

Wykres 1. Przykadowy biorytm autora tego tekstu :-)

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

Tabela 10. Dugoci cykli biorytmu

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:

Screen 26. Efekt dziaania aplikacji obliczajcej biorytm

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

Jest to liczba sekund, ktre upyny od pnocy 1 stycznia 1970 roku.

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:

Schemat 12. Model przechowywania unii w pamici operacyjnej

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.

Rysunek 2. Rozgrywka w kko i krzyyk

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.

Struktury danych w aplikacji


Pierwszym zadaniem jest okrelenie struktur danych, wykorzystywanych przez program. Oznacza to ustalenie zmiennych, ktre przewidujemy w naszej aplikacji oraz danych, jakie maj one przechowywa. Poniewa wiemy ju niemal wszystko na temat sposobw organizowania informacji w C++, nasze instrumentarium w tym zakresie bdzie bardzo szerokie. Zatem do dziea! Chyba najbardziej oczywist potrzeb jest konieczno stworzenia jakiej programowej reprezentacji planszy, na ktrej toczy si rozgrywka. Patrzc na ni, nietrudno jest znale odpowiedni drog do tego celu: wrcz idealna wydaje si bowiem tablica dwuwymiarowa o rozmiarze 33. Sama wielko to jednak nie wszystko - naley take okreli, jakiego typu elementy ma zawiera ta tablica. Aby to uczyni, pomylmy, co si dzieje z plansz podczas rozgrywki. Na pocztku zawiera ona wycznie puste pola; potem kolejno pojawiaj si w nich kka lub krzyyki Czy ju wiesz, jaki typ bdzie waciwy? Naturalnie, chodzi tu o odpowiedni typ wyliczeniowy, dopuszczajcy jedynie trzy moliwe wartoci: pole puste, kko lub krzyyk. To byo od pocztku oczywiste, prawda? :)

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:

Schemat 13. Przebieg gry w kko i krzyyk

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:

Schemat 14. Dziaanie programu do gry w kko i krzyyk

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:

Screen 27. Interfejs uytkownika naszej gry

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

Kilka moduw i wasne nagwki


Na pocztek utworzymy i dodamy do projektu wszystkie pliki, z jakich docelowo ma si skada. Zgadza si - pliki. Pisany przez nas program moe okaza si cakiem duy, dlatego rozsdnie bdzie podzieli jego kod pomidzy kilka odrbnych moduw. Utrzymamy wtedy jego wzgldny porzdek oraz skrcimy czas kolejnych kompilacji. Zwyczajowo zaczniemy od pliku main.cpp, w ktrym umiecimy gwn funkcj programu, main(). Chwilowo jednak nie wypenimy j adn treci: void main() { } Zamiast tego wprowadzimy do projektu jeszcze jeden modu, w ktrym wpiszemy waciwy kod naszej gry. Przy pomocy opcji menu Project|Add New Item dodaj wic do aplikacji drugi ju plik typu C++ File (.cpp) i nazwij go game.cpp. W tym module znajd si wszystkie zasadnicze funkcje programu. To jednak nie wszystko! Na deser zostawiem bowiem pewn nowo, z ktr nie mielimy okazji si do tej pory zetkn. Stworzymy mianowicie swj wasny plik nagwkowy, idcy w parze ze wieo dodanym moduem game.cpp. Uczynimy to podobny sposb, co dotychczas - z t rnic, i tym razem zmienimy typ dodawanego pliku na Header File (.h).

54

Kompletny kod caej aplikacji jest zawarty w przykadach do tego rozdziau i opatrzony nazw TicTacToe.

Zoone zmienne

143

Screen 28. Dodawanie pliku nagwkowego do projektu

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 :)

Tre pliku nagwkowego


W nagwku game.h umiecimy przerne deklaracje wikszoci tworw programistycznych, wchodzcych w skad naszej aplikacji. Bd to chociaby zmienne oraz funkcje. Rozpoczniemy jednak od wpisania do definicji trzech typw wyliczeniowych, ktre ustalilimy podczas projektowania programu. Chodzi naturalnie o SIGN, FIELD i GAMESTATE: enum SIGN { SGN_CIRCLE, SGN_CROSS }; enum FIELD { FLD_EMPTY, FLD_CIRCLE, FLD_CROSS }; enum GAMESTATE { GS_NOTSTARTED, GS_MOVE, GS_WON, GS_DRAW }; Jest to powszechny zwyczaj w C++. Powysze linijki moglibymy wszake z rwnym powodzeniem umieci wewntrz moduu game.cpp. Wyodrbnienie ich w pliku nagwkowym ma jednak swoje uzasadnienie: wasne typy zmiennych s bowiem takimi zasobami, z ktrych najczciej korzysta wiksza cz danego programu. Jako kod wspdzielony (ang. shared) s wic idealnym kandydatem do umieszczenia w odpowiednim nagwku. W dalszej czci pomylimy ju o konkretnych funkcjach, ktrym powierzymy zadanie kierowania nasz gr. Pamitamy z fazy projektowania, i przewidzielimy przynajmniej dwie takie funkcje: odpowiedzialn za rozpoczcie gry oraz za przebieg rozgrywki, czyli wykonywanie ruchw i sprawdzanie ich skutku. Moemy jeszcze dooy do nich algorytm rysujcy (jeli mona tak powiedzie w odniesieniu do konsoli) aktualny stan planszy.

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.

Waciwy kod gry


Zastanowienie moe budzi powd, dla ktrego adna z trzech powyszych funkcji nie zostaa zadeklarowana jako void. Przecie zgodnie z tym, co ustalilimy podczas projektowania wszystkie maj przede wszystkim wykonywa jakie dziaania, a nie oblicza wartoci. To rzeczywicie prawda. Rezultat zwracany przez te funkcje ma jednak inn rol - bdzie informowa o powodzeniu lub niepowodzeniu danej operacji. Typ bool zapewnia tutaj

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

S one konieczne do prawidowego funkcjonowania kodu, ktry napiszemy za chwil.

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:

Schemat 15. Numerowanie pl planszy do gry w kko i krzyyk

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

if (Pole != ZgodnePole) { ZgodnePole = Pole; uLiczbaZgodnychPol = 1; } else ++uLiczbaZgodnychPol;

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:

Screen 29. Ekran gry w kko i krzyyk

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

Funkcja main(), czyli skadamy program


By moe trudno w to uwierzy, ale mamy za sob zaprogramowanie wszystkich funkcji sterujcych przebiegiem gry! Zanim jednak bdziemy mogli cieszy si dziaajcym programem musimy wypeni kodem gwn funkcj aplikacji, od ktrej zacznie si jej wykonywanie - main(). W tym celu zostawmy ju wymczony modu game.cpp i wrmy do main.cpp, w ktrym czeka nietknity szkielet funkcji main(). Poprzedzimy go najpierw dyrektywami doczenia niezbdnych nagwkw - take naszego wasnego, game.h: #include <iostream> #include <conio.h> #include "game.h" Wasne pliki nagwkowe najlepiej umieszcza na kocu szeregu instrukcji #include, doczajc je po tych pochodzcych od kompilatora. Teraz ju moemy zaj si treci najwaniejszej funkcji w naszym programie. Zaczniemy od nastpujcego wywoania: StartGry(); Spowoduje ono rozpoczcie rozgrywki - jak pamitamy, oznacza to midzy innymi wylosowanie gracza, ktremu przypadnie pierwszy ruch, oraz ustawienie stanu gry na GS_MOVE. Od tego momentu zaczyna si wic zabawa, a nam przypada obowizek jej prawidowego poprowadzenia. Wywiemy si z niego w nieznany dotd sposb - uyjemy ptli nieskoczonej: for (;;) { // ... } Konstrukcja ta wcale nie jest taka dziwna, a w grach spotyka si j bardzo czsto. Istota ptli nieskoczonej jest czciowo zawarta w jej nazwie, a po czci mona j wydedukowa ze skadni. Mianowicie, nie posiada ona adnego warunku zakoczenia58, wic w zasadzie wykonywaaby si do koca wiata i o jeden dzie duej ;) Aby tego unikn, naley gdzie wewntrz jej bloku umieci instrukcj break;, ktra spowoduje przerwanie tego zakltego krgu. Uczynimy to, kodujc kolejne instrukcje w teje ptli. Najpierw funkcja RysujPlansze() wywietli nam aktualny stan rozgrywki: RysujPlansze(); Pokae wic tytu gry, plansz oraz dolny komunikat - komunikat, ktry przez wikszo czasu bdzie prob o kolejny ruch. By sprawdzi, czy tak jest w istocie, porwnamy zmienn opisujc stan gry z wartoci GS_MOVE:
58

Zwanego te czasem warunkiem terminalnym.

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

Mwic cile: gdzie poza biecym zakresem.

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:

Screen 30. Gra w Kko i krzyyk w akcji

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 :)

Przedstawiamy klasy i obiekty


Poznamy teraz sposb kodowania znany jako programowanie obiektowe (ang. objectoriented programming - OOP), ktre spenia wszystkie niedawno zoone przeze mnie obietnice. Nie dziwi wic, i jest to najszerzej stosowana przez dzisiejszych programistw technika projektowania i implementacji programw. Nie zawsze jednak tak byo; myl zatem, e warto przyjrze si drodze, jak pokona fach o nazwie programowanie komputerw - od pocztku a do chwili obecnej. Dziki temu bdziemy mogli lepiej doceni uywane przez siebie narzdzia, z C++ na czele :)

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.

Mao zachcajce pocztki


Nie dziwi wic, i pojawienie si mzgw elektronowych, jak wtedy nazywano komputery, na wielu uniwersytetach w latach 50. wywoao spory entuzjazm. Mnstwo
60

Najbardziej znanym jest maszyna Turinga.

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

Byy to na przykad LISP albo Smalltalk.

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.

Wszystko jest obiektem


Ale czym w istocie jest taki obiekt? W jzyku potocznym sowo to moe przecie oznacza w zasadzie wszystko. Obiektem mona nazwa lamp stojc na biurku, drzewo za oknem, ssiedni dom, samochd na ulicy, a nawet cae miasto. Jakkolwiek czasem bdzie to do dziwny sposb nazewnictwa, ale jednak naley go uzna za cakowicie dopuszczalny.

Rysunek 3, 4 i 5. Obiekty otaczaj nas z kadej strony

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.

Schemat 16. Przykadowy obiekt samochodu

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.

Obiekt obiektowi nierwny


Zestaw pl i metod rzadko jest charakterystyczny dla pojedynczego obiektu. Najczciej istnieje wiele obiektw, kady z waciwymi sobie wartociami pl. czy je jednak przynaleno do jednego i tego samego rodzaju, ktry nazywamy klas. Klasy wprowadzaj wic pewn systematyk w wiat obiektw. Byty nalece do tej samej klasy s bowiem do siebie podobne: maj ten sam pakiet pl oraz mog wykonywa na sobie te same metody. Informacje te zawarte s w definicji klasy i wsplne dla wszystkich wywodzcych si z niej obiektw. Klasa jest zatem czym w rodzaju wzorca - matrycy, wedle ktrego produkowane s kolejne obiekty (instancje) w programie. Mog one rni si od siebie, ale tylko co do wartoci poszczeglnych pl; wszystkie bd jednak nalee do tej samej klasy i bd mogy wykonywa na sobie te same metody. Kot o czarnej sierci i kot o biaej sierci to przecie jeden i ten sam gatunek Felis catus

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

W C++ rnica midzy klas a struktur jest zreszt czysto kosmetyczna.

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!

Obiekty i klasy w C++


Szczycc si chlubnym mianem jzyka w peni obiektowego, C++ posiada wszystko, co niezbdne do praktycznej realizacji idei programowanie zorientowanego obiektowo. Teraz wanie przyjrzymy si dokadnie tym konstrukcjom jzykowym - wytumaczymy sobie ich dziaanie oraz sposb uycia.

Klasa jako typ obiektowy


Wiemy ju, e pisanie programu zgodnie z filozofi OOP polega na definiowaniu i implementowaniu odpowiednich klas oraz tworzeniu z nich obiektw i manipulowaniu nimi. Klasa jest wic dla nas pojciem kluczowym, ktre na pocztek wypadaoby wyjani: Klasa to zoony typ zmiennych, skadajcy si z pl, przechowujcych dane, oraz posiadajcy metody, wykonujce zaprogramowane czynnoci. Zmienne nalece do owych typw obiektowych nazywamy oczywicie obiektami.

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.

Dwa etapy okrelania klasy


Skoro dowiedzielimy si dokadnie, czym s klasy i jak (w teorii) dziaaj, spjrzmy na sposoby ich wykorzystania w jzyku C++. Zaczniemy rzecz jasna od wprowadzania do programu wasnych typw obiektowych, gdy bez tego ani rusz :) Na pocztek warto przypomnie, i klasa jest typem (podobnie jak struktura czy enum), wic waciwym dla niej miejscem byby zawsze plik nagwkowy. Jednoczenie jednak zawiera ona kod swoich funkcji skadowych, czyli metod, co czyni j przynalen do jakiego moduu (bo tylko wewntrz moduw mona umieszcza funkcje). Te dwa przeciwstawne stanowiska sprawiaj, e okrelenie klasy jest najczciej rozdzielone na dwie czci: definicj, wstawian w pliku nagwkowym, w ktrej okrelamy pola klasy oraz wpisujemy prototypy jej metod implementacj, umieszczan w module, bdc po prostu kodem wczeniej zdefiniowanych metod Ukad ten nie do, e dziaa nadzwyczaj dobrze, to jeszcze realizuje jeden z postulatw programowania obiektowego, jakim jest ukrywanie niepotrzebnych szczegw. Tymi szczegami bdzie tutaj kod poszczeglnych metod, ktrego znajomo nie jest wcale potrzebna do korzystania z klasy. Co wicej, moe on nie by w ogle dostpny w postaci pliku .cpp, a jedynie w wersji skompilowanej! Tak jest chociaby w przypadku biblioteki DirectX, o czym przekonasz si za czas jaki. Domylasz si zatem, e za chwil skoncentrujemy si na tych dwch etapach okrelania klasy, a wic na definicji i implementacji. Jakkolwiek nie brzmi to zbyt odkrywczo, jednak masz tutaj cakowit suszno :D Czasem, jeszcze przed definicj klasy musimy poinformowa kompilator, e dana nazwa jest faktycznie klas. Robimy tak na przykad wtedy, gdy obiekt klasy A odwouje si do klasy B, za B do A. Uywamy wtedy deklaracji zapowiadajcej, piszc po prostu class A; lub class B;. Takie przypadki s dosy rzadkie, ale warto wiedzie, jak sobie z nimi radzi. O tym sposobie wspomnimy zreszt nieco dokadniej, gdy bdziemy zajmowa si klasami zaprzyjanionymi.

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

[specyfikator_dostpu:] [pola] [metody]

}; 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 :)

Kontrola dostpu do skadowych klasy


Fraza oznaczona jako specyfikator_dostpu pewnie nie mwi ci zbyt wiele, chocia spotkalimy si ju z ni w ktrej z przykadowych klas. Przyjmowaa ona tam form private lub public, dzielc caa definicj na jakby dwie odrbne sekcje. Nietrudno wywnioskowa, i podzia ten nie ma jedynie charakteru wizualnego, ale powoduje dalej idce konsekwencje. Jakie? Nazwa specyfikator_dostpu, chocia brzmi moe nieco sztucznie (jak zreszt wiele terminw w programowaniu :)), dobrze oddaje rol, jak ta konstrukcja peni. Ot specyfikuje ona wanie prawa dostpu do czci skadowych klasy (czyli pl lub metod), wyrniajc ich dwa rodzaje: prywatne (ang. private) oraz publiczne (ang. public). Rnica midzy nimi jest znaczca i bardzo wana, gdy wpywa na to, ktre elementy klasy s widoczne tylko w ramach jej samej, a ktre take na zewntrz. Te pierwsze nazywamy wic prywatnymi, za drugie publicznymi. Prywatne skadowe klasy (wpisane po sowie private: w jej definicji) s dostpne jedynie wewntrz samej klasy, tj. tylko dla jej wasnych metod. Publiczne skadowe klasy (wpisane po sowie public: w jej definicji) widoczne s zawsze i wszdzie - nie tylko dla samej klasy (jej metod), ale na zewntrz - np. dla jej obiektw. Danym specyfikatorem objte s wszystkie nastpujce po nim czci klasy, a do jej koca lub kolejnego specyfikatora :) Ich ilo nie jest bowiem niczym ograniczona. Nic wic nie stoi na przeszkodzie, aby nie byo ich wcale! W takiej sytuacji wszystkie skadowe bd miay domylne reguy dostpu. W przypadku klas (definiowanych poprzez class) jest to dostp prywatny, natomiast dla typw strukturalnych64 (swko struct) - dostp publiczny. Trudno uwierzy, ale w C++ jest to jedyna rnica pomidzy klasami a strukturami! Sowa class i struct s wic niemal synonimami; jest to rzecz niespotykana w innych jzykach programowania, w ktrych te dwie konstrukcje s zupenie odrbne. Dla skuteczniejszego rozwiania z powyszego opisu moliwej mgy niejasnoci, spjrzmy na ten oto przykadowy program i klas: // DegreesCalc - kalkulator temperatur // typ wyliczeniowy okrelajcy skal temperatur
64

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:

Screen 31. Kalkulator przeliczajcy wartoci 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.

Metody i ich prototypy


Metody czyni klasy. To dziki swym funkcjom skadowym pasywne zbiory danych, ktrymi s struktury, staj si aktywnymi obiektami. Z praktycznego punktu widzenia metody niewiele rni si od zwyczajnych funkcji oczywicie poza faktem, i s deklarowane zawsze wewntrz jakiej klasy: class CFoo { public: void Metoda(); int InnaMetoda(int); // itp. }; Deklaracje te mog mie form prototypw funkcji, a stworzone w ten sposb metody wymaga jeszcze implementacji, czyli wpisania ich kodu. Czynnoci t zajmiemy si dokadnie w nastpnym paragrafie. Warto jednak wiedzie, e dopuszczalne jest take wprowadzanie kodu metod bezporednio wewntrz bloku class. Robilimy tak zreszt w przypadku metod dostpowych do pl, a w podobnych sytuacjach rozwizanie to sprawdza si bardzo dobrze. Nie naley aczkolwiek postpowa w ten sposb z dugimi metodami, zawierajcymi skomplikowane algorytmy, gdy moe to spowodowa znaczcy wzrost rozmiaru wynikowego pliku EXE. Kompilator traktuje bowiem takie funkcje jako inline, tzn. rozwijane w miejscu wywoania, i wstawia cay ich kod przy kadym odwoaniu si do nich. Dla krtkich, jednolinijkowych metod jest to dobre rozwizanie, przyspieszajce dziaanie programu. Dla duszych nie musi wcale takie by. Dokadniejszych informacji na ten temat oraz o samych funkcjach inline tradycyjnie mona znale w MSDN. To jeszcze nie koniec zabawy z metodami :) Niektre z nich mona mianowicie uczyni staymi. Zabieg ten sprawia, e funkcja, na ktrej go zaaplikujemy, nie moe modyfikowa adnego z pl klasy68, a tylko je co najwyej odczytywa. Po co komu takie udziwnienie? Teoretycznie jest to pewna wskazwka dla kompilatora, ktry by moe uczyni nam w zamian ask poczynienia jakich optymalizacji. Praktycznie jest to te pewien sposb na zabezpieczenie si przed omykowym zmodyfikowaniem obiektu w metodzie, ktra wcale nie miaa czego takiego robi. Jednym sowem korzyci s piorunujce ;) Uczynienie jakiej metody sta jest banalnie proste: wystarczy tylko doda za list jej parametrw magiczne swko const, np.: class CFoo { private:
68

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) };

{ m_fX = m_fY = 0.0; } { m_fX = fX; m_fY = 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.).

Deklarowanie zmiennych i tworzenie obiektw


Zaczynamy oczywicie od deklaracji zmiennej, niebdcej dla nas adn niespodziank: CFoo Obiekt; Powysza linijka kodu wykonuje jednak znacznie wicej czynnoci, ni jest to widoczne na pierwszy czy nawet drugi rzut oka. Ona mianowicie: wprowadza nam now zmienn Obiekt typu CFoo. Nie jest to rzecz jasna adna nowo, ale dla porzdku warto o tym przypomnie. tworzy w pamici operacyjnej obszar, w ktrym bd przechowywane pola obiektu. To take nie jest zaskoczeniem: pola, jako bd co bd zmienne, musz rezydowa gdzie w pamici, wic robi to w identyczny sposb jak pola struktur. wywouje konstruktor klasy CFoo (czyli procedur CFoo::CFoo()), by dokoczy aktu kreacji obiektu. Po jego zakoczeniu moemy uzna nasz obiekt za ostatecznie stworzony i gotowy do uycia. Te trzy etapy s niezbdne, abymy mogli bez problemu korzysta z stworzonego obiektu. W tym przypadku s one jednak realizowane cakowicie automatycznie i nie wymagaj od nas adnej uwagi. Przekonamy si pniej, e nie zawsze tak jest i, co ciekawe, wcale nie bdziemy tym zmartwieni :D Musz jeszcze wspomnie o pewnym drobnym wymaganiu, stawianym nam przez kompilator, ktremu chcemy poda wiersz kodu umieszczony na pocztku paragrafu. Ot klasa CFoo musi tutaj posiada bezparametrowy konstruktor, albo te nie mie wcale procedury tego rodzaju (wtedy etap z jej wywoywaniem zostanie po prostu pominity). W innym przypadku potrzebne jest jeszcze przekazanie odpowiednich parametrw konstruktorowi, ktry takowych wymaga. Konieczno t realizujemy podobn metod, co wywoanie zwyczajnej funkcji: CFoo Foo(10, "jaki tekst"); // itp.

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

Deklarowanie wskanikw i tworzenie obiektw


Od czeg to mielibymy zacz, jeeli nie od jakiej zmiennej? W kocu bez zmiennych nie ma obiektw, a bez obiektw nie ma programowania (obiektowego :D). Zadeklarujmy wic na pocztek tak oto dziwn zmienn: CFoo* pFoo; Wszystko byoby tu znajome, gdyby nie ta gwiazdka przy nazwie klasy CFoo. To wanie ona sprawia, e pFoo nie jest zmienn obiektow, ale wanie wskanikiem na obiekt, w tym przypadku obiekt klasy CFoo. To wane stwierdzenie - pFoo nie jest tutaj obiektem, on moe co najwyej na taki obiekt wskazywa. Innymi sowy, moe by jedynie odwoaniem do obiektu, poczeniem z nim - ale zmienna ta nie bdzie nigdy sama przechowywa adnych danych, nalecych do owego obiektu. Bdzie raczej czym w rodzaju pozycji w spisie treci, odnoszcej si do rozdziau w ksice. Niniejsza linijka kodu nie tworzy wic adnego obiektu, a jedynie przygotowuje na miejsce w programie. Waciwa kreacja musi nastpi pniej i wyglda nieco inaczej ni to, do czego przywyklimy: pFoo = new CFoo; Swko new (nowy, niektrzy ka je zwa operatorem) suy wanie do utworzenia obiektu. Wykonuje ono prawie wszystkie czynnoci potrzebne do realizacji tego procesu, a wic przydziela odpowiedni ilo pamici dla naszego obiektu i wywouje konstruktor jego klasy. Czym zatem zasuguje sobie na odrbno? Podstawow rnic jest to, e tworzony obiekt jest umieszczany w dowolnym miejscu pamici, a nie w ktrej z naszych zmiennych (a ju na pewno nie w pFoo!). Nie oznacza to jednake, i nie mamy o nim adnych informacji i nie moemy z niego normalnie korzysta. Ot pFoo staje si tutaj cznikiem z naszym odlegym tworem; za porednictwem tego wskanika mamy bowiem pen swobod dostpu do stworzonego obiektu. Jak si wkrtce przekonasz, moliwe jest przy jego pomocy odwoywanie si do skadnikw obiektu (pl i metod) w niemal taki sam sposb, jak w przypadku zmiennych obiektowych.

Schemat 18. Wskanik na obiekt jest pewnego rodzaju kluczem do niego

Jeden dla wszystkich, wszystkie do jednego


Ogromne i wane rnice ujawniaj si dopiero podczas manipulowania kilkoma takimi wskanikami. Mam tu na myli przede wszystkim instrukcje przypisania, rozwaane ju dokadnie dla zmiennych obiektowych. Teraz podobne eksperymenta bdziemy dokonywali na wskanikach; zobaczymy, dokd nas one zaprowadz Do naszych celw po raz kolejny spoytkujemy zdefiniowan w poprzednim paragrafie klas CLamp. Zaczniemy jednak od zadeklarowania wskanika na obiekt tej klasy z jednoczesnym stworzeniem obiektu lampy:

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.

Schemat 19. Moemy mie wiele wskanikw do tego samego obiektu

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

Stosowanie wskanikw na obiekty


Wczytujc si w powyszy opis i spogldajc na krytycznym okiem mona uzna, e stosowanie wskanikw na obiekty jest tylko niepotrzebnym zawracaniam sobie gowy i utrudnianiem ycia. Nie do, e trzeba samemu dba o tworzenie i niszczenie obiektw, to jeszcze nasz program moe si niechybnie wysypa, jeli sprbujemy odwoa si do nieistniejcego obiektu. I gdzie s te obiecane korzyci? Taka ocena jest naturalnie mocno niesprawiedliwa, a moim zadaniem jest przekonanie ci, i wskaniki s nie tylko przydatne w programowaniu obiektowym, ale wydaj si wrcz niezbdne. Przypomnijmy sobie najpierw, c ciekawego powiedzielimy o obiektach na samych pocztku rozdziau. Mianowicie wyjanilimy sobie, e s to drobne cegieki, z ktrych programista buduje swoj aplikacj. To cakiem dobre porwnanie, gdy kryje w sobie jeszcze jeden ukryty sens: niewiele mona zrobi z zestawem cegie, jeeli nie bdziemy dysponowali jakim spoiwem, czcym je w calo. Rol cznikw speniaj wanie wskaniki. Kady obiekt, aby by uytecznym, powinien by jako poczony z innym obiektem. To w zasadzie dosy oczywista prawda, jednak na pocztku mona sobie nie cakiem zdawa z niej spraw. Takie relacje najprociej realizowa za pomoc wskanikw. Sposb, w jaki cz one obiekty, jest bardzo prosty: ot jeden z nich powinien posiada pole, bdce wskanikiem na drugi obiekt. w drugi koniec cza moe, jak wiemy, istnie w dowolnym miejscu pamici, co wicej - moliwe jest, by dochodzi do niego wicej ni jeden wskanik! W ten sposb obiekty mog bra udzia w dowolnej liczbie wzajemnych relacji.

Schemat 20. Dziaanie aplikacji opiera si na zalenociach midzy obiektami

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

Schemat 21. Fragment przykadowego diagramu powiza obiektw w grze

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.

O powstawaniu klas drog doboru naturalnego


Czowiek jest tak dziwn istot, ktra bardzo lubi posiada uporzdkowany i usystematyzowany obraz wiata. Wprowadzanie porzdku i pewnej hierarchii co do postrzeganych zjawisk i przedmiotw jest dla nas niemal naturaln potrzeb. Chyba najlepiej przejawia si to w klasyfikacji biologicznej. Widzc na przykad psa wiemy przecie, e nie tylko naley on do gatunku zwanego psem domowym, lecz take do gromady znanej jako ssaki (wraz z komi, soniami, lwami, mapami, ludmi i ca reszt tej menaerii). Te z kolei, razem z gadami, ptakami czy rybami nale do kolejnej, znacznie wikszej grupy organizmw zwanych po prostu zwierztami. Nasz pies jest zatem jednoczenie psem domowym, ssakiem i zwierzciem:

Schemat 22. Klasyfikacja zwierzt jako przykad hierarchii typw obiektw

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.

Od prostoty do komplikacji, czyli ewolucja


Powrmy wic do naszego przykadu ze zwierztami. Chcc stworzy programowy odpowiednik zaproponowanej hierarchii, musielibymy zdefiniowa najpierw odpowiednie klasy bazowe. Nastpnie odziedziczylibymy ich pola i metody w klasach pochodnych i dodali nowe, waciwe tylko im. Powstae klasy same mogyby by potem bazami dla kolejnych, jeszcze bardziej wyspecjalizowanych typw. Idc dalej t drog dotarlibymy wreszcie do takich klas, z ktrych sensowne byoby ju tworzenie normalnych obiektw. Pojcie klas bazowych i klas pochodnych jest zatem wzgldne: dana klasa moe wprawdzie pochodzi od innych, ale jednoczenie by baz dla kolejnych klas. W ten sposb ustala si wielopoziomowa hierarchia, podobna zwykle do drzewka. Ilustracj tego procesu moe by poniszy diagram:

Schemat 23. Hierarchia klas zwierzt

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.

Z klasy bazowej do pochodnej, czyli dziedzictwo przodkw


O podstawowej konsekwencji takiego rozwizania zdyem ju wczeniej wspomnie. Jest ni mianowicie przekazywanie pl oraz metod pochodzcych z klasy bazowej do wszystkich klas pochodnych, ktre si z niej wywodz. Zatem: Klasa pochodna zawiera pola i metody odziedziczone po klasach bazowych. Moe take posiada dodatkowe, unikalne dla siebie skadowe - nie jest to jednak obowizkiem. Przeledmy teraz sposb, w jaki odbywa si odziedziczanie skadowych na przykadzie naszej prostej hierarchii klas zwierzt. U jej podstawy ley najbardziej bazowa klasa Zwierz. Zawiera ona dwa pola, okrelajce mas i wiek zwierzcia, oraz metody odpowiadajce za takie czynnoci jak widzenie i oddychanie. Skadowe te mogy zosta umieszczone tutaj, gdy dotycz one wszystkich interesujcych nas zwierzt i bd miay sens w kadej z klas pochodnych. Tymi klasami, bezporednio dziedziczcymi od klasy Zwierz, s Ryba, Ssak oraz Ptak. Kada z nich niejako z miejsca otrzymuje zestaw pl i metod, ktrymi legitymowao si bazowe Zwierz. Klasy te wprowadzaj jednak take dodatkowe, wasne metody: i tak Ryba moe pywa, Ssak biega75, za Ptak lata. Nie ma w tym nic dziwnego, nieprawda? :) Wreszcie, z klasy Ssak dziedziczy najbardziej interesujca nas klasa, czyli Pies domowy. Przejmuje ona wszystkie pola i metody z klasy Ssak, a wic porednio take z klasy Zwierz. Uzupenia je przy tym o kolejne skadowe, waciwe tylko sobie. Ostatecznie wic klasa Pies domowy zawiera znacznie wicej pl i metod ni mogoby si z pocztku wydawa:

Schemat 24. Skadowe klasy Pies domowy

75

Delfiny musz mi wybaczy nieuwzgldnienie ich w tym przykadzie :D

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.

Obiekt o kilku klasach, czyli zmienno gatunkowa


Oczywicie klas nie definiuje si dla samej przyjemnoci ich definiowania, lecz dla tworzenia z nich obiektw. Jeeli wic posiadalibymy przedstawion wyej hierarchi w jakim prawdziwym programie, to z pewnoci pojawiyby si w nim take instancje zaprezentowanych klas, czyli odpowiednie obiekty. W ten sposb wracamy do problemu postawionego na samym pocztku: jak obiekt moe nalee do kilku klas naraz? Rnica polega wszak na tym, e mamy ju jego gotowe rozwizanie :) Ot nasz obiekt psa naleaby przede wszystkim do klasy Pies domowy; to wanie tej nazwy uylibymy, by zadeklarowa reprezentujc go zmienn czy te pokazujcy na wskanik. Jednoczenie jednak byby on typu Ssak oraz typu Zwierz, i mgby wystpowa w tych miejscach programu, w ktrych byby wymagany jeden z owych typw. Fakt ten jest przyczyn istnienia w programowaniu obiektowym zjawiska zwanego polimorfizmem. Poznamy je dokadnie jeszcze w tym rozdziale.

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.

Definicja klasy bazowej i specyfikator protected


Jak pamitamy, definicja klasy skada si przede wszystkim z listy deklaracji jej pl oraz metod, podzielonych na kilka czci wedle specyfikatorw praw dostpu. Najczciej kady z tych specyfikatorw wystpuje co najwyej w jednym egzemplarzu, przez co skadnia definicji klasy wyglda nastpujco: class nazwa_klasy { [private:] [deklaracje_prywatne] [protected:] [deklaracje_chronione] [public:]
76

Dorwnuj mu chyba tylko rozwizania znane z Javy.

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.

Definicja klasy pochodnej


Dopiero posiadajc zdefiniowan klas bazow moemy przystpi do okrelania dziedziczcej z niej klasy pochodnej. Jest to konieczne, bo w przeciwnym wypadku kazalibymy kompilatorowi korzysta z czego, o czym nie miaby wystarczajcych informacji. Skadni definicji klasy pochodnej moemy pogldowo przedstawi w ten sposb: class nazwa_klasy [: [specyfikatory] [nazwa_klasy_bazowej] [, ...]] { deklaracje_skadowych }; Poniewa z sekwencj deklaracji_skadowych spotkalimy si ju nie raz i nie dwa razy, skupimy si jedynie na pierwszej linijce podanego schematu.

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

// elipsa, klasa bazowa

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.

Co nie jest dziedziczone?


Wydawaoby si, e klasa pochodna powinna przejmowa wszystkie skadowe pochodzce z klasy bazowej - oczywicie z wyjtkiem tych oznaczonych jako private. Tak jednak nie jest, gdy w trzech przypadkach nie miaoby to sensu. Owe trzy nieprzechodnie skadniki klas to: konstruktory. Zadaniem konstruktora jest zazwyczaj inicjalizacja pl klasy na ich pocztkowe wartoci, stworzenie wewntrznych obiektw czy te alokacja dodatkowej pamici. Czynnoci te prawie zawsze wymagaj zatem dostpu do prywatnych pl klasy. Jeeli wic konstruktor z klasy bazowej zostaby wrzucony do klasy pochodnej, to utraciby z nimi niezbdne poczenie - wszak zostayby one w klasie bazowej! Z tego te powodu konstruktory nie s dziedziczone.

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

Metody wirtualne i polimorfizm


Dziedziczenie jest oczywicie niezwykle wanym, a wrcz niezbdnym skadnikiem programowania obiektowego. Stanowi jednak tylko podstaw dla dwch kolejnych technik, majcych duo wiksze znaczenie i pozwalajcych na o wiele efektywniejsze pisanie kodu. Mam tu na myli tytuowe metody wirtualne oraz czciowo bazujcy na nich polimorfizm. Wszystkie te dziwne terminy zostan wkrtce wyjanione, zatem nie wpadajmy zbyt pochopnie w panik ;)

Wirtualne funkcje skadowe


Idea dziedziczenia w znanej nam dotd postaci jest nastawiona przede wszystkim na uzupenianie definicji klas bazowych o kolejne skadowe w klasach pochodnych. Tylko czasami zastpowalimy ju istniejce metody ich nowymi wersjami, waciwymi dla tworzonych klas. Takie sytuacje s jednak w praktyce dosy czste - albo raczej korzystne jest prowokowanie takich sytuacji, gdy niejednokrotnie daj one wietne rezultaty i niespotykane wczeniej moliwoci przy niewielkim nakadzie pracy. Oczywicie dzieje si tak tylko wtedy, gdy mamy odpowiednie podejcie do sprawy

To samo, ale inaczej


Raz jeszcze zajmijmy si nasz hierarchi klas zwierzt. Tym razem skierujemy uwag na metod Oddychaj z klasy Zwierz. Jej obecno u szczytu diagramu, w klasie, z ktrej pocztek bior wszystkie inne, jest z pewnoci uzasadniona. Kade zwierz, niezalenie od gatunku, musi przecie pobiera z otoczenia tlen niezbdny do ycia, a proces ten nazywamy potocznie wanie oddychaniem. Jest to bezdyskusyjne.
79

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:

Schemat 27. Przedefiniowanie metody z klasy bazowej w klasach pochodnych

Deklaracja metody wirtualnej


Teoretycznie klasa Zwierz mogaby by cakowicie niewiadoma tego, e jedna z jej metod jest definiowana w inny sposb w klasie pochodnej. Lepiej jednak, abymy przewidzieli tak konieczno i poczynili odpowiedni krok. Jest nim uczynienie funkcji Oddychaj metod wirtualn w klasie Zwierz. Metoda wirtualna jest przygotowana na zastpienie siebie przez now wersj, zdefiniowan w klasie pochodnej. Aby dan funkcj skadow zadeklarowa jako wirtualn, naley poprzedzi jej prototyp sowem kluczowym virtual: #include <iostream> class CAnimal { // (pomijamy pozostae skadowe klasy) public: virtual void Oddychaj() { std::cout << "Oddycham..." << std::endl; }

};

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

Przedefiniowanie metody wirtualnej


Celem wprowadzenia funkcji wirtualnej Oddychaj() do klasy CAnimal byo, jak to zaznaczylimy na pocztku, jej pniejsze przedefiniowanie (ang. override) w klasach pochodnych. Operacji tej dokonujemy prost drog, bowiem zwyczajnie definiujemy now wersj metody w owych klasach: class CFish : public CAnimal { public: void Oddychaj() // redefinicja metody wirtualnej { std::cout << "Oddycham skrzelami..." << std::endl; } void Plyn(); }; class CMammal : public CAnimal { public: void Oddychaj() // jak wyej { std::cout << "Oddycham pucami..." << std::endl; } void Biegnij(); }; class CBird : public CAnimal { public: void Oddychaj() // i znowu jak wyej :) { std::cout << "Oddycham pucami..." << std::endl; } void Lec(); }; Kompilator sam domyla si, e nasza metody jest tak naprawd redefinicj metody wirtualnej z klasy bazowej. Moemy jednak wyranie to zaznaczy poprzez ponowne zastosowanie sowa virtual. Wedug mnie jest to mao szczliwe rozwizanie skadniowe, poniewa moe czsto powodowa pomyki. Nie sposb bowiem odrni deklaracji przedefiniowanej metody wirtualnej od jej pierwotnej wersji (jeeli jeszcze raz uylimy virtual) lub od zwykej funkcji skadowej (gdy nie skorzystalimy ze wspomnianego swka). Bardziej przejrzycie rozwizano to na przykad w Delphi, gdzie now wersj metody wirtualnej trzeba opatrzy fraz override;. Nowa wersja metody cakowicie zastpuje star, ktra jest jednak dostpna i w razie potrzeby moemy j wywoa. Suy do tego konstrukcja: nazwa_klasy_bazowej::nazwa_metody([parametry]); W powyszym przypadku byoby to wywoanie CAnimal::Oddychaj(). W Visual C++ zamiast nazwy_klasy_bazowej moliwe jest uycie specjalnego sowa kluczowego __super, opisanego tutaj.

Pojedynek: metody wirtualne przeciwko zwykym


Czytajc powysze objanienie metod wirtualnych, zadawae sobie zapewne proste pytanie o gbokiej treci, a mianowicie: Po co mi to? ;-) Najlepsz odpowiedzi na nie bdzie wyjanienie rnicy pomidzy zwykymi oraz wirtualnymi metodami.

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.

Zaczynamy od zera dosownie


Deklarujc metody opatrzone modyfikatorem virtual, tworzymy grunt pod ich przysz, ponown implementacj w klasach dziedziczcych. Mona te powiedzie, i w pewnym sensie zmieniamy charakter zawierajcej je klasy: jej rol nie jest ju przede wszystkim tworzenie obiektw, gdy rwnie wane staje si suenie jako baza dla klas pochodnych. Niekiedy suszne jest pjcie jeszcze dalej, to znaczy cakowite pozbawienie klasy moliwoci tworzenia z niej obiektw. Ma to nierzadko rozsdne uzasadnienie i takimi wanie przypadkami zajmiemy si w tym paragrafie.

210

Podstawy programowania

Czysto wirtualne metody


Wirtualna funkcja skadowa umieszczona w klasie bazowej jest przygotowana na to, aby ustpi miejsca swej bardziej wyspecjalizowanej wersji, zdefiniowanej w klasie pochodnej. Nie zmienia to jednak faktu, i musiaaby ona jako implementowa czynno, ktrej przebiegu czsto nie sposb ustali na tym etapie. Posiadamy dobry przykad, ilustrujcy tak wanie sytuacj. Chodzi mianowicie o metod CAnimal::Oddychaj(). Wewntrz klasy bazowej, z ktrej maj dopiero dziedziczy konkretne grupy zwierzt, niemoliwe jest przecie ustalenie uniwersalnego sposobu oddychania. Sensowna implementacja tej metody jest wic w zasadzie niemoliwa. Sprawia to, i jest ona wywienitym kandydatem na czysto wirtualn funkcj skadow. Metody nazywane czysto wirtualnymi (ang. pure virtual) nie posiadaj adnej implementacji i s przeznaczone wycznie do przedefiniowania w klasach pochodnych. Deklaracja takiej metody ma do osobliw posta. Oczywicie z racji nie posiadania adnego kodu zbdne staj si nawiasy klamrowe wyznaczajce jej blok, zatem cao przypomina zwyky prototyp funkcji. Samo oznaczenie, czynice dan metod czysto wirtualn, jest jednak raczej niecodzienne: class CAnimal { // (definicja klasy jest skromna z przyczyn oszczdnociowych :)) public: virtual void Oddychaj() = 0;

};

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]).

Abstrakcyjne klasy bazowe


Nie wida tego na pierwszy, drugi, ani nawet na dziesity rzut oka, ale zadeklarowanie jakiej metody jako czysto wirtualnej powoduje jeszcze jeden, dodatkowy efekt. Ot klasa, w ktrej tak funkcj stworzymy, staje si klas abstrakcyjn.

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

Oglny kod do szczeglnych zastosowa


Zjawisko polimorfizmu pozwala na znaczne uproszczenie wikszoci algorytmw, w ktrych du rol odgrywa zarzdzanie wieloma rnymi obiektami. Nie chodzi tu wcale o jakie skomplikowane operacje sortowania, wyszukiwania, kompresji itp., tylko o czsto spotykane operacje wykonywania tej samej czynnoci dla wielu obiektw rnych rodzajw. Opis ten jest w zaoeniu do oglny, bowiem sposb, w jaki uywa si obiektowych technik polimorfizmu jest cile zwizany z konkretnymi programami. Postaram si jednak przytoczy w miar klarowne przykady takich rozwiza, aby mia chocia oglne pojcie o tej metodzie programowania i mg j stosowa we wasnych aplikacjach.

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:

Screen 32. Informacje o dokumencie uzyskane z uyciem prostego polimorfizmu

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.

Klasy wiedz same, co naley robi


Z poprzednim przykadem zwizany jest pewien mankament, nietrudno zreszt zauwaalny. Niezalenie od tego, jakie dodatkowe dane o dokumencie zadeklarujemy w klasach pochodnych, nasza funkcja wywietli tylko i wycznie te przewidziane w klasie CDocument. Nie uzyskamy wic nic ponad autora, tytu oraz dat stworzenia dokumentu. Trzeba jednak przyzna, e sami niejako jestemy sobie winni. Wyodrbniajc czynno prezentacji obiektu poza sam obiekt postpilimy niezgodnie z ide OOPu, ktra nakazuje czy dane i operujcy na nich kod. Zatem przykad z poprzedniego paragrafu to zdecydowanie zy przykad :D O wiele lepszym rozwizaniem jest dodanie do klasy CDocument odpowiedniej metody, odpowiedzialnej za czynno wypisywania. A ju cakowitym ideaem bdzie uczynienie jej funkcj wirtualn - wtedy klasy dziedziczce od CDocument bd mogy ustali wasny sposb prezentacji swoich danych. Wszystkie te doskonae pomysy praktycznie realizuje poniszy program przykadowy: // Polymorphism - wykorzystanie techniki polimorfizmu // *** documents.h *** class CDocument { // (wikszo skadowych wycito z powodu zbyt duej objtoci) public: virtual void PokazDane();

};

// (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" );

pDokument->PokazDane(); delete pDokument;

// 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();

Wynikiem jego dziaania bdzie ponisze zestawienie:

Screen 33. Aplikacja prezentujca polimorfizm z wykorzystaniem metod wirtualnych

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.

Typy pod kontrol


Uniwersalny kod dla wszystkich klas w hierarchii jest bardzo wygodnym rozwizaniem. Okazjonalnie jednak zdarza si, e trzeba w nim uwzgldni take bardziej szczegowe przypadki, co oznacza koniecznoc sprawdzania faktycznego typu obiektw, na ktre wskazuj nasze wskaniki. Na szczcie C++ oferuje proste mechanizmy, umoliwiajce realizacj tego zadania.

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.

typeid i informacje o typie podczas dziaania programu


Oprcz dynamic_cast - operatora, ktry pozwala sprawdzi, czy dany wskanik do klasy bazowej wskazuje te na obiekt klasy pochodnej - C++ dysponuje nieco bardziej zaawansowanymi konstrukcjami, dostarczajcymi wiadomoci o typach obiektw i wyrae w programie. S to tak zwane informacje o typie czasu wykonania (ang. Run-Time Type Information), oznaczane angielskim skrtowcem RTTI. Znajomo opisywanej tu czci RTTI, czyli operatora typeid, generalnie nie jest tak potrzebna, jak umiejtno posugiwania si operatorami rzutowania, ale daje kilka bardzo ciekawych moliwoci, najczciej nieosigalnych inn drog. Moesz aczkolwiek tymczasowo pomin ten paragraf, by wrci do niego pniej. Skorzystanie z RTTI wymaga podjcia dwch wstpnych krokw: wczenia odpowiedniej opcji kompilatora:

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(); }

break; break; break; break;

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.

Projektowanie zorientowane obiektowo


A miao by tak piknie Programowanie obiektowe miao by przecie wyjtkowo naturalnym sposobem kodowania, a poprzednie paragrafy raczej nie bardzo o tym przekonyway, prawda? Jeeli rzeczywicie odnosisz takie wraenie, to by moe zwyczajnie utone w powodzi szczegw, dodajmy - niezbdnych szczegw, koniecznych do stosowania OOPu w praktyce. Czas jednak wypyn na powierzchni i ponownie spojrze na zagadnienie bardziej caociowo. Temu celowi bdzie suy niniejszy podrozdzia. Wiele podrcznikw opisujcych programowanie obiektowe (czy nawet programowanie jako takie) wspomina skpo, jeeli w ogle, o praktycznym stosowaniu prezentowanych mechanizmw, czyli po prostu o projektowaniu aplikacji z uyciem omawianych technik. Monaby to wybaczy tym publikacjom, ktrych gwnym celem jest jedynie kompletny opis danego jzyka. Jeeli jednak mwimy o materiaach dla cakiem pocztkujcych, bdcych w zaoeniu wprowadzeniem w wiat programowania, wtedy zdecydowanie niewskazane jest pomijanie praktycznych stron projektowania i kodowania aplikacji. Na co bowiem przyda si znajomo budowy motka, jeli nie uatwi to zadania, jakim jest wbicie gwodzia? :) Staram si wic unikn tego bdu i przedstawiam programowanie obiektowe take od strony programisty-praktyka. Mam jednoczenie nadziej, e w ten sposb przynajmniej czciowo uchroni ci przed wywaaniem otwartych drzwi w poszukiwaniu informacji w gruncie rzeczy oczywistych - ktre jednak wcale takie nie s, gdy si ich nie posiada. Naturalnie, nic nie zastpi dowiadczenia zdobytego samodzielnie podczas prawdziwego kodowania. Prezentowana tutaj wiedza teoretyczno-praktyczna moe by jednak bardzo pomocnym punktem startowym, uatwiajcym koderskie ycie przynajmniej na jego pocztku. C wic znajdziemy w aktualnym podrozdziale? auj, ale nie bdzie to przegld kolejnych krokw, jakie naley czyni programujc konkretn aplikacj. Zamiast na mniej lub bardziej trywialnym programiku skoncentrujemy si raczej na oglnym procesie budowania wewntrznej, obiektowej struktury programu - czyli na tak zwanym modelowaniu klas i ich zwizkw. Najpierw poznamy zatem trzy podstawowe rodzaje obiektw albo, jak kto woli, rl, w ktrych one wystpuj. Dalej zajmiemy si kwesti definiowania odpowiednich klas - ich interfejsu i implementacji, a wreszcie zwizkami pomidzy nimi, dziki ktrym programy stworzone wedug zasad OOPu mog poprawnie funkcjonowa. Znajomo powyszego zestawu zagadnie powinna znacznie poprawi twoje szanse w starciu z problemami projektowymi, zwizanymi z programowaniem obiektowym. By moe ich rozwizywanie nie bdzie ju wwczas wiedz tajemn, ale normaln i, co waniejsze, satysfakcjonujc czci pracy kodera. Nie przeduajc ju wicej zacznijmy zatem waciw tre tego podrozdziau.

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

// jedna jedyna instancja powyszej klasy extern CGame* g_pGra;

//

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

Praktyczna implementacja z uyciem skadowych statycznych


Nawet najlepszy pomys nie jest zbyt wiele wart, jeeli nie mona jego skutkw zobaczy w dziaaniu. Singletony mona na szczcie zaimplementowa a na kilka sposobw, rnicych si wygod i bezpieczestwem. Najprostszy, z wykorzystaniem globalnego wskanika na obiekt lub globalnej zmiennej obiektowej, posiada kilka wad, zwizanych przede wszystkim z kontrol nad tworzeniem oraz niszczeniem obiektu. Dlatego lepiej zastosowa tutaj inne rozwizanie, oparte na skadowych statycznych klas. Statyczne skadowe s przypisane do klasy jako caoci, a nie do jej poszczeglnych instancji (obiektw). Deklarujemy je przy pomocy sowa kluczowego static. Wwczas peni wic ono inn funkcj ni ta, ktr znalimy dotychczas.

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.

Definiowanie odpowiednich klas


Tworzenie obiektowego modelu programu przebiega zwykle dwuetapowo. Jednym z zada jest identyfikacja klas, ktre bd si na skaday, oraz pl i metod, ktre zostan zawarte w ich definicjach. Drugim jest okrelenie zwizkw pomidzy tymi klasami, dziki ktrym aplikacja mogaby realizowa zaplanowane czynnoci. Przestrzeganie powyszej kolejnoci nie jest cile konieczne. Oczywicie, majc ju kilka zdefiniowanych klas, mona pewnie prociej poczy je waciwymi relacjami. Rwnie dobre jest jednak wyjcie od tyche relacji i korzystanie z nich przy definiowaniu klas. Obydwa wspomniane procesy czsto wic odbywaj si jednoczenie. Poniewa jednak lepiej jest opisa kady z nich osobno, zatem od ktrego naley zacz :) Zdecydowaem tedy, e najpierw poszukamy waciwych klas oraz ich skadowych, a dopiero potem zajmiemy si czeniem ich w odpowiedni model. Zaprojektowanie kompletnego zbioru klas oznacza konieczno dopracowywania dwch aspektw kadej z nich: abstrakcji, czyli opisu tego, co dana klasa ma robi implementacji, to znaczy okrelenia, jak ma to robi Teraz naturalnie zajmiemy si kolejno obydowami kwestiami.

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.

Skadowe interfejsu klasy


Publiczny interfejs klasy to zbir metod, ktre mog wywoywa jej uytkownicy. Jego okrelenie jest drugim etapem definiowania klasy i wyznacza zadania, jakie naley wykona podczas jej implementacji. Nasza klasa Dokument bdzie naturalnie zawieraa kilka publicznych metod. Co ciekawe, sporo informacji o nich moemy wycign i wydedukowa z ju raz analizowanego opisu caego programu. Na jego podstawie daj si sprecyzowa takie funkcje jak: Otwrz - otwierajc dokument zapisany w pliku Zapisz - zachowujca dokument w postaci pliku Eksportuj - metoda eksportujca dokument do pliku graficznego z moliwoci wyboru docelowego formatu Z pewnocia w toku dalszego projektowania aplikacji (by moe w trakcie definicji kolejnych klas albo ich zwizkw?) mona by znale take inne metody, ktrych umieszczenie w klasie bdzie susznym posuniciem. W kadej sytuacji musimy jednak pamita, aby posta klasy zgadzaa si z jej abstrakcj. Mwi o tym, gdy nie powiniene zapomina, e projektowanie jest procesem cyklicznym, w ktrym moe wystpowa wiele iteracji oraz kilka podej do tego samego problemu.

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.

Schemat 28. Proces tworzenia klasy

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.

Zwizki midzy klasami


Potg programowania obiektowego nie s autonomiczne obiekty, ale wsppracujce ze sob klasy. Kada musi wic wchodzi z innymi przynajmniej w jedn relacj, czyli zwizek. Obecnie zapoznamy si z trzema rodzajami takich zwizkw. Spajaj one obiekty poszczeglnych klas i umoliwiaj realizacj zaoonych funkcji programu.

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.

Schemat 29. Ilustracja zwizku generalizacji-specjalizacji

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.

Schemat 30. Ilustracja zwizku agregacji

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.

Odwieczny problem: by czy mie?


Rozrnienie pomidzy dziedziczeniem a zawieraniem moe czasami nastrcza pewnych trudnoci. W takich sytuacjach istnieje na szczcie jedno proste rozwizanie. Ot jeeli relacj pomidzy dwoma obiektami lepiej opisuje okrelenie ma (zawiera, skada si itp.), to naley zastosowa agregacj. Kiedy natomiast klasy s naturalnie poczone poprzez stwierdzenie jest, wtedy odpowiedniejszym rozwizaniem jest dziedziczenie. Co to znaczy? Dokadnie to, co widzisz i o czym mylisz. Naley po prostu sprawdzi, ktre ze sformuowa:
Klasa1 jest rodzajem Klasa2. Klasa1 zawiera obiekt typu Klasa2.

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 :)

Tam i (by moe) z powrotem


Gdy do obiektu jakiej klasy dodamy pole - wskanik na obiekt innej klasy, wtedy utworzymy midzy nimi relacj asocjacji. Zwizek ten bdzie jednokierunkowy, gdy jedynie obiekt posiadajcy wskanik stanie si jego aktywn czci i bdzie inicjowa komunikacj z drugim obiektem. Ten drugi obiekt moe w zasadzie nie wiedzie, e jest czci relacji! W zwizku jednokierunkowym z pierwszego obiektu moemy otrzyma drugi, lecz odwrotna sytuacja nie jest moliwa. Naturalnie, niekiedy bdziemy potrzebowali obustronnego, wzajemnego dostpu do obiektw relacji. W takim przypadku naley zastosowa zwizek dwukierunkowy. W zwizku dwukierunkowym oba obiekty maj do siebie wzajemny dostp. Taka sytuacja czsto uatwia pisanie bardziej skomplikowanego kodu oraz organizacj przeplywu danych. Jej implementacja napotyka jednak ma pewn, zdawaoby si nieprzekraczaln przeszkod. Popatrzmy bowiem na taki oto kod: class CFoo { private: // wskanik do poczonego obiektu CBar CBar* m_pBar; }; class CBar { private: // wskanik do poczonego obiektu CFoo CFoo* m_pFoo; }; Zdawaoby si, e poprawnie realizuje on zwizek dwukierunkowy klas CFoo i CBar. Prba jego kompilacji skoczy si jednak niepowodzeniem, a to z powodu wskanika na obiekt klasy CBar, zadeklarowanego wewntrz CFoo. Kompilator analizuje bowiem kod sekwencyjnie, wiersz po wierszu, zatem na etapie definicji CFoo nie ma jeszcze bladego pojcia o klasie CBar, wic nie pozwala na zadeklarowanie wskanika do niej. atwo przewidzie, e zamiana obu definicji miejscami w niczym tu nie pomoe. Dochodzimy do paradoksu: aby zdefiniowa pierwsz klas, potrzebujemy drugiej klasy, za by zdefniowa drug klas, potrzebujemy definicji pierwszej klasy! Sytuacja wydaje si by zupenie bez wyjcia A jednak rozwizanie istnieje, i jest do tego bardzo proste. Skoro kompilator nie wie, e CBar jest klas, trzeba mu o tym zawczasu powiedzie. Aby jednak znowu nie wpa w bdne koo, nie udzielimy o CBar adnych bliszych informacji; zamiast definicji zastosujemy deklaracj zapowiadajc: class CBar; // rzeczona deklaracja

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:

Wykres 2. Szybko oraz pojemno kilku typw pamici komputera

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

Zmienne przechowywane w rejestrach


Moemy jednak odnie pewne korzyci z istnienia rejestrw procesora i sprawi, by zaczy dziaa po naszej stronie. Jako niezwykle szybkie porcje pamici s idealne do przechowywania maych, ale czsto i intensywnie uywanych zmiennych. Na dodatek nie musimy wcale martwi si o to, w ktrym dokadnie rejestrze moemy w danej chwili zapisa dane oraz czy pozostan one tam nienaruszone. Czynnoci te mona bowiem zleci kompilatorowi: wystarczy jedynie uy sowa kluczowego register - na przykad: register int nZmiennaRejestrowa; Gdy opatrzymy deklaracj zmiennej tym modyfikatorem, to bdzie ona w miar moliwoci przechowywana w ktrym z rejestrw uniwersalnych procesora. Powinno to rzecz jasna przyspieszy dziaanie caego programu.

88

Wszystkie nazwy rejestrw odnosz si do procesorw 32-bitowych.

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

Skd si bierze pami operacyjna?


Pami tego rodzaju utosamiamy zwykle z jedn lub kilkoma elektronicznymi ukadami scalonymi (tzw. komi), woonymi w odpowiednie miejsca pyty gwnej peceta.

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

Organizacja pamici operacyjnej


Spord wszystkich trzech rodzajw pamici, dla nas w kontekcie wskanikw najwaniejsza bdzie pami operacyjna. Poznamy teraz jej budow widzian z koderskiego punktu widzenia.

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.

Epoka niewygodnych segmentw


Dawno, dawno temu (co oznacza przeom lat 80. i 90. ubiegego stulecia) wikszo programistw nie moga by zbytnio zadowolona z metod, jakich musieli uywa, by obsugiwa wiksze iloci pamici operacyjnej. Bya ona bowiem podzielona na tzw. segmenty, kady o wielkoci 64 kilobajtw. Aby zidentyfikowa konkretn komrk naleao wic poda a dwie opisujce jej liczby: oczywicie numer segmentu, a take offset, czyli konkretny ju indeks w ramach danego segmentu.

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

Paski model pamici


Dzisiejsze systemy operacyjne maj znacznie wygodniejszy sposb organizacji pamici RAM. Jest nim wanie w paski model (ang. flat memory model), likwidujcy wiele mankamentw swego segmentowego poprzednika. 32-bitowe procesory pozwalaj mianowicie, by caa pami bya jednym segmentem. Taki segment moe mie rozmiar nawet 4 gigabajtw, wic z atwoci zmieszcz si w nim wszystkie fizyczne i wirtualne zasoby RAMu. To jednake nie wszystko. Ot paski model umoliwia zgrupowanie wszystkich dostpnych rodzajw pamici operacyjnej (koci RAM, plik wymiany, pami karty graficznej, itp.) w jeden cigy obszar, zwany przestrzeni adresow. Programista nie musi si przy tym martwi, do jakiego szczeglnego typu pamici odnosi si dany wskanik! Na poziomie jzyka programowania znikaj bowiem wszelkie praktyczne rnice midzy nimi: oto mamy jeden, wielki segment caej pamici operacyjnej i basta!

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.

Czym jest stos?


Stos (ang. stack) jest obszarem pamici, ktry zostaje automatycznie przydzielony do wykorzystania dla programu.

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.

Uywanie wskanikw na zmienne


Wskanik jest przede wszystkim liczb - adresem w pamici, i w takiej te postaci istnieje w programie. Jzyk C++ ma ponadto cise wymagania dotyczce kontroli typw i z tego powodu kady wskanik musi mie dodatkowo okrelony typ, na jaki wskazuje. Innymi

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.

Nieodaowany spr o gwiazdk


Dowiedzielimy si ju, e piszc gwiazdk po nazwie jakiego typu, uzyskujemy odpowiedni wskanik na ten typ. Potem moemy uy go, deklarujc waciwy wskanik; co wicej, moliwe jest uczynienie tego a na cztery sposoby: int* pnWskaznik; int *pnWskaznik; int*pnWskaznik; int * pnWskaznik; Wida wic, e owa gwiazdka nie trzyma si kurczowo nazwy typu (tutaj int) i moe nawet oddziela go od nazwy deklarowanej zmiennej, bez potrzeby uycia w tym celu spacji. Wydawaoby si, e taka swoboda skadniowa powinna tylko cieszy. W rzeczywistoci jednak powoduje najczciej trudnoci w rozumieniu kodu napisanego przez innych,

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

Mona skorzysta z podanego wczeniej linka do FAQa.

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.

Podsumowanie deklaracji wskanikw


Na sam koniec tematu deklarowania wskanikw tradycyjnie podam troch wskazwek dotyczacych skadni oraz stosowalnoci praktycznej. Skadnie deklaracji wskanika moemy, opierajc si na przykadach z poprzednich paragrafw, przedstawi nastpujco: [const] typ* [const] wskanik; Moliwo wystpowania lub niewystpowania modyfikatora const w a dwch miejscach deklaracji pozwala stwierdzi, e z kadego typu moemy wyprowadzi cznie nawet cztery odpowiednie typy wskanikowe. Ich charakterystyk przedstawia ponisza tabelka: typ wskanikowy typ* const typ* typ* const const typ* const nazwa wskanik (zwyky) wskanik do staej stay wskanik stay wskanik do staej dostp do pamici odczyt i zapis wycznie odczyt odczyt i zapis wycznie odczyt zmiana adresu dozwolona dozwolona niedozwolona niedozwolona

Tabela 12. Zestawienie typw wskanikowych

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.

Pobieranie adresu i dereferencja


Wskanik powinien na co wskazywa - to znaczy przechowywa adres jakie komrki w pamici. Taki adres mona uzyska na wiele sposobw, w zalenoci od tego, jakie znaczenie ma owa komrka w programie. Dla zmiennych waciw metod jest uycie operatora pobierania adresu, oznaczanego znakiem & (ampersandem). Popatrzmy na niniejszy przykad: // zadeklarowanie zmiennej oraz odpowiedniego wskanika unsigned uZmienna; unsigned* puWskaznik; // pobranie adresu zmiennej i zapisanie go we wskaniku puWskaznik = &uZmienna; Wyraenie &uZmienna reprezentuje tutaj warto liczbow, bdc adresem miejsca w pamici, w ktrym rezyduje zmienna uZmienna. Typem tej zmiennej jest unsigned; wyraenie &uZmienna jest natomiast przynalene typowi wskanikowemu unsigned*. Przypisujemy go wic zmiennej tego typu, czyli wskanikowi puWskaznik. Odtd odnosi si on do naszej zmiennej liczbowej i moe by uyty w celu odwoania si do niej. Prezentowany tu operator & jest wic unarny - d tylko jednego argumentu: obiektu, ktrego adres ma uzyska. Zwraca go w wyniku, za typem tego rezultatu jest odpowiedni typ wskanikowy - zobaczylimy to zreszt na powyszym przykadzie. Przypominam, e adres zmiennej moemy przypisa jedynie do niestaego (ruchomego) wskanika. Majc wskanik, chciaoby si odwoa do komrki w pamici, czyli zmiennej, na ktr on wskazuje. Potrzebujemy zatem operatora, ktry dokona czynnoci odwrotnej ni operator &, a wic wydobdzie zmienn spod adresu przechowywanego przez wskanik. Dokonuje tego operator dereferencji, symbolem ktrego jest * (asterisk albo po prostu gwiazdka). Czynno przez niego wykonywan nazywamy wic dereferencj wskanika. Wystarczy spojrze na poniszy kod, a wszystko stanie si jasne: // zapisanie wartoci w komrce pamici, na ktr pokazuje wskanik *puWskaznik = 123; // odczytanie i wywietlenie tej wartoci std::cout << "Wartosc zmiennej uZmienna: " << *puWskaznik; Widzimy, e operator ten jest take unarny, co w oczywisty sposb rni go od operatora mnoenia, ktry w C++ jest przecie reprezentowany przez ten sam znak.

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:

Schemat 33. Dziaanie operatorw: pobrania adresu i dereferencji

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.

Konwersje typw wskanikowych


Dwa poznane operatory nie wyczerpuj rzecz jasna asortymentu operacji, jakich moemy dokonywa na wskanikach. Dosy czsto zachodzi bowiem potrzeba przypisywania wskanikw, ktrych typy s w wikszym lub mniejszym stopniu niezgodne - podobnie

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

Matka wszystkich wskanikw


Przypomnijmy sobie definicj wskanika, jak podalimy na pocztku rozdziau. Ot jest to przede wszystkim adres jakiej komrki (miejsca) w pamici. Przy jej paskim modelu sprowadza si to do pojedynczej liczby bez znaku. Na przechowywanie takiej liczby wystarczyby wic tylko jeden typ zmiennej liczbowej! C++ oferuje jednak moliwo definiowania wasnych typw wskanikowych w oparciu o ju istniejce, inne typy. Cel takiego postpowania jest chyba oczywisty: tylko znajc typ wskanika moemy dokona jego dereferencji i uzyska zmienn, na ktr on wskazuje. Informacja o docelowym typie wskazywanych danych jest wic niezbdna do ich uytkowania. Moliwe jest aczkolwiek zadeklarowanie oglnego wskanika (ang. void pointer lub pointer to void), ktremu nie s przypisane adne informacje o typie. Taki wskanik jest wic jedynie adresem samym w sobie, bez dodatkowych wiadomoci o rodzaju danych, jakie si pod tym adresem znajduj. Aby zadeklarowa taki wskanik, zamiast nazwy typu wpisujemy mu void: void* pWskaznik; // wskanik, ktry moe pokazywa na wszystko

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

Przywracanie do stanu uywalnoci


Cay problem z oglnymi wskanikami polega na tym, e przy ich pomocy nie moemy w zasadzie zrobi niczego konkretnego. Dereferencja nie wchodzi w gr z powodu niedostatecznych informacji o typie danych, na ktre wskanik pokazuje. eby mc z tych danych skorzysta, musimy wic przekaza kompilatorowi niezbdne informacje o ich typie. Dokonujemy tego poprzez rzutowanie. Operacja rzutowania wskanika typu void* na inny typ wskanikowy jest przede wszystkim zabiegiem formalnym. Zarwno przed ni, jak i po niej, mamy bowiem do czynienia z adresem tej samej komrki w pamici. Jej zawarto jest jednak inaczej interpretowana. Dokonanie takiego rzutowania nie jest trudne - wystarczy posuy si standardowym operatorem static_cast: // zmienna oraz oglny wskanik, do ktrej zapiszemy jej adres int nZmienna = 17011987; void* pVoid = &nZmienna; // ponowne wykorzystanie owego adresu we wskaniku na typ unsigned // stosujemy rzutowanie, aby przypisa mu wskanik typu void* unsigned* puLiczba = static_cast<unsigned*>(pVoid); // wywietlenie wartoci pokazywanej przez wskanik std::cout << *puLiczba; // wynikiem jest warto zmiennej nZmienna W powyszym przykadzie wskanik typu int* zostaje najpierw zredukowany do void*, by potem poprzez rzutowanie zosta zinterpretowany jako unsigned*. Cay czas pokazuje on oczywicie na to samo miejsce w pamici, tyle e w toku programu jest ono traktowane na rne sposoby.

Midzy palcami kompilatora


Chwileczk! Przecie t drog moemy zwyczajnie oszuka kompilator i sprawi, e zacznie on traktowa jaki typ danych jako zupenie inny, nawet cakowicie niezwizany z tym oryginalnym! Istotnie - za porednictwem wskanika typu void* moliwe jest dosownie zinterpretowanie cigu bitw jako dowolnego typu zmiennych. Dzieje si tak dlatego, e podczas rzutowania nie jest dokonywane adne sprawdzenie faktycznej poprawnoci typw. static_cast nie dziaa tak jak dynamic_cast i nie kontroluje sensownoci oraz celowoci rzutowania. Zakres stosowalnoci dynamic_cast jest za, jak pamitamy, ograniczony tylko do typw polimorficznych. Skalarne typy podstawowe z pewnocia nimi nie s, dlatego nie moemy do nich uywa tego typu rzutowania. Potencjalnie wic dostajemy do rki brzytw, ktr mona si niele pokaleczy. W okrelonych sytuacjach potrzebne jest jednak takie dosowne potraktowanie pewnego

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.

Tablice jednowymiarowe w pamici


Swego czasu powiedzielimy sobie, e tablice s zespoem wielu zmiennych opatrzonych t sam nazw i identyfikowanych poprzez indeksy. Symbolicznie przedstawialimy na diagramach tablice jednowymiarowe jako rwny rzd prostoktw, wyobraajcych kolejne elementy. To nie by wcale przypadek. Tablice takie maj bowiem wan cech: Kolejne elementy tablicy jednowymiarowej s uoone obok siebie, w cigym obszarze pamici.

262

Podstawy programowania

Nie s wic porozrzucane po caej dostpnej pamici (czyli pofragmentowane), ale grzecznie zgrupowane w jeden pakiet.

Schemat 34. Uoenie tablicy jednowymiarowej w pamici operacyjnej

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

Tablice wielowymiarowe w pamici


Dla tablic wielowymiarowych sprawa ich rozmieszczenia w pamici jest nieco bardziej skomplikowana. W przeciwiestwe do pamici nie maj one bowiem struktury liniowej, zatem kompilator j jako symulowa (czyli linearyzowa tablic). Nie jest to specjalnie trudna czynno, ale praktyczny sens jej omawiania jest raczej wtpliwy. Z tego wzgldu mao kto stosuje wskaniki do pracy z wielowymiarowymi tablicami, za my nie bdziemy tutaj adnym wyjtkiem od reguy :) Zainteresowanym mog wyjani, e wymiary tablicy s ukadane w pamici wedug kolejnoci ich zadeklarowania w kodzie, od lewej do prawej. Posuwajc si wzdu takiej zlinearyzowanej tablicy najszybciej zmienia si wic ostatni indeks, wolniej przedostatni, i tak dalej. Formuka matematyczna suca do obliczania wskanika na element wielowymiarowej tablicy jest natomiast podana w MSDN.

acuchy znakw w stylu C


Kiedy ju omawiamy wskaniki w odniesieniu do tablic, nadarza si niepowtarzalna okazja, aby zapozna si take z acuchami znakw w jzyku C - poprzedniku C++. Po co? Ot jak dotd jest to najczeciej wykorzystywana forma wymiany tekstu midzy aplikacjami oraz bibliotekami. Do koronnych przykadw naley choby Windows API, ktrej obsugi przecie bdziemy si w przyszoci uczy.

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.

Przekazywanie wskanikw do funkcji


Jedn z waniejszych paszczyzn zastosowa wskanikw jest usprawnienie korzystania z funkcji. Wskaniki umoliwiaj osignicie kilku niespotykanych dotd moliwoci i optymalizacji.

Dane otrzymywane poprzez parametry


Wskanik jest odwoaniem do zmiennej (kluczem do niej), ktre ma jedn zasadnicz zalet: moe mianowicie by przekazywane gdziekolwiek i nadal zachowywa swoj podstawow rol. Niezalenie od tego, w ktrym miejscu programu uyjemy wskanika, bdzie on nadal wskazywa na ten sam adres w pamici, czyli na t sam zmienn. Jeeli wic przekaemy wskanik do funkcji, wtedy bdzie ona moga operowa na jego docelowej komrce pamici. W ten sposb moemy na przykad sprawi, aby funkcja zwracaa wicej ni jedn warto w wyniku swego dziaania. Spjrzmy na prosty przykad takiego zachowania: // funkcja oblicza cakowity iloraz dwch liczb oraz jego reszt int Podziel(int nDzielna, int nDzielnik, int* const pnReszta) { // zapisujemy reszt w miejscu pamici, na ktre pokazuje wskanik *pnReszta = nDzielna % nDzielnik; // zwracamy iloraz return nDzielna / nDzielnik;

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:

Screen 37. Dwie wartoci zwracane przez jedn funkcj

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.

Zapobiegamy niepotrzebnemu kopiowaniu


Oprcz otrzymywania kilku wynikw z jednej funkcji, zastosowanie wskanikw moe mie te podoe optymalizacyjne. Pomylmy, e taki wskanik to zawsze jest tylko zwyka liczba cakowita, zajmujca zaledwie 4 bajty w pamici. Jednoczenie jednak moe ona odnosi si do bardzo wielkich obiektw.

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.

Dynamiczna alokacja pamici


Kto wie, czy nie najwaniejszym polem do popisu dla wskanikw jest zawaszczanie nowej pamici w trakcie dziaania programu. Mechanizm ten daje nieosigaln inaczej elastyczno aplikacji i pozwala manipulowa danymi o zmiennej wielkoci. Bez niego wszystkie programy miayby z gry narzuczone limity na ilo przetwarzanych informacji, ktrych nijak nie monaby przekroczy. Koniecznie wic musimy przyjrze si temu zjawisku.

Przydzielanie pamici dla zmiennych


Wszystkie zmienne deklarowane w kodzie maj statycznie przydzielon pami o staym rozmiarze. Rezyduj one w obszarze pamici zwanym stosem, ktry rwnie ma niezmienn wielko. Stosujc wycznie takie zmienne, nie moemy wic przetwarza danych cechujcych si du rozpitoci zajmowanego miejsca w pamici. Oprcz stosu istnieje wszak take sterta. Jest to reszta pamici operacyjnej, niewykorzystana przez program w momencie jego uruchomienia, ale stanowica rezerw na przyszo. Aplikacja moe ze czerpa potrzebn w danej chwili ilo pamici (nazywamy to alokacj), wypenia wasnymi danymi i pracowa na nich, a po zakoczeniu roboty zwyczajnie odda j z powrotem (zwolni) do wsplnej puli. Najwaniejsze, e o iloci niezbdnego miejsca mona zdecydowa w trakcie dziaania programu, np. obliczy j na podstawie liczb pobranych od uytkownika czy te z jakiegokolwiek innego rda. Nie jestemy wic skazani na stay rozmiar stosu, lecz moemy dynamicznie przydziela sobie ze sterty tyle pamici, ile akurat potrzebujemy. Zbiory informacji o niestaej wielkoci staj si wtedy moliwe do opanowania.

Alokacja przy pomocy new


Cae to dobrodziejstwo jest cile zwizane z wskanikami, gdy to wanie za ich pomoc uzyskujemy now pami, odwoujemy si do niej i wreszcie zwalniamy j po skoczonej pracy. Wszystkie te czynnoci przeledzimy na prostym przykadzie. Wemy wic sobie zwyczajny wskanik na typ int:

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.

Zwalnianie pamici przy pomocy delete


Z kadej potgi trzeba jednak korzysta z rozwag. W przypadku dynamicznej alokacji zasada BHP brzmi: Zawsze zwalniaj zaalokowan przez siebie pami. Suy do tego odrbny operator delete (usu). Uycie go jest nadzwyczaj atwe: wystarczy jedynie poda mu wskanik na przydzielony obszar pamici, a on posusznie posprzta po nim i zwrci go do dyspozycji systemu operacyjnego, a wic i wszystkich pozostaych programw. Bez zwolnienia pamici operacyjnej nastpuje jej wyciek (ang. memory leak). Zaalokowana, a niezwolniona pami nie jest ju bowiem dostpna dla innych aplikacji. Po skoczeniu pracy z nasz dynamicznie stworzon zmienn musimy j zatem usun. Wyglda to nastpujco:

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.

Nowe jest lepsze


Jeeli czytaj to jakie osoby znajce jzyk C (w co wtpie, ale wyjtki zawsze si zdarzaj :D), to pewnie nie darowayby mi, gdybym nie wspomnia o sposobach na alokacj i zwalnianie pamici w tym jzyku. Chc zapewne wiedzie, dlaczego powinny o nich zapomnie (a powinny!) i stosowa wycznie new oraz delete. Ot w C mielimy dwie funkcje, malloc() i free(), suce odpowiednio do przydzielania obszaru pamici o danej wielkoci oraz do jego pniejszego zwalniania. Radziy sobie z tym zadaniem cakiem dobrze i mogyby w zasadzie nadal sobie z nim radzi. W C++ doszy jednak nowe zadania zwizane z dynamiczn alokacj pamici operacyjnej. Chodzi tu naturalnie o kwesti klas z programowania obiektowego i zwizanymi z nimi konstruktorami i destruktorami. Kiedy uywamy new i delete do tworzenia i niszczenia obiektw, w poprawny sposb wywouj one te specjalne metody. Funkcje znane z C nie robi tego; nie ma w tym jednak niczego dziwnego, bo w ich macierzystym jzyku w ogle nie istniao pojcie klasy czy obiektu, nie mwic ju o metodach uruchamianych podczas ich tworzenia i niszczenia. Nowy sposb alokacji ma jeszcze jedn zalet. Ot malloc() zwraca w wyniku wskanik oglny, typu void*, zamiast wskanika na okrelony typ danych. Aby przypisa go do wybranej zmiennej wskanikowej, naleao uy rzutowania. Przy korzystaniu z new nie jest to konieczne. Za pomoc tego operatora od razu uzyskujemy waciwy typ wskanika i nie musimy stosowa adnych konwersji.

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.

Prawo staoci referencji


Najdziwniej wyglda pewnie linijka z przypisaniem wartoci. Mimo e po lewej stronie znaku = stoi zmienna nReferencja, to jednak now warto otrzyma nie ona, lecz nZmienna, na ktr tamta pokazuje. Takie s po prostu uroki referencji i trzeba do nich przywykn. No dobrze, ale jak w takim razie zmieni adres pamici, na ktry pokazuje nasza referencja? Powiedzmy, e zadeklarujemy sobie drug zmienn: short nInnaZmienna; Chcemy mianowicie, eby odtd nReferencja pokazywaa wanie na ni (a nie na nZmienna). Jak (czy?) mona to uczyni? Niestety, odpowied brzmi: nijak. Raz ustalona referencja nie moe by bowiem doczepiona do innej zmiennej, lecz do koca pozostaje zwizana wycznie z t pierwsz. A zatem:

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.

Parametry przekazywane przez referencje


Ju przy okazji wskanikw zauwaylimy, e wykorzystanie ich jako parametrw funkcji moe przyspieszy dziaanie programu. Zamiast caych obiektw funkcja otrzymuje wtedy odwoania do nich, za poprzez nie moe odnosi si do faktycznych obiektw. Na potrzeby funkcji kopiowane s wic tylko 4 bajty odwoania, a nie czasem wiele kilobajtw waciwego obiektu! Przy tej samej okazji narzekalimy jednak, e zastosowanie wskanikw wymaga przeformatowania skadni caego kodu, w ktrym naley doda konieczne dereferencje i zmieni operatory wyuskania. To niewielki, ale jednak dolegliwy kopot. I oto nagle pojawia si cudowne rozwizanie :) Referencje, bo o nich rzecz jasna mwimy, s take odwoaniami do obiektw, ale moliwe jest stosowanie wobec nich zwyczajnej skadni, bez uciliwoci zwizanych ze wskanikami. Czynic je parametrami funkcji, powinnimy wic upiec dwie pieczenie na jednym ogniu, poprawiajc zarwno osigi programu, jak i wasne samopoczucie :D Spjrzmy zatem jeszcze raz na funkcj Wyszukaj(), z ktr spotkalimy si ju przy wskanikach. Tym razem jej parametry bd jednak referencjami. Oto jak wpynie to na wygld kodu: #include <string> int Wyszukaj (const std::string& strSzukany, const std::string& strPrzeszukiwany) { // przeszukujemy nasz napis for (unsigned i = 0; i <= strPrzeszukiwany.length() - strSzukany.length(); ++i) {

280

Podstawy programowania
// porwnujemy kolejne wycinki napisu if (strPrzeszukiwany.substr(i, strSzukany.length()) == strSzukany) // jeeli wycinek zgadza si, to zwracamy jego indeks return i;

// w razie niepowodzenia zwracamy -1 return -1;

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.

Cechy charakterystyczne funkcji


Rnego rodzaju funkcji - czy to wasnych, czy te wbudowanych w jzyk - uywalimy dotd tak czsto i w takich ilociach, e chyba nikt nie ma najmniejszych wtpliwoci, czym one s, do czego su i jaka jest ich rola w programowaniu. Teraz wic przypomnimy sobie jedynie te wasnoci funkcji, ktre bd dla nas istotne przy omawianiu wskanikw. Nie omieszkamy take pozna jeszcze jednego aspektu funkcji, o ktrym nie mielimy okazji dotychczas mwi. Wszystko to pomoe nam zrozumie koncepcj i stosowanie wskanikw na funkcje. Trzeba tu zaznaczy, e w tym momencie absolutnie nie chodzi nam o to, jakie instrukcje mog zawiera funkcje. Przeciwnie, nasza uwaga bdzie skoncentrowana wycznie na prototypie funkcji, jej wizytwce. Dla wskanikw na funkcje peni on bowiem podobne posugi, jak typ danych dla wskanikw na zmienne. Sama zawarto bloku funkcji, podobnie jak warto zmiennej, jest ju zupenie jednak inn (wewntrzn) spraw. Na pocztek przyjrzyjmy si skadni prototypu (deklaracji) funkcji. Wydaje si, e jest ona doskonale nam znana, jednak tutaj przedstawimy jej pen wersj: zwracany_typ [konwencja_wywoania] nazwa_funkcji([parametry]); Kademu z jej elementw przypatrzymy si natomiast w osobnym paragrafie.

Typ wartoci zwracanej przez funkcj


Wiele jzykw programowania rozrnia dwa rodzaje podprogramw. I tak procedury maj za zadanie wykonanie jakich czynnoci, za funkcje s przeznaczone do obliczania pewnych wartoci. Dla obu tych rodzajw istniej zwykle odmienne rozwizania skadniowe, na przykad inne sowa kluczowe. W C++ jest nieco inaczej: tutaj zawsze mamy do czynienia z funkcjami, gdy bezwgldnie konieczne jest okrelenie typu wartoci, zwracanej przez nie. Naturalnie moe by nim kady typ, ktry mgby rwnie wystepowa w deklaracji zmiennej: od typw wbudowanych, poprzez wskaniki, referencje, a do definiowanych przez uytkownika typw wyliczeniowych, klas czy struktur (lecz nie tablic).

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.

Instrukcja czy wyraenie


Od kwestii, czy funkcja zwraca jak warto czy nie, zaley to, jak moemy nazwa jej wywoanie: instrukcj lub te wyraeniem. Rnica pomidzy tymi dwoma elementami jzyka programowania jest do oczywista: instrukcja to polecenie wykonania jakich dziaa, za wyraenie - obliczenia pewnej wartoci; warto ta jest potem reprezentowana przez owo wyraenie. C++ po raz kolejny raczy nas tu niespodziank. Ot w tym jzyku niemal wszystko jest wyraeniem - nawet taka wybitnie instrukcyjna dziaalno jak choby przypisanie. Rzadko jednak uywamy jej w takim charakterze, za o wiele czciej jako zwyk instrukcj i jest to wwczas cakowicie poprawne. Wyraenie moe by w programowaniu uyte jako instrukcja, natomiast instrukcja nie moe by uyta jako wyraenie. Dla wyraenia wystpujcego w roli instrukcji jest wprawdzie obliczana jego warto, ale nie zostaje potem do niczego wykorzystana. To raczej typowa sytuacja i chocia moe brzmi niepokojco, wikszo kompilatorw nigdy o niej nie ostrzega i trudno poczytywa to za ich beztrosk. Pedantyczni programici stosuj jednak niecodzienny zabieg rzutowania na typ void dla wartoci zwrconej przez funkcj uyt w charakterze instrukcji. Nie jest to rzecz jasna konieczne, ale niektrzy twierdz, i mona w ten sposb unika nieporozumie. Przeciwny przypadek: kiedy staramy si umieci wywoanie procedury (niezwracajcej adnej wartoci) wewntrz normalnego wyraenia, jest ju w oczywisty sposb nie do przyjcia. Takie wywoanie nie reprezentuje bowiem adnej wartoci, ktra mogaby by uyta w obliczeniach. Mona to rwnie interpretowa jako niezgodno typw, poniewa void jako typ pusty jest niekompatybilny z adnym innym typem danych. Widzimy zatem, e kwestia zwracania lub niezwracania przez funkcj wartoci oraz jej rodzaju jest nierzadko bardzo wana.

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.

O czym mwi konwencja wywoania?


Jak ju wspomniaem, konwencja wywoania determinuje gwnie przekazywanie parametrw aktualnych dla funkcji, by moga ona uywa ich w swoim kodzie. Obejmuje to miejsce w pamici, w ktrym s one tymczasowo przechowywane oraz porzdek, w jakim s w tym miejscu kolejno umieszczane. Podstawowym rejonem pamici operacyjnej, uywanym jako porednik w wywoaniach funkcji, jest stos. Dostp do tego obszaru odbywa si w do osobliwy sposb, ktry znajduj zreszt odzwierciedlenie w jego nazwie. Stos charakteryzuje si bowiem tym, e gdy pooymy na nim po kolei kilka elementw, wtedy mamy bezporedni dostp jedynie do tego ostatniego, pooonego najpniej (i najwyej). Jeeli za chcemy dosta si do obiektu znajdujcego si na samym dole, wwczas musimy zdj po kolei wszystkie pozostae elementy, umieszczone na stosie pniej. Czynimy to wic w odwrotnej kolejnoci ni nastpowao ich odkadanie na stos. Dobr przykadem stosu moe by hada ksiek, pitrzca si na twoim biurku ;D Jeli zatem wywoujcy funkcj (ang. caller) umieci na stosie jej parametry w pewnym porzdku (co zreszt czyni), to sama funkcja (ang. callee - wywoywana albo routine podprogram) musi je pozyska w kolejnoci odwrotnej, aby je waciwie zinterpretowa. Obie strony korzystaj przy tym z informacji o konwencji wywoania, lecz w opisach katalogowych poszczeglnych konwencji podaje si wycznie porzdek stosowany przez wywoujcego, a wic tylko kolejno odkadania parametrw na stos. Kolejno ich podejmowania z niego jest przecie dokadnie odwrotna. Nie myl jednak, e kompilatory dokonuj jakich zoonych permutacji parametrw funkcji podczas ich wywoywania. Tak naprawd istniej jedynie dwa porzdki, ktre mog by kanw dla konwencji i stosowa si dla kadej funkcji bez wyjtku. Mona mianowicie podawa parametry wedle ich deklaracji w prototypie funkcji, czyli od lewej do prawej strony. Wwczas to wywoujcy jest w uprzywilejowanej pozycji, gdy uywa bardziej naturalnej kolejnoci; sama funkcja musi uy odwrotnej. Drugi wariant to odkadanie parametrw na stos w odwrotnej kolejnoci ni w deklaracji funkcji; wtedy to funkcja jest w wygodniejszej sytuacji. Oprcz stosu do przekazywania parametrw mona te uywa rejestrw procesora, a dokadniej jego czterech rejestrw uniwersalnych. Im wicej parametrw zostanie tam umieszczonych, tym szybsze powinno by (przynajmniej w teorii) wywoanie funkcji.

Wskaniki

285

Typowe konwencje wywoania


Gdyby kady programista ustala wasne konwencje wywoania funkcji (co jest teoretycznie moliwe), to oczywicie natychmiast powstaby totalny rozgardiasz w tej materii. Konieczno uwzgldniania upodoba innych koderw byaby z pewnoci niezwykle frustrujca. Za spraw jzykw wysokiego poziomu nie ma na szczcie a tak wielkich problemw z konwencjami wywoania. Jedynie korzystajc z kodu napisanego w innym jzyku trzeba je uwzgldnia. W zasadzie wic zdarza si to do czsto, ale w praktyce cay wysiek woony w zgodno z konwencjami ogranicza si co najwyej do dodania odpowiedniego sowa kluczowego do prototypu funkcji - w miejsce, ktre oznaczyem w jego skadni jako konwencja_wywoania. Czsto nawet i to nie jest konieczne, jako e prototypy funkcji oferowanych przez przerne biblioteki s umieszczane w ich plikach nagwkowych, za zadanie programisty-uytkownika ogranicza si jedynie do wczenia tyche nagwkw do wasnego kodu. Kompilator wykonuje zatem spor cz pracy za nas. Warto jednak przynajmniej zna te najczciej wykorzystywane konwencje wywoania, a nie jest ich wcale a tak duo. Ponisza lista przedstawia je wszystkie: cdecl - skrt od C declaration (deklaracja C).. Zgodnie z nazw jest to domylna konwencja wywoania w jzykach C i C++. W Visual C++ mona j jednak jawnie okreli poprzez sowo kluczowe __cdecl. Parametry s w tej konwencji przekazywane na stos w kolejnoci od prawej do lewej, czyli odwrotnie ni s zapisane w deklaracji funkcji stdcall - skrt od Standard Call (standardowe wywoanie). Jest to konwencja zbliona do cdecl, posuguje si na przykad tym samym porzdkiem odkadania parametrw na stos. To jednoczenie niepisany standard przy pisaniu kodu, ktry w skompilowanej formie bdzie uywany przez innych. Korzysta z niego wic chociaby system Windows w swych funkcjach API. W Visual C++ konwencji tej odpowiada sowo __stdcall fastcall (szybkie wywoanie) jest, jak nazwa wskazuje, zorientowany na szybko dziaania. Dlatego te w miar moliwoci uywa rejestrw procesora do przekazywania parametrw funkcji. Visual C++ obsuguj t konwencj poprzez swko __fastcall pascal budzi suszne skojarzenia z popularnym ongi jzykiem programowania. Konwencja ta bya w nim wtedy intensywnie wykorzystywana, lecz dzisiaj jest ju przestarzaa i coraz mniej kompilatorw (wszelkich jzykw) wspiera j thiscall to specjalna konwencja wywoywania metod obiektw w jzyku C++. Funkcje wywoywane z jej uyciem otrzymuj dodatkowy parametr, bdcy wskanikiem na obiekt danej klasy90. Nie wystpuje on na licie parametrw w deklaracji metody, ale jest dostpny poprzez sowo kluczowe this. Oprcz tej szczeglnej waciwoci thiscall jest identyczna z stdcall. Ze wzgldu na specyficzny cel istnienia tej konwencji, nie ma moliwoci zadeklarowania zwykej funkcji, ktra by jej uywaa. W Visual C++ nie odpowiada jej wic adne sowo kluczowe A zatem dotychczas (niewiadomie!) uywalimy tylko dwch konwencji: cdecl dla zwykych funkcji oraz thiscall dla metod obiektw. Kiedy zaczniemy nauk programowania aplikacji dla Windows, wtedy ten wachlarz zostanie poszerzony. W kadym przypadku skadnia wywoania funkcji w C++ bdzie jednak identyczna.

90

Jest on umieszczany w jednym z rejestrw procesora.

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 :)

Rozterki kompilatora i linkera


Jedynie w czasie kompilacji kodu nazwy funkcji maj jakie znaczenie. Kompilator musi bowiem zapewni ich unikalno w skali caego projektu, tj. wszystkich jego moduw. Nie jest to wcale proste, jeeli przypomnimy sobie o funkcjach przecianych, ktre z zaoenia maj te same nazwy. Poza tym funkcje o tej samej nazwie mog te wystpowa w rnych zakresach: jedna moe by na przykad metod jakiej klasy, za druga zwyczajn funkcj globaln. Kompilator rozwizuje te problemy, stosujc tak zwane dekorowanie nazw. Wykorzystuje po prostu dodatkowe informacje o funkcji (jej prototyp oraz zakres, w ktrym zostaa zadeklarowana), by wygenerowa jej niepowtarzaln, wewntrzn nazw. Zawiera ona wiele rnych dziwnych znakw w rodzaju @, ^, ! czy _, dlatego wanie jest okrelana jako nazwa dekorowana. Wywoania z uyciem takich nazw s umieszczane w skompilowanych moduach. Dziki temu linker moe bez przeszkd poczy je wszystkie w jeden plik wykonywalny caego programu.

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.

Uywanie wskanikw do funkcji


Przypomnielimy sobie i uzupenilimy wszystkie niezbdne wiadomoci funkcjach, konieczne do poznania i stosowania wskanikw na nie. Teraz wic moemy ju przej do waciwej czci tematu.

Typy wskanikw do funkcji


Jakkolwiek wskaniki s przede wszystkim adresami miejsc w pamici operacyjnej, niemal wszystkie jzyki programowania oraz ich kompilatory wprowadzaj pewne dodatkowe informacje, zwizane ze wskanikami. Chodzi tu gwnie o typ wskanika. W przypadku wskanikw na zmienne by on pochodn typu zmiennej, na ktr dany wskanik pokazywa. Podobne pojcie istnieje take dla wskanikw do funkcji - w tym wypadku moemy wic mwi o typie funkcji, na ktre wskazuje okrelony wskanik.

Wasnoci wyrniajce funkcj


Co jednak mamy rozumie pod pojciem typ funkcji? W jaki sposb funkcja moe w ogle by zakwalifikowana do jakiego rodzaju? W odpowiedzi moe nam znowu pomc analogia do zmiennych. Ot typ zmiennej okrelamy w momencie jej deklaracji - jest nim w zasadzie caa ta deklaracja z wyczeniem nazwy. Okrela ona wszystkie cechy deklarowanej zmiennej, ze szczeglnym uwzgldnieniem rodzaju informacji, jakie bdzie ona przechowywa. Typu funkcji moemy zatem rwnie szuka w jej deklaracji, czyli prototypie. Kiedy bowiem wyczymy z niego nazw funkcji, wtedy pozostae skadniki wyznacz nam jej typ. Bd to wic kolejno: typ wartoci zwracanej przez funkcj konwencja wywoania funkcji parametry, ktre funkcja przyjmuje Wraz z adresem danej funkcji stanowi to wystarczajcy zbir informacji dla kompilatora, na podstawie ktrych moe on dan funkcj wywoa.

Typ wskanika do funkcji


Posiadajc wyliczone wyej wiadomoci na temat funkcji, moemy ju bez problemu zadeklarowa waciwy wskanik na ni. Typ tego wskanika bdzie wic oparty na typie funkcji - to samo zjawisko miao miejsce take dla zmiennych. Typ wskanika na funkcj okrela typ zwracanej wartoci, konwencj wywoania oraz list parametrw funkcji, na ktre wskanik moe pokazywa i ktre mog by za jego porednictwem wywoywane. Wiedzc to, moemy przystpi do poznania sposbu oraz skadni, poprzez ktre jzyk C++ realizuje mechanizm wskanikw do funkcji.

288

Podstawy programowania

Wskaniki do funkcji w C++


Deklarujc wskanik do funkcji, musimy poda jego typ, czyli te trzy cechy funkcji, o ktrych ju kilka razy mwiem. Jednoczenie kompilator powinien wiedzie, e ma do czynienia ze wskanikiem, a nie z funkcj jako tak. Oba te wymagania skutkuj specjaln skadni deklaracji wskanikw na funkcje w C++. Zacznijmy zatem od najprostszego przykadu. Oto deklaracja wskanika do funkcji, ktra nie przyjmuje adnych parametrw i nie zwraca te adnego rezultatu91: void (*pfnWskaznik)(); Jestemy teraz wadni uy tego wskanika i wywoa za jego porednictwem funkcj o odpowiednim nagwku (czyli nic niebiorc oraz nic niezwracajc). Moe to wyglda chociaby tak: #include <iostream> // funkcja, ktr bdziemy wywoywa void Funkcja() { std::cout << "Zostalam wywolana!"; } void main() { // deklaracja wskanika na powysz funkcj void (*pfnWskaznik)(); // przypisanie funkcji do wskanika pfnWskaznik = &Funkcja; // wywoanie funkcji poprzez ten wskanik (*pfnWskaznik)();

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

float __fastcall (*pfnWskaznik)(float, float);

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

Skadnia deklaracji wskanika do funkcji


Obecnie moemy ju zobaczy ogln posta deklaracji wskanika do funkcji. Jeeli uwanie przestudiowae poprzednie akapity, to nie bdzie on dla ciebie adn niespodziank. Przedstawia si za nastpujco: zwracany_typ ([konwencja_wywoania] *nazwa_wskanika)([parametry]); Pasujcy do niego prototyp funkcji wyglda natomiast w ten sposb: zwracany_typ [konwencja_wywoania] nazwa_funkcji([parametry]); Z obu tych wzorcw wida, e deklaracja wskanika do funkcji na podstawie jej prototypu oznacza wykonanie jedynie trzech prostych krokw: zamiany nazwy_funkcji na nazw_wskanika dodania * (gwiazdki) przed nazw_wskanika ujcia w par nawiasw ewentualn konwencj_wywoania oraz nazw_wskanika Nie jest to wic tak trudna operacja, jak si niekiedy powszechnie sdzi.

Wskaniki do funkcji w akcji


Zadeklarowanie wskanika to naturalnie tylko pocztek jego wykorzystania w programie. Aby by on uyteczny, powinnimy przypisa mu adres jakiej funkcji i skorzysta z niego celem wywoania teje funkcji. Przypatrzmy si bliej obu tym czynnociom. W tym celu zdefiniujmy sobie nastpujc funkcj: int PobierzLiczbe() { int nLiczba; std::cout << "Podaj liczbe: "; std::cin >> nLiczba; } return nLiczba;

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.

Przykad wykorzystania wskanikw do funkcji


Wskaniki do funkcji umoliwiaj wykonywanie oglnych operacji przy uyciu funkcji, ktrych implementacja nie musi by im znana. Wane jest, aby miay one nagwek zgodny z typem wskanika. Prawie podrcznikowym przykadem moe by tu poszukiwanie miejsc zerowych funkcji matematycznej. Procedura takiego poszukiwania jest zawsze identyczna, rwnie same

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

*pfZero = fXp; return true;

// 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();

Aplikacja ta wyszukuje miejsca zerowe funkcji okrelonej wzorem:

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

Screen 38. Program poszukujcy miejsc zerowych funkcji

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.

Zwyczajowy przebieg budowania programu


W jzyku programowania nieposiadajcym preprocesora generowanie docelowego pliku z programem przebiega, jak wiemy, w dwch etapach. Pierwszym jest kompilacja, w trakcie ktrej kompilator przetwarza kod rdowy aplikacji i produkuje skompilowany kod maszynowy, zapisany w osobnych plikach. Kady taki plik - wynik pracy kompilatora - odpowiada jednemu moduowi kodu rdowego. W drugiem etapie nastpuje linkowanie skompilowanych wczeniej moduw oraz ewentualnych innych kodw, niezbdnych do dziaania programu. W wyniku tego procesu powstaje gotowy program.

Schemat 36. Najprostszy proces budowania programu z kodu rdowego

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.

Schemat 37. Budowanie programu C++ z udziaem preprocesora

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

Ciekawostka: sekwencje trjznakowe


Istnieje jeszcze jedna, bardzo rzadka dzisiaj sytuacja, gdy preprocesor zostaje wezwany do akcji. Jest to jedyny przypadek, kiedy jego praca jest niezwizana z dyrektywami obecnymi w kodzie. Chodzi o tak zwane sekwencje trjznakowe (ang. trigraphs). C to takiego? W kadym dugo i szeroko wykorzystywanym produkcie pewne funkcje mog by po pewnym czasie uznane za przestarzae i przesta by wykorzystywane. Jeeli mimo to s one zachowywane w kolejnych wersjach, to zyskuj suszne miano skamieniaoci (ang. fossils). Jzyk C++ zawiera kilka takich zmumifikowanych konstrukcji, odziedziczonych po swoim poprzedniku. Jedn z nich jest na przykad moliwo wpisywania do kodu liczb w systemie semkowym (oktalnym), poprzedzajc je zerem (np. 042 to dziesitnie 34). Obecnie jest to cakowicie niepotrzebne, jako e wspczesny programista nie odniesie adnej korzyci z wykorzystania tego systemu liczbowego. W architekturze komputerw zosta on bowiem cakowicie zastpiony przez szesnastkowy (heksadecymalny) sposb liczenia. Ten jest na szczcie take obsugiwany przez C++94, natomiast zachowana moliwo uycia systemu oktalnego staa si raczej niedogodnoci ni plusem jzyka. atwo przecie omykowo wpisa zero przed liczb dziesitn i zastanawia si nad powstaym bdem Inn skamieniaoci s wanie sekwencje trjznakowe. To specjalne zoenia dwch znakw zapytania (??) oraz innego trzeciego znaku, ktre razem udaj symbol wany dla jzyka C++. Preprocesor zastpuje te sekwencje docelowym znakiem, postpujc wedug tej tabelki: trjznak ??= ??/ ???? ??! ??( ??) ??< ??> symbol # \ ~ ^ | [ ] { }

Tabela 13. Sekwencje trjznakowe w C++

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 a reszta kodu


Nadmieniem wczeniej, e dyrektywy preprocesora rni si od normalnych instrukcji jzyka C++ - choby tym, e na ich kocu nie stawiamy rednika. Ale nie jest to jeszcze caa prawda. Najwaniejsze jest to, jak preprocesor obchodzi si kodem rdowym programu. Jego podejcie jest odmienne od kompilatora. Tak naprawd to preprocesor w zasadzie nie wie, e przetwarzany przez niego tekst jest programem! Wiedza ta nie jest mu do niczego potrzeba, gdy traktuje on kod jak kady inny tekst. Dla preprocesora nie ma rnicy, czy pracuje na prostym programie konsolowym, zaawansowanej aplikacji okienkowej, czy nawet (hipotetycznie) na sidmej ksidze Pana Tadeusza. Monaby wic powiedzie, e preprocesor jest po prostu gupi - gdyby nie to, e bardzo dobrze radzi sobie ze swoim zadaniem. A jest nim przetwarzanie tekstu programu w taki sposb, aby uatwi ycie programicie. Dziki preprocesorowi mona bowiem automatycznie wykona operacje, ktre bez niego zajmowayby mnstwo czasu i susznie wydaway si jego kompletn, frustrujc strat. Jak zwykle jednak trzeba wtrci jakie ale :) Cakowita niewiedza preprocesora na temat podmiotu jego dziaa moe i jest bogosawiestwem dla niego samego, lecz stosunkowo atwo moe sta si przyczyn bdw kompilacji. Dotyczy to w szczeglnoci jednego z aspektw wykorzystania preprocesora - makr.

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.

Definiowianie prostych makr


Jak wyglda przykad opisanego wyej uycia #define? Popatrzmy: #define SIEDEM 7 // (tutaj troch kodu programu...) std::cout << SIEDEM << "elementow tablicy" << std::endl;;
95

Samodzielne - to znaczy jako odrbne sowo (token).

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.

Zastpowanie wikszych fragmentw kodu


Zamiast jednej liczby czy innego wyraenia, jako tre makra moemy te poda instrukcj. Moe to nam zaoszczdzi pisania. Przykadowo, jeeli przed wyjciem z funkcji musimy zawsze wyzerowa jak zmienn globaln, to moemy napisa sobie odpowiednie makro: #define ZAKONCZ { g_nZmienna = 0; return; }

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.

Makra korzystajce z innych makr


Nic nie stoi na przeszkodzie, aby nasze makra korzystay z innych wczeniej zdefiniowanych makr: #define PI 3.1415926535897932384

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.

Pojedynek: makra kontra stae


No wanie - staych Wikszo przedstawionych tutaj makr peni przecie tak sam rol, jak stae deklarowane swkiem const. Czy obie konstrukcje s wic sobie rwnowane? Nie. Stae deklarowane przez const i stae (makra) definiowane przez #define rni si od siebie, i to znacznie. Te rnice daj przewag obiektom const - powiedzmy tu sobie, dlaczego.

Makra nie s zmiennymi


Patrzc na ten tytu pewnie si umiechasz. Oczywicie, e makra nie s zmiennymi przecie to stae a raczej stae. To jednak nie jest wcale takie oczywiste, bo z kolei stae deklarowane przez const maj cechy zmiennych. Swego czasu mwiem nawet na poy artobliwie, i te stae s to zmienne, ktre s niezmienne. Makra #define takimi zmiennymi nie s, a przez to trac ich cenne waciwoci. Jakie? Zasig, miejsce w pamici i typ.

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.

Miejsce w pamici i adres


Nazwy makr nie s znane kompilatorowi, poniewa znikaj one po przetworzeniu programu przez preprocesor. Stae definiowane przez #define nie mog zatem istnie fizycznie w pamici, bo za jej przydzielanie dla obiektw niedynamicznych odpowiedzialny jest wycznie kompilator. Makra nie zajmuj miejsca w pamici operacyjnej i nie moemy pobiera ich adresw. Byoby to podobne do pobierania wskanika na liczb 5, czyli cakowicie bezsensowne i niedopuszczalne. Ale chwileczk Brak moliwoci pobrania wskanika atwo mona przetrawi, bo przecie nie robi si tego czsto. Nieobecno makr w pamici ma natomiast oczywist zalet: nie zajmuj jej swoimi wartociami. To chyba dobrze, prawda? Tak, to dobrze. Ale jeszcze lepiej, e obiekty const take to potrafi. Kady szanujcy si kompilator nie bdzie alokowa pamici dla staej, jeeli nie jest to potrzebne. Jeli wic nie pobieramy adresu staej, to bdzie ona zachowywaa si w identyczny sposb jak

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

Tabela 14. Typ staej liczbowej w zalenoci od sposobu jej zapisu

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++

#define DZIESIEC 10;

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

Nawiasy i priorytety operatorw


Popatrz na ten oto przykad: #define #define #define #define SZEROKOSC 10 WYSOKOSC 20 POLE SZEROKOSC * WYSOKOSC LUDNOSC 10000

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.

Dygresja: odpowied na pytanie o sens ycia


Jak ciekawe skutki moe wywoywa niewaciwe uycie makr? Cakiem znamienne. Przypadkowo mona na przykad pozna Najwaniejsz Liczb Wszechwiata. A t liczb jest 42. w magiczny numer pochodzi z serii science-fiction Autostopem przez Galaktyk autorstwa Douglasa Adamsa. Tam te pada odpowied na Najwaniejsze Pytanie o ycie, Uniwersum i Wszystko, ktra zostaje udzielona grupie myszy. Jak twierdzi Adams, myszy s trjwymiarowymi postaciami hiperinteligentnych istot wielowymiarowych, ktre zbudoway ogromny superkomputer, zdolny udzieli odpowiedzi na wspomniane Pytanie. Po siedmiu i p milionach lat uzyskuj j: jest to wanie czterdzieci dwa. Za chwil jednak komputer stwierdzi, e tak naprawd nie wiedzia do koca, jakie pytanie zostao mu zadane. Pod koniec jednego z tomw serii dowiadujemy si jednak, c to byo za pytanie:
Co otrzymamy, jeeli pomnoymy sze przez dziewi?

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;

Jak mona zobaczy, rzeczywicie drukuje on liczb 42:

Screen 39. Komputer prawd ci powie

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?

Predefiniowane makra kompilatora


Istnieje kilka makr, ktrych definiowaniem zajmuje si sam kompilator. Dostarczaj one kilku uytecznych informacji zwizanych z nim samym oraz z przebiegiem kompilacji. Dane te mog by czsto przydatne przy usuwaniu bdw, wic przyjrzyjmy si im. We wszystkich poniszych nazwach makr dugie kreski oznaczaj dwa znaki podkrelenia. Tak wic __ oznacza dwukrotne wpisanie znaku _, a nie jedn dug kresk.

Numer linii i nazwa pliku


Jednymi z najbardziej przydatnych makr s __FILE__ i __LINE__. Pozwalaj one na wykrycie miejsca w kodzie, gdzie np. zaszed bd wpywajcy na dziaanie programu.

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.

Nazwa pliku z kodem


Do pary z numerem wiersza potrzebujemy jeszcze nazwy pliku, aby precyzyjnie zlokalizowa bd. T za zwraca makro __FILE__: std::cout << "Ten kod pochodzi z moduu " << __FILE__; Jest ono zamieniane na nazw pliku kodu, ujt w podwjne cudzysowy - waciwe dla napisw w C++. Zatem jeli nasz modu nazywa si main.cpp, to __FILE__ zostanie zastpione przez "main.cpp".

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.

Czas modyfikacji pliku


Makro __TIMESTAMP__ jest nieco inne. Nie podaje ono czasu kompilacji, lecz dat i czas ostatniej modyfikacji pliku z kodem. Jest to dana w formacie Ddd Mmm d hh:mm:ss yyyy, gdzie Ddd jest skrtem dnia tygodnia, za d jest numerem dnia miesica. Popatrz na przykad. Jeli wpisz teraz do moduu ponisz linijk i zachowam plik kodu: std::cout << "Data ostatniej modyfikacji " << __TIMESTAMP__; to w programie zapisany zostanie napis "Wed Jul 14 18:38:37 2004". Bdzie tak niezalenie od chwili, w ktrej skompiluj program - chyba e do czasu jego zbudowania poczyni w kodzie jeszcze jakie poprawki. Wwczas __TIMESTAMP__ zmieni si odpowiednio, wywietlajc moment zapisywania ostatnich zmian. Pisz tu, i __TIMESTAMP__ co wywietli, ale to oczywicie skrt mylowy. Naprawd to makro zostanie zastpione przez preprocesor odpowiednim napisaem, za jego prezentacj zajmie si rzecz jasna strumie wyjcia.

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.

Definiowanie parametrycznych makr


Makrodefinicje nazywamy parametryzowanymi makrami, poniewa maj one co w rodzaju parametrw. Nie s to jednak konstrukcje podobne do parametrw funkcji - w dalszej czci sekcji przekonamy si, dlaczego. Na razie spojrzyjmy na przykadow definicj: #define SQR(x) ((x) * (x))

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.

Brak kontroli typw


Pocztek definicji sparametryzowanego makra (zaraz za #define) przypomina deklaracj funkcji, lecz bez okrelenia typw. Nie podajemy tu zarwno typw parametrw, jak i typw zwracanej wartoci. Dla preprocesora wszystko jest bowiem zwyczajnym tekstem, ktry ma by jedynie przetransformowany wedug podanego wzoru. Potencjalnie wic moe to rodzi problemy. Na szczcie jednak s one zawsze wykrywane ju ne etapie kompilacji. Jest tak, gdy o ile preprocesor posusznie rozwninie wyraenie typu: SQR("Tekst") do postaci: (("Tekst") * ("Tekst")) o tyle kompilator nigdy nie pozwoli na mnoenie dwch napisw. Taka operacja jest przecie kompletnie bez sensu. Dezorientacj moe jedynie wzbudza komunikat o bdzie, jaki dostaniemy w tym przypadku. Nie bdzie to co w rodzaju: "Bdny argument makra", bo dla kompilatora makra ju tam nie ma - jest tylko iloczyn dwch acuchw. Bd bdzie wic dotyczy niewaciwego uycia operatora *, co nie od razu moe nasuwa skojarzenia z makrami. Jeli wic kompilator zgasza nam dziwnie wygldajcy bd na (z pozoru) niewinnej linijce kodu, to sprawdmy przede wszystkim, czy nie ma w niej niewaciwego uycia makrodefinicji.

Preprocesor

321

Parokrotne obliczanie argumentw


Bdy zwizane z typami wyrae nie s zbyt kopotliwe, gdy wykrywane s ju w trakcie kompilacji. Inne problemy z makrami nie s a tak przyjemne Rozpatrzmy teraz taki kod: int nZmienna = 7; std::cout << SQR(nZmienna++) << std::endl; std::cout << nZmienna; Kompilator z pewnoci nie bdzie mia nic przeciwko niemu, ale jego dziaanie moe by co najmniej zaskakujce. Wedle wszelkich przewidywa powinien on przecie wydrukowa liczby 49 i 8, prawda? Dlaczego wic wynik jego wykonania przedstawia si tak:
56 9

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.

Makra kontra funkcje inline


C wic wynika z zapoznania si z funkcjami inline? Ano to, e powinnimy je stosowa zawsze wtedy, gdy przyjdzie nam ochota na wykorzystanie makrodefinicji. Funkcje inline s po prostu lepsze, gdy cz w sobie zarwno zalety zwykych funkcji, jak i zalety makr.

Brak kontroli typw


Wydawaoby si jednak, e jest jedna sytuacja, gdy makra maj przewag nad zwykymi funkcjami. Ta wyszo ujawnia si w cesze, ktr poprzednio wskazalimy jako ich sabo: w braku kontroli typw. Ot czsto jest to wrcz podana waciwo. Nie wiem czy zauwaye, ale wikszo zdefiniowanych przez nas makr dziaa rwnie dobrze dla liczb cakowitych, jak i rzeczywistych. Dziaa dla kadego typu zmiennych liczbowych: SQR(-14) SQR(12u) SQR(3.14f) SQR(-87.56) // // // // int unsigned float double

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.

Ciekawostka: funkcje szablonowe


Niecierpliwym poka ju teraz, w jaki sposb makro SQR() zastpi funkcj szablonow. Odpowiedni kod moe wyglda tak:

324

Zaawansowane C++

template <typename T> inline T Sqr(T a)

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

Nie korzystajmy z makr, lecz z obiektw const


Przede wszystkim nie powinnimy uywa makr tam, gdzie lepiej sprawdzaj si inne konstrukcje jzyka. Jeeli kompilator dostarcza nam narzdzi zastpujcych dane pole zastosowa makr, to zawsze bdzie to lepszy mechanizm ni same makra. Dotyczy to na przykad staych. Ju na samym pocztku kursu podkreliem, eby stosowa przydomek const do ich definiowania. Uycie #define pozbawia bowiem stae cennych cech niezmiennych zmiennych - typu, zasigu oraz miejsca w pamici.

Nie korzystajmy z makr, lecz z (szablonowych) funkcji inline


Podobnie nie powinnimy korzysta z makrodefinicji, by zyska na szybkoci programu. Te same efekty szybkociowe osigniemy bowiem za pomoc funkcji inline, za przy okazji nie pozbawimy si wygody i bezpieczestwa, jakie daje ich stosowanie (w przeciwiestwie do makr). A jeli chodzi o niewraliwo na typy danych, to obecnie moe to by dla ciebie zalet. Kiedy jednak poznasz technik szablonw, take i ten argument straci swoj wano.

Korzystajmy z makr, by zastpowa powtarzajce si fragmenty kodu


Jak wic poprawnie stosowa makra? Najwaniejsze jest, aby zapamita, czym one s. Powiedzielimy sobie dotd, czym makra nie s - nie s staymi i nie s funkcjami. Makra to najsampierw sposb na zastpienie jednego fragmentu kodu innym. Uywamy ich wic wtedy, gdy zauwaymy czsto powtarzajce si sekwencje dwch-trzech instrukcji, ktrych wyodrbnienie w osobnej funkcji nie jest moliwe, lecz ktrych rczne wpisywanie staje si nuce. Dla takich wanie sytuacji stworzono makra.

Korzystajmy z makr, by skraca sobie zapis


Makra s narzdzami do operacji na tekcie - tekcie programu, czyli kodzie. Stosujmy je wic, aby dokonywa takich automatycznych dziaa. Jeden przykad takiego zastosowania ju podaem: to bezpieczne zniszczenie obiektu poaczone z wyzerowaniem wskanika. Innym moe by chociaby pobranie liczby elementw niedynamicznej tablicy: #define ELEMENTS(tab) ((sizeof(tab) / sizeof((tab)[0]))

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

Korzystajmy z makr zgodnie z ich przeznaczeniem


Na koniec nie mog jeszcze nie wspomnie o bardzo wanym zastosowaniu makr, przewidzianym przez twrcw jzyka. Zastosowanie to przetrwao prb czasu i nikt nawet myli o jego zastpieniu czy likwidacji. Tym polem wykorzystania makr jest kompilacja warunkowa. Ten uyteczny sposb na kontrol procesu kompilacji programu jest tematem nastpnego podrozdziau.

Kontrola procesu kompilacji


Preprocesor wkracza do akcji, przegldajc kod jeszcze zanim zrobi to kompilator. Sprawia to, e moliwe jest wykorzystanie go do sprawowania kontroli nad procesem kompilacji programu. Moemy okreli, jakie jego fragmenty maj pojawi si w wynikowym pliku EXE, a jakie nie. Podejmowanie takich decyzji nazywamy kompilacj warunkow (ang. conditional compilation). Do czego moe to si przyda? Przede wszystkim pozwala to doczy do programu dodatkowy kod, pomocny w usuwaniu z niego bdw. Zazwyczaj jest to kod wywietlajcy pewne porednie wyniki oblicze, logujcy przebieg pewnych czynnoci lub prezentujcy co okrelony czas wartoci kluczowych zmiennych. Po zakoczeniu testowania aplikacji monaby byo w kod usun, ale jest przecie niewykluczone, e stanie si on przydatny w pracy nad kolejn wersj. Wyjciem byoby wic jego czasowe wyczenie w momencie finalnego kompilowania. Najprostszym rozwizaniem wydaje si uycie komentarza blokowego i jest to dobre wyjcie - pod jednym warunkiem: e nasz kompilator pozwala na zagniedanie takich komentarzy. Nie jest to wcale obowizkowy wymg i dlatego nie zawsze to si sprawdza. Komentowanie ma jeszcze jedn wad: komentarze trzeba za kadym razem dodawa lub usuwa rcznie. Po kilku-kilkunastu-kilkudziesiciu powtrzeniach kompilacji staje si to prawdziw udrk. A przecie mona sprytniej. Kompilacja warunkowa pozwala bowiem w prosty sposb wcza i wycza kompilowanie okrelonego kodu w zalenoci od stanu pewnych ustalonych warunkw. Mechanizm ten ma jeszcze jedn zalet, zwizana z przenonoci programw. Daje si to najbardziej odczu w aplikacjach rozprowadzanych wraz z kodem rdowym czy nawet wycznie w postaci rdowego (programach na licencjach Open Source i GNU GPL). Takie programy mog by teoretycznie kompilowane na wszystkich systemach operacyjnych i platformach sprztowych, dla ktrych istniej kompilatory C++. W praktyce zaley to od warunkow zewntrznych: wiadomo na przykad doskonale, e program dla rodowiska Windows nie uruchomi si ani nie skompiluje w systemie Linux. Jednak nawet pomidzy komputerami pracujcymi pod kontrol tych samych systemw operacyjnych wystpuj rnice (zwaszcza jeli chodzi o Linux). Przykadowo, procesory tych komputerw mog rni si architektur: obecnie dominuj jednostki 32-bitowe, ale w wielu zastosowaniach mamy ju procesory o 64 bitach w sowie maszynowym. Kompilatory wykorzystujce te procesory maj odmienn wielko typu int: odpowiednio 4 i 8 bajtw. Moe to rodzi problemy z zapisywaniem i odczytywaniem danych. Podobnych przykadw jest bardzo duo, wic twrcy aplikacji rozprowadzanych jako kod musz liczy si z tym, e bd one kompilowane na bardzo rnych systemach. Technika kompilacji warunkowej pozwala przygotowa si na wszystkie ewentualnoci. Wikszo opisanych tu problemw dotyczy aczkolwiek systemw z wolnym kodem rdowym, takich jak Linux. Stosowanie kontrolowanej kompilacji nie ogranicza si jednak tylko do programw pracujcych pod kontrol takich systemw. Take wiele funkcji Windows jest dostpnych jedynie w okrelonych wersjach systemu, a chcc z nich

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.

Dyrektywy #ifdef i #ifndef


Wpywanie na proces kompilacji odbywa si za pomoc kilku specjalnych dyrektyw preprocesora. Teraz poznamy kilka pierwszych, midzy innymi tytuowe #ifdef i #ifndef. Najpierw jednak drobne przypomnienie makr.

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++

Dyrektywa warunkowa #if


Uoglnieniem dyrektyw #ifdef i #ifndef jest dyrektywa #if: #if warunek instrukcje #endif Przypomina ona instrukcj if, z tym e odnosi si do zagadnienia kompilowania lub niekompilowania wyszczeglnionych instrukcji. #if ma te wersj z #else: #if warunek instrukcje_1 #else instrukcje_2 #endif Jak susznie przypuszczasz, #if sprawi, e w przypadku spenienia warunku skompilowany zostanie kod instrukcje_1, za w przeciwnym przypadku instrukcje_2 (lub aden, jeli #else nie wystpuje).

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

Skomplikowane warunki kompilacji


To jeszcze nie wszystkie moliwoci dyrektyw kompilacji warunkowej. Do bardziej wyszukanych naley ich zagniedanie i spitrzanie.

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.

Dwa warianty #include


Zaczniemy od przypomnienia skadni tej dyrektywy. Jak wiemy, istniej jej dwa warianty: #include <nazwa_pliku> #include "nazwa_pliku" Oba powoduj doczenie pliku o wskazanej nazwie. Podczas przetwarzania kodu preprocesor usuwa po prostu wszystkie dyrektywy #include, wstawiajc na ich miejsce zawarto wskazywanego przez nie plikw. Dzieki temu, e robi to preprocesor, a nie my, zyskujemy na kilku sprawach: nasze pliki kodu nie s (zbyt) due, bo zawarto doczanych plikw (nagwkowych) nie jest w nich wstawiona na stae, a jedynie doczana na czas kompilacji chcc zmieni zawarto wspdzielonych plikw, nie musimy modyfikowa ich kopii we wszystkich moduach, ktre ze korzystaj mamy wicej czasu, a przecie czas to pienidz ;D Skoro za #include oddaje nam tak cenne usugi, pomwmy o jej dwch wariantach i rnicach midzy nimi.

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?

Nasz czy biblioteczny


Decyzja jest jednak bardzo prosta: jeeli doczamy nasz wasny plik nagwkowy - taki, ktry znajduje si gdzie blisko, na przykad w tym samym katalogu - to powinnimy skorzysta z dyrektywy #include, podajc nazw pliku w cudzysowach jeli natomiast wykorzystujemy nagwek biblioteczny, pochodzcy od kompilatora czy innych zwizanych z nim komponentw - stosujmy #include z nawiasami ostrymi Teoretycznie mona byc zawsze stosowa wariant z cudzysowami. To jednak obniaoby czytelnoc kodu, gdy nie mona byoby atwo odrni, ktre dyrektywy doczaj nasze wasne nagwki, a ktre - nagwki biblioteczne. Lepiej wic stosowa rozrznienie. Nie pisaem tego na pocztku tej sekcji, ale chyba wiesz doskonale (bo mwiem o tym wczeniej), e poprawne jest doczanie wycznie plikw nagwkowych. S to pliki zawierajce deklaracje (prototypy) funkcji nie-inline, definicje funkcji inline, deklaracje

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.

Zabezpieczenie przed wielokrotnym doczaniem


Dyrektywa #include jest gupia jak cay preprocesor. Ona tylko wstawia tekst podanego w pliku w miejsce swego wystpienia. Nie dba przy tym, czy takie wstawienie spowoduje jakie niepodane efekty. A atwo moe przecie takie skutki wywoa Wyobramy sobie, e doczamy plik A, ktry sam docza plik B i X. Niech plik B te docza plik X i ju mamy problem: ewentualne definicje zawarte w X bd przez kompilator odczytane dwukrotnie. Zareaguje on wtedy bdem. Trzeba wic podj ku temu jaki rodki zaradcze.

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

Polecenia zalene od kompilatora


Na koniec omwimy sobie takie polecenia, ktrych wykonanie jest zalene od kompilatora, jakiego uywamy.

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.

Waniejsze parametry #pragma w Visual C++ .NET


Uywajcy innego kompilatora ni Visual C++ .NET mog opuci ten paragraf. Poniewa zakadam, e wikszo czytelnikw uywa zalecanego na samym pocztku kursu kompilatora Visual C++ .NET, sdz, e poyteczne bdzie przyjrzenie si kilku parametrom dyrektywy #pragma, jakie s tam dostpne.

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

W powyszym przypadku zobaczymy ostrzeenie w rodzaju:


warning C4995: 'Funkcja': name was marked as #pragma deprecated

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.

Deklaracja przyjani z funkcj


Chcc uczyni jak funkcj przyjacielem klasy, musimy w definicji klasy poda deklaracj zaprzyjanionej funkcji, poprzedzajc j sowem kluczowym friend. Ilustracj tego faktu nie bdzie poniszy przykad. Mamy w nim klas opisujc okrg CCircle. Zaprzyjaniona z ni funkcja PrzecinajaSie() sprawdza, czy podane jej dwa okrg maj punkty wsplne: #include <cmath> class CCircle { private: // rodek okrgu struct { float x, y; } m_ptSrodek; // jego promie float m_fPromien; public: // konstruktor CCircle (float fPromien, float fX = 0.0f, float fY = 0.0f) { m_fPromien = fPromien; m_ptSrodek.x = fX; m_ptSrodek.y = fY; } // deklaracja przyjani z funkcj friend bool PrzecinajaSie(CCircle&, CCircle&);

};

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

Na co jeszcze trzeba zwrci uwag


Wszystko wydawaoby si raczej proste. Nie zaszkodzi jednak powiedzie wprost o pewnych oczywistych faktach zwizanych z zaprzyjanionymi funkcjami.

Funkcja zaprzyjaniona nie jest metod


Jedno swko friend moe bardzo wiele zmieni. Porwnajmy choby te dwie klasy: class CFoo { public: void Funkcja(); }; class CBar { public: friend void Funkcja(); }; Rni si one tylko tym swkiem ale jest to rnica znaczca. W pierwszej klasie Funkcja() jest jej metod: zadeklarowalimy j tak, jak wszystkie normalne metody klas. Znamy to ju dobrze, gdy proces definiowania metod poznalimy przy pierwszym spotkanie z OOPu. Do peni szczci na ley jeszcze tylko zdefiniowa ciao emtody CFoo::Funkcja() i wszystko bdzie w porzdku. Deklaracja w drugiej klasie jest natomiast opatrzona swkiem friend, ktre zupenie zmienia jej znaczenie. Funkcja() nie jest tu metod klasy CBar. Jest wprawdzie zaprzyjaniona z ni, ale nie jest jej skadnikiem: nie ma dostpu do wskanika this. Aby z tej zaprzyjanionej funkcji mg by w ogle jaki uytek, trzeba jej zapewni dostp do obiektu klasy CBar, bo jej samej nikt go nie da. Wobec braku parametrw funkcji pewnie bdzie to wymagao zadeklarowania globalnej zmiennej obiektowej typu CBar.

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.

Deklaracja przyjani jest te deklaracj funkcji


Mamy te drugi wany fakt zwizany z deklaracj funkcji zaprzyjanionej.

Deklaracja przyjani jako prototyp funkcji


Ot, taka deklaracja przyjani jest jednoczenie deklaracj funkcji jako takiej. Musimy zauway, e w zaprezentowanych przykadach funkcje, ktre byy przyjacielami klasy, zostay zdefiniowane dopiero po definicji teje klasy. Wczeniej kompilator nic o nich nie wiedzia - a mimo to pozwoli na ich zaprzyjanienie! Czy to jaka niedorbka? Ale skd! Kompilator uznaje po prostu deklaracj przyjani z funkcj take za deklaracj samej funkcji. Linijka ze sowem friend peni wic funkcj prototypu funkcji, ktra moe by swobodnie zdefiniowana w zupenie innym miejscu. Z kolei wczeniejsze prototypowanie funkcji, przed deklaracj przyjani, nie jest konieczne. Mwic po ludzku, w poniszym kodzie: bool PrzecinajaSie(CCircle&, CCircle&); class CCircle { // (ciach - szczegy) }; friend bool PrzecinajaSie(CCircle&, CCircle&);

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

Przyja z pojedynczymi metodami


Wiemy ju, e moemy zadeklarowa przyja klasy z funkcj globaln. Teraz dowiemy si, e przyjacielem moe by take inny rodzaj funkcji - metoda klasy. Ponownie spojrzyj na odpowiedni przykad: // deklaracja zapowiadajca klasy CCircle class CCircle; class CGeometryManager { public: bool PrzecinajaSie(CCircle&, CCircle&); }; class CCircle { // (pomijamy reszt) }; friend bool CGeometryManager::PrzecinajaSie(CCircle&, CCircle&);

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

Jeszcze kilka uwag


Przyja nie jest szczegolnie zawiym aspektem programowania obiektowego w C++. Wypada jednak nieco ucili jej wpyw na pozostae elementy OOPu.

Cechy przyjani klas w C++


Przyja klas w C++ ma trzy znaczce cechy, na ktre chc teraz zwrci uwag.

Przyja nie jest automatycznie wzajemna


W prawdziwym yciu kto, kogo uwaamy za przyjaciela, ma zwykle to samo zdanie o nas. To wicej ni naturalne. W programowaniu jest inaczej. Mona to uzna za kolejny argument, i jest ono zupenie oderwane od rzeczywistoci, a mona po prostu przyj to do wiadomoci. A prawda jest taka: Klasa deklarujca przyja udostpnia przyjacielowi swoje niepubliczne skadowe - lecz nie powoduje to od razu, e klasa zaprzyjaniona jest tak samo otwarta. Powiedziaem ju, e chcc stworzy wzajemny zwizek przyjani trzeba umieci odpowiednie deklaracje w obu klasach. Wymaga to zawsze zapowiadajcej deklaracji przynajmniej jeden z powizanych klas.

Przyja nie jest przechodnia


Inaczej mwic: przyjaciel mojego przyjaciela nie jest moim przyjacielem. Przekadajc to na C++: Jeeli klasa A deklaruje przyja z klas B, za klasa B z klas C, to nie znaczy to, e klasa C jest od razu przyjacielem klasy A. Gdybymy chcieli, eby tak byo, powinnimy wyranie to zadeklarowa: friend class C;

Przyja nie jest dziedziczna


Przyja nie jest rwnie dziedziczona. Tak wic przyjaciel klasy bazowej nie jest automatycznie przyjacielem klasy pochodnej. Aby tak byo, klasa pochodna musi sama zadeklarowa swojego przyjaciela. Mona to uzasadni na przykad w ten sposb, e deklaracja przyjani nie jest skadnikiem klasy - tak jak metoda czy pole. Nie mona wic go odziedziczy. Inne wytumaczenie: deklaracja friend nie ma przypisanego specyfikatora dostpu (public, private), zatem nie wiadomo by byo, co z ni zrobi w procesie dziedziczenia; jak wiemy, skadniki private nie s dziedziczone, a pozostae owszem102. Dwie ostatnie uwagi moemy te uoglni do jednej: Klasa ma tylko tych przyjaci, ktrych sama sobie zadeklaruje.

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.

Wykorzystanie przyjani z funkcj


Do czego mog przyda si zaprzyjanione funkcje? Teoretycznie korzyci jest wiele, ale w praktyce na przd wysuwa si jedno gwne zastosowanie. To przecianie operatorw. O tym uytecznym mechanimie jzyka bdziemy mwi w dalszej czci tego rozdziau. Teraz mog powiedzie, e jest to sposb na zdefiniowanie wasnych dziaa podejmowanych w stosunku do klas, ktrych obiekty wystpuj w wyraeniach z operatorami: arytmetycznymi, bitowymi, logicznymi, i tak dalej. Precyzyjniej: chodzi o stworzenie funkcji, ktre zostan wykonywane na argumentach operatorw, bdcych naszymi klasami. Takie funkcje potrzebuj czsto dostpu do prywatnych skadnikw klas, na rzecz ktrych przeciamy operatory. Tutaj wanie przydaj si funkcje globalne, jako e zapewniaj taki dostp, a jednoczenie swobod definiowania kolejnoci argumentw operatora. Jeli nie bardzo to rozumiesz, nie przejmuj si. Przecianie operatorw jest w rzeczywistoci bardzo proste, a zaprzyjanione funkcji globalne upraszczaj to jeszcze bardziej. Wkrtce sam si o tym przekonasz.

Korzyci czerpane z przyjani klas


A co mona zyska zaprzyjaniajc klasy? Tutaj trudniej o konkretn odpowied. Wszystko zaley od tego, jak zaprojektujemy swj obiektowy program. Warto jednak wiedzie, e mamy tak wanie moliwo, jak zaprzyjanianie klas. Jak wszystkie z pozoru nieprzydatne rozwizania, okae si ona uyteczna w najmniej spodziewanych sytuacjach. *** T pocieszajc konkluzj zakoczylimy omawianie przyjani klas i funkcji w C++. Kolejnym elementem OOPu, na jakim skupimy swoj uwag, bd konstruktory. Ich rola w naszym ulubionym jzyka jest bowiem wcale niebagatelna i nieogranicza si tylko do inicjalizacji obiektw Zobaczmy sami.

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; }

};

Jak widzimy, nie podajemy tu adnej wartoci zwracanej.

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.

Kiedy wywoywany jest konstruktor


Popatrzmy teraz na sytuacje, w ktrych pracuje knnstruktor. Nie jest ich zbyt wiele, tylko kilka.

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++

inicjalizator moe mie jednak rn posta, w zalenoci od typu deklarowanej zmiennej.

Inicjalizacja typw podstawowych


W przypadku zmiennych typow elementarnych sprawa jest najprostsza. W inicjalizatorze podajemy po prostu odpowiedni warto, jaka zostanie przypisana temu typowi, np.: unsigned nZmienna = 42; float fZmienna = 10.5; Zauwamy, e bardzo czsto inicjalizacja zwizana jest niejawn konwersj wartoci do odpowiedniego typu. Tutaj na przykad 42 (typu int) zostanie zamienione na typ unsigned, za 10.5 (double) na typ float.

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.

Wywoanie konstruktora klasy bazowej


Lista inicjalizacyjna pozwala zrobi co jeszcze zanim waciwy konstruktor ruszy do pracy. Pozwala to nie tylko na inicjalizacj skadowych klasy, ktre tego wymagaj, ale take - a moe przede wszystkim - wywoanie konstruktorw klas bazowych. Przy pierwszym spotkaniu z dziedziczeniem mwiem, e klasa, ktra ma by dziedziczona, powinna posiada bezparametrowy konstruktor. Byo to spowodowane kolejnoci wywoywania konstruktorw: jak wiemy, najpierw pracuje ten z klasy bazowej (poczynajc od najstarszego pokolenia), a dopiero potem ten z klasy pochodnej. Kompilator musi wic wiedzie, jak wywoa konstruktor z klasy bazowej. Jeeli nie pomoemy mu w decyzji, to uprze si na konstruktor domyslny - bezparametrowy. Teraz bdziemy ju wiedzie, jak mona pomc kompilatorowi. Suy do tego wanie lista inicjalizacyjna. Oprcz inicjalizacji pl klasy moemy te wywoywa w niej konstruktory klas bazowych. W ten sposb zniknie konieczno posiadania przez nie konstruktora domylnego. Oto jak moe to wyglda: class CIndirectBase { protected: int m_nPole1;

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.

Gdy to nie wystarcza


Nie wszyscy obiekty podpadaj jednak pod ustanowion wyej kategori. Czy pamitasz moe klas CIntArray, ktr pokazaem, omawiajc wskaniki? Jeli nie, to spjrz jeszcze raz na jej definicj (usprawnion wykorzystaniem list inicjalizacyjnych): class CIntArray { // domylny rozmiar tablicy static const unsigned DOMYSLNY_ROZMIAR = 5; private: // wskanik na waciw tablic oraz jej rozmiar unsigned m_uRozmiar; int* m_pnTablica; 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) { 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++.

Do czego suy konstruktor kopiujcy


Konstruktor kopiujcy (ang. copy constructor) suy do tworzenia nowego obiektu danej klasy na podstawie ju istniejcego, innego obiektu tej klasy. Konstruktor ten, jak wszystkie konstruktory, wkracza do akcji podczas kreowania nowego obiektu klasy. Czym si w takim razie rni od zwykego konstruktora? Przypomnijmy dwie sporne linijki z poprzedniego paragrafu: CIntArray aTablica1; CIntArray aTablica2 = aTablica1; Pierwsza z nich to normalne stworzenie obiektu klasy CIntArray. Pracuje tu zwyky konstruktor, domylny zreszt. Natomiast druga linijka moe by take zapisana jako: CIntArray aTablica2 = CIntArray(aTablica1); albo nawet: CIntArray aTablica2(aTablica1);

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.

Konstruktor kopiujcy a przypisanie - rnica maa lecz wana


Moesz spyta: a co kompilator zrobi w takiej sytuacji: CIntArray aTablica1; CIntArray aTablica2; aTablica1 = aTablica2;

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

Dlaczego konstruktor kopiujcy


Ale w takim razie po co nam konstruktor kopiujcy? Przecie jego praca jest w wikszoci normalnych sytuacji rwnowana: wywoaniu zwykego konstruktora (czyli normalnemu stworzeniu obiektu) wywoaniu operatora przypisania Czy tak? C, niezupenie. W zasadzie zgadza si to tylko dla takich obiektw, dla ktrych wystarczajce jest kopiowanie pole po polu. Dla nich faktycznie nie potrzeba specjalnego konstruktora kopiujcego. Jeli jednak mamy do czynienia z tak klas, jak CIntArray, konstruktor taki jest konieczny. Sposb jego pracy bdzie si rni od zwykego przypisania - wemy choby pod uwag to, e konstruktor pracuje na pustym obiekcie, natomiast przypisanie oznacza zastpienie jednego obiektu drugim Dokadniej wyjanimy t spraw, gdy poznamy przecianie operatorw. Teraz zobaczmy, jak moemy zdefiniowa wasny konstruktor kopiujcy.

Definiowanie konstruktora kopiujcego


Skadni definicji konstruktora kopiujcego moemy zapisa tak: nazwa_klasy::nazwa_klasy([const] nazwa_klasy& obiekt)

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.

Inicjalizator klasy CIntArray


Gdy wiemy ju, do czego su konstruktory kopiujce i jak si je definiuje, moemy t wiedz wykorzysta. Zdefiniujmy inicjalizator dla klasy, ktra tak bardzo go potrzebuje CIntArray. Nie bdzie to trudne, jeeli zastanowimy si wpierw, co ten konstruktor ma robi. Ot powinien on zaalokowa pamie rwn rozmiarowi kopiowanej tablicy oraz przekopiowa z niej dane do nowego obiektu. Proste? Zatem do dziea: #include <memory.h> CIntArray::CIntArray(const CIntArray& aTablica) { // alokujemy pami m_uRozmiar = aTablica.m_uRozmiar; m_pnTablica = new int [m_uRozmiar]; // kopiujemy pami ze starej tablicy do nowej memcpy (m_pnTablica, aTablica.m_pnTablica, m_uRozmiar * sizeof(int));

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.

Sposoby dokonywania konwersji


Sprecyzujmy, o co nam waciwie chodzi. Ot chcemy, aby liczby cakowite (typu int) mogy by przez kompilator interpretowane jako obiekty naszej klasy CRational.

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.

Konstruktor z jednym obowizkowym parametrem


Konstruktor konwertujcy moe przyjmowa dokadnie jeden parametr okrelonego typu i wykonywa jego konwersj na typ swojej klasy. Jest to ten mechanizm, ktrego aktualnie potrzebujemy. Zdefiniujmy wic konstruktor konwertujcy w klasie CRational: CRational::CRational(int nLiczba) : m_nLicznik(nLiczba), m_nMianownik(1) Od tej pory wywoanie typu: CRational Wynik = Pomnoz(Raz, 5); albo nawet: CRational Wynik = Pomnoz(14, 5); jest cakowicie poprawne. Kompilator wie bowiem, w jaki sposb zamieni obiekt typu int na obiekt typu CRational. To samo osigna mona nawet prociej. Zasada jeden argument dla konstruktora konwertujcego dziaa tak samo jak brak argumentw dla konstruktora domylnego. A zatem dodatkowe argumenty mog by, lecz musz mie wartoci domylne. W naszej klasie moemy wic po prostu zmodyfikowa normalny konstruktor: CRational(int nLicznik, int nMianownik = 1) : m_nLicznik(nLicznik), m_nMianownik(nMianownik) { } { }

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

Definiowanie operatora konwersji


T now metod jest operator konwersji. Metod w sensie dosownym - musimy bowiem zdefiniowa go jako metod klasy CRational: CRational::operator float() { return m_nLicznik / static_cast<float>(m_nMianownik); } Oglnie wic funkcja w postaci: klasa::operator typ() { ciao_funkcji } definiuje sposb, w jaki dokonywna jest konwersja klasy do podanego typu. Zatem: Operatorw konwersji moemy uywa, aby zdefiniowa niejawn konwersj typu swojej klasy na inny, dowolny typ. Zyskujemy to, na czym nam zaleao. Odtd moemy swobodnie przekazywa liczby wymierne w tych miejscach, gdzie funkcje daj liczb rzeczywistych: CRational Liczba(3, 4); float fPierwiastek = sqrt(Liczba); Jest to zasuga operatorw konwersji. Operatory konwersji, w przeciwiestwie do konstruktorw, s dziedziczone i mog by metodami wirtualnymi.

Zaawansowana obiektowo

369

Wybr odpowiedniego sposobu


Mamy wic dwa sposoby konwersji typw. Nasuwa si pytanie: ktry wybra? Pytanie to jest zasadne, bowiem jeli w konwersji dwch typw uyjemy obu drg (konstruktor oraz operator), to powstanie wieloznaczno. Gdy kompilator bdzie zmuszony sign po konwersj, nie bdzie mg zdecydowa si na aden sposb i zaprotestuje. Aby odpowiedzie na to wane pytanie, przypomnijmy, jak dziaaj obie metody konwersji: konstruktor konwertujcy dokonuje zamiany innego typu w obiekt naszej klasy operator konwersji zamienia obiekt naszej klasy w obiekt innego typu

Schemat 38. Sposoby dokonywania niejawnych konwersji w C++

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.

Unarne operatory arytmetyczne


Mamy dwa podstawowe jednoargumentowe operatory arytmetyczne: operator zachowania znaku, czyli +. On praktycznie nie robi nic - zachowuje znak liczby, przy ktrej stoi. Obecny w C++ chyba tylko dla zgodnoci z zasadami matematyki operator zmiany znaku, czyli -. Zamienia liczb na przeciwn, zupenie tak jak w arytmetyce Troch przykadw: int nA = 7 int nB = +nA; int nB = -NA; // 7 // -7

Myl, e jest to na tyle oczywiste, e nie wymaga dalszych komentarzy.

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

Binarne operatory arytmetyczne


Przypomnijmy, e w C++ mamy pi takich operatorw, zwanych popularnie znakami dziaa: operator dodawania - plus (+). Dodaje dwie liczby do siebie operator odejmowania - minus (-). Zwraca wynik odejmowania drugiego argumentu od pierwszego operator mnoenia - gwiazdka (*). Mnoy oba argmenty operator dzielenia - slash (/). W zalenoci od typu swoich operandw moe albo wykonywa dzielenie cakowitoliczbowe (gdy oba argumenty s liczbami cakowitymi), albo zmiennoprzecinkowe operator reszty z dzielenia, czyli %. Zwraca reszt z dzielenia podanych liczb Znowu popatrzmy na kilka przykadw: int nA = 9, nB = 4, nX; float fX; nX nX nX nX fX nX = = = = = = nA nA nA nA nA nA + * / / % nB; nB; nB; nB; static_cast<float>(nB); nB; // // // // // // 13 5 36 2 2.25f 1

Ponownie nie ma tu nic nieoczekiwanego.

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++

Zwyky operator przypisania


Operator przypisania (ang. assignment operator) ma posta pojedynczego znaku rwna si (=). Doskonale te wiemy, jak si go uywa: int nX; nX = 7; Po wykonaniu tego kodu, zmienna nX bdzie mia warto 7.

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

Wyjtkiem s pola oznaczone sowem mutable, ktre zawsze mog by modyfkowane.

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.

Uwaga na przypisanie w miejscu rwnoci


Niestety, traktowanie przypisania jako wyraenia ma te swoj ciemn stron. Bardzo atwo jest umieci je omykowo w warunku if lub ptli zamiast operatora ==, np.: while (nA = 5) std::cin >> nA; Jeeli nasz kompilator jest lekkoduchem, to moe nas nie ostrzec przed niebezpieczestwem tej ptli. A zagroenie jest spore, bo jest nic innego, jak ptla nieskoczona, Podobno komputer Cray wykonaby j w dwie sekundy - jeeli chcesz, moesz sprawdzi, ile zajmie to twojej maszynie ;D Lepiej jednak zaradzi powstaemu problemowi. Jak on jednak powstaje? Ot sprawa jest do prosta, a wszystkiemu winien warunek ptli. Jest to przecie przypisanie - przypisanie wartoci 5 do zmiennej nA. Jako test logiczny wykorzystywana jest warto tego przypisania - czyli pitka. Pi jest oczywicie rne od zera, zatem zostanie uznane za warunek prawdziwy. Tak oto ptla si zaptla i zaciska na szyi biednego programisty. Moemy si koci, e to wina C++, ktry nie do, e uznaje liczby cakowite (jak 5) za wyraenia logiczne, to jeszcze pozwala na wykonywanie przypisania w warunkach ifw i ptli. Moliwoci te zostay jednak dopuszczone z uzasadnionych wzgldw (praca ze wskanikami), wic wcale niewykluczone, e kiedy je docenimy. Niezalenie od tego, czy

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!

Zoone operatory przypisania


Dla wygody programisty C++ posiada jeszcze dziesi innych operatorw przypisania. S one po prostu krtszym zapisem czsto stosowanych instrukcji. Ich posta i rozwinicia przedstawia to oto tabelka: przypisanie a += b a -= b a *= b a /= b a %= b a &= b a |= b a ^= b a <<= b a >>= b rozwinicie a = a + b a = a - b a = a * b a = a / b a = a % b a = a & b a = a | b a = a ^ b a = a << b a = a >> b

Tabela 17. Zoone operatory przypisania w C++

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

Dostp do pamici poprzez wskanik


Do obszaru pamici, do ktrego posiadamy wskanik, moemy odnie si na kilka sposobw. Dokadnie: na dwa.

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

Jeeli int ma 4 bajty dugoci, a tak jest na kadej platformie 32-bitowej.

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 { /* ... */ }; { /* ... */ };

void Funkcja(CFoo* pFoo) { CBar* pBar = dynamic_cast<CBar*>(pFoo); } // ...

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();

// int // class CFoo * // class CBar

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

Operatory dostpu do skadowych


Pi kolejnych operatorw suy do wybierania skadnikw klas, struktur, unii, itd. Przy ich pomocy mona wic dosta si do zagniedonych skadowych. Nie zawsze jest to jednak moliwe - wszystko zaley od ich widocznoci, czyli od tego, jakimi specyfikatorami dostpu s one opatrzone (private, protected, public). O tyche specyfikatorach mwilimy ju bardzo wiele, wic teraz przypomnijmy sobie tylko same operatory wyuskania.

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.

Nowe znaczenia dla operatorw


Przypomnielimy sobie wszystkie operatory C++ i ich domylne znaczenia. Nam to jednak nie wystarcza - chcemy przecie zdefiniowa dla nich cakiem nowe funkcje. Zobaczmy zatem, jak moemy to uczyni.

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.

Kilka uwag wstpnych


Zobaczmy wic, jak mona zdefiniowa dodatkowe znaczenia dla operatorw w C++.

Oglna skadnia funkcji operatorowej


Przecienie operatora oznacza napisanie dla niego funkcji, odpowiedzialnej za jego nowe dziaanie. Oto najbardziej oglna skadnia takiej funkcji: zwracany_typ operator symbol([parametry]) { tre_funkcji } Zamiast nazwy mamy tu sowo kluczowe operator, za ktrym naley poda symbol przecianego operatora (mona go oddzieli od spacj, lecz nie jest to wymagane). Jeeli wic chcemy np. zdefiniowia nowe znaczenie dla plusa (+), to piszemy funkcj operator+(). Jak kada funkcja, take i ta przyjmuje pewne parametry. Ich liczba zaley cile od tego, jaki operator chcemy przeadowa. Jeli jest to operator binarny, to si rzeczy konieczne bd dwa parametry; dla jednoargumentowych operatorw wystarczy jeden parametr. Ale uwaga - parametry podane w nawiasie niekoniecznie s jedynymi, ktre funkcja otrzymuje. Pamitasz zapewne, e metody klas maj ukryty parametr - obiekt, na rzecz ktrego metoda zostaa wywoana, dostpny poprzez wskanik this. Ot ten parametr jest brany pod uwag w tym przypadku. Pamitaj wic, e: Funkcja operatorowa przyjmuje tyle argumentw, ile ma przeciany przy jej pomocy operator. Do tych argumentw zalicza si wskanik this, jeeli funkcja operatorowa jest metod klasy. Od tej zasady istnieje tylko jeden wyjtek (a w zasadzie dwa). Stanowi go operatory postinkrementacji i postdekrementacji: wprowadzono do nich dodatkowy parametr typu int, ktry naley zignorowa. Dziki temu moliwe jest odrnienie tych operatorw od wariantw prefiksowych.

Operatory, ktre moemy przecia


Moemy przecia bardzo wiele operatorw - zarwno takich, dla ktrych natychmiast znajdziemy praktyczne zastosowanie, jak i tych, ktrych przecianie wydawaoby si dziwaczne. Oto kompletna lista przecialnych operatorw: + - * / % & | ^ << << ~ && || ! == != < <= > >= += -= *= /= %= &= |= ^= <<= >>= ++ -- = -> ->* () [] new delete ,
Tabela 18. Przecialne operatory C++

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)

Czego nie moemy zmieni


Przeciajc operatory moemy zdefiniowa dla nich dodatkowe znaczenie. Nie moemy jednak: tworzy wasnych operatorw, jak np. @, ?, === czy \ zmieni liczby argumentw, na ktrych pracuj przeciane operatory. Przykadowo, nie stworzymy dwuargumentowego operatora ! czy te jednoargumentowego || zmodyfikowa priorytetu operatora zmieni cznoci przeadowanego operatora Dla kadego typu C++ automatycznie generuje te pi niezbdnych operatorw, ktrych nie musimy przecia, aby dziaay poprawnie, S to: zwyky operator przypisania (=). Dokonuje on dosownego kopiowania obiektu (pole po polu) operator pobrania adresu (jednoargumentowy &). Zwraca on adres obiektu w pamici new dokonuje alokacji pamici dla obiektu delete niszczy i usuwa obiekt z pamici przecinek (,) - jego znaczenie jest takie same, jak dla typw wbudowanych Moliwe jest aczkolwiek przecienie tych piciu symboli, aby dziaay inaczej dla naszych klas. Nie mona jednak uniewani ich domylnej funkcjonalnoci, jak dostarcza kompilator dla kadego typu. Mwic potocznie, nie mona ich rozdefiniowa.

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

Definiowanie przecionych wersji operatorw


Operator moemy przeciy na kilka sposobw, w zalenoci od tego, gdzie umiecimy funkcj operatorow. Moe by ona bowiem zarwno skadnikiem (metod) klasy, na rzecz ktrej dziaa, jak i funkcj globaln.

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.

Operator jako funkcja skadowa klasy


Wpierw sprbujmy zdefiniowa operator*() jako funkcj skadow klasy. Wiemy, e nasz operator jest dwuargumentowy; wiemy take, e kada metoda klasy przyjmuje jeden ukryty parametr - wskanik this. Wynika std, e funkcja operatorowa bdzie u nas mia tylko jeden prawdziwy parametr i wygldaa na przykad tak: CRational CRational::operator*(const CRational& Liczba) const { return CRational(m_nLicznik * Liczba.m_nLicznik, m_nMianownik * Liczba.m_nMianownik); } To wystarczy - po tym zabiegu moemy bez problemu mnoy przez siebie zarwno dwa obiekty klasy CRational: CRational DwieTrzecie(2, 3), TrzySiodme(3, 7); CRational Wynik = DwieTrzecie * TrzySiodme; jak i jeden obiekt przez liczb cakowit: CRational Polowa(1, 2); CRational Calosc = Polowa * 2; Jak to dziaa? Najlepiej przeledzi funkcjonowanie operatora, jeeli wyraenia zawierajce go: DwieTrzecie * TrzySiodme Polowa * 2 zapiszemy z jawnie wywoan funkcj operatorow: DwieTrzecie.operator*(TrzySiodme) Polowa.operator*(2) Wida wyranie, e pierwszy argument operatora jest przekazywany jako wskanik this. Drugi jest natomiast normalnym parametrem funkcji operator*(). A jakim sposobem zyskalimy od razu moliwo mnoenia take przez liczby cakowite? Myl, e to nietrudne. Zadziaaa tu po prostu niejawna konwersja, zrealizowana przy pomocy konstruktora klasy CRational. Drugie wyraenie jest wic w rzeczywistoci wywoaniem: Polowa.operator*(CRational(2)) Mimochodem uzyskalimy zatem dodatkow funkcjonalno. A wszystko za pomoc jednej funkcji operatorowej (no i jednego konstruktora).

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

Operator jako zwyka funkcja globalna


Wynika z tego prosty wniosek: Houston, mamy problem :) Nie rozwiemy go na pewno, definiujc operator*() jako funkcj skadow klasy. Trzebaby bowiem dosta si do definicji klasy int i doda do niej odpowiedni metod. Szkoda tylko, e nie mamy dostpu do tej definicji, co zreszt nie zaskakuje, bo int nie jest przecie adn klas. Gdyby jednak zaoga Apollo 13 zaamywaa si po napotkaniu tak prostych problemw, nie wrciaby na Ziemi caa i zdrowa. Nasza sytuacja nie jest a tak dramatyczna, chocia czciowo przemienny operator mnoenia te nie jest szczytem komfortu. Trzeba co na to poradzi. Rozwizanie oczywicie istnieje: naley uczyni operator*() funkcj globaln: CRational operator*(const CRational& Liczba1, const CRational& Liczba2) { return CRational(Liczba1.Licznik() * Liczba2.Licznik(), Liczba1.Mianownik() * Liczba2.Mianownik()); } Zmieni to bardzo wiele. Odtd dwa rozwaane wyraenia bd rozwijane do postaci: operator*(TrzySiodme, DwieTrzecie) operator*(2, Polowa) // OK // te 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.

Operator jako zaprzyjaniona funkcja globalna


Porwnajmy jeszcze tre obu wariantw funkcji operator*(): jako metody klasy CRational i jako funkcji globalnej. Widzimy, e w pierwszym przypadku operowaa ona bezporednio na prywatnych polach m_nLicznik i m_nMianownik. Jako funkcja globalna musiaa z kolei posikowa si metodami dostpowymi - Licznik() i Mianownik(). Nie powinno ci to dziwi. operator*() jako zwyka funkcja globalna jest wanie zwyk funkcj globaln, zatem nie ma adnych specjalnych uprawnie w stosunku do klasy CRational. Jest tak nawet pomimo faktu, e definiuje dla operacj mnoenia. adne specjalne uprawnienia nie s potrzebne, bo funkcja doskonale radzi sobie bez nich. Czasem jednak operator potrzebuje dostpu do niepublicznych skadowych klasy, ktrych nie uzyska za pomoc publicznego interfejsu. W takiej sytuacji konieczne staje si uczynienie funkcji operatorowej zaprzyjanion. Podkrelmy jeszcze raz: Globalna funkcja operatorowa nie musi by zaprzyjaniona z klas, na rzecz ktrej definiuje znaczenie operatora. Ten fakt pozwala na przecianie operatorw take dla nieswoich klas. Jak bardzo moe to by przydatne, zobaczymy przy okazji omawiania strumieni STL z Biblioteki Standardowej.

Sposoby przeciania operatorw


Po generalnym zapoznaniu si z przecianiem operatorw, czas na konkretne przykady. Dowiedzmy si wic, jak przecia poszczeglne typy operatorw.

Najczciej stosowane przecienia


Najpierw poznamy takie rodzaje przecionych operatorw, ktre stosuje si najczciej. Pomoc bdzie nam tu gwnie suy klasa CVector2D, ktr jaki czas temu pokazaem: class CVector2D { private: float m_fX, m_fY; public: explicit CVector2D(float fX = 0.0f, float fY = 0.0f) { m_fX = fX; m_fY = fY; }

};

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.

Typowe operatory jednoargumentowe


Operatory unarne, jak sama nazwa wskazuje, przyjmuj jeden argument. Chcc dokona ich przecienia, mamy do wyboru: zdefiniowanie odpowiedniej metody w klasie, na rzecz ktrej dokonujemy redefinicji: klasa klasa::operator symbol() const; napisanie globalnej funkcji operatorowej: klasa operator symbol(const klasa&); Zauwaye zapewne, e w obu wzorach podaj parametry oraz typ zwracanej wartoci108. Przestrzeganie tego schematu nie jest jednak wymogiem jzyka, lecz raczej powszechnie przyjtej konwencji dotyczcej przeciania operatorw. Mwi ona, e: Dziaanie operatorw wobec typw zdefiniowanych przez programist powinno w miar moliwoci pokrywa si z ich funkcjonalnoci dla typw wbudowanych. Co to znaczy? Ot wikszo operatorw jednoargumentowych (poza in/dekrementacj) nie modyfikuje w aden sposb przekazanych im obiektw. Przykadowo, operator jednoargumentowego minusa - zastosowany wobec liczby zwraca po prostu liczb przeciwn. Chcc zachowa t konwencj, naley umieci w odpowiednich miejscach deklaracje staoci const. Naturalnie nie trzeba tego bezwarunkowo robi - pamitajmy jednak, e przestrzeganie szeroko przyjtych (i rozsdnych!) zwyczajw jest zawsze w interesie programisty. Dotyczy to zarwno piszcego, jak i czytajcego i konserwujcego kod. No, ale do tych tyrad. Pora na zastosowanie zdobytej wiedzy w praktyce. Zastanwmy si, jakie operatory moemy logicznie przeciy dla naszej klasy CVector2D. Nie jest ich wiele - w zasadzie tylko plus (+) oraz minus (-). Pierwszy nie powinien w ogle zmienia obiektu wektora i zwrci go w nienaruszonym stanie, za drugi musi odda wektor o przeciwnym zwrocie. Sdz, e bez problemu napisaby takie funkcje. S one przecie niezwykle proste: class CVector2D { // (pomijamy szczegy) public: // (tu te)

108

Nie dotyczy to operatorw inkrementacji i dekrementacji, ktrych omwienie znajduje si dalej.

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; }

{ ++m_fX; ++m_fY; return *this; }

// (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.

Typowe operatory dwuargumentowe


Operatory dwuargumentowe, czyli binarne, przyjmuj po argumenty. Powiedzmy sobie od razu, e nie musz by to operandy tych samych typw. Wobec tego nie ma czego takiego, jak oglna skadnia prototypu funkcji operatora binarnego. Ponownie jednak moemy mie do czynienia z dwoma drogami implementacji takiej funkcji: jako metody jednej z klas, na obiektach ktrej pracuje operator. Jego jawne wywoanie wyglda wwczas tak: operand1.operator symbol(operand2) jako funkcji globalnej - zaprzyjanionej bd nie: operator symbol(operand1, operand2) Obie linijki zastpuj normalne uycie operatora w formie: operand1 symbol operand2 O tym, ktra moliwo przeciania jest lepsza, wspominaem ju na pocztku. Przy wyborze najwiksz rol odgrywaj ewentualne niejawne konwersje - jeeli chcemy, by kompilator takowych dokonywa. W bardzo uproszczonej formie mona powiedzie, e jeli jednym z argumentw ma by typ wbudowany, to funkcja operatorowa jest dobrym kandydatem na globaln (z przyjani bd nie, zalenie od potrzeb). W innym przypadku moemy pozosta przy metodzie klasy - lub kierowa si innymi przesankami, jak w poniszych przykadach Celem ujrzenia tych przykadw wrmy do naszego wektora. Jak wiemy, na wektorach w matematyce moemy dokonywa mnstwa operacji. Nie wszystkie nas interesuj, wic tutaj zaimplementujemy sobie tylko: dodawanie i odejmowanie wektorw mnoenie i dzielenie wektora przez liczb iloczyn skalarny

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;

};

Definiowanie operatorw binarnych jest wic bardzo proste, czy nie? :D

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.

Ciekawostka: operator ->*


Drugi z operatorw wyuskania, ->*, jest bardzo rzadko uywany. Nie dziwi wic, e sytuacje, w ktrych jest on przeciany, s wrcz sporadyczne. Niemniej, skoro ju mwimy o przecianiu, to moemy wspomnie take o nim. Wpierw przydaoby si aczkolwiek, aby zna mechanizm wskanikw na skadowe klasy, opisany w nastpnym podrozdziale. ->* jest uywany do wybierania skadnikw obiektu poprzez wskaniki do skadowych. Podobnie jak ->, nie ma on predefiniowanego znaczenia dla zmiennych obiektowych, a jedynie dla wskanikw na obiekty. Na tym jednak podobiestwa si kocz. ->* jest przeciany jako operator binarny dla konkretnego zestawu dwch danych, ktre stanowi: referencja do obiektu (argument lewostronny) wskanik do skadowej klasy (argument prawostronny) Nie ma te wymogu, aby funkcja operator->*() bya funkcj skadow klasy. Moe by rwnie dobrze funkcj globaln. Jak wic przeciy ten operator? Poniewa, jak mwiem, definiujemy go dla konkretnego typu skadnika, posta prototypu funkcji operator->*() rni si dla wskanikw do pl oraz do metod klasy.

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.

Operator wywoania funkcji


Czas na kolejny operator, chyba jeden z bardziej interesujcych. To operator wywoania funkcji (ang. function-call operator), czyli nawiasy okrge (). Nawiasy maj jeszcze dwa znaczenia w C++: grupuj one wyraenia oraz pozwalaj wykonywa rzutowanie (w stylu C lub funkcyjnym). adnego z tych pozostaych znacze nie moemy jednak zmienia. Przecieniu moe ulec tylko operator wywoania funkcji. Tak jest, on take moe by przeciony. O czym w tym przypadku naley pamita? Ot: Operator wywoania funkcji moe by zdefiniowany tylko jako niestatyczna funkcja skadowa klasy. Jest to ostatni rodzaj operator, ktrego dotyczy to ograniczenie. Przypominam, e pozostaymi s: operatory przypisania, indeksowania oraz wyuskania (->). Na tym zastrzeeniu kocz si jednak jakiegolwiek obostrzeenia nakadane na to przecienie. operator()() (tak, dwie pary nawiasw) moe by bowiem funkcj przyjmujc dowolne argumenty i zwracajc dowolny typ wartoci: zwracany_typ klasa::operator()([parametry]); To jedyny operator, ktry moe przyjmowa kad ilo argumentw. To zreszt zrozumiae: skoro normalnie suy on do wywoywania funkcji, mogcych mie przecie dowoln liczb parametrw, to i jego przeciona wersja nie powinna nakada ogranicze w tym zakresie. Podobnie dzieje si, jeeli chodzi o typ zwracanej wartoci.

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.

Operatory zarzdzania pamici


Oto kolejne dwa wyjtkowe operatory: new i delete. Jak doskonale wiemy, su one do dynamicznego tworzenia w pamici operacyjnej (a dokadniej na stercie) zmiennych, tablic i obiektw. To moe wydawa si niemal niesamowite, ale je take moemy przeadowa! Wpierw jednak musz przypomnie, e praca tych operatorw nie ogranicza si w rzeczywistoci tylko do przydzielenia pamici (new) i jej zwolnienia (delete). Jestemy wiadomi, e moe za tym i take zainicjowanie lub sprztniecie alokowanego obszaru pamici. Oznacza to na przykad wywoanie konstruktora (new) i destruktora (delete) klasy, ktrej obiekt tworzymy. Widzimy wic, e oba operatory wykonuj wicej ni jedn czynno. Zmodyfikowa moemy jednak tylko jedn z nich: Przecione operatory new i delete mog jedynie zmieni sposb alokowania i zwalniania pamici. Nie mona ingerowa w inicjalizacj (wywoanie konstruktorw) i sprztanie (przywoanie destruktorw), ktre temu towarzysz. Zauwamy, e fakt ten niweluje dla nas rnice midzy operatorem new a new[] oraz delete i delete[]. Na poziomie alokacji (zwalniania) pamici niczym si one bowiem nie rni. Dlatego te dla potrzeb przeciania mwimy tylko o operatorach new i delete, majc jednak w pamici t uwag. Czy to, e kontrolujemy jedynie zarzdzanie pamici znaczy, e przecianie tych operatorw nie jest interesujce? Przeciwnie - alokacja i zwalnianie pamici to s wanie te czynnoci, ktre najbardziej nas interesuj. Napisanie wasnego algorytmu ich wykonywania, albo chocia ledzenia tych standardowych, jest podstaw dziaania tak zwanych menederw pamici (ang. memory managers). S to mechanizmy zajmujce si kontrol wykorzystania pamici operacyjnej, zapobiegajce zwykle jej wyciekom i czsto optymalizujce program. Stworzenie dobrego menedera pamici nie jest oczywicie proste, jednak przecienie new i delete to bardzo atwa czynno. Aby j wykona, spjrzmy na prototypy obu funkcji - operator new() i operator delete(): void* [klasa::]operator new(size_t); void [klasa::]operator delete(void*); To nie pomyka: funkcje te maj cile okrelone listy parametrw oraz typy zwracanych wartoci. W tym wzgldzie jest to wyjtek wrd wszystkich operatorw. operator new() przyjmuje jeden parametr typu size_t - jest to ilo bajtw, jaka ma by zaalokowana. W zamian powinien on zwrci void* - jak mona si domyla: wskanik do przydzielonego obszaru pamici o danym rozmiarze.

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.

Lokalne wersje operatorw


Operatory new i delete moemy przeciy w stosunku do pojedynczej klasy. W takiej sytuacji bd one uywane do alokowania i (lub) zwalniania pamici dla obiektw wycznie tej klasy. Moe to si przyda np. do zapobiegania fragmentacji pamici, spowodowanej czstym tworzeniem i zwalnianiem maych obiektw. W takim przypadku operator new moe zarzdza wikszym kawakiem pamici i wirtualnie odcina z niego mniejsze fragmenty dla kolejnych obiektw. delete dokonywaby wtedy tylko pozornej dealokacji pamici. Zobaczmy zatem, jak odbywa si przeadowanie lokalnych operatorw new i delete. Oto prosty przykad, korzystajcy w zasadzie ze standardowych sposb przydzielania i oddawania pamici, ale jednoczenie wypisujcy informacje o tych czynnociach: class CFoo { public: // 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;

};

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.

Wskazwki dla pocztkujcego przeciacza


Przecianie operatorw jest wspania moliwoci jzyka C++. Nie ma jednak adnego przymusu stosowania jej - do powiedzie, e do tej pory wietnie radzilimy sobie bez niej. Nie ma aczkolwiek powodu, aby j cakiem odrzuca - trzeba tylko nauczy si j waciwie wykorzystywa. Temu wanie suy ten paragraf.

Zachowujmy sens, logik i konwencj


Jakkolwiek jzyk C++ jest znany ze swej elastycznoci, przez lata jego uytkowania wypracowano wiele regu, dzcych midzy innymi dziaaniem operatorw. Chcc przecia operatory dla wasnych klas, naleaoby ich w miar moliwoci przestrzega zwaszcza, e czsto s one zbiene ze zdrowym rozsdkiem. Podczas przeadowania operatorw trzeba po prostu zachowa ich pierwotny sens. Jak to zrobi?

Symbole operatorw powinny odpowiada ich znaczeniom


W pierwszej kolejnoci naley powstrzyma si od radosnej twrczoci, sprzecznej z wszelk logik. Moe i zabawne bdzie uycie operatora == jako symbolu dodawania, ^ w charakterze operatora mnoenia i & jako znaku odejmowania. Pomyl jednak, co w takiej sytuacji oznacza bdzie zapis: if (Foo ^ Bar & (Baz == Qux) == Thud)

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

Zapewnijmy analogiczne zachowania jak dla typw wbudowanych


Wszystkie operatory posiadaj ju jakie zdefiniowane dziaanie dla typw wbudowanych. Dla naszych klas moe ono cakiem rni si od tego pocztkowego, ale dobrze byoby, aby przynajmniej zalenoci midzy poszczeglnymi operatorami zostay zachowane. Co to znaczy? Zauwamy na przykad, e trzy ponisze instrukcje: int nA; // o te nA = nA + 1; nA += 1; nA++; dla typu int (i dla wszystkich podstawowych typw) s w przyblieniu rwnowane. Dobrze byoby, aby dla naszych przeadowanych operatorw te tosamoci zostay zachowane. Podobnie jest dla typw wskanikowych: CFoo* pFoo = new CFoo; // instrukcje robice to samo pFoo->Metoda(); (*pFoo).Metoda(); // ewentualnie jeszcze pFoo[0].Metoda() delete pFoo; Jeli tworzymy klasy inteligentnych wskanikw, naleaoby wobec tego przeciy dla nich operatory ->, * i ewentualnie [] (a take operator bool(), aby mona je byo stosowa w wyraeniach warunkowych).

Nie przeciajmy wszystkiego


Na koniec jeszcze jedna, oczywista uwaga: nie ma sensu przecia wszystkich operatorw - przynajmniej do chwili, gdy nie piszemy klasy symulujcej wszystkie typy w C++. Jeeli mimo wszystko wykonamy t niepotrzebn zwykle prac i udostpnimy nasz piknie opakowan klas innym programistom, najprawdopodobniej zignoruj oni te przecienia, ktre nie bd miay dla nich sensu. A jeli sami uywa bdziemy takiej klasy, to zapewne szybko sami przekonamy si, e uporczywe uywanie operatorw nie ma zbytniego sensu. Drog naturalnej selekcji w obu przypadkach zostan wic w uyciu tylko te operatory, ktre s naprawd potrzebne. Nie powinnimy jednak czeka, a ycie zweryfikuje nasze przypuszczenia, bo przeciajc niepotrzebnie operatory, stracimy mnstwo czasu. Lepiej wic od razu zastanowi si, co warto przeadowa, a czego nie. Kierujmy si w tym jedn, prost zasad: Symbol operatora powinien kojarzy si z czynnoci przez niego wykonywan. Zastosowanie si do tej reguy likwiduje zazwyczaj wikszo niepewnoci. *** Zakoczylimy w ten sposb poznawanie przydatnej techniki programowania, jak jest przecianie operatorw dla naszych wasnych klas.

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.

Wskaniki do skadowych klasy


W ostatnim rozdziale czci pierwszej poznalimy zwyke wskaniki jzyka C: pokazujce na zmienne oraz na funkcje. Tutaj zajmiemy si pewn nowoci, jak do wskanikw wprowadzio programowanie obiektowe: wskanikami do skadowych (ang. pointersto-members). Ten podrozdzia nie jest niezbdny do kontynuowania nauki jzyka C++. Jeeli stwierdzisz, e jest ci na razie niepotrzebny lub za trudny, moesz go opuci. Zalecam to szczeglnie przy pierwszym czytaniu kursu. Podobnie jak dla normalnych wskanikw, wskaniki na skadowe take mog odnosi si do danych (pl) oraz do kodu (metod). Omwimy sobie osobno kady z tych rodzajw wskanikw.

Wskanik na pole klasy


Wskaniki na pola klas s obiektowym odpowiednikiem zwykych wskanikw na zmienne, jakie doskonale znamy. Funkcjonuj one jednak nieco inaczej. Jak? O tym traktuje niniejsza sekcja.

Wskanik do pola wewntrz obiektu


Przypomnijmy, jak wyglda zwyky wskanik - na przykad na typ int: int nZmienna; int* pnZmienna = &nZmienna; Zadeklarowany tu wskanik pnZmienna zosta ustawiony na adres zmiennej nZmienna. Wobec tego ponisza linijka: *pnZmienna = 14; spowoduje przypisanie liczby 14 do nZmienna. Stanie si to za porednictwem wskanika.

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.

Pokazujemy na skadnik obiektu


Czas wic na nowo. Pytanie brzmi: czy zwykym wskanikiem mona odnie si do pola we wntrzu obiektu? A owszem. Wystarczy pomyle, e wyraenie: Foo.nSkladnik jest l-wartoci typu int, zatem mona pobra jej adres zapisa we wskaniku typu int*: int* pnSkladnikFoo = &(Foo.nSkladnik); Powiedzmy jeszcze wyranie, co tu zrobilimy. Ot pobralimy adres konkretnego pola (nSkladnik) w konkretnym obiekcie (Foo). Jest to najzupeniej moliwe, bo przecie obiekt reprezentuj w pamici jego pola. Skoro za moemy odnie si do obiektu jako caoci, to moemy take pobra adres jego pl. Jeli teraz wypiszemy wartoc pola przy pomocy tego wskanika: std::cout << *pnSkladnikFoo; to zobaczymy oczywicie 76, jako e nic nie zmienilimy od poprzedniego akapitu. Musz jeszcze powiedzie, e manewr z pobraniem adresu pola w obiekcie powiedzie si tylko wtedy, jeeli to pole jest publiczne. W innej sytuacji wyraenie Foo.nSkladnik zostanie odrzucone przez kompilator. Zawsze mona aczkolwiek pobiera adresy pl wewntrz klasy (np. w jej metodach) oraz w funkcjach i klasach zaprzyjanionych. Te obszary kodu maj bowiem dostp do wszystkich skadnikw - take niepublicznych i mog z nimi robi cokolwiek: na przykad pobiera ich adresy w pamici.

Wskanik do pola wewntrz klasy


Kontynuujemy nasz zabaw. Teraz wemy pod lup troch inn klas, z ktr ju mnstwo razy si spotykalimy - wektor: struct VECTOR3 { float x, y, z; }; Formalnie jest to struktura, ale jak wiemy, w C++ rnica midzy struktur a klas jest drobnostk i sprowadza si do domylnej widocznoci skadnikw. Dla swka struct jest to public, wic nasze trzy pola s tu publiczne bez koniecznoci jawnego okrelania tego faktu. Majc klas (albo struktur - jak kto woli) z trzema polami moemy j naturalnie instancjowa (czyli stworzy jej obiekt):

416
VECTOR3 Wektor;

Zaawansowane C++

Nastpnie moemy te pobra adres jej pola - ktrej ze wsprzdnych: float* pfX = &Wektor.x;

Miejsce pola w definicji klasy


Przyjrzyjmy si jednak definicji klasy. Mamy w niej trzy takie same pola, nastpujce jedno po drugim. Pierwsze (x), drugie (y) i trzecie (z) Jeeli ci to pomoe, moesz nawet wyobrazi sobie nasz wektor jako trjelementow tablic, w ktrej nazwalimy poszczeglne elementy (pola). Zamiast odwoywa si do nich poprzez indeksy, potrafimy posuy si ich nazwami (x, y, z). Porwnanie z tablic jest jednak cakiem trafne - choby dlatego, e nasze pola s uoone w pamici w kolejnoci wystpowania w definicji klasy. Najpierw mamy wic x, potem y, a dalej z. Polu x moemy wic przypisa indeks 0, y - 1, a dla z indeks 2. Sowo indeks bior tu w cudzysw, bo jest to tylko takie pojcie pomocnicze. Wiesz, e w przypadku tablic indeksy s ostatecznie zamieniane na wskaniki w ten sposb, e do adresu caej tablicy (czyli jej pierwszego elementu) dodawany jest indeks: int* aTablica[5]; // te dwie linijki s rwnowane aTablica[3] = 12; *(aTablica + 3) = 12; Dodawanie, jakie wystpuje w ostatnim wierwszu, nie jest dosownym dodaniem trzech bajtw do wskanika aTablica, jest przesuniciem si o trzy elementy. Waciwie wic kompilator zamienia to na:
aTablica + 3 * sizeof(int)

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.

Deklaracja wskanika na pole klasy


No dobrze, pobranie wskanika to jedno, ale jego zapisanie i wykorzystanie to zupenie co innego. Najpierw wic dowiedzmy si, jak mona zachowa warto uzyskan wyraeniem &VECTOR3::y do pniejszego wykorzystania. By moe domylasz si, e bdzie potrzebowali specjalnej zmiennej typu wskanikowego - czyli wskanika na pole klasy. Aby go zadeklarowa, musimy przypomnie sobie, czym charakteryzuj si wskaniki w C++. Nie jest to trudne. Kady wskanik ma swj typ: w przypadku wskanikw na zmienne by to po prostu typ docelowej zmiennej. Dla wskanikw na funkcje sprawa bya bardziej skomplikowana, niemniej te miay one swoje typy. Podobnie jest ze wskanikami na skadowe klasy. Kady z nich ma przypisan klas, na ktre skadniki pokazuje - dotyczy to zarwno odniesie do pl, ktrymi zajmujemy si teraz, jak i do metod, ktre poznamy za chwil. Oprcz tego wskanik na pole klasy musi te zna typ docelowego pola, czyli wiedzie, jaki rodzaj danych jest w nim przechowywany. Czy wiemy to wszystko? Tak. Wiemy, e nasz klas jest VECTOR3. Pamitamy te, e jej wszystkie pola zadeklarowalimy jako float. Korzystajc z tej informacji, moemy zadeklarowa wskanik na pola typu float w klasie VECTOR3: float VECTOR3::*p2mfWspolrzedna; Huh, co za zakrcona deklaracja Gdzie tu jest w ogle nazwa tej zmiennej? Spokojnie, nie jest to a takie straszne - to tylko tak wyglda :) Nasz wskanik nazywa si oczywicie p2mfWspolrzedna112, za niezbyt przyjazna forma deklaracji stanie si janiejsza, jeeli popatrzymy na jej ogln skadni: typ klasa::*wskanik; Co to jest? Ot jest to deklaracja wskanika, pokazujcego na pola podanego typu, znajdujce si we wntrzu okrelonej klasy. Nic prostrzego, prawda? ;-) Teraz, kiedy mamy ju zmienn odpowiedniego typu wskanikowego, moemy przypisa jej wzgldny adres pola y w klasie VECTOR3: p2mfWspolrzedna = &VECTOR3::y; Pamitajmy, e w ten sposb nie pokazujemy na konkretn wsprzdn Y (pole y) w konkretnym wektorze (obiekcie VECTOR3), lecz na miejsce pola w definicji klasy. Pojedynczo taki wskanik nie jest wic uyteczny, bo jego wartoc nabiera znaczenia dopiero w momencie zastosowania jej dla konkretnego obiektu. Jak to zrobi zobaczymy w nastpnym akapicie. Zwrmy jeszcze uwage, e y nie jest jedynym polem typu float w klasie VECTOR3. Z rwnym powodzeniem moemy pokazywa naszym wskanikiem take na pozostae: p2mfWspolrzedna = &VECTOR3::x;
112

p2mf to skrt od pointer-to-member float.

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.

Wskanik na metod klasy


Normalne wskaniki mog te pokazywa na kod, czyli funkcje. Obiektowym odpowiednikiem tego faktu s wskaniki do metod klasy. Zajmiemy si nimi w tej sekcji.

Wskanik do statycznej metody klasy


Zwyczajny wskanik do funkcji globalnej deklarujemy np. tak: int (*pfnFunkcja)(float); Przypominam, e aby odczyta deklaracj funkcji pasujcych do tego wskanika, wystarczy usun gwiazdk oraz nawiasy otaczajce jego nazw. Tutaj wic moemy do wskanika pfnFunkcja przypisa adresy wszystkich funkcji globalnych, ktre przyjmuj jeden parametr typu float i zwracaj liczb typu int: int Foo(float) // ... pfnFunkcja = Foo; // albo pfnFunkcja = &Foo; { /* ... */ }

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

Wskanik do niestatycznej metody klasy


A jak jest z metodami niestatycznymi? Czy na nie te moemy pokazywa zwykymi wskanikami? Niestety nie. Fakt ten moe si wyda zaskakujcy, ale mona go wyjani nawet na kilka sposobw. Po pierwsze: wspomniaem ju, e metody niestatyczne korzystaj ze specjalnej konwencji thiscall. Oprcz normalnych parametrw musza one bowiem dosta obiekt, ktry w ich wntrzu bdzie reprezentowany przez wskanik this. C++ nie pozwala na zadeklarowanie funkcji uywajcych konwencji thiscall - nie bardzo wiadomo, jak taka deklaracja miaaby wyglda113. Po drugie: metody niestatyczne potrzebuj wskanika this. Gdyby dopuci do sytuacji, w ktrej wskaniki na funkcje mog pokazywa na metody, wwczas trzebaby byo zapewni jako dostarczenie tego wskanika this (czyli obiektu, na rzecz ktrego metoda jest wywoywana). Jak? Poprzez dodatkowy parametr? Wtedy mielibymy koszmarn nieciso skadni: deklaracje wskanikw do funkcji nie zgadzayby si z prototypami pasujcych do nich metod. Nawet jeli nie bardzo zrozumiae te argumenty, musisz przyj, e na niestatyczne metody klasy nie pokazujemy zwykymi wskanikami do funkcji. Zamiast tego wykorzystujemy drugi rodzaj wskanikw na skadowe klasy.

Wykorzystanie wskanikw na metody


Mam tu na myli wskaniki na metody klas. Wskanik do metody w klasie (ang. pointer-to-member function) okrela miejsce deklaracji tej metody w definicji klasy. Wida tu analogie ze wskanikami do pl klasy. Tutaj take okrelamy umiejscowienie danej metody wzgldem No wanie - wzgldem czego?! W przypadku pl moglimy jeszcze mwi, e wskanik jest okreleniem przesunicia (offsetu), ktry pozwala znale pole danego obiektu, gdy mamy adres pocztku tego obiektu. Ale przecie metody nie podlegaj tym zasadom. Dla wszystkich obiektw mamy przecie jeden zestaw metod. Jak wic mona mwi o tym, e wskaniki na nie dziaaj w ten sam sposb? Ekhm, tego raczej nie powiedziaem. Wskaniki te mog dziaa ten sam sposb, czyli by adresami wzgldnymi. Mog one take by adresami bezwzgldnymi (w sumie dlaczego nie? Przecie metody to te funkcje), a nawet indeksami jakiej wewntrznej tablicy czy jeszcze dziwniejszymi liczbami z gatunku identyfikatorw-uchwytw. Tak naprawd nie powinno nas to interesowa, gdy jest to wewntrzna sprawa kompilatora. Dla nas wskaniki te pokazuj po prostu na jak metod wewntrz danej klasy. Jak to robi - to ju nie nasze zmartwienie.

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

p2mfn to skrt od pointer-to-member function.

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

Pobranie wskanika na metod klasy


Kiedy mamy ju zadeklarowany waciwy wskanik, powimy go z ktr z metod klasy CNumber. Robimy to w prosty i raczej przewidywalny sposb: p2mfnMetoda = &CNumber::Dodaj; Podobnie jak dla zwykych funkcji, take i tutaj operator & nie jest niezbdny: p2mfnMetoda = CNumber::Odejmij; Znowu te stosuje si tu zasada o publicznoci skadowych. Jeeli sprbujemy pobra wskanik na metod prywatn lub chronion, to kompilator oczywicie zaprotestuje.

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.

Ciekawostka: wskanik do metody obiektu


Zatrzymajmy si na chwilk Jeeli przebrne przed ten rozdzia od pocztku a dotd, to szczerze ci gratuluj. Wskaniki na skadowe nie s bynajmniej atw czci jzyka choby dlatego, e operuj do dziwnymi koncepcjami (miejsce w definicji klasy). Co gorsza, czytajc o nich jako trudno od razu wpa na sensowne zastosowanie tego mechanizmu. Wiem, e podobne odczucia mogy ci towarzyszy przy lekturze opisw wielu innych elementw jzyka. Pniej jednak nieczsto widziae zastosowania omawianych wczeniej rzeczy w dalszej czciu kursu, a pewnie sam znalajdowae niektre z nich po odpoczynku od lektury i duszym zastanowieniu.

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

wskaniki do zmiennych wskaniki do funkcji

Tabela 19. Rne rodzaje wskanikw

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.

Wskanik na metod obiektu konkretnej klasy


Najpierw zajmijmy si prostszym przypadkiem. Znajdmy sposb na symulacj wskanika, za porednictwem ktrego monaby wywoywa metod: o okrelonej sygnaturze (nagwku) na rzecz okrelonego obiektu nalecego do okrelonej klasy Dosy duo tych okrele Najlepiej bdzie, jeli popatrzysz na dziaanie tego wskanika. Przypomnij sobie klas CNumber; stwrzmy obiekt tej klasy: CNumber Liczba; Teraz wyobramy sobie, e w jzyku C++ pojawia si moliwo zadeklarowania wskanikw, o jakie nam chodzi. Niech p2ofnMetoda bdzie tym podanym wskanikiem115. Wwczas mona z nim zrobi co takiego: // przypisanie wskanikowi "adresu metody" Dodaj w obiekcie Liczba p2ofnMetoda = Liczba.Dodaj; // wywoanie metody Dodaj() dla obiektu Liczba (*p2ofnMetoda)(10); Jak wida, dokonujemy tu zwykej dereferencji - zupenie tak, jak w przypadku wskanikw na funkcje globalne. Tym sposobem wywoujemy jednak metod klasy dla konkretnego obiektu. Ostatnia linijka jest wic rwnowana tej: Liczba.Dodaj(10); Zamiast wywoania obiekt.metoda() mamy wic (*wskanik_do_metody_obiektu)(). I o to nam chodzi. Wrmy teraz do rzeczywistoci. Niestety C++, nie posiada wskanikw na metody obiektw, lecz chcemy przynajmniej czciowo uzupeni ten brak. Jak to zrobi? Przyjrzyjmy si temu, co chcemy osign. Chcemy mianowicie, aby nasz wskanik zastpowa wywoanie: obiekt.metoda([parametry]) w ten sposb: (*wskanik)([parametry]) Wskanik musi wic zawiera informacje zarwno o obiekcie, ktrego dotyczy metoda, jak i samej metodzie. Jeden wskanik? Nie - dwa: pierwszy to wskanik na obiekt, na rzecz ktrego metoda bdzie wywoywana
115

p2ofn to skrt od pointer to object-function.

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);

// wskanik na obiekt // wskanik na metod

// ------------------------------------------------------------------// 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);

To wanie chcielimy osigna.

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.

Wskanik na metod obiektu dowolnej klasy


Nasz callback wydaje si dziaa (bo i dziaa), ale jego przydatno jest niestety niewielka. Wskanik potrafi bowiem pokazywa tylko na metod w konkretnej klasie, natomiast do zastosowa praktycznych (jak informowanie o zdarzeniach czy bdach) powinien on umie wskaza na zgodn ustalonym prototypem metod obiektu w dowolnej klasie. Tak wic niezalenie od tego, czy nasz obiekt byby klasy CFoo, CVector2D, CEllipticTable czy CBrokenWindow, jeli tylko klasa ta posiada metod o okrelonej sygnaturze, to powinno da si na ni wskaza w konkretnym obiekcie. Dopiero wtedy dostaniemy do rki wartociowy mechanizm. Ten mechanizm ma nazw: closure. Trudno to przetumaczy na polski (dosownie jest to przymknicie, domknicie, itp.), wic bdziemy posugiwa si dotychczasow nazw wskanik na metod obiektu. Czasami aczkolwiek spotyka si te nazw delegat (ang. delegate). Czy mona go osign w C++? Owszem. Wymaga to jednak do daleko idcego kroku: ot musimy sobie zdefiniowa uniwersaln klas bazow. Z takiej klasy bd dziedziczy wszystkie inne klasy, ktrych obiekty i ich metody maj by celami tworzonych wskanikw. Taka klasa moe by bardzo prosta, nawet pusta: class IObject { };

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++

class CBar : public IObject { public: float Funkcja(int x) };

{ 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);

// wskanik na obiekt // wskanik na metod

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

Mechanizm wyjtkw w C++


Czym waciwie jest taka sytuacja wyjtkowa, ktra moe narobi tyle zamieszania? Ot: Sytuacja wyjtkowa (ang. exceptional state) ma miejsce wtedy, gdy warunki zewntrzne uniemoliwiaj danemu fragmentowi kodu poprawne wykonanie. w fragment nie jest winny zaistnienia sytuacji wyjtkowej. Oglnie sytuacj wyjtkow mona nazwa kady bd wystpujcy podczas dziaania programu, ktry nie jest spowodowany przez bdy w jego kodzie. To co w rodzaju przykrej niespodzianki: nieprawidoowych danych, nieprzewidzianego braku zasobw, i tak dalej. Takie przypadki mog zdarzy si w kadym programie, nawet napisanym pozornie bezbdnie i dziaajcym doskonale w zwykych warunkach. Sytuacje wyjtkowe, jak sama ich nazwa wskazuje, zdarzaj si bowiem tylko w warunkach wyjtkowych

434

Zaawansowane C++

Tradycyjne metody obsugi bdw


Wystpieniu sytuacji wyjtkowej zwykle nie mona zapobiec - a przynajmniej nie moe tego zrobi ten kawaek kodu, w ktrym ona faktycznie wystpuje. Jego rol powinno by zatem poinformowanie o zainstniaym zdarzeniu kodu, ktry stoi wyej w strukturze programu. Kod wyszego poziomu moe wtedy podj jakie sensowne akcje, a jeli nie jest to moliwe - w ostatecznoci zakoczy dziaanie programu. Dziaania wykonywane w reakcji na bdy s do specyficzne dla kadego programu. Obejmowac mog na przykad zapisanie informacji o zdarzeniu w specjalnym dzienniku, pokazanie komunikatu dla uytkownika czy te jeszcze inne czynnoci. Tym zagadnieniem nie bedziemy si wic zajmowa. Zobaczmy raczej, jakimi sposobami moe odbywa si powiadamianie o bdach. Tutaj istnieje kilka potencjalnym rozwiza - niektre s lepsze, inne nieco gorsze Oto te najczciej wykorzystywane.

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.

Zwracanie nietypowego wyniku


Najprostsz drog poinformowania o bdzie jest zwrcenie pewnej specjalnej wartoci, ktra w normalnych warunkach nie ma prawa wystpi. Aby to zilustrowa, zamy przez chwil, e mamy napisa funkcj obliczajc pierwiastek kwadratowy z podanej liczby. Wiedzc to, ochoczo zabieramy si do pracy, produkujc np. taki oto kod: float Pierwiastek(float x) { // staa okreljca dokadno static const float EPSILON = 0.0001f; /* liczymy pierwiastek kwadratowy metod Newtona */ // wybieramy punkt pocztkowy (poow wartoci) float fWynik = x / 2; // wykonujemy tyle iteracji, aby otrzyma rozsdne przyblienie while (abs(x - fWynik * fWynik) > EPSILON) fWynik = (fWynik + x / fWynik) / 2; // zwracamy wynik return fWynik;

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.

Wady tego rozwizania


Takie rozwizanie ma jednak kilka mankamentw. Pierwsz wida ju tutaj: nie wyglda ono szczeglnie estetycznie od strony wywoujcego. Druga kwestia jest powaniejsza. Jest ni problem doboru wartoci specjalnej, sygnalizujcej bd. Zwracam uwag, e nie ma ona prawa pojawienia si w jakiejkolwiek poprawnej sytuacji - musi ona jednoznacznie identyfikowa bd, a nie przydatny rezultat. W przypadku funkcji Pierwiastek() byo to proste, gdy potencjalnych wartoci jest mnstwo: moemy przecie wykorzysta wszystkie liczby ujemne - poprawnym wynikiem funkcji jest bowiem tylko liczba dodatnia. Nie zawsze jednak musi tak by - czas na kolejny przykad matematyczny, tym razem z logarytmem o dowolnej podstawie: float LogA(float a, float x) { return log(x) / log(a); }

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.

Oddzielenie rezultatu od informacji o bdzie


Obie te dane trzeba od siebie odseparowa. Funkcja powinna zatem zwraca dwie wartoci: jedn waciw oraz drug, informujc o powodzeniu lub niepowodzeniu operacji. Ma to rozliczne zalety - midzy innymi: pozwala przekaza wicej danych na temat charakteru bdu upraszcza kontrol poprawnoci wykonania funkcji umoliwia swobod zmian w kodzie i ewentualne rozszerzenie funkcjonalnoci Wydaje si jednak, e jest do powany problem: jak funkcja miaaby zwraca dwie wartoci? C, chyba brak ci pomysowoci - istnieje bowiem kilka drg zrealizowania tego mechanizmu.

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.

Niezbyt dobre wyjcia


Oba zaprezentowane w poprzednim paragrafie sposoby obsugi bdw zakaday proste poinformowanie wywoujcego funkcj o zainstniaym problemie. Mimo tej prostoty, sprawdzaj si one bardzo dobrze. Istniej aczkolwiek take inne metody raportowania bdw, ktre nie maj ju tak licznych zalet i nie s szeroko stosowane w praktyce. Oto te metody.

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.

Uwaga o niedostatku mechanizmw


Wreszcie trzeba wspomnie, e w C++ nie ma dobrych sposobw na realizacj funkcji zwrotnych. Owszem, mamy wskaniki na funkcje - jednak one pozwalaj pokazywa jedynie na funkcje globalne lub statyczne metody klas. Nie posiadamy natomiast niezbdnego w programowaniu obiektowym mechanizmu wskanika na niestatyczn metod obiektu (ang. closure, spotyka si te nazw delegat - ang. delegate), przez co trudno jest zrealizowa callback. W poprzednim rozdziale opisaem pewien sposb na obejcie tego problemu, ale jak wszystkie poowiczne rozwizania, nie jest on zbyt elegancki

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++

Rzucanie i apanie wyjtkw


Technik obsugi wyjtkw mona streci w trzech punktach, ktre od razu wska nam jej najwaniejsze elementy. Tak wic, te trzy zaoenia wyjtkw s nastpujce: jeeli piszemy kod, w ktrym moe zdarzy si co wyjtkowego i niecodziennego, czyli po prostu sytuacja wyjtkowa, oznaczamy go odpowiednio. Tym oznaczeniem jest ujcie kodu w blok try (sprbuj). To cakiem obrazowa nazwa: kod wewntrz tego bloku nie zawsze moe by poprawnie wykonany, dlatego lepiej jest mwi o prbie jego wykonania: jeeli si ona powiedzie, to bardzo dobrze; jeeli nie, bdziemy musieli co z tym fantem zrobi zamy, e wykonuje si nasz kod wewntrz bloku try i stwierdzamy w nim, e zachodzi sytuacja wyjtkowa, ktr naley zgosi. Co robimy? Ot uywamy instrukcji throw (rzu), podajc jej jednoczenie tzw. obiekt wyjtku (ang. exception object). Ten obiekt, mogcy by dowolnym typem danych, jest zwykle informacj o rodzaju i miejscu zainstniaego bdu rzucenie obiektu wyjtku powoduje przerwanie wykonywania bloku try, za nasz rzucony obiekt leci sobie przez chwil - a zostanie przez kogo zapany. Tym za zajmuje si blok catch (zap), nastpujcy bezporednio po bloku try. Jego zadaniem jest reakcja na sytuacj wyjtkow, co zazwyczaj wie si z odczytaniem obiektu wyjtku (rzuconego przez throw) i podjciem jakiej sensownej akcji A zatem mechanizmem wyjtkw dz te trzy proste zasady: Blok try obejmuje kod, w ktrym moe zaj sytuacja wyjtkowa. Instrukcja throw wewntrz bloku try suy do informowania o takiej sytuacji przy pomocy obiektu wyjtku. Blok catch przechwytuje obiekty wyrzucone przez throw i reaguje na zainstaniae sytuacje wyjtkowe. Tak to wyglda w teorii - teraz czas na obejrzenie kodu obsugi wyjtkw w 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).

Schemat 39. Wdrwka wyjtku rzuconego w funkcji

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; }

// wyjtek typu float // wyjtek typu int // pusty wskanik

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

Kolejno blokw catch


Obecno kilku blokw catch po jednej instrukcji try to powszechna praktyka. Dziki niej mona bowiem zabezpieczy si na okoliczno rnych rodzajw wyjtkw. Warto wic o tym porozmawia.

444
Dopasowywanie typu obiektu wyjtku
Zamy wic, e mamy tak oto sekwencj try-catch: try {

Zaawansowane C++

} catch (float fLiczba) catch (int nLiczba) catch (double fLiczba)

// rzucamy wyjtek throw 90;

{ /* ... */ } { /* ... */ } { /* ... */ }

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 {

} catch catch catch catch

// ... throw 16u; // ... throw -87; // ... throw 9.242f; // ... throw 3.14157;

// unsigned // int // float // double { { { { /* /* /* /* ... ... ... ... */ */ */ */ } } } }

(double fLiczba) (int nLiczba) (float fLiczba) (unsigned uLiczba)

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++

unsigned int float double

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.

Zagniedone bloki try-catch


Wewntrz bloku try moe znale si dowolny kod, jaki moe by umieszczany we wszystkich blokach instrukcji C++. Przypisania, instrukcje warunkowe, ptle, wywoania funkcji - wszystko to jest dopuszczalne. Co wicej, w bloku try mog si znale inne bloki try-catch. Nazywami je wtedy zagniedonymi, zupenie tak samo jak zagniedone instrukcje if czy ptle. Formalnie skadnia takiego zagniedenia moe wyglda tak: try {

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.

Blok catch(...), czyli chwytanie wszystkiego


W poczeniu z zagniedonymi blokami try i instrukcj throw; czesto wystpuje specjalny rodzaj bloku catch. Nazywany jest on uniwersalnym, a powstaje poprzez wpisanie po catch wielokropka (trzech kropek) w nawiasie: try {

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

Midzy rzuceniem a zapaniem


Odwijanie stosu rozpoczyna si wraz z rzuceniem jakiegokolwiek wyjtku przy pomocy instrukcji throw i postpuje a do momentu natrafienia na pasujcy do niego blok catch. W skrajnym przypadku odwijanie moe doprowadzi do zakoczenia dziaania programu jest tak jeli odpowiednia procedura obsugi wyjtku nie zostanie znaleziona.

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.

Porwnanie throw z break i return


Nieprzypadkowo porwnaem instrukcj throw do break, a wczeniej do return. Czas jednak zebra sobie cechy wyrniajce i odrniajce te trzy instrukcje. Oto stosowna tabela: instrukcja cecha przekazywanie sterowania throw do najbliszego pasujcego bloku catch obiekt wyjtku dowolnego typu obsuga sytuacji wyjtkowych break jeden blok wyej (wyjcie z ptli lub bloku switch) nie jest zwizana z adn wartoci return zakoczenie dziaania funkcji i powrt do kodu, ktry j wywoa warto tego samego typu, jaki zosta okrelony w deklaracji funkcji

warto

zastosowanie

oglne programowanie

Tabela 20. Porwnanie throw z break i return

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.

Wyjtek opuszcza funkcj


Rzucenie oraz zapanie i obsuga wyjtku moe odbywa si w ramach tej samej funkcji. Czsto jednak mamy sytuacj, w ktrej to jedna funkcja sygnalizuje sytuacj wyjtkow, a dopiero inna (wywoujca j) zajmuje si reakcj na zainstniay problem. Jest to zupenie dopuszczalne, co zreszt parokrotnie podkrelaem. W procesie odwijania stosu obiekt wyjtku moe wic opuci swoj macierzyst funkcj. Nie jest to aden bd, lecz normalna praktyka. Nie zwalnia ona jednak z obowizku zapania wyjtku: nadal kto musi to zrobi. Kto - czyli wywoujcy funkcj.

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.

Kamstwo nie popaca


Specyfikacja wyjtkw jest przyczeniem zoonym przez twrc funkcji jej uytkownikowi. W ten sposb autor procedury zawiadcza, e jego dzieo bdzie wyrzucao do wywoujcego wyjtki wycznie podanych typw.

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.

Niszczenie obiektw lokalnych


Wspominajc o opuszczaniu kolejno zagniedonych blokw czy nawet funkcji, posuyem si porwnaniu z break i return. throw ma z nimi jeszcze jedn cech wspln - nie liczc tych odrniajcych. Wychodzenie z blokw przebiega mianowicie w sposb cakiem czysty - tak jak w normalnym kodzie. Oznacza, to e wszystkie stworzone obiekty lokalne s niszczone, a ich pami zwalniania. W przypadku typw podstawowych oznacza to po prostu usunicie zmiennych z pamici. Dla klas mamy jeszcze wywoywanie destruktorw i wszystkie tego konsekwencje. Mona zatem powiedzie, e: Opuszczanie blokw kodu dokonywane podczas odwijania stosu przebiega tak samo, jak to si dzieje podczas normalnego wykonywania programu. Obiekty lokalne s wic niszczone poprawnie. Sama nazwa odwijanie stosu pochodzi zreszt od tego sprztania, dokonywanego przy okazji wychodzenia na wierzch programu. Obiekty lokalne (zwane te automatycznymi) s bowiem tworzone na stosie, a jego odwinicie to wanie usunicie tych obiektw oraz powrt z wywoywanych funkcji.

Wypadki przy transporcie


To niszczenie obiektw lokalnych moe si wydawa tak oczywiste, e nie warto powica temu a osobnego paragrafu. Jest jednak co na rzeczy: czynno ta moe by bowiem powodem pewnych problemw, jeeli nie bdziemy jej wiadomi. Jakich problemw?

Niedozwolone rzucenie wyjtku


Musimy powiedzie sobie o jednej bardzo wanej zasadzie zwizanej z mechanizmem wyjtkw w C++. Brzmi ona: Nie naley rzuca nastpnego wyjtku w czasie, gdy kompilator zajmuje si obsug poprzedniego. Co to znaczy? Czy nie moemy uywa instrukcji throw w blokach catch?

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().

Zarzdzanie zasobami w obliczu wyjtkw


Napisaem wczeniej, e transport rzuconego wyjtku do bloku catch powoduje zniszczenie wszystkich obiektw lokalnych znajdujcych si po drodze. Nie musimy si o to martwi; zreszt, nie troszczylimy si o nie take i wtedy, gdy nie korzystalimy z wyjtkw. Obiekty lokalne nie s jednak jedynymi z jakich korzystamy w C++. Wiemy te, e moliwe jest dynamiczne tworzenie obiektw na stercie, czyli w rezerwuarze pamici. Dokonujemy tego poprzez new. Pami jest z kolei jednym z tak zwanych zasobw (ang. resources), czyli zewntrznych bogactw naturalnych komputera. Moemy do nich zaliczy nie tylko pami operacyjn, ale np. otwarte pliki dyskowe, wyczno na wykorzystanie pewnych urzdze lub aktywne poczenia internetowe. Waciwe korzystanie z takich zasobw jest jednym z zada kadego powanego programu. Zazwyczaj odbywa si ono wedug prostego schematu: najpierw pozyskujemy dany zasb w jaki sposb (np. alokujemy pami poprzez new) potem moemy do woli korzysta z tego zasobu (np. zapisywac dane do pamici) na koniec zwalniamy zasb, jeeli nie jest ju nam potrzebny (czyli korzystamy z delete w przypadku pamici) Najbardziej znany nam zasob, czyli pami opercyjna, jest przez nas wykorzystywany choby tak: CFoo* pFoo = new CFoo; // (robimy co...) // alokacja (utworzenie) obiektu-zasobu

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.

Rne typy wskanikw


Zaprezentowana wyej klasa CFooSmartPtr jest typem inteligentnego wskanika, ktry moe pokazywa na obiekty jakiej zdefiniowanej wczeniej klasy CFoo. Przy jego pomocy nie moemy odnosi si do obiektw innych klas czy typw podstawowych. Jeli jednak bdzie to konieczne, wwczas musimy niestety napisa now klas wskanika. Nie jest to trudne: wystarczy w definicji CFooSmartPtr zmieni wystpienia CFoo np. na int. W nastpnym rozdziale poznamy zreszt o wiele bardziej efektywn technik (mianowicie szablony), ktra uwolni nas od tej mudnej pracy. Za chwil te przyjrzymy si rozwizaniu, jakie przygotowali dla nas sami twrcy C++ w Bibliotece Standardowej.

Uywajmy tylko tam, gdzie to konieczne


Musz te powtrzy to, o czym ju wspomniaem przy pierwszym spotkaniu ze sprytnymi wskanikami. Ot trzeba pamita, e nie s one uniwersalnym lekiem na wszystkie bolczki programisty. Nie naley ich stosowa wszdzie, poniewa kady rodzaj inteligentnego wskanika (my na razie poznalimy jeden) ma cile okrelone zastosowania. W sytuacjach, w ktrych z powodzeniem sprawdzaj si zwyke wskaniki, powinnimy nadal z nich korzysta. Dopiero w takich przypadkach, gdy s one niewystarczajce, musimy sign go bardziej wyrafinowane rozwizania. Takim przypadkiem jest wanie rzucanie 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.

Pliki w Bibliotece Standardowej


Oprcz pamici drugim wanym rodzajem zasobw s pliki dyskowe. O dokadnym sposobie ich obsugi powiemy sobie aczkolwiek dopiero wtedy, gdy zajmiemy si strumieniami Biblioteki Standardowej. Tutaj chc tylko wspomnie, e metody dostpu do plikw, jakie s tam oferowane, cakowicie poprawnie wsppracuj z wyjtkami. Oto przykad: #include <fstream> try {

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

Projektowanie klas wyjtkw


C++ umoliwia rzucenie w charakterze wyjtkw obiektw dowolnych typw, take tych wbudowanych. Taka moliwo jest jednak mao pocigajca, jako e pojedyncza liczba czy napis nie nios zwykle wystarczajcej wiedzy o powstaej sytuacji. Dlatego te powszechn praktyk jest tworzenie wasnych typw (klas) dla obiektw wyjtkw. Takie klasy zawieraj w sobie wicej informacji zebranych z miejsca katastrofy, ktre mog by przydatne w rozpoznaniu i rozwizaniu problemu.

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;

Jest to ju cakiem zadowalajca informacja o bdzie.

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.

Organizacja obsugi wyjtkw


Zdefiniowana uprzednio klas lub jej hierarchi bdziemy pewnie mieli okazj nieraz wykorzysta. Poniewa nie jest to takie oczywiste, warto powici temu zagadnieniu osobny paragraf.

Umiejscowienie blokw try i catch


Wydawaoby si, e obsuga wyjtkw to bardzo prosta czynno - szczeglnie, jeli mamy ju zdefiniowany dla nich odpowiednie klasy. Niestety, polega to na czym wicej ni tylko napisaniu niepewnego kodu w bloku try i instrukcji obsugi bdw catch.

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

Tylko wtedy zachowujemy wymienione wyej zalety warstwowoci programu.

Podawanie bdw wyej


Podzia warstwowy wymusza poza tym do cile ustalony przepyw danych w aplikacji. Odbywa si on zawsze tak, e kod wyszego poziomu przekazuje do niszych warstw konieczne informacje (np. nazw pliku, ktry ma by otwarty) i odbiera rezultaty wykonanych operacji (czyli zawarto pliku). Potem wykorzystuje je do swych wasnych zada (np. do wywietlenia pliku na ekranie). Ten naturalny ukad dziaa dobrze dopki si nie zepsuje :) Przyczyn mog by sytuacje wyjtkowe wystpujce w kodzie niszego poziomu. Typowym przykadem moe by brak danego pliku, wobec czego jego otwarcie nie jest moliwe. Funkcja, ktra miaa tego dokona, nie bdzie potrafia poradzi sobie z tym bdem, poniewa nazwa pliku do otwarcie pochodzia z zewntrz - z gry. Moe jedynie poinformowa wywoujcego o zainstniaej sytuacji. I tutaj wkraczaj na scen opisane na samym pocztku rozdziau mechanizmy obsugi bdw. Jednym z nich s wanie wyjtki.

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.

Chwytanie wyjtkw w blokach catch


Poprawne chwytanie wyjtkw w blokach catch to kolejne (ostatnie ju na szczcie) zagadnienie, o ktrym musimy pamita. Wiesz na ten temat ju cakiem sporo, ale nigdy nie zaszkodzi powtrzy sobie przyswojone wiadomoci i przyswoi nowe.

Szczegy przodem - druga odsona


Swego czasu zwrciem ci uwag na wan spraw kolejnoci blokw catch. Uwiadomiem, e ich dziaanie tylko z pozoru przypomina przecione funkcje, jako e porzdek dopasowywania obiektu wyjtku cile pokrywa si z porzdkiem samych blokw catch, a same dopasowywanie koczy przy pierwszym sukcesie. W zwizku naley tak ustawia bloki catch, aby na pocztek szy te, ktre precyzyjniej opisuj typ wyjtku. Gdy zdefiniujemy sobie hierarchi klas wyjtkw, ta zasada zyskuje jeszcze pewniejsz podstaw. W przypadku typw podstawowych (int, double) moe by do trudne wyobraenie si relacji typ oglny - typ szczegowy. Natomiast dla klas jest to oczywiste: wchodzi tu bowiem w gr jednoznaczny zwizek dziedziczenia. Jakie s wic konkretne wnioski? Ano takie, e: Gdy stosujemy hierarchi klas wyjtkw, powinnimy najpierw prbowa apa obiekty klas pochodnych, a dopiero potem obiekty klas bazowych. Mam nadziej, i wiesz doskonale, z jakiej fundamentalnej reguy programowania obiektowego wynika powysza zasada119. Jeeli zastosujemy klasy wyjtkw z poprzedniego paragrafu, to ilustracj moe by taki kawaek kodu: try {

// } catch { // } catch { // } catch { // }

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

Korzyci ze stosowania wyjtkw


Podstawowe zalety wyjtkw przedstawiem na pocztku rozdziau, gdy porwnywaem je z innymi sposobami obsugi bdw. Teraz jednak masz ju za sob dogbne poznanie tej techniki, wic pewnie zwtpie w te przymioty ;) Nawet jeli nie, to pokazane niej argumenty przemawiajce na korzy wyjtkw mog pomc ci w decyzji co do ich wykorzystania w konkretnej sytuacji.

Informacja o bdzie w kadej sytuacji


Pierwsz przewag, jak wyjtki maj nad innymi sposobami sygnalizowania bdw, jest uniwersalno: moemy je bowiem stosowa w kadej sytuacji i w kadej funkcji. No ale czy to co nadzwyczajnego? Przecie wydawaoby si, e zarwno technika zwracania kodu bdu jak i wywoanie zwrotne, moe by zastosowane wszdzie. To jednak nieprawda; oba te sposoby wymagaj odpowiedniej deklaracji funkcji, uwzgldniajcej ich wykorzystanie. A nagwek funkcji moe by czsto ograniczony przez sam jzyk albo inne czynniki - jest tak na przykad w: konstruktorach wikszoci przecionych operatorw funkcjach zwrotnych dla zewntrznych bibliotek

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?

Wzrost niezawodnoci kodu


Wreszcie mona wytoczy najcisze dziaa. Wyjtki nie pozwalaj na obojtno - na ignorowanie bdw. Poprzedni akapit uwiadamia, e tradycyjne metody w rodzaju zwracania rezultatu musz by aktywnie wspomagane przez programist, ktry uywa wykorzystujcych je funkcji. Nie musi jednak tego robi; kod skompiluje si tak samo poprawnie, jeeli wartoci zwracane zostan cakowicie pominite. Co wicej, moe to prowadzi do pominicia krytycznych bdw, ktre wprawdzie nie daj natychmiast katastrofalnych rezultatw, ale potrafi przyczai si w zakamarkach aplikacji, by ujawni si w najmniej spodziewanym momencie. Mechanizm wyjtkw jest skonstruowany zupenie przeciwnie. Tutaj nie trzeba si wysila, aby bd da zna o sobie, bowiem wyjtek zawsze wywoa jak reakcj choby nawet awaryjne zakoczenie programu. Natomiast wiadome zignorowanie wyjtku wymaga z kolei pewnego wysiku. Tak wic tutaj mamy do czynienia z sytuacj, w ktrej to nie programista szuka bedu, lecz bd szuka programisty. Jest to naturalnie znacznie lepsza sytuacja z punktu widzenia niezawodnoci programu, bo pozwala na atwiejsze odszukanie wystpujcych we bdw.

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.

Nie uywajmy ich tam, gdzie wystarcz inne konstrukcje


Pocztkujcy programici maj czasem skonno do uwaania, i kade niepowodzenie wykonania jakiego zadania zasuguje na rzucenie wyjtku. Oto (zy) przykad: // funkcja wyszukuje liczb w tablicy unsigned Szukaj(const CIntArray& aTablica, int nLiczba) { // ptla porwnuje kolejne elementy tablicy z szukan liczb for (unsigned i = 0; i < aTablica.Rozmiar{]; ++i) if (aTablica[i] == nLiczba) return i; // w razie niepowodzenia - wyjtek?... throw CError(__FILE__, __LINE__, "Nie znaleziono liczby");

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.

Nie uywajmy wyjtkw na si


Nareszcie, musz powstrzyma wszystkich tych, ktrzy z zapaem rzucili si do implementacji wyjtkw w swych gotowych i dziaajcych programach. Niesusznie! Prawdopodobnie bdzie to kawa cikiej, nikomu niepotrzebnej roboty. Nie ma sensu jej wykonywa, poniewa zysk zwykle bdzie nieadekwatny do woonego wysiku. Co najwyej mona pokusi si o zastosowanie wyjtkw w przypadku, gdy nowa wersja danego programu wymaga napisania jego kodu od nowa. Decyzja o tym, czy tak ma si sta w istocie, powinna by podjta jak najwczeniej.

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

ciso C++ powodem blu gowy


Pewnie syszae ju wczeniej, e C++ jest jzykiem o cisej kontroli typw. Znaczy to, e typy danych peni w nim due znaczenie i e zawsze istnieje wyrane rozgraniczenie pomidzy nimi. Jednoczenie wiele mechanizmw tego jzyka suy, paradoksalnie, wanie zatarciu granic pomidzy typami danych. Wystarczy przypomnie chociaby niejawne konwersje, ktre pozwalaj dokonywa w locie zamiany z jednego typu na drugi, w sposb niezauwaalny. Ponadto klasy w C++ s skonstruowane tak, aby w razie potrzeby mogy niemal doskonale imitowa typy wbudowane. Mimo to, ciy podzia informacji na liczby, napisy, struktury itd. moe by czsto spor przeszkod

Dwa typowe problemy


Kopoty zaczynaj si, gdy chcemy napisa kod, ktry powinien dziaa w odniesieniu do kilku moliwych typw danych. Z grubsza mona tu rozdzieli dwie sytuacje: gdy prbujmy napisa uniwersaln funkcj i gdy podobn prb czynimy przy definiowaniu klasy.

Problem 1: te same funkcje dla rnych typw


Tradycyjnym, wrcz klasycznym przykadem tego pierwszego problemu jest funkcja wyznaczajca wiksza liczb spord dwch podanych. Prawdopodobnie z takiej funkcji bdziesz czsto skorzysta, wic kiedy moesz j zdefiniowa np. jako: int max(int nLiczba1, int nLiczba2) { return (nLiczba1 > nLiczba2 ? nLiczba1 : nLiczba2); } Taka funkcja dziaa dobrze dla liczb cakowitych, ale ju cakiem nie radzi sobie z liczbami typu float czy double, bo zarwno wynik, jak i parametry s zaokrglane do jednoci. Dla zdefiniowanych przez nas typw danych jest za zupenie nieprzydatna, co chyba zreszt cakowicie zrozumiae. Naturalnie, moemy sobie doda inne, przecione wersje funkcji - jak chociaby tak: double max(double fLiczba1, double fLiczba2) {

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

Problem 2: klasy operujce na dowolnych typach danych


Innym problemem s klasy, ktre z jakich wzgldw musz by elastyczne i operowa na danych dowolnego typu. Koronnym przykadem s pojemniki, jak np. tablice dynamiczne, podobne do naszej klasy CIntArray. Jak wiemy, ma ona spor wad: przy jej pomocy nie mona bowiem zarzdza tablic elementw innego typu ni int. Chcc to osign, naleaoby napisa now klas - zapewne bardzo podobn do wspomnianej. T sam prac trzebaby wykona dla kadego nastpnego typu elementw To na pewno nie jest dobre wyjcie!

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.

Uywanie oglnych typw


Jeszcze mniej oczywisty jest sposb na zaimplementowanie oglnej klasy, np. tablicy przechowujcej dowolny typ elementw. Tutaj aczkolwiek take istnieje pewne rozwizanie: mona uy oglnego wskanika, tworzc tablic elementw typu void*: class CPtrArray { private: // tablica i jej rozmiar void** m_ppvTablica; unsigned m_uRozmiar; // itd. (metody i przecione operatory)

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!

Szablony jako rozwizanie


W porzdku, dosy tych bezowocnych poszukiwa. Myl, e domylasz si, i to szablony s tym rozwizaniem, ktrego poszukujemy. Zatem nie tracc wicej czasu, znajdmy je wreszcie :)

Kod niezaleny od typu


Wrmy wpierw do prb napisania funkcji max(). Patrzc na jej dwie wersje, dla typw int i double, moemy atwo zauway, e rni si one bardzo niewiele. Waciwie to mona stwierdzi, e po prostu drugi z wariantw ma wpisane double tam, gdzie w pierwszym widnieje typ int. Gdybymy wic chcieli napisa oglny wzorzec dla funkcji max(), wygldaby on tak: typ max(typ Parametr1, typ Parametr2) { return (Parametr > Parametr2 ? Parametr1 : Parametr2); } No dobrze, moemy sobie pisa takie wzorce, ale co nam z tego? Nie znamy przecie adnego sposobu, aby przekaza go kompilatorowi do wykorzystania Czy na pewno?

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.

Definiowanie szablonu funkcji


Definicja szablonu funkcji nie rni si zbytnio od zwykej definicji funkcji. Ot, po prostu jeden typ (lub wicej) nie s w niej podane explicit, lecz wnioskowane z wywoania funkcji szablonowej. Niemniej, temu wszystkiemu trzeba si przyjrze bliej.

Podstawowa definicja szablonu funkcji


Oto jeden z prostszych chyba przykadw szablonu funkcji - warto bezwzgldna: template <typename TYP> TYP Abs(TYP Liczba) { return (Liczba >= 0 ? Liczba : -Liczba); } Posiada takiego szablonu ma t niezaprzeczaln zalet, e bez dodatkowego wysiku moemy posugiwa si t funkcj dla liczb dowolnego typu: int, float, double, itd. Co najwaniejsze, w wyniku otrzymamy warto tego samego typu, co podany parametr, zatem nie musimy posugiwa si rzutowaniem - co byoby konieczne w przypadku zdefiniowania zwykej funkcji dla najbardziej pojemnego typu double. Dlaczego tak jest? Oczywicie dlatego, i symboliczne oznaczenie TYP (czyli parametr szablonu) wystpuje zarwno jako typ wartoci zwracanej, jak i typ parametru funkcji. W konkretnych egzemplarzach funkcji w obu miejscach wystpi wic ten sam typ, np. int.

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.

Parametr szablonu uyty w ciele funkcji


Trudno zauway to na pierwszy rzut oka, ale przedstawiony wyej szablon ma jeden do powany zgrzyt. Mianowicie, wymusza on na podanym mu typie danych, aby pozwala na porwnywanie go z typem int. Do takiego typu naley bowiem newralgiczne 0. Nie jest to zbyt dobre i lepiej, eby funkcja nie korzystaa z takiego rozwizania. Interpretacja zera w rnych typach liczbowych moe by bowiem cakiem odmienna od zakadanej przez nas. Lepiej wic, eby punkt zerowy mg by ustalony przez domylny konstruktor. Wwczas szablon bdzie wyglda tak - zmiana jest niewielka: template <typename TYP> TYP Abs(TYP Liczba) { return (Liczba >= TYP() ? Liczba : -Liczba); } Teraz bdzie on jednak dziaa poprawnie dla kadego sensownego typu danych liczbowych. Chwileczk, rzekniesz. A co z typami podstawowymi? Przecie one nie maj konstruktorw! Faktycznie, suszna uwaga. Tak uwag poczyni pewnie swego czasu ktry z twrcw C++, gdy zaowacowaa ona wprowadzeniem do jzyka tzw. inicjalizacji zerowej. Jest to bardzo prosta rzecz: ot typy wbudowane (jak int czy bool) zostay wyposaone w swego rodzaju konstruktory. Nie s to prawdziwe funkcje skadowe, jak w przypadku klas, lecz po prostu moliwo uycia tej samej skadni jawnego wywoania domylnego konstruktora. Wyglda ona tak: typ()

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.

Parametr szablonu i parametr funkcji


Mwic o szablonach funkcji, mona si nieco zagubi w znaczeniu sowa parametr. Mamy mianowicie a dwa rodzaje parametrw: parametry funkcji - czyli te znane nam ju od dawna, bo wystpuje one w kadej niemal funkcji. Kady taki parametr ma swj typ i nazw parametry szablonu poznalimy w tym rozdziale. W przypadku szablonw funkcji mog to by wyacznie nazwy typw. Parametry szablonu stosujemy wic w nagwku i w ciele funkcji tak, jak gdyby byy to nazwy typw, np. float czy VECTOR2D To naturalne, e oba te rodzaje parametrw s ze sob cile zwizane. Popatrzmy choby na nagwek funkcji max(): template <typename TYP> TYP max(TYP Parametr1, TYP Parametr2)

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.

Kilka parametrw szablonu


Dotd widzielimy jednoparametrowe szablony funkcji, ale nie jest to kres moliwoci szablonw. Tak naprawd bowiem mog mie one dowoln liczb parametrw. Oto na przykad inny wariant funkcji max(): template <typename TYP1, typename TYP2> TYP1 max(TYP1 Parametr1, TYP2 Parametr2) { return (Parametr > Parametr2 ? Parametr1 : Parametr2); }

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.

Specjalizacja szablonu funkcji


Podstawowy szablon funkcji definiuje nam ogln rodzin funkcji, ktrej czonkowie (specjalizacje) dla kadego typu (parametru szablonu) zachowuj si tak samo. Nasza funkcja max() bdzie wic zwracay wiksz liczb niezalenie od tego, czy typem jest liczby bdzie double czy int. Powiesz: I bardzo dobrze! O to nam przecie chodzi. No tak, ale jest pewien szkopu. Dla pewnych typw danych algorytm wyznaczania wikszej wartoci moe by nieodpowiedni. Uoglniajc spraw, mona zkonkludowa, e niekiedy potrzebna nam jest specjalna wersja szablonu funkcji, ktra dla jakiego konkretnego typu (parametru szablonu) bdzie si zachowywaa inaczej ni dla reszty. Wtedy wanie musimy sami zdefiniowa ow konkretn specjalizacj szablonu funkcji. Tym zajmiemy si w niniejszym paragrafie.

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.

Ciekawostka: specjalizacja czciowa szablonu funkcji


Jak kada Ciekawostka, take i ta nie jest przeznaczona dla pocztkujcych, a ju na pewno nie podczas pierwszego kontaktu z tekstem. Poprzednio specjalizowalimy funkcj dla cile okrelonego typu danych. Teoretycznie monaby jednak zrobi co innego: napisa specjaln jej wersj dla pewnego rodzaju typw. No, teraz to ju przesadzasz!, moesz tak odpowiedzie. To jednak moe mie sens; wyobramy sobie, e przy pomocy max() sprbujemy porwna dwa wskaniki. Co

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.

Wywoywanie funkcji szablonowej


Skoro ju mniej wicej wiemy, jak mona definiowa szablony funkcji, nauczmy si teraz z nich korzysta. Zwaywszy, e ju to robilimy, nie powinno to sprawia adnych trudnoci. Zastanwmy si jednak, co dzieje si w momencie wywoania funkcji szablonowej. Oto przykad takiego wywoania:

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.

Jawne okrelenie typu


Zwykle uywajc szablonw funkcji pozwalamy kompilatorowi na samodzielne wydedukowanie typu, dla ktrego ma on by skonkretyzowany. Zdarza si jednak, e chcemy go sami wyranie okreli. To rwnie jest moliwe.

Wywoywanie konkretnej wersji funkcji szablonowej


Moemy wic zayczy sobie, aby funkcja max() dziaaa w danym przypadku, powiedzmy, na liczbach typu unsigned - mimo e typem jej argumentw bdzie int: unsigned uMax = max<unsigned>(45, 3); // 45 i 3 to liczby typu int Skadnia max<unsigned> pozwala nam poda dany typ. cilej mwic, w nawiasach ostrych podajemy parametry szablonu (w odrnieniu od parametrw funkcji, podanych jak zwykle w nawiasach okrgych). Tutaj jest to jeden parametr, bdcy typem; nadajemy mu warto unsigned, czyli typu liczb bez znaku. Takie wywoanie powoduje, e nie jest ju przeprowadza adna dedukacja typu argumentw funkcji. Kompilator nie zwaa ju na nie, lecz oczekuje, e bd one zgadzay si z typem pdoanym jawnie - parametrem szablonu. W tym wic przypadku liczby musz pasowa do typu unsigned i oczywicie pasuj do niego (s dodatnie), cho ich waciwy typ to int. Nie gra on jednak adnej roli, gdy sami odgrnie narzucilimy tutaj parametr szablonu.

Uycie wskanika na funkcj szablonow


max<unsigned> wystpuje tutaj w miejscu, gdzie zwykle pojawia si nazwa funkcji w przypadku normalnych procedur. To nie przypadek: moemy t fraz traktowa wanie jako nazw funkcji - konkretnej ju funkcji, a nie jej szablonu.

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.

Dedukcja typu na podstawie argumentw funkcji


Jawne podawanie parametrw szablonu funkcji jest generalnie nieczsto stosowane. Zdecydowanie najwiksz zalet tych szablonw jest to, i potrafi same wykry typ argumentw funkcji i na tej podstawie dopasowa odpowiedni parametr szablonu. Spjrzmy, jak to si odbywa.

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

Dedukcja przy wykorzystaniu kilku parametrw szablonu


Proces dedukcji zaczyna nabiera rumiecw, gdy mamy do czynienia z szablonem o wikszej liczbie parametrw ni jeden. Przypomnijmy sobie szablon funkcji max() z dwoma parametrami (deklaracj tylko, bo definicja jest chyba oczywista): template <typename TYP1, typename TYP2> TYP1 max(TYP1 Parametr1, TYP2 Parametr2); Tutaj wszystko jest nawet znacznie prostsze ni poprzednio. Dziki temu, e kady parametr funkcji ma swj wasny typ (parametr szablonu), kompilator ma uatwione zadanie. Nie musi ju bra pod uwag adnych niejawnych konwersji. Z powyszym szablonem zwizanym jest jednak pewien problem. Nie bardzo wiadomo, jaki ma by typ zwracany tej funkcji. Moe to by zarwno TYP1, jak i TYP2 - zaley po prostu, ktra z wartoci zwyciy w tecie porwnawczym. Tego jego nie sposb ustali w czasie kompilacji; mona jednak doda typ oddawany do parametrw szablonu: template <typename TYP1, typename TYP2, typename ZWROT> ZWROT max(TYP1 Parametr1, TYP2 Parametr2); Prba wywoania tej funkcji w zwykej formie zakoczy si jednak bdem - a to dlatego, e ten nowy, trzeci parametr nie moe zosta wydedukowany przez kompilator! Mwiem przecie, e dedukcja dokonywana jest wycznie na podstawie parametrw funkcji. Warto zwracana si zatem nie liczy. Hmm, to nie jest a taki problem, odpowiesz moe. Ten jeden parametr mog przecie poda; wpisze tam po prostu typ oglniejszy spord dwch poprzedzajcych. Tak si jednak nie da! Nie moemy poda do szablonu ostatniego parametru, gdy wpierw musielibymy poda dwa poprzedzajce go: max<int, float, float>(17, 67f); To chyba adna niespodzianka: analogicznie jest z parametrami funkcji. W ten sposb tracimy jednak wszystkie wspaniaoci automatycznej dedukcji parametrw szablonu.

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.

Definicja szablonu klas


Wpierw wic zajmiemy si definiowaniem szablonu klasy. Popatrzmy sobie najpierw na prosty przykad szablonu, bdcy rozszerzeniem klasy CIntArray, przewijajcej si przez kilka poprzednich rozdziaw. Dalej zajmiemy si te bardziej zaawansowanymi aspektami definicji szablonw klas.

488

Zaawansowane C++

Prosty przykad tablicy


W rozdziale o wskanikach pokazaem ci prost klas dynamicznej tablicy int-w CIntArray. Wtedy interesowaa nas dynamiczna alokacja pamici, wic nie przeszkadza nam fakt nieporcznoci teje klasy. Miaa ona bowiem dwa mankamenty: nie pozwalaa na uycie nawiasw kwadratowych [] celem dostpu do elementw tablicy, no i potrafia przechowywa wycznie liczby typu int. Obiecaem jednoczenie, e w swoim czasie pozbdziemy si obu tych niedogodnoci. Miae si ju okazj przekona, e nie rzucam sw na wiatr, bowiem nauczylimy ju nasz klas poprawnie reagowa na operator []. Zapewne domylasz si, e teraz usuniemy drugi z mankamentw i wyposaymy j w moliwo przechowywania elementw dowolnego typu. Jak nietrudno zgadn, bdzie to wymagao uczynienia jej szablonem klasy. Zanim przystpimy do dziea, spjrzmy na aktualn wersj naszej klasy: 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 explicit CIntArray(unsigned uRozmiar = DOMYSLNY_ROZMIAR) : m_uRozmiar(uRozmiar); m_pnTablica(new int [m_uRozmiar]) CIntArray(const CIntArray&); // 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) { 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

Litera T w nazwie TArray to skrt od template, czyli szablon.

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.

Implementacja metod poza definicj


Szablon jest ju prawie gotowy. Musimy jeszcze doda do niego implementacje dwch metod: konstruktora kopiujcego i operatora przypisania - ze wzgldu na ich dugo lepiej bdzie, jeli znajd si poza definicj. W przypadku zwykych klas byo to jak najbardziej moliwe a jak jest dla szablonw? Zapewne nie jest niespodziank to, i rwnie tutaj jest to dopuszczalne. Warto jednak uwiadomi sobie, e metody szablonw klas s szablonami metod. Oznacza to ni mniej, ni wicej, ale to, i powinnimy je traktowa podobnie, jak szablony funkcji. Wie si z tym gwnie inna skadnia. Popatrz wic na przykad - oto szablonowa wersja konstruktora kopiujcego: template <typename TYP> TArray<TYP>::TArray(const TArray& aTablica) { // alokujemy pami m_uRozmiar = aTablica.m_uRozmiar; m_pTablica = new TYP [m_uRozmiar]; // kopiujemy pami ze starej tablicy do nowej memcpy (m_pTablica, aTablica.m_pTablica, m_uRozmiar * sizeof(TYP));

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++

TArray<float> aLiczbyRzeczywiste(7); TArray<bool> aFlagi(8) TArray<CFoo*> aFoo;

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

Dziedziczenie i szablony klas


Nowy wspaniay wynalazek jzyka C++ - szablony - moe wsppracowa ze starym wspaniaym wynalazkiem jzyka C++ - dziedziczeniem. A tam, gdzie spotykaj si dwa wspaniae wynalazki, musi by doprawdy cudownie :) Zajmijmy si wic dziedziczeniem poczonym z szablonami klas.

Dziedziczenie klas szablonowych


Szablony klas (jak TArray) s podstawami do generowania specjalizowanych klas szablonowych (jak np. TArray<int>). Ten specjalizowane klasy zasadniczo niczym nie rni si od innych uprzednio zdefiniowanych klas. Mog wic na przykad by klasami bazowymi dla nowych typw. Czas na ilustracj zagadnienia w postaci przykadowego kodu. Oto klasa wektora liczb: class CVector : public TArray<double> { public: // operator mnoenia skalarnego double operator*(const CVector&); }; Dziedziczy ona z TArray<double>, czyli zwykej tablicy liczb. Dodaje ona jednak dodatkow metod - przeciony operator mnoenia *, obliczajcy iloczyn skalarny: double CVector::operator*(const CVector& aWektor) { // jeeli rozmiary wektorw nie s rwne, rzucamy wyjtek if (Rozmiar() != aWektor.Rozmiar()) throw CError(__FILE__, __LINE__, "Blad iloczynu skalarnego"); // liczymy iloczyn

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.

Dziedziczenie szablonw klas


Szablony i dziedziczenie umoliwiaj rwnie tworzenie nowych szablonw klas na podstawie ju istniejcych, innych szablonw. Na czym polega rnica? Ot na tym, e w ten sposb tworzymy nowy szablon klas, a nie pojedyncz, zwyk klas - jak to si dziao poprzednio. Wtedy definiowalimy normaln klas przy pomocy innej, niemale normalnej klasy - rnica bya tylko w tym, e t klas bazow bya specjalizacja szablonu (TArray<double>). Teraz natomiast bdziemy konstruowali szablon klas pochodnych przy uyciu szablonu klas bazowych. Cay czas bdziemy wic porusza si w obrebie czysto szablonowego kodu z nasz ulubion fraz template <...> ;) Oto nasz nowy szablon - tablica, ktra potrafi dynamicznie zmienia swj rozmiar w czasie swego istnienia: template <typename TYP> class TDynamicArray : public TArray<TYP> { public: // funkcja dokonujca ponownego wymiarowania tablicy bool ZmienRozmiar(unsigned); }; Poniewa jest to szablon, wic rozpoczynamy go od zwyczajowego pocztku i listy parametrw. Nadal bdzie to jeden TYP elementw tablicy, ale nic nie staoby na przeszkodzie, aby lista parametrw szablonu zostaa w jaki sposb zmodyfikowana. W dalszej kolejnoci widzimy znajomy pocztek definicji klasy. Jako klas bazow wstawiamy tu TArray<TYP>. Przypomina to poprzedni punkt, ale pamitajmy, e teraz korzystamy z parametru szablonu (TYP) zamiast z konkretnego typu (double). Nazwa klasy bazowej jest wic tak samo zszablonowana jak caa reszta definicji TDynamicArray. Pozostaje jeszcze kwestia implementacji metody ZmienRozmiar(). Nie powinna by ona niespodzienka, bowiem wiesz ju, jak kodowa metody szablonw klas poza blokiem ich definicji. Tre funkcji jest natomiast niemal wiern kopi tej z rozdziau o wskanikach: template <typename TYP> bool TDynamicArray<TYP>::ZmienRozmiar(unsigned uNowyRozmiar) { // sprawdzamy, czy nowy rozmiar jest wikszy od starego

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.

Deklaracje w szablonach klas


Pola i metody to najwaniejsze skadniki definicji klas - take tych szablonowych. Jeeli jednak chodzi o szablony, to znacznie czciej moemy tam spotka rwnie inne deklaracje. Trzeba si im przyjrze, co teraz uczynimy. Ten paragraf moesz pomin przy pierwszym podejciu do lektury, jeli wyda ci si zbyt trudny, i przej dalej.

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.

Szablony funkcji skadowych


Istnieje bardzo ciekawa moliwo122: metody klas mog by szablonami. Naturalnie, moesz pomyle, e to adna nowo, bo przecie w przypadku szablonw klas wszystkie ich metody s swego rodzaju szablonami funkcji. Chodzi jednak o co innego, co najlepiej zobaczymy na przykadzie. Nasz szablon TArray dziaa cakiem znonie i umoliwia podstawow funkcjonalno w zakresie tablic. Ma jednak pewn wad; spjrzmy na poniszy kod: TArray<float> aFloaty1(10), aFloaty2; TArray<int> aInty(7);

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 :)

Korzystanie z klas szablonowych


Zdefiniowanie szablonu klasy to naturalnie dopiero poowa sukcesu. Pieczoowicie stworzony szablon chcemy przecie wykorzysta w praktyce. Porozmawiajmy wic, jak to zrobi.

Tworzenie obiektw
Najbardziej oczywistym sposobem korzystania z szablonu klasy jest tworzenie obiektw bazujcych na specjalizacji tego szablonu.

Stwarzamy obiekt klasy szablonowej


W kreowaniu obiektw klas szablonowych nie ma niczego nadzwyczajnego; robilimy to ju kilkakrotnie. Zobaczmy na najprostszy przykad - utworzenia tablicy elementw typu long:

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.

Co si dzieje, gdy tworzymy obiekt szablonu klasy


Aby ten paragraf nie by jedynie prezentacj rzeczy oczywistych (tworzenie obiektw klas szablowych) i denerwujcych (vide kwestia nawiasw ostrych), powiedzmy sobie jeszcze o jednej sprawie. Co w zasadzie dzieje si, gdy w kodzie napotka kompilator na instrukcj tworzc obiekt klasy szablonowej? Oczywicie, oglna odpowied brzmi generuje odpowiedni kod maszynowy. Warto jednak zagbi si nieco w szczegy, bo dziki temu spotka nas pewna mia niespodzianka A zatem - co si dzieje? Przede wszystkim musimy sobie uwiadomi fakt, e takie nazwy jak TArray<int>, TDynamicArray<double> i inne nazwy szablonw klas z
123 To waciwie problem nie tylko kompilatora, ale samego Standardu C++, ktry pozwala im na takie beztroskie zachowanie. Pozostaje mie nadziej, e to si zmieni

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

Domniemane produkty pracy kompilatora zapisuj bez charakterystycznego formatowania.

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? :)

Funkcje operujce na obiektach klas szablonowych


Szablony funkcji s czsto przystosowane do manipulowanai obiektami klas szablonowych - w zbliony sposb, w jaki czyni to zwyke funkcje z normalnymi klasami. Popatrzmy na ten oto przykad funkcji Szukaj(): template <typename TYP> int Szukaj(const TArray<TYP>& aTablica, TYP Szukany) { // przelatujemy po tablicy i porwnujemy elementy for (unsigned i = 0; i < aTablica.Rozmiar(); ++i) if (aTablica[i] == Szukany) return i; // jeli nic nie znajdziemy, zwracamy -1 return -1;

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.

Specjalizacje szablonw klas


Teraz porozmawiamy sobie o definiowaniu specjalnych wersji szablonw klas dla okrelonych parametrw (typw). Mechanizm ten dziaa do podobnie jak w przypadku szablonw funkcji, wic nie powinno by z tym zbyt wielu problemw.

Specjalizowanie szablonu klasy


Specjalizacja szablonu klasy oznacza ni mniej wicej, jak tylko zdefiniowanie pewnej szczeglnej wersji tego szablonu dla pewnego wyjtkowego typu (parametru szablonu). Dodatkowo, istnieje moliwo specjlizacji pojedynczej metody; zajmiemy si pokrtce oboma przypadkami.

Wasna klasa specjalizowana


Jako przykad na wasn, kompletn specjalizacj szablonu klasy posuymy si oczywicie naszym szablonem tablicy jednowymiarowej - TArray. Dziaa on cakiem dobrze w oglnej wersji, lecz przecie chcemy zdefiniowa jego specjalizacj. W tym przypadku moe to by sensowne w odniesieniu do typu char. Tablica elementw tego typu jest bowiem niczym innym, jak tylko acuchem znakw. Niestety, w obecnej formie klasa TArray<char> nie moe by jako traktowana napis (obiekt std::string), bo dla kompilatora nie ma teraz adnej praktycznej rnicy midzy wszystkimi typami tablic TArray. Aby to zmieni, musimy rzecz jasna wprowadzi swoj wasn specjalizacj TArray dla parametru char. Klasa ta bdzie rnia si od wersji oglnej tym, i wewntrznym mechanizmem przechowywania tablicy bdzie nie tablica dynamiczna typu char* (w oglnoci: TYP*), lecz napis w postaci obiektu std::string. Pozwoli to na dodanie operatora konwersji, aczkolwiek zmieni nieco kilka innych metod klasy. Spjrzmy wic na t specjalizacj: #include <string> template<> class TArray<char> { static const unsigned DOMYSLNY_ROZMIAR = 5; private: // rzeczona tablica w postaci napisu std::string std::string m_strTablica; public: // konstruktor explicit TArray(unsigned uRozmiar = DOMYSLNY_ROZMIAR) : m_strTablica(uRozmiar, '\0') { } // (destruktor niepotrzebny) // ------------------------------------------------------------// (pomijam metody Pobierz() i Ustaw())

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.

Specjalizacja metody klasy


Przygldajc si uwanie specjalizacji TArray dla typu char mona odnie wraenie, e przynajmniej czciowo zosta on stworzony poprzez skopiowanie skadowych z definicji samego szablonu TArray. Przykadowo, funkcja dla operatora [] jest praktycznie identyczna z t zamieszczon w oglnym szablonie (kwestia nazwy m_strTablica czy m_pTablica jest przecie czysto symboliczna). To moe nam si nieszczeglnie podoba, ale jest do przyjcia. Gorzej, jeli w klasie specjalizowanej chcemy napisa nieco inn wersj tylko jednej metody z pierwotnego szablonu. Czy wwczas jestemy skazani na specjalizowanie caej klasy oraz niewygodne kopiowanie i bezsensowne zmiany prawie caego jej kodu? Odpowied brzmi na szczcie Nie! Niech do jednej metody szablonu klasy nie oznacza, e musimy obraa si na w szablon jako cao. Moliwe jest specjalizowanie metod dla szablonw klas; wyjanijmy to na przykadzie. Przypumy mianowicie, e zachciao nam si, aby tablica TArray zachowywaa si w specjalny sposb w odniesieniu do elementw bdacych wskanikami typu int*. Ot pragniemy, aby przy niszczeniu tablicy zwalniana bya take pami, do ktrej odnosz si te wskaniki (elementy tablicy). Nie rozwodmy si nad tym, na ile jest to dorzeczne programistycznie, lecz zastanwmy si raczej, jak to wykona. Chwila zastanowienia i rozwizanie staje si jasne: potrzebujemy troch zmienionej wersji destruktora. Powinien on jeszcze przed usuniciem samej tablicy zadba o zwolnienie pamici przynalenej zawartym we wskanikom. Zmiana maa, lecz wana. Musimy wic zdefiniowa now wersj destruktora dla klasy TArray<int*>. Nie jest to specjalnie trudne: template<> TArray<int*>::~TArray() { // przelatujemy po elementach tablicy (wskanikach) i kademu // aplikujemy operator delete for (unsigned i = 0; i < Rozmiar(); ++i) delete m_pTablica[i]; // potem jak zwykle usuwamy te sam tablic delete[] m_pTablica;

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++

Czciowa specjalizacja szablonu klasy


Pena specjalizacja szablonu oznacza zdefiniowanie klasy dla konkretnego, precyzyjnie okrelonego zestawy argumentw. W naszym przypadku byo to dosowne podanie typ elementw tablict TArray, np. char. Czasem jednak taka precyzja nie jest podana. Niekiedy zdarza si, e wygodniej byloby wprowadzi bardziej szczegow wersj szablonu, ktra nie operowaaby przy tym konkretnymi typami. Wtedy wanie wykorzystujemy specjalizacj czciow (ang. partial specialization). Zobaczymy to tradycyjnie na odpowiednim przykadzie.

Problem natury tablicowej


A zatem Nasz szablon tablicy TArray sprawdza si cakiem dobrze. Dotyczy to szczeglnie prostych zastosowa, do ktrych zosta pierwotnie pomylany - jak na przykad jednowymiarowa tablica liczb czy lista napisw. Idc dalej, atwo mona sobie jednak wyobrazi bardziej zaawansowane wykorzystanie tego szablonu - w tym take jako dwuwymiarowej tablicy tablic, np.: TArray<TArray<int> > aInty2D; Naturalnie chcielibymy, aby taka funkcjonalnoc bya nam dana niejako z urzdu, z samej tylko definicji TArray. Zdawaoby si zreszt, e wszystko jest tutaj w porzadku i e faktycznie moemy si posugiwa zmienn aInty2D jak tablic o dwch wymiarach. Niestety, nie jest tak rowo; mamy tu przynajmniej dwa problemy. Po pierwsze: w jaki sposb mielibymy ustali rozmiar(y) takiej tablicy? Typ zmiennej aInty2D jest tu wprawdzie podwjny, ale przy jej tworzeniu nadal uywany jest normalny konstruktor TArray, ktry jest jednoparametrowy. Moemy wic poda wycznie jeden wymiar tablicy, za drugi zawsze musiaby by rwny wartoci domylnej! Oprcz tego oczywistego bdu (cakowicie wykluczajcego uycie tablicy) mamy jeszcze jeden mankament. Mianowicie, zawarto tablicy nie jest rozmieszczona w pamici w postaci jednego bloku, jak to czyni kompilator w wypadku statycznych tablic. Zamiast tego kady jej wiersz (podtablica) jest umieszczony w innym miejscu, co przy wikszej ich liczbie, rozmiarach i czstym dostpie bdzie ujemnie odbijao si na efektywnoci kodu.

Rozwizanie: przypadek szczeglniejszy, ale nie za bardzo


Co mona na to poradzi? Rozwizaniem jest specjalne potraktowanie klasy TArray<TArray<typ_elementu> > i zdefiniowanie jej odmiennej postaci - nieco innej ni wyjciowy szablon TArray. Przedtem jednak zwrmy uwag, i nie moemy tutaj zastosowa cakowitej specjalizacji tego szablonu, bowiem typ_elementu nadal jest tu parametrem o dowolnej wartoci (typie). Jak si pewnie domylasz, trzeba tu zastosowa specjalizacj czciow. Bdzie ona traktowaa zagniedone szablony TArray w specjalny sposb, zachowujc jednak moliwo dowolnego ustalania typu elementw tablicy. Popatrzmy wic na definicj tej specjalizacji: template <typename TYP> class TArray<TArray<TYP> > { static const unsigned DOMYSLNY_ROZMIAR = 5; private: // wskanik na tablic TYP* m_pTablica; // wymiary tablicy unsigned m_uRozmiarX;

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.

Domylne parametry szablonu klasy


Szablon klasy ma swoj list parametrw, z ktrych kady moe mie swoj warto domyln. Dziaa to w analogiczny sposb, jak argumenty domylne wywoa funkcji. Popatrzmy wic na t technik.

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 ;)

Przypominamy banalny przykad


W tym celu przywoajmy pierwszy przykad szablonu, z jakim mielimy do czynienia, czyli szablon funkcji max(): template <typename T> T max(T a, T b) { return (a > b ? a : b); } Ma on jeden parametr, bdcy typem; parametr ten nosi nazw T. Jest to zwyczajowa ju nazwa dla takich parametrw szablonu, ktr mona spotka niezwykle czsto. Zgodnie z t konwencj, nazw T nadaje si parametrowi bdcemu typem, jeli jest on jednoczenie jedynym parametrem szablonu i w zwizku z tym peni jak szczegln rol. Moe to by np. typ elementw tablicy czy, tak jak tutaj, typ parametrw funkcji i zwracanej przez ni wartoci. Nazwa T jest tu wic symbolem zastpczym dla waciwego typu porwnywanych wartoci. Jeeli pojcie to sprawia ci trudno, wyobra sobie, e dziaa ono podobnie jak alias typedef. Mona wic przyj, e kompilator, stosujc funkcj w konkretnym przypadku, definiuje T jako alias na waciwy typ. Przykadowo, specjalizacj max<int> mona traktowa jako kod:

510

Zaawansowane C++

typedef int T; T max (T a, T b)

{ return (a > b ? a : b); }

Naturalnie, w rzeczywistoci generowana jest po prostu funkcja:


int max<int>(int a, int b);

Niemniej powyszy sposb moe ci z pocztku pomc, jeli dotd nie rozumiae idei parametru szablonu bdcego typem.

class zamiast typename


Parametr szablonu bdcy typem oznaczalimy dotd za pomoc sowa kluczowego typename. Okazuje sie, e mona to take robi poprzez swko class: template <class T> T max(T a, T b); Nie oznacza to bynajmniej, e podany parametr szablonu moe by wycznie klas zdefiniowan przez uytkownika125. Przeciwnie, ot: Slowa class i typename w s synonimami w deklaracjach parametrw szablonu bdcych typami. Po co zatem istniej dwa takie sowa? Jest to spowodowane tym, i pierwotnie jedynym sposobem na deklarowanie parametrw szablonu byo class. typename wprowadzono do jzyka pniej, i to w cakiem innym przeznaczeniu (o ktrym te sobie powiemy). Przy okazji aczkolwiek pozwolono na uycie tego nowego slowa w deklaracjach parametrw szablonw, jako e znacznie lepiej pasuje tutaj ni class. Dlatego te mamy ostatecznie dwa sposoby na zrobienie tego samego. Mona z tego wycign pewn korzy. Wprawdzie dla kompilatora nie ma znaczenia, czy do deklaracji parametrw uywamy class czy typename, lecz nasz wybr moe mie przecie znaczenie dla nas. Logiczne jest mianowicie uywanie class wycznie tam, gdzie faktycznie spodziewamy si, e przekazanym typem bdzie klasa (bo np. wywoujemy jej metody). W pozostaych przypadkach, gdy typ moe by absolutnie dowolny (jak choby w uprzednich szablonach max() czy TArray), rozsdne jest stosowanie typename. Naturalnie, to tylko sugestia, bo jak mwiem ju, kompilatorowi jest w tej kwestii wszystko jedno.

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.

Uycie parametrw pozatypowych


Ich wykorzystanie najlepiej bdzie zobaczy na paru rozsdnych przykadach.

125

Czyli typem zdefiniowanym poprzez struct, union lub class.

Szablony

511

Przykad szablonu klasy


W poprzednim paragrafie zdefiniowalimy sobie szablon klasy TArray. Suy on jako jednowymiarowa tablica dynamiczna, ktrej rozmiar podawalimy przy tworzeniu i ewentualnie zmienialimy w trakcie korzystania z obiektu. Mona sobie jeszcze wyobrazi podobny szablon dla tablicy statycznej, ktrej rozmiar jest znany podczas kompilacji. Oto propozycja szablonu TStaticArray: template <typename T, unsigned N> class TStaticArray { private: // tablica T m_aTablica[N]; public: // rozmiar tablicy jako staa static const unsigned ROZMIAR = N; // ------------------------------------------------------------// operator indeksowania T& operator[](unsigned uIndeks) { return m_aTablica[uIndeks]; } }; // (itp.)

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.

Przykad szablonu funkcji


Gdy mamy ju zdefiniowany nowy szablon tablicy, moemy sprbowa stworzy dla niego odpowiadajc wersj funkcji Szukaj(). Naturalnie, bdzie to rwnie szablon: template <typename T, unsigned N> int Szukaj(const TStaticArray<T, N>& aTablica, T Szukany) { // przegld tablicy for (unsigned i = 0; i < N; ++i) if (aTablica[i] == Szukany) return i; // -1, gdy nie znaleziono return -1;

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

Dwie wartoci, dwa rne typy


Wyobramy sobie, e mamy dwie tablice tego samego typu, ale o rnych rozmiarach: TStaticArray<int, 20> a20Intow; TStaticArray<int, 10> a10Intow; Sprbujmy teraz przypisa t mniejsz do wikszej, w ten oto sposb: a20Intow = a10Intow; // hmm...

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.

Ograniczenia dla parametrw pozatypowych


Pozatypowe parametry szablonw w przeciwiestwie do parametrw funkcji nie mog by wywoywane z dowolnymi wartociami. Typami tyche parametrw mog by bowiem tylko: typy liczbowe, czyli int i jego pochodne (signed lub unsigned) typy wyliczeniowe (definiowane poprzed enum) wskaniki do obiektw i funkcji globalnych wskaniki do skadowych klas Lista ta jest do krtka i moe si wydawa nazbyt restrykcyjna. Tak jednak nie jest. Gwnie ze wzgldu na sposb dziaania szablonw ich parametry pozatypowe s ograniczone tylko do takich rodzajw. Przyjrzyjmy si jeszcze kilku szczeglnym przypadkom tych ogranicze.

Wskaniki jako parametry szablonu


Nie ma adnych przeciwskaza, aby deklarowa szablony z parametrami bdcymi typami wskanikowymi. Wyglda to na przykad tak: template <int* P> class TClass { // ... };

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 :)

Dodatkowy parametr: typ wewntrznej tablicy


Moe si okaza, e w naszym programie zmuszeni jestemy do operowania zarwno wielkimi tablicami, jak i mniejszymi. W wypadku tych drugim wewntrzny szablon TArray sucy do ich przechowywania pewnie zda egzamin, ale gdy liczba elementw ronie, mog by konieczne bardziej wyrafinowane techniki zarzdzania pamici. Aby sprosta temu wymaganiu, rozsdnie byoby umoliwi wybr typu wewntrznej tablicy dla szablonu TNumericArray: template <typename T, typename TAB = TArray<T> > class TNumericArray { private: TAB m_aTablica; }; // ...

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?

Deklarowanie szablonowych parametrw szablonw


Bynajmniej. Midzy innymi na takie okazje cakiem niedawno jzyk C++ wyposaono w moliwo deklarowania szczeglnego rodzaju parametrw szablonu. Te specjalne parametry charakteryzuj si tym, e s nazwami zastpczymi dla szablonw klas. Jako takie wymagaj wic podania nie konkretnego typu danych, lecz jego uoglnienia szablonu klasy. Dobr, nieszablonow analogi dla tej niecodziennej konstrukcji jest sytuacja, gdy funkcja przyjmuje jako parametr inn funkcj poprzez wskanik. W mniej wicej zbliony koncepcyjnie sposb dziaaj szablonowe parametry szablonw. Oto jak deklaruje si i uywa tych specjalnych parametrw: template <typename T, template <typename> class TAB = TArray> class TNumericArray { private: TAB<T> m_aTablica; }; // ...

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

Uatwienia dla kompilatora


ledzc opis czynnoci, jakie wykonuje kompilator w zwizku z szablonami, mona zauway, e zmuszony jest do icie ekwilibrystycznych wygibasw. To zrozumiae, jeli przypomnimy sobie, e kontrola typw jest w C++ jednym z filarw programowania, za szablony czciowo su wanie do jej obejcia.

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.

Ciekawostka: dlaczego tak si dzieje


By moe zastanawiasz si, dlaczego kompilator ma w ogle problemy z poprawnym rozpoznawianiem uycia nawiasw ostrych. Przecie nic podobnego nie dotyczy ani nawiasw okrgych (wyraenia, wywoania funkcji, itd.), ani nawiasw kwadratowych (indeksowanie tablicy), ani nawet nawiasw klamrowych (bloki kodu). Skd wic problemy wynikaj problemy objawiajce si w szablonach?

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.

Sowo kluczowe typename


Czas wic na kawaek szablonu :) Popatrzmy na tak problematyczn funkcj, ktra ma za zadanie wybra najwikszy spord elementw tablicy: template <class TAB> TAB::ELEMENT Najwiekszy(const TAB& aTablica) { // zmienna na przechowanie wyniku TAB::ELEMENT Wynik = aTablica[0]; // ptla szukajca for (unsigned i = 1; i < aTablica.Rozmiar(); ++i) if (aTablica[i] > Wynik) Wynik = aTablica[i]; // zwrcenie wyniku return Wynik;

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.

Ciekawostka: konstrukcje ::template, .template i ->template


Podobny, cho znacznie raczej ujawniajcy si problem dotyczy szablonw zagniedonych. Oto nieszczeglnie sensowny przykad takiej sytuacji: template <typename T> class TFoo { public: // zagniedony szablon klasy template <typename U> class TBar { public: // zagniedony szablon statycznej metody template <typename V> static void Baz(); } }; Pytanie brzmi: jak wywoa metod Baz()? No c, wyglda to moe tak: template <typename T> void Funkcja() { // wywoanie jako statycznej metody bez obiektu TFoo<T>::template TBar<T>::Baz(); // utworzenie lokalnego obiektu i wywoanie metody typename TFoo<T>::template TBar<T> Bar; Bar.template Baz<T>(); // utworzenie dynamicznego obiektu i wywoanie metody typename TFoo<T>::template TBar<T>* pBar; pBar = new typename TFoo<T>::template TBar<T>; pBar->template Baz<T>(); delete pBar;

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.

Organizacja kodu szablonw


Wykorzystanie szablonw moe nastrcza problemw natury logistycznej. Nie chodzi o sam czynno ich implementacji czy pniejszego wykorzystania, ale o, zdawaoby si: prozaiczn, spraw nastpujc: jak rozmieci kod szablonw w plikach z kodem programu? Sprawa nie jest wcale taka prosta, bo kod korzystajcy z szablonw rni si znacznie pod tym wzgldem od zwykego, nieszablonowego kodu. W sumie mona powiedzie, e szablony s czym midzy normalnymi instrukcjami jzyka, a dyrektywami preprocesora. Ten fakt wpywa istotnie na sposb ich organizacji w programie. Obecnie znanych jest kilka moliwych drg prowadzcych do celu; nazywamy je modelami. W tym paragrafie popatrzymy sobie zatem na wszystkie trzy modele porzdkowania kodu szablonw.

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?

Prbujemy zastosowa szablony


Sprbujmy wic podobn metod zastosowa wobec szablonu funkcji max(). Najpierw umiemy jej prototyp (deklaracj) w pliku nagwkowym: // max.hpp // prototyp szablonu max() template <typename T> T max(T, T); Nastpnie tre funkcji podamy w module kodu max.cpp: // max.cpp #include "max.hpp" // implementacja szablonu max() template <typename T> T max(T a, T b) { return (a > b ? a : b); } Wreszcie, wykorzystamy nasz funkcj w programie, wypisujc na przykad na ekranie wiksz z podanych liczb: // TemplatesTryout - prba zastosowania szablonu funkcji // main.cpp #include <iostream> #include <conio.h> #include "max.hpp" int main() { std::cout << "Podaj dwie liczby:" << std::endl; double fLiczba1, fLiczba2;

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.

Rozwizanie - istota modelu wczania


Sytuacja patowa? Bynajmniej. Istnieje oczywicie rozwizanie tego problemu: trzeba po prostu zapewni widoczno definicji szablonu max() (czyli zawartoci max.cpp) w miejscu jego uycia (czyli main.cpp). Mona to uczyni poprzez: doczenie zawartoci max.cpp do max.hpp (dodanie #include "max.cpp" na kocu max.hpp) doczenie max.cpp w module main.cpp zamiast doczania max.hpp przeniesienie zawartoci moduu max.cpp (czyli definicj szablonu max()) do pliku nagwkowego max.hpp Wszystkie te sposoby s wariantami modelu wczania, o ktrym mwimy w tym paragrafie. Zastosowanie ktregokolwiek spowoduje podany efekt, czyli poprawn kompilacj kodu. W praktyce jednak najczciej stosuje si sposb trzeci, czyli umieszczanie caego kodu szablonw w pliku nagwkowym. Mimo takiego postpowania funkcje szablonowe nie bd rozwijane w miejscu wywoania. Aby szablon funkcji by funkcj inline, naley jawnie poprzedzi j przydomkiem inline po klauzuli template <...>. Model wczania dziaa cakiem dobrze zarwno dla maych, jak i nieco wikszych rednich projektw. Jest z nim jednak zwizany pewien mankament: w oczywisty sposb powoduje on rozrost plikw nagwkowych. Sprawia to, e koszt ich doczania staje si coraz wikszy, co w konsekwencji wydua czas kompilacji projektw. Staje si to aczkolwiek zauwaalne i znaczce dopiero w naprawd duych programach (rzdu kilkunastu-kilkudziesiciu tysicy linii).

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.

Instrukcje jawnej konkretyzacji


Wyjaniem, e powodem komunikatu linkera i nieudanej konsolidacji przykadu z poprzedniego akapitu jest nieobecno funkcji max<double>() w adnym ze skompilowanych moduw. Moemy to zmieni, sami wprowadzajc rzeczon funkcj czyli jawnie j skonkretyzowa. Czynimy w nastpujcy sposb: // max_inst.cpp #include "max.cpp" // jawna konkretyzacjia szablonu max() dla parametru double template double max<double>(double, double); Mamy tutaj dyrektyw konkretyzacji jawnej (ang. explicit instantiation directive). Jak wida, skada si ona z samego sowa template (bez nawiasw ostrych) oraz penej deklaracji specjalizacji szablonu (czyli max<double>()). Tutaj akurat mamy funkcj, ale podobnie konkretyzacja jawna wyglda klas. W kadym przypadku konieczna jest definicja konkretyzowanego szablonu - std doczenie do naszego nowego moduu pliku max.cpp. Nalezy zwrci uwag, aby kada specjalizacja szablonu bya wprowadzana jawnie tylko jeden raz. W przeciwnym razie zwrci na to uwag linker.

Wady i zalety konkretyzacji jawnej


Zastosowanie takiego wybiegu spowoduje teraz poprawn kompilacj i linkowanie programu. Moemy si wic przekona, e konkretyzacja jawna faktycznie dziaa. Nie ma jednak ry bez kolcw. Ten sposb zarzdzania specjalizacjami szablonu ma oczywist wad - jedn, ale za to bardzo dotkliw. Wymaga on od programisty ledzenia kodu, ktry wykorzystuje szablony, celem rozpoznawania wymaganych specjalizacji oraz ich jawnego deklarowania. Zwykle robi si to w osobnym module (u nas max_inst.cpp), aby nie zamieca waciwego kodu programu. Nie da si ukry, e niweluje to jedn z bezdyskusyjnych zalet szablonw, czyli moliwo zrzucenia na barki kompilatora kwestii wygenerowania waciwego ich kodu. Jest to szczeglnie niezadowalajce w przypadku szablonw funkcji, gdzie przy kadym ich wywoaniu musimy zastanowi si, jaka wersja szablonu zostanie w tym konkretnym wypadku uyta. Faktycznie wic trudno nawet czerpa korzyci z automatycznej dedukcji parametrw szablonu na podstawie parametrw funkcji - a to jest przecie jedno z gwnych dobrodziejstw szablonw. Konkretyzacja jawna ma aczkolwiek take kilka zalet, do ktrych nale: moliwo sprawowania kontroli nad procesem rozwijania szablonw zapobieganie nadmiernego rozdciu plikw nagwkowych, a wic potencjalne skrcenie czasu kompilacji

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

Nie ma ry bez kolcw


Model separacji moe ci si teraz wydawa rodzajem biaej magii, likwidujcej wszystkie mankamenty organizacji kodu szablonw. Trzeba sobie jednak zdawa spraw, e nie jest on pozbawiony wad. Czas wic zdj z twarzy ten szczliwy umieszek i przyjrze si rzeczywistoci. A rzeczywisto skrzeczy. Przede wszystkim naley wiedzie, e mimo kilkuletniej ju obecnoci w Standardzie C++ i w wiadomoci sporej czci programistw (przynajmniej tych co bardziej zainteresowanych rozwojem jzyka), szablony eksportowane s w peni obsugiwane przez nieliczne kompilatory. Dopiero ich najnowsze wersje (jak na przykad Visual Studio .NET 2003) radz sobie ze sowem kluczowym export. Ze wzgldu na tak nike dowiadczenia praktyczne trudno te przewidzie potencjalne problemy, jakie mog (cho oczywicie nie musz) przydarzy si podczas korzystania z modelu separacji. Te rzadkie kompilatory radzce sobie z tym modelem mog bowiem dziaa wietnie przy maych czy nawet rednich projektach, ale nie jest wcale powiedziane, czy przy wikszych programach nie ujawni si w nich jakie kopoty. Wiadomo wszake, e najlepszym probierzem jakoci wszelkich produktw - take moliwoci kompilatorw - jest ich intensywne wykorzystywanie przez rzesze uytkownikw. W tym za przypadku nie jest to jeszcze powszechn praktyk (przynajmniej nie tak bardzo, jak inne elementy C++), cho naley rzecz jasna oczekiwa, e sytuacji bdzie si z czasem poprawia. Druga sprawa zwizana jest z samym dziaaniem sowa kluczowego export. W przyblieniu mona je scharakteryzowa jako ukrycie funkcjonalnoci nieeleganckiego modelu wczenia - oczywicie wraz z pewnymi usprawnieniami. Oznacza to wic, e nie dokonuj si tu adne cuda: pozorne zerwanie zwizku midzy definicj a konkretyzacj szablonu musi i tak by odtworzone przez kompilator. To sprawia, e jakoby niezalene od siebie moduy kodu staj si zwizane wanie ze wzgldu na obecno w nich implementacji szablonw. W ostatecznoci koszt czasowy kompilacji programu wcale nie musi by wiele mniejszy od tego, jaki jest dowiadczany w modelu wczania. Wszystko to nie znaczy jednak, e nie naley spoglda na model separacji przychylnym okiem. Czas dziaa bowiem na jego korzy. Gdy obsuga szablonw eksportowanych stanie si powszechna, postpowa bdzie take jej usprawnienie pod wzgldem niezawodnoci i efektywnoci. Wcale niewykluczone, e na tym polu zostawi za jaki czas daleko w tyle model wczania. A ju teraz model separacji oferuje nam zalet niespotykan w innych rozwizaniach problemu szablonw: elegancj, podobn do tej znanej ze zwykego, nieszablonowego kodu. Dalej bdzie zapewne ju tylko lepiej.

Wsppraca modelu wczania i separacji


Ucieszy moe take fakt, e stosunkowo atwo zorganizowa kod szablonw w taki sposb, aby przeczanie midzy modelem wczania i separacji nie zajmowao wicej ni kilka sekund (nie liczc rekompilacji). Dosy dobrze do tego celu nadaj si dyrektywy preprocesora.

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?

Szablon funkcji i makro


Aby si o tym przekona, porwnajmy funkcj max() - napisan raz w postaci szablonu i drugi raz w postaci makra: // szablon funkcji max() template <typename T> T max(T a, T b) { return (a > b ? a : b); } // makro MAX() #define MAX(a,b) ((a) > (b) ? (a) : (b))

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

Starcie drugie: problem dopasowania tudzie wydajnoci


Popatrzmy wic, co si waciwie stanie w tym kodzie. Pomylmy mianowicie, w jaki sposb poradzi sobie z zadaniem szablon funkcji, a w jaki - makrodefinicja.

Jak zadziaa szablon


Funkcjonowanie szablonw byo przedmiotem sporej czci aktualnego rozdziau, zatem odpowied na pytanie powyej nie powinna ci nastrcza trudnoci. Szablon max() zadziaa tak, jak si spodziewamy: jego uycie spowoduje konkretyzacj dla waciwego parametru, co w wyniku da normaln funkcj, wykorzystywan przez program. Wpierw jednak musi by znany parametr T szablonu - zostanie on oczywicie wydedukowany z wywoania funkcji max(). Mamy w nim argumenty bdce zmiennymi: pierwsza jest typu int, za druga typu float. Parametr szablonu T jest natomiast tylko jeden - c wic? Naturalnie, kompilator wybierze tak, aby nie skrzywdzi adnego z argumentw funkcji, decydujc si na typ float. Pomieci on bowiem zarwno liczb cakowit, jak i rzeczywist. Szablon max() zostanie wic skonkretyzowany do postaci:
float max<float>(float a, float b) { return (a > b ? a : b); }

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.

Jak zadziaa makro


Przekonajmy si wic, co ma do powiedzenia makrodefinicja. Tutaj caa sprawa jest rzecz jasna znacznie atwiejsza: preprocesor rozwinie nam po prostu kod MAX(nA, fB) do postaci nastpujcego wyraenia: ((nA) ? (nB) ? (nA) : (nB)) Nie ma tutaj absolutnie rzadnej rnicy z sytuacj, w ktrej to wyraenie zostaoby wpisane bezporednio do kodu. adna funkcja nie jest generowana, adne konwersje argumentw nie s wykonywane, po prostu nie ma adnego przeskoku z miejsca wywoania makra w inne miejsce programu. Kompilator jest wrcz utrzymywany w bogiej niewiadomoci, gdy dostaje wyklarowany ju kod bez makr. Wszystkim zajmuje si preprocesor i to on sprawia, e makro dziaa.

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 :)

Starcie trzecie: problem rozwinicia albo poprawnoci


Prba trzecia jest w takim razie decydujca. Ponownie rozoymy na czynniki pierwsze sposb dziaania szablonu i makra.

Jak zadziaa szablon


Dziaanie szablonu bdzie tu udzco podobne do poprzedniej prby. Znowu bowiem argumenty funkcji max() musza by dopasowane do typu oglniejszego - czyli do float. Powstanie wic specjalizacja max<double>(). Funkcja ta bdzie potem wywoywana z argumentami nA++ i fB. Wobec tego zwrci ona wiksz spord liczb: nA+1 i fB. Waciwie wic nie ma nad czym duej deliberowa; nasz szablon zachowa si zupenie poprawnie, prawie jak zwyczajna funkcja. Naturalnie, stosuj si tutaj wszystkie uwagi z poprzedniego akapitu - nie ma sensu ponownie ich przytacza. Ogem test uwaamy za zaliczony.

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.

Trjki i wysze krotki


Przygldajc si uwaniej szablonowi pary, nietrudno jest dostrzec miejsca, ktre naley zmodyfikowa, by otrzyma krotki wyszego rzdu - trjki, czwrki, pitki, itd. Pewnym problemem jest stae zwikszanie dugoci klauzul template <...> i nazw typw krotek, ale to ju jest niestety nieuknione. W praktyce wic rzadko uywa si wielkich krotek powyej trzech, czterech elementw - take z tego powodu, e nie ma dla nich zbyt wielu sensownych zastosowa. Dlatego te tutaj popatrzymy sobie tylko na analogiczny do TPair szablon trjki TTriplet: template <typename T1, typename { T1 Pierwszy; // warto T2 Drugi; // warto T3 Trzeci; // warto T2, typename T3> struct TTriplet pierwszego pola drugiego pola trzeciego pola

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

Przykad klasy kontenera - stos


Zgodnie ze zwyczajem, kontenery poznamy na przykadzie jednego z prostszych rodzajw. Bdzie to stos.

Czym jest stos


Pojcie stosu jest ci znane; podczas omawiania wskanikw na funkcje wyjaniem bowiem, e jest to pomocny obszar pamici, poprzez ktry odbywa si transfer argumentw od wywoujcego funkcj. Stos (ang. stack) ma te inne znaczenie. Jest to rodzaj pojemnika przechowujcego dowolne elementy, charakteryzujcy si tym, i: obiekty s na stos jedynie odkadane (ang. push) i pobierane (ang. pop) w danej chwili ma si dostp jedynie do ostatnio pooonego, szczytowego elementu obiekty s zdejmowane w odwrotnej kolejnoci ni byy odkadane na stos Wida wic analogi do stosu - obszaru pamici. Tam obiektami odkadanymi byy parametry funkcji. Pooone w jednej kolejnoci, musiay by nastpnie podejmowane w porzdku odwrotnym. Cay czas rwnie dobre jest porwnanie do stosu ksiek: jeli pooymy na biurku sownik ortograficzny, na nim ksik kucharsk, a na samej grze podrcznik fizyki, to aby pozna prawidow pisowni sowa gegka bdziemy musieli wpierw zdj dwie ksiki lece na sowniku. Przy czym najpierw pozbdziemy si podrcznika, a potem ksiki z przepisami.

Definicja szablonu klasy


Na tej samej zasadzie dziaa stos - struktura danych. Jest to co w rodzaju tablicy, przechowujcej obiekty dowolnego typu, bdce odoonymi na stos elementami. Nie pozwala ona jednak na pobranie dowolnego elementu (o ustalonym indeksie), lecz wymaga zdejmowania obiektw w kolejnoci odwrotnej do porzdku ich odkadania. Najlepszym sposobem na wprowadzenie stosu do programowania w C++ jest zdefiniowanie odpowiedniego szablonu klasy. Dziki temu wszystkie szczegy implementacji zostan ukryte (zaleta OOPu), a nasz stos bdzie potrafi operowa elementami dowolnych typw (zaleta szablonw). Spjrzmy wic na propozycj takiego szablonu stosu: template <typename T, unsigned N> class TStack { private: // zawarto stosu T m_aStos[N]; // aktualny rozmiar (liczba elementw) stosu unsigned m_uRozmiar; public:

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

LICENCJA GNU WOLNEJ DOKUMENTACJI


Tumaczenie GNU Free Documentation License wedug GNU.org.pl Copyright (c) 2000 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Wersja 1.1
marzec 2002

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.

Licencja GNU Wolnej Dokumentacji

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.

Licencja GNU Wolnej Dokumentacji

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.

7. Zestawienia z pracami niezalenymi


Kompilacja Dokumentu lub jego pochodnych z innymi oddzielnymi i niezalenymi dokumentami lub pracami nie jest uznawana za Zmodyfikowan wersj Dokumentu, chyba e odnosz si do niej jako do caoci prawa autorskie. Taka kompilacja jest nazywana zestawieniem, a niniejsza Licencja nie dotyczy samodzielnych prac skompilowanych z Dokumentem, jeli nie s to pochodne Dokumentu. Jeeli do kopii Dokumentu odnosz si wymagania dotyczce Treci okadki wymienione w czci 3 i jeeli Dokument stanowi mniej ni jedn czwart caoci zestawienia, Tre okadki Dokumentu moe by umieszczona na okadkach zamykajcych Dokument w obrbie zestawienia. W przeciwnym razie Tre okadki musi si pojawi na okadkach caego zestawienia.

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.

10. Przysze wersje Licencji


W miar potrzeby Free Software Foundation moe publikowa nowe poprawione wersje GNU Free Documenation License. Wersje te musz pozostawa w duchu podobnym do wersji obecnej, cho mog si rni w szczegach dotyczcych nowych problemw czy zagadnie. Patrz http://www.gnu.org/copyleft/. Kadej wersji niniejszej Licencji nadaje si wyrniajcy j numer. Jeeli w Dokumencie podaje si numer wersji Licencji, oznaczajcy, i odnosi si do niego podana "lub jakakolwiek pniejsza" wersja licencji, Licencjobiorca ma do wyboru stosowa si do postanowie i warunkw albo tej wersji, albo ktrejkolwiek wersji pniejszej opublikowanej oficjalnie (nie jako propozycja) przez Free Software Foundation. Jeli Dokument nie podaje numeru wersji niniejszej Licencji, Licencjobiorca moe wybra dowoln wersj kiedykolwiek opublikowan (nie jako propozycja) przez Free Software Foundation.

Zacznik: Jak zastosowa t Licencj do swojego dokumentu?


Aby zastosowa t Licencj w stosunku do dokumentu swojego autorstwa, docz kopi Licencji do dokumentu i zamie nastpujc informacj o prawach autorskich i uwagi o licencji bezporednio po stronie tytuowej.
Copyright (c) ROK TWOJE IMIE I NAZWISKO Udziela si zezwolenia do kopiowania rozpowszechniania i/lub modyfikacj tego dokumentu zgodnie z zasadami Licencji GNU Wolnej Dokumentacji w wersji 1.1 lub dowolnej pniejszej opublikowanej przez Free Software Foundation; wraz z zawartymi Sekcjami Niezmiennymi LISTA TYTUW SEKCJI, wraz z Tekstem na Przedniej Okadce LISTA i z Tekstem na Tylnej Okadce LISTA. Kopia licencji zaczona jest w sekcji zatytuowanej "GNU Free Documentation License"

Licencja GNU Wolnej Dokumentacji

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.

You might also like