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

Funkcja main(), czyli skadamy program ____________________________________


Uroki kompilacji ________________________________________________________
Uruchamiamy aplikacj __________________________________________________
Wnioski _________________________________________________________________
Dziwaczne projektowanie_________________________________________________
Do skomplikowane algorytmy____________________________________________
Organizacja kodu _______________________________________________________
Podsumowanie _____________________________________________________________
Pytania i zadania _________________________________________________________
Pytania _______________________________________________________________
wiczenia _____________________________________________________________

155
156
158
158
158
159
159
159
160
160
160

Zaczynamy __________________________________________________________________
Deklarujemy zmienne __________________________________________________________
Funkcja StartGry() ___________________________________________________________
Funkcja Ruch() _______________________________________________________________
Funkcja RysujPlansze() _______________________________________________________

146
147
147
147
152

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


Wskanik this _______________________________________________________________ 179

Praca z obiektami _________________________________________________________ 179


Zmienne obiektowe _____________________________________________________ 180
Deklarowanie zmiennych i tworzenie obiektw _______________________________________
onglerka obiektami ___________________________________________________________
Dostp do skadnikw __________________________________________________________
Niszczenie obiektw ___________________________________________________________
Podsumowanie _______________________________________________________________

180
180
182
182
183

Wskaniki na obiekty ____________________________________________________ 183


Deklarowanie wskanikw i tworzenie obiektw ______________________________________
Jeden dla wszystkich, wszystkie do jednego _________________________________________
Dostp do skadnikw __________________________________________________________
Niszczenie obiektw ___________________________________________________________
Stosowanie wskanikw na obiekty _______________________________________________

Podsumowanie _____________________________________________________________
Pytania i zadania _________________________________________________________
Pytania _______________________________________________________________
wiczenia _____________________________________________________________

184
184
185
186
187

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 ____________________________________________________________

228
228
231
231

Identyfikacja klas _____________________________________________________________ 232


Abstrakcja klasy ______________________________________________________________ 233
Skadowe interfejsu klasy _______________________________________________________ 234

Implementacja _________________________________________________________ 234


Zwizki midzy klasami ____________________________________________________ 235
Dziedziczenie i zawieranie si _____________________________________________ 235
Zwizek generalizacji-specjalizacji ________________________________________________ 235
Zwizek agregacji _____________________________________________________________ 236
Odwieczny problem: by czy mie?________________________________________________ 237

Zwizek asocjacji _______________________________________________________ 237


Krotno zwizku______________________________________________________________ 238
Tam i (by moe) z powrotem ___________________________________________________ 239

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

Wskaniki i tablice ______________________________________________________ 261


Tablice jednowymiarowe w pamici _______________________________________________
Wskanik w ruchu _____________________________________________________________
Tablice wielowymiarowe w pamici ________________________________________________
acuchy znakw w stylu C______________________________________________________

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

Konwencja wywoania ___________________________________________________ 283


O czym mwi konwencja wywoania? ______________________________________________ 284
Typowe konwencje wywoania ___________________________________________________ 285

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_______________________________________

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

288
289
290
290
291

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

Preprocesor a reszta kodu ________________________________________________


Makra ____________________________________________________________________
Proste makra ____________________________________________________________
Definiowianie prostych makr ______________________________________________

306
307
307
307

Bez rednika _________________________________________________________________ 304


Ciekawostka: sekwencje trjznakowe ______________________________________________ 305

Zastpowanie wikszych fragmentw kodu _________________________________________ 308


W kilku linijkach ____________________________________________________________ 309
Makra korzystajce z innych makr ________________________________________________ 309

Pojedynek: makra kontra stae ____________________________________________ 310


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 _____________________________________

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

Zalety makrodefinicji ____________________________________________________ 322


Efektywno _________________________________________________________________
Funkcje inline ______________________________________________________________
Makra kontra funkcje inline ___________________________________________________
Brak kontroli typw ____________________________________________________________
Ciekawostka: funkcje szablonowe ______________________________________________

322
322
323
323
323

Zastosowania makr _____________________________________________________ 324


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 _________________________________

Kontrola procesu kompilacji ___________________________________________________


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

324
324
324
324
325

325
326
326
326
327
327
328
328

Operator defined _____________________________________________________________ 328


Zoone warunki ______________________________________________________________ 329

Skomplikowane warunki kompilacji _________________________________________ 329


Zagniedanie dyrektyw ________________________________________________________ 329
Dyrektywa #elif______________________________________________________________ 330

Dyrektywa #error ______________________________________________________


Reszta dobroci _____________________________________________________________
Doczanie plikw _________________________________________________________
Dwa warianty #include __________________________________________________

Z nawiasami ostrymi ___________________________________________________________


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

330
331
331
331
331
332
332
332
333

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

Klasy zaprzyjanione ______________________________________________________


Przyja z pojedynczymi metodami _________________________________________
Przyja z ca klas ____________________________________________________
Jeszcze kilka uwag ________________________________________________________
Cechy przyjani klas w C++ ______________________________________________

348
348
349
351
351

Funkcja zaprzyjaniona nie jest metod ____________________________________________


Deklaracja przyjani jest te deklaracj funkcji ______________________________________
Deklaracja przyjani jako prototyp funkcji ________________________________________
Dodajemy definicj __________________________________________________________

346
347
347
347

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 ___________________________________________________________

353
353
353
354
355
355
355

Inicjalizacja ___________________________________________________________ 355


Kiedy si odbywa _____________________________________________________________
Jak wyglda__________________________________________________________________
Inicjalizacja typw podstawowych ______________________________________________
Agregaty __________________________________________________________________
Inicjalizacja konstruktorem ___________________________________________________

Listy inicjalizacyjne________________________________________________________
Inicjalizacja skadowych __________________________________________________
Wywoanie konstruktora klasy bazowej ______________________________________
Konstruktory kopiujce ____________________________________________________
O kopiowaniu obiektw __________________________________________________

355
355
356
356
356

357
357
358
359
359

Pole po polu__________________________________________________________________ 360


Gdy to nie wystarcza _________________________________________________________ 360

Konstruktor kopiujcy ___________________________________________________ 361


Do czego suy konstruktor kopiujcy ______________________________________________
Konstruktor kopiujcy a przypisanie - rnica maa lecz wana________________________
Dlaczego konstruktor kopiujcy ________________________________________________
Definiowanie konstruktora kopiujcego ____________________________________________
Inicjalizator klasy CIntArray __________________________________________________

361
362
362
362
363

Konstruktory konwertujce ______________________________________________________


Konstruktor z jednym obowizkowym parametrem _________________________________
Swko explicit ___________________________________________________________
Operatory konwersji ___________________________________________________________
Stwarzamy sobie problem ____________________________________________________
Definiowanie operatora konwersji ______________________________________________

365
365
367
367
368
368

Konwersje_______________________________________________________________ 363
Sposoby dokonywania konwersji ___________________________________________ 364

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 ______________________________________________________________

380
381
381
381

Operatory pamici ______________________________________________________ 382


Alokacja pamici ______________________________________________________________
new ______________________________________________________________________
new[] ____________________________________________________________________
Zwalnianie pamici ____________________________________________________________
delete ___________________________________________________________________
delete[] _________________________________________________________________
Operator sizeof ______________________________________________________________
Ciekawostka: operator __alignof ______________________________________________

382
382
382
383
383
383
383
384

Operatory rzutowania __________________________________________________________


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

384
384
385
385
386
386
386
387

Operatory typw _______________________________________________________ 384

Operatory dostpu do skadowych __________________________________________ 387

Wyuskanie z obiektu __________________________________________________________ 387


Wyuskanie ze wskanika _______________________________________________________ 388
Operator zasigu ______________________________________________________________ 388

Pozostae operatory _____________________________________________________ 388

Nawiasy okrge ______________________________________________________________ 388


Operator warunkowy ___________________________________________________________ 389
Przecinek ____________________________________________________________________ 389

Nowe znaczenia dla operatorw ______________________________________________ 389


Funkcje operatorowe ____________________________________________________ 389
Kilka uwag wstpnych __________________________________________________________
Oglna skadnia funkcji operatorowej____________________________________________
Operatory, ktre moemy przecia ____________________________________________
Czego nie moemy zmieni ___________________________________________________
Pozostae sprawy ___________________________________________________________
Definiowanie przecionych wersji operatorw _______________________________________
Operator jako funkcja skadowa klasy ___________________________________________
Problem przemiennoci_______________________________________________________
Operator jako zwyka funkcja globalna___________________________________________
Operator jako zaprzyjaniona funkcja globalna ____________________________________

390
390
390
391
391
391
392
393
393
394

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 ___________________________________________________________

395
396
397
399
402
404
404
406
407
409
410
411
412

Wskazwki dla pocztkujcego przeciacza __________________________________ 412


Zachowujmy sens, logik i konwencj _____________________________________________
Symbole operatorw powinny odpowiada ich znaczeniom ___________________________
Zapewnijmy analogiczne zachowania jak dla typw wbudowanych _____________________
Nie przeciajmy wszystkiego ____________________________________________________

412
412
413
413

Wskaniki do skadowych klasy ________________________________________________ 414


Wskanik na pole klasy ____________________________________________________ 414
Wskanik do pola wewntrz obiektu ________________________________________ 414
Wskanik na obiekt ____________________________________________________________ 414
Pokazujemy na skadnik obiektu __________________________________________________ 415

Wskanik do pola wewntrz klasy __________________________________________ 415


Miejsce pola w definicji klasy ____________________________________________________
Pobieranie wskanika __________________________________________________________
Deklaracja wskanika na pole klasy _______________________________________________
Uycie wskanika _____________________________________________________________

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 ______________________________________

Podsumowanie _____________________________________________________________
Pytania i zadania _________________________________________________________
Pytania _______________________________________________________________
wiczenia _____________________________________________________________

421
421
423
423
423
425
427

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 ____________________________________________________________

434
435
435
436
436
437

Niezbyt dobre wyjcia ___________________________________________________ 438


Wywoanie zwrotne ____________________________________________________________
Uwaga o wygodnictwie _______________________________________________________
Uwaga o logice _____________________________________________________________
Uwaga o niedostatku mechanizmw_____________________________________________
Zakoczenie programu _________________________________________________________

438
438
439
439
439

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

Porwnanie throw z break i return _______________________________________________


Wyjtek opuszcza funkcj _______________________________________________________
Specyfikacja wyjtkw _______________________________________________________
Kamstwo nie popaca ________________________________________________________
Niezapany wyjtek ____________________________________________________________

450
450
451
451
453

Odwijanie stosu ____________________________________________________________ 449


Midzy rzuceniem a zapaniem_______________________________________________ 449
Wychodzenie na wierzch _________________________________________________ 449

Porzdki ______________________________________________________________ 454


Niszczenie obiektw lokalnych ___________________________________________________
Wypadki przy transporcie _______________________________________________________
Niedozwolone rzucenie wyjtku ________________________________________________
Strefy bezwyjtkowe ________________________________________________________
Skutki wypadku ____________________________________________________________

454
454
454
455
456

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

Organizacja obsugi wyjtkw _____________________________________________ 466


Umiejscowienie blokw try i catch _______________________________________________
Kod warstwowy_____________________________________________________________
Podawanie bdw wyej _____________________________________________________
Dobre wyporodkowanie______________________________________________________
Chwytanie wyjtkw w blokach catch _____________________________________________
Szczegy przodem - druga odsona_____________________________________________
Lepiej referencj____________________________________________________________

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

Naduywanie wyjtkw __________________________________________________ 471


Nie uywajmy ich tam, gdzie wystarcz inne konstrukcje ______________________________ 471
Nie uywajmy wyjtkw na si __________________________________________________ 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 ______________________________

478
479
479
480
480
481
482
482

Wywoywanie funkcji szablonowej __________________________________________ 483


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 ____________________________

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 __________________________________________________

488
489
490
491
492
492
493
494
494
495
495

Korzystanie z klas szablonowych ___________________________________________ 497


Tworzenie obiektw____________________________________________________________
Stwarzamy obiekt klasy szablonowej ____________________________________________
Co si dzieje, gdy tworzymy obiekt szablonu klasy _________________________________
Funkcje operujce na obiektach klas szablonowych ___________________________________

497
497
498
500

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 _________________________________________

501
501
503
504
504
504
506
506
508

Wicej informacji ___________________________________________________________ 508


Parametry szablonw ______________________________________________________ 508
Typy _________________________________________________________________ 509
Przypominamy banalny przykad__________________________________________________ 509
class zamiast typename ________________________________________________________ 510

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 _____________________________________________________________

510
511
512
512
513
513
514

Szablony parametrw ___________________________________________________ 514


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

514
515
516
516

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

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____________________________________________________________________

522
523
523
524
525
525
525
526
526
527
527

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 _________________________________________________________

Podsumowanie _____________________________________________________________
Pytania i zadania _________________________________________________________
Pytania _______________________________________________________________
wiczenia _____________________________________________________________

538
538
538
539
540

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.

Podstawy programowania

20

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

21

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

Podstawy programowania

22

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

23

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

Podstawy programowania

24

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

Podstawy programowania

26

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

Podstawy programowania

28

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.

Podstawy programowania

30

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

Podstawy programowania

32

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

Podstawy programowania

34

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.

Podstawy programowania

36

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

Podstawy programowania

38

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.

Podstawy programowania

40

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?

41

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

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

Podstawy programowania

42

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?

43

Wany jest za to nastpny wiersz:


int nWynik = nLiczba1 + nLiczba2;
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

Podstawy programowania

44

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?

45

i = i + 1;
j = j 1;
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 :)

11

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

Podstawy programowania

46

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.

Podstawy programowania

48

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

Podstawy programowania

50

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;

Podstawy programowania

52

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

14

// ... (reszta funkcji)

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

Podstawy programowania

54

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

Podstawy programowania

56

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

Podstawy programowania

58

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

Podstawy programowania

60

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

Podstawy programowania

62

(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

63

// wypisanie dziesiciu liczb cakowitych w osobnych linijkach


while (nLicznik <= 10)
{
std::cout << nLicznik << std::endl;
nLicznik++;
}
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.
16

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

Podstawy programowania

64

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
}

17

Jak zapewne pamitasz, jedn linijk w bloku kodu moemy zapisa bez nawiasw klamrowych {}
dowiedzielimy si tego przy okazji instrukcji if :)

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.

Podstawy programowania

66

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

Podstawy programowania

70

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

18

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.

Podstawy programowania

72

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

Podstawy programowania

74

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

20

Wikszo jzykw go nie posiada!


Jest to element notacji wgierskiej, aczkolwiek szeroko stosowany przez wielu programistw. Wicej
informacji w Dodatku A.
21

Operacje na zmiennych

75

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

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

Podstawy programowania

76

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

77

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.

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;

23

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

Podstawy programowania

78

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

27

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.

Podstawy programowania

80

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

Podstawy programowania

82

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

83

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

Podstawy programowania

84

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

< 256; i += 4)
"| " << (char)
(char) (i + 1)
(char) (i + 2)
(char) (i + 3)
std::endl;

(i) << " == " <<


<< " == " << i +
<< " == " << i +
<< " == " << i +

i
1
2
3

<<
<<
<<
<<

"
"
"
"

| ";
| ";
| ";
|";

getch();

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

85

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

"
"
"
"

| ";
| ";
| ";
|";

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

Podstawy programowania

86

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

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

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.

Podstawy programowania

88

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

89

fX = sqrt(pow(fY, 2));

// fY

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


x

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

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.

Podstawy programowania

90

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

91

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

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

39

Liczba ta naley do przedziau <0; RAND_MAX>, gdzie RAND_MAX jest sta zdefiniowan przez kompilator (w
Visual C++ .NET ma ona warto 32767).

Podstawy programowania

92

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.

Podstawy programowania

94

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

95

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

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.

Podstawy programowania

96
int nY = +nX;
nY = -nX;

// nY == 5
// nY == -5

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

Podstawy programowania

98

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.

Podstawy programowania

100

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

Podstawy programowania

104

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

105

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.

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

107

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

Podstawy programowania

108

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

109

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;

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.

Podstawy programowania

110

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

111

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.

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

Podstawy programowania

114

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

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

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.

Podstawy programowania

116

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.

50

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.

Podstawy programowania

120

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

121
{ 2, 3, 4, 5, 6 },
{ 3, 4, 5, 6, 7 } };

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:

Podstawy programowania

122

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

123

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

Podstawy programowania

124
nKierunek = PobierzWybranyPrzezGraczaKierunek();
switch (nKierunek)
{
case KIERUNEK_GORA:
case KIERUNEK_DOL:
case KIERUNEK_LEWO:
case KIERUNEK_PRAWO:
default:
}

//
//
//
//
//

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

125

Kod ten bdzie poprawny oczywicie tylko wtedy, gdy funkcja


PobierzWybranyPrzezGraczaKierunek() bdzie zwracaa warto bdc take typu
DIRECTION.
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

Podstawy programowania

126

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

127

z ktrymi stykalimy si na pocztku kursu. Zamiast tego pomwmy lepiej o


zastosowaniach opisywanych typw w konstruowaniu normalnych, przydatnych
programw - take gier.
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.
52

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.

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

Podstawy programowania

130

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

131

if (aKontakty[i].strNick == strNick)
// zwrcenie indeksu pasujcej osoby
return i;

// ewentualnie, jeli nic nie znaleziono, zwracamy -1


return -1;

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

Podstawy programowania

132

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

04-02-06

04-02-05

04-02-04

04-02-03

04-02-02

04-02-01

04-01-31

04-01-30

04-01-29

04-01-28

04-01-27

04-01-26

04-01-25

04-01-24

04-01-23

04-01-22

04-01-21

04-01-20

04-01-19

04-01-18

04-01-17

04-01-16

04-01-15

04-01-14

04-01-13

04-01-12

04-01-11

04-01-10

04-01-09

04-01-08

-50

04-01-07

-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

133
cykl
emocjonalny
intelektualny

dugo
28 dni
33 dni

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

Podstawy programowania

134

// 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 */
// ot i
std::cout
std::cout
std::cout

on
<<
<<
<<
<<
std::cout <<

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

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.

Podstawy programowania

136

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

Podstawy programowania

138

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

139

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

Podstawy programowania

140

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

Podstawy programowania

142

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

145

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

Podstawy programowania

148

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.

Podstawy programowania

150

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;

Podstawy programowania

152

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

Podstawy programowania

154

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

o nastpny ruch
<< "Podaj numer pola, w ktorym" << std::endl;
<< "chcesz postawic ";
<< (g_AktualnyGracz == SGN_CIRCLE ?
"kolko" : "krzyzyk") << ": ";

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;

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.

Podstawy programowania

156

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.

Podstawy programowania

158

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.

Podstawy programowania

162

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.

Podstawy programowania

164

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

Podstawy programowania

166

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.

Podstawy programowania

172

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.

Podstawy programowania

174

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.

Podstawy programowania

176

};

int m_nPole;
public:
int Pole() const

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

Podstawy programowania

180

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.

70

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

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.

Podstawy programowania

184

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

185

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

Podstawy programowania

186
pLampa1->Wlacz();

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

187

wskaniki odnoszce si do tego obiektu! To zreszt naturalne, skoro co do jednego


wskazuj one na t sam, nieaktualn ju lokacj w pamici.

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:

Podstawy programowania

188

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?

wiczenia
1. Zdefiniuj prost klas reprezentujc ksik.
2. Napisz program podobny do przykadu DegreesCalc, ale przeliczajcy midzy
jednostkami informacji (bajtami, kilobajtami itd.).

189

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.

Podstawy programowania

194

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.

Podstawy programowania

196
[deklaracje_publiczne]
};

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.

Podstawy programowania

198

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

Podstawy programowania

200

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

// Ryba

class CMammal : public CAnimal


{
public:
void Biegnij();
};

// Ssak

class CBird : public CAnimal


{
public:
void Lec();
};

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

Podstawy programowania

204

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

205

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

207

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

Podstawy programowania

208

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.

Podstawy programowania

210

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:

Podstawy programowania

214
#include <iostream>
void PokazDaneDokumentu(CDocument* pDokument)
{
// wywietlenie autora
std::cout << "AUTOR: ";
std::cout << pDokument->Autor() << std::endl;

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

215

Eureka! Na tym przecie polega polimorfizm :)


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

Podstawy programowania

216

pDokument->PokazDane();
delete pDokument;

"http://programex.risp.pl/?"
"strona=cyfrowe_przetwarzanie_tekstu"
);

// drugi dokument - ksika


std::cout << std::endl << "--- 2. pozycja ---" << std::endl;
pDokument = new CBook("Sam Williams",
"W obronie wolnosci",
"83-7361-247-5");
pDokument->PokazDane();
delete pDokument;
}

getch();

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

Podstawy programowania

218
// zainicjowanie generatora liczb losowych
srand (static_cast<unsigned>(time(NULL)));

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

81

Obiekt klasy CMammal jest tworzony zarwno poprzez new CMammal, jak i new CHomeDog. Klasa CHomeDog
dziedziczy przecie po klasie CMammal.

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.

Podstawy programowania

220

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

name()

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

raw_name()

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

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
{

Podstawy programowania

222

};

public:
ANIMAL Typ() const { return A_MAMMAL; }

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

225

class CGame
{
private:
CGraphicsSystem* m_pGFX;
CSoundSystem*
m_pSFX;
CNetworkSystem* m_pNet;
// itd.
};

// (reszt skadowych pominiemy)

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

83

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.

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

Podstawy programowania

230
{

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

Podstawy programowania

236

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

237

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

239

Pojedyncze zwizki s natomiast z powodzeniem programowane za pomoc pl,


bdcych wskanikami na obiekty.
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

Podstawy programowania

240
// (dalej definicje obu klas, jak w kodzie wyej)

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

Podstawy programowania

244

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.

Podstawy programowania

246

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.

Podstawy programowania

250

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.

Podstawy programowania

254

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

255

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

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

259

zreszt jak to czasem bywa dla zwykych zmiennych. W takich przypadkach z pomoc
przychodz nam rne metody konwersji typw wskanikowych, jakie oferuje C++.

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.

Podstawy programowania

262

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;

// BD!

Visual C++ zareagowaby za takim oto bdem:


error C2110: '+': cannot add two pointers

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;

Podstawy programowania

266
}

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;

Podstawy programowania

268
}

// w razie niepowodzenia zwracamy -1


return -1;

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

269

int* pnLiczba;
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

Podstawy programowania

delete pnLiczba;
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[]:

Podstawy programowania

272
delete[] pfTablica;

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

273
{ if (uIndeks >= m_uRozmiar) return false;
m_pnTablica[uIndeks] = uWartosc;
return true;

};

// inne
unsigned Rozmiar() const

{ return m_uRozmiar; }

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:

Podstawy programowania

274

#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];

Podstawy programowania

276
Przeanalizuj go dokadnie. Zwr uwag szczeglnie na linijk:
ppnTablica[i] = new int [4];

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

Podstawy programowania

280

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

Podstawy programowania

288

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:

Podstawy programowania

290

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

291

int (*pfnWskaznik)();
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

Podstawy programowania

292

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

Podstawy programowania

294

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

295

przycita z dziedzin funkcji - std te operator warunkowy ?: i porwnanie granicy


przedziau ze wspczynnikiem p.
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
1.
2.
3.
4.
5.
6.
7.
8.
9.

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?

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.

Zaawansowane C++

302

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

305

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.

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

94

Aby zapisa liczb w systemie szesnastkowym, naley j poprzedzi sekwencj 0x lub 0X. Tak wic 0xFF to
dziesitnie 255.

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

Zaawansowane C++

308
int aTablica[SIEDEM];
for (unsigned i = 0; i < SIEDEM; ++i)
std::cout << aTablica[i] << std::endl;
std::cout << "Wypisalem SIEDEM elementow tablicy";

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

Zaawansowane C++

#define PROMIEN 10
#define OBWOD_KOLA (2 * PI * PROMIEN)
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:

Zaawansowane C++

312

#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

Zaawansowane C++

314

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.

Zaawansowane C++

318

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

Zaawansowane C++

322

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:

Zaawansowane C++

324

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

Zaawansowane C++

326

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

329

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

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

1
1

#define PLATFORM
#define WIN_VER

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

98

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

Zaawansowane C++

330

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

#define PLATFORM

1
2
3
4
WINDOWS

#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

331

#else
#error "Nieznany system operacyjny, kompilacja przerwana!"
#endif
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.

100

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

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

Zaawansowane C++

336
"zmodyfikowany: " __TIMESTAMP__)

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

exestr

user

compiler

lib

linker

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.

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.

Zaawansowane C++

340

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

Zaawansowane C++

344

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

345

Deklaracja przyjani moe by umieszczona w kadym miejscu definicji klasy i


zawsze ma to samo znaczenie.
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);

Zaawansowane C++

346

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

347

Pamitaj zatem, i:
Funkcje zaprzyjanione z klas nie s jej skadnikami. Nie posiadaj dostpu do
wskanika this tej klasy, gdy nie s jej metodami.
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;

Zaawansowane C++

348
public:
CVector2D(float fX = 0.0f, float fY = 0.0f)
{ m_fX = fX;
m_fY = fY; }

};

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

349

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

Zaawansowane C++

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;

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.

Zaawansowane C++

352

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:
9 nie mona ich czyni metodami staymi (const)
9 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.
9 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:

Zaawansowane C++

354

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.

Zaawansowane C++

358

staych (pl z przydomkiem const)


staych wskanikw (typ* const)
referencji
obiektw, ktrych klasy nie maj domylnych konstruktorw

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

};

359

public:
CIndirectBase(int nPole1) : m_nPole1(nPole) { }

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.

Zaawansowane C++

360

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

361
{ return m_uRozmiar; }

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

Zaawansowane C++

362

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.

Zaawansowane C++

364

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:

Zaawansowane C++

366

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

Zaawansowane C++

Stwarzamy sobie problem


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

371

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

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.

Zaawansowane C++

372

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.

Zaawansowane C++

374

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

Zaawansowane C++

376

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.

Zaawansowane C++

378

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

379

R-wartoci to oczywicie te, jak i wszystkie inne wyraenia.

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

Zaawansowane C++

380

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

Zaawansowane C++

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

Zaawansowane C++

Ciekawostka: operator __alignof


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

385

rzutowania wskanikw do klas zwizanych relacj dziedziczenia

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.

Zaawansowane C++

386

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

{ int x; };

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.

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

389

Z kolei nadmiar nawiasw jeszcze nikomu nie zaszkodzi :)

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

Zaawansowane C++

390

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)

// OK

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.

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!

Zaawansowane C++

394

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.

Zaawansowane C++

396

};

CVector2D operator+() const


{ return CVector2D(+m_fX, +m_fY); }
CVector2D operator-() const
{ return CVector2D(-m_fY, -m_fY); }

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

// preinkrementacja
CVector2D CVector2D::operator++()

397

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

// postinkrementacja
CVector2D CVector2D::operator++(int)
{
CVector2D vWynik = *this;
++(*this);
return vWynik;
}
// (dekrementacja przebiega analogicznie)
Spostrzemy, e nic nie stoi na przeszkodzie, aby w postinkrementacji uy operatora
preinkrementacji:
++(*this);
Przy okazji mona dostrzec wyranie, dlaczego wariant prefiskowy jest wydajniejszy. W
odmianie przyrostkowej trzeba przecie ponie koszt stworzenia tymczasowego obiektu,
aby go potem zwrci jako rezultat.

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

Zaawansowane C++

398

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

399

W kadym wic przypadku jeden operator*() nie wystarczy109. Musimy doda jego
kolejn wersj:
class CVector2D
{
// (pomijamy szczegy)

};

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

Zaawansowane C++

400

ma skadnik bdcy sta (const typ) lub staym wskanikiem (typ* const)
posiada skadnik bdcy referencj
istnieje prywatny (private) operator przypisania:
9 w klasie bazowej
9 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

401

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

Zaawansowane C++

402

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.

Zaawansowane C++

404

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

405

A oto i odpowied. Kiedy przeciymy operator ->, wyraenie w formie:


obiekt->skadnik
zostanie zmienione na:
(obiekt.operator->())->skadnik
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;

Zaawansowane C++

406

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

407

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

Zaawansowane C++

408

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

//
//
//
//
//

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

Srednia(56);

// rednia z 56

Zaawansowana obiektowo
Srednia(90);
Srednia(4 * atan(1));
std::cout << Srednia(13);

409
// rednia z 56 i 90
// rednia z 56, 90 i pi
// wywietlenie redniej z 56, 90, pi i 13

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.

Zaawansowane C++

410

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

411

Nadal jednak moemy skorzysta z normalnych wersji new i delete - wystarczy


poprzedzi ich nazwy operatorem zakresu:
CFoo* pFoo = ::new CFoo;
// ...
::delete pFoo;
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

Zaawansowane C++

412

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

415

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

Zaawansowane C++

VECTOR3 Wektor;
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

419

p2mfWspolrzedna = &VECTOR3::z;
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:

Zaawansowane C++

420

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

Zaawansowane C++

422
float m_fLiczba;

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.

Zaawansowane C++

424

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

obiektowe
strukturalne

na skadowe
statyczne

dane

wskaniki do zmiennych

kod

wskaniki do funkcji

na skadowe niestatyczne
w klasach
w obiektach
wskaniki do pl
wskaniki do
klasy
zmiennych
wskaniki do
BRAK
metod klasy

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

425

Powiedziaem jednak, e nasze wieo poznane wskaniki mog pomc w poradzeniu


sobie z tym problemem. Zobaczmy jak to zrobi.
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.

Zaawansowane C++

426

drugi to wskanik na metod klasy, ktra ma by wywoywana

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

427

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

Zaawansowane C++

428

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.

116

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.

Zaawansowane C++

430

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

Zaawansowane C++

434

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

Zaawansowane C++

436

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

Zaawansowane C++

};
Zmodyfikowany prototyp bdzie wic wyglda tak:
RESULT StrToUInt(const std::string& strLiczba);
Myl, e nietrudno zgadn, jakie zmiany zajd w treci funkcji.
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

Zaawansowane C++

442

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.

Zaawansowane C++

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

// rzucamy wyjtek
throw 90;

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

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

W bloku try rzucamy jako wyjtek liczb 90. Poniewa nie podajemy jej adnych
przyrostkw, kompilator uznaje, i jest to warto typu int. Nasz obiekt wyjtku jest
wic obiektem typu int, ktry leci na spotkanie swego losu.
Gdzie si zakoczy jego droga? Wszystko zaley od tego, ktry z trzech blokw catch
przechwyci ten wyjtek. Wszystkie one s do tego zdolne: typ int pasuje bowiem
zarwno do typu float, jak i double (no i oczywicie int).
Mwic pasuje, mam tu na myli dokadnie taki sam mechanizm, jaki jest uruchamiany
przy wywoywaniu funkcji z parametrami. Majc bowiem trzy funkcje:
void Funkcja1(float);
void Funkcja2(int);
void Funkcja3(double);
kadej z nich moemy przekaza warto typu int. Naturalnie, jest on najbardziej
zgodna z Funkcja2(), ale pozostae te si do tego nadaj. W ich przypadku zadziaaj
po prostu wbudowane, niejawne konwersje: kompilator zamieni liczb na int na typ
float lub double.
A jednak to tylko cz prawdy. Zgodno typu wyjtku z typem zadeklarowanym w
bloku catch to tylko jedno z kryterium wyboru - w dodatku wcale nie najwaniejsze!
Ot najpierw w gr wchodzi kolejno instrukcji catch. Kompilator przeglda je w takim
samym porzdku, w jakim wystpuj w kodzie, i dla kadej z nich wykonuje test
dopasowania argumentu. Jeli stwierdzi jakkolwiek zgodno (niekoniecznie
najlepsz moliw), ignoruje wszystkie pozostae bloki catch i wybiera ten pierwszy
pasujcy.
Co to znaczy w praktyce? Spjrzmy na nasz przykad. Mamy obiekt typu int, ktry
zostanie kolejno skonfrontowany z typami trzech blokw catch: float, int i double.
Wobec przedstawionych wyej zasad, ktry z nich zostanie wybrany?
Odpowied nie jest trudna. Ju pierwsze dopasowanie int do float zakoczy si
sukcesem. Nie bdzie ono wprawdzie najlepsze (wymaga bdzie niejawnej konwersji),
ale, jak podkresliem, kompilator poprzestanie wanie na nim. Porzdek blokw catch
wemie po prostu gr nad ich zgodnoci.
Pamitaj wic zasad dopasowywania typu obiektu rzuconego do wariantw catch:
Typy w blokach catch s sprawdzane wedle ich kolejnoci w kodzie, a wybierana jest
pierwsza pasujca moliwo. Przy dopasowywaniu brane s pod uwag wszystkie
niejawne konwersje.
Szczeglnie natomiast we sobie do serca, i:
Kolejno blokw catch czsto ma znaczenie.

Wyjtki

445

Mimo e z pozoru przypominaj one funkcje, funkcjami nie s. Obowizuj w nich wic
inne zasady wyboru waciwego wariantu.

Szczegy przodem
Jak w takim razie naley ustawia procedury obsugi wyjtkw, aby dziaay one zgodnie
z naszymi yczeniami? Popatrzmy wpierw na taki przykad:
try
{

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

}
catch
catch
catch
catch

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

Zaawansowane C++

446

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
{

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

Wyjtki

447

}
catch (typ_zewntrzny_1 obiekt_zewntrzny_2)
{
zewntrzne_instrukcje_obsugi_2
}
// ...
dalsze_instrukcje
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
{

for (/* ... */)


{
switch (/* ... */)
{
case 1:
if (/* ... */)
{
// ...
throw obiekt;
}
}
}

}
catch
{
// ...
}

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

Zaawansowane C++

450

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

throw

break

return

przekazywanie
sterowania

do najbliszego
pasujcego bloku
catch

jeden blok wyej


(wyjcie z ptli lub
bloku switch)

warto

obiekt wyjtku
dowolnego typu

nie jest zwizana z


adn wartoci

zakoczenie dziaania
funkcji i powrt do kodu,
ktry j wywoa
warto tego samego
typu, jaki zosta
okrelony w deklaracji
funkcji

zastosowanie

obsuga sytuacji
wyjtkowych

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;

Zaawansowane C++

454

exit (1);

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

455

Ot nie - jest to dozwolone, ale w sumie nie o tym chcemy mwi :) Musimy sobie
powiedzie, co rozumiemy poprzez obsug wyjtku dokonywan przez kompilator.
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

457

delete pFoo;

// zwolnienie obiektu-zasobu

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

Zaawansowane C++

458
throw;
}
// ...
delete pFoo;
}
catch (typ obiekt)
{
// ...
}

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.

Zaawansowane C++

460

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:

Zaawansowane C++

462
#include <memory>

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

463

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

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

Zaawansowane C++

464

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

Zaawansowane C++

466

};

virtual void Wyswietl();

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

Zaawansowane C++

468

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

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.

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.

Zaawansowane C++

472

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

475

return (fLiczba1 > fLiczba2 ? fLiczba1 : fLiczba2);

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

477

Dopki nie wyjanimy sobie dokadnie kwestii umieszczania szablonw w plikach


rdowych, zapamitaj, aby wpisywa je w caoci w plikach nagwkowych.
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)

Zaawansowane C++

478

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

Zaawansowane C++

480

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.

Zaawansowane C++

482
Wyjtkowy przypadek

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

483

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

485

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

487

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

Zaawansowane C++

488

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

489

Przerbmy j zatem na szablon.

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.

Zaawansowane C++

490

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

Zaawansowane C++

492

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

493

double fWynik = 0.0;


for (unsigned i = 0; i < Rozmiar(); ++i)
fWynik += (*this)[i] * aWektor[i];

// zwracamy wynik
return fWynik;

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

Zaawansowane C++

494
if (!(uNowyRozmiar > m_uRozmiar)) return false;
// alokujemy now tablic
TYP* pNowaTablica = new TYP [uNowyRozmiar];

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

Zaawansowane C++

496
// ...
aFloaty1 = aFloaty2;
aFloaty2 = aInty;

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

Zaawansowane C++

498
TArray<long> aLongi;

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.

Zaawansowane C++

500

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

Zaawansowane C++

502

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.

Zaawansowane C++

504

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

505
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; }
// ------------------------------------------------------------// 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:

Zaawansowane C++

506

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.

Zaawansowane C++

508
Skorzystanie z poprzedniego parametru

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:

Zaawansowane C++

510

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.

Zaawansowane C++

512

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

513

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

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

Zaawansowane C++

514

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

Zaawansowane C++

516

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

aDuzaTablica(1000);
aGigaTablica(250000);

rwnie dobrze mogoby si sprawdza co w tym rodzaju:


TNumericArray<float, TOptimizedArray>
TNumericArray<double, TSuperFastArray>

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.

Zaawansowane C++

518

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.

Zaawansowane C++

520

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

521

moliwa interpretacja tej konstrukcji. Czynimy to poprzedzajc problematyczn fraz


swkiem typename:
typename TAB::ELEMENT Wynik;
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:
9 prototypy funkcji
9 deklaracje zapowiadajce zmiennych globalnych (opatrzone sowem
extern)
9 definicje wasnych typw danych i aliasw, wprowadzane sowami typedef,
enum, struct, union i class
9 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:
9 instrukcje funkcji globalnych oraz metod klas
9 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;

Zaawansowane C++

524
std::cin >> fLiczba1;
std::cin >> fLiczba2;

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

Zaawansowane C++

526

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

527

// (dalej implementacja szablonu max())


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.

Zaawansowane C++

528

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?

Zaawansowane C++

530

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

Zaawansowane C++

Jak zadziaa makro


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.

Zaawansowane C++

534

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

535

return Para(true, 42);


Przydomek inline zabezpiecza natomiast przed niewybaczalnym uszczerbkiem na
wydajnoci spowodowanym poredni drog kreacji obiektu.

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

Zaawansowane C++

536
}
// operator nierwnoci
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);

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

537
Drugi = Trojka.Drugi;
Trzeci = Trojka.Trzeci;

};

return *this;

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

Zaawansowane C++

538

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

539
// konstruktor
TStack()
: m_uRozmiar(0)

{ }

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

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

_
__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

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

D
default (instrukcja) 57
defined (operator) 328

Inne

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

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

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

wartoci zwracane 49
zwrotne 295, 424
funkcje zaprzyjanione 345
definiowanie wewntrz klasy 347
deklaracje 345
funktory 408

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

J
Java 25
jzyk kontekstowy 519
jzyk programowania 22
niskiego poziomu 162
wysokiego poziomu 162

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

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

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

547
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

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

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

Inne

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

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

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

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

549
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

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

Inne

550
unsigned (typ) 80

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

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

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