Professional Documents
Culture Documents
Kurs C++ PDF
Kurs C++ PDF
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.
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
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
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
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
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
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
180
180
182
182
183
Podsumowanie _____________________________________________________________
Pytania i zadania _________________________________________________________
Pytania _______________________________________________________________
wiczenia _____________________________________________________________
184
184
185
186
187
188
188
188
189
191
192
193
194
195
195
195
198
199
201
202
205
206
206
207
Polimorfizm______________________________________________________________ 211
Oglny kod do szczeglnych zastosowa_____________________________________ 212
Sprowadzanie do bazy _________________________________________________________ 212
Klasy wiedz same, co naley robi _______________________________________________ 215
7
Praktyczna implementacja z uyciem skadowych statycznych___________________________ 225
Obiekty zasadnicze______________________________________________________
Obiekty narzdziowe ____________________________________________________
Definiowanie odpowiednich klas______________________________________________
Abstrakcja ____________________________________________________________
228
228
231
231
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
252
253
254
255
261
262
263
263
8
Tablice jednowymiarowe ________________________________________________________ 271
Opakowanie w klas ___________________________________________________________ 272
Tablice wielowymiarowe ________________________________________________________ 274
Zastosowania __________________________________________________________
Podsumowanie _____________________________________________________________
Pytania i zadania _________________________________________________________
Pytania _______________________________________________________________
wiczenia _____________________________________________________________
288
289
290
290
291
295
295
296
296
296
301
302
302
303
304
304
306
307
307
307
310
310
310
311
311
311
312
313
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
318
318
318
319
319
319
322
322
323
323
323
324
324
324
324
325
325
326
326
326
327
327
328
328
330
331
331
331
331
332
332
332
333
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
343
345
345
346
348
348
349
351
351
346
347
347
347
353
353
353
354
355
355
355
Listy inicjalizacyjne________________________________________________________
Inicjalizacja skadowych __________________________________________________
Wywoanie konstruktora klasy bazowej ______________________________________
Konstruktory kopiujce ____________________________________________________
O kopiowaniu obiektw __________________________________________________
355
355
356
356
356
357
357
358
359
359
361
362
362
362
363
365
365
367
367
368
368
Konwersje_______________________________________________________________ 363
Sposoby dokonywania konwersji ___________________________________________ 364
11
Cechy operatorw ________________________________________________________ 371
Liczba argumentw _____________________________________________________ 371
Operatory jednoargumentowe____________________________________________________ 372
Operatory dwuargumentowe_____________________________________________________ 372
Priorytet ______________________________________________________________
czno ______________________________________________________________
Operatory w C++ _________________________________________________________
Operatory arytmetyczne _________________________________________________
372
373
373
374
378
378
379
379
380
380
381
381
381
382
382
382
383
383
383
383
384
384
384
385
385
386
386
386
387
390
390
390
391
391
391
392
393
393
394
12
Typowe operatory jednoargumentowe ___________________________________________
Inkrementacja i dekrementacja ________________________________________________
Typowe operatory dwuargumentowe ____________________________________________
Operatory przypisania _______________________________________________________
Operator indeksowania _________________________________________________________
Operatory wyuskania __________________________________________________________
Operator -> _______________________________________________________________
Ciekawostka: operator ->*____________________________________________________
Operator wywoania funkcji______________________________________________________
Operatory zarzdzania pamici __________________________________________________
Lokalne wersje operatorw____________________________________________________
Globalna redefinicja _________________________________________________________
Operatory konwersji ___________________________________________________________
395
396
397
399
402
404
404
406
407
409
410
411
412
412
412
413
413
416
417
418
419
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
438
438
439
439
439
440
441
441
441
13
Szczegy przodem __________________________________________________________
Zagniedone bloki try-catch ___________________________________________________
Zapanie i odrzucenie ________________________________________________________
Blok catch(...), czyli chwytanie wszystkiego ____________________________________
445
446
448
448
450
450
451
451
453
454
454
454
455
456
459
459
461
461
461
466
466
467
467
468
468
469
Podsumowanie _____________________________________________________________
Pytania i zadania _________________________________________________________
Pytania _______________________________________________________________
wiczenia _____________________________________________________________
472
472
472
472
474
474
475
475
475
475
14
Co moe by szablonem ______________________________________________________ 477
478
479
479
480
480
481
482
482
484
484
484
485
485
486
488
489
490
491
492
492
493
494
494
495
495
497
497
498
500
501
501
503
504
504
504
506
506
508
510
511
512
512
513
513
514
514
515
516
516
518
518
518
519
15
Sowo kluczowe typename ____________________________________________________ 520
Ciekawostka: konstrukcje ::template, .template i ->template ______________________ 521
522
523
523
524
525
525
525
526
526
527
527
529
529
529
530
530
530
531
531
531
531
532
532
532
533
533
534
535
536
Podsumowanie _____________________________________________________________
Pytania i zadania _________________________________________________________
Pytania _______________________________________________________________
wiczenia _____________________________________________________________
538
538
538
539
540
540
541
541
541
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.
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.
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
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
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
Podstawy programowania
24
3. C++
C++ jest teraz chyba najpopularniejszym jzykiem do zastosowa wszelakich.
Powstao do niego bardzo wiele kompilatorw pod rne systemy operacyjne i
dlatego jest uwaany za najbardziej przenony. Istnieje jednak druga strona
medalu mnogo tych narzdzi prowadzi do niewielkiego rozgardiaszu i pewnych
Krtko o programowaniu
25
trudnoci w wyborze ktrego z nich. Na szczcie sam jzyk zosta w 1997 roku
ostatecznie ustandaryzowany.
O C++ nie mwi si zwykle, e jest atwy by moe ze wzgldu na dosy
skondensowan skadni (na przykad odpowiednikiem pascalowych sw begin i
end s po prostu nawiasy klamrowe { i }). To jednak dosy powierzchowne
przekonanie, a sam jzyk jest spjny i logiczny.
Jeeli chodzi o moliwoci, to w przypadku C++ s one bardzo due w sumie
mona powiedzie, e nieco wiksze ni Delphi. Jest on te chyba najbardziej
elastyczny niejako dopasowuje si do preferencji programisty.
4. Java
Ostatnimi czasy Java staa si niemal czci kultury masowej wystarczy choby
wspomnie o telefonach komrkowych i przeznaczonych do aplikacjach. Ilustruje
to dobrze gwny cel Javy, a mianowicie przenono i to nie kodu, lecz
skompilowanych programw! Osignito to poprzez kompilacj do tzw. bytecode,
ktry jest wykonywany w ramach specjalnej maszyny wirtualnej. W ten sposb,
program w Javie moe by uruchamiany na kadej platformie, do ktrej istnieje
maszyna wirtualna Javy a istnieje prawie na wszystkich, od Windowsa przez
Linux, OS/2, QNX, BeOS, palmtopy czy wreszcie nawet telefony komrkowe. Z
tego wanie powodu Java jest wykorzystywana do pisania niewielkich programw
umieszczanych na stronach WWW, tak zwanych apletw.
Cen za t przenono jest rzecz jasna szybko bytecode Javy dziaa znacznie
wolniej ni zwyky kod maszynowy, w dodatku jest strasznie pamicioerny.
Poniewa jednak zastosowaniem tego jzyka nie s wielkie i wymagajce
aplikacje, lecz proste programy, nie jest to a tak wielki mankament.
Skadniowo Java bardzo przypomina C++.
5. PHP
PHP (skrt od Hypertext Preprocessor) jest jzykiem uywanym przede wszystkim
w zastosowaniach internetowych, dokadniej na stronach WWW. Pozwala doda im
znacznie wiksz funkcjonalno ni ta oferowana przez zwyky HTML. Obecnie
miliony serwisw wykorzystuje PHP du rol w tym sukcesie ma zapewne jego
licencja, oparta na zasadach Open Source (czyli brak ogranicze w
rozprowadzaniu i modyfikacji).
Moliwoci PHP s cakiem due, nie mona tego jednak powiedzie o szybkoci
jest to jzyk interpretowany. Jednake w przypadku gwnego zastosowania PHP,
czyli obsudze serwisw internetowych, nie ma ona wikszego znaczenia czas
Podstawy programowania
26
Screen 6. Popularny skrypt forw dyskusyjnych, phpBB, take dziaa w oparciu o PHP
To oczywicie nie wszystkie jzyki jak ju pisaem, jest ich cae mnstwo. Jednake w
ogromnej wikszoci przypadkw gwn rnic midzy nimi jest skadnia, a wic
sprawa mao istotna (szczeglnie, jeeli dysponuje si dobr dokumentacj :D). Z tego
powodu poznanie jednego z nich bardzo uatwia nauk nastpnych po prostu im wicej
jzykw ju znasz, tym atwiej uczysz si nastpnych :)
Krtko o programowaniu
27
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)
Podstawy programowania
28
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
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:
31
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
Wybierz wic pozycj Console application na licie Application type. Dodatkowo zaznacz
te opcj Empty project spowoduje to utworzenie pustego projektu, a oto nam
aktualnie chodzi.
Pierwszy program
Gdy wreszcie ustalimy i zatwierdzimy wszystkie opcje projektu, moemy przystpi do
waciwej czci tworzenia programu, czyli kodowania.
Aby doda do naszego projektu pusty plik z kodem rdowym, wybierz pozycj menu
Project|Add New Item. W okienku, ktre si pojawi, w polu Templates zaznacz ikon
C++ File (.cpp), a jako nazw wpisz po prostu main. W ten sposb utworzysz plik
main.cpp, ktry wypenimy kodem naszego programu.
Plik ten zostanie od razu otwarty, wic moesz bez zwoki wpisa do taki oto kod:
// First - pierwszy program w C++
#include <iostream>
#include <conio.h>
void main()
{
std::cout << "Hurra! Napisalem pierwszy program w C++!" << std::endl;
getch();
}
Tak jest, to wszystko te kilka linijek kodu skadaj si na cay nasz program. Pewnie
niezbyt wielka to teraz pociecha, bo w kod jest dla ciebie zapewne troch niejasny, ale
spokojnie powoli wszystko sobie wyjanimy :)
33
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 :)
Podobny optymizm jest zazwyczaj grub przesad i moemy sobie na niego pozwoli tylko w najprostszych
programach, takich jak niniejszy :)
35
Niniejsze dwa nale do przestrzeni std, gdy s czci Biblioteki Standardowej jzyka
C++ (wszystkie jej elementy nale do tej przestrzeni). Moemy uwolni si od
koniecznoci pisania przedrostka przestrzeni nazw std, jeeli przed funkcj main()
umiecimy deklaracj using namespace std;. Wtedy moglibymy uywa krtszych
nazw cout i endl.
Konkludujc: strumie wyjcia pozwala nam na wywietlanie tekstu w konsoli, za
uywamy go poprzez std::cout oraz strzaki <<.
***
Druga linijka funkcji main() jest bardzo krtka:
getch();
Podobnie krtko powiem wic, e odpowiada ona za oczekiwanie programu na nacinicie
dowolnego klawisza. Traktujc rzecz cilej, getch()jest funkcj podobnie jak main(),
jednake do zwizanego z tym faktem zagadnienia przejdziemy nieco pniej. Na razie
zapamitaj, i jest to jeden ze sposobw na wstrzymanie dziaania programu do
momentu wcinicia przez uytkownika dowolnego klawisza na klawiaturze.
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
37
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
Przebieg programu
Konkluzj naszej przygody z funkcjami i prac krokow bdzie diagram, obrazujcy
dziaanie programu od pocztku do koca:
Czarne linie ze strzakami oznaczaj wywoywanie i powrt z funkcji, za due biae ich
wykonywanie. Program zaczyna si u z lewej strony schematu, a koczy po prawej;
zauwamy te, e w obu tych miejscach wykonywan funkcj jest main(). Prawd jest
zatem fakt, i to ona jest gwn czci aplikacji konsolowej.
Jeli opis sowny nie by dla ciebie nie do koca zrozumiay, to ten schemat powinien
wyjani wszystkie wtpliwoci :)
10
39
Zmienne i stae
Umiemy ju wypisywa tekst w konsoli i tworzy wasne funkcje. Niestety, nasze
programy s na razie zupenie bierne, jeeli chodzi o kontakt z uytkownikiem. Nie ma on
przy nich nic do roboty oprcz przeczytania komunikatu i wcinicia dowolnego klawisza.
Najwyszy czas to zmieni. Napiszmy wic program, ktry bdzie porozumiewa si z
uytkownikiem. Moe on wyglda na przykad tak:
// Input uycie zmiennych i strumienia wejcia
#include <string>
#include <iostream>
#include <conio.h>
void main()
{
std::string strImie;
std::cout << "Podaj swoje imie: ";
std::cin >> strImie;
std::cout << "Twoje imie to " << strImie << "." << std::endl;
}
getch();
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)
By moe zauwaye, e na pocztku kadej nazwy widnieje tu przedrostek, np. str czy
n. Jest to tak zwana notacja wgierska; pozwala ona m.in. rozrni typ zmiennej na
podstawie nazwy. Zapis ten sta si bardzo popularny, szczeglnie wrd programistw
jzyka C++ - spora ich cz uwaa, e znacznie poprawia on czytelno kodu.
Szerszy opis notacji wgierskiej moesz znale w Dodatku A.
Strumie wejcia
C by nam jednak byo po zmiennych, jeli nie mielimy skd wzi dla nich danych?
Prostych sposobem uzyskania ich jest proba do uytkownika o wpisanie odpowiednich
informacji z klawiatury. Tak te czynimy w aktualnie analizowanym programie
odpowiada za to kod:
std::cin >> strImie;
Wyglda on podobnie do tego, ktry jest odpowiedzialny za wypisywanie tekstu w
konsoli. Wykonuje jednak czynno dokadnie odwrotn: pozwala na wprowadzenie
sekwencji znakw i zapisuje j do zmiennej strImie.
std::cin symbolizuje strumie wejcia, ktry zadaniem jest wanie pobieranie
wpisanego przez uytkownika tekstu. Nastpnie kieruje go (co obrazuj strzaki >>) do
wskazanej przez nas zmiennej.
Zauwamy, e w naszej aplikacji kursor pojawia si w tej samej linijce, co komunikat
Podaj swoje imi. Nietrudno domyle si, dlaczego nie umiecilimy po nim
std::endl, wobec czego nie jest wykonywane przejcie do nastpnego wiersza.
41
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.
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
// :-)
// 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;
43
opis
dodawanie
odejmowanie
mnoenie
dzielenie
reszta z dzielenia
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)
*, /, %
+, -
Najlepiej jednak nie polega na tej wasnoci operatorw i uywa nawiasw w przypadku
jakichkolwiek wtpliwoci.
Nawiasy chroni przed trudnymi do wykrycia bdami zwizanymi z pierwszestwem
operatorw, dlatego stosuj je w przypadku kadej wtpliwoci co do kolejnoci dziaa.
***
W taki oto sposb zapoznalimy si wanie z operatorami arytmetycznymi.
Tajemnicze znaki
Twrcy jzyka C++ mieli chyba na uwadze oszczdno palcw i klawiatur programistw,
uczynili wic jego skadni wyjtkowo zwart i dodali kilka mechanizmw skracajcych
zapis kodu. Z jednym z nich, bardzo czsto wykorzystywanym, zapoznamy si za chwil.
Ot instrukcje w rodzaju
nZmienna = nZmienna + nInnaZmienna;
nX = nX * 10;
45
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
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
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;
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
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:
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
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
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.
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
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
"
"
"
" = "
getch();
Dziaanie programu
57
dokadniej jego kod rdowy - zwaywszy, i zawiera interesujc nas w tym momencie
instrukcj switch.
Zajmuje ona zreszt pokan cz listingu; na dodatek jest to ten fragment, w ktrym
wykonywane s obliczenia, bdce podstaw dziaania programu. Jaka jest zatem rola tej
konstrukcji?
C, nie jest trudno domyle si jej skoro mamy w naszym programie menu,
bdziemy te mieli kilka wariantw jego dziaania. Wybranie przez uytkownika jednego z
nich zostaje wcielone w ycie wanie poprzez instrukcj switch. Porwnuje ona kolejno
warto zmiennej nOpcja (do ktrej zapisujemy numer wskazanej pozycji menu) z
picioma wczeniej ustalonymi przypadkami. Kademu z nich odpowiada fragment kodu,
zaczynajcy si od swka case (przypadek) i koczcy na break; (przerwij). Gdy
ktry z nich zostanie uznany za waciwy (na podstawie wartoci wspomnianej ju
zmiennej), wykonywane s zawarte w nim instrukcje. Jeeli za aden nie bdzie
pasowa, program skoczy do dodatkowego wariantu default (domylny) i uruchomi
jego kod. Ot, i caa filozofia :)
Po tym pobienym wyjanieniu dziaania instrukcji switch, poznamy jej pen posta
skadniow:
switch (wyraenie)
{
case warto_1:
instrukcje_1
[break;]
case warto_2:
instrukcje_2
[break;]
...
case warto_n;
instrukcje_n;
[break;]
[default:
instrukcje_domylne]
}
Korzystajc z niej, jeszcze prociej zrozumie przeznaczenie konstrukcji switch oraz
wykonywane przez ni czynnoci. Mianowicie, oblicza ona wpierw wynik wyraenia, by
potem porwnywa go kolejno z podanymi (w instrukcjach case) wartociami. Kiedy
stwierdzi, e zachodzi rwno, skacze na pocztek pasujcego wariantu i wykonuje cay
kod a do koca bloku switch.
Zaraz jak to do koca bloku? Przecie w naszym przykadowym programie, gdy
wybralimy, powiedzmy, operacj odejmowania, to otrzymywalimy wycznie rnic
liczb bez iloczynu i ilorazu (czyli dalszych opcji). Przyczyna tego tkwi w instrukcji
Podstawy programowania
58
Ptle
Ptle (ang. loops), zwane te instrukcjami iteracyjnymi, stanowi podstaw prawie
wszystkich algorytmw. Lwia cz zada wykonywanych przez programy komputerowe
opiera si w caoci lub czciowo wanie na ptlach.
Ptla to element jzyka programowania, pozwalajcy na wielokrotne, kontrolowane
wykonywanie wybranego fragmentu kodu.
Liczba takich powtrze (zwanych cyklami lub iteracjami ptli) jest przy tym
ograniczona w zasadzie tylko inwencj i rozsdkiem programisty. Te potne narzdzia
daj wic moliwo zrealizowania niemal kadego algorytmu.
Ptle s te niewtpliwie jednym z atutw C++: ich elastyczno i prostota jest wiksza
ni w wielu innych jzykach programowania. Jeeli zatem bdziesz kiedy kodowa jak
zoon funkcj przy uyciu skomplikowanych ptli, z pewnoci przypomnisz sobie i
docenisz te zalety :)
Ptla do
Prosty przykad obrazujcy ten mechanizm prezentuje si nastpujco:
// Do pierwsza ptla warunkowa
Dziaanie programu
59
#include <iostream>
#include <conio.h>
void main()
{
int nLiczba;
do
{
Program ten, podobnie jak jeden z poprzednich, oczekuje od nas o liczby wikszej ni
dziesi. Tym razem jednak nie daje si zby byle czym - jeeli nie bdziemy skonni od
razu przychyli si do jego proby, bdzie j niezomnie powtarza a do skutku (lub do
uycia Ctrl+Alt+Del ;D).
Upr naszej aplikacji bierze si oczywicie z umieszczonej wewntrz niej ptli do (czy) .
Wykonuje ona kod odpowiedzialny za prob do uytkownika tak dugo, jak dugo ten
jest konsekwentny w ignorowaniu jej :) Przejawia si to rzecz jasna wprowadzaniem
liczb, ktre nie s wiksze od 10, lecz mniejsze lub rwne tej wartoci odpowiada to
warunkowi ptli nLiczba <= 10. Instrukcja niniejsza wykonuje si wic dopty, dopki
(ang. while) zmienna nLiczba, ktra przechowuje liczb pobran od uytkownika, nie
przekracza granicznej wartoci dziesiciu. Przedstawia to pogldowo poniszy diagram:
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
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.
Podstawy programowania
62
Dziaanie programu
63
std::cout << "Suma liczb od 1 do " << nLiczba << " wynosi "
<< Suma(nLiczba) << ".";
getch();
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:
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
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
{
if (nLiczba == 0) break;
} while (nLiczba <= 10);
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
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
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;
}
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();
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();
Operacje na zmiennych
73
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
Podstawy programowania
74
int nX = 7;
std::cout << "Lokalna zmienna nX: " << nX << std::endl;
std::cout << "Modulowa zmienna nX: " << ::nX << std::endl;
}
getch();
getch();
Modyfikatory zmiennych
W aktualnym podrozdziale szczeglnie upodobalimy sobie deklaracje zmiennych. Oto
bowiem omwimy kolejne zagadnienie z nimi zwizane tak zwane modyfikatory
20
Operacje na zmiennych
75
Zmienne statyczne
Kiedy aplikacja opuszcza zakres zmiennej lokalnej, wtedy ta jest usuwana z pamici. To
cakowicie naturalne po co zachowywa zmienn, do ktrej i tak nie byoby dostpu?
Logiczniejsze jest zaoszczdzenie pamici operacyjnej i pozbycie si nieuywanej
wartoci, co te program skrztnie czyni. Z tego powodu przy ponownym wejciu w
porzucony wczeniej zasig wszystkie podlegajce mu zmienne bd ustawione na swe
pocztkowe wartoci.
Niekiedy jest to zachowanie niepodane czasem wolelibymy, aby zmienne lokalne nie
traciy swoich wartoci w takich sytuacjach. Najlepszym rozwizaniem jest wtedy uycie
modyfikatora static. Rzumy okiem na poniszy przykad:
// Static - zmienne statyczne
void Funkcja()
{
static int nLicznik = 0;
++nLicznik;
std::cout << "Funkcje wywolano po raz " << nLicznik << std::endl;
void main()
{
std::string strWybor;
do
{
Funkcja();
std::cout << "Wpisz 'q', aby zakonczyc: ";
std::cin >> strWybor;
} while (strWybor != "q");
}
w program jest raczej trywialny i jego jedynym zadaniem jest kilkukrotne uruchomienie
podprogramu Funkcja(), dopki yczliwy uytkownik na to pozwala :) We wntrzu teje
funkcji mamy zadeklarowan zmienn statyczn, ktra suy tam jako licznik
uruchomie.
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
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.
23
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)
26
1 bajt to 8 bitw.
Operacje na zmiennych
79
C znacz jednak te, nieco artobliwe, okrelenia krtkiej i dugiej liczby? Chyba
najlepsz odpowiedzi bdzie tu stosowna tabelka :)
nazwa
int
short int
long int
rozmiar
4 bajty
2 bajty
4 bajty
przedzia wartoci
od 231 do +231 - 1
od -32 768 do +32 767
od 231 do +231 - 1
Niespodziank moe by brak typu o rozmiarze 1 bajta. Jest on jednak obecny w C++
to typ char :) Owszem, reprezentuje on znak. Nie zapominajmy jednak, e komputer
operuje na znakach jak na odpowiadajcym im kodom liczbowym. Dlatego te typ char
jest w istocie take typem liczb cakowitych!
Visual C++ udostpnia te nieco lepszy sposb na okrelenie wielkoci typu liczbowego.
Jest nim uycie frazy __intn, gdzie n oznacza rozmiar zmiennej w bitach. Oto przykady:
__int8 nZmienna;
__int16 nZmienna;
__int32 nZmienna;
__int64 nZmienna;
//
//
//
//
__int8 jest wic rwny typowi char, __int16 short int, a __int32 int lub long
int. Gigantyczny typ __int64 nie ma natomiast swojego odpowiednika.
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
rozmiar
4 bajty
8 bajtw
precyzja
67 cyfr
15-16 cyfr
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
<<
<<
<<
<<
<<
<<
getch();
Uruchomienie programu z listingu powyej, jak susznie mona przypuszcza, bdzie nam
skutkowao krtkim zestawieniem rozmiarw typw podstawowych.
29
W chwili pisania tych sw pod koniec roku 2003 mamy ju coraz wyraniejsze widoki na powane
wykorzystanie procesorw 64-bitowych w domowych komputerach. Jednym ze skutkw tego zwikszenia
bitowoci bdzie zmiana rozmiaru typu liczbowego int.
Operacje na zmiennych
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
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.
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
==
==
==
==
"
"
"
"
<<
<<
<<
<<
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
Operacje na zmiennych
87
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);
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
x=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.
// 1
// e
// 10.000000
// 0
// 2.302585093
// x
37
Tak zwanej staej Nepera, podstawy logarytmw naturalnych - rwnej w przyblieniu 2.71828182845904.
Podstawy programowania
90
log a x =
log b x
log b a
Funkcje trygonometryczne
Dla nas, (przyszych) programistw gier, funkcje trygonometryczne s szczeglnie
przydatne, gdy bdziemy korzysta z nich niezwykle czsto choby przy rnorakich
obrotach. Wypadaoby zatem dobrze zna ich odpowiedniki w jzyku C++.
Na pocztek przypomnijmy sobie (znane, mam nadziej :D) okrelenia funkcji
trygonometrycznych. Posuy nam do tego poniszy rysunek:
38
Operacje na zmiennych
91
// sinus
// cosinus
// tangens
{ return 1 / tan(alfa); }
{ return 1 / cos(alfa); }
{ return 1 / sin(alfa); }
// cotangens
// secant
// cosecant
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
Funkcja ta zwraca liczb sekund, jakie upyny od pnocy 1 stycznia 1970 roku.
Operacje na zmiennych
93
getch();
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
// 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 :)
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.
Operacje na zmiennych
97
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
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
typ znaku
char
wchar_t
rozmiar znaku
1 bajt
2 bajty
zastosowanie
tylko znaki ANSI
znaki ANSI i Unicode
Inicjalizacja
Najprostsza deklaracja zmiennej tekstowej wyglda, jak wiemy, mniej wicej tak:
std::string strNapis;
Operacje na zmiennych
101
Wprowadzona w ten sposb nowa zmienna jest z pocztku cakiem pusta - nie zawiera
adnych znakw. Jeeli chcemy zmieni ten stan rzeczy, moemy j zainicjalizowa
odpowiednim tekstem - tak:
std::string strNapis = "To jest jakis tekst";
albo tak:
std::string strNapis("To jest jakis tekst");
Ten drugi zapis bardzo przypomina wywoanie funkcji. Istotnie, ma on z nimi wiele
wsplnego - na tyle duo, e moliwe jest nawet zastosowanie drugiego parametru, na
przykad:
std::string strNapis("To jest jakis tekst", 7);
Jaki efekt otrzymamy t drog? Ot do naszej zmiennej zostanie przypisany jedynie
fragment podanego tekstu - dokadniej mwic, bdzie to podana w drugim parametrze
ilo znakw, liczonych od pocztku napisu. U nas jest to zatem sekwencja "To jest".
Co ciekawe, to wcale nie s wszystkie sposoby na inicjalizacj zmiennej tekstowej.
Poznamy jeszcze jeden, ktry jest wyjtkowo uyteczny. Pozwala bowiem na uzyskanie
cile okrelonego kawaka danego tekstu. Rzumy okiem na poniszy kod, aby
zrozumie t metod:
std::string strNapis1 = "Jakis krotki tekst";
std::string strNapis2(strNapis1, 6, 6);
Tym razem mamy a dwa parametry, ktre razem okrelaj fragment tekstu zawartego
w zmiennej strNapis1. Pierwszy z nich (6) to indeks pierwszego znaku tego fragmentu
- tutaj wskazuje on na sidmy znak w tekcie (gdy znaki liczymy zawsze od zera!).
Drugi parametr (znowu 6) precyzuje natomiast dugo podanego urywka - bdzie on
w tym przypadku szecioznakowy.
Jeeli takie opisowe wyjanienie nie bardzo do ciebie przemawia, spjrz na ten
pogldowy rysunek:
Wida wic czarno na biaym (i na zielonym :)), e kopiowan czci tekstu jest wyraz
"krotki".
102
Podstawy programowania
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
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.
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
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.
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
b
prawda
fasz
prawda
fasz
a && b
prawda
fasz
fasz
fasz
a || b
prawda
prawda
prawda
fasz
a
prawda
fasz
!a
fasz
prawda
Podstawy programowania
108
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;
}
}
46
Nazwa pochodzi od nazwiska matematyka Georgea Boolea, twrcy zasad logiki matematycznej (zwanej te
algebr Boolea).
Operacje na zmiennych
109
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();
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
Podsumowanie
Nadludzkim wysikiem dobrnlimy wreszcie do samego koca tego niezwykle dugiego i
niezwykle wanego rozdziau. Poznae tutaj wikszo szczegw dotyczcych
zmiennych oraz trzech podstawowych typw wyrae. Cay ten baga bdzie ci bardzo
48
Operacje na zmiennych
111
Pytania i zadania
Nieubaganie zblia si starcie z prac domow ;) Postaraj si zatem odpowiedzie na
ponisze pytania oraz wykona zadania.
Pytania
1.
2.
3.
4.
5.
6.
7.
8.
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!!!
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
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
Zoone zmienne
117
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
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
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:
Zoone zmienne
121
{ 2, 3, 4, 5, 6 },
{ 3, 4, 5, 6, 7 } };
Otrzymany efekt jest zreszt taki sam, jak ten osignity przez dwie wczeniejsze,
zagniedone ptle.
Warto rwnie wiedzie, e inicjalizujc tablic wielowymiarow moemy pomin
wielko pierwszego wymiaru:
int aTablica[][5] = { {
{
{
{
0,
1,
2,
3,
1,
2,
3,
4,
2,
3,
4,
5,
3,
4,
5,
6,
4
5
6
7
},
},
},
} };
Tablice w tablicy
Sposb obsugi tablic wielowymiarowych w C++ rni si zasadniczo od podobnych
mechanizmw w wielu innych jzykach. Tutaj bowiem nie s one traktowane wyjtkowo,
jako byty odrbne od swoich jednowymiarowych towarzyszy. Powoduje to, e w C++
dozwolone s pewne operacje, na ktre nie pozwala wikszo pozostaych jzykw
programowania.
Dzieje si to za przyczyn do ciekawego pomysu potraktowania tablic
wielowymiarowych jako zwykych tablic jednowymiarowych, ktrych elementami s
inne tablice! Brzmi to troch topornie, ale w istocie nie jest takie trudne, jak by moe
wyglda :)
Najprostszy przykad tego faktu, z jakim mielimy ju do czynienia, to konstrukcja
dwuwymiarowa. Z punktu widzenia C++ jest ona jednowymiarow tablic swoich
wierszy; zwrcilimy zreszt na to uwag, dokonujc jej inicjalizacji. Kady z owych
wierszy jest za take jednowymiarow tablic, tym razem skadajc si ju ze
zwykych, skalarnych elementw.
Zjawisko to (oraz kilka innych ;D) niele obrazuje poniszy diagram:
Podstawy programowania
122
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.
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? :)
// ...
// ...
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
[ = 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 ;)
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
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 :)
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
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;
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
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
Podstawy programowania
134
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;
Jaki jest efekt tego pokanych rozmiarw listingu? S nim trzy wartoci okrelajce
dzisiejszy biorytm osoby o podanej dacie urodzenia:
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
Podstawy programowania
136
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];
};
Zoone zmienne
137
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
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.
Zoone zmienne
139
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
Zoone zmienne
141
Interfejs uytkownika
Hmm, jaki interfejs?
Zazwyczaj pojcie to utosamiamy z okienkami, przyciskami, pola tekstowymi, paskami
przewijania i innymi zdobyczami graficznych systemw operacyjnych. Tymczasem termin
ten ma bardziej szersze znaczenie:
Interfejs uytkownika (ang. user interface) to sposb, w jaki aplikacja prowadzi dialog
z obsugujcymi j osobami. Obejmuje to zarwno pobieranie od nich danych
wejciowych, jak i prezentacj wynikw pracy.
Niewtpliwie wic moemy czu si uprawnieni, aby nazwa nasz skromn konsol
penowartociowym rodkiem do realizacji interfejsu uytkownika! Pozwala ona przecie
zarwno na uzyskiwanie informacji od osoby siedzcej za klawiatur, jak i na
wypisywanie przeznaczonych dla niej komunikatw programu.
Jak zatem mgby wyglda interfejs naszego programu? Twoje dotychczasowe, bogate
dowiadczenie z aplikacjami konsolowymi powinny uatwi ci odpowied na to pytanie.
Informacja, ktr prezentujemy uytkownikowi, to oczywicie aktualny stan planszy. Nie
bdzie ona wprawdzie miaa postaci rysunkowej, jednake zwyky tekst cakiem dobrze
sprawdzi si w roli grafiki.
Po wywietleniu biecego stanu rozgrywki mona poprosi o gracza o wykonanie
swojego ruchu. Gdybymy mogli obsuy myszk, wtedy posunicie byoby po prostu
klikniciem, ale w tym wypadku musimy zadowoli si poleceniem wpisanym z
klawiatury.
Ostatecznie wygld naszego programu moe by podobny do poniszego:
Przy okazji zauway mona jedno z rozwiza problemu pt. Jak umoliwi wykonywanie
ruchw, posugujc si jedynie klawiatur? Jest nim tutaj ponumerowanie kolejnych
elementw tablicy-planszy liczbami od 1 do 9, a nastpnie proba do gracza o podanie
jednej z nich. To chyba najwygodniejsza forma gry, jak potrafimy osign w tych
niesprzyjajcych, tekstowych warunkach
Podstawy programowania
142
Kodowanie
Nareszcie moemy uruchomi swoje ulubione rodowisko programistyczne, wspierajce
ulubiony jzyk programowania C++ i zacz waciwe programowanie zaprojektowanej
ju gry. Uczy to wic, stwrz w nim nowy projekt, nazywajc go dowolnie54, i czekaj na
dalsze rozkazy ;D
54
Kompletny kod caej aplikacji jest zawarty w przykadach do tego rozdziau i opatrzony nazw TicTacToe.
Zoone zmienne
143
Po co nam taki wasny nagwek? W jakim celu w ogle tworzy nagwki we wasnych
projektach?
Na powysze pytania istnieje dosy prosta odpowied. Aby j pozna przypomnijmy
sobie, dlaczego doczamy do naszych programw nagwki w rodzaju iostream czy
conio.h. Hmm?
Tak jest - dziki nim jestemy w stanie korzysta z takich dobrodziejstw jzyka C++ jak
strumienie wejcia i wyjcia czy acuchy znakw. Generalizujc, mona powiedzie, e
nagwki udostpniaj pewien kod wszystkim moduom, ktre docz je przy pomocy
dyrektywy #include.
Dotychczas nie zastanawialimy si zbytnio nad miejscem, w ktrym egzystuje kod
wykorzystywany przez nas za porednictwem nagwkw. Faktycznie moe on znajdowa
si tu obok - w innym module tego samego projektu (i tak bdzie u nas), lecz rwnie
dobrze istnie jedynie w skompilowanej postaci, na przykad biblioteki DLL.
W przypadku dodanego wanie nagwka game.h mamy jednak niczym nieskrpowany
dostp do odpowiadajcego mu moduu game.cpp. Zdawaoby si zatem, e plik
nagwkowy jest tu cakowicie zbdny, a z kodu zawartego we wspomnianym module
moglibymy z powodzeniem korzysta bezporednio.
Nic bardziej bdnego! Za uyciem pliku nagwkowego przemawia wiele argumentw, a
jednym z najwaniejszych jest zasada ograniczonego zaufania. Wedug niej kada
czstka programu powinna posiada dostp jedynie do tych jego fragmentw, ktre s
niezbdne do jej prawidowego funkcjonowania.
U nas t czstk bdzie funkcja main(), zawarta w module main.cpp. Nie napisalimy jej
jeszcze, ale potrafimy ju okreli, czego bdzie potrzebowaa do swego poprawnego
dziaania. Bez wtpienia bd dla konieczne funkcje odpowiedzialne za wykonywanie
posuni wskazanych przez graczy czy te procedury wywietlajce aktualny stan
rozgrywki. Sposb, w jaki te zadania s realizowane, nie ma jednak adnego znaczenia!
144
Podstawy programowania
55
Nie chodzi tu o podrcznik uytkownika programu, ale raczej o jego dokumentacj techniczn, czyli opis
dziaania aplikacji od strony programisty.
Zoone zmienne
145
146
Podstawy programowania
Zaczynamy
Skoro wiemy ju dokadnie, jak wygldaj wizytwki naszych funkcji oraz z grubsza
znamy naleyte algorytmy ich dziaania, napisanie odpowiedniego kodu powinno by po
prostu dziecinn igraszk, prawda? :) Dobre samopoczucie moe si jednak okaza
przedwczesne, gdy na twoim obecnym poziomie zaawansowania zadanie to wcale nie
naley do najatwiejszych. Nie zostawi ci jednak bez pomocy!
Dla szczeglnie ambitnych proponuj aczkolwiek samodzielne dokoczenie caego
programu, a nastpnie porwnanie go z kodem doczonym do kursu. Samodzielne
rozwizywanie problemw jest bowiem istot i najlepsz drog nauki programowania!
Podczas zmagania si z tym wyzwaniem moesz jednak (i zapewne bdziesz musia)
korzysta z innych rde informacji na temat programowania w C++, na przykad MSDN.
Wiadomociami, ktre niemal na pewno oka ci si przydatne, s dokadne informacje o
plikach nagwkowych i zwizanej z nimi dyrektywie #include oraz sowie kluczowym
extern. Poszukaj ich w razie napotkania nieprzewidzianych trudnoci
Jeeli poradzisz sobie z tym niezwykle trudnym zadaniem, bdziesz mg by z siebie
niewypowiedzianie dumny :D Nagrod bdzie te cenne dowiadczenie, ktrego nie
zdobdziesz inn drog!
Mamy wic zamiar pisa instrukcje stanowice blok kodu funkcji, przeto powinnimy
umieci je wewntrz moduu, a nie pliku nagwkowego. Dlatego te chwilowo
porzucamy game.h i otwieramy nieskaony jeszcze adnym znakiem plik game.cpp.
Nie znaczy to wszak, e nie bdziemy naszego nagwka w ogle potrzebowa.
Przeciwnie, jest ona nam niezbdny - zawiera przecie definicje trzech typw
wyliczeniowych, bez ktrych nie zdoamy si obej.
Powinnimy zatem doczy go do naszego moduu przy pomocy poznanej jaki czas
temu i stosowanej nieustannie dyrektywy #include:
#include "game.h"
Zwrmy uwag, i, inaczej ni to mamy w zwyczaju, ujlimy nazw pliku
nagwkowego w cudzysowy zamiast nawiasw ostrych. Jest to konieczne; w ten
sposb naley zaznacza nasze wasne nagwki, aby odrni je od fabrycznych
(iostream, cmath itp.)
Nazw doczanego pliku nagwkowego naley umieszcza w cudzysowach (""), jeli
jest on w tym samym katalogu co modu, do ktrego chcemy go doczy. Moe by on
take w jego pobliu (nad- lub podkatalogu) - wtedy uywa si wzgldnej cieki do pliku
(np. "..\plik.h").
Doczenie wasnego nagwka nie zwalnia nas jednak od wykonania tej samej czynnoci
na dwch innych tego typu plikach:
#include <iostream>
#include <ctime>
Zoone zmienne
147
Deklarujemy zmienne
Wczajc plik nagwkowy game.h mamy do dyspozycji zdefiniowane w nim typy SIGN,
FIELD i GAMESTATE. Logiczne bdzie wic zadeklarowanie nalecych do zmiennych
g_aPlansza, g_StanGry i g_AktualnyGracz:
FIELD g_aPlansza[3][3] = { { FLD_EMPTY, FLD_EMPTY, FLD_EMPTY },
{ FLD_EMPTY, FLD_EMPTY, FLD_EMPTY },
{ FLD_EMPTY, FLD_EMPTY, FLD_EMPTY } };
GAMESTATE g_StanGry = GS_NOTSTARTED;
SIGN g_AktualnyGracz;
Skorzystamy z nich niejednokrotnie w kodzie moduu game.cpp, zatem powysze linijki
naley umieci poza wszelkimi funkcjami.
Funkcja StartGry()
Nie jest to trudne, skoro nie napisalimy jeszcze absolutnie adnej funkcji :) Niezwocznie
wic zabieramy si do pracy. Rozpoczniemy od tej procedury, ktra najszybciej da o
sobie zna w gotowym programie - czyli StartGry().
Jak pamitamy, jej rol jest przede wszystkim wylosowanie gracza, ktry rozpocznie
rozgrywk. Wczeniej jednak przydaoby si, aby funkcja sprawdzia, czy jest
wywoywana w odpowiednim momencie - gdy gra faktycznie si jeszcze nie zacza:
if (g_StanGry != GS_NOTSTARTED) return false;
Jeeli warunek ten nie zostanie speniony, funkcja zwrci warto wskazujc na
niepowodzenie swych dziaa.
Jakich dziaa? Nietrudno zapisa je w postaci kodu C++:
// losujemy gracza, ktry bdzie zaczyna
srand (static_cast<unsigned>(time(NULL)));
g_AktualnyGracz = (rand() % 2 == 0 ? SGN_CIRCLE : SGN_CROSS);
// ustawiamy stan gry na ruch graczy
g_StanGry = GS_MOVE;
Losowanie liczby z przedziau <0; 2) jest nam czynnoci na wskro znajom. W
poczeniu z operatorem warunkowym ?: pozwala na realizacj pierwszego z celw
funkcji. Drugi jest tak elementarny, e w ogle nie wymaga komentarza. W kocu nie od
dzi stykamy si z przypisaniem wartoci do zmiennej :)
To ju wszystko, co byo przewidziane do zrobienia przez nasz funkcj StartGry(). W
peni usatysfakcjonowani moemy wic zakoczy j zwrceniem informacji o
pozytywnym rezultacie podjtych akcji:
return true;
Wywoujcy otrzyma wic wiadomo o tym, e czynnoci zlecone funkcji zostay
zakoczone z sukcesem.
Funkcja Ruch()
Kolejn funkcj, na ktrej spocznie nasz wzrok, jest Ruch(). Ma ona za zadanie umieci
w podanym polu znak aktualnego gracza (kko lub krzyyk) oraz sprawdzi stan planszy
pod ktem ewentualnej wygranej ktrego z graczy lub remisu. Cakiem sporo do
zrobienia, zatem do pracy, rodacy! ;D
Podstawy programowania
148
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 };
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
Podstawy programowania
150
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:
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
}
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;
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
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
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:
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
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
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.
wiczenia
1. Napisz program, ktry pozwoli uytkownikowi na wprowadzenie dowolnej iloci
liczb (ilo t bdzie podawa na pocztku) i obliczenie ich redniej arytmetycznej.
Podawane liczby przechowuj w 100-elementowej tablicy (wykorzystasz ze tylko
cz).
(Trudne) Moesz te zrobi tak, by program nie pyta o ilo liczb, lecz prosi o
kolejne a do wpisania innych znakw.
(Bardzo trudne) Czy mona jako zapobiec marnotrawstwu pamici,
zwizanemu z tak du, lecz uywan tylko czciowo tablic? Jak?
2. Stwrz aplikacj, ktra bdzie pokazywaa liczb dni do koca biecego roku.
Wykorzystaj w niej struktur tm i funkcj localtime() w taki sam sposb, jak w
przykadzie Biorhytm.
3. (Trudne) W naszej grze w kko i krzyyk jest ukryta pewna usterka. Objawia si
wtedy, gdy gracz wpisze co innego ni liczb jako numer pola. Sprbuj naprawi
ten bd; niech program reaguje tak samo, jak na warto spoza przedziau
<1; 9>.
Wskazwka: zadanie jest podobne do trudniejszego wariantu wiczenia 1.
4. (Bardzo trudne) Ulepsz napisan gr. Niech rozmiar planszy nie bdzie zawsze
wynosi 33, lecz mg by zdefiniowany jako staa w pliku game.h.
Wskazwka: poniewa plansza pozostanie kwadratem, warunkiem zwycistwa
bdzie nadal uoenie linii poziomej, pionowej lub ukonej z wasnych symboli.
Modyfikacji musi jednak ulec algorytm sprawdzania planszy (ten straszny :D) oraz
sposb numerowania i rysowania pl.
6
OBIEKTY
yka nie istnieje
Neo w filmie Matrix
Lektura kilku ostatnich rozdziaw daa ci spore pojcie o programowaniu w jzyku C++,
ze szczeglnym uwzgldnieniem sposb realizacji w nim pewnych algorytmw oraz
uycia takich konstrukcji jak ptle czy instrukcje warunkowe. Zapoznae si take z
moliwociami, jakie oferuje ten jzyk w zakresie manipulowania bardziej zoonymi
porcjami informacji.
Wreszcie, miae sposobno realizacji konkretnej aplikacji - poczynajc od jej
zaprojektowania, a na kodowaniu i ostatecznej kompilacji skoczywszy. Wierz, i samo
programowanie byo wtedy raczej zrozumiae - chocia nie pisalimy ju wwczas
trywialnego kodu.
Podejrzewam jednak, e wstpne konstruowanie programu nosio dla ciebie znamiona co
najmniej dziwnej czynnoci; wspominaem o tym zreszt w podsumowaniu caego
naszego projektu, obiecujc pokazanie w niniejszym rozdziale znacznie przyjaniejszej,
naturalniejszej i, jak sdz, przyjemniejszej techniki programowania. Przyszed czas, by
speni t obietnic.
Zatem nie tracc czasu, zajmijmy si tym wyczekiwanym tsknie zagadnieniem :)
Skrawek historii
Pomys programowania komputerw jest nawet starszy ni one same. Zanim bowiem
powstay pierwsze maszyny zdolne do wykonywania sekwencji oblicze, istniao ju wiele
teoretycznych modeli, wedle ktrych miayby funkcjonowa60.
Podstawy programowania
162
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
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
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.
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.
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.
Schemat 17. Definicja klasy oraz kilka nalecych do obiektw (jej instancji)
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);
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
170
Podstawy programowania
Kady obiekt posiada swj wasny pakiet opisujcych go pl, ktre rezyduj w pamici
operacyjnej w identyczny sposb jak pola struktur. Metody s natomiast kodem
wsplnym dla caej klasy, zatem w czasie dziaania programu istnieje w pamici tylko
jedna ich kopia, wywoywana w razie potrzeby na rzecz rnych obiektw. Jest to, jak
sdz, do oczywiste: tworzenie odrbnych kopii tych samych przecie funkcji dla
kadego nowego obiektu byoby niewtpliwie szczytem absurdu.
Definicja klasy
Jest to konieczna i czsto pierwsza czynno przy wprowadzaniu do programu nowej
klasy. Jej definicja precyzuje bowiem zawarte w niej pola oraz deklaracje metod, ktrymi
klasa bdzie dysponowaa.
Informacje te s niezbdne, aby mc utworzy obiekt danej klasy; dlatego te
umieszczamy je niemal zawsze w pliku nagwkowym - miejscu nalenym wasnym
typom danych.
Skadnia definicji klasy wyglda natomiast nastpujco:
class nazwa_klasy
Obiekty
{
171
[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 :)
A take dla unii, chocia jak wiemy, funkcjonuj one inaczej ni struktury i klasy.
Podstawy programowania
172
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
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;
}
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
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)
};
178
Podstawy programowania
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.).
// itp.
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
// 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; }
182
Podstawy programowania
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
Obiekty
183
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
Obiekty
185
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;
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
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
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
Programowanie obiektowe
193
74
A raczej do siedmiu lub omiu, gdy dla prostoty pominem tu wikszo poziomw systematyki.
Podstawy programowania
194
75
Programowanie obiektowe
195
Wykazuje poza tym pewn budow wewntrzn: niektre jej pola i metody moemy
bowiem okreli jako wasne i unikalne, za inne s odziedziczone po klasie bazowej i
mog by wsplne dla wielu klas. Nie sprawia to jednak adnej rnicy w korzystaniu z
nich: funkcjonuj one identycznie, jakby byy zawarte bezporednio wewntrz klasy.
Dziedziczenie w C++
Pozyskawszy oglne informacje o dziedziczeniu jako takim, moemy zobaczy, jak idea
ta zostaa przeoona na nasz nieoceniony jzyk C++ :) Dowiemy si wic, w jaki sposb
definiujemy nowe klasy w oparciu o ju istniejce oraz jakie dodatkowe efekty s z tym
zwizane.
Podstawy
Mechanizm dziedziczenia jest w C++ bardzo rozbudowany, o wiele bardziej ni w
wikszoci pozostalych jzykw zorientowanych obiektowo76. Udostpnia on kilka
szczeglnych moliwoci, ktre by moe nie s zawsze niezbdne, ale pozwalaj na du
swobod w definiowaniu hierarchii klas. Poznanie ich wszystkich nie jest konieczne, aby
sprawnie korzysta z dobrodziejstw programowania obiektowego, jednak wiemy
doskonale, e wiedza jeszcze nikomu nie zaszkodzia :D
Zaczniemy oczywicie od najbardziej elementarnych zasad dziedziczenia klas oraz
przyjrzymy si przykadom ilustrujcym ich wykorzystanie.
Podstawy programowania
196
[deklaracje_publiczne]
};
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
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.
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
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; }
};
// metody
float Pole() const
float Obwod() const
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
// Ssak
// Ptak
Nie zapominamy rzecz jasna, e oprcz widocznych powyej deklaracji zawieraj one
take wszystkie skadowe wzite od klasy CAnimal. Powtarzam to tak czsto, e chyba
nie masz ju co do tego adnych wtpliwoci :D
Ostatni klas z naszego drzewa gatunkowego by, jak pamitamy, Pies domowy.
Definicja jego klasy take jest dosy prosta:
class CHomeDog : public CMammal // Pies domowy
{
protected:
// nowe pola
RACE m_Rasa;
COLOR m_KolorSiersci;
public:
// metody
void Aportuj();
void Szczekaj();
};
// metody dostpowe do pl
RACE Rasa() const
COLOR KolorSiersci() const
{ return m_Rasa; }
{ return m_KolorSiersci; }
Programowanie obiektowe
201
Jak zwykle typy RACE i COLOR s mocno umowne. Ten pierwszy byby zapewne
odpowiednim enumem.
Wiemy jednake, i kryje si za ni cae bogactwo pl i metod odziedziczonych po
klasach bazowych. Dotyczy to zarwno bezporedniego przodka klasy CHomeDog, czyli
CMammal, jak i jej poredniej bazy - CAnimal. Jedyn znaczca tutaj rnic pomidzy
tymi dwoma klasami jest fakt, e pierwsza wystpuje w definicji CHomeDog, za druga nie.
Paskie hierarchie
Oprcz rozbudowanych, wielopoziomowych relacji typu baza-pochodna w powszechnym
zastosowaniu s te takie modele, w ktrych z jednej klasy bazowej dziedziczy wiele klas
pochodnych. Jest to tzw. paska hierarchia i wyglda np. w ten sposb:
// Figura szachowa
//
//
//
//
//
//
Pionek
Skoczek78
Goniec
Wiea
Hetman
Krl
78
Nazwy klas nie s tumaczeniami z jzyka polskiego, lecz po prostu angielskimi nazwami figur szachowych.
202
Podstawy programowania
Mona zauway, ze bazowa klasa CChessPiece nie bdzie tutaj suy do tworzenia
obiektw, lecz tylko do wyprowadzania z niej kolejnych klas. Sprawia to, e byaby ona
dobrym kandydatem na tzw. klas abstrakcyjn. O tym zagadnieniu bdziemy mwi
przy okazji metod wirtualnych.
Podsumowanie
Myl, e po takiej iloci przykadw oraz opisw koncepcja tworzenia klas pochodnych
poprzez dziedziczenie powinna by ci ju doskonale znana :) Nie naley ona wszake do
trudnych; wane jest jednak, by pozna zwizane z ni niuanse w jzyku C++.
O dziedziczeniu pojedynczym mona take poczyta nieco w MSDN.
Dziedziczenie wielokrotne
Skoro moliwe jest dziedziczenie z wykorzystaniem jednej klasy bazowej, to raczej
naturalne jest rozszerzenie tego zjawiska take na przypadki, w ktrej z kilku klas
bazowych tworzymy jedn klas pochodn. Mwimy wtedy o dziedziczeniu
wielokrotnym (ang. multiple inheritance).
C++ jest jednym z niewielu jzykw, ktre udostpniaj tak moliwo. Nie wiadczy to
jednak o jego niebotycznej wyszoci nad nimi. Tak naprawd technika dziedziczenia
wielokrotnego nie daje adnych nadzwyczajnych korzyci, a jej uycie jest przy tym do
skomplikowane. Decydujc si na jej wykorzystanie naley wic posiada cakiem spore
dowiadczenie w programowaniu.
Jakkolwiek zatem dziedziczenie wielokrotne bywa czasem przydatnym narzdziem,
stosowanie go (przynajmniej powszechne) w tworzonych aplikacjach nie jest zalecane.
Jeeli pojawia si taka konieczno, naley wtedy najprawdopodobniej zweryfikowa swj
projekt; w wikszoci sytuacji te same, a nawet lepsze efekty mona osign nie
korzystajc z tego wielce wtpliwego rozwizania.
Dla szczeglnie zainteresowanych i odwanych istnieje oczywicie opis w MSDN.
Puapki dziedziczenia
Chocia idea dziedziczenia jest teoretycznie cakiem prosta do zrozumienia, jej
praktyczne zastosowanie moe niekiedy nastrcza pewnych problemw. S one
zazwyczaj specyficzne dla konkretnego jzyka programowania, jako e wystpuj w tym
wzgldzie pewne rnice midzy nimi.
W tym paragrafie zajmiemy si takimi wanie drobnymi niuansami, ktre s zwizane z
dziedziczeniem klas w jzyku C++. Sekcja ta ma raczej charakter formalnego
uzupenienia, dlatego pocztkujcy programici mog j ze spokojem pomin szczeglnie podczas pierwszego kontaktu z tekstem.
Programowanie obiektowe
203
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
Podstawy programowania
204
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:
};
public:
virtual void Oddychaj()
{ std::cout << "Oddycham..." << std::endl; }
206
Podstawy programowania
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
};
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
};
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.
Podstawy programowania
210
};
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]).
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
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;
}
};
// 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; }
Podstawy programowania
214
#include <iostream>
void PokazDaneDokumentu(CDocument* pDokument)
{
// wywietlenie autora
std::cout << "AUTOR: ";
std::cout << pDokument->Autor() << 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
Programowanie obiektowe
215
};
public:
virtual void PokazDane();
Podstawy programowania
216
pDokument->PokazDane();
delete pDokument;
"http://programex.risp.pl/?"
"strona=cyfrowe_przetwarzanie_tekstu"
);
getch();
Programowanie obiektowe
217
Niezalenie od tego, jak bardzo byaby rozbudowana hierarchia naszych klas (np.
jednostek w grze strategicznej, wrogw w grze RPG, i tak dalej), zastosowanie
polimorfizmu z metodami wirtualnymi upraszcza kod wikszoci operacji do podobnie
trywialnych konstrukcji jak powysza.
Od tej pory do nas naley wic tylko zdefiniowanie odpowiedniego modelu klas i ich
metod, gdy zarzdzanie poszczeglnymi obiektami staje si, jak wida, banalne. Co
waniejsze, zastosowanie technik obiektowych nie tylko upraszcza kod, ale te pozwala
na znacznie wiksz elastyczno.
Pamitaj, e praktyka czyni mistrza! Poznanie teoretycznych aspektw programowania
obiektowego jest wprawdzie niezbdne, ale najwicej wartociowych umiejtnoci
zdobdziesz podczas samodzielnego projektowania i kodowania programw. Wtedy
szybko przekonasz si, e stosowanie technik polimorfizmu jest prawie e intuicyjne nawet jeli teraz nie jeste zbytnio tego pewien.
Operator dynamic_cast
Konwersja wskanika do klasy pochodnej na wskanik do klasy bazowej jest czynnoci
do naturaln, wic przebiega cakowicie automatycznie. Niepotrzebne jest nawet
zastosowanie jakiej formy rzutowania. Nie powinno to wcale dziwi - w kocu na tym
polega sama idea dziedziczenia, e obiekt klasy potomnej jest take obiektem
przynalenym klasie bazowej.
Inaczej jest z konwersj w odwrotn stron - ta nie zawsze musi si przecie powie.
C++ powinien wic udostpnia jaki sposb na sprawdzenie, czy taka zamiana jest
moliwa, no i na samo jej przeprowadzanie. Do tych celw suy operator rzutowania
dynamic_cast.
Jest to drugi z operatorw rzutowania, jakie mamy okazj pozna. Zosta on
wprowadzony do jzyka C++ po to, by umoliwi kompleksow obsug typw
polimorficznych w zakresie konwersji w d hierarchii klas. Jego przeznaczenie jest
zatem nastpujce:
Operator dynamic_cast suy do rzutowania wskanika do obiektu klasy bazowej na
wskanik do obiektu klasy pochodnej.
Powiedzielimy sobie rwnie, e taka konwersja niekoniecznie musi by moliwa. Rol
omawianego operatora jest wic take sprawdzanie, czy rzeczywicie mamy do czynienia
z wskanikiem do obiektu potomnego, przechowywanym przez zmienn bdc
wskanikiem do typu bazowego.
Uff, wszystko to wydaje si bardzo zakrcone, zatem najlepiej bdzie, jeeli przyjrzymy
si odpowiednim przykadom. Po raz kolejny posuymy si przy tym nasz ulubion
systematyk klas zwierzt i napiszemy tak oto funkcj:
#include <stdlib.h>
#include <ctime>
IAnimal* StworzLosoweZwierze()
{
Podstawy programowania
218
// zainicjowanie generatora liczb losowych
srand (static_cast<unsigned>(time(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
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>
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()
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 };
//
//
//
//
//
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; }
break;
break;
break;
break;
Sprawdzenie przy uyciu typeid take upowaniaoby nas do stosowania static_cast podczas rzutowania.
Programowanie obiektowe
223
Sucham? Mwisz, e to wcale nie jest takie proste? Zgadza si, na pocztku mylenie w
kategoriach obiektowych moe rzeczywicie sprawia ci trudnoci. Pomylaem wic, e
dobrze bdzie powici nieco czasu take na zagadnienia zwizane z samym
projektowaniem aplikacji z uyciem poznanych technik. Zajmiemy si tym w
nadchodzcym podrozdziale.
Rodzaje obiektw
Kady program zawiera w mniejszej lub wikszej czci nowatorskie rozwizania,
stanowice gwne wyzwanie stojce przed jego twrc. Niemniej jednak pewne cechy
224
Podstawy programowania
prawie zawsze pozostaj stae - a do nich naley take podzia obiektw skadowych
aplikacji na trzy fundamentalne grupy.
Podzia ten jest bardzo oglny i niezbyt sztywny, ale przez to stosuje si w zasadzie do
kadego projektu. Bdzie on zreszt punktem wyjcia dla nieco bardziej szczegowych
kwestii, opisanych pniej.
Pomwmy wic kolejno o kadym rodzaju z owej podstawowej trjki.
Singletony
Wikszo obiektw jest przeznaczonych do istnienia w wielu egzemplarzach, rnicych
si przechowywanymi danymi, lecz wykonujcych te same dziaania poprzez metody.
Istniej jednake wyjtki od tej reguy, a nale do nich wanie singletony.
Singleton (jedynak) to klasa, ktrej jedyna instancja (obiekt) spenia kluczow rol w
caym programie.
W danym momencie dziaania aplikacji istnieje wic co najwyej jeden egzemplarz
klasy, bdcej singletonem.
Obiekty takie s dosownie jedyne w swoim rodzaju i dlatego zwykle przechowuj one
najwaniejsze dane programu oraz wykonaj wikszo newralgicznych czynnoci.
Najczciej s te rodzicami i wacicielami pozostaych obiektw.
W jakich sytuacjach przydaj si takie twory? Ot jeeli podzielilibymy nasz projekt na
jakie skadowe (sposb podziau jest zwykle spraw mocno subiektywn), to dobrymi
kandydatami na singletony byyby przede wszystkim te skadniki, ktre obejmowayby
najszerszy zakres funkcji. Moe to by obiekt aplikacji jako takiej albo te
reprezentacje poszczeglnych podsystemw - w grach byyby to: grafika, dwik, sie,
AI, itd., w edytorach: moduy obsugi plikw, dokumentw, formatowania itp.
Niekiedy zastosowanie singletonw wymuszaj warunki zewntrzne, np. jakie
dodatkowe biblioteki, uywane przez program. Tak jest chociaby w przypadku funkcji
Windows API odpowiedzialnych za zarzdzanie oknami.
Si rzeczy singletony stanowi te punkty zaczepienia dla caego modelu klas, gdy ich
pola s w wikszoci odwoaniami do innych obiektw: niekiedy do wielu drobnych, ale
czciej do kilku kolejnych zarzdcw, czyli nastpnego, niszego poziomu hierarchii
zawierania si obiektw.
O relacji zawierania si (agregacji) bdziemy jeszcze szerzej mwi.
Przykady wykorzystania
Najbardziej oczywistym przykadem singletonu moe by caociowy obiekt programu,
a wic klasa w rodzaju CApplication czy CGame. Bdzie ona nadrzdnym obiektem
wobec wszystkich innych, a take przechowywaa bdzie globalne dane dotyczce
aplikacji jako caoci. To moe by chociaby cieka do jej katalogu, ale take kluczowe
informacje otrzymane od bibliotek Windows API, DirectX czy jakichkolwiek innych.
Jeeli chodzi o inne moliwe singletony, to z pewnoci bd to zarzdcy poszczeglnych
moduw; w grach s to obiekty klas o tak wiele mwicych nazwach jak
CGraphicsSystem, CSoundSystem, CNetworkSystem itp., podobne twory mona te
wyrni w programach uytkowych.
Wszystkie te klasy wystpuj w pojedynczych instancjach, gdy unikatowa jest ich rola.
Kwesti otwart jest natomiast ich ewentualna podlego najbardziej nadrzdnemu
obiektowi aplikacji - na przykad w ten sposb:
Programowanie obiektowe
225
class CGame
{
private:
CGraphicsSystem* m_pGFX;
CSoundSystem*
m_pSFX;
CNetworkSystem* m_pNet;
// itd.
};
//
83
83
226
Podstawy programowania
Podstawow cech skadowych statycznych jest to, e do skorzystania z nich nie jest
potrzebny aden obiekt macierzystej klasy. Odwoujemy si do nich, podajc po prostu
nazw klasy oraz oznaczenie skadowej, w ten oto sposb:
nazwa_klasy::skadowa_statyczna
Moliwe jest take tradycyjne uycie obiektu danej klasy lub wskanika na niego oraz
operatorw wyuskania . lub ->. We wszystkich przypadkach efekt bdzie ten sam.
Musimy jakkolwiek pamita, e nadal obowizuj tutaj specyfikatory praw dostpu, wic
jeli powyszy kod umiecimy poza metodami klasy, to bdzie on poprawny tylko dla
skadowych zadeklarowanych jako public.
Blisze poznanie statycznych elementw klas wymaga rozrnienia spord nich pl i
metod. Dziaanie modyfikatora static jest bowiem nieco inne dla danych oraz dla kodu.
I tak statyczne pola s czym w rodzaju zmiennych globalnych dla klasy. Mona si do
nich odwoywa z kadej metody, a take z klas pochodnych i/lub z zewntrz - zgodnie ze
specyfikatorami praw dostpu. Kade odniesienie do statycznego pola bdzie jednak
dostpem do tej samej zmiennej, rezydujcej w tym samym miejscu pamici. W
szczeglnoci poszczeglne obiekty danej klasy nie bd posiaday wasnej kopii takiego
pola, bo bdzie ono istniao tylko w jednym egzemplarzu.
Podobiestwo do zmiennych globalnych przejawia si w jeszcze jednym aspekcie:
mianowicie statyczne pola musz zosta w podobny sposb przydzielone do ktrego z
moduw kodu w programie. Ich deklaracja w klasie jest bowiem odpowiednikiem
deklaracji extern dla zwykych zmiennych. Odpowiednia definicja w module wyglda za
nastpujco:
typ nazwa_klasy::nazwa_pola [= warto_pocztkowa];
Kwalifikatora nazwa_klasy:: moemy tutaj wyjtkowo uy nawet wtedy, kiedy nasze
pole nie jest publiczne. Spostrzemy te, i nie korzystamy ju ze sowa static, jako e
poza definicj klasy ma ono odmienne znaczenie.
Statyczno metod polega natomiast na ich niezalenoci od jakiegokolwiek obiektu
danej klasy. Metody opatrzone kwalifikatorem static moemy bowiem wywoywa bez
koniecznoci posiadania instancji klasy. W zamian za to musimy jednak
zaakceptowa fakt, i nie posiadamy dostpu do wszelkich niestatycznych skadnikw
(zarwno pl, jak i metod) naszej klasy. To aczkolwiek do naturalne: jeli wywoanie
funkcji statycznej moe obej si bez obiektu, to skd moglibymy go wzi, aby
skorzysta z niestatycznej skadowej, ktra przecie takiego obiektu wymaga? Ot
wanie nie mamy skd, gdy w metodach statycznych nie jest dostpny wskanik
this, reprezentujcy aktualny obiekt klasy.
No dobrze, ale w jaki sposb statyczne skadowe klas mog nam pomc w implementacji
singletonw? C, to dosy proste. Zauwa, e takie skadowe s unikalne w skali caej
klasy - tak samo, jak unikalny jest pojedynczy obiekt singletonu. Moemy zatem uy
ich, by sprawowa kontrol nad naszym jedynym i wyjtkowym obiektem.
Najpierw zadeklarujemy wic statyczne pole, ktrego zadaniem bdzie przechowywanie
wskanika na w kluczowy obiekt:
// *** plik nagwkowy ***
// klasa singletonu
class CSingleton
{
private:
Programowanie obiektowe
227
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; }
228
Podstawy programowania
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();
// -------------------------------------------------------------------
};
//
//
//
//
wektorowy ma
vWektor1.y *
vWektor2.x *
vWektor1.x *
Programowanie obiektowe
231
return vWynik;
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
Zauwamy, e powysze streszczenie nic nie mwi choby o formie, w jakiej nasz
dokument-schemat bdzie przechowywany w pamici. Czy to bdzie bitmapa, rysunek
wektorowy, zbir innych obiektw albo moe jeszcze co innego? Wszystkie te
odpowiedzi mog by poprawne, jednak na etapie okrelania abstrakcji klasy s one poza
obszarem naszego zainteresowania.
Abstrakcja klasy jest okreleniem roli, jak ta klasa peni w programie.
Jawne formuowanie opisu podobnego do powyszego moe wydawa si niepotrzebne,
skoro i tak przecie bdzie on wymaga uszczegowienia. Posiadanie go daje jednak
moliwo prostej kontroli poprawnoci definicji klasy. Jeeli nie spenia ona zaoonych
rl, to najprawdopodobniej zawiera bdy.
Implementacja
Implementacja klasy wyznacza drog, po jakiej przebiega realizacja zada klasy,
okrelonych w abstrakcji oraz przyblionych poprzez jej interfejs. Skadaj si na ni
wszystkie wewntrzne skadniki klasy, niedostpne jej uytkownikw - a wic prywatne
pola, a take kod poszczeglnych metod.
Dogmaty cisej inynierii oprogramowania mwi, aby dokadne implementacje
poszczeglnych metod (zwane specyfikacjami algorytmw) byy dokonywane jeszcze
podczas projektowania programu. Do tego celu najczciej uywa si pseudokodu, o
ktrym ju kiedy wspominaem. W nim zwykle zapisuje si wstpne wersje algorytmw
metod.
Jednak wedug mnie ma to sens chyba tylko wtedy, kiedy nad projektem pracuje wiele
osb albo gdy nie jestemy zdecydowani, w jakim jzyku programowania bdziemy go
ostatecznie realizowa. Wydaje si, e obie sytuacje na razie nas nie dotycz :)
Programowanie obiektowe
235
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
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.
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.
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
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
// 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
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:
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.
88
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).
Rzeczywicie jest to najwaniejsza cz tej pamici (sama zwana jest czasem pamici
fizyczn), ale na pewno nie jedyna. Obecnie wiele podzespow komputerowych posiada
wasne zasoby pamici operacyjnej, przystosowane do wykonywania bardziej
specyficznych zada.
W szczeglnoci dotyczy to kart graficznych i dwikowych, zoptymalizowanych do pracy
z waciwymi im typami danych. Ilo pamici, w jak s wyposaane, systematycznie
ronie.
Wskaniki
247
Pami wirtualna
Istnieje jeszcze jedno, przebogate rdo dodatkowej pamici operacyjnej: jest nim dysk
twardy komputera, a cilej jego cz zwana plikiem wymiany (ang. swap file) lub
plikiem stronnicowania (ang. paging file).
Obszar ten suy systemowi operacyjnemu do udawania, i ma pokanie wicej pamici
ni posiada w rzeczywistoci. Wanie dlatego tak symulowan pami nazywamy
wirtualn.
Podobny zabieg jest niewtpliwie konieczny w rodowisku wielozadaniowym, gdzie naraz
moe by uruchomionych wiele programw. Chocia w danej chwili pracujemy tylko z
jednym, to pozostae mog nadal dziaa w tle - nawet wwczas, gdy czna ilo
potrzebnej im pamici znacznie przekracza fizyczne moliwoci komputera.
Cen za ponadplanowe miejsce jest naturalnie wydajno. Dysk twardy charakteryzuje
si duszym czasem dostpu ni ukady RAM, zatem wykorzystanie go jako pamici
operacyjnej musi pocign za sob spowolnienie dziaania systemu. Dzieje si jednak
tylko wtedy, gdy uruchamiamy wiele aplikacji naraz.
Mechanizm pamici wirtualnej, jako niemal niezbdny do dziaania kadego
nowoczesnego systemu operacyjnego, funkcjonuje zazwyczaj bardzo dobrze. Mona
jednak poprawi jego osigi, odpowiednio ustawiajc pewne opcje pliku wymiany. Przede
wszystkim warto umieci go na nieuywanej zwykle partycji (Linux tworzy nawet sam
odpowiedni partycj) i ustali stay rozmiar na mniej wicej dwukrotno iloci
posiadanej pamici fizycznej.
Pami trwaa
Przydatno komputerw nie wykraczaaby wiele poza zastosowania kalkulatorw, gdyby
swego czasu nie wynaleziono sposobu na trwae zachowywanie informacji midzy
kolejnymi uruchomieniami maszyny. Tak narodziy si dyskietki, dyski twarde,
zapisywalne pyty CD, przenone noniki dugopisowe i inne media, suce do
dugotrwaego magazynowania danych.
Spord nich na najwicej uwagi zasuguj dyski twarde, jako e obecnie s niezbdnym
elementem kadego komputera. Zwane s czasem pamici trwa (z wyjanionych
wyej wzgldw) albo masow (z powodu ich duej pojemnoci).
Moliwo zapisania duego zbioru informacji jest aczkolwiek okupiona lamazarnoci
dziaania. Odczytywanie i zapisywanie danych na dyskach magnetycznych trwa bowiem
zdecydowanie duej ni odwoanie do komrki pamici operacyjnej. Ich wykorzystanie
ogranicza si wic z reguy do jednorazowego wczytywania duych zestaww danych (na
przykad caych plikw) do pamici operacyjnej, poczynienia dowolnej iloci zmian oraz
powtrnego, trwaego zapisania. Wszelkie operacje np. na otwartych dokumentach s
wic w zasadzie dokonywane na ich kopiach, rezydujcych wewntrz pamici
operacyjnej.
Nie zajmowalimy si jeszcze odczytem i zapisem informacji z plikw na dysku przy
pomocy kodu C++. Nie martw si jednak, gdy ostatecznie poznamy nawet wicej ni
jeden sposb na dokonanie tego. Pierwszy zdarzy si przy okazji omawiania strumieni,
bdcych czci Biblioteki Standardowej C++.
248
Podstawy programowania
Adresowanie pamici
Wygodnie jest wyobraa sobie pami operacyjn jako co w rodzaju wielkiej tablicy
bajtw. W takiej strukturze kady element (zmiemy go komrk) powinien da si
jednoznacznie identyfikowa poprzez swj indeks. I tutaj rzeczywicie tak jest - numer
danego bajta w pamici nazywamy jego adresem.
W ten sposb dochodzimy te do pojcia wskanika:
Wskanik (ang. pointer) jest adresem pojedynczej komrki pamici operacyjnej.
Jest to wic w istocie liczba, interpretowana jako unikalny indeks danego miejsca w
pamici. Specjalne znaczenie ma tu jedynie warto zero, interpretowana jako wskanik
pusty (ang. null pointer), czyli nieodnoszcy si do adnej konkretnej komrki pamici.
Wskaniki su wic jako cz do okrelonych miejsc w pamici operacyjnej; poprzez
nie moemy odwoywa si do tyche miejsc. Bdziemy rwnie potrafili pobiera
wskaniki na zmienne oraz funkcje, zdefiniowane we wasnych aplikacjach, i wykonywa
przy ich pomocy rne wspaniae rzeczy :)
Zanim jednak zajmiemy si bliej samymi wskanikami w jzyku C++, powimy nieco
uwagi na to, w jaki sposb systemy operacyjne zajmuj si organizacj i systematyzacj
pamici operacyjnej - czyli jej adresowaniem. Pomoe nam to lepiej zrozumie dziaanie
wskanikw.
Schemat 31. Segmentowe adresowanie pamici. Adres zaznaczonej komrki zapisywano zwykle
jako 012A:0007, a wic oddzielajc dwukropkiem numer segmentu i offset (oba zapisane w
systemie szesnastkowym). Do ich przechowywania potrzebne byy dwie liczby 16-bitowe.
Moe nie wydaje si to wielk niedogodnoci, ale naprawd ni byo. Przede wszystkim
niemoliwe byo operowanie na danych o rozmiarze wikszym ni owe 64 kB (a wic
chociaby na dugich napisach). Chodzi te o fakt, i to programista musia martwi si o
rozmieszczenie kodu oraz danych pisanego programu w pamici operacyjnej. Czas
pokaza, e obowizek ten z powodzeniem mona przerzuci na kompilator - co zreszt
wkrtce stao si moliwe.
Wskaniki
249
Schemat 32. Idea paskiego modelu pamici. Adresy skadaj si tu tylko z offsetw,
przechowywanych jako liczby 32-bitowe. Mog one odnosi si do jakiegokolwiek rzeczywistego
rodzaju pamici, na przykad do takich jak na ilustracji.
Stos i sterta
Na koniec wspomnimy sobie o dwch wanych dla programistw rejonach pamici
operacyjnych, a wic wanie o stosie oraz stercie.
Podstawy programowania
250
O stercie
Reszta pamici operacyjnej nosi oryginaln nazw sterty.
Sterta (ang. heap) to caa pami dostpna dla programu i mogca by mu przydzielona
do wykorzystania.
Czytajc oba opisy (stosu i sterty) pewnie trudno jest wychwyci midzy nimi jakie
rnice, jednak w rzeczywistoci s one cakiem spore.
Przede wszystkim, rozmiar stosu jest ustalany raz na zawsze podczas kompilacji
programu i nie zmienia si w trakcie jego dziaania. Wszelkie dane, jakie s na nim
przechowywane, musz wic mie stay rozmiar - jak na przykad skalarne zmienne,
struktury czy te statyczne tablice.
Kontrol pamici sterty zajmuje si natomiast sam programista i dlatego moe przyzna
swojej aplikacji odpowiedni jej ilo w danej chwili, podczas dziaania programu. Jest
to bardzo dobre rozwizanie, kiedy konieczne jest przetwarzanie zbiorw informacji o
zmiennym rozmiarze.
Terminy stos i sterta maj w programowaniu jeszcze jedno znaczenie. Tak mianowicie
nazywaj si dwie czsto wykorzystywane struktury danych. Omwimy je przy okazji
poznawania Biblioteki Standardowej C++.
***
Na tym zakoczymy ten krtki wykad o samej pamici operacyjnej. Cz tych
wiadomoci bya niektrym pewnie doskonale znana, ale chyba kady mia okazj
dowiedzie si czego nowego :)
Wiedza ta bdzie nam teraz szczeglnie przydatna, gdy rozpoczynamy wreszcie
zasadnicz cz tego rozdziau, czyli omwienie wskanikw w jzyku C++: najpierw na
zmienne, a potem wskanikw na funkcje.
Wskaniki na zmienne
Trudno zliczy, ile razy stosowalimy zmienne w swoich programach. Takie statystyki nie
maj zreszt zbytniego sensu - programowanie bez uycia zmiennych jest przecie tym
samym, co prowadzenie samochodu bez korzystania z kierownicy ;D
Wiele razy przypominaem te, e zmienne rezyduj w pamici operacyjnej. Mechanizm
wskanikw na nie jest wic zupenie logiczn konsekwencj tego zjawiska. W tym
podrozdziale zajmiemy si wanie takimi wskanikami.
Wskaniki
251
sowy, kompilator musi zna odpowied na pytanie: Jakiego rodzaju jest zmienna, na
ktr pokazuje dany wskanik?. Dziki temu potrafi zachowywa kontrol nad typami
danych w podobny sposb, w jaki czyni to w stosunku do zwykych zmiennych.
Obejmuje to take rzutowanie midzy wskanikami, o ktrym te sobie powiemy.
Wiedzc o tym, spjrzmy teraz na ten elementarny przykad deklaracji oraz uycia
wskanika:
// deklaracja zmiennej typu int oraz wskanika na zmienne tego typu
int nZmienna = 10;
int* pnWskaznik;
// nasz wskanik na zmienne typu int
// przypisanie adresu zmiennej do naszego wskanika i uycie go do
// wywietlenia jej wartoci w konsoli
pnWskaznik = &nZmienna;
// pnWskaznik odnosi si teraz do nZmienna
std::cout << *pnWskaznik; // otrzymamy 10, czyli warto zmiennej
Dobra wiadomo jest taka, i mimo prostoty ilustruje on wikszo zagadnie
zwizanych ze wskanikami na zmiennej. Nieco gorsz jest pewnie to, e owa prostota
moe dla niektrych nie by wcale taka prosta :) Naturalnie, wyjanimy sobie po kolei, co
dzieje si w powyszym kodzie (chocia komentarze mwi ju cakiem sporo).
Oczywicie najpierw mamy deklaracj zmiennej (z inicjalizacj), lecz nas interesuje
bardziej sposb zadeklarowania wskanika, czyli:
int* pnWskaznik;
Poprzez dodanie gwiazdki (*) do nazwy typu int informujemy kompilator, e oto nie ma
ju do czynienia ze zwyk zmienn liczbow, ale ze wskanikiem przeznaczonym do
przechowywania adresu takiej zmiennej. pWskaznik jest wic wskanikiem na
zmienne typu int, lub, krcej, wskanikiem na (typ) int.
A zatem mamy ju zmienn, mamy i wskanik. Przydaoby si zmusi je teraz do
wsppracy: niech pWskaznik zacznie odnosi si do naszej zmiennej! Aby tak byo,
musimy pobra jej adres i przypisa go do wskanika - o tak:
pnWskaznik = &nZmienna;
Zastosowany tutaj operator & suy wanie w tym celu - do uzyskania adresu miejsca
w pamici, gdzie egzystuje zmienna. Potem rzecz jasna zostaje on zapisany w
pnWskaznik; odtd wskazuje on wic na zmienn nZmienna.
Na koniec widzimy jeszcze, e za porednictwem wskanika moemy dosta si do
zmiennej i uy jej w ten sam sposb, jaki znalimy dotychczas, choby do wypisania jej
wartoci w oknie konsoli:
std::cout << *pnWskaznik;
Jak z pewnoci przypuszczasz, operator * nie dokonuje tutaj mnoenia, lecz podejmuje
warto zmiennej, z ktr poczony zosta pnWskaznik; nazywamy to dereferencj
wskanika. W jej wyniku otrzymujemy na ekranie liczb, ktr oryginalnie przypisalimy
do zmiennej nZmienna. Bez zastosowania wspomnianego operatora zobaczylimy
warto wskanika (a wic adres komrki w pamici), nie za warto zmiennej, na
ktr on pokazuje. To oczywicie wielka rnica.
252
Podstawy programowania
Deklaracje wskanikw
Stwierdzilimy, e wskaniki mog z powodzeniem odnosi si do zmiennych - albo
oglnie mwic, do danych w programie. Czyni to poprzez przechowywanie numeru
odpowiedniej komrki w pamici, a zatem pewnej wartoci. Sprawia to, e wskaniki s
w rzeczy samej take zmiennymi.
Wskaniki w C++ to zmienne nalece do specjalnych typw wskanikowych.
Taki typ atwo pozna po obecnoci przynajmniej jednej gwiazdki w jego nazwie. Jest nim
wic choby int* - typ zmiennej pWskaznik z poprzedniego przykadu. Zawiera on
jednoczenie informacj, na jaki rodzaj danych bdzie nasz wskanik pokazywa - tutaj
jest to int. Typ wskanikowy jest wic typem pochodnym, zdefiniowanym na
podstawie jednego z ju wczeniej istniejcych.
To definiowanie moe si odbywa ad hoc, podczas deklarowania konkretnej zmiennej
(wskanika) - tak byo w naszym przykadzie i tak te postpuje si najczciej.
Dozwolone (i przydatne) jest aczkolwiek stworzenie aliasw na typy wskanikowe
poprzez instrukcj typedef; standardowe nagwki systemu Windows zawieraj na
przykad wiele takich nazw.
Deklarowanie wskanikw jest zatem niczym innym, jak tylko wprowadzeniem do kodu
nowych zmiennych - tyle tylko, i maj one swoiste przeznaczenie, inne ni reszta ich
licznych wspbraci. Czynno ich deklarowania, a take same typy wskanikowe
zasuguj przeto na szersze omwienie.
Wskaniki
253
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
Podstawy programowania
254
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
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
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
Niezbdne operatory
Na wszelkich zmiennych mona w C++ wykonywa jakie operacje i wskaniki nie s w
tym wzgldnie adnym wyjtkiem. Posiadaj nawet wasne instrumentarium specjalnych
operatorw, dokonujcych na nich pewnych szczeglnych dziaa. To na nich wanie
skupimy si teraz.
Wskaniki
257
Wyuskiwanie skadnikw
Trzeci operator wskanikowy jest nam ju znany od wprowadzenia OOPu. Operator
wyuskania -> (strzaka) suy do wybierania skadnikw obiektu, na ktry wskazuje
wskanik. Pod pojciem obiektu kryje si tu zarwno instancja klasy, jak i typu
strukturalnego lub unii.
Poniewa znamy ju doskonale t konstrukcj, na prostym przykadzie przeledzimy
jedynie zwizek tego operatora z omwionymi przed chwil & i *.
Zamy wic, e mamy tak oto klas:
258
Podstawy programowania
class CFoo
{
public:
int Metoda() const { return 1; }
};
Tworzc dynamicznie jej instancj przy uyciu wskanika, moemy wywoa skadowe
metody:
// stworzenie obiektu
CFoo* pFoo = new CFoo;
// wywoanie metody
std::cout << pFoo->Metoda();
pFoo jest tu wskanikiem, takim samym jak te, z ktrych korzystalimy dotd; wskazuje
na typ zoony - obiekt. Wykorzystujc operator -> potrafimy dosta si do tego obiektu i
wywoa jego metod, co te niejednokrotnie czynilimy w przeszoci.
Zwrmy jednakowo uwag, e ten sam efekt osignlibymy dokonujc dereferencji
naszego wskanika i stosujc drugi z operatorw wyuskania - kropk:
// inna metoda wywoania metody Metoda() ;D
(*pFoo).Metoda();
// zniszczenie obiektu
delete pFoo;
Nawiasy pozwalaj nie przejmowa si tym, ktry z operatorw: * czy . ma wyszy
priorytet. Ich wykorzystywanie jest wic zawsze wskazane, o czym zreszt nie raz
wspominam :)
Analogicznie, mona instancjowa obiekt poprzez zmienn obiektow i mimo to uywa
operatora -> celem dostpu do jego skadowych:
// zmienna obiektowa
CFoo Foo;
// obie ponisze linijki robi to samo
std::cout << Foo.Metoda();
std::cout << (&Foo)->Metoda();
Tym razem bowiem pobieramy adres obiektu, czyli wskanik na niego, i aplikujemy do
wskanikowy operator wyuskania ->.
Widzimy zatem wyranie, e oba operatory wyuskania maj charakter mocno umowny i
teoretycznie mog by stosowane zamiennie. W praktyce jednak korzysta si zawsze z
kropki dla zmiennych obiektowych oraz strzaki dla wskanikw, i to z bardzo prostego
powodu: wymuszenie zaakceptowania drugiego z operatorw wie si przecie z
dodatkow czynnoci pobrania adresu albo dereferencji. cznie zatem uywamy
wtedy dwch operatorw zamiast jednego, a to z pewnoci moe odbi si na
wydajnoci kodu.
Wskaniki
259
zreszt jak to czasem bywa dla zwykych zmiennych. W takich przypadkach z pomoc
przychodz nam rne metody konwersji typw wskanikowych, jakie oferuje C++.
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;
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
Wskaniki
261
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.
Podstawy programowania
262
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
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!
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.
Podstawy programowania
266
}
// wywietlenie rezultatu
std::cout << std::endl;
std::cout << nDzielna << " / " <<nDzielnik << " = "
<< nIloraz << " r " << nReszta;
getch();
W podobny sposb dziaa wiele funkcji z Windows API czy DirectX. Zalet tego
rozwizania jest take moliwo oddzielenia zasadniczego wyniku funkcji (zwracanego
przez wskanik) od ewentualnej informacji o bdzie czy te sukcesie jego uzyskania
(przekazywanego w tradycyjny sposb).
Oczywicie nic nie stoi na przeszkodzie, aby t drog zwraca wicej ni jeden
dodatkowy rezultat funkcji. Jeli jednak ich liczba jest znaczna, lepiej zczy je w
struktur ni deklarowa po kilkanacie parametrw w nagwku funkcji.
Wskaniki
267
Podstawy programowania
268
}
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.
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.
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; }
Wskaniki
273
{ if (uIndeks >= m_uRozmiar) return false;
m_pnTablica[uIndeks] = uWartosc;
return true;
};
// inne
unsigned Rozmiar() const
{ return m_uRozmiar; }
};
public:
bool ZmienRozmiar(unsigned);
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;
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
Podstawy programowania
276
Przeanalizuj go dokadnie. Zwr uwag szczeglnie na linijk:
ppnTablica[i] = new int [4];
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];
Referencje
Naocznie przekonae si, e domena zastosowa wskanikw jest niezwykle szeroka.
Jeeli nawet nie dayby w danym programie jakich niespotykanych moliwoci, to na
pewno za ich pomoc mona poczyni spore optymalizacje w kodzie i przyspieszy jego
dziaanie.
Za popraw wydajnoci trzeba jednak zapaci wygod: odwoywanie si do obiektw
poprzez wskaniki wymaga bowiem ich dereferencji. Wprowadza ona nieco zamieszania
do kodu i wymaga powicenia mu wikszej uwagi. C, zawsze co za co, prawda?
Ot nieprawda :) Twrcy C++ wyposayli bowiem swj jzyk w mechanizm referencji,
ktry czy zalety wskanikw z normaln skadni zmiennych. Zatem i wilk jest syty, i
owca caa.
Referencje (ang. references) to zmienne wskazujce na adresy miejsc w pamici, ale
pozwalajce uywa zwyczajnej skadni przy odwoywaniu si do tyche miejsc.
Mona je traktowa jako pewien szczeglny rodzaj wskanikw, ale stworzony dla czystej
wygody programisty i poprawy wygldu pisanego przeze kodu. Referencje s aczkolwiek
niezbdne przy przecianiu operatorw (o tym powiemy sobie niedugo), jednak swoje
zastosowania mog znale niemal wszdzie.
Przy takiej rekomendacji trudno nie oprze si chci ich poznania, nieprawda? ;) Tym
wanie zagadnieniem zajmiemy si wic teraz.
278
Podstawy programowania
Typy referencyjne
Podobnie jak wskaniki wprowadziy nam pojcie typw wskanikowych, tak i referencje
dodaj do naszego sownika analogiczny termin typw referencyjnych.
W przeciwiestwie jednak do wskanikw, dla kadego normalnego typu istniej jedynie
dwa odpowiadajce mu typy referencyjne. Dlaczego tak jest, dowiesz si za chwil. Na
razie przypatrzmy si deklaracjom przykadowych referencji.
Deklarowanie referencji
Referencje odnosz si do zmiennych, zatem najpierw przydaoby si jak zmienn
posiada. Niech bdzie to co w tym rodzaju:
short nZmienna;
Odpowiednia referencja, wskazujca na t zmienn, bdzia natomiast zadeklarowana w
ten oto sposb:
short& nReferencja = nZmienna;
Koczcy nazw typu znak & jest wyrnikiem, ktry mwi nam i kompilatorowi, e
mamy do czynienia wanie z referencj. Inicjalizujemy j od razu tak, aeby wskazywaa
na nasz zmienn nZmienna. Zauwamy, e nie uywamy do tego adnego
dodatkowego operatora!
Posugujc si referencj moliwe jest teraz zwyczajne odwoywanie si do zmiennej, do
ktrej si ona odnosi. Wyglda to wic bardzo zachcajco - na przykad:
nReferencja = 1;
// przypisanie wartoci zmiennej nZmienna
std::cout << nReferencja; // wywietlenie wartoci zmiennej nZmienna
Wszystkie operacje, jakie tu wykonujemy, odbywaj si na zmiennej nZmienna, chocia
wyglda, jakby to nReferencja bya jej celem. Ona jednak tylko w nich poredniczy,
tak samo jak czyni to wskaniki. Referencja nie wymaga jednak skorzystania z
operatora * (zwanego notabene operatorem dereferencji) celem dostania si do miejsca
pamici, na ktre sama wskazuje. Ten wanie fakt (midzy innymi) rni j od
wskanika.
Wskaniki
279
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.
Podstawy programowania
280
Zwracanie referencji
Na podobnej zasadzie, na jakiej funkcje mog pobiera referencje poprzez swoje
parametry, mog te je zwraca na zewntrz. Uzasadnienie dla tego zjawiska jest
rwnie takie samo, czyli zaoszczdzenie niepotrzebnego kopiowania wartoci.
Najprotszym przykadem moe by ciekawe rozwizanie problemu metod dostpowych tak jak poniej:
class CFoo
{
private:
unsigned m_uPole;
public:
unsigned& Pole() { return m_uPole; }
};
Poniewa metoda Pole() zwraca referencj, moemy uywa jej niemal tak samo, jak
zwyczajnej zmiennej:
CFoo Foo;
Foo.Pole() = 10;
std::cout << Foo.Pole();
Oczywicie kwestia, czy takie rozwizanie jest w danym przypadku podane, jest mocno
indywidualna. Zawsze naley rozway, czy nie lepiej zastosowa tradycyjnego wariantu
metod dostpowych - szczeglnie, jeeli chcemy zachowywa kontrol nad wartociami
przypisywanymi polom.
Wskaniki
281
Z praktycznego punktu widzenia zwracanie referencji nie jest wic zbytnio przydatn
moliwoci. Wspominam jednak o niej, gdy stanie si ona niezbdna przy okazji
przeadowywania operatorw - zagadnienia, ktrym zajmiemy si w jednym z przyszych
rozdziaw.
***
Tym drobnym wybiegniciem w przyszo zakoczymy nasze spotkania ze wskanikami
na zmienne. Jeeli miae jakiekolwiek wtpliwoci co do uytecznoci tego elementu
jzyka C++, to chyba do tego momentu zostay one cakiem rozwiane. Najlepiej jednak
przekonasz si o przydatnoci mechanizmw wskanikw i referencji, kiedy sam bdziesz
mia okazj korzysta z nich w swoich wasnych aplikacjach. Przypuszczam take, e owe
okazje nie bd wcale odosobnionymi przypadkami, ale sta praktyk programistyczn.
Oprcz wskanikw na zmienne jzyk C++ oferuje rwnie inn ciekaw konstrukcj,
jak s wskaniki na funkcje. Nie od rzeczy bdzie wic zapoznanie si z nimi, co te
pilnie uczynimy.
Wskaniki do funkcji
Mylc o tym, co jest przechowywane w pamici operacyjnej, zwykle wyobraamy sobie
rne dane programu: zmienne, tablice, struktury itp. One stanowi informacje
reprezentowane w komrkach pamici, na ktrych aplikacja wykonuje swoje dziaania.
Caa pami operacyjna jest wic usiana danymi kadego z aktualnie pracujcych
programw.
Hmm Czy aby na pewno o czym nie zapomnielimy? A co z samymi programami?! Kod
aplikacji jest przecie pewn porcj binarnych danych, zatem i ona musi si gdzie
podzia. Przez wikszo czasu egzystuje wprawdzie na dysku twardym w postaci pliku
(zwykle o rozszerzeniu EXE), ale dla potrzeb wykonywania kodu jest to z pewnoci zbyt
wolne medium. Gdyby system operacyjny co rusz siga do pliku w czasie dziaania
programu, wtedy na pewno wszelkie czynnoci cignyby si niczym toffi i przyprawiay
zniecierpliwionego uytkownika o bia gorczk. Co wic zrobi z tym fantem?
Rozsdnym wyjciem jest umieszczenie w pamici operacyjnej take kodu dziaajcej
aplikacji. Dostp do nich jest wwczas wystarczajco szybki, aby programy mogy dziaa
w normalnym tempie i bez przeszkd wykonywa swoje zadania. Pami RAM jest
przecie stosunkowo wydajna, wielokrotnie bardziej ni nawet najszybsze dyski twarde.
Tak wic podczas uruchamiania programu jego kod jest umieszczany wewntrz pamici
operacyjnej. Kady podprogram, kada funkcja, a nawet kada instrukcja otrzymuj
wtedy swj unikalny adres, zupenie jak zmienne. Maszynowy kod binarny jest bowiem
take swoistego rodzaju danymi. Z tych danych korzysta system operacyjny (glwnie
poprzez procesor), wykonujc kolejne instrukcje aplikacji. Wiedza o tym, jaka komenda
ma by za chwil uruchomiona, jest przechowywana wanie w postaci jej adresu - czyli
po prostu wskanika.
Nam zwykle nie jest potrzebna a tak dokadna lokalizacja jakiego wycinka kodu w
naszej aplikacji, szczeglnie jeeli programujemy w jzyku wysokiego poziomu, ktrym
jest z pewnoci C++. Trudno jednak pogardzi moliwoci uzyskania adresu funkcji w
programie, jeli przy pomocy tego adresu (oraz kilku dodatkowych informacji, o czym za
chwil) mona ow funkcj swobodnie wywoywa. C++ oferuje wic mechanizm
wskanikw do funkcji, ktry udostpnia taki wanie potencja.
Wskanik do funkcji (ang. pointer to function) to w C++ zmienna, ktra przechowuje
adres, pod jakim istnieje w pamici operacyjnej dana funkcja.
282
Podstawy programowania
Wiem, e pocztkowo moe by ci trudno uwiadomi sobie, w jaki sposb kod programu
jest reprezentowany w pamici i jak wobec tego dziaaj wskaniki na funkcje. Dokadnie
wyjanienie tego faktu wykracza daleko poza ramy tego rozdziau, kursu czy nawet
programowania w C++ jako takiego (oraz, przyznam szczerze, czciowo take mojej
wiedzy :D). Dotyka to ju bowiem niskopoziomowych aspektw dziaania aplikacji.
Niemniej postaram si przystpnie wyjani przynajmniej te zagadnienia, ktre bd
nam potrzebne do sprawnego posugiwania si wskanikami do funkcji. Zanim to si
stanie, moesz myle o nich jako o swoistych czach do funkcji, podobnych w swych
zaoeniach do skrtw, jakie w systemie Windows mona tworzy w odniesieniu do
aplikacji. Tutaj natomiast mamy do czynienia z pewnego rodzaju skrtami do
pojedynczych funkcji; przy ich pomocy moemy je bowiem wywoywa niemal w ten sam
sposb, jak to czynimy bezporednio.
Omawianie wskanikw do funkcji zaczniemy nieco od tyu, czyli od bytw na ktre one
wskazuj - a wic od funkcji wanie. Przypomnimy sobie, c takiego charakteryzuje
funkcj oraz powiemy sobie, jakie jej cechy bd szczeglne istotne w kontekcie
wskanikw.
Potem rzecz jasna zajmiemy si uywaniem wskanikw do funkcji w naszych wasnych
programach, poczynajc od deklaracji a po wywoywanie funkcji za ich porednictwem.
Na koniec uwiadomimy sobie take kilka zastosowa tej ciekawej konstrukcji
programistycznej.
Wskaniki
283
Specjaln rol peni tutaj typ void (pustka), ktry jest synonimem niczego. Nie mona
wprawdzie stworzy zmiennych nalecych do tego typu, jednak moliwe jest uczynienie
go typem zwracanym przez funkcj. Taka funkcj bdzie zatem zwraca nic, czyli po
prostu nic nie zwraca; mona j wic nazwa procedur.
Konwencja wywoania
Troch trudno w to uwierzy, ale podanie (zdawaoby si) wszystkiego, co mona
powiedzie o danej funkcji: jej parametrw, wartoci przeze zwracanej, nawet nazwy nie wystarczy kompilatorowi do jej poprawnego wywoania. Bdzie on aczkolwiek
wiedzia, co musi zrobi, ale nikt mu nie powie, jak ma to zrobi.
C to znaczy? Celem wyjanienia porwnajmy ca sytuacj do telefonowania. Gdy
mianowicie chcemy zadzwoni pod konkretny numer telefonu, mamy wiele moliwych
drg uczynienia tego. Moemy zwyczajnie pj do drugiego pokoju, podnie suchawk
stacjonarnego aparatu i wystuka odpowiedni numer. Moemy te siegnc po telefon
komrkowy i uy go, wybierajc na przykad waciw pozycj z jego ksiki adresowej.
Teoretycznie moemy te wybra si do najbliszej budki telefonicznej i skorzysta z
zainstalowanego tam aparatu. Wreszcie, moliwe jest wykorzystanie modemu
umieszczonego w komputerze i odpowiedniego oprogramowania albo te dowolnej formy
dostpu do globalnej sieci oraz protokou VoIP (Voice over Internet Protocol).
Technicznych moliwoci mamy wic mnstwo i zazwyczaj wybieramy t, ktra jest nam
w aktualnej chwili najwygodniejsza. Zwykle te osoba po drugiej stronie linii nie odczuwa
przy tym adnej rnicy.
284
Podstawy programowania
Wskaniki
285
90
286
Podstawy programowania
Nazwa funkcji
To zadziwiajce, e chyba najwaniejsza dla programisty cecha funkcji, czyli jej nazwa,
jest niemal zupenie nieistotna dla dziaajcej aplikacji! Jak ju bowiem mwiem,
widzi ona swoje funkcje wycznie poprzez ich adresy w pamici i przy pomocy tych
adresw ewentualnie wywouje owe funkcje.
Mona dywagowa, czy to dowd na cakowity brak skrzyowania midzy drogami
czowieka i maszyny, ale fakt pozostaje faktem, za jego przyczyna jest prozaicznie
pragmatyczna. Chodzi tu po prostu o wydajno: skoro funkcje programu s podczas
jego uruchamiania umieszczane w pamici operacyjnej (mona adnie powiedzie:
mapowane), to dlaczego system operacyjny nie miaby uywa wygenerowanych przy
okazji adresw, by w razie potrzeby rzeczone funkcje wywoywa? To przecie proste i
szybkie rozwizanie, naturalne dla komputera i niewymagajce adnego wysiku ze
strony programisty. A zatem jest ono po prostu dobre :)
Parametry funkcji
Ogromna wikszo funkcji nie moe oby si bez dodatkowych danych, przekazywanych
im przy wywoywaniu. Pierwsze strukturalne jzyki programowania nie oferoway
adnego wspomagania w tym zakresie i skazyway na korzystanie wycznie ze
zmiennych globalnych. Bardziej nowoczesne produkty pozwalaj jednak na deklaracj
parametrw funkcji, co te niejednokrotnie czynimy w praktyce.
Aby wywoa funkcj z parametrami, kompilator musi zna ich liczb oraz typ kadego z
nich. Informacje te podajemy w prototypie funkcji, za w jej kodzie zwykle nadajemy
take nazwy poszczeglnym parametrom, by mc z nich pniej korzysta.
Parametry peni rol zmiennych lokalnych w bloku funkcji - z t jednak rnic, e ich
pocztkowe wartoci pochodz z zewntrz, od kodu wywoujcego funkcj. Na tym
wszake kocz si wszelkie odstpstwa, poniewa parametrw moemy uywa
identycznie, jak gdyby byo one zwykymi zmiennymi odpowiednich typw. Po
zakoczeniu wykonywania funkcji s one niszczone, nie pozostawiajc adnego ladu po
ewentualnych operacjach, ktre mogy by na nich dokonywane kodzie funkcji.
Wnioskujemy std, e:
Parametry funkcji s w C++ przekazywane przez wartoci.
Regua ta dotyczy wszystkich typw parametrw, mimo e w przypadku wskanikw
oraz referencji jest ona pozornie amania. To jednak tylko zudzenie. W rzeczywistoci
take i tutaj do funkcji s przekazywane wycznie wartoci - tyle tylko, e owymi
Wskaniki
287
Podstawy programowania
288
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
// a co to jest?
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; }
Podstawy programowania
290
// 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.
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.
Podstawy programowania
292
Wskaniki
{
}
293
*pfZero = fXp;
return true;
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
Wskaniki
295
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
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.
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
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
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.
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
Dodajemy preprocesor
Ujawni si nam pierwszy cel istnienia preprocesora: w jzyku C(++) suy on do czenia
w jedn cao moduw kodu wraz z deklaracjami, ktre s niezbdne do dziaania tego
kodu. A skd brane s te deklaracje?
Oczywicie - z plikw nagwkowych. Zawieraj one przecie prototypy funkcji i definicje
klas, z jakich mona korzysta, jeeli doczy si dany nagwek do swojego moduu.
Jednak kompilator nic nie wie o plikach nagwkowych. On tylko oczekuje, e zostan
mu podane pliki z kodem rdowym, do ktrego bd si zaliczay take deklaracje
pewnych zewntrznych elementw - nieobecnych w danym module. Kompilator
potrzebuje tylko ich okrelenia z wierzchu, bez wnikania w implementacj, gdy ta
moe znajdowa si w innych moduach lub nawet innych bibliotekach i staje si wana
dopiero przy linkowaniu. Nie jest ju ona spraw kompilatora - on da tylko tych
informacji, ktre s mu potrzebne do kompilacji.
Niezbdne deklaracje powinny si znale na pocztku kadego moduu. Trudno jednak
oczekiwa, ebymy wpisywali je rcznie w kadym module, ktry ich wymaga. Byoby
to niezmiernie uciliwe, wic wymylono w tym celu pliki nagwkowe i preprocesor.
Jego zadaniem jest tutaj poczenie napisanych przez nas moduw oraz plikw
nagwkowych w pliki z kodem, ktre mog mog by bez przeszkd przetworzone przez
kompilator.
304
Zaawansowane C++
Skd preprocesor wie, jak ma to zrobi? Ot, mwimy o tym wyranie, stosujc
dyrektyw #include. W miejscu jej pojawienia si zostaje po prostu wstawiona tre
odpowiedniego pliku nagwkowego.
Wczanie nagwkw nie jest jednak jedynym dziaaniem podejmowanym przez
preprocesor. Gdyby tak byo, to przecie nie powicalibymy mu caego rozdziau :) Jest
wrcz przeciwnie: doczanie plikw to tylko jedna z czynnoci, jak moemy zleci temu
mechanizmowi - jedna z wielu czynnoci
Wszystkie zadania preprocesora s rnorodne, ale maj te kilka cech wsplnych.
Przyjrzyjmy si im w tym momencie.
Dziaanie preprocesora
Komendy, jakie wydajemy preprocesorowi, rni si od normalnych instrukcji jzyka
programowania. Take sposb, w jaki preprocesor traktuje kod rdowy, jest zupenie
inny.
Dyrektywy
Polecenie dla preprocesora nazywamy jego dyrektyw (ang. directive). Jest to specjalna
linijka kodu rdowego, rozpoczynajca si od znaku # (hash), zwanego potkiem93:
#
Na nim te moe si zakoczy - wtedy mamy do czynienia z dyrektyw pust. Jest ona
ignorowana przez preprocesor i nie wykonuje adnych czynnoci.
Bardziej praktyczne s inne dyrektywy, ktrych nazwy piszemy zaraz za znakiem #. Nie
oddzielamy ich zwykle adnymi spacjami (cho mona to robi), wic w praktyce potek
staje si czci ich nazw. Mwi si wic o instrukcjach #include, #define, #pragma i
innych, gdy w takiej formie zapisujemy je w kodzie.
Dalsza cz dyrektywy zaley ju od jej rodzaju. Rne parametry dyrektyw poznamy,
gdy zajmiemy si szczegowo kad z nich.
Bez rednika
Jest bardzo wane, aby zapamita, e:
Dyrektywy preprocesora kocz si zawsze przejciem do nastpnego wiersza.
Innymi sowy, jeeli preprocesor napotka w swojej dyrektywie na znak koca linijki (nie
wida go w kodzie, ale jest on dodawany po kadym wciniciu Enter), to uznaje go
take za koniec dyrektywy. Nie ma potrzeby wpisywania rednika na zakoczenie
instrukcji. Wicej nawet: nie powinno si go wpisywa! Zostanie on bowiem uznany za
cz dyrektywy, co w zalenoci od jej rodzaju moe powodowa rne niepodane
efekty. Kocz si one zwykle bdami kompilacji.
Zapamitaj zatem zalecenie:
Nie kocz dyrektyw preprocesora rednikiem. Nie s to przecie instrukcje jzyka
programowania, lecz polecenia dla moduu wspomagajcego kompilator.
93
Przed hashem mog znajdowa si wycznie tzw. biae znaki, czyli spacje lub tabulatory. Zwykle nie
znajduje si nic.
Preprocesor
305
symbol
#
\
~
^
|
[
]
{
}
94
Aby zapisa liczb w systemie szesnastkowym, naley j poprzedzi sekwencj 0x lub 0X. Tak wic 0xFF to
dziesitnie 255.
306
Zaawansowane C++
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.
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.
{ 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
if
//
if
//
if
//
(!DrugaFunkcja())
{ g_nZmienna = 0; return; };
...
(!TrzeciaFunkcja()) { g_nZmienna = 0; return; };
...
(CosSieStalo())
{ g_nZmienna = 0; return; };
...
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
\
\
\
\
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.
Zasig
Brak zasigu jest szczeglnie dotkliwy. Makra maj wprawdzie zakres obowizywania,
wyznaczany przez dyrektywy #define i #undef (wzgldnie koniec pliku), ale absolutnie
nie jest to tosame pojcia.
Makro zdefiniowane - jak si zdaje - wewntrz funkcji:
void Funkcja()
{
#define STALA 1500.100900
}
nie jest wcale dostpne tylko wewntrz niej. Z rwnym powodzeniem moemy z niego
korzysta take w kodzie nastpujcym dalej. Wszystko dlatego, e preprocesor nie zdaje
sobie w ogle sprawy z istnienia takiego czego jak funkcje czy bloki kodu, a ju na
pewno nie zasig zmiennych. Nie jest zatem dziwne, e jego makra nie posiadaj
zasigu.
Preprocesor
311
Typ
Makra nie maj te typw. Jak to?!, odpowiesz. A czy 67 jest napisem, albo czy
"klawiatura" jest liczb? A przecie i te, i podobne wyraenia mog by treci makr!
Faktycznie wyraenia te maj swoje typy i mog by interpretowane tylko w zgodzie z
nimi. Ale jakie s to typy? 67 moe by przecie rwnie dobrze uznana za warto int,
jak i BYTE, unsigned, nawet float. Z kolei napis jest formalnie typu const char[], ale
przecie moemy go przypisa do obiektu std::string. Poprzez wystpowanie
niejawnych konwersji (powiemy sobie o nich w nastpnym rozdziale) sytuacja z typami
nie jest wic taka prosta.
A makra dodatkowo j komplikuj, bo nie pozwalaj na ustalenie typu staej. Nasze 67
mogo by przecie docelowo typu float, ale staa zdefiniowana jako:
#define STALA 67
zostanie bez przeszkd przyjta dla kadego typu liczbowego. O to nam chyba nie
chodzio?!
Z tym problemem mona sobie aczkolwiek poradzi, nie uciekajc od #define.
Pierwszym wyjciem jest jawne rzutowanie:
#define (float) 67
Chyba nieco lepsze jest dodanie do liczby odpowiedniej kocwki, umoliwiajcej inn
interpretacj jej typu. Stosujc te kocwki moemy zmieni typ wyraenia wpisanego w
kodzie. Oto jak zmienia si typ liczby 67, gdy dodamy jej rne sufiksy (nie s to
wszystkie moliwoci):
liczba
67
67u
67.0
67.0f
typ
int
unsigned int
double
float
Przewaga staych const zwizana z typami objawia si najpeniej, gdy chodzi o tablice.
Nie ma bowiem adnych przeciwskaza, aby zadeklarowa sobie tablic wartoci staych:
const int STALE = { 1, 2, 3, 4 };
a potem odwoywa si do jej poszczeglnych elementw. Podobne dziaanie jest
cakowicie niemoliwe dla makr.
Efekty skadniowe
Z wartociami staymi definiowanymi jako makra zwizane te s pewne nieoczekiwane i
trudne do przewidzenia efekty skadniowe. Powoduje je fakt, i dziaanie preprocesora
jest operacj na zwykym tekcie, a kod przecie zwykym tekstem nie jest
rednik
Podkrelaem na pocztku, e dyrektyw preprocesora, w tym i #define, nie naley
koczy rednikiem. Ale co by si stao, gdyby nie zastosowa si do tego zalecenia?
Sprawdmy. Zdefiniujmy na przykad takie oto makro:
Zaawansowane C++
312
// 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!
SZEROKOSC 10
WYSOKOSC 20
POLE SZEROKOSC * WYSOKOSC
LUDNOSC 10000
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
int
int
int
int
SZEROKOSC = 10;
WYSOKOSC = 20;
POLE = SZEROKOSC * WYSOKOSC;
LUDNOSC = 10000;
1 + 5
8 + 1
Zaawansowane C++
314
int main()
{
std::cout << "Szesc razy dziewiec rowna sie " << SZESC * DZIEWIEC;
getch();
}
return 0;
Czyby wic bya to faktycznie tak magiczna liczba, i specjalnie dla niej naginane s
zasady matematyki? Niestety, wyjanienie jest bardziej prozaiczne. Spjrzmy tylko na
wyraenie SZESC * DZIEWIEC. Jest ono rozwijane do postaci:
1 + 5 * 8 + 1
Tutaj za, zgodnie z wanymi od pocztku do koca Wszechwiata reguami arytmetyki,
pierwszym obliczanym dziaaniem jest mnoenie. Ostatecznie wic mamy 1 + 40 + 1,
czyli istotnie 42.
Nie musimy jednak wierzy temu prostego wytumaczeniu. Czy nie lepiej sdzi, e nasz
poczciwy preprocesor ma dostp do rozwiza niewyjanionych od wiekw zagadek
Uniwersum?
Numer wiersza
Makro __LINE__ zostaje przez preprocesor zamienione na numer wiersza w aktualnie
przetwarzanym pliku rdowym. Wiersze licz si od 1 i obejmuj take dyrektywy oraz
puste linijki. Zatem w poniszym programie:
#include <iostream>
#include <conio.h>
int main()
{
std::cout << "Wypisanie tekstu w wierszu " << __LINE__ << std::endl;
return 0;
}
Preprocesor
315
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++
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
((x) * (x))
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)
Preprocesor
319
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
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"
Niebezpieczestwa makr
Niech wielu programistw do uywania makr nie jest bezpodstawna. Te konstrukcje
jzykowe kryj w sobie bowiem kilka puapek, ktrych umiejscowienie naley zna.
Dziki temu mona je omija - same te puapki, albo nawet makra w caoci.
Zobaczmy wic, na co trzeba zwrci uwag przy korzystaniu z makrodefinicji.
Preprocesor
321
Aby dociec rozwizania, rozpiszmy druga linijk tak, jak robi to preprocesor:
std::cout << ((nZmienna++) * (nZmienna++)) << std::endl;
Wida wyranie, e nZmienna jest tu inkrementowana dwukrotnie. Pierwsza
postinkrementacja zwraca wprawdzie wyniku 7, ale po niej nZmienna ma ju warto 8,
zatem druga inkrementacja zwrci w wyniku wanie 8. Obliczymy wic iloczyn 78, czyli
56.
Ale to nie wszystko. Druga inkrementacja zwikszy jeszcze warto 8 o jeden, zatem
nZmienna bdzie miaa ostatecznie warto 9. Obie te niespodziewane liczby ujrzymy na
wyjciu programu.
Jaki z tego wniosek? Ano taki, e wyraenia podane jako argumenty makr s obliczane
tyle razy, ile razy wystpuj w ich definicjach. Przyznasz, e to co najmniej
nieoczekiwane zachowanie
Priorytety operatorw
Pora na akt trzeci dramatu. Obiecaem wczeniej, e wyjani, dlaczego tak gsto
stawiam nawiasy w definicjach makr. Jeli uwanie czytae sekcj o makrach-staych, to
najprawdopodobniej ju si tego domylasz. Wytumaczmy to jednak wyranie.
Najlepiej bdzie przekona o roli nawiasw na przykadzie, w ktrym ich nie ma:
#define SUMA(a,b,c)
a + b + c
Zaawansowane C++
322
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; }
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.
//
//
//
//
int
unsigned
float
double
Zaawansowane C++
324
{ 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.
((sizeof(tab) / sizeof((tab)[0]))
Preprocesor
325
Zaawansowane C++
326
Puste makra
Wprowadzajc makra napomknem, e podawanie ich treci nie jest obowizkowe.
Mwic dosownie, preprocesor uzna za cakowicie poprawn definicj:
#define MAKRO
Jeli MAKRO wystpi dalej w pliku kompilowanym, to zostanie po prostu usunite. Nie
bdzie on zatem zbyt przydatne, jeli chodzi o operacje na tekcie programu. To jednak
nie jest teraz istotne.
Wane jest samo zdefiniowanie tego makra. Poniewa zrobilimy to, preprocesor
bdzie wiedzia, e taki symbol zosta mu podany i zapamita go. Pozwala nam to na
zastosowanie kompilacji warunkowej.
Przypomnijmy jeszcze, e moemy odwoa definicj makra dyrektyw #undef.
Dyrektywa #ifdef
Najprostsz i jedn z czciej uywanych dyrektyw kompilacji warunkowej jest #ifdef:
#ifdef makro
instrukcje
#endif
Jej nazwa to skrt od angielskiego if defined, czyli jeli zdefiniowane. Dyrektywa #ifdef
powoduje wic kompilacje kodu instrukcji, jeli zdefiniowane jest makro.
instrukcje mog by wielolinijkowe; koczy je dyrektywa #endif.
#ifdef pozwala na czasowe wyczenie lub wczenie okrelonego kodu. Typowym
zastosowaniem tej dyrektywy jest pomoc w usuwaniu bdw, czyli debuggowaniu.
Moemy obj ni na przykad kod, ktry drukuje parametry przekazane do jakiej
funkcji:
void Funkcja(int nParametr1, int nParametr2, float
{
#ifdef DEBUG
std::cout << "Parametr 1: " << nParametr1
std::cout << "Parametr 2: " << nParametr2
std::cout << "Parametr 3: " << fParametr3
#endif
}
// (kod funkcji)
fParametr3)
<< std::endl;
<< std::endl;
<< std::endl;
Preprocesor
327
Kod ten zostanie skompilowany tylko wtedy, jeli wczeniej zdefiniujemy makro DEBUG:
#define DEBUG
Tre makra nie ma znaczenia, bo liczy si sam fakt jego zdefiniowania. Moemy wic
pozostawi j pust. Po zakoczeniu testowania usuniemy lub wykomentujemy t
definicj, a linijki drukujce parametry nie zostan wczone do programu. Jeli uyjemy
#ifdef (lub innych dyrektyw warunkowych) wiksz liczb razy, to oszczdzimy
mnstwo czasu, bo nie bdziemy musieli przeszukiwa programu i oddzielnie
komentowa kadej porcji diagnostycznego kodu.
W wielu kompilatorach moemy wybra tryb kompilacji, jak np. Debug (testowa) i
Release (wydaniowa) w Visual C++. Rni sie one stopniem optymalizacji i
bezpieczestwa, a take zdefiniowanymi makrami. W trybie Debug kompilator Microsoftu
sam definiuje makro _DEBUG, ktrego obecno moemy testowa.
Dyrektywa #ifndef
Przeciwnie do #ifdef dziaa druga dyrektywa - #ifndef:
#ifndef makro
instrukcje
#endif
Ta opozycja polega na tym, e instrukcje ujte w #ifndef/#endif zostan
skompilowane tylko wtedy, gdy makro nie jest zdefiniowane. #ifndef znaczy if not
defined, czyli wanie jeeli nie zdefiniowane.
Nawizujc do kolejnego przykadu, moemy uy #ifndef w stosunku do kodu, ktry
ma si kompilowa wycznie w wersjach wydaniowych. Moe to by choby wywietlanie
ekranu powitalnego (ang. splash screen). Jego widok przy setnym, testowym
uruchamianiu programu moe by bowiem naprawd denerwujcy.
Dyrektywa #else
Do spki z obiema dyrektywami #ifdef i #ifndef (a take z #if, opisan w nastpnym
paragrafie) wchodzi polecenie #else. Jak mona si domyle, pozwala ono na wybr
dwch wariantw kodu: jednego, ktry jest kompilowany w razie zdefiniowania (#ifdef)
lub niezdefiniowania (#ifndef) makra oraz drugiego - w przeciwnych sytuacjach:
#if[n]def makro
instrukcje_1
#else
instrukcje_2
#endif
Zastosowaniem dla tej dyrektywy moe by na przykad system raportowania bdw. W
trybie testowania mona chcie zrzutu caej pamici programu, jeli wystpi w nim jaki
powany bd. W wersjach wydaniowych i tak nie monaby byo nic z krytycznym bdem
zrobi, wic nie powinno si zmusza (zdenerwowanego przecie) klienta do czekania na
tak wyczerpujc operacj. Wystarczy wtedy zapis wartoci najwaniejszych zmiennych.
Zwrmy uwag, e dyrektywa #else suy w tym przypadku wycznie naszej wygodzie.
Rwnie dobrze poradzilibycie sobie bez niej, piszc najpierw warunek z #ifdef
(#ifndef), a potem z #ifndef (#ifdef).
328
Zaawansowane C++
Konstruowanie warunkw
Co moe by warunkiem? W oglnoci wszystko, co znane jest preprocesorowi w
momencie napotkania dyrektywy #if. S to wic:
wartoci dosownych staych liczbowych, podane bezporednio w kodzie jako
liczby, np. -8, 42 czy 0xFF
wartoci makr-staych, zdefiniowane wczeniej dyrektyw #define
wyraenia z operatorem defined
A co z reszt staych wartoci, np. obiektami const? Ot one nie mog (albo raczej nie
powinny) by skadnikami warunkw #if. Jest tak, poniewa obiekty te nale do
kompilatora, a nie do preprocesora. Ten nie ma o nich pojcia, gdy zna tylko swoje
makra #define. To jedyny przypadek, gdy maj one przewag na staymi const.
Podobnie rzecz ma si z operatorem sizeof, ktry jest wprawdzie operatorem czasu
kompilacji, ale nie jest operatorem preprocesora.
Gdyby #if rozpoznawao warunki z uyciem staych const i operatora sizeof, nie
mogoby ju by obsugiwane przez preprocesor. Musisz bowiem pamita, e dla
preprocesora istniej tylko jego dyrektywy, za cay tekst midzy nimi moe by
czymkolwiek (cho dla nas jest akurat kodem). Chcc zmusi preprocesor do obsugi
obiektw const i operatora sizeof naleaoby w istocie obarczy go zadaniami
kompilatora.
Operator defined
Operator defined suy do sprawdzenia, czy dane makro zostao zdefiniowane. Warunek:
#if defined(makro)
jest wic rwnowany z:
#ifdef makro
Natomiast dla #ifndef alternatyw jest:
Preprocesor
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
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
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
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
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.
Z nawiasami ostrymi
Model z nawiasami ostrymi (tworzonymi poprzez znak mniejszoci i wikszoci):
#include <nazwa_pliku>
332
Zaawansowane C++
Z cudzysowami
Drugi typ instrukcji #include wyglda nastpujco:
#include "nazwa_pliku"
Z nimtake zdylimy si ju spotka - stosowalimy go do wczania wasnych plikw
nagwkowych do swoich moduw.
Ten wariant #include dziaa w sposb nieco bardziej kompleksowy ni poprzedni.
Wpierw bowiem przeszukuje on biecy katalog - tzn. ten katalog, w ktrym
umieszczono plik zawierajcy dyrektyw #include. Jeli tam nie znajdzie podanego
pliku, wwczas zaczyna zachowywa si tak, jak #include z nawiasami ostrymi.
Przeglda wic zawarto katalogw z listy folderw plikw doczanych.
Ktry wybra?
Dwa rodzaje jednej dyrektywy to cakiem sporo. Ktr wybra w konkretnej sytuacji?
99
Te nagwki sa niezalecane, naley stosowa ich odpowiedniki bez rozszerzenia .h i literk c na pocztku.
Zamiast np. math.h uywamy wic cmath.
Preprocesor
333
zapowiadajce zmiennych oraz definicje klas (a czsto take definicje szablonw, ale o
tym pniej). Pliki te maj zwykle rozszerzenie .h, .hh, .hxx lub .hpp.
cieki wzgldne
W obu wersjach #include moemy wykorzystywa tzw. cieki wzgldne (ang. relative
paths), cho prawdziwie przydatne s one tylko w dyrektywie z cudzysowami.
cieki wzgldne pozwalaj docza pliki znajdujce si w innym katalogu ni biecy100:
w podkatalogach lub w nadkatalogu czy te w innych katalogach tego samego poziomu.
Oto kilka przykadw:
#include "gui\buttons.h"
#include "..\base.h"
#include "..\common\pointers.hpp"
// 1
// 2
// 3
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++.
Dyrektywa #pragma
Do wydawania tego typu polece suy dyrektywa #pragma:
#pragma polecenie
To, czy dane polecenie zostanie faktycznie wzite pod uwage podczas kompilacji, zaley
od posiadanego przez nas kompilatora. Preprocesor zachowuje si jednak bardzo
porzdnie: jeli stwierdzi, e dana komenda jest nieznana kompilatorowi, wwczas caa
dyrektywa zostanie po prostu zignorowana. Niektre troskliwe kompilatory wywietlaj
ostrzeenie o tym fakcie.
Po opis polece, jakie s dostpne w dyrektywie #pragma, musisz uda si do
dokumentacji swojego kompilatora.
Preprocesor
335
Nie omwimy ich wszystkich, gdy nie jest to podrcznik VC++, a poza tym wiele z nich
dotyczy sprawa bardzo niskopoziomowych. Przypatrzymy si aczkolwiek tym, ktre mog
by przydatne przecitnemu programicie.
Opisy wszystkich parametrw dyrektywy #pragma w Visual C++ .NET moesz rzecz jasna
znale w dokumentacji MSDN.
Wybrane parametry podzieliem na kilka grup.
Komunikaty kompilacji
Pierwsza trjka parametrw #pragma pozwala na wywietlanie pewnych informacji
podczas procesu kompilacji programu. W przeciwiestwie do #error, polcenia nie
powoduje jednak przerwania tego procesu, lecz tylko peni funkcj powiadamiajc np.
o pewnych decyzjach podjtych w czasie kompilacji warunkowej.
Przyjrzyjmy si tym komendom.
message
Skadnia polecenia message jest nastpujca:
#pragma message("komunikat")
Gdy preprocesor napotka powysz linijk kodu, to wywietli w oknie komunikatw
kompilatora (tam, gdzie zwykle podawane s bdy) wpisany tutaj komunikat. Jego
wypisanie nie spowoduje jednak przerwania procesu kompilacji, co rni #pragma
message od dyrektywy #error.
Przykadowym uyciem tego polecenie moe by pitrowy #if podobny do tego z jakim
mielimy do czynienia w poprzednim podrozdziale:
#define
#define
#define
#define
KEYBOARD
MOUSE
TRACKBALL
JOYSTICK
1
2
3
4
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
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
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
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.
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
};
// 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
Funkcje zaprzyjanione
Najpierw zobaczymy, jak zaprzyjani klas z funkcj - tak, aby funkcja miaa dostp do
niepublicznych skadnikw z danej klasy.
};
// zaprzyjaniona funkcja
bool PrzecinajaSie(CCircle& Okrag1, CCircle& Okrag2)
{
// obliczamy odlego midzy rodkami
float fRoznicaX = Okrag2.m_ptSrodek.x - Okrag1.m_ptSrodek.x;
float fRoznicaY = Okrag2.m_ptSrodek.y - Okrag1.m_ptSrodek.y;
float fOdleglosc = sqrt(fRoznicaX*fRoznicaX + fRoznicaY*fRoznicaY);
Zaawansowane C++
346
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.
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; }
};
Nie zapominajmy, e nawet wwczas funkcja zaprzyjaniona nie jest metod klasy pomimo tego, e jej umieszczenie wewntrz definicji klasy sprawia takie wraenie. W tym
przypadku funkcja Dodaj() jest nadal funkcj globaln - wywoujemy j bez pomocy
adnego obiektu, cho oczywicie przekazujemy jej obiekty CVector2D w parametrach i
taki te obiekt otrzymujemy z powrotem:
CVector2D vSuma = Dodaj(CVector2D(1.0f, 2.0f), CVector2D(0.0f, -1.0f));
Umieszczenie definicji funkcji zaprzyjanionej w bloku definicji klasy ma jednak pewien
skutek. Ot funkcja staje si wtedy funkcj inline, czyli jest rozwijana w miejscu swego
wywoania. Przypomina pod tym wzgldem metody klasy, ale jeszcze raz powtarzam, e
metod nie jest.
Moe najlepiej bdzie, jeli zapamitasz, e:
Wszystkie funkcje zdefiniowane wewntrz definicji klasy s automatycznie inline,
jednak tylko te bez swka friend s jej metodami. Pozostae s funkcjami
globalnymi, lecz zaprzyjanionymi z klas.
Klasy zaprzyjanione
Zaprzyjanianie klas z funkcjami globalnymi wydaje si moe nieco dziwnym
rozwizaniem (gdy czciowo amie zalet OOPu - hermetyzacj), ale niejednokrotnie
bywa przydatnym mechanizmem. Bardziej obiektowym podejciem jest przyja klas z
innymi klasami - jako caociami lub tylko z ich pojedynczymi metodami.
Zaawansowana obiektowo
349
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; }
};
Wyznanie przyjani, ktry czyni klasa CPoint, sprawia, e zaprzyjaniona klasa CRect ma
peen dostp do jej skadnikw niepublicznych. Metoda CRect::PunktWewnatrz() moe
wic odczyta wsprzdne podanego punktu i sprawdzi, czy ley on wewntrz
prostokta opisanego przez obiektt klasy CRect.
Zauwamy jednoczenie, e klasa CPoint nie ma tutaj podobnego dostpu do
prywatnych skadowych CRect. Klasa CRect nie zadeklarowaa bowiem przyjani z klas
CPoint. Wynika std bardzo wana zasada:
Przyja klas w C++ nie jest automatycznie wzajemna. Jeeli klasa A deklaruje
przyja z klas B, to klasa B nie jest od razu take przyjacielem klasy A. Obiekty klasy B
maj wic dostp do niepublicznych danych klasy A, lecz nie odwrotnie.
Do czsto aczkolwiek yczymy sobie, aby klasy wzajemnie deklaroway sobie przyja.
Jest to jak najbardziej moliwe: po prostu w obu klasach musz by deklaracje przyjani:
class CBar;
class CFoo
{
friend class CBar;
};
class CBar
{
friend class CFoo;
};
Wymaga to zawsze zastosowania deklaracji zapowiadajcej, gdy kompilator musi
wiedzie, e dana nazwa jest klas, zanim pozwoli na jej zastosowanie w konstrukcji
friend class. Nie musi natomiast zna caej definicji klasy, co byo wymagane dla
przyjani z pojedynczymi metodami. Gdyby tak byo, to wzajemna przyja klas nie
byaby moliwa. Kompilator zadowala si na szczcie sam informacj CBar jest klas,
bez wnikania w szczegy, i przyjmuje deklaracj przyjani z klas, o ktrej w zasadzie
nic nie wie.
Kompilator nie przyjmie natomiast deklaracji przyjani z pojedyncz metod nieznanej
bliej klasy. Sprawia to, e wybircza przyja dwch klas nie jest moliwa, bo
wymagaaby niemoliwego: zdefiniowania pierwszej klasy przed definicj drugiej oraz
zdefiniowania drugiej przed definicj pierwszej. To oczywicie niemoliwe, a kompilator
nie zadowoli si niestety sam deklaracj zapowiadajc - jak to czyni przy deklarowaniu
cakowitej przejani (friend class klasa;).
Zaawansowana obiektowo
351
102
Jest tak, gdy stosujemy dziedziczenie publiczne (class pochodna : public bazowa), ale tak robimy niemal
zawsze.
Zaawansowane C++
352
Zastosowania
Mwic o zastosowaniach przyjani, musimy rozgraniczy zaprzyjanione klasy i funkcje
globalne.
Konstruktory w szczegach
Konstruktory peni w C++ wyjtkowo duo rl. Cho oczywicie najwaniejsza (i w
zasadzie jedyn powan) jest inicjalizacja obiektw - instancji klas, to niejako przy
okazji mog one dokonywa kilku innych, przydatnych operacji. Wszystkie one wi si
z tym gwnym zadaniem.
W tym podrozdziale nie bdziemy wic mwi o tym, co robi konstruktor (bo to wiemy),
ale jak moe to robi. Innymi sowy, dowiesz si, jak wykorzysta rne rodzaje
konstruktorw do wasnych szczytnych celw programistycznych.
Maa powtrka
Najpierw jednak przyda si mae powtrzenie wiedzy, ktra bdzie nam teraz przydatna.
Przy okazji moe j troch usystematyzujemy; powinno si te wyjani to, co do tej
pory mogo by dla ciebie ewentualnie niejasne.
Zaczniemy od przypomnienia konstruktorw, a pniej procesu inicjalizacji.
Zaawansowana obiektowo
353
Konstruktory
Konstruktor jest specjaln metod klasy, wywoywan podczas tworzenia obiektu. Nie
jest on, jak si czasem bdnie sdzi, odpowiedzialny za alokacj pamici dla obiektu,
lecz tylko za wstpne ustawienie jego pl. Niejako przy okazji moe on aczkolwiek
podejmowa te inne czynnoci, jak zwyka metoda klasy.
Cechy konstruktorw
Konstruktory tym jednak rni si od zwykych metod, i:
nie posiadaj wartoci zwracanej. Konstruktor nic nie zwraca (bo i komu?),
nawet typu pustego, czyli void. Zgoda, mona si spiera, e wynikiem jego
dziaania jest obiekt, lecz konstruktor nie jest jedynym mechanizmem, ktry
bierze udzia w jego tworzeniu: liczy si jeszcze alokacja pamici. Dlatego te
przyjmujemy, e konstruktor nie zwraca wartoci. Wida to zreszt w jego
deklaracji
nie mog by wywoywane za porednictwem wskanika na funkcje. Przyczyna
jest prosta: nie mona pobra adresu konstruktora
maj mnstwo ogranicze co do przydomkw w deklaracjach:
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; }
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; }
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
T drug zasad wprowadziem przy okazji dziedziczenia, cho nie wspominaem o owych
dodatkowych zabiegach. Bd one treci tego podrozdziau.
Niejawne wywoanie
Niejawne wywoanie (ang. implicit call) wystpuje wtedy, gdy to kompilator wywouje
nasz konstruktor. Jest par takich sytuacji:
najprostsza: gdy deklarujemy zmienn obiektow, np.:
CFoo Foo;
Jawne wywoanie
Konstruktor moemy te wywoa jawnie. Mamy wtedy wywoanie niejawne (ang. explicit
call), ktre wystpuje np. w takich sytuacjach:
przy konstruowaniu obiektu operatorem new
przy jawnym wywoaniu konstruktora: nazwa_klasy([parametry])
W tym drugim przypadku mamy tzw. obiekt chwilowy. Zwracalimy taki obiekt, kopiujc
go do rezultatu funkcji Dodaj(), prezentujc funkcje zaprzyjanione.
Inicjalizacja
Teraz powiemy sobie wicej o inicjalizacji. Jest to bowiem proces cile zwizany z
aspektami konstruktorw, ktre omwimy w tym podrozdziale.
Inicjalizacja (ang. initialization) jest to nadanie obiektowi wartoci pocztkowej w
chwili jego tworzenia.
Kiedy si odbywa
W naturalny sposb inicjalizacj wiemy z deklaracj zmiennych. Odbywa si ona
jednak take w innych sytuacjach.
Dwie kolejne zwizane z funkcjami. Ot jest to:
przekazanie wartoci poprzez parametr
zwrcenie wartoci jako rezultatu funkcji
Wreszcie, ostatnia sytuacja zwizana jest inicjalizacj obiektw klas - poznamy j za
chwil.
Jak wyglda
Inicjalizacja w oglnoci wyglda mniej wicej tak:
typ zmienna = inicjalizator;
356
Zaawansowane C++
Agregaty
Bardziej zozone typy danych moemy inicjalizowa w specjalny sposb, jako tzw.
agregaty. Agregatem jest tablica innych agregatw (wzgldnie elementw typw
podstawowych) lub obiekt klasy, ktra:
nie dziedziczy z adnej klasy bazowej
posiada tylko skadniki publiczne (public, ewentualnie bez specyfikatora w
przypadku typw struct)
nie posiada funkcji wirtualnych
nie posiada zadeklarowanego konstruktora
Agregaty moemy inicjalizowa w specjalny sposb, podajc wartoci wszystkich ich
elementw (pl). Znamy to ju tablic, np.:
int aTablica[13] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41 };
Podobnie moe to si odbywa take dla struktur (tudzie klas), speniajcych cztery
podane warunki:
struct VECTOR3 { float x, y, z; };
VECTOR3 vWektor = { 6.0f, 12.5f, 0.0f };
W przypadku bardziej skomplikowanych, zagniedonych agregatw, bdziemy mieli
wicej odpowiednich par nawiasw klamrowych:
VECTOR3 aWektory[3] = { { 0.0f, 2.0f, -3.0f },
{ -1.0f, 0.0f, 0.0f },
{ 8.0f, 6.0f, 4.0f } };
Mona je aczkolwiek opuci i napisa te 9 wartoci jednym cigiem, ale przyznasz
chyba, e w tej postaci inicjalizacja wyglda bardziej przejrzycie. Po inicjalizatorze wida
przynajmniej, e inicjujemy tablic trj-, a nie dziewicioelementow.
Inicjalizacja konstruktorem
Ostatni sposb to inicjalizacja obiektu jego wasnym konstruktorem - na przykad:
std::string strZmienna = "Hmm...";
Tak, to jest jak najbardziej taki wanie przykad. W rzeczywistoci kompilator rozwinie
go bowiem do:
std::string strZmienna("Hmm...");
Zaawansowana obiektowo
357
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; }
Zaawansowane C++
358
Lista inicjalizacyjna gwarantuje, e zostan one zainicjalizowane we waciwym czasie podczas tworzenia obiektu:
class CFoo
{
private:
const float m_fPole;
// nie moe by: const float m_fPole = 42; !!
};
public:
// konstruktor - inicjalizacja pola
CFoo() : m_fPole(42)
{
/* m_fPole = 42; // te nie moe by - za pno!
// m_fPole musi mie warto ju
// na samym pocztku wykonywania
// konstruktora */
}
Mwiem te, e inicjalizacja przy pomocy listy inicjalizacyjnej jest szybsza od przypisa
w ciele konstruktora. Powinnimy wic stosowa j, jeeli mamy tak moliwo, a
decyzja na ktrej z dwch rozwiza nie robi nam rnicy. Zauwamy te, e zapis na
licie inicjalizacyjnej jest po prostu krtszy.
W licie inicjalizacyjnej moemy umieszcza nie tylko czyste stae i argumenty
konstruktora, lecz take zloone wyraenia - nawet z wywoaniami metod czy funkcji
globalnych. Nie ma wic adnych ogranicze w stosunku do przypisania.
Zaawansowana obiektowo
};
359
public:
CIndirectBase(int nPole1) : m_nPole1(nPole) { }
};
public:
// wywoanie konstruktora klasy bezporednio bazowej
CDerived(int nPole1, float fPole2)
: CDirectBase(nPole1), m_fPole2(fPole2) { }
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.
{ delete[] m_pnTablica; }
Zaawansowana obiektowo
};
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++.
Zaawansowane C++
362
// a co to jest?...
Zaawansowana obiektowo
{
}
363
ciao_konstruktora
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.
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.
{ }
{ }
{ }
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
};
// 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; }
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;
{ }
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++
Zaawansowana obiektowo
369
Wszystko zaley wic od tego, ktry z typw - rdowy, docelowy - jest klas, do ktrej
definicji mamy dostp:
jeeli jestemy w posiadaniu definicji klasy docelowej, to moemy zastosowa
konstruktor konwertujcy
jeli mamy dostp do klasy rdowej, moliwe jest zastosowanie operatora
konwersji
W przypadku gdy oba warunki s spenione (tzn. chcemy wykona konwersj z
wasnorcznie napisanej klasy do innej wasnej klasy), wybr sposobu jest w duej
mierze dowolny. Trzeba jednak pamita, e:
konstruktory nie s dziedziczone, wic w jeli chcemy napisac konwersj typu do
klasy pochodnej, potrzebujemy osobnego konstruktora w tej klasie
konstruktory nie s metodami wirtualnymi, w przeciwiestwie do operatorw
konwersji
argument konstruktora konwertujcego musi mie typ cile dopasowany do
zadeklarowanego
W sumie wic wnioski z tego s takie (czytaj: przechodzimy do sedna :D):
chcc wykona konwersj typu podstawowego (lub klasy bibliotecznej) do typu
wasnej klasy, stosujemy konstruktor konwertujcy
chcc dokona konwersji typu wasnej klasy do typu podstawowego (lub klasy
bibliotecznej), wykorzystujemy operator konwersji
definiujc konwersj midzy dwoma wasnymi klasami moemy wybra, kierujc
si innymi przesankami, jak np. wpywem dziedziczenia na konwersje czy nawet
kolejnoci definicji obu klas w pliku nagwkowym
***
Zbiorem dobrych rad odnonie stosowania rnych typw konwersji zakoczylimy
omawianie zaawansowanych aspektw konstruktorw w C++.
370
Zaawansowane C++
Przecianie operatorw
W tym podrozdziale przyjrzymy si unikalnej dla C++, a jednoczenie wspaniaej
technice przeciania operatorw. To jedno z najwikszych osigni tego jzyka w
zakresie uatwiania programowania i uczynienia go przyjemniejszym.
Zanim jednak poznamy t cudowno, czas na krtk dygresj :) Jak ju wielokrotnie
wspomniaem, C++ jest czonkiem bardzo licznej dzisiaj rodziny jzykw obiektowych.
Takie jzyki charakteryzuje moliwo tworzenia wasnych typw danych - klas zawierajcych w sobie (kapsukujcych) pewne dane (pola) oraz pewne dziaania
(metody). Na tym polega OOP.
aden jzyk programowania nie moe si jednak oby bez mniej lub bardziej
rozbudowanego wachlarza typw podstawowych. W C++ mamy ich mnstwo, z czego
wikszo jest spadkiem po jego poprzedniku, jzyku C.
Z jednej strony mamy wic typy wbudowane (w C++: int, float, unsigned, itd.), a
drugiej typy definiowane przez uytkownika (struktury, klasy, unie). W jakim stopniu s
one do siebie podobne?
Pomylisz: Gupie pytanie! One przecie wcale nie s do siebie podobne. Typw
podstawowych uywamy przeciez inaczej ni klas, i na odwrt. Nie ma mowy o jakim
wikszym podobiestwie - moe poza tym, e dla wszystkich typw moemy deklarowa
zmienne i parametry funkcji No i moe jeszcze wystpuj podobne konwersje Jeeli
faktycznie tak pomylae, to nie bdziesz zdziwiony, e twrcy wielu jzykw
obiektowych take przyjli tak strategi. W jzykach Java, Object Pascal (Delphi), Visual
Basic, PHP i jeszcze wielu innych, typy definiowane przez uytkownika (klasy) s jakby
wydzielon czci jzyka. Maj niewiele punktw wsplnych z typami wbudowanymi,
poza tymi naprawd niezbdnymi, ktre sam wyliczye.
Jednak wcale nie musi tak by i C++ jest tego najlepszym przykadem. Autorzy tego
jzyka (z Bjarne Stroustrupem na czele) dyli bowiem do tego, aby definiowane przez
programist typy byy funkcjonalnie jak najbardziej zblione do typw wbudowanych. Ju
sam fakt, e moemy tworzy obiekty na dwa sposoby - jak normalne zmienne oraz
poprzez new - dobrze o tym wiadczy. Moliwo zdefiniowania konstruktorw
kopiujcych i konwersji wiadczy o tym jeszcze bardziej.
Ale ukoronowaniem tych wysikw jest obecno w C++ mechanizmu przeciania
operatorw.
Czy wic jest ten wspaniay mechanizm?
Przecianie operatorw (ang. operator overloading), zwane te ich
przeadowaniem, polega na nadawaniu operatorom nowych znacze - tak, aby mogy
by one wykorzystane w stosunku do obiektw zdefiniowanych klas.
Polega to wic na napisaniu takiego kodu, ktry sprawi, e wyraenia w rodzaju:
a = b + c
a /= d
if (b == c) { /* ... */ }
bd poprawne nie tylko wtedy, gdy a, b, c i d bd zmiennymi, nalecymi do typw
wbudowanych. Po przecieniu operatorw (tutaj: +, =, /= i ==) dla okrelonych klas
bdzie mona pisa takie wyraenia: zawierajce operatory i obiekty naszych klas. W ten
sposb zdefiniowane przez nas klasy nie bd si rniy praktycznie niczym od typw
wbudowanych.
Zaawansowana obiektowo
371
// 1/2, czyli p :)
// 5
// zmienna na wynik
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)
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
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.
// 7
// -7
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
Zaawansowana obiektowo
375
=
=
=
=
=
=
nA
nA
nA
nA
nA
nA
+
*
/
/
%
nB;
nB;
nB;
nB;
static_cast<float>(nB);
nB;
//
//
//
//
//
//
13
5
36
2
2.25f
1
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
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
L-warto i r-warto
Zauwamy, e odwrotne przypisanie:
7 = nX;
// le!
jest niepoprawne. Nie moemy nic przypisa do sidemki, bo ona nie zajmuje adnej
komrki w pamici - w przeciwiestwie do zmiennej, jak np. nX.
Zarwno 7, jak i nX, s jednak poprawnymi wyraeniami jzyka C++. Widzimy
aczkolwiek, e rni si pod wzgldem wsppracy z przypisaniem. nX moe by celem
przypisania, za 7 - nie.
Mwimy, e nX jest l-wartoci, za 7 - r-wartoci lub p-wartoci.
L-warto (ang. l-value) jest wyraeniem mogcym wystpi po lewej stronie
operatora przypisania - std ich nazwa.
R-warto (ang. r-value), po polsku zwana p-wartoci, moe wystpi tylko po
prawej stronie operatora przypisania.
Zauwamy, e nic nie stoi na przeszkodzie, aby nX pojawio si po prawej stronie
operatora przypisania:
int nY;
nY = nX;
Jest tak, poniewa:
Kada l-warto jest jednoczenie r-wartoci (p-wartoci) - lecz nie odwrotnie!
Domylasz si pewnie, e w C++ kade wyraenie jest r-wartoci, poniewa
reprezentuje jakie dane. L-wartociami s natomiast te wyraenia, ktre:
odpowiadaj komrkom pamici operacyjnej
nie s oznaczone jako stae (const)
Najbardziej typowymi rodzajami l-wartoci s wic:
zmienne wszystkich typw niezadeklarowane jako const
wskaniki do powyszych zmiennych, wobec ktrych stosujemy operator
dereferencji, czyli gwiazdk (*)
niestae referencje do tyche zmiennych
elementy niestaych tablic
niestae pola klas, struktur i unii, ktre podpadaj pod jeden z powyszych
punktw i nie wystpuj w ciele staych metod106
106
Zaawansowana obiektowo
379
Rezultat przypisania
Wyraeniem jest take samo przypisanie, gdy samo w sobie reprezentuje pewn
warto:
std::cout << (nX = 5);
Ta linijka kodu wyprodukuje rezultat:
5
Zaawansowane C++
380
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
Rozwinicie wziem w cudzysw, poniewa nie jest tak, e jaki mechanizm w rodzaju
makrodefinicji zamienia te skrcone wyraenia do ich penych form. O nie, one s
kompilowane w tej postaci. Ma to taki skutek, e wyraenie po lewej stronie operatora
jest obliczane jeden raz. W wersji rozwinitej byoby natomiast obliczane dwa razy.
Podobna zasada obowizuje te w operatorach pre/postin/dekrementacji.
Jest to te realizacja bardziej fundamentalnej reguy, ktra mwi, e skadniki kadego
wyraenia s obliczane tylko raz.
Operatory wskanikowe
Wskaniki byy ongi kluczow cech jzyka C, a i w C++ nie straciy wiele ze swojego
znaczenia. Do ich obsugi mamy w naszym ulubionym jzyku trzy operatory.
Pobranie adresu
Jednoargumentowy operator & suy do pobrania adresu obiektu, przy ktrym stoi. Oto
przykad:
int nZmienna;
int* pnWskaznik = &nZmienna;
Argument tego operatora musi by l-wartoci. To raczej oczywiste, bo przecie musi
ona rezydowa w jakim miejscu pamici. Inaczej niemoliwe byoby pobranie adresu
tego miejsca. Typowo operandem dla & jest zmienna lub funkcja.
Zaawansowana obiektowo
381
Dereferencja
Najprostszym i najczciej stosowanym sposobem jest dereferencja:
int nZmienna;
int* pnWskaznik = &nZmienna;
*pnWskaznik = 42;
Odpowiada za ni jednoargumentowy operator *, zwany operatorem dereferencji lub
adresowania poredniego. Pozwala on na dostp do miejsca w pamici, ktremu
odpowiada wskanik. Operator ten wykorzystuje ponadto typ wskanika, co gwarantuje,
e odczytana zostanie waciwa ilo bajtw. Dla int* bdzie to sizeof(int), zatem
*pnWskaznik reprezetuje u nas liczb cakowit.
To, czy *wskanik jest l-wartoci, czy nie, zaley od staoci wskanika. Jeeli jest to
stay wskanik (const typ*), wwczas nie moemy modyfikowa pokazywanej przeze
pamici. Mamy wic do czynienia z r-wartoci. W pozostaych przypadkach mamy lwarto.
Indeksowanie
Jeeli wskanik pokazuje na tablic, to moemy dosta si do jej kolejnych elementw za
pomoc operatora indeksowania (ang. subscript operator) - nawiasw kwadratowych
[].
Oto zupenie banalny przykad:
std::string aBajka[3];
aBajka[0] = "Dawno, dawno temu, ...";
aBajka[1] = "w odleglej galaktyce...";
aBajka[2] = "zylo sobie siedmiu kransoludkow...";
Jeeli zapytasz A gdzie tu wskanik?, to najpierw udam, e tego nie syszaem i pozwol
ci na chwil zastanowienia. A jeli nadal bdziesz si upiera, e adnego wskanika tu
nie ma, to bd zmuszony naoy na ciebie wyrok powtrnego przeczytania rozdziau o
wskanikach. Chyba tego nie chcesz? ;-)
Wskanikiem jest tu oczywicie aBajka - jaka nazwa tablicy wskazuje na jej pierwszy
element. W zasadzie wic mona dokona jego dereferencji i dosta si do tego
elementu:
*aBajka = "Dawno, dawno temu, ...";
Przesuwajc wskanik przy pomocy dodawania mona te dosta si do pozostaej czci
tablicy:
*(aBajka + 1) = "w odleglej galaktyce...";
*(aBajka + 2) = "zylo sobie siedmiu kransoludkow...";
Taki zapis jest jednak do kopotliwy w interpretacji - cho koniecznie trzeba go zna
(przydaje si przy iteratorach STL). C++ ma wygodniejszy sposb dostepu do elementw
tablicy o danym indeksie - jest to wanie operator indeksowania.
382
Zaawansowane C++
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
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)
384
Zaawansowane C++
Operatory typw
Istniej jzyki programowania, ktre cakiem dobrze radz sobie bez posiadania cile
zarysowanych typw danych. C++ do nich nie naley: w nim typ jest spraw bardzo
wan, a do pracy z nim oddelegowano kilka specjalnych operatorw.
Operatory rzutowania
Rzutowanie jest zmian typu wartoci, czyli jej konwersj. Mamy par operatorw, ktre
zajmuj si tym zadaniem i robi to w rny sposb.
Wrd nich s tak zwane cztery nowe operatory, o skadni:
okrelenie_cast<typ_docelowy>(wyraenie)
To wanie one s zalecane do uywania we wszystkich sytuacjach, wymagajcych
rzutowania. C++ zachowuje aczkolwiek take star form rzutowania, znan z C.
static_cast
Ten operator moe by wykorzystywany do wikszoci konwersji, jakie zdarza si
przeprowadza w C++. Nie oznacza to jednak, e pozwala on na wszystko:
Poprawno rzutowania static_cast jest sprawdzana w czasie kompilacji programu.
static_cast mona uywa np. do:
konwersji midzy typami numerycznymi
rzutowania liczby na typ wyliczeniowy (enum)
107
Zaawansowana obiektowo
385
dynamic_cast
Przy pomocy dynamic_cast mona rzutowa wskaniki i referencje do obiektw w d
hierarchii dziedziczenia. Oznacza to, e mona zamieni odwoanie do obiektu klasy
bazowej na odwoanie do obiektu klasy pochodnej. Wyglda to np. tak:
class CFoo
class CBar : public CFoo
{ /* ... */ };
{ /* ... */ };
// ...
Taka zamiana nie zawsze jest moliwa, bo przecie dany wskanik (referencja)
niekoniecznie musi pokazywa na obiekt danej klasy pochodnej. Operacja jest jednak
bezpieczna, poniewa:
Poprawno rzutowania dynamic_cast jest sprawdzana w czasie dziaania programu.
Wiemy doskonale, w jaki sposb pozna rezultat tego sprawdzania. dynamic_cast
zwraca po prostu NULL (wskanik pusty, zero), jeeli rzutowanie nie mogo zosta
wykonane. Naley to zawsze skontrolowa:
if (!pBar)
{
// OK - pBar faktycznie pokazuje na obiekt klasy CBar
}
Dla skrcenia zapisu mona wykorzysta warto zwracan operatora przypisania:
if (pBar = dynamic_cast<CBar*>(pFoo))
{
// rzutowanie powiodo si
}
Znak = jest tu oczywicie zamierzony. Warunek bdzie mia bowiem warto rwn
rezultatowi rzutowania, zatem bdzie prawdziwy tylko wtedy, gdy si ono powiedzie.
Zwrcony wskanik bdzie wtedy rny od zera.
reinterpret_cast
reinterpret_cast moe suy do dowolnych konwersji midzy wskanikami, a take do
rzutowania wskanikw na typy liczbowe i odwrotnie. Wachlarz moliwoci jest wic
szeroki, niestety:
Poprawno rzutowania reinterpret_cast nie jest sprawdzana.
Zaawansowane C++
386
// liczba 32-bitowa
// wskanik na liczby 8-bitowe (bajty)
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
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
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
Operator warunkowy
Operator ?: jest nazywamy ternarnym, czyli trjargumentowym. Jako jedyny bierze
bowiem trzy dane:
warunek ? wynik_dla_prawdy : wynik_dla_faszu
Umiejtne uycie tego operatora skraca kod i pozwala unikn niepotrzebnych instrukcji
if. Co ciekawe, moe on by take uyty w deklaracjach, np. pl w klasach. Wtedy
jednak wszystkie jego operandy musz by staymi.
Przecinek
Przecinek (ang. comma) to operator o najniszym priorytecie. Oprcz tego, e oddziela
on argumenty funkcji, moe te wystpowa samodzielnie, np.:
(nX + 17, 26, rand() % 5, nY)
W takim wyraeniu operandy s obliczane od lewej do prawej, natomiast wynikiem jest
warto ostatniego wyraenia. Tutaj wic bdzie to nY.
Przecinek przydaje si, gdy chcemy wykona pewn dodatkow czynno w trakcie
wyliczania jakiej wartoci. Przykadowo, spjrzmy na tak ptl odczytujc znaki:
char chZnak;
while (chZnak = ReadChar(), chZnak != ' ')
{
// zrb co ze znakiem, ktry nie jest spacj
}
ReadChar() jest funkcj, ktra pobiera nastpny znak (np. z pliku). Sama ptla ma za
wykonywa si a do napotkania spacji. Zanim jednak mona sprawdzi, czy dany znak
jest spacj, trzeba go odczyta. Robimy to w warunku ptli, posugujc si przecinkiem.
Bez niego trzebaby najprawdopodobniej zmieni ca ptl na do, co spowodowaoby
konieczno powtrzenia kodu wywoujcego ReadChar(). Inne wyjcie to uycie ptli
nieskoczonej. C++ pozwala jednak osign ten sam efekt na kilka sposobw, spord
ktrych wybieramy ten najbardziej nam pasujcy.
Funkcje operatorowe
Pomylmy: co waciwie robi kompilator, gdy natrafi w wyraeniu na jaki operator? Czy
tylko sobie znanymi sposobami oblicza on docelow warto, czy moe jednak jest w tym
jaka zasada?
Ot tak. Dziaanie operatora definiuje pewna funkcja, zwana funkcj operatorow
(ang. operator function). Istnieje wiele takich funkcji, ktre s wbudowane w kompilator i
dziaaj na typach podstawowych. Dodawanie, odejmowanie i inne predefiniowane
dziaania na liczbach s dostpne bez adnych stara z naszej strony.
Kiedy natomiast chcemy przeciy jaki operatory, to oznacza to konieczno napisania
wasnej funkcji dla nich. Zwyczajnie, trzeba poda jej argumenty oraz warto zwracan i
Zaawansowane C++
390
wypeni kodem. Nie ma w tym adnej magii. Za chwil zreszt przekonasz si, jak to
dziaa.
Zaawansowana obiektowo
391
Pozostae sprawy
Warto jeszcze powiedzie o pewnych naturalnych sprawach:
przynajmniej jeden argument przecianego operatora musi by innego typu ni
wbudowane. To naturalne: operatory przeciamy na rzecz wasnych typw
(klas), bo dziaania na typach podstawowych s wyaczn domen kompilatora.
Nie wtrcamy si w nie
funkcja operatorowa nie moe posiada parametrw domylnych
przecienia nie kumuluj si, tzn. jeeli na przykad przeciymy operatory +
oraz =, nie bdzie to oznaczao automatycznego zdefiniowania operatora +=.
Kade nowe znaczenie dla operatora musimy poda sami
392
Zaawansowane C++
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
// OK
// te OK!
Zaawansowane C++
394
};
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.
108
Zaawansowane C++
396
};
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
// 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.
Zaawansowane C++
398
Zaawansowana obiektowo
399
W kadym wic przypadku jeden operator*() nie wystarczy109. Musimy doda jego
kolejn wersj:
class CVector2D
{
// (pomijamy szczegy)
};
};
// iloczyn skalarny
friend float operator*(const
const
{
return vWektor1.m_fX *
+ vWektor1.m_fY
}
CVector2D& vWektor1,
CVector2D& vWektor2)
vWektor2.m_fX,
* vWektor2.m_fY;
Operatory przypisania
Teraz porozmawiamy sobie o pewnym wyjtkowym operatorze. Jest on unikalny pod
wieloma wzgldami; mowa o operatorze przypisania (ang. assignment operator)
tudzie podstawienia.
Do czsto nie potrzebujemy nawet jego wyranego zdefiniowania. Kompilator dla
kadej klasy generuje bowiem taki operator, o domylnym dziaaniu. Taki automatyczny
operator dokonuje przypisania skadnik po skadniku - tak wic po jego zastosowaniu
przypisywane obiekty s sobie rwne na poziomie wartoci pl110. Taka sytuacja nam
czsto odpowiada - przykadowo, dla naszej klasy CVector2D bdzie to idealne
rozwizanie. Niekiedy jednak nie jest to dobre wyjcie - za chwil zobaczymy, dlaczego.
Powiedzmy jeszcze tylko, e domylny operator przypisania nie jest tworzony przez
kompilator, jeeli klasa:
109
Pomijam tu zupenie fakt, e za chwil funkcj t zdefiniujemy po raz trzeci - tym razem jako iloczyn
skalarny dwch wektorw.
110
W tym kopiowanie pole po polu wykorzystywane s aczkolwiek indywidualne operatory przypisania od klas,
ktre instancjujemy w postaci pl. Nie zawsze wic obiekty takie faktycznie s sobie doskonale rwne.
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
// 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;
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;
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();
// !!!
Zaawansowana obiektowo
405
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 }
Zaawansowana obiektowo
407
};
// operator ->*
int& operator->*(int CFoo::*)
{ return nPole1; }
Zaawansowane C++
408
};
//
//
//
//
//
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
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.
// delete
void operator delete(void* pWskaznik)
{
// informacja
std::cout << "Zwalniamy wskaznik " << pWskaznik;
};
// usuwamy pami
::delete pWskaznik;
Zaawansowana obiektowo
411
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";
// 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.
== 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
414
Zaawansowane C++
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.
416
Zaawansowane C++
VECTOR3 Wektor;
Nastpnie moemy te pobra adres jej pola - ktrej ze wsprzdnych:
float* pfX = &Wektor.x;
i tak oto uzyskuje adres czwartego elementu tablicy (o indeksie 3). Spjrzmy na
dodawane wyraenie:
3 * sizeof(int)
Okrela ono przesunicie (ang. offset) elementu tablicy o indeksie 3 wzgldem jej
pocztku. Znajc t warto kompilator oraz adres pierwszego elementu tablicy,
kompilator moe wyliczy pozycj w pamici dla elementu numer 3.
Dlaczego jednak o tym mwi? Ot bardzo podobna operacja zachodzi przy
odwoywaniu si do pola w obiekcie klasy (struktury). Kiedy bowiem odnosimy si
jakiego pola w ten oto sposb:
Wektor.y
to po pierwsze, kompilator zamienia to wyraenie tak, aby posugiwa si wskanikami,
bo to jest jego mow ojczyst:
(&Wektor)->y
Nastpnie stosuje on ten sam mechanizm, co dla elementw tablic. Oblicza wic adres
pola (tutaj y) wedug schematu:
&Wektor + offset_pola_y
Zaawansowana obiektowo
417
W tym przypadku sprawa nie jest aczkolwiek taka prosta, bo definicja klasy moe
zawiera pola wielu rnych typw o rnych rozmiarach. Offset nie bdzie wic mg by
wyliczany tak, jak to si dzieje dla elementu tablicy. On musi by znany ju wczeniej
Skd?
Z definicji klasy! Okrelajc nasz klas w ten sposb:
struct VECTOR3 { float x, y, z; };
zdefiniowalimy nie tylko jej skadniki, ale te kolejno pl w pamici. Oczywicie nie
musimy podawa dokadnych liczb, precyzujcych pooenie np. pola z wzgldem obiektu
klasy VECTOR3. Tym zajmie si ju sam kompilator: przeanalizuje ca definicj i dla
kadego pola wyliczy sobie oraz zapisze gdzie odpowiednie przesunicie.
I t wanie liczb nazywamy wskanikiem na pole klasy:
Wskanik na pole klasy jest okreleniem miejsca w pamici, jakie zajmuje pole
danej klasy, wzgldem pocztku obiektu w pamici.
W przeciwniestwie do zwykego wskanika nie jest to wic liczba bezwzgldna. Nie
mwi nam, e tu-i-tu znajduje si takie-a-takie pole. Ona tylko informuje, o ile bajtw
naley si przesun, poczynajc od adresu obiektu, a znale w pamici konkretne pole
w tym obiekcie.
Moe jeszcze lepiej zrozumiesz to na przykadzie kodu. Jeeli stworzymy sobie obiekt
(statycznie, dynamicznie - niewane) - na przykad obiekt naszego wektora:
VECTOR3* pWektor = new VECTOR3;
i pobierzemy adres jego pola - na przykad adres pola y w tym obiekcie:
int* pnY = &pWektor->y;
to rnica wartoci obu wskanikw (adresw) - na obiekt i na jego pole:
pnY - pWektor
bedzie niczym innym, jak wanie offsetem tego pola, czyli jego miejscem w definicji
klasy! To jest ten rodzaj wskanikw C++, jakim si chcemy tutaj zaj.
Pobieranie wskanika
Zauwamy, e offset pola jest wartoci globaln dla caej klasy. Kady bowiem obiekt
ma tak samo rozmieszczone w pamici pola. Nie jest tak, e wrd kilku obiektw naszej
klasy VECTOR3 jeden ma pola uoone w kolejnoci x, y, z, drugi - y, z, x, trzeci - z, y, x,
itp. O nie, tak nie jest: wszystkie pola s poukadane dokadnie w takiej kolejnoci,
jak ustalilimy w definicji klasy, a ich umiejscowienie jest dla kadego obiektu
identyczne.
Uzyskanie offsetu danego pola, czyli wskanika na pole klasy, moe wic odbywa si bez
koniecznoci posiadania obiektu. Wystarczy tylko poda, o jak klas i o jakie pole nam
chodzi, np.:
&VECTOR3::y
Powysze wyraenie zwrci nam wskanik na pole y w klasie VECTOR3. Powtarzam
jeszcze raz (aby dobrze to zrozumia), i bdzie to ilo bajtw, o jak naley si
418
Zaawansowane C++
przesun poczynajc od adresu jakiego obiektu klasy VECTOR3, aby natrafi na pole y
tego obiektu. Jeeli jest to dla ciebie zbyt trudne, to moesz mysle o tym wskaniku
jako o indeksie pola y w klasie VECTOR3.
Zaawansowana obiektowo
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;
Zaawansowane C++
420
{ /* ... */ }
// ...
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
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
Zaawansowana obiektowo
423
czyli funkcja biorca jeden argument float i zwracajca wskanik do pl typu float w
klasie CNumber. Zatem znowu - zamiast wskanika na funkcj otrzymujemy funkcj
zwracajc wskanik.
Dla wskanikw na metody klas nie ma problemu z umieszczenia sowa kluczowego
konwencji wywoania, bo wszystkie metody klas uywaj domylnej i jedynie susznej w
ich przypadku konwencji thiscall. Nie ma moliwoci jej zmiany (mam nadziej, e jest
oczywiste, dlaczego).
Uycie wskanika
Czas wreszcie na akcj. Zobaczmy, jak mona wywoa metod pokazywan przez
wskanik:
CNumber Liczba = 42;
std::cout << (Liczba.*p2mfnMetoda)(2);
Potrzebujemy naturalnie jakiego obiektu klasy CNumber, aby na jego rzecz wywoa
metod. Tworzymy go wic; dalej znowu korzystamy z operatora .*, wywoujc przy
jego pomocy metod klasy CNumber dla naszego obiektu - przekazujemy jej jednoczenie
parametr 2. Poniewa po naszej zabawie z przypisywaniem p2mfnMetoda pokazywa na
metod Odejmij(), na ekranie zobaczylibymy:
40
Zaawansowane C++
424
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
Zaawansowana obiektowo
425
Zaawansowane C++
426
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)
};
{ }
Zaawansowana obiektowo
427
{ };
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
{ return x * 1.42f; }
// wskanik na obiekt
// wskanik na metod
// ------------------------------------------------------------------// konstruktor
METHODPOINTER(IObject* pObj, float (IObject::*p2mfn)(int))
: pObject(pObj), p2mfnMethod(p2mfn)
{ }
};
Zaawansowana obiektowo
429
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
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.
Zaawansowane C++
434
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.
// zwracamy wynik
return fWynik;
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;
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.
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;
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.
Wywoanie zwrotne
Idea wywoania zwrotnego (ang. callback) jest nieskomplikowana. Jeeli w pisanej
przez nas funkcji zachodzi sytuacja wyjtkowa, wywoujemy inn funkcj pomocniczn.
Taka funkcja moe peni rol ratunkow i sprbowa naprawi okolicznoci, ktre
doprowadziy do powstania problemu - jak np. bdne argumenty dla naszej funkcji. W
ostatecznoci moe to by tylko sposb na powiadomienie o nienaprawialnej sytuacji
wyjtkowej.
Uwaga o wygodnictwie
Zaleta wywoania zwrotnego uwidacznia si w powyszym opisie. Przy jego pomocy nie
jestemy skazani na bierne przyjcie do wiadomoci wystpienia bdu; przy odrobinie
dobrej woli mona postara si go naprawi.
Nie zawsze jest to jednak moliwe. Mona wprawdzie poprawi nieprawidowy parametr,
przekazany do funkcji, ale ju nic nie zaradzimy chociaby na brak pamici.
117
Wykorzystanie szablonw zlikwidowaoby obie te niedogodnoci, ale czy naprawd s one tego warte?
Wyjtki
439
Poza tym, technika callback z gry czyni pesymistyczne zaloenie, e sytuacje wyjtkowe
bd trafiay si na tyle czsto, e konieczny staje si mechanizm wywoa zwrotnych.
Jego stosowanie nie zawsze jest wspmierne do problemu, czasem jest to zwyczajne
strzelanie z armaty do komara. Przykadowo, w funkcji Pierwiastek() spokojnie
moemy sobie pozwoli na inne sposoby informowania o bdach - nawet w obliczu faktu,
e naprawienie nieprawidowego argumentu byoby przecie moliwe. Funkcja ta nie jest
bowiem na tyle kosztowna, aby opacao si chroni j przed niespodziewanym
zakoczeniem.
Dlaczego jednak wywoanie zwrotne jest taki cikim rodkiem? Ot wymaga ono
specjalnych przygotowa. Od strony programisty-klienta obejmuj one przede wszystkim
napisania odpowiednich funkcji zwrotnych. Od strony piszcego kod biblioteczny
wymagaj natomiast gruntowego obmylenia mechanizmu takich funkcji zwrotnych: tak,
aby nie mnoy ich ponad miar, a jednoczenie zapewni dla siebie pewn wygod i
uniwersalno.
Uwaga o logice
Funkcje callback s te bardzo kopotliwe z punktu widzenia logiki programu i jego
konstrukcji. Zakadaj bowiem, by kod niszego poziomu - jak funkcje biblioteczne w
rodzaju wspomnianej Pierwiastek() lub StrToUInt() - wywoyway kod wyszego
poziomu, zwizany bezporednio z dziaaniem samej aplikacji. amie to naturaln
hierarchi warstw kodu i burzy porzdek jego wykonywania.
Zakoczenie programu
Wyjtkowy bd moe spowodowa jeszcze jedn moliw akcj: natychmiastowe
zakoczenie dziaania programu.
Brzmi to bardzo drastycznie i takie jest w istocie. Naprawd trudno wskaza sytuacj, w
ktrej byoby konieczne przerwanie wykonywania aplikacji - zwaszcza niepoprzedzone
adnym ostrzeeniem czy zapytaniem do uytkownika. Chyba tylko krytyczne braki
pamici lub niezbdnych plikw mog by tego czciowym usprawiedliwieniem.
Na pewno jednak fatalnym pomysem jest stosowanie tego rozwizania dla kadej
sytuacji wyjtkowej. I chyba nawet nie musz mwi, dlaczego
Wyjtki
Takie s tradycyjne sposobu obsugi sytuacji wyjtkowych. Byy one przydatne przez
wiele lat i nadal nie straciy nic ze swojej uytecznoci. Nie myl wic, e mechanizm,
ktry zaraz poka, moe je cakowicie zastpi.
Tym mechanizmem s wyjtki (ang. exceptions). Skojarzenie tej nazwy z sytuacjami
wyjtkowymi jest jak najbardziej wskazane. Wyjtki su wanie do obsugi
niecodzienych, niewystpujcych w normalnym toku programu wypadkw.
Spjrzmy wic, jak moe si to odbywa w C++.
440
Zaawansowane C++
Blok try-catch
Obsuga sytuacji wyjtkowych zawiera si wewntrz blokw try i catch. Wygldaj one
na przykad tak:
try
{
ryzykowne_instrukcje
}
catch (...)
{
kod_obsugi_wyjtkw
}
ryzykowne_instrukcje zawarte wewntrz bloku try s kodem, ktry poddawany jest
pewnej specjalnej ochronie na wypadek wystpienia wyjtku. Na czym ta ochrona polega
- bdziemy mwi w nastpnym podrozdziale. Na razie zapamitaj, e w bloku try
umieszczamy kod, ktrego wykonanie moe spowodowa sytuacj wyjtkow, np.
wywoania funkcji bibliotecznych.
Jeeli tak istotnie si stanie, to wwczas sterowanie przenosi si do bloku catch.
Instrukcja catch apie wystpujce wyjtki i pozwala przeprowadzi ustalone dziaania
w reakcji na nie.
Wyjtki
441
Instrukcja throw
Kiedy wiadomo, e wystpia sytuacja wyjtkowa? Ot musi ona zosta
zasygnalizowana przy pomocy instrukcji throw:
throw obiekt;
Wystpienie tej instrukcji powoduje natychmiastowe przerwanie normalnego toku
wykonywania programu. Sterowanie przenosi si wtedy do najbliszego pasujcego bloku
catch.
Rzucony obiekt peni natomiast funkcj informujc. Moe to by warto dowolnego
typu - rwnie bdca obiektem zdefiniowanej przez nas klasy, co jest szczeglnie
przydatne. obiekt zostaje wyrzucony poza blok try; mona to porwna do pilota
katapultujcego si z samolotu, ktry niechybnie ulegnie katastrofie. Wystpienie throw
jest bowiem sygnaem takiej katastrofy - sytuacji wyjtkowej.
Wdrwka wyjtku
Zaraz za blokiem try nastpuje najczciej odpowiednia instrukcja catch, ktra zapie
obiekt wyjtku. Wykona potem odpowiednie czynnoci, zawarte w swym bloku, a
nastpnie program rozpocznie wykonywanie dalszych instrukcji, zaraz za blokiem
catch.
Jeli jednak wyjtek nie zostanie przechwycony, to moe on opuci sw macierzyst
funkcj i dotrze do tej, ktr j wywoaa. Jeli i tam nie znajdzie odpowiadajcego
bloku catch, to wyjdzie jeszcze bardziej na powierzchni. W przypadku gdy i tam nie
bdzie pasujcej instrukcji catch, bdzie wyskakiwa jeszcze wyej, i tak dalej.
Proces ten nazywamy odwijaniem stosu (ang. stack unwinding) i trwa on dopki jaka
instrukcja catch nie zapie leccego wyjtku. W skrajnym (i nieprawidowym) przypadku,
odwijanie moe zakoczy si przerwaniem dziaania programu - mwimy wtedy, e
wystpi niezapany wyjtek (ang. uncaught exception).
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
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;
Wyjtki
try
{
443
srand (static_cast<unsigned>(time(NULL)))
// losujemy rzucony wyjtek
switch (rand() % 4)
{
case 0:
throw "Wyjatek tekstowy";
case 1:
throw 1.5f;
case 2:
throw -12;
case 3:
throw (void*) NULL;
}
}
catch (int nZlapany)
{
std::cout << "Zlapalem wyjatek liczbowy z wartoscia " << nZlapany;
}
Komunikaty o bdach powinny by w zasadzie kierowane do strumienia cerr, a nie
cout. Tutaj jednak, dla zachowania prostoty, bd posugiwa si standardowym
strumieniem wyjcia. O pozostaych dwch rodzajach strumieni wyjciowych pomwimy
w rozdziale o strumieniach STL.
W tym kawaku kodu blok catch zapie liczb typu int - jeeli takowa zostanie
wyrzucona przez instrukcj throw. Przechwyci j w postaci lokalnej zmiennej nZlapany,
aby potem wywietli jej warto w konsoli.
A co z pozostaymi wyjtkami? Nie mamy instrukcji catch, ktre by je apay. Wobec
tego zostan one wyrzucone ze swej macierzystej funkcji i bd wdroway t ciek a
do natrafienia pasujcych blokw catch. Jeeli ich nie znajd, spowoduj zakoczenie
programu.
Powinnimy zatem zapewni obsug take i tych wyjtkw. Robimy w taki sposb, i
dopisujemy po prostu brakujce bloki catch:
catch (const
{
std::cout
}
catch (float
{
std::cout
}
catch (void*
{
std::cout
}
char szNapis[])
<< szNapis;
fLiczba)
<< "Zlapano liczbe: " << fLiczba;
pWskaznik)
<< "Wpadl wskaznik " << pWskaznik;
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)
{
{
{
{
/*
/*
/*
/*
...
...
...
...
*/
*/
*/
*/
}
}
}
}
Zaawansowane C++
446
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++
Zapanie i odrzucenie
Przy zagniedaniu blokw try (niewane, czy z porednictwem funkcji, czy nie) moe
wystpi czsta w praktyce sytuacja. Moliwe jest mianowicie, e po zapaniu wyjtku
przez bardziej wewntrzny catch nie potrafimy podj wszystkich akcji, jakie byyby
dla niego konieczne. Przykadowo, moemy tutaj jedynie zarejestrowa go w dzienniku
bdw; bardziej uyteczn reakcj powinien zaj si kto wyej.
Moglibymy pomin wtedy ten wewntrzny catch, ale jednoczenie pozbawilibymy si
moliwoci wczesnego zarejestrowania bdu. Lepiej wic pozostawi go na miejscu, a po
zakoczeniu zapisywania informacji o wyjtku wyrzuci go ponownie. Robimy to
instrukcj throw bez adnych parametrw:
throw;
Ta instrukcja powoduje ponowne rzucenie tego samego obiektu wyjtku. Teraz jednak
bd mogy zaj si nim bardziej zewntrzne bloki catch. Bd one pewnie bardziej
kompetentne ni nasze siy szybkiego reagowania.
// instrukcje
}
catch (...)
{
// obsuga wyjtkw
}
Uniwersalno tego specjalnego rodzaju catch polega na tym, i pasuj do niego
wszystkie obiekty wyjtkw. Jeeli kompilator, transportujc wyjtek, natrafi na
catch(...), to bezwarunkowo wybierze wanie ten wariant, nie ogldajc si na
adne inne. catch(...) jest wic wszystkoerny: pochania dowolne typy wyjtkw.
Pochania to zreszt dobre sowo. Wewntrz bloku catch(...) nie mamy mianowicie
adnych informacji o obiekcie wyjtku. Nie tylko o jego wartoci, ani nawet o jego typie.
Wiemy jedynie, e jaki wyjtek wystpi - i skromn t wiedz musimy si zadowoli.
Po co nam wobec tego taki dziwny blokcatch? Jest on przydatny tam, gdzie moemy
jako wykorzysta samo powiadomienie o wyjtku, nie znajc jednak jego typu ani
wartoci. Wewntrz catch(...) moemy jedynie podja pewne domylne dziaania.
Moemy na przykad dokona maego zrzutu pamici (ang. memory dump), zapisujc w
bezpiecznym miejscu wartoci zmiennych na wypadek zakoczenia programu. Moemy
te w jaki sposb przygotowa si do waciwej obsugi bdw.
Cokolwiek zrobimy, na koniec powinnimy przekaza wyjtek dalej, czyli uy
konstrukcji:
throw;
Wyjtki
449
Jeeli tego nie zrobimy, to catch(...) zdusi w zarodku wszelkie wyjtki, nie pozwalajc
na to, by dotary one dalej.
***
Na tym kocz si podstawowe informacje o mechanizmie wyjtkw. To jednak nie
wszystkie aspekty tej techniki. Musimy sobie jeszcze porozmawia o tym, co dzieje si
midzy rzuceniem wyjtku poprzez throw i jego zapaniem przy pomocy catch.
Porozmawiamy zatem o odwijaniu stosu.
Odwijanie stosu
Odwijanie stosu (ang. stack unwinding) jest procesem cile zwizanym z wyjtkami.
Jakkolwiek sama jego istota jest raczej prosta, musimy wiedze, jakie ma on
konsekwencje w pisanym przez nas kodzie.
Wychodzenie na wierzch
Na czym jednak polega samo odwijanie? Ot mona opisa je w skrcie jako
wychodzenie punktu wykonania ze wszystkich blokw kodu. Co to znaczy,
najlepiej wyjani na przykadzie.
Zamy, e mamy tak oto sytuacj:
try
{
}
catch
{
// ...
}
Zaawansowane C++
450
throw
break
return
przekazywanie
sterowania
do najbliszego
pasujcego bloku
catch
warto
obiekt wyjtku
dowolnego typu
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
Wszystkie te trzy wasnoci trzech instrukcji s bardzo wane i koniecznie musisz o nich
pamita. Nie bdzie to chyba dla ciebie problemem, skoro dwie z omawianych instrukcji
znasz doskonale, a o wszystkich aspektach trzeciej porozmawiamy sobie jeszcze cakiem
obszernie.
Wyjtki
451
Specyfikacja wyjtkw
Aby jednak mona byo to uczyni, naley wiedzie, jakiego typu wyjtki funkcja moe
wyrzuca na zewntrz. Dziki temu moemy opakowa jej przywoanie w blok try i
doda za nim odpowiednie instrukcje catch, chwytajce waciwe obiekty.
Skd mamy uzyska t tak potrzebn wiedz? Wydawaoby si, e nic prostszego.
Wystarczy przejrze kod funkcji, znale wszystkie instrukcje throw i okreli typ
obiektw, jakie one rzucaj. Nastpnie naley odrzuci te, ktre s obsugiwane w samej
funkcji i zaj si tylko wyjtkami, ktre z niej uciekaj.
Ale to tylko teoria i ma ona jedn powan sabostk. Wymaga przecie dostpu do kodu
rdowego funkcji, a ten nie musi by wcale osigalny. Wiele bibliotek jest
dostarczanych w formie skompilowanej, zatem nie ma szans na ujrzenie ich wntrza.
Mimo to ich funkcjom nikt cakowicie nie zabroni rzucania wyjtkw.
Dlatego naleao jako rozwiza ten problem. Uzupeniono wic deklaracje funkcji o
dodatkow informacj - specyfikacj wyjtkw.
Specyfikacja albo wyszczeglnienie wyjtkw (ang. exceptions specification) mwi
nam, czy dana funkcja wyrzuca z siebie jakie nieobsuone obiekty wyjtkw, a jeli
tak, to informuje take o ich typach.
Takie wyszczeglnienie jest czci deklaracji funkcji - umieszczamy je na jej kocu, np.:
void Znajdz(int* aTablica, int nLiczba) throw(void*);
Po licie parametrw (oraz ewentualnych dopiskach typu const w przypadku metod
klasy) piszemy po prostu sowo throw. Dalej umieszczamy w nawiasie list typw
wyjtkw, ktre bd opuszczay funkcj i ktrych zapanie bdzie naleao do
obowizkw wywoujcego. Oddzielamy je przecinkami.
Ta lista typw jest nieobowizkowa, podobnie zreszt jak caa fraza throw(). S to
jednak dwa szczeglne przypadki - wygldaj one tak:
void Stepuj();
void Spiewaj() throw();
Brak specyfikacji oznacza tyle, i dana funkcja moe rzuca na zewntrz wyjtki
dowolnego typu. Natomiast podanie throw bez okrelenia typw wyjtkw informuje,
e funkcja w ogle nie wyrzuca wyjtkw na zewntrz. Widzc tak zadeklarowan
funkcj moemy wic mie pewno, e jej wywoania nie trzeba umieszcza w bloku try
i martwi si o obsug wyjtkw przez catch.
Specyfikacja wyjtkw jest czci deklaracji funkcji, zatem bdzie ona wystpowa
np. w pliku nagwkowym zewntrznej biblioteki. Jest to bowiem niezbdna informacja,
potrzebna do korzystania z funkcji - podobnie jak jej nazwa czy parametry. Kiedy jednak
tamte wiadomoci podpowiadaj, w jaki sposb wywoywa funkcj, wyszczeglnienie
throw() mwi nam, jakie wyjtki musimy przy okazji tego wywoania obsugiwa.
Warto te podkreli, e mimo swej obecnoci w deklaracji funkcji, specyfikacja wyjtkw
nie naley do typu funkcji. Do niego nadal zaliczamy wycznie list parametrw oraz
typ wartoci zwracanej. Na pokazane wyej funkcje Stepuj() i Spiewaj() mona wic
pokazywa tym samym wskanikiem.
452
Zaawansowane C++
Wyjtki
453
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
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.
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++
Skutki wypadku
Co si stanie, jeeli zignorujemy ktry z zakazw podanych wyej i rzucimy nowy
wyjtek w trakcie obsugi innego?
Bdzie to wtedy bardzo powana sytuacja. Oznacza ona bdzie, e kompilator nie jest w
stanie poprawnie przeprowadzi obsugi wyjtku. Inaczej mwic, mechanizm
wyjtkw zawiedzie - tyle e bdzie to rzecz jasna nasza wina.
Co moe wwczas zrobi kompilator? Niewiele. Jedyne, co wtedy czyni, to wywoanie
funkcji terminate(). Skutkiem jest wic nieprzewidziane zakoczenie programu.
Naturalnie, zmiana funkcji terminate() (poprzez set_terminate()) sprawi, e zamiast
domylnej bdzie wywoywana nasza procedura. Piszc j powinnimy pamita, e
funkcja terminate() jest wywoywana w dwch sytuacjach:
gdy wyjtek nie zosta zapany przez aden blok catch
gdy zosta rzucony nowy wyjtek w trakcie obsugi poprzedniego
Obie s sytuacjami krytycznymi. Zatem niezalenie od tego, jakie dodatkowe akcje
bdziemy podejmowa w naszej funkcji, zawsze musimy na koniec zamkn nasz
program. W aplikacjach konsolowych mona uczyni to poprzez exit().
Wyjtki
457
delete pFoo;
// zwolnienie obiektu-zasobu
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
{
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
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
{
}
// (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
};
// 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
{
}
// (tutaj catch)
Wyjtki
461
Nieco uwag
Aby jednak nie byo a tak bardzo piknie, na koniec paragrafu musz jeszcze troch
pogldzi :) Chodzi mianowicie o dwie wane sprawy zwizane ze sprytnymi
wskanikami, ktrych uywamy w poczeniu z mechanizmem wyjtkw.
Co ju zrobiono za nas
Metoda opakowywania zasobw moe si wydawa nazbyt praco- i czasochonna, a
przede wszystkim wtrna. Stosujc j pewnie szybko zauwayby, e napisane przez
ciebie klasy powinny by obecne w niemal kadym programie korzystajcym z wyjtkw.
Naturalnie, mog by one dobrym punktem wyjcia dla twojej wasnej biblioteki z
przydatnymi kodami, uywanymi w wielu aplikacjach. Niewykluczone, e kiedy bdziesz
musia napisa przynajmniej kilka takich klas-opakowa, jeeli zechcesz skorzysta z
zasobw innych ni pami operacyjna czy pliki dyskowe.
Na razie jednak lepiej chyba sprawdz si narzdzia, ktre otrzymujesz wraz z jzykiem
C++ i jego Bibliotek Standardow. Zobaczmy pokrtce, jak one dziaaj; ich dokadny
opis znajdziesz w kolejnych rozdziaach, powiconych samej tylko Bibliotece
Standardowej.
Klasa std::auto_ptr
Sprytne wskaniki chronice przed wyciekami pamici, powstajcymi przy rzucaniu
wyjtkw, s do czsto uywane w praktyce. Samodzielne ich definiowanie byoby wic
uciliwe. W C++ mamy wic ju stworzon do tego klas std::auto_ptr.
cilej mwic, auto_ptr jest szablonem klasy. Co to dokadnie znaczy, dowiesz si w
nastpnym rozdziale. Pki co bdziesz wiedzia, i pozwala to na uywanie auto_ptr w
charakterze wskanika do dowolnego typu danych. Nie musimy ju zatem definiowia
adnych klas.
Aby skorzysta z auto_ptr, trzeba jedynie doczy standardowy plik nagwkowy
memory:
Zaawansowane C++
462
#include <memory>
Teraz moemy ju korzysta z tego narzdzia. Z powodzeniem moe ono zastpi nasz
pieczoowicie wypracowan klas CFooSmartPtr:
try
{
}
// (tutaj catch)
}
// (tutaj catch)
Wyjtki
463
Wykorzystanie wyjtkw
Dwa poprzednie podrozdziay mwiy o tym, czym s wyjtki i jak dziaa ten mechanizm
w C++. W zasadzie na tym monaby poprzesta, ale taki opis na pewno nie bdzie
wystarczajcy. Jak kady element jzyka, take i wyjtki naley uywa we waciwy
sposb; korzystaniu z wyjtkw w praktyce zostanie wic powicony ten podrozdzia.
Wyjtki w praktyce
Zanim z pieni na ustach zabierzemy si do wykorzystywania wyjtkw, musimy sobie
odpowiedzie na jedno fundamentalne pytanie: czy tego potrzebujemy? Takie
postawienie sprawy jest pewnie zaskakujce - dotd wszystkie poznawane przez nas
elementy C++ byy waciwie niezbdne do efektywnego stosowania tego jzyka. Czy z
wyjtkami jest inaczej? Przyjrzyjmy si sprawie bliej
Moe powiedzmy sobie o dwch podstawowych sytuacjach, kiedy wyjtkw nie
powinnimy stosowa. W zasadzie mona je zamkn w jedno stwierdzenie:
Nie powinno si wykorzystywa wyjtkw tam, gdzie z powodzeniem wystarczaj inne
techniki sygnalizowania i obsugi bdw.
Oznacza to, e:
nie powinnimy na si dodawa wyjtkw do istniejcego programu. Jeeli po
przetestowaniu dziaa on dobrze i efektywnie bez wyjtkw, nie ma adnego
powodu, aby wprowadza do kodu ten mechanizm
dla tworzonych od nowa, lecz krtkich programw wyjtki mog by zbyt
potnym narzdziem. Wysiek woony w jego zaprogramowanie (jak si zaraz
przekonamy - wcale niemay) nie musi si opaca. Co oznacza pojcie krtki
program, to ju kady musi sobie odpowiedzie sam; zwykle uwaa si, e
krtkie s te aplikacje, ktre nie przekraczaj rozmiarami 1000-2000 linijek kodu
Wida wic, e nie kady program musi koniecznie stosowa ten mechanizm. S
oczywicie sytuacje, gdy oby si bez niego jest bardzo trudno, jednak naduywanie
wyjtkw jest zazwyczaj gorsze ni ich niedostatek. O obu sprawach (korzyciach
pyncych z wyjtkw i ich przesadnemu stosowaniu) powiemy sobie jeszcze pniej.
Zamy jednak, e zdecydowalimy si wykorzystywa wyjtki. Jak poprawnie
zrealizowa te intencje? Jak wikszo rzeczy w programowaniu, nie jest to trudne :)
Musimy mianowicie:
pomyle, jakie sytuacje wyjtkowe mog wystpi w naszej aplikacji i wyrni
wrd nich poszczeglne rodzaje, a nawet pewn hierarchi. To pozwoli na
stworzenie odpowiednich klas dla obiektw wyjtkw, czym zajmiemy si w
pierwszym paragrafie
Zaawansowane C++
464
Potem moemy ju tylko mie nadziej, e nasza ciko wykonana praca nigdy nie
bdzie potrzebna. Najlepiej przecie byoby, aby sytuacje wyjtkowe nie zdarzay si, a
nasze programy dziaay zawsze zgodnie z zamierzeniami C, praca programisty nie
jest usana rami, wic tak nigdy nie bdzie. Nauczmy si wic poprawnie reagowa na
wszelkiego typu nieprzewidziane zdarzenia, jakie mog si przytrafi naszym aplikacjom.
Definiujemy klas
Co wic powinien zawiera taki obiekt? Najwaniejsze jest ustalenie rodzaju bdu oraz
miejsca jego wystpienia w kodzie. Typowym zestawem danych dla wyjtku moe by
zatem:
nazwa pliku z kodem i numer wiersza, w ktrym rzucono wyjtek. Do tego mona
doda jeszcze dat kompilacji programu, aby rozrni jego poszczeglne wersje
dane identyfikacyjne bdu - w najprostszej wersji tekstowy komunikat
Nasza klasa wyjtku mogaby wic wyglda tak:
#include <string>
class CException
{
private:
// dane wyjtku
std::string m_strNazwaPliku;
unsigned
m_uLinijka;
std::string m_strKomunikat;
public:
// konstruktor
CException(const std::string& strNazwaPliku,
unsigned uLinijka,
const std::string& strKomunikat)
: m_strNazwaPliku(strNazwaPliku),
m_uLinijka(uLinijka),
m_strKomunikat(strKomunikat)
{ }
// -------------------------------------------------------------
};
// metody dostpowe
std::string NazwaPliku() const
unsigned
Linijka() const
std::string Komunikat() const
{ return m_strNazwaPliku; }
{ return m_uLinijka; }
{ return m_strKomunikat; }
Wyjtki
465
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
};
plikami
: public IException
specyficzne dla tego rodzaju wyjtku
Wyswietl();
Kod warstwowy
Jednym z podstawowych powodw, dla ktrych wprowadzono wyjtki w C++, bya
konieczno zapewnienia jakiego sensownego sposobu reakcji na bdy w programach o
skomplikowanym kodzie. Kady wikszy (i dobrze napisany) program ma bowiem
skonno do rozwarstwiania kodu.
Nie jest to bynajmniej niepodane zjawisko, wrcz przeciwnie. Polega ono na tym, e w
aplikacji moemy wyrni fragmenty wyszego i niszczego poziomu. Te pierwsze
odpowiadaj za ca logik aplikacji, w tym za jej komunikacj z uytkownikiem; te
drugie wykonuj bardziej wewntrzne czynnoci, takie jak na przykad zarzdzanie
pamici operacyjn czy dostp do plikw na dysku.
Taki podzia jest korzystny, poniewa uatwia konserwacj programu, a take
wykorzystywanie pewnych fragmentw kodu (zwaszcza tych niskopoziomowych) w
kolejnych projektach. Funkcje odpowiedzialne za pewne proste czynnoci, jak
wspomniany dostp do plikw nie musz nic wiedzie o tym, kto je wywouje - waciwie
to nawet nie powinny. Innymi sowy:
Kod niszego poziomu powinien by zazwyczaj niezaleny od kodu wyszego poziomu.
Wyjtki
467
Dobre wyporodkowanie
Ich stosowanie jest szczeglnie wskazane wanie wtedy, gdy nasz kod ma kilka
logicznych warstw, co zreszt powinno zdarza si jak najczciej. Wwczas odnosimy
jedn zasadnicz korzy: nie musimy martwi si o sposb, w jaki informacja o bdzie
dotrze z pokadw gbinowych programu, gdzie wystpia, na grne pitra, gdzie
mogaby zosta waciwie obsuona.
Naszym problemem jest jednak co innego. O ile zazwyczaj dokadnie wiadomo, gdzie
wyjtek naley rzuci (wiadomo - tam gdzie co si nie powiodo), o tyle trudno moe
sprawi wybranie waciwego miejsca na jego zapanie:
jeeli bdzie ono za nisko, wtedy najprawdopodobniej nie bdzie moliwe
podjcie adnych rozsdnych dziaa w reakcji na wyjtek. Przykadowo,
wymieniona funkcja otwierajca plik nie powinna sama apa wyjtku, ktry rzuci,
bo bdzie wobec niego bezradna. Skoro przecie rzucia ten wyjtek, jest to
wanie znak, i nie radzi sobie z powstaa sytuacj i oddaje inicjatyw komu
bardziej kompetentnemu
z drugiej strony, umieszczenie blokw catch za wysoko powoduje zbyt due
zamieszanie w funkcjonowaniu programu. Powoduje to, e punkt wykonania
przeskakuje o cae kilometry, niespodziewanie przerywajc wszystko znajdujce
si po drodze zdania. Nie naley bowiem zapomina, e po rzuceniu wyjtku nie
ma ju powrotu - dalsze wykonywanie zostanie co najwyej podjte po
wykonaniu bloku catch, ktry ten wyjtek. Cakowitym absurdem jest wic np.
ujcie caej zawartoci funkcji main() w blok try i obsuga wszystkich wyjtkw w
nastpujcym dalej bloku catch. Nietrudno przecie domyli si, e takie
rozwizanie spowoduje zakoczenie programu po kadym wystpieniu wyjtku
Pytanie brzmi wic: jak osign rozsdny kompromis? Trzeba pogodzi ze sob dwie
racje:
konieczno sensownej obsugi wyjtku
konieczno przywrcenia programu do normalnego stanu
Naley wic apa wyjtek w takim miejscu, w ktrym ju moliwe jest jego
obsuenie, ale jednoczenie po jego zakoczeniu program powinien nadal mc podja
podj w miar normaln prac.
Przykad? Jeeli uytkownik wybierze opcj otwarcia pliku, ale potem poda nieistniejc
nazw, program powinien po prostu poinformowa o tym i ponownie zapyta o nazw
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.
//
}
catch
{
//
}
catch
{
//
}
catch
{
//
}
...
(CMemoryException& Wyjatek)
...
(CFilesException& Wyjatek)
...
(IException& Wyjatek)
...
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
Lepiej referencj
We wszystkich przytoczonych ostatnio kodach apaem wyjatki poprzez referencje do
nich, a nie poprzez same obiekty. Zbywalimy to dotd milczeniem, ale czas ten fakt
wyjani.
Przyczyna jest waciwie cakiem prosta. Referencje s, jak pamitamy,
zakamuflowanymi wskanikami: faktycznie rni si od wskanikw tylko drobnymi
szczegami, jak choby skadni. Zachowuj jednak ich jedn cenn waciwo
obiektow: pozwalaj na stosowanie polimorfizmu metod wirtualnych.
To doskonalne znane nam zjawisko jest wic moliwe do wykorzystania take przy
obsudze wyjtkw. Oto przykad:
try
{
// ...
}
catch (IException& Wyjatek)
{
// wywoanie metody wirtualnej, pno wizanej
Wyjatek.Wyswietl();
}
Metoda wirtualna Wyswietl() jest tu pno wizana, zatem to, ktry jej wariant - z klasy
podstawowej czy pochodnej - zostanie wywoany, decyduje si podczas dziaania
programu. Jest to wic inny sposb na swoiste rozrnienie typu wyjtku i podjcie
dziaa celem jego obsugi.
Uwagi oglne
Na sam koniec podziel si jeszcze garci uwag oglnych dotyczcych wyjtkw. Przede
wszystkim zastanowimy si nad korzyciami z uywania tego mechanizmu oraz
sytuacjami, gdzie czsto jest on naduywany.
470
Zaawansowane C++
Do tej grupy monaby prbowa zaliczy te destruktory, ale jak przecie, z destruktorw
nie mona rzuca wyjtkw.
Dziki temu, e wyjtki nie opieraj si na normalnym sposobie wywoywania i powrotu z
funkcji, mog by uywane take i w tych specjalnych funkcjach.
Uproszczenie kodu
Jakkolwiek dziwnie to zabrzmi, wyjtki umoliwiaj te znaczne uproszczenie kodu i
uczynienie go przejrzystszym. Jest tak, gdy pozwalaj one przenie sekwencje
odpowiedzialne za obsug bdw do osobnych blokw, z dala od waciwych instrukcji.
W normalnym kodzie procedury wygldaj mniej wicej tak:
zrb co
sprawd, czy si udao
zrb co innego
sprawd, czy si udao
zrb jeszcze co
sprawd, czy nie byo bdw
itd.
Wyrnione tu sprawdzenia bdw s realizowane zwykle przy pomocy instrukcji if lub
switch. Przy ich uyciu kod staje si wic pltanin instrukcji warunkowych, raczej
trudnych do czytania.
Gdy za uywamy wyjtkw, to obsuga bdw przenosi si na koniec algorytmu:
zrb co
zrb co innego
zrb jeszcze co
itd.
obsu ewentualne niepowodzenia
Oczywicie dla tych, ktrzy nie dbaj o porzdek w kodzie, jest to aden argument, ale ty
si chyba do nich nie zaliczasz?
Wyjtki
471
Naduywanie wyjtkw
Czytajc o zaletach wyjtkw, nie mona wpa w bezkrytyczny zachwyt nad nimi. One
nie s ani obowizkow technik programistyczn, ani te nie s lekarstwem na bdy w
programach, ani nawet nie s pasujcym absolutnie wszdzie rozwizaniem. Wyjtkw
atwo mona naduy i dlatego chc si przed tym przestrzec.
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
Szablony
475
Takich wersji musiaoby by jednak bardzo wiele: za kadym kolejnym typem, dla
ktrego chcielibymy stosowa max(), musiaaby i odrbna funkcja. Ich definiowanie
byoby uciliwe i nudne, a podczas wykonywania tej nucej czynnoci trudno byoby nie
zwtpi, czy jest to aby na pewno suszne rozwizanie
Moliwe rozwizania
Ale jakie mamy wyjcie?, spytasz pewnie. C, mona sobie jako radzi
Wykorzystanie preprocesora
Ogln funkcj max() (i podobne) moemy zasymulowa przy uyciu parametryzowanych
makr:
#define MAX(a,b)
476
Zaawansowane C++
};
Bdziemy musieli si jednak zmaga z niedogodnociami wskanikw void* - przede
wszystkim z utrat informacji o rzeczywistym typie danych:
CPtrArray Tablica(5);
// alokacja pamici dla elementu (!)
Tablica[2] = new int;
// przypisanie - nieszczeglnie adne...
*(static_cast<int*>(Tablica[2])) = 10;
Kadorazowe rzutowanie na waciwy typ elementw (tutaj int) na pewno nie bdzie
naleao do przyjenoci. Poza tym trzeba bdzie pamita o zwolnieniu pamici
zaalokowanej dla poszczeglnych elementw. W przypadku maych obiektw, jak liczby,
nie ma to adnego sensu
Zatem nie! To na pewno nie jest zadowalajce wyjcie!
Kompilator to potrafi
Ale nie! Moemy ten wzorzec - ten szablon (ang. template) - wpisa do kodu, tworzc
ogln funkcj max(). Trzeba to jedynie zrobi w odpowiedni sposb - tak, aby
kompilator wiedzia, z czym ma do czynienia. Zobaczmy wic, jak mona tego dokona.
Skadnia szablonu
A zatem: chcc zdefiniowa wzorzec funkcji max(), musimy napisa go w ten oto sposb
sposb:
template <typename TYP>
TYP max(TYP Parametr1, TYP Parametr2)
{
return (Parametr1 > Parametr2 ? Parametr1 : Parametr2);
}
Szablony
477
// 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.
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.
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.
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
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.
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); }
};
Szablony
483
// szablon max<TYP*>(),
// gdzie TYP = int
W tym wic przypadku wywietlan liczb bdzie zawsze 98, bo liczy si bd tutaj
faktyczne wartoci, a nie rozmieszczenie zmiennych w pamici (a wic nie adresy, na
ktre pokazuj wskaniki).
Czciowe specjalizacje szablonw funkcji nie wygldaj moe na zbytnio
skomplikowane. Moe ci jednak zaskoczy to, i to jeden z najbardziej zaawansowanych
aspektw szablonw - tak bardzo, e pki co Standard C++ o nim nie wspomina (!), a
tylko nieliczne kompilatory obsuguj go. Pki co jest to wic bardzo rzadko uywana
technika i dlatego na razie naley j traktowa jako ciekawostk.
484
Zaawansowane C++
max(12, 56)
max() jest tu szablonem funkcji, ktrego parametr (typ) jest stosowany w charakterze
typu obu parametrw funkcji, jak rwnie zwracanej przez ni warto. Nie podajemy
jednak tego typu dosownie; to wanie wielka zaleta szablonw funkcji, gdy waciwy
typ - parametr szablonu, tutaj int - moe by wydedukowany z jej wywoania. O tym,
jak to si dzieje, mwi nastpny akapit.
Aby jednak zrozumie istot szablonw funkcji, musimy cho z grubsza wiedzie, jak
kompilator traktuje takie wywoania jak powysze. Generalnie nie jest trudne. Jak
wspomniaem wczeniej, szablony w C++ s implementowane w ten sposb, i podczas
kompilacji tworzony jest ich waciwy (nieszablonowy) kod dla kadego typu, dla
ktrego uywamy danego szablonu. Proces ten nazywamy konkretyzacj
(ang. instantiation) a poszczeglne egzemplarze szablonw - specjalizacjami
(ang. specialization albo instance).
Tak wic kompilator musi sobie wytworzy odpowiednie specjalizacje, ktre bd
wykorzystywane w miejscach uycia szablonu. W przykadzie powyej szablon funkcji
max() posuy do wygenerowania jej konkretnej wersji: funkcji max() dla parametru
szablonu rwnego int. Dopiero ta konkretna wersja - specjalizacja - bdzie
skompilowana w normalny sposb, do normalnego kodu maszynowego. W ten sposb
zarwno funkcje, jak te klasy szablonowe zachowuj niemal wszystkie cechy zwykych
funkcji i klas.
To, jak szablon funkcji zostanie skonkretyzowany w danym przypadku, zaley wycznie
od sposobu jego uycia w kodzie. Przyjrzyjmy si wic sposobom na wywoywanie funkcji
szablonowych.
Szablony
485
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++
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.
Zaawansowane C++
488
{ }
{ delete[] m_pnTablica; }
{ return m_uRozmiar; }
};
Szablony
489
Definiujemy szablon
Jak wic zdefiniowa szablon klasy w C++? Patrzc na ogln skadni szablonu mona
by nawet domyli si tego, lecz spjrzmy na poniszy - pusty na razie - przykad:
template <typename TYP> class TArray
{
// ...
};
Jest to szkielet definicji szablonu klasy TArray, czyli tablicy dynamicznej na elementy
dowolnego typu121. Wida tu znane ju czci: przede wszystkim, fraza template
<typename TYP> identyfikuje konstrukcj jako szablon i deklaruje parametry tego
szablonu. Tutaj mamy jeden parametr - bdzie nim rzecz jasna typ elementw tablicy.
Dalej mamy waciwie zwyk definicj klasy i w zasadzie jedyn dobrze widoczn rnic
jest to, e wewntrz niej moemy uy nazwy TYP - parametru szablonu. U nas bdzie
on peni identyczn rol jak int w CIntArray, zatem pena wersja szablonu TArray
bdzie wygldaa nastpujco:
template <typename TYP> class TArray
{
// domylny rozmiar tablicy
static const unsigned DOMYSLNY_ROZMIAR = 5;
private:
// wskanik na waciw tablic oraz jej rozmiar
TYP* m_pTablica;
unsigned m_uRozmiar;
public:
// konstruktory
explicit TArray(unsigned uRozmiar = DOMYSLNY_ROZMIAR)
: m_uRozmiar(uRozmiar),
m_pTablica(new TYP [m_uRozmiar]) { }
TArray(const TArray&);
// destruktor
~TArray()
{ delete[] m_pTablica; }
// ------------------------------------------------------------// pobieranie i ustawianie elementw tablicy
TYP Pobierz(unsigned uIndeks) const
{ if (uIndeks < m_uRozmiar)
return m_pTablica[uIndeks];
else
return TYP();
}
bool Ustaw(unsigned uIndeks, TYP Wartosc)
{ if (uIndeks >= m_uRozmiar) return false;
m_pTablica[uIndeks] = Wartosc;
return true;
}
// inne
unsigned Rozmiar() const
{ return m_uRozmiar; }
// ------------------------------------------------------------121
Zaawansowane C++
490
// operator indeksowania
TYP& operator[](unsigned uIndeks)
{ return m_pTablica[uIndeks]; }
};
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
Sekwencja nazwa_szablonu<typ> peni rol nazwy typu klasy tam, gdzie jest to
konieczne.
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;
Szablony
493
// zwracamy wynik
return fWynik;
Zaawansowane C++
494
if (!(uNowyRozmiar > m_uRozmiar)) return false;
// alokujemy now tablic
TYP* pNowaTablica = new TYP [uNowyRozmiar];
Widzimy wic, e dziedziczenie szablonu klasy nie jest wcale trudne. W jego wyniku
powstaje po prostu nowy szablon klas.
Aliasy typedef
Cech wyrniajc szablony jest to, i operuj one na typach danych w podobny
sposb, jak inny kod na samych danych. Naturalnie, wszystkie te operacje s
przeprowadzane w czasie kompilacji programu, a ich wiksz czci jest konkretyzacja tworzenie specjalizowanych wersji funkcji i klas na podstawie ich szablonw.
Proces ten sprawia jednoczenie, e niektre przewidywalne i, zdawaoby si, znajome
konstrukcje jzykowe nabieraj nowych cech. Naley do nich choby instrukcja typedef;
w oryginale suy ona wycznie do tworzenia alternatywnych nazw dla typw np. tak:
typedef void* PTR;
Nie jest to adna rewolucja w programowaniu, co zreszt podkrelaem, prezentujc t
instrukcj. Ciekawie zaczyna si robi dopiero wtedy, jeli uwiadomimy sobie, e
aliasowanym typem moe by parametr szablonu! Ale skd on pochodzi?
Oczywicie - z szablonu klasy. Jeeli bowiem umiecimy typedef wewntrz definicji
takiego szablonu, to moemy w niej wykorzysta parametryzowany typ. Oto najprostszy
przykad:
template <typename TYP> class TArray
{
public:
// alias na parametr szablonu
typedef TYP ELEMENT;
};
// (reszta niewana)
Szablony
495
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
{ /* ... */ };
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;
// (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>
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
Niespodzianek raczej brak - moe z wyjtkiem ptli uytej do kopiowania zawartoci. Nie
posugujemy si tutaj memcpy() z prostego powodu: chcemy, aby przy przepisywaniu
elementw zadziaay niejawne konwersje. Dokonuj si one oczywicie w linijce:
m_pTablica = aTablica[i];
To wanie ona sprawi, e w razie niedozwolonego przypisywania tablic (np.
TArray<std::string> do TArray<double>) kompilacja nie powiedzie si. Natomiast we
wszystkich innych przypadkach, jeli istniej niejawne konwersje midzy elementami
tablicy, wszystko bdzie w porzdku.
Do penego szczcia naleaoby jeszcze w podobny sposb zdefiniowa konstruktor
konwertujcy (albo kopiujcy - zaley jak na to patrze), bdcy rwnie szablonem
metody. To oczywicie zadanie dla ciebie :)
Tworzenie obiektw
Najbardziej oczywistym sposobem korzystania z szablonu klasy jest tworzenie obiektw
bazujcych na specjalizacji tego szablonu.
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?...
// i teraz jest OK
Moe wyglda to nieadnie, ale pki co naley tak wanie pisa. Zapamitaj wic, e:
W miejsach, gdy w kodzie uywajcym szablonw maj wystpi obok siebie dwa
ostre nawiasy zamykajce (>>), naley wstawi midzy nimi spacj (> >), by nie
pozwoli na ich interpretacj jako operatora przesunicia.
O tym i o podobnych lapsusach jzykowych napomkn wicej w stosownym czasie.
Szablony
499
{}
};
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]; }
};
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.
Zaawansowane C++
502
};
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
Zaawansowane C++
504
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]; }
};
Zaawansowane C++
506
{ }
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);
Wielkosc(CFoo(), sizeof(CFoo));
Pierwiastek(sqrt(2), 2);
DwaRaz(12, 6);
// TPair<CFoo, int>
// TPair<double, int>
// TPair<int, int>
// 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<>
// 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
DwaDo(8, 256);
Tlumaczenie("tablica", "array");
DwieWazneStale(3.14, 2.71);
};
// konstruktor
TPair(const TYP& e1, const TYP& e2) : Pierwszy(e1), Drugi(e2) { }
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
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 ;)
Zaawansowane C++
510
typedef int T;
T max (T a, T b)
Niemniej powyszy sposb moe ci z pocztku pomc, jeli dotd nie rozumiae idei
parametru szablonu bdcego typem.
Stae
Cenn waciwoci szablonw jest moliwo uycia w nich innego rodzaju parametrw
ni tylko typy. S to tak zwane parametry pozatypowe (ang. non-type parameters), a
dokadniej mwic: stae.
125
Szablony
511
// (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];
};
// ...
Zaawansowane C++
512
// hmm...
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;
Zaawansowane C++
514
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
{ /* ... */ };
// 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
{ /* ... */ };
{ /* ... */ };
{ /* ... */ };
Mwi teoretycznie, gdy wiele kompilatorw pozwala na ich uycie. Nie ma bowiem ku
temu adnych technicznych przeciwwskaza (w odrnieniu od pozostaych ogranicze
parametrw pozatypowych). Niemniej, w Standardzie C++ nadal zakorzenione jest to
przestarzae ustalenie. Zapewne jednak tylko kwesti czasu jest jego usunicie.
Szablony parametrw
Ostatnim rodzajem parametrw s tzw. szablony parametrw szablonw
(ang. template templates parameters). Pod t dziwnie brzmic nazw kryje si
moliwo przekazania jako parametru nie konkretnego typu, ale uprzednio
zdefiniowanego szablonu. Poniewa zapewnie nie brzmi to zbyt jasno, najrozsdniej
bdzie doj do sedna sprawy przy pomocy odpowiedniego przykadu.
Idc za potrzeb
A wic Swego czasu stworzylimy sobie szablon oglnej klasy TArray. Okazuje si
jednak, e niekiedy moe by on niewystarczajcy. Chocia dobrze nadaje si do samej
czynnoci przechowywania wartoci, nie pomylelimy o adnych mechanizmach
operowania na tyche wartociach.
Szablony
515
Z drugiej strony, nie ma sensu zmiany dobrze dziaajcego kodu w co, co nie zawsze
bdzie nam przyadtne. Takie czynnoci jak dodawnie, odejmowanie czy mnoenie tablic
maj bowiem sens tylko w przypadku wektorw liczb. Lepiej wic zdefiniowa sobie nowy
szablon do takich celw:
template <typename T> class TNumericArray
{
private:
// wewntrzna tablica
TArray<T> m_aTablica;
public:
// ...
// jakie operatory...
// (np. indeksowania)
TNumericArray operator+(const TNumericArray& aTablica)
{
TNumericArray Wynik(*this);
for (unsigned i = 0; i < Wynik.Rozmiar(); ++i)
Wynik[i] += aTablica[i];
}
return Wynik;
// (itp.)
};
W sumie nic specjalnego nie moemy powiedzie o tym szablonie klasy TNumericArray.
Jak si pewnie domylasz, to si za chwil zmieni :)
// ...
Domylnie byby to nadal szablon TArray, niemniej przy takim szablonie TNumericArray
monaby w miar atwo deklarowa zarwno due, jak i mae tablice:
TNumericArray<int>
TNumericArray<float, TOptimizedArray<float> >
TNumericArray<double, TSuperFastArray<double> >
aMalaTablica(50);
aDuzaTablica(1000);
aGigaTablica(250000);
Zaawansowane C++
516
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);
aDuzaTablica(1000);
aGigaTablica(250000);
{ /* ... */ };
{ /* ... */ };
// ...
Szablony
517
aDuzaTablica(1000);
aGigaTablica(250000);
Problemy z szablonami
Szablony s rzeczywicie jednym z najwikszych osigni jzyka C++. Jednak, jak to
jest z wikszoci zaawansowanych technik, ich stosowanie moe za soba pociga
pewne problemy. Nie, nie chodzi mi tu wcale o to, e szablony s trudne do nauczenia,
cho pewnie masz takie nieodparte wraenie ;) Chciabym raczej porozmawia o kilku
puapkach czyhajcych na programist (szczeglnie pocztkujcego), ktry zechce
uywa szablonw. Dziki temu by moe atwiej unikniesz mniej lub bardziej powanych
problemw z tymi konstrukcjami jzykowymi.
Zobaczmy wic, co moe stan nam na drodze
126
Nie podajemy nazwy parametru szablonu TAB, bo nie ma takiej potrzeby. Nazwa ta nie jest nam po prostu
do niczego potrzebna.
Zaawansowane C++
518
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;
Dla czytajcego (i piszcego) kod czowieka jest cakiem wyrane widoczne, e drugim
parametrem szablonu TStaticArray jest tu 16>>2 (czyli 64). Kompilator uczulony na
problem nawiasw ostrych zinterpretuje aczkolwiek ponisz linijk jako:
TStaticArray<int, 16> >2>
aInty;
// ojej!
W sumie wic nie bardzo wiadomo, co jest lepsze. Waciwie jednak wyraenia podobne
do powyszego s raczej rzadkie i prawd mwic nie powinny by w ogle stosowane.
Gdyby zachodzia taka konieczno, najlepiej posuy si pomocniczymi nawiasami
okrgymi:
TStaticArray<int, (16>>2)> aInty;
// OK
Wniosek z tego jest jeden: kiedy chodzi o nawiasy ostre i szablony, lepiej by
wyrozumiaym dla kompilatora i w odpowiednich miejscach pomc mu w zrozumieniu, o
co nam tak naprawd chodzi.
Szablony
519
Przyczyn jest po czci sposb, w jaki kompilatory C++ dokonuj analizy kodu.
Dokadne omwienie tego procesu jest skomplikowane i niepotrzebne, wic je sobie
darujemy. Interesujc nas czynnoci jest aczkolwiek jeden z pierwszych etapw
przetwarzania - tak zwana tokenizacja (ang. tokenization).
Tokenizacja polega na tym, i kompilator, analizujc kod znak po znaku, wyrnia w nim
elementy leksykalne jzyka - tokeny. Do tokenw nale gwnie identyfikatory (nazwy
zmiennych, funkcji, typw, itp.) oraz operatory. Kompilator wpierw dokonuje ich analizy
(parsowania) i tworzy list takich tokenw.
Sk polega na tym, e C++ jest jzykiem kontekstowym (ang. context-sensitive
language). Oznacza to, e identyczne sekwencje znakw mog w nim znaczy zupenie
co innego w zalenoci od kontekstu. Przykadowo, fraza a*b moe by zarwno
mnoeniem zmiennej a przez zmienn b, jak te deklaracj wskanika na typ a o nazwie
b. Wszystko zaley od znaczenia nazw a i b.
W przypadku operatorw mamy natomiast jeszcze jedn zasad, zwan zasad
maksymalnego dopasowania (ang. maximum match rule). Mwi ona, e naley
zawsze prbowa uj jak najwicej znakw w jeden token.
Te dwie cechy C++ (kontekstowo i maksymalne dopasowanie) daj w efekcie
zaprezentowane wczeniej problemy z nawiasami ostrymi. Problem jest bowiem w tym, i
zalenie od kontekstu i ssiedztwa znaki < i > mog by interpretowane jako:
operatory wikszoci i mniejszoci
operatory przesunicia bitowego
nawiasy ostre
Nie ma to wikszego znaczenia, jeli nie wystpuj one w bliskim ssiedztwie. W
przeciwnym razie zaczynaj si powane kopoty - jak choby tutaj:
TSomething<32>>4 > FOO>
CosTam;
// no i?...
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.
// 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
522
Zaawansowane C++
Wiem, e wyglda to jak skryowanie trolla z goblinem, ale mwimy teraz o naprawd
specyficznym szczegliku, ktrego uycie jest bardzo rzadkie. Powyszy kod wyglaby
pewnie przejrzyciej, gdyby usun z niego wyrazy typename i template:
// UWAGA: ten kod NIE JEST poprawny!
// wywoanie jako statycznej metody bez obiektu
TFoo<T>::TBar<T>::Baz();
// utworzenie lokalnego obiektu i wywoanie metody
TFoo<T>::TBar<T> Bar;
Bar.Baz<T>();
// utworzenie dynamicznego obiektu i wywoanie metody
TFoo<T>::TBar<T>* pBar;
pBar = new TFoo<T>::TBar<T>;
pBar->Baz<T>();
delete pBar;
Tym samym jednak pozbawiamy kompilator informacji potrzebnych do skompilowania
szablonu. Rol typename znamy, wic zajmijmy si dodatkowymi uyciami template.
Ot tutaj template (a waciwie ::template, .template i ->template) suy do
poinformowania, e nastpujca dalej nazwa zalena jest szablonem. Patrzc na
definicj TFoo wiemy to oczywicie, jednak kompilator nie dowie si tego a do chwili
konkretyzacji. Dla niego nazwy TBar i Baz mog by rwnie dobrze skadowymi
statycznymi, za nastpujce dalej znaki < i > - operatorami relacji. Musimy wic
wyprowadzi go bdu.
Stosuj kontrukcje ::template, .template i ->template zamiast samych operatorw
::, . i -> w tych miejscach, gdzie podana dalej nazwa zalena jest szablonem.
Stosowalno tych konstrukcji jest wic ograniczona i zawa si do przypadkw
zagniedonych szablonw. W codziennej i nawet troch bardziej niecodziennej praktyce
programistycznej mona si bez nich obej, aczkolwiek warto o nich wiedzie, by mc je
zastosowa w tych nielicznych sytuacjach ujawniajcej si niewiedzy kompilatora.
Model wczania
Najwczeniejszym i do dzi najpopularniejszym sposobem zarzdzania szablonami jest
model wczania (ang. inclusion model). Jest on jednoczenie cakiem prosty w
stosowaniu i czsto wystarczajcy. Przyjrzyjmy mu si.
Szablony
523
Zwyky kod
Wpierw jednak przypomnimy sobie, jak naley radzi sobie z kodem C++ bez szablonw.
Ot, jak wiemy, wyrniamy w nim pliki nagwkowe oraz moduy kodu. I tak:
pliki nagwkowe s opatrzone rozszerzeniami .h, .hh, .hpp, lub .hxx i zawieraj
deklaracje wspuytkowanych czci kodu. Nale do nich:
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?
Zaawansowane C++
524
std::cin >> fLiczba1;
std::cin >> fLiczba2;
Wynika z niego klarownie, e funkcja max() w wersji skonkretyzowanej dla double nie
istnieje! Jak to wyjani?
Wytumaczenie jest w miar proste. Zwr uwag, e doczenie pliku max.hpp wcza
do main.cpp jedynie deklaracj szablonu, a nie jego definicj. Nie majc definicji
kompilator nie moe natomiast skonkretyzowa szablonu dla parametru double. Wobec
tego czyni on zaoenie, e funkcja max<double>() zostaa wygenerowana gdzie indziej.
Nie ma w tym nic zdronego - ten sam mechanizm dziaa przecie dla zwykych funkcji,
ktre s deklarowane (prototypowane) w pliku nagwkowym, a implementowane w
innym module. Niestety, w tym przypadku jest to zaoenie bdne: konkretyzacja nie
zostanie bowiem przeprowadzona z powodu wspomnianego braku informacji (definicji
szablonu).
Ostatecznie wic powstaje zewntrzne dowizanie do specjalizacji szablonu max() dla
parametru double - specjalizacji, ktra nie istnieje! Ten fakt nie umknie ju uwadze
linkera, czego skutkiem jest zaprezentowany wyej bd i poraka konsolidacji.
Szablony
525
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.
Zaawansowane C++
526
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
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?
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) << " | " <<
Szablony
531
Wynik
Ostatecznie moemy uzna remis obu rozwiza, aczkolwiek z lekkim wskazaniem na
makrodefinicje. Z wyjtkiem fanatykw wydajnoci nie ma jednak bodaj nikogo, kto
uwaaby nieefektywne dziaanie szablonw za wielki bd. A tym, ktrzy rzeczywicie
tak uwaaj, pozostaje chyba tylko przerzucenie si na jzyk asemblera :)
532
Zaawansowane C++
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
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
};
// 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
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
Zaawansowane C++
536
}
// operator nierwnoci
template <typename T1, typename
inline bool operator!=(const
const
{
return (Para1.Pierwszy
|| Para1.Drugi
}
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
Szablony
539
// konstruktor
TStack()
: m_uRozmiar(0)
{ }
m_aStos[m_uRozmiar] = Element;
++m_uRozmiar;
// dodanie elementu
// zwiksz. licznika
};
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
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
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
_
__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
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
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
2. Kopiowanie dosowne
Licencjobiorca moe kopiowa i rozprowadza Dokument komercyjnie lub niekomercyjnie,
w dowolnej postaci, pod warunkiem zamieszczenia na kadej kopii Dokumentu treci
Licencji, informacji o prawie autorskim oraz noty mwicej, e do Dokumentu ma
zastosowanie niniejsza Licencja, a take pod warunkiem nie umieszczania adnych
dodatkowych ogranicze, ktre nie wynikaj z Licencji. Licencjobiorca nie ma prawa
uywa adnych technicznych metod pomiarowych utrudniajcych lub kontrolujcych
czytanie lub dalsze kopiowanie utworzonych i rozpowszechnianych przez siebie kopii.
Moe jednak pobiera opaty za udostpnianie kopii. W przypadku dystrybucji duej
liczby kopii Licencjobiorca jest zobowizany przestrzega warunkw wymienionych
w punkcie 3.
Licencjobiorca moe take wypoycza kopie na warunkach opisanych powyej, a take
wystawia je publicznie.
553
3. Kopiowanie ilociowe
Jeeli Licencjobiorca publikuje drukowane kopie Dokumentu w liczbie wikszej ni 100,
a licencja Dokumentu wymaga umieszczenia Treci okadki, naley doczy kopie
okadek, ktre zawieraj ca wyran i czyteln Tre okadki: tre przedniej okadki,
na przedniej okadce, a tre tylnej okadki, na tylnej okadce. Obie okadki musz te
jasno i czytelnie informowa o Licencjobiorcy jako wydawcy tych kopii. Okadka przednia
musi przedstawia peny tytu; wszystkie sowa musz by rwnie dobrze widoczne
i czytelne. Licencjobiorca moe na okadkach umieszcza take inne informacje
dodatkowe. Kopiowanie ze zmianami ograniczonymi do okadek, dopki nie narusza
tytuu Dokumentu i spenia opisane warunki, moe by traktowane pod innymi wzgldami
jako kopiowanie dosowne.
Jeeli napisy wymagane na ktrej z okadek s zbyt obszerne, by mogy pozosta
czytelne po ich umieszczeniu, Licencjobiorca powinien umieci ich pocztek(tak ilo,
jaka wydaje si rozsdna) na rzeczywistej okadce, a pozosta cz na ssiednich
stronach.
W przypadku publikowania lub rozpowszechniania Niejawnych kopii Dokumentu w liczbie
wikszej ni 100, Licencjobiorca zobowizany jest albo doczy do kadej z nich Jawn
kopi czyteln dla komputera, albo wymieni w lub przy kadej kopii Niejawnej publicznie
dostpn w sieci komputerowej lokalizacj penej kopii Jawnej Dokumentu, bez adnych
informacji dodanych -- lokalizacj, do ktrej kady uytkownik sieci miaby bezpatny
anonimowy dostp za pomoc standardowych publicznych protokow sieciowych.
W przypadku drugim Licencjobiorca musi podj odpowiednie rodki ostronoci, by
wymieniona kopia Jawna pozostaa dostpna we wskazanej lokalizacji przynajmniej przez
rok od momentu rozpowszechnienia ostatniej kopii Niejawnej (bezporedniego lub przez
agentw albo sprzedawcw) danego wydania.
Zaleca si, cho nie wymaga, aby przed rozpoczciem rozpowszechniania duej liczby
kopii Dokumentu, Licencjobiorca skontaktowa si z jego autorami celem uzyskania
uaktualnionej wersji Dokumentu.
4. Modyfikacje
Licencjobiorca moe kopiowa i rozpowszechnia Zmodyfikowan wersj Dokumentu na
zasadach wymienionych powyej w punkcie 2 i 3 pod warunkiem cisego przestrzegania
niniejszej Licencji. Zmodyfikowana wersja peni wtedy rol Dokumentu, a wic Licencja
dotyczca modyfikacji i rozpowszechniania Zmodyfikowanej wersji przenoszona jest na
kadego, kto posiada jej kopi. Ponadto Licencjobiorca musi w stosunku do
Zmodyfikowanej wersji speni nastpujce wymogi:
A. Uy na Stronie tytuowej (i na okadkach, o ile istniej) tytuu innego ni tytu
Dokumentu i innego ni tytuy poprzednich wersji (ktre, o ile istniay, powinny
zosta wymienione w Dokumencie, w sekcji Historia). Tytuu jednej z ostatnich
wersji Licencjobiorca moe uy, jeeli jej wydawca wyrazi na to zgod.
B. Wymieni na Stronie tytuowej, jako autorw, jedn lub kilka osb albo jednostek
odpowiedzialnych za autorstwo modyfikacji Zmodyfikowanej wersji, a take
przynajmniej piciu spord pierwotnych autorw Dokumentu (wszystkich, jeli
byo ich mniej ni piciu).
C. Umieci na Stronie tytuowej nazw wydawcy Zmodyfikowanej wersji.
D. Zachowa wszelkie noty o prawach autorskich zawarte w Dokumencie.
E. Doda odpowiedni not o prawach autorskich dotyczcych modyfikacji obok
innych not o prawach autorskich.
554
Inne
555
5. czenie dokumentw
Licencjobiorca moe czy Dokument z innymi dokumentami wydanymi na warunkach
niniejszej Licencji, na warunkach podanych dla wersji zmodyfikowanych w czci 4
powyej, jednak tylko wtedy, gdy w poczeniu zostan zawarte wszystkie Sekcje
niezmienne wszystkich oryginalnych dokumentw w postaci niezmodyfikowanej i gdy
bd one wymienione jako Sekcje niezmienne poczenia w jego nocie licencyjnej.
Poczenie wymaga tylko jednej kopii niniejszej Licencji, a kilka identycznych Sekcji
niezmiennych moe zosta zastpionych jedn. Jeeli istnieje kilka Sekcji niezmiennych
o tym samym tytule, ale rnej zawartoci, Licencjobiorca jest zobowizany uczyni tytu
kadej z nich unikalnym poprzez dodanie na jego kocu, w nawiasach, nazwy
oryginalnego autora lub wydawcy danej sekcji, o ile jest znany, lub unikalnego numeru.
Podobne poprawki wymagane s w tytuach sekcji na licie Sekcji niezmiennych w nocie
licencyjnej poczenia.
W poczeniu Licencjobiorca musi zawrze wszystkie sekcje zatytuowane "Historia"
z dokumentw oryginalnych, tworzc jedn sekcj "Historia". Podobnie ma postpi
z sekcjami "Podzikowania" i "Dedykacje". Wszystkie sekcje zatytuowane "Adnotacje"
naley usun.
6. Zbiory dokumentw
Licencjobiorca moe utworzy zbir skadajcy si z Dokumentu i innych dokumentw
wydanych zgodnie z niniejsz Licencj i zastpi poszczeglne kopie Licencji pochodzce
z tych dokumentw jedn kopi doczon do zbioru, pod warunkiem zachowania zasad
Licencji dotyczcych kopii dosownych we wszelkich innych aspektach kadego
z dokumentw.
Z takiego zbioru Licencjobiorca moe wyodrbni pojedynczy dokument i
rozpowszechnia go niezalenie na zasadach niniejszej Licencji, pod warunkiem
zamieszczenia w wyodrbnionym dokumencie kopii niniejszej Licencji oraz zachowania
zasad Licencji we wszystkich aspektach dotyczcych dosownej kopii tego dokumentu.
556
Inne
8. Tumaczenia
Tumaczenie jest uznawane za rodzaj modyfikacji, a wic Licencjobiorca moe
rozpowszechnia tumaczenia Dokumentu na zasadach wymienionych w punkcie 4.
Zastpienie Sekcji niezmiennych ich tumaczeniem wymaga specjalnej zgody wacicieli
prawa autorskiego. Dopuszcza si jednak zamieszczanie tumacze wybranych lub
wszystkich Sekcji niezmiennych obok ich wersji oryginalnych. Podanie tumaczenia
niniejszej Licencji moliwe jest pod warunkiem zamieszczenia take jej oryginalnej wersji
angielskiej. W przypadku niezgodnoci pomidzy zamieszczonym tumaczeniem
a oryginaln wersj angielsk niniejszej Licencji moc prawn ma oryginalna wersja
angielska.
9. Wyganicie
Poza przypadkami jednoznacznie dopuszczonymi na warunkach niniejszej Licencji nie
zezwala si Licencjobiorcy na kopiowanie, modyfikowanie, czy rozpowszechnianie
Dokumentu ani te na cedowanie praw licencyjnych. We wszystkich pozostaych
wypadkach kada prba kopiowania, modyfikowania lub rozpowszechniania Dokumentu
albo cedowania praw licencyjnych jest niewana i powoduje automatyczne wyganicie
praw, ktre licencjobiorca naby z tytuu Licencji. Niemniej jednak w odniesieniu do stron,
ktre ju otrzymay od Licencjobiorcy kopie albo prawa w ramach niniejszej Licencji,
licencje nie zostan anulowane, dopki strony te w peni si do nich stosuj.
557
Jeli nie zamieszczasz Sekcji Niezmiennych, napisz "nie zawiera Sekcji Niezmiennych"
zamiast spisu sekcji niezmiennych. Jeli nie umieszczasz Teksu na Przedniej Okadce
wpisz "bez Tekstu na Okadce" w miejsce "wraz z Tekstem na Przedniej Okadce LISTA",
analogicznie postp z "Tekstem na Tylnej Okadce"
Jeli w twoim dokumencie zawarte s nieszablonowe przykady kodu programu, zalecamy
aby take uwolni te przykady wybierajc licencj wolnego oprogramowania, tak jak
Powszechna Licencja Publiczna GNU, w celu zapewnienia moliwoci ich uycia w wolnym
oprogramowaniu.