You are on page 1of 647

Ksika omawia zagadnienia zwizane z programowaniem, wyjania, dlaczego s one problemami, oraz opisuje ujcie stosowane w jzyku C++

przy ich rozwizywaniu. Podrcznik poprowadzi ci krok po kroku od rozumienia C do etapu, w ktrym zbir poj jzyka C++ stanie si twoim jzykiem ojczystym. Celem kadego rozdziau jest przyblienie pojedynczego pojcia lub niewielkiej grupy powizanych ze sob poj w sposb nie wymagajcy korzystania z adnych dodatkowych terminw. Wprowadzenie do jzyka bdzie odbywao si stopniowo, odzwierciedlajc sposb, w jaki zazwyczaj przyswajasz sobie nowe informacje.

Tytu oryginau: Thinking in C++ Tumaczenie: Piotr Imiela

Authorized translation from the English language edition entitled THINKING IN C++: INTRODUCTION TO STANDARD C++, VOLUME ONE, 2nd edition by ECKEL, BRUCE, published by Pearson Education, Inc, publishing as Prentice Hall, Copyright 2000 All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Polish language edition published by Wydawnictwo Helion. Copyright 2002 ISBN: 83-7197-709-3 Wydawnictwo HELION ul. Chopina 6, 44-100 GLIWICE tel. (prefiks-32) 231-22-19, (prefiks-32) 230-98-63 e-mail: helion@helion.pl WWW: http://helion.pl (ksigarnia internetowa, katalog ksiek) Drogi Czytelniku! Jeeli chcesz oceni t ksik, zajrzyj pod adres http://helion.pl/user/opinie?thicpp Moesz tam wpisa swoje uwagi, spostrzeenia, recenzj.

Wszystkie znaki wystpujce w tekcie s zastrzeonymi znakami firmowymi bd towarowymi ich wacicieli. Autor oraz Wydawnictwo HELION dooyli wszelkich stara, by zawarte w tej ksice informacje byy kompletne i rzetelne. Nie bior jednak adnej odpowiedzialnoci ani za ich wykorzystanie, ani za zwizane z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz Wydawnictwo HELION nie ponosz rwnie adnej odpowiedzialnoci za ewentualne szkody wynike z wykorzystania informacji zawartych w ksice. Wszelkie prawa zastrzeone. Nieautoryzowane rozpowszechnianie caoci lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metod kserograficzn, fotograficzn, a take kopiowanie ksiki na noniku filmowym, magnetycznym lub innym powoduje naruszenie praw autorskich niniejszej publikacji. Printed in Poland.

Moim rodzicom, siostrze i bratu

Podzikowania
Dzikuj przede wszystkim tym, ktrzy za porednictwem Internetu przestali mi poprawki i sugestie. Bylicie ogromnie pomocni w podniesieniujakoci ksiki nie zdoabym zrobi tego bez waszej pomocy. Szczeglne podzikowania nale si Johnowi Cookowi. Pomysy oraz wiedza zawarte w ksice pochodzz wielu rde: od przyjaci, takichjak Chuck Allison, Andrea Provaglio, Dan Saks, Scott Meyers, Charles Petzold i Michael Wilk, pionierw jezyka,jak Bjarne Stroustrup, Andrew Koenig i Rob Murray, czonkw komitetu standaryzacyjnego C++ Standards Committee, takich jak Nathan Myers (ktrego uwagi byy szczegktie pomocne i yczhwe), Biil Ptauger, Reg Charney, Tom Peneilo, Tom Plum, Sam Druker i Uwe Steinmueller, od osb, ktre zabierafy gos w ramach prowadzonej przeze mnie grupy tematycznej C++ na Software Development Conference, a take od uczestnikw moich seminariw, zadajcych pytania, ktre musiaemusysze, byuczyni materiabardziej przejrzystym. Ogromne podzikowania nale si Genowi Kiyooka, ktrego firma Digigami uyczya mi swojego serwera internetowego. Jzyka C++ nauczaem wraz z przyjacielem, Richardem Hale Shawem. Wskazwki i pomoc Richarda okazay si bardzo pomocne (podobnie jak Kim). Dzikuj rwnie KoAnn Vikoren, Ericowi Faurotowi, JenniferJessup, Tarze Arrowood, Marcowi Pardiemu, Nicole Freeman, Barbarze Hanscome, ReginieRidley, Alexowi Dunne'owi, atakepozostaej obsadziei zaodzeMFI. Szczeghiie gorco chciaem podzikowa wszystkim moim nauczycielom i studentom (ktrzy rwnie stali si moimi nauczycielami). Chciaem rwnie wyrazi gbokie uznanie i sympati za ich wysiki moim ulubionym pisarzom: Johnowi Irvingowi, Nealowi Stephensonowi, Robertsonowi Daviesowi (bdzie nam Ciebie brakowa), Tomowi Robbinsowi, Williamowi Gibsonowi, Richardowi Bachowi, Carlosowi Castanedzie oraz Gene'owi Wolfe. Dzikuj Guido van Rossumowi za wymylenie Pythona i bezinteresowne oddanie go wiatu. Swoim przyczynkiem wzbogacie moje ycie. Dzikuj pracownikom Pretince Hali: Alanowi Apcie, Anie Terry, Scottowi Disanno, Toniemu Holmowi oraz mojej elektronicznej redaktorce Stephanie Enghsh. W dziale marketingu natomiast sowa wdzicznoci nalesi Bryanowi Gambrelowi i Jennie Burger. Sonda Donovan pomoga w produkcji pyty CD-ROM. Daniel Wiil-Harris (oczywicie) przygotowa projekt sitodruku, ktry znalaz si na pycie. Wszystkim wspaniaym ludziom w Crested Butte dzikuj za uczynienie go magicznym miejscem, a szczeglnie Alowi Smithowi (twrcy cudownego Camp4 Coffee Garden), moim ssiadom Dave'owi i Erice, Marshy z ksigarni Heg's Place, Patowi i Johnowi z TeocaUi Tamale, Samowi z Bakery Cafe oraz Tiller zajego pomoc w pracach nad dwikiem. A take wszystkim znakomitociom, ktre przesiadyway w Camp4, czynic moje poranki interesujcymi. Na licie moich przyjaci (nie jest ona jeszcze zamknita) znaleli si: Zack Urlocker, Andrew Binstock, Neil Rubenking, Jiraig Brockschmidt, Steve Sinofsky, JD Hildebrandt, Brian McElhinney, Brinkley Barr, Larry O'Brien, Bill Gates z Midnigh1 Engineering Magazine, Larry Constantine, Lucy Lockwood, Tom Keffer, Dan Putterman, Gene Wang, Dave Mayer, David Intersimone. Claire Sawyers, Wosi (Andrea Provaglio, Rossella Gioia, Laura Fallai, Marco i Leila Cantu, Corrado, Iisa i Christina Giustozzi), Chris i Laura Strandowie (oraz Parker), A1mquistowie, Brad Jerbic, Marilyn Cvitanic, Mabry'owie, Haflingerowie, Pollockowie, Peter Vinci, Robbinsowie, Moelterowie, Dave Stoner, Laurie Adams, Cranstonowie, Larry Fogg, Mike i Karen Sequeira, Gary Entsminger i Allison Brody, Kevin, Sonda, i Ella Donovanowie, Chester i Shannon Andersenowie, Joe Lordi, Dave i Brenda Bartlettowie, Rentschlerowie, Lynn i Todd, oraz ich rodziny. No i, oczywicie, Mama i Tata.

Spis treci
Co nowego w drugim wydaniu? Zawarto drugiego tomu ksiki Skd wzi drugi tom ksiki? Wymagania wstpne Nauka jzyka C++ Cele Zawarto rozdziaw wiczenia Rozwizania wicze Kod rdowy Standardy jzyka Obsuga jzyka Bdy Okadka Rozdzia 1. Wprowadzenie do obiektw Postp abstrakcji Obiekt posiada interfejs Ukryta implementacja Wykorzystywanie istniejcej implementacji Dziedziczenie wykorzystywanie istniejcego interfejsu Relacje typu jest" i jest podobny do" Zastpowanie obiektw przy uyciu polimorfizmu Tworzenie i niszczenie obiektw Obsuga wyjtkw sposb traktowania bdw Analiza i projektowanie Etap 0. Przygotuj plan Etap 1. Co tworzymy? Etap 2. Jak to zrobimy? Etap 3. Budujemy jdro Etap 4. Iteracje przez przypadki uycia Etap 5. Ewolucja Planowanie si opaca Programowanie ekstremalne Najpierw napisz testy Programowanie w parach Dlaczego C++ odnosi sukcesy? Lepsze C Zacze si ju uczy

Wstp

13
13 14 14 14 15 16 17 21 21 21 22 23 23 24

25 26 27 30 31 32 35 36 40 41 42 44 45 49 52 53 53 55 55 56 57 58 59 59

Thinking in C++. Edycja polska Efektywno Systemy s atwiejsze do opisania i do zrozumienia Maksymalne wykorzystanie bibliotek Wielokrotne wykorzystywanie kodu dziki szablonom Obsuga bdw Programowanie na wielk skal Strategie przejcia Wskazwki Problemy z zarzdzaniem Podsumowanie Rozdzia 2. Tworzenie! uywanie obiektw Proces tumaczenia jzyka Interpretery Kompilatory Proces kompilacji Narzdzia do rozcznej kompilacji Deklaracje i definicje czenie Uywanie bibliotek Twj pierwszy program w C++ Uywanie klasy strumieni wejcia-wyjcia Przestrzenie nazw Podstawy struktury programu Witaj, wiecie!" Uruchamianie kompilatora Wicej o strumieniach wejcia-wyjcia czenie tablic znakowych Odczytywanie wejcia Wywoywanie innych programw Wprowadzenie do acuchw Odczytywanie i zapisywanie plikw Wprowadzenie do wektorw Podsumowanie wiczenia Rozdzia 3. 60 60 60 61 61 61 62 62 64 66 67 68 68 68 69 71 71 76 76 78 78 79 80 81 82 82 83 84 84 85 86 88 92 93

Jzyk C w C++ 95 Tworzenie funkcji 95 Wartoci zwracane przez funkcje 97 Uywanie bibliotek funkcji jzyka C 98 Tworzenie wasnych bibliotekza pomoc programu zarzdzajcego bibliotekami....99 Sterowanie wykonywaniem programu 99 Prawda i fasz 99 if-else 100 while 101 do-while 101 for 102 Sowa kluczowe break i continue 103 switch 104 Uywanie i naduywanie instrukcji goto 105 Rekurencja 106 Wprowadzenie do operatorw 107 Priorytety 107 Automatyczna inkrementacja i dekrementacja 108

Spis treci Wprowadzenie do typw danych Podstawowe typy wbudowane bool, true i false Specyfikatory Wprowadzenie do wskanikw Modyfikacja obiektw zewntrznych Wprowadzenie do referencji Wskaniki i referencje jako modyfikatory Zasig Definiowanie zmiennych wlocie" Specyfikacja przydziau pamici Zmienne globalne Zmienne lokalne static extern Stae yolatile Operatory i ich uywanie Przypisanie Operatory matematyczne Operatory relacji Operatory logiczne Operatory bitowe Operatory przesuni Operatory jednoargumentowe Operator trj argumentowy Operator przecinkowy Najczstsze puapkizwizane z uywaniem operatorw Operatory rzutowania Jawne rzutowanie w C++ sizeof samotny operator Sowo kluczowe asm Operatory dosowne Tworzenie typw zoonych Nadawanie typom nowych nazw za pomoc typedef czenie zmiennych w struktury Zwikszanie przejrzystoci programwza pomoc wylicze Oszczdzanie pamici za pomoc unii Tablice Wskazwki dotyczce uruchamiania programw Znaczniki uruchomieniowe Przeksztacanie zmiennych i wyrae w acuchy Makroinstrukcja assert() jzyka C Adresy funkcji Definicja wskanika do funkcji Skomplikowane deklaracje i definicje Wykorzystywanie wskanikw do funkcji Tablice wskanikw do funkcji Mk zarzdzanie rozczn kompilacj Dziaanie programu mk Pliki makefile uywane w ksice Przykadowy plik makefile Podsumowanie wiczenia 108 109 110 111 112 115 117 118 120 120 122 122 124 124 126 127 129 129 130 130 131 131 132 133 135 136 137 137 138 139 143 143 144 144 144 145 148 150 151 159 160 162 162 163 163 164 165 166 167 168 171 171 173 173

Thinking in C++. Edycja polska

Rozdzia 4. Abstrakcja danych

Miniaturowa biblioteka w stylu C Dynamiczny przydzia pamici Bdne zaoenia Na czym polega problem? Podstawowy obiekt Czym s obiekty? Tworzenieabstrakcyjnych typw danych Szczegy dotyczce obiektw Zasady uywania plikw nagwkowych Znaczenie plikw nagwkowych Problem wielokrotnych deklaracji Dyrektywy preprocesora #defme, #ifdef i #endif Standard plikw nagwkowych Przestrzenie nazw w plikach nagwkowych Wykorzystywanie plikw nagwkowych w projektach Zagniedone struktury Zasig globalny Podsumowanie wiczenia Okrelanie ogranicze Kontrola dostpu w C++ Specyfikator protected Przyjaciele Zagniedeni przyjaciele Czy jest to czyste"? Struktura pamici obiektw Klasy Modyfikacja programu Stash, wykorzystujca kontrol dostpu Modyfikacja stosu, wykorzystujca kontrol dostpu Klasy-uchwyty Ukrywanie implementacji Ograniczanie powtrnych kompilacji Podsumowanie wiczenia Konstruktor gwarantuje inicjalizacj Destruktor gwarantuje sprztanie Eliminacja bloku definicji Ptle for Przydzielanie pamici Klasa Stash z konstruktorami i destruktorami Klasa Stack z konstruktorami i destruktorami Inicjalizacja agregatowa Konstruktory domylne Podsumowanie wiczenia Dalsze uzupenienia nazw Przecianie na podstawie zwracanych wartoci czenie bezpieczne dla typw

179
180 183 186 188 188 194 195 196 197 198 199 200 201 202 202 202 206 206 207

Rozdzia 5. Ukrywanie implementacji

211 212 214 214 216 218 219 219 222 223 223 224 224 226 227

211

Rozdzia 6.

Inicjalizacjai kocowe porzdki

229

230 232 233 235 236 237 240 242 245 246 246

Rozdzia 7. Przecianie nazw funkcji i argumenty domylne

249

250 251 252

Spis treci Przykadowe przecienie Unie Argumenty domylne Argumenty-wypeniacze Przecianiekontra argumenty domylne Podsumowanie wiczenia 253 255 258 259 260 264 265

Rozdzia 8. Stae

Podstawianie wartoci Stae w plikach nagwkowych Bezpieczestwo staych Agregaty Rnice w stosunku do jzyka C Wskaniki Wskaniki do staych Stae wskaniki Przypisanie a kontrola typw Argumenty funkcji i zwracane wartoci Przekazywanie staej przez warto Zwracanie staej przez warto Przekazywanie i zwracanie adresw Klasy Stae w klasach Stae o wartociach okrelonych podczas kompilacji, zawarte w klasach Stae obiekty i funkcje skadowe yolatile Podsumowanie wiczenia Puapki preprocesora Makroinstrukcje a dostp Funkcje inline Funkcje inline wewntrz klas Funkcje udostpniajce Klasy Stash i Stack z funkcjami inline Funkcje inline a kompilator Ograniczenia Odwoania do przodu Dziaania ukryte w konstruktorach i destruktorach Walka z baaganem Dodatkowe cechy preprocesora Sklejanie symboli Udoskonalona kontrola bdw Podsumowanie wiczenia Statyczne elementy jzyka C Zmienne statyczne znajdujce si wewntrz funkcji Sterowanie czeniem Inne specyfikatory klas pamici Przestrzenie nazw Tworzenie przestrzeni nazw Uywanie przestrzeni nazw Wykorzystywanie przestrzeni nazw

267

267 268 269 270 271 272 272 273 274 275 275 276 279 282 282 285 287 292 293 294

Rozdzia 9. Funkcje inline

297
298 300 301 302 303 308 311 312 313 313 314 315 316 316 319 320

Rozdzia 10. Zarzdzanie nazwami

323

323 324 328 330 330 330 332 336

10

Thinking in C++. Edycja polska Statyczne skadowe w C++ Definiowanie pamicidla statycznych danych skadowych Klasy zagniedone i klasy lokalne Statyczne funkcje skadowe Zalenoci przy inicjalizacjiobiektw statycznych Jak mona temu zaradzi? Specyfikacja zmiany sposobu czenia Podsumowanie wiczenia 337 337 341 342 344 346 352 353 353 359 359 360 361 363 363 364 369 374 376 378 380 382 383 387 387 388 389 390 393 402 405 412 413 414 415 416 425 425 427 429 430 432 432 437 438 439 440 441 442 442

Rozdzia 1 1 . Referencje! konstruktor kopiujcy Wskaniki w C++ Referencje w C++ Wykorzystanie referencji w funkcjach Wskazwki dotyczce przekazywania argumentw Konstruktor kopiujcy Przekazywanie i zwracanie przez warto Konstrukcja za pomoc konstruktora kopiujcego Domylny konstruktor kopiujcy Moliwoci zastpienia konstruktora kopiujcego Wskaniki do skadowych Funkcje Podsumowanie wiczenia Rozdzia 12. Przecianie operatorw Ostrzeenie i wyjanienie Skadnia Operatory, ktre mona przecia Operatory j ednoargumentowe Operatory dwuargumentowe Argumenty i zwracane wartoci Nietypowe operatory Operatory, ktrych nie mona przecia Operatory niebdce skadowymi Podstawowe wskazwki Przecianie operacji przypisania Zachowanie si operatora = Automatyczna konwersja typw Konwersja za pomoc konstruktora Operator konwersji Przykad konwersji typw Puapki automatycznej konwersji typw Podsumowanie wiczenia Rozdzia 13. Dynamiczne tworzenie obiektw Tworzenie obiektw Obsuga sterty w jzyku C Operator new Operator delete Prosty przykad Narzut menedera pamici

Spis treci Zmiany w prezentowanych wczeniej przykadach Usuwanie wskanika void* jest prawdopodobnie bdem Odpowiedzialno za sprztanie wskanikw Klasa Stash przechowujca wskaniki Operatory new i delete dla tablic Upodabnianie wskanika do tablicy Brak pamici Przecianie operatorw new i delete Przecianie globalnych operatorw new i delete Przecianie operatorw new i delete w obrbie klasy Przecianie operatorw new i deletew stosunku do tablic Wywoania konstruktora Operatory umieszczania new i delete Podsumowanie wiczenia

11 443 443 445 445 450 451 451 452 453 455 458 460 461 463 463

Rozdzia 14. Dziedziczenie! kompozycja

Skadnia kompozycji Skadnia dziedziczenia Lista inicjatorw konstruktora Inicjalizacja obiektw skadowych Typy wbudowane znajdujce si na licie inicjatorw czenie kompozycji i dziedziczenia Kolejno wywoywaniakonstruktorw i destruktorw Ukrywanie nazw Funkcje, ktre nie s automatycznie dziedziczone Dziedziczenie a statyczne funkcje skadowe Wybr midzy kompozycj a dziedziczeniem Tworzenie podtypw Dziedziczenie prywatne Specyfikator protected Dziedziczenie chronione Przecianie operatorw a dziedziczenie Wielokrotne dziedziczenie Programowanie przyrostowe Rzutowanie w gr Dlaczego rzutowanie w gr"? Rzutowanie w gr a konstruktor kopiujcy Kompozycja czy dziedziczenie (po raz drugi) Rzutowanie w gr wskanikw i referencji Kryzys Podsumowanie wiczenia Ewolucja programistw jzykaC++ Rzutowanie w gr Problem Wizanie wywoania funkcji Funkcje wirtualne Rozszerzalno W jaki sposb jzyk C++ realizuje pne wizanie? Przechowywanie informacji o typie Obraz funkcji wirtualnych

467

468 469 471 471 472 473 474 476 480 483 484 485 487 488 489 490 491 492 492 494 494 497 498 498 498 499

Rozdzia 15. Polimorfizmi funkcje wirtualne

503

504 504 506 506 506 508 510 511 512

Thinking in C++. Edycja polska Rzut oka pod mask Instalacja wskanika wirtualnego Obiekty s inne Dlaczego funkcje wirtualne? Abstrakcyjne klasy podstawowe i funkcje czysto wirtualne Czysto wirtualne definicje Dziedziczenie i tablica YTABLE Okrajanie obiektw Przecianie i zasanianie Zmiana typu zwracanej wartoci Funkcje wirtualne a konstruktory Kolejno wywoywania konstruktorw Wywoywanie funkcji wirtualnychwewntrz konstruktorw Destruktory i wirtualne destruktory Czysto wirtualne destruktory Wirtualne wywoania w destruktorach Tworzenie hierarchii bazujcej na obiekcie Przecianie operatorw Rzutowanie w d Podsumowanie wiczenia 514 515 516 517 518 522 523 525 527 529 530 531 532 533 535 537 538 541 543 546 546

Rozdzia 16. Wprowadzenie do szablonw

Kontenery Potrzeba istnienia kontenerw Podstawy szablonw Rozwizanie z wykorzystaniem szablonw Skadnia szablonw Definicje funkcji niebdcych funkcjami inline Klasa IntStack jako szablon Stae w szablonach Klasy Stack i Stash jako szablony Kontener wskanikw Stash,wykorzystujcy szablony Przydzielanie i odbieranieprawa wasnoci Przechowywanie obiektwjako wartoci Wprowadzenie do iteratorw Klasa Stack z iteratorami Klasa PStash z iteratorami Dlaczego iteratory? Szablony funkcji Podsumowanie wiczenia

551
551 553 554 556 558 559 560 562 563 565 570 573 575 582 585 590 593 594 594

Dodatek A Styl kodowania Dodatek B Wskazwki dla programistw Dodatek C Zalecana literatura
Jzyk C OglnieojzykuC++ Ksiki, ktre napisaem Gbia i mroczne zauki Analiza i projektowanie

599 609 621


621 621 622 623 623

Skorowidz

627

Wstp
Jzyk C++, podobnie jak inne jzyki uywane przez ludzi, umoliwia wyraanie poj. Jeeli robi to skutecznie, jako rodek wyrazu bdzie znacznie atwiejszy w uyciu i bardziej elastyczny ni inne dostpne rodki w miar jak problemy stan si coraz wiksze i bardziej zoone. Nie mona postrzega C++ jedynie jako zbioru waciwoci pewne cechy nie maj sensu w oderwaniu od pozostaych. Mona uywa sumy poszczeglnych elementw z myl o projekcie, a nie o zwyczajnym kodowaniu. Aby tak pojmowa C++, trzeba rozumie problemy za pomoc jzyka C oraz samego programowania. Ksika omawia zagadnienia zwizane z programowaniem, wyjania, dlaczego s one problemami, oraz opisuje ujcie stosowane w jzyku C++ przy ich rozwizywaniu. A zatem zbir cech, ktry opisuj w kadym rozdziale, bdzie wynika ze sposobu postrzegania rozwizania okrelonego rodzaju problemw za pomoc jzyka. Mam nadziej poprowadzi ci krok po kroku od rozumienia C do etapu, w ktrym zbir poj jzyka C++ stanie si twoim jzykiem ojczystym. Nadal bd przyjmowa zaoenie, e chcesz zbudowa w swoim umyle model, ktry umoliwi ci zrozumienie jzyka a do samych jego podstaw jeeli napotkasz problem, bdziesz mg wykorzysta swj model, uzyskujc rozwizanie. Sprbuj przekaza ci wiedz, ktra umoliwia mi mylenie w C++".

Co nowego w drugim wydaniu?


Ksika powstaa w wyniku gruntownej modyfikacji jej pierwszego wydania. Modyfikacja ta miaa na celu odzwierciedlenie wszystkich zmian, wprowadzonych do jzyka C++ w w y n i k u zakoczenia prac nad standardem C++, a take wynikaa z tego, czego nauczyem si od czasu pierwszej edycji. Cay tekst pierwszego wydania zosta przejrzany i napisany ponownie, co wizao si czsto ze zmian przedstawionych przykadw, dopisaniem nowych, a take dodaniem wielu nowych wicze. Istotna zmiana ukadu i porzdku materiau miaa na celu odzwierciedlenie dostpnoci lepszych narzdzi oraz bya rezultatem mojej pogbionej wiedzy na temat tego, w jaki

14

Thinking In C++. Edycja polska sposb ludzie ucz si C++. Dodaem rozdzia, bdcy krtkim wprowadzeniem do poj jzyka C i najistotniejszych cech C++, umoliwiajcy zrozumienie pozostaej czci ksiki czytelnikom nieznajcym podstaw jzyka C. A zatem krtka odpowied na pytanie co nowego w drugim wydaniu?" brzmi: to, co nie jest w nim zupenie nowe, zostao gruntownie przeredagowane, czasami w stopniu uniemoliwiajcym rozpoznanie pierwotnych przykadw i towarzyszcego im materiau.

Zawarto drugiego tomu ksiki


Zakoczenie prac nad standardem C++ spowodowao dodanie do standardowej biblioteki C++ pewnej liczby nowych i wanych bibliotek, takich jak acuchy, kontenery i algorytmy, a take wprowadzenie zoonych konstrukcji zwizanych z szablo1 nami. Te i inne trudniejsze tematy zostay przeniesione do drugiego tomu ksiki , zawierajcego takie zagadnienia, jak: wielokrotne dziedziczenie, obsuga wyjtkw, wzorce projektowe, a take przykady dotyczce budowy i uruchamiania stabilnych systemw.

Skd wzi drugi tom ksiki?


Podobnie jak ksika, ktr trzymasz w rce, jej drugi tom zatytuowany Thinking in C++, Volume 2 mona w caoci pobra z internetowej witryny, znajdujcej si pod adresem http://helion.pl/online/thinking/index.html. W witrynie tej mona rwnie znale informacje dotyczce przewidywanego terminu druku drugiego tomu. Witryna zawiera rwnie kod rdowy programw zawartych w obu ksikach, cznie z poprawkami oraz informacjami dotyczcymi kursw na CD-ROM-ach, oferowanych przez MindView, Inc., otwartych seminariw, szkole stacjonarnych, konsultacji, doradztwa oraz prezentacji.

Wymagania wstpne
W pierwszym wydaniu ksiki przyjem zaoenie, e znasz jzyk C przynajmniej na poziomie umoliwiajcym czytanie napisanych w tym jzyku programw. Moim podstawowym celem byo uproszczenie tego, co uwaaem za trudne: jzyka C++. W tym wydaniu dodaem rozdzia stanowicy krtkie wprowadzenie do C, lecz nadal zakadam, e masz pewne dowiadczenie w programowaniu. Czytajc powie uczysz si wielu nowych sw w sposb intuicyjny, odwoujc si do kontekstu, w ktrym wystpuj; podobnie moesz uzyska wiele informacji dotyczcych C.

"! loatksitfci jwt vbmie it*9*y wyqcnie w angielskiej wersji jzykowej pnyp. /fam.

Wstp

li

Nauka jzyka C++


Rozpoczem swoj drog prze? C++ w tym samym miejscu, w ktrym, jak sdz znajduje si wielu czytelnikw ksiki jako programista z bardzo powanym ugruntowanym podejciem do programowania Co gorsza, moje przygotowanie i do wiadczenie pochodziy z programowania systemw wbudowanych na poziomie sprztowym, w ktrym C by czsto postrzegany jako jzyk wysokiego poziomu, sta nowicy nieefektywny i rozrzutny sposb upychania bitw Pniej odkryem, ze nie byem nawet bardzo dobrym programist C, ukrywajc moj ignorancj dotyczc struktur, funkcji malloc() i free(), setjmp() i longjmp(), a take innych wyrafinowanych" poj Unikaem dyskusji na te tematy zamiast skorzysta z okazji pozyskania nowej wiedzy Kiedy rozpoczynaem swoj przygod z jzykiem C++, jedyn przyzwoit ksik na ten temat by samozwanczy przewodnik eksperta" Bjarne Stroustrupa2, a zatem pozostao mi uproszczenie podstawowych poj na wasn rk Rezultatem bya moja pierwsza ksika o C++3, w ktrej przede wszystkim przelaem na papier wasne dowiadczenia Bya ona pomylana jako poradnik, umoliwiajcy programistom rwnoczesn nauk C i C++ Oba wydania ksiki4 spotkay si z entuzjastycznym przyjciem Mniej wicej w tym samym czasie, gdy ukazao si Using C++, rozpoczem nauczanie tego jzyka w ramach seminariw i prezentacji Nauczanie C++ (a pniej je zyka Java) stao si moim zawodem Poczynajc od 1989 roku widywaem potakujce gowy, twarze bez wyrazu i zagadkowe miny suchaczy na caym wiecie Kiedy rozpoczem prowadzenie szkole przeznaczonych dla mniejszych grup, cos odkryem w trakcie tych zaj Nawet ci spord uczestnikw, ktrzy umiechali si i potakiwali, byli czsto zdezorientowani Przez wiele lat zajmujc si tematyk C++ i Javy w ramach Software Development Confeience doszedem do wniosku, ze zarwno ja sam, jak i inni prelegenci prbujemy przekaza typowemu odbiorcy nadmiern ilo informacji w zbyt krtkim czasie W rezultacie z powodu zrnicowanego poziomu suchaczy i sposobu, w jakj prezentowaem materia, koczyem prelekcje, me docierajc do czci audytorium By moe chciaem osign zbyt wiele, ale poniewa jestem przeciwnikiem tradycyjnego sposobu prowadzenia wykadw (dla wikszoci suchaczy, jak sdz, sprzeciw taki wynika ze znudzenia), dyem do utrzymania jednakowego tempa dla wszystkich Przez pewien czas tworzyem wiele rnych prezentacji w krtkich odstpach czasu W taki sposb skoczyem z nauk prowadzon metod eksperymentowania i nawrotw (ta technika sprawdza si rwnie przy projektowaniu programw w C++) Ostatecznie opracowaem kurs, wykorzystujc wszystko, czego nauczyem si w wyniku moich dowiadcze zwizanych z nauczaniem Kady problem jest przedstawiony osobno, w formie atwych do zrozumienia krokw, za po kadej prezentacji nastpuj wiczenia praktyczne (idealna metoda nauki) Wicej informacji na temat Bjarne Stroustrup The C++ Programmmg Language Addison Wesley 1986 (pierwsze wydanie)
4

Vsmg C++, Osborne/McGraw Hill, 1989 Usmg C++ and C++ Inside & Out, Osborne/McGraw-Hill, 1993

Thinking in C++. Edycja polska prowadzonych przeze mnie otwartych seminariw znale mona na stronie www. BruceEckel.com s tam rwnie informacje na temat kursw, ktre zapisaem na CD-ROM-ach. Materia zawarty w pierwszym wydaniu ksiki, opracowany w cigu ponad dwch lat, zosta przetestowany na wiele sposobw, na licznych rozmaitych kursach. Opinie, ktre zebraem w trakcie kadego kursu, pomogy mi dokona poprawek w materiale, tak aby lepiej suy celom nauczania. Nie jest to jednak zapis treci kursu na niniejszych stronach umieciem tyle informacji, ile zdoaem, i uporzdkowaem je w taki sposb, by umoliwi przejcie do nastpnego tematu. Ksikajest adresowana przede wszystkim do czytelnika samotnie zgbiajcego tajniki nowego jzyka programowania.

Cele
W trakcie pisania niniejszej ksiki przywiecay mi nastpujce cele: 1. Prezentacja materiau metodmaych krokw", dziki ktrym czytelnik moe atwo zrozumie kade pojcie, zanim przejdzie do kolejnych zagadnie. 2. Posugiwanie si przykadami moliwiejak najprostszymi i najkrtszymi. Uniemoliwia to czsto rozpatrywanie rzeczywistych przykadw, ale zauwayem, e pocztkujcy s na og bardziej zadowoleni, kiedy potrafi zrozumie kady szczeg przykadu ni gdy s pod wraeniem zakresu problemu, ktry w przykad ilustruje. Obowizuje rwnie ograniczenie co do wielkoci kodu, ktry moe by przyswojony w czasie lekcji. Z tego powodu otrzymuj czasami uwagi krytyczne dotyczce uywania przeze mnie dziecinnych przykadw", alejestem skonny to zaakceptowa w imi skutecznoci metody nauczania. 3. Staranny dobr kolejnoci prezentacji poszczeglnych waciwoci, by czytelnik nie napotka czego, czego wczeniej nie pozna. Oczywicie, nie zawsze jest to moliwe w takich przypadkach zamieszczone zostanie krtkie wprowadzenie. 4. Przekazywanie tego, co uwaam za istotne dla zrozumieniajzyka, nie za wszystkiego, co wiem najego temat. Wierz w hierarchi wanoci informacji" oraz w to, e istniej wiadomoci, ktrych 95 procent programistw nigdy nie bdzie potrzebowao; wprowadzaj onejedynie zamieszanie i wywouj wraenie zoonoci jzyka. Na przykad wjzyku C zapamitanie tabeli priorytetw (nigdy nie udao mi si tego dokona) pozwala na przemylniejsze zapisanie kodu. Jeeli jednak zastanowisz si nad tym, dojdziesz do wniosku, e sprawi to kopot komu, kto bdzie ten kod czyta lub zajmowa sijego pielgnacj. Zapomnij wic o priorytetach i uzywaj w przypadku niejasnoci nawiasw. Ta sama uwaga dotyczy niektrych informacji ojzyku C++, ktre jak sdz s waniejsze dla twrcw kompilatorw ni dla programistw.

Wstgp

17

5. Przedstawienie poszczeglnych partii ksiki w sposb na tyle zwizy, by zachowa zarwno rozsdny czas lektury, jak i odpowiednie przerwy pomidzy wiczeniami. Pozwala to na aktywny i twrczy udzia uczestnikw w zajciach praktycznych, a take daje czytelnikowi poczucie lepszego zrozumienia materiau. 6. Dostarczenie czytelnikom solidnych podstaw, umoliwiajcych im zrozumienie zagadnie w stopniu pozwalajcym na przejcie do trudniejszych kursw oraz ksiek (w szczeglnoci, do drugiego tomu ksiki). 7. Unikaem uywania wersjijzyka C++ zwizanej zjakim konkretnym producentem, poniewa uwaam, e w przypadku naukijzyka szczegy dotyczce implementacji nie stak istotne,jak samjzyk. Zawarto wikszoci dokumentacji, dostarczanych przez producentw i opisujcych szczegy dokonanych przez nich implementacji, jest w zupenoci zadowalajca.

Zawarto rozdziaw
C++ jest jzykiem, ktrego nowe i rnorodne waciwoci zostay zbudowane na fundamencie istniejcej ju skadni (z uwagi na tojest on nazywany hybrydowymjzykiem obiektowym). W miarjak kolejne osoby uczestnicz w procesie uczenia si, zaczynamy rozumie drog przebyt przez programistw osigajcych kolejne etapy poznawania cechjzyka C++. Poniewa wydaje si ona naturalnym kierunkJem umysu uksztatowanego proceduralnie, postanowiem to zjawisko zrozumie, a nastpnie pody t sam drog. Pragnem przyspieszy ten proces za pomoc formuowania pyta nasuwajcych si w trakcie nauki, udzielania na nie odpowiedzi, a take odpowiadania na pytania suchaczy, ktrych sam uczyemjzyka. Niniejszy kurs zosta przygotowany z myl o uatwieniu nauki C++. Uwagi suchaczy pozwoliy mi zrozumie, ktre partie materiau sprawiaj trudnoci i wymagaj dodatkowego nawietlenia. W trakcie prezentacji materiau przekonaem si, e w miejscach, w ktrych ambitnie zawarem opis zbyt wielu cechjednoczenie, wprowadzenie duej liczby nowych poj wymaga ich wyjanienia, co prowadzi do szybkiego wzrostu poziomu frustracji suchaczy. W rezultacie zadaem sobie trud jednoczesnego wprowadzania moliwie jak najmniejszej liczby poj w idealnym przypadku tylkojednego istotnego pojcia w kadym z rozdziaw. A zatem celem kadego rozdziau jest przyblienie pojedynczego pojcia Iub niewielkiej grupy powizanych ze sob poj w sposb niewymagajcy korzystania z adnych dodatkowych terminw. Dziki temu moliwe jest zrozumienie kadej partii materiau w kontekcie dotychczas posiadanej wiedzy, jeszcze przed przystpieniem do dalszej lektury. Aby to osign, pozostawiem pewne elementy jzyka C duej ni zamierzaem. Wynikajc z tego korzyci jest to, e nie bdziesz poirytowany z powodu stosowania wszystkich cech jzyka C++, zanim zostan one wyjanione. Dziki temu wprowadzenie do jzyka bdzie odbywao si stopniowo, odzwierciedlajc sposb, wjaki zazwyczaj przyswajasz sobie nowe informacje.

L8

Thinking in C++. Edycja polska Poniej zamieszczono krtki opis rozdziaw zawartych w ksice: Rozdzia 1.: Wprowadzenie do obiektw. Kiedy projekty staj si zbyt wielkie i zoone, by mona byo nimi w atwy sposb zarzdza, dochodzi do ,J<ryzysu oprogramowania". Programici oznajmiaj wwczas: nie jestemy w slanie dokoczy realizowanych projektw, ajeeli nawet potrafimy, to s one zbyt kosztowne!". Wywouje to lawin reakcji, omawianych w tym rozdziale. Zostay w nim rwnie opisane idee programowania obiektowego (ang. OOP object oriented programming) oraz wskazwki dotyczce sposobw przezwycienia kryzysu oprogramowania. W tym rozdziale zapoznasz si take z podstawowymi pojciami cech programowania obiektowego, a take ze wstpem do procesw analizy i projektowania. Dowiesz si ponadto o korzyciach i obawach zwizanych z wykorzystaniem jzyka oraz o przesankach przemawiajcych za przejciem do wiata C++. Rozdzia 2.: Tworzenie i uywanie obiektw. W rozdziale zosta omwiony proces budowy programw z wykorzystaniem kompilatorw i bibliotek. Zaprezentowano pierwszy program w jzyku C++ i opisano sposb tworzenia i kompilacji programw. Nastpnie przedstawione s niektre z podstawowych bibliotek obiektw, dostpnych w standardzie C++. Zapoznawszy si z trecitego rozdziau, bedzieszju2 dobrze zorientowany w tym, czym jest pisanie programw w jzyku C++, wykorzystujce standardowe biblioteki obiektw. Rozdzia 3.: Jzyk C w C++. Ten rozdzia stanowi zwizy przegld tych elementw jzyka C, ktre s uywane w C++, oraz podstawowych cech jzyka dostpnych wycznie w C++. Prezentuje rwnie program make", uywany powszechnie przy projektowaniu oprogramowania, a take wykorzystywany we wszystkich przykadach zawartych w ksice (kody rdowe przykadw w ksice, dostpne pod adresem ftp://ftp.helion.pl/przyklady/thicpp.zip, zawieraj pliki programu make dla kadego zrozdziaw). Rozdzia 3. zakada, e masz solidne podstawy w dziedzinie programowania w jzykach proceduralnych, takich jak Pascal, C lub nawet niektre odmiany Basica (o ile napisae dostatecznie duo kodu w tymjzyku szczeglnie funkcji). Rozdzia 4.: Abstrakcja danych. Wikszo wasnoci jzyka C++ dotyczy moliwoci tworzenia nowych typw danych. Nie tylko zapewnia to doskona organizacj kodu, ale stanowi rwnie podstaw bardziej zaawansowanych moliwoci programowania obiektowego. Przekonasz si, w jakj sposb jest realizowana ta idea, poprzez umieszczanie funkcji wewntrz struktur, szczegy pokazujce, jak to zrobi; dowiesz si take, jaki rodzaj kodu wwczas powstaje. Nauczysz si rwnie najlepszego sposobu organizowania kodu w pliki nagwkowe oraz pliki zawierajce implementacj. Rozdzia 5.: Ukrywanie implementacji. Uywajc sowa kluczowego private, mona sprawi, by niektre dane i funkcje zawarte w strukturze byy niedostpne dla uytkownika nowo utworzonego typu. Oznacza to, e jest moliwe oddzielenie wewntrznej implementacji od interfejsu widzianego przez programist wykorzystujcego kod, co pozwala na atw zmian tej implementacji, bez koniecznoci modyfikacji programu klienta. Zostao wprowadzone sowo kluczowe class, bdce eleganckim sposobem opisu nowego typu danych. Ponadto rozszyfrowano znaczenie sowa obiekt" Qest to szczeglny rodzaj zmiennej).

Wstp

19 Rozdzia 6.: Inicjalizacja i sprztanie. Jedn z najczstszych przyczyn bdw w C s niezainicjowane zmienne. Konstruktor w C++ gwarantuje, e zmienne utworzonego przez ciebie typu danych (obiekty twojej klasy") zostan zawsze prawidowo zainicjowane. Jeeli obiekty te wymagaj rwnie, aby po nich posprzta", mona uy w jzyku C++ destruktora, zapewniajcego, e sprztanie to zostanie zawsze wykonane. Rozdzia 7.: Przecianie nazw funkcji i argumenty domylne. Jzyk C++ ma za zadanie wspomaganie realizacji duych, zoonych projektw. W czasie ich tworzenia moe si zdarzy doczenie wielu bibliotek, uywajcych tych samych nazw funkcji. By moe zechcesz rwnie uywa tej samej nazwy, nadajc jej rne znaczenia w obrbie pojedynczej biblioteki. Jzyk C++ uatwia to za pomocprzeciania nazw funkcji, pozwalajcego na uywanie tych samych nazw funkcji, pod warunkiem, e rni si one midzy sob listami argumentw. Domylne argumenty pozwalaj natomiast na wywoywanie tej samej funkcji na rne sposoby, automatyczne dostarczajc domylne wartoci niektrych argumentw. RozdziaJ8.: Stae. Rozdzia opisuje sowa kluczowe const oraz volatile, posiadajce w C++ dodatkowe znaczenie szczeglnie gdy s uywane w obrbie klas. Dowiesz si, co oznacza zastosowanie sowa kluczowego const w definicji wskanika. W rozdziale pokazano rwnie, jak zmienia si znaczenie sowa kluczowego const w zalenoci od tego, czy jest ono uywane wewntrz, czy na zewntrz klas, a take w j a k i sposb utworzy wewntrz klas stae o wartociach okrelonych w czasie kompilacji. Rozdzia 9.: Funkcje inline. Makroinstrukcje preprocesora eliminuj narzut zwizany z wywoaniem funkcji, ale pozbawiaj rwnie zyskw wynikajcych z kontroli typw w C++. Funkcje inline cz wszystkie korzyci makroinstrukcji preprocesora z korzyciami wynikajcymi z rzeczywistego wywoania funkcji. W rozdziale opisano szczegowo implementacj oraz sposb uywania funkcji inline. Rozdzia 10.: Zarzdzanie nazwami. Tworzenie nazw naley do podstawowych czynnoci podczas programowania w miar rozrastania si projektu liczba uywanych nazw moe nadmiernie wzrosn. C++ pozwala na zachowanie doskonaej kontroli nad nazwami pod wzgldem ich tworzenia, widocznoci, umieszczania w pamici i czenia. Rozdzia prezentuje kontrol nazw w C++ z wykorzystaniem dwch technik. W pierwszej z nich do kontroli widocznoci i czenia jest uywane sowo kluczowe static. Opisano takejego specjalne znaczenie zwizane z klasami. Znacznie bardziej uyteczn technik kontroli nazw i ich globalnego zasigu jest przestrze nazw C++, pozwalajca na rozbicie globalnej przestrzeni nazw na rozczne regiony. Rozdzia 11.: Referencje i konstruktor kopiujcy. Wskaniki w C++ dziaaj tak, jak wskaniki w C, z dodatkow korzyci wynikajc z silniejszej kontroli typw w C++. Jzyk C++ dostarcza kolejnej metody adresowania, zapoyczonej z Algolu i Pascala referencji, umoliwiajcej obsug adresw przez kompilator przy zachowaniu normalnego sposobu zapisu programu. W rozdziale tym zetkniesz si rwnie z konstruktorem kopiujcym, zarzdzajcym sposobem, w jaki obiekty s przekazywane przez warto do i z funkcji. Na koniec wyjaniono zagadnienie uywania wskanikw do skadowych klas.

Thinking in C++. Edycja polska

Rozdzia 12.: Przecianie operatorw. Waciwo ta jest czasem nazywana cukierkiem skadniowym" pozwala osodzi" skadni uywania wasnych typw poprzez moliwo stosowania zarwno operatorw, jak i wywoa funkcji. W rozdziale tym dowiesz si, e przecienie operatora jesl innym rodzajem wywoania funkcji. Zapoznasz si z tworzeniem wasnych operatorw i radzeniem sobie z mylcym czasami sposobem uywania argumentw i zwracanymi typami. Nauczysz si take podejmowania decyzji, czy operator powinien by skadow klasy, czy te funkcj zaprzyjanion. Rozdzia 13.: Dynamiczne tworzenie obiektw. Iloma samolotami bdzie zarzdza system kontroli lotw? Ilu figur geometrycznych potrzebowa bdzie system CAD? W przypadku oglnego problemu programistycznego nie jest znana liczba, czas istnienia oraz typ obiektw wymaganych przez dziaajcy program. W tym rozdziale dowiesz si, w jaki sposb operatory new i delete elegancko rozwizuj ten problem w jzyku C++, w bezpieczny sposb tworzc obiekty na stercie (ang. heap]. Zobaczysz rwnie, jak operatory new i delete mog by przeciane na rne sposoby, umoliwiajce kontrol nad sposobem przydzielania i zwalniania pamici. Rozdzia 14.: Dziedziczenie i kompozycja. Abstrakcja danych umoliwia tworzenie od podstaw nowych typw, jednake kompozycja i dziedziczenie pozwalaj na tworzenie nowych typw na podstawie typwju istniejcych. W przypadku kompozycji nowy typ jest tworzony z innych typw, jak z klockw, podczas gdy dziedziczenie pozwala na utworzenie bardziej wyspecjalizowanej wersji istniejcego typu. W rozdziale tym poznasz skadni, sposb redefiniowania funkcji, a take znaczenie konstrukcji i destrukcji dla dziedziczenia i kompozycji. Rozdzia 15.: Polimorfizm i funkcje wirtualne. Moesz straci dziewi miesicy, by odkry na wasn rk i zrozumie znaczenie tego kamienia wgielnego programowania obiektowego. Na podstawie niewielkich, prostych przykadw zobaczysz, wjaki sposb mona tworzy rodzin typw, uywajc dziedziczenia i manipulujc obiektami nalecymi do tej rodziny za pomoc ich wsplnej klasy podstawowej. Sowo kluczowe virtual pozwala na traktowanie wszystkich obiektw nalecych do tej rodziny w sposb oglny, co oznacza, e wikszo kodu nie jest zalena od informacji dotyczcej jakiego konkretnego typu. Umoliwia to rozbudow programw, czynic ich tworzenie oraz pielgnacj kodu atwiejszymi i taszymi. Rozdzia 16.: Wprowadzenie do szablonw. Dziedziczenie i kompozycja umoliwiaj wielokrotne wykorzystywanie kodu obiektu, ale nie zaspokajaj wszystkich potrzeb zwizanych z wielokrotnym uyciem kodu. Szablony umoliwiaj powtrne wykorzystanie kodu rdtowego, pozwalajc kompilatorowi na zastpienie nazw typw wystpujcych w ciele klasy lub funkcji. Umoliwia to uywanie bibliotek klas kontenerowych, stanowicych wane narzdzie w szybkim i niezawodnym projektowaniu programw obiektowych (standardowa biblioteka C++ zawiera wan bibliotek klas kontenerowych). Rozdzia dostarcza gruntownych informacji dotyczcych tego istotnego tematu. Dodatkowe tematy (i trudniejsze przykady) zawarto w drugim tomie ksiki, ktry monapobra z witryny internetowej http:/flielion.pUonline/thinking/index.html.

Wstp

___;

21

wiczenia
Odkryem, e wiczenia s wyjtkowo uyteczne w czasie seminariw, pogbiajc rozumienie materiau przez ich uczestnikw, dlatego te znajduj si one na kocu kadego rozdziau. Liczba wicze zostaa znacznie zwikszona w stosunku do pierwszego wydania ksiki. W wikszoci wiczenia sdostatecznie atwe, by mogy by wykonane w rozsdnym czasie w sali wykadowej lub laboratorium, pod kontrol osoby prowadzcej zajcia, dziki czemu moe ona upewni si, e wszyscy suchacze przyswoili sobie materia. Niektre wiczenia s nieco trudniejsze, by przyku uwag rwnie zaawansowanych uczestnikw zaj. Wiele wicze przygotowano w taki sposb, by mona je byo szybko rozwiza su one raczej sprawdzeniu i ugruntowaniu wiedzy ni prezentacji istotnych problemw (te ostatnie zapewne znajdziesz samodzielnie albo, co bardziej prawdopodobne, to one dopadn ciebie).

Rozwizania wicze
Rozwizania wybranych wicze zamieszczono w dokumencie elektronicznym The Thinking in C++ Annotated Solution Guide, ktry mona pobra za niewielk opat w witrynie www.BruceEckel.com.

Kod rdowy
Kod rdowy programw zawartych w ksice jest oprogramowaniem bezpatnie dostpnym, chronionym prawem autorskim. Monaje pobra pod 'ddrescmftp://ftp.helion,pl/ przyklady/thicpp.zip*. Prawo autorskie uniemoliwia przedruk kodu w rodkach masowego przekazu bez uzyskania na to zgody, ale daje prawo uywania go na wiele innych sposobw (zob. informacje poniej). Kodjest dostpny w postaci spakowanego archiwum, moliwego do rozpakowania na dowolnej platformie sprztowo-programowej, na ktrej dostpny jest program uytkowy zip" (dla wikszoci platform jest on dostpny poszukaj odpowiedniej wersji w Internecie, o ile nie jest ona ju zainstalowana w twoim systemie). W katalogu, do ktrego rozpakujesz pliki, znajdziesz nastpujc informacj o prawach autorskich:
/ / : ! :Copyright.txt Copyright (c) 2000, Bruce Eckel Kod rdowy pochodzcy z ksiki "Thinking in C++" Wszelkie prawa zastrzeone, l WYJTKIEM dozwolonych poniej: Niniejszy plik moe by bezpatnie wykorzystywany do wasnych celw (osobistych lub komercyjnych), w c z a j c w to modyfikacj
5

e wzgldu na przejrzysto tumaczenia, komentarze w kodach rdowych widoczne w ksice Posiadaj polskie znaki diakrytyczne, natomiast kody pobrane z serwera FTP nie posiadaj tych znakw ~przyp. red.

Thinking in C++. Edycja polska oraz dystrybucj wycznie w postaci wykonywalnej. Udzielane jest pozwolenie na uywanie tego pliku podczas zaj dydaktycznych, wczajc w to wykorzystanie go w materiaach prezentacyjnych, pod warunkiem wskazania ksiki "Thinking in C++" jako rda jego pochodzenia. Poza zastosowaniami dydaktycznymi nie mona kopiowa ani rozpowszechnia niniejszego kodu. Wycznym miejscem dystrybucji jest witryna http://www.BruceEckel .com (i jej oficjalne witryny lustrzane), w ktrej jest on dostpny bezpatnie. Nie mona usuwa zastrzeenia prawa autorskiego ani niniejszej informacji. Nie wolno rozpowszechnia zmodyfikowanej wersji kodu rdowego, zawartego w niniejszym pakiecie. Nie wolno publikowa niniejszego pliku drukiem, bez wyranej zgody autora. Bruce Eckel nie bierze odpowiedzialnoci za to, e niniejsze oprogramowanie bdzie przydatne do jakichkolwiek celw. Oprogramowanie jest dostarczane "takie, jakie jest", bez bezporedniej ani poredniej gwarancji jakiegokolwiek rodzaju, wczajc w to gwarancj przydatnoci handlowej, przydatnoci do konkretnego zastosowania oraz nie naruszania prawa. Cakowite ryzyko zwizane z jakoci i wydajnoci oprogramowania ponosi jego uytkownik. Bruce Eckel oraz wydawca nie ponosz odpowiedzialnoci za jakiekolwiek zniszczenia wywoane u uytkownika ani u jakiejkolwiek strony trzeciej w wyniku uywania lub dystrybucji niniejszego oprogramowania. W adnym wypadku Bruce Eckel ani wydawca nie ponosz odpowiedzialnoci za jakiekolwiek utracone przychody, zyski lub dane, a take bezporednie, porednie, szczeglne, wynikowe, przypadkowe, lub wynikajce z naruszenia prawa uszkodzenia, wywoane w jakikolwiek sposb, niezalenie od teorii odpowiedzialnoci wynikajcej z uywania lub niemonoci uywania oprogramowania, nawet jeeli Bruce Eckel oraz wydawca zostali powiadomieni o moliwoci wystpienia takich uszkodze. W przypadku ujawnienia bdw zawartych w oprogramowaniu naley wzi pod uwag poniesienie kosztw niezbdnego serwisu, jego naprawy lub korekty. Kady, kto uwaa, e znalaz bd. proszony jest o jego zgoszenie za pomoc formularza dostpnego na stronie www.BruceEckel.com (tego samego formularza naley uy do zgoszenia bdw niezwizanych z kodem). Moesz uywa kodu we wasnych projektach oraz w dydaktyce, dopki pozostaje w nim informacja o zastrzeeniu prawa autorskiego.

Standardyjzyka
Kiedy odwouj si do zgodnoci ze standardem ISO jzyka C, w caej ksice na og pisz C". Tylko w przypadku koniecznoci rozrnienia pomidzy standardowym C i starsz (sprzed ustanowienia standardu) wersjjzyka, wyranie to zaznaczam. W czasie pisania ksiki Komitet Standaryzacyjny C++ zakoczy prac nad jzykiem. A zatem uywam okrelenia standardowe C++ w celu odwoania si do ustanowionego standardu jzyka. Jeeli za odwouj si do C++, czytelnik powinien zaoy, e mam na myli zwyke C++".

Wstp

23

Istnieje pewna niejednoznaczno zwizana z obecn nazw komitetu standaryzacyjnego C++ oraz sam nazw standardu. Steve Clamage, przewodniczcy komitetu, wyjani to nastpujco: Istniej dwa komitety standaryzacyjne C++: Komitet J16 NClTS (poprzednio X3) oraz komitet ISO JTCl/SC22AVGJ4. ANSIpowoa NClTSpo to, by utworzy komitet techniczny, majcy na celu zaprojektowanie amerykaskich standardw narodowych. J16 zostapowoany w 1989 r. w celu utworzenia amerykaskiego standardu C++. Mniej wicej w 1991 r. w celu utworzenia standardu midzynarodowego zostalpowolany WG14. ProjektJ16zostalprzeksztalcony wprojekt typu I" (midzynarodowy) i podporzdkowany dziaaniom standaryzacyjnym lSO. Oba komitety spotkafy si w tym samym czasie i w tym samym miejscu; gos J16 stanowil amerykaski gtos za W14. WG14 przekazal prace techniczne projektowi JJ6. Nastpnie WGJ4 przegosowal prace techniczne przeprowadzone przez JJ6. Standard C++ zostatpierwotnie utworzonyjako standardISO. PniejANSI przeglosowai (zgodnie z rekomendacj J16) przyjcie standardu ISO C+ + jako amerykaskiego standardu C++, Tak wic ,,ISO"jest waciwym sposobem odwoywania si do standardu C++.

Obsuga jzyka
Uywany przez ciebie kompilator moe nie obsugiwa wszystkich waciwoci jzyka, omawianych w niniejszej ksice, zwaszcza gdy nie posiadasz jego najnowszej wersji. Implementacja takiegojzyka, jak C++ jest herkulesowym zadaniem; moesz si zatem spodziewa, e ujawni si cz tych waciwoci ni wszystkie naraz. Jeeli jednak sprbujesz wykona jeden z zawartych w ksice przykadw i otrzymasz mnstwo bdw zgoszonych przez kompilator, niekoniecznie oznacza to bd zawarty w kodzie lub w kompilatorze. Cecha ta moe by po prostu jeszcze niezaimplementowana w uywanym przez ciebie kompilatorze.

Bdy
Niezalenie od tego, jakich sztuczek uyje autor w celu znalezienia bdw, niektre z nich s nieuniknione i czsto okazuj si potem oczywiste dla kogo czytajcego ksik po raz pierwszy. Jeeli odkryjesz co, co uwaasz za bd, skorzystaj, prosz, z formularza dostpnego na stronie www.BruceEckel.com. Twoja pomoc bdzie mile widziana.

Thinking in C++. Edycja polska

3kladka
Na okadce pierwszego wydania ksiki widnia wizerunek mojej twarzy, ale pierwotnie chciaem, by okadka drugiego wydania bya raczej dzieem artysty, podobnie jak okadka ksiki Thinking in Java. Z jakiego powodu C++ kojarzyo mi si z art deco, z jego nieskomplikowanymi krzywymi i zmatowionymi chromami. Miaem na myli te plakaty statkw i samolotw o wyduonych kadubach. Mj przyjaciel, Daniel Will-Harris (www.Will-Harris.com), z ktrym spotkaem si po raz pierwszy w klasie chru, w gimnazjum, zrobi karier wiatowej sawy projektanta i pisarza. Wykona waciwie wszystkie moje projekty, w tym okadk pierwszego wydania tej ksiki. Podczas projektowania okadki Daniel, niezadowolony z postpw, ktre poczynilimy, pyta: W jaki sposb czy to ludzi z komputerami?". Ugrzlimy. Pod wpywem impulsu, bez adnego konkretnego zamysu, Daniel poprosi mnie, abym pooy twarz na skanerze. Za pomocjednego ze swoich programw graficznych (ulubionego Corela Xary) dokona automatycznego trasowania" zeskanowanego wizerunku mojej twarzy. Opisa to nastpujco: Automatyczne trasowanie jest komputerowym sposobem przeksztacenia obrazu w linie i krzywe, ktre go naprawd przypominaj". Nastpnie bawi si tym obrazem dopty, dopki uzyska co w rodzaju topograficznej mapy mojej twarzy ilustracj tego, w jaki sposb komputery mogyby postrzega ludzi. Skopiowaem ten obraz na papierze akwarelowym (niektre kolorowe kserokopiarki wykonuj kopie na grubych materiaach) i zrobiem mnstwo prb, kolorujc go ' akwarelami. Nastpnie wybralimy te, ktre wyglday najlepiej; Daniel zeskanowa je ponownie i uoy na okadce, dodajc tekst i inne elementy projektu. Wszystko to trwao kilka miesicy, gwnie z uwagi na czas, jaki zajo mi malowanie akwarel. Ale jestem z tej okadki szczeglnie zadowolony, poniewa mam swj udzia w prezentowanym na niej dziele sztuki, a ponadto praca nad ni zachcia mnie do namalowania kolejnych akwarel (prawdjest to, co si mwi o praktyce).

Wprowadzenie do obiektw
Rewolucja komputerowa rozpocza si od rozwoju sprztu. Pierwsze jzyki programowania byy wic prb naladowania zachowa komputerw. Komputery jednak s nie tyle maszynami, ile raczej wzmacniaczami umysu" oraz nowym rodkiem ekspresji. W rezultacie, coraz mniej przypominaj maszyny, upodabniajc si do elementw naszych umysw i przypominajc inne rodki wyrazu, takiejak literatura, malarstwo, rzeba, animacja i twrczo filmowa. Programowanie obiektowe stanowi krok w kierunku uywania komputera w charakterze rodka ekspresji. Rozdzia ten wprowadzi ci w podstawowe pojcia programowania obiektowego (ang. OOP object oriented programming), wczajc w to przegld obiektowych metod projektowania. Przyjto zaoenie, e masz dowiadczenie w uywaniu jakiego jzyka proceduralnego, ktrym niekoniecznie musi by C. Jeeli uwaasz, e zanim zaczniesz czyta t ksik, potrzebujesz lepszego przygotowania w zakresie programowania oraz informacji na temat skadni jzyka C, warto zapozna si z kursem ,,Thinking in C++: Foundations for C++ and Java", zamieszczonym na doczonym do ksiki CD-ROM-ie (dostpny rwnie pod adresem: http:Melion.pl/online/ thinking/index.html). Rozdzia zawiera zarwno podstawowe informacje, jak i materia uzupeniajcy. Wiele osb odczuwa dyskomfort, debiutujc w dziedzinie programowania obiektowegoJeeli nie zrozumie najpierwjego oglnej idei. Dlatego te wprowadzono wiele poj, zapewniajcych gruntowny przegld programowania obiektowego. Niektrzy jednak nie s w stanie uchwyci oglnych idei, dopki nie poznaj szczegw mog oni czu si zagubieni, jeeli nie dostan do rki fragmentu programu. Jeeli naleysz do tej drugiej grupy ijeste gotw pozna szczegyjzyka, moesz bez wahania opuci niniejszy rozdzia nie przeszkodzi ci to w pisaniu programw ani w nauce jzyka. Jednake zapewne bdziesz chcia powrci do niego, by uzupeni swoj wiedz; dowiesz si bowiem, dlaczego obiekty s wane i jak ich uywa podczas projektowania.

Rozdzia 1.

26

Thinking in C++. Edycja polska

Postp abstrakcji
Wszystkie jzyki programowania dostarczaj pewnych abstrakcji. Mona negowa twierdzenie, e zoono problemw, ktre mona rozwiza, jest bezporednio zwizana z rodzajem i jakoci abstrakcji. Pod pojciem rodzaj" rozumiem: to, co jest przedmiotem abstrakcji". Jzyk asemblera jest w niewielkim stopniu abstrakcj komputera. Wiele tak zwanych jzykw imperatywnych, ktre pojawiy si pniej (takich jak Fortran, BASIC i C), byo abstrakcjami jzyka asemblera. Stanowiy one istotny postp w stosunku do jzyka asemblera, jednak ich podstawowy poziom abstrakcji wymaga nadal mylenia w kategoriach struktury komputera, a nie struktury rozwizywanego problemu. Programista musi okreli zwizek pomidzy modelem maszyny (w przestrzeni rozwizania", bdcej miejscem, w ktrym modelowanyjest problem takim jak komputer) i modelem rozwizywanego problemu (w przestrzeni problemu", czyli miejscu, w ktrym ten problem wystpuje). Wysiek konieczny do dokonania takiego odwzorowania, a take fakt, e nie jest on zwizany zjzykiem programowania, prowadz do tworzenia programw trudnych do napisania i kosztownych w utrzymaniu, a ich ubocznym efektem jest powstanie caej dziedziny metod programowania". Rozwizaniem alternatywnym w stosunku do modelowania maszyny jest modelowanie rozwizywanego problemu. Wczesne jzyki programowania, takie jak LISP i APL, preferoway pewne szczeglne punkty widzenia wiata (,,wszystkie problemy s w istocie listami" lub wszystkie problemy s algorytmiczne"). PROLOG nadawa wszystkim problemom posta cigw decyzji. Powstay jzyki przeznaczone do programowania z ograniczeniami (ang. constraint-based programming), a take do programowania wykorzystujcego wycznie operacje na obiektach graficznych (te ostatnie okazay si zbyt ograniczone). Kade z powyszych uj stanowi dobre rozwizanie klasy problemw, dla ktrych zostao stworzone, lecz poza t dziedzin okazuje si niewygodne. Programowanie obiektowe idzie o krok dalej, dostarczajc programicie narzdzi umoliwiajcych reprezentacj elementw w przestrzeni problemu. Reprezentacja ta jest wystarczajco oglna, by nie ogranicza programisty do adnego okrelonego rodzaju problemw. Odwoujemy si do elementw w przestrzeni problemu oraz ich reprezentacji w przestrzeni rozwizania jako do obiektw" (oczywicie, potrzebne bd rwnie obiekty niemajce swych odpowiednikw w przestrzeni problemu). Idea polega na umoliwieniu programowi dopasowania si do specyficznego dialektu problemu poprzez dodawanie nowych typw obiektw, dziki czemu kod oznaczajcy rozwizanie wyraony jest sowami opisujcymi problem. Jest to bardziej elastyczny i efektywniejszy poziom abstrakcjijzyka ni te, ktrymi dysponowalimy do tej pory. A zatem programowanie obiektowe umoliwia wyraenie problemu we waciwych mu kategoriach, a nie w kategoriach opisujcych komputer, na ktrym bdzie on rozwizywany. Istnieje jednak nadal pewien zwizek z komputerem. Kady obiekt przypomina may komputer znajduje si w jakim stanie, a take posiada zbir operacji, o ktrych wykonanie mona go poprosi. Wydaje si to stanowi niez analogi do obiektw wystpujcych w rzeczywistym wiecie wszystkie one posiadajpewne szczeglne cechy oraz charakteryzujsi waciwym sobie zachowaniem.

Rozdzia 1. * Wprowadzenie do obiektw

27

Niektrzy projektanci jzykw programowania doszli do wniosku, e samo programowanie obiektowe nie umoliwia atwego rozwizywania wszystkich problemw programistycznych i stali si ordownikami kombinacji rnych podej, tworzc jzyki progra[ mowania wieloparadygmatowego (ang. multiparadigmprogramming languages) . Alan Kay podsumowa pi podstawowych cech jzyka Smalltalk pierwszegc udanegojzyka obiektowego, stanowicego zarazemjeden zjzykw, w oparciu o ktre powstao C++. Cechy te reprezentujpodejcie czysto obiektowe: 1. Wszystkojest obiektem. Obiekt naley ujmowajako szczeglny rodzaj zmiennej przechowuje on dane, ale mona rwnie w stosunku do niego zgosi danie", proszc obiekt o wykonanie operacji na sobie samym. Teoretycznie, kady skadnik pojciowy rozwizywanego problemu (psy, budynki, usugi itp.) moe by reprezentowany w programie w postaci obiektu. 2. Program jest grup obiektw, przekazujcych sobie wzajemnie informacje o tym, co naley zrobi, za pomoc komunikatw. Zgoszenie dania w stosunku do obiektu odbywa si poprzez wysanie do niego komunikatu". Komunikat taki moe by traktowanyjako danie wywoania funkcji nalecej do tego obiektu. 3. Kady obiekt posiada wasn pamie, zoon z innych obiektw. Innymi sowy, nowy rodzaj obiektujest tworzony poprzez utworzenie pakietu zoonego z j u istniejcych obiektw. A zatem mona powiksza zoono programu, ukrywajcjrwnoczenie za prostotobiektw. 4. Kady obiekt posiada typ. Mwic potocznie, kady obiektjest egzemplarzem jakiej klasy, przy czym sowo ,,klasa"jest synonimem sowa typ". Najistotniejszcechodrniajcod siebie klasyjest informacja o tym, jakie komunikaty mogby do nich wysyane. 5. Wszystkie obiekty okrelonego typu mog odbiera te same komunikaty. Jak przekonamy si pniej, jest to twierdzenie idce nieco zbyt daleko. Poniewa obiekt typu ,,okrag"jest zarazem obiektem typu figura", nie ulega wtpliwoci, e odbiera on komunikaty dotyczce figur. Oznacza to, e mona napisa program odwoujcy si do figur, automatycznie obsugujcy wszystko to, co odpowiada opisowi figury. Ta zastpowalno (ang. substitutability) naley do najwaniejszych koncepcji programowania obiektowego.

Obiekt posiada interfejs


Prawdopodobnie pierwszym, ktry rozpocz systematyczne badania zwizane z pojciem typu, by Arystoteles. Mwi on o klasie ryb i klasie ptakw". Pomys, by wszystkie obiekty, bdc unikatowymi, byy zarazem elementami klasy obiektw, posiadajcych pewne wsplne cechy i sposoby zachowania, zosta bezporednio wykorzystany w pierwszym jzyku obiektowym Simuli-67. Jej podstawowym sowem kluczowym byo sowo class, umoliwiajce utworzenie w programie nowego typu.
-Timothy Budd: Multipamdigm Programming in Leda, Addison-Wesley, 1995.

Thinking in C++. Edycja polska Simula, jak na to wskazuje jej nazwa, powstaa z myl o przeprowadzaniu symulacji, takich jak klasyczny problem kasjera bankowego" 2 . W problemie tym wystpuje zbir kasjerw, klientw, kont, transakcji i jednostek pieninych czyli wiele obiektw". Obiekty, ktre sidentyczne (z wyjtkiem ich aktualnego stanu w czasie wykonywania programu), zostay pogrupowane w klasy obiektw" std pochodzi wanie sowo kluczowe class. Tworzenie abstrakcyjnych typw danych (klas) jest fundamentaln ide programowania obiektowego. Abstrakcyjne typy danych funkcjonuj niemal tak samo, jak typy wbudowane: mona tworzy zmienne tych typw (nazywane w argonie programowania obiektowego obiektami lub egzemplarzami) oraz operowa na tych zmiennych (co nazywane jest wysylaniem komunikatw lub da wysyany jest komunikat, a obiekt okrela, co powinien z nim zrobi). Skadniki (elementy) kadej klasy maj pewne cechy wsplne kade konto ma saldo, kady kasjer przyjmuje wpaty itp. Rwnoczenie kady element posiada swj wasny stan kade konto ma inne saldo, a kady kasjer nosi jakie nazwisko. A zatem unikatowe jednostki programu komputerowego mog reprezentowa poszczeglnych kasjerw, kJientw, kade konto, transakcj itp. Jednostki takie s obiektami. Kady obiekt naley do konkretnej klasy, okrelajcej jego cechy i sposb dziaania. A zatem mimo e podczas programowania obiektowego zajmujemy si tworzeniem nowych typw danych, praktycznie we wszystkich obiektowych jzykach programowania uywamy sowa kluczowego class". Widzc siowo typ", powinnimy zatem myle klasa" i na odwrt3. Poniewa klasa opisuje zbir obiektw posiadajcych takie same cechy (elementy danych) i dziaania (funkcjonalno), jest ona w rzeczywistoci typem danych, podobnie jak np. liczba zmiennopozycyjna, majca rwnie waciwy sobie zbir cech i moliwych dziaa. Rnica polega na tym, e programista definiuje klas w taki sposb, by odpowiadaa ona problemowi, nie jest wic zmuszony do uywania istniejcego typu danych, zaprojektowanego w celu odzwierciedlenia jednostki pamici komputera. Rozszerzamy zatem jzyk programowania, dodajc do niego typy danych odpowiadajce naszym potrzebom. System programowania przyjmuje nowe klasy, traktujcje w taki sam sposb i zapewniajc im taksamkontrolJak typom wbudowanym. Obiektowe podejcie do programowania nie ogranicza si do tworzenia symulacji. Niezalenie od tego, czy zgadzamy si z twierdzeniem, e kady program jest symulacj projektowanego systemu, uycie technik obiektowych pozwala na atwe zredukowanie duego zbioru problemw do prostego rozwizania. Po utworzeniu klasy mona z atwoci utworzy dowoln liczb obiektw tej klasy, a nastpnie operowa na nich w taki sposb, jakby byy rzeczywistymi elementami rozwizywanego problemu. W istocie, jednym z zada programowania obiektowego jest ustalenie wzajemnie jednoznacznego odwzorowania pomidzy elementami przestrzeni problemu a elementami przestrzeni rozwizania.

Interesujcimplementacj tego problemu mona znale w drugim tomie ksiki, dostpnym pod adresem http://helion.pl/online/thinking/index.html. Niektrzy Niektrzy czynirozrnienie, czynirozrnienie, twierdzc, e typ okrela interfejs, podczas gdy klasajest szczegln implementacjtego interfejsu.

Rozdzia 1. Wprowadzenie do obiektw

29

Jakjednak mona zmusi obiekt do wykonaniajakiego poytecznego zadania? Musi istnie sposb, w jaki mona zada od obiektu, by np. wykona transakcj, narysowa co na ekranie albo zmieni stan przecznika. Ponadto kady obiekt moe spenia tylko pewne okrelone dania. dania, ktre mona przekazywa obiektowi, s zdefiniowane poprzez jego interfejs. Tym, co okrela interfejs, jest natomiast typ 4 obiektu. Prostym tego przykadem moe by reprezentacja arwki :

Nazwa typu

Interfejs

arwka zr; zr.zapal():

Interfejs okresla,jakie dania mogby kierowane do okrelonego obiektu. Jednake gdzie musi istnie kod realizujcy to danie. Wraz z ukrytymi danymi skada si on na implementacj. Z punktu widzenia programowania proceduralnego niejest to a tak skomplikowane. Typ posiada funkcj zwizan z kadym moliwym daniem i kiedy jest ono zgaszane, funkcja ta jest wywoywana. Jest to zazwyczaj przedstawiane w taki sposb, e do obiektu wysyany jest komunikat" (zgas/ane jest danie), a obiekt podejmuje decyzj, co z nim zrobi (wykonuje kod). W powyszym przykadzie nazw typu (klasy) jest arwka, natomiast nazw konkretnego obiektu typu arwka jest zr. daniami, jakie mona skierowa do obiektu klasy arwka, jest jej zapalenie, zgaszenie, rozjanienie i przyciemnienie. Mona utworzy obiekt klasy arwka, okrelajc jego nazw (zr). Aby wysa obiektowi komunikat, naley wpisa jego nazw, czc j z komunikatem dania za pomoc kropki. Z punktu widzenia uytkownika predefiniowanej klasy jest to niemal wszystko, co dotyczy programowania z wykorzystaniem obiektw. Przedstawiony powyej diagramjest zgodny z notacjjzyka UML (ang. Unified Modeling Language zunifikowany jzyk modelowania). Kada klasa jest reprezentowana przez prostokt; w jego grnej czci znajduje si nazwa typu, w rodkowej dane skadowe, ktre zamierzamy opisa, a w dolnej funkcje skadowe (funkcje nalece do obiektu, odbierajce wszelkie wysyane do niego komunikaty). Diagramy projektowe UML przedstawiaj czsto jedynie nazw klasy oraz jej publiczne funkcje skadowe, w zwizku z czym niejest widoczna ich rodkowa cz. Jeeli interesuje ci jedynie nazwa kIasy, to mona pomin rwnie doln cz diagramu.

Zawarte w niniejszym rozdziale

Thinking in C++. Edycja polska

Ukryta implementacja
Pomocne jest dokonanie podziau na twrcw klas (osoby tworzce nowe typy danych) oraz klientw-programistw (ang. client programmers) konsumentw klas", uywajcych tych typw danych w swoich aplikacjach. Celem klientaprogramisty jest skompletowanie zestawu narzdzi, skadajcego si z klas, ktry umoliwi mu szybkie opracowanie aplikacji. Celem twrcy klasy jest natomiast utworzenie klasy w taki sposb, by ujawniaa jedynie to, co jest niezbdne klientowiprogramicie, zachowujc reszt w ukryciu. Dlaczego? Dziki temu, e klientowiprogramicie nie wolno uywa tego, co jest niewidoczne, twrca klasy ma moliwo dowolnego zmieniania niewidocznej czci klasy, bez koniecznoci uwzgldniania wpywu, jaki bdzie to miao na kogokolwiek. Ukryt czci jest zazwyczaj wraliwe wntrze obiektu, ktre mogoby zosta atwo uszkodzone przez nieostronego, lub posiadajcego zbyt ma wiedz, klienta-programist. A zatem ukrywanie implementacji zmniejsza liczb bdw wystpujcych w programach i odgrywa rol nie do przecenienia. W kadej relacji istotne jest okrelenie granic, respektowanych przez wszystkie zaangaowane w ni strony. Tworzc bibliotek, ustanawiamy relacj z klientemprogramist, bdcym rwnie programist, lecz uywajcym naszej biblioteki do zbudowania wasnej aplikacji lub utworzenia wikszej biblioteki. Jeeli wszystkie skadowe klasy s dostpne dla wszystkich, klient-programista moe wykona dowolne dziaania i nie jest moliwe wyegzekwowanie przestrzegania jakichkolwiek regu. Nawet w przypadku gdy nie chcemy, by programista bezporednio operowa na niektrych skadowych klasy, bez mechanizmu kontroli dostpu, nie jestemy w stanie temu zapobiec. Wszystko jest wystawione na widok publiczny. A zatem pierwszym powodem wprowadzenia kontroli jest uniemoliwienie programistom dostpu do tego, czego nie powinni si ima elementw niezbdnych do wykonywania wewntrznych operacji zwizanych z typem danych, lecz niebdcych czci interfejsu, potrzebnego uytkownikom do rozwizania ich szczeglnych problemw. Jest to w rzeczywistoci pomoc udzielana uytkownikom, poniewa dziki temu mog atwo odrni to, co jest dla nich istotne, od tego, co mog pomin. Drugim powodem wprowadzenia kontroli dostpu jest umoliwienie projektantowi biblioteki zmiany wewntrznych mechanizmw klasy, bez troszczenia si o to, jaki bdzie to miao wpyw na klienta-programist. Na przykad mona uatwi sobie prac, implementujc jak klas w sposb uproszczony, a nastpnie doj do wniosku, e konieczne jest jej zmodyfikowanie, tak by dziaaa szybciej. Jeeli jej interfejs oraz implementacja s od siebie wyranie oddzielone i chronione, mona to atwo zrobi, wymagajc od uytkownika jedynie powtrnej konsolidacji programu. Jzyk C++ posiada trzy sowa kluczowe, wykorzystywane bezporednio do okrelenia ogranicze w klasach: public, private i protected. Ich sposb uycia oraz znaczenie nie budz wtpliwoci s one specyfikatorami dostpu (ang. access specifiers),
Jestem wdziczny za ten termin mojemu przyjacielowi, Scottowi Mayersowi.

Rozdzia 1. Wprowadzenie do obiektw

31

okrelajcymi, kto moe uywa nastpujcych po nich definicji. Specyfikator public oznacza, e s one dostpne dla wszystkich. Z kolei sowo kluczowe private oznacza, e definicje te mog by uywane wycznie przez ciebie (twrc typu) jedynie wewntrz funkcji skadowych. Sowo private stanowi barier pomidzy tob i klientemprogramist. Kady, kto sprbuje odwoa si do prywatnej skadowej klasy, otrzyma komunikat o bdzie ju na etapie kompilacji. Znaczenie sowa protected jest zblione do private, z wyjtkiem tego, e klasa dziedziczca ma dostp do skadowych chronionych (wystpujcych po specyfikatorze protected) klasy, nie ma natomiast dostpu do jej skadowych prywatnych (priyate). Pojcie dziedziczenia zostanie wkrtce wprowadzone.

Wykorzystywanie istniejcej implementacji


Kiedy klasa zostanie utworzona i przetestowana, powinna (w idealnym przypadku) stanowi uyteczny fragment kodu. Okazuje si, e moliwo jego ponownego wykorzystania nie jest tak atwa do osignicia, jak mona by si tego spodziewa dobry projekt wymaga bowiem dowiadczenia i intuicji. Moliwo wielokrotnego wykorzystywania kodu jest jedn z najwikszych korzyci zapewnianych przez obiektowe jzyki programowania. Najprostszym sposobem ponownego wykorzystania klasy jest bezporednie uycie obiektu tej klasy. Mona rwnie umieci ten obiekt wewntrz nowej klasy, co nazywamy utworzeniem obiektu skadowego". Nowa klasa moe by utworzona z dowolnej liczby innych obiektw dowolnych typw, w dowolnej kombinacji, niezbdnej do osignicia wymaganej funkcjonalnoci tworzonej klasy. Poniewa nowa klasa jest tworzona (komponowana) z klas ju istniejcych, proces ten nosi nazw kompozycji (lub bardziej oglnie agregacji). Kompozycja jest czsto okrelana jako relacja typu posiada", rozumiana w taki sposb, jak wystpujca w zdaniu samochd posiada silnik".

Na powyszym diagramie jzyka UML kompozycja zostaa oznaczona wypenionym rombem, ktry symbolizuje istnienie pojedynczego samochodu. Zazwyczaj bdzie uywane prostsze oznaczenie poczenia zwyka linia (bez rombu)6. Kompozycja zapewnia duy stopie elastycznoci. Obiekty skadowe nowej klasy s zazwyczaj prywatne, co czyni je niedostpnymi dla klienta-programisty wykorzystujcego t klas. Pozwala to na zmian tych skadowych, bez wpywu na istniejcy
Jest to zazwyczaj wystarczajco precyzyjne w przypadku wikszoci diagramw, na og nie jest rwnie konieczne okrelanie, czy stosowana jest agregacja czy te kompozycja.

32

Thinking in C++. Edycja polska kod, napisany przez klienta. Mona rwnie zmienia obiekty skadowe w czasie wykonywania kodu, modyfikujc dynamicznie zachowanie programu. Opisane w nastpnym podrozdziale dziedziczenie nie posiadaju takiej elastycznoci z uwagi na ograniczenia dotyczce klas tworzonych zajego pomoc, nakadaneju w czasie kompilacji. Ze wzgldu na to, e dziedziczenie odgrywa tak wan rol w programowaniu obiektowym,jego znaczeniejest czsto podkrelane. Moe to wywoa u niedowiadczonego programisty przekonanie, e dziedziczenie naley stosowa w kadym przypadku. W rezultacie tworzone przez niego programy mogby niespjne i nadmiernie skomplikowane. Podczas tworzenia nowych klas naley zawsze mie przede wszystkim na uwadze kompozycj, poniewa jest ona prostsza i bardziej elastyczna. Takie podejcie umoliwi ci tworzenie bardziej przejrzystych programw. Gdy ju nabierzesz pewnego dowiadczenia, stanie si dla ciebie wystarczajco oczywiste, w jakich przypadkach naley uywa dziedziczenia.

Dziedziczenie wykorzystywanie istniejcego interfejsu


Idea obiektu jest sama w sobie wygodnym narzdziem. Pozwala ona na poczenie ze sob danych oraz funkcji w pojcia, umoliwiajce reprezentacj odpowiednich elementw przestrzeni problemu, bez koniecznoci uywania dialektu waciwego wykorzystywanej maszynie. Pojcia te s wyraane w postaci podstawowych jednostek jzyka programowania, za pomoc sowa kluczowego class. Wydaje si jednak, e szkoda byoby, gdybymy zadali sobie trud utworzenia jakiej klasy, a nastpnie byIi zmuszeni do utworzenia zupenie nowej, posiadajcej by moe podobne waciwoci. Znacznie lepiej sklonowa" istniejc klas, a nastpnie dokona, na utworzonej w ten sposb kopii, wszelkich niezbdnych rozszerze i modyfikacji. Uzyskujemy ten efekt wanie dziki dziedziczeniu (ang. inheritance). Jedyna rnica polega na tym, e w przypadku gdy oryginalna klasa (zwana klas podstawow, klas bazow, nadklas l u b klas nadrzdn) zostanie zmieniona, przeksztacenia te zostan rwnie uwzgldnione w zmodyfikowanym ,,klonie" (nazywanym klas pochodn, klasapotomna,podklasa lub klaspodrzdn).

' _(

Strzaka na powyszym diagramie UML jest skierowana od klasy pochodnej do podstawowej. Jak przekonamy si pniej, moe istnie wicej nijedna klasa pochodna.
I

Rozdzia 1. * Wprowadzenie do obiektw

33

Typ to nie tylko opis ogranicze dotyczcych pewnego zbioru obiektw posiada on rwnie powizania z innymi typami. Dwa typy mog mie wsplne cechy i metody dziaania, lecz jeden z nich moe zawiera wicej cech ni drugi; moe rwnie obsugiwa wiksz liczb komunikatw (lub obsugiwa je w odmienny sposb). Dziedziczenie opisuje takie wanie podobiestwo pomidzy typami, posugujc si pojciami typw podstawowych oraz typw pochodnych. Typ podstawowy posiada wszystkie cechy i sposoby zachowania wsplne dla utworzonych na jego podstawie typw pochodnych. Tworzymy go po to, by reprezentowa istot naszych wyobrae dotyczcych niektrych obiektw zawartych w systemie. Z typu podstawowego wyprowadzamy inne typy, prezentujc rne sposoby, na jakie istota ta moe by urzeczywistniona. Rozwamy przykad maszyny sortujcej mieci, przeznaczonej do recyklingu odpadw. Typem podstawowym jest odpad. Kady odpad ma swoj wag, warto itp., a take moe by pocity, przetopiony lub rozmontowany. Z tego typu mona wyprowadzi bardziej precyzyjnie zdefiniowane typy odpadkw, posiadajce dodatkowe cechy (butelka ma na przykad jaki kolor) lub sposoby funkcjonowania (np. puszka aluminiowa moe zosta zgnieciona, a puszka stalowa podlega oddziaywaniu pola magnetycznego). W dodatku mog si rnie zachowywa (np. warto papieru zaley od jego rodzaju oraz stanu). Dziki wykorzystaniu dziedziczenia moemy budowa hierarchi typw, opisujc rozwizywany problem za pomoc poj dotyczcych wystpujcych w nim typw. Jako drugi rozpatrzymy klasyczny przykad figur geometrycznych, ktry mgby znale zastosowanie w komputerowym systemie wspomagajcym projektowanie lub w symulacji gry. Typem podstawowym jest w tym przypadku figura". Kada figura posiada wielko, kolor, pooenie itd.; moe ona by rwnie narysowana, usunita, przesunita, pokolorowana itp. Z typu tego wyprowadzono (za pomoc dziedziczenia) typy poszczeglnych figur: okrgu, kwadratu, trjkta itd. z ktrych kady moe posiada wasne dodatkowe cechy i sposoby dziaania. Pewne figury mog by na przykad odwrcone na drug stron. Niektre dziaania mog rni si midzy sob, np. w przypadku gdy chcemy policzy pole powierzchni figury. Hierarchia typw wyraa zarwno podobiestwa, jak i rnice wystpujce wrd figur.

Figura
narysuj() usu() przesu() pobierzKolor() ustawKolor()

Okrg

Kwadrat

Trjkt

Sformuowanie rozwizania za pomoc poj nalecych do jzyka problemu jest wyjtkowo korzystne, poniewa nie wymaga tworzenia szeregu modeli porednich, umoliwiajcych przejcie od opisu problemu do opisu rozwizania. W przypadku

Thinking in C++. Edycja polska obiektw, hierarchia typw jest modelem podstawowym, co pozwala na bezporednie przejcie od opisu systemu w rzeczywistym wiecie do opisu systemu w postaci kodu. Okazuje si, e jedn z trudnoci napotykanych przez osoby zajmujce si projektowaniem obiektowym jest zbyt atwa droga prowadzca od pocztku do koca. Czsto zdarza si, e umys, przyzwyczajony do poszukiwania skomplikowanych rozwiza, jest pocztkowo t prostot zaskoczony. Poprzez dziedziczenie z istniejcego typu tworzymy nowy typ. Zawiera on nie tylko wszystkie skadowe klasy podstawowej (mimo e jej skadowe prywatne s ukryte i niedostpne), ale co waniejsze powiela rwniejej interfejs. Oznacza to, e te wszystkie komunikaty, ktre moemy wysa do obiektu klasy podstawowej, moemy rwnie przekaza do obiektu klasy pochodnej. Poniewa typ kasy poznajemy po komunikatach, ktre moemy do niej wysa, oznacza to, e klasa pochodna jest tego samego typu, co klasa podstawowa. W opisanym powyej przykadzie oznacza to, e ,,okrag jest figur". Ten rodzaj rwnowanoci, uzyskiwanej poprzez dziedziczenie, jest jednym z kamieni milowych na drodze do zrozumienia znaczenia programowania obiektowego. Poniewa zarwno klasa podstawowa, jak i klasa pochodna posiadaj taki sam interfejs, istnieje jaka zwizana z nim implementacja. Oznacza to, e musi istnie jaki kod, wykonywany wwczas, gdy obiekt otrzymuje okrelony komunikat. Jeeli utworzymy za pomoc dziedziczenia jak klas i nie zrobimy niczego wicej, to metody klasy podstawowej przejd w caoci do klasy pochodnej. Oznacza to, e w takim przypadku obiekty klasy pochodnej maj nie tylko ten sam typ, co obiekty klasy podstawowej, ale rwnie zachowuj si tak samo, co niejest szczeglnie interesujce. Istniej dwa sposoby modyfikacji klasy pochodnej w stosunku do jej klasy podstawowej. Pierwszyjest do prosty mona doda do klasy pochodnej zupenie nowe funkcje. Funkcje te nie s czci interfejsu klasy podstawowej. Oznacza to, e klasa podstawowa po prostu nie wykonaa wszystkiego tego, co byo nam potrzebne, wic dodalimy nowe funkcje. Ten prosty i prymitywny sposb zastosowania dziedziczenia jest czasami najlepszym sposobem rozwizania problemu. Jednake powinnimy rozway dokadniej moliwo, e klasa podstawowa rwnie potrzebuje dodanych przez nas funkcji. Taki wanie proces iteracyjnego udoskonalania projektu odbywa si regularnie podczas programowania obiektowego.

Rozdzia 1. Wprowadzenie do obiektw

35

Mimo e moe si czasami wydawa, i dziedziczenie pociga za sob dodawanie do interfejsu nowych funkcji, to nie zawsze jest to zgodne z prawd. Drugim, i zarazem najwaniejszym, sposobem modyfikacji klasy pochodnej jest zmiana dziaania istniejcej funkcji klasy podstawowej. Nosi ona nazw zasaniania (ang. overriding) funkcji.

Aby zasoni funkcj, naley po prostu utworzy now definicj tej funkcji w klasie pochodnej. W ten sposb owiadczasz: uywam w tym miejscu tej samej funkcji interfejsu, ale chc, by w nowym typie robia ona co innego".

Relacje typu jest" i Jest podobny do"


A oto przedmiot dyskusji, ktra mogaby dotyczy dziedziczenia: czy nie powinno ono zasania jedynie funkcji obecnych w klasie podstawowej (nie dodajc nowych funkcji skadowych, jeeli nie wystpuj one w klasie podstawowej)? Oznaczaoby to, e klasa pochodna jest dokladnie tego samego typu, co klasa podstawowa, poniewa ma ona identyczny interfejs. W rezultacie mona by cakowicie zastpi obiekt klasy podstawowej obiektem klasy pochodnej. Mona uzna taki przypadek za czyste zastpowanie odwoujemy si do niego czsto jako do zasady zastpowania (ang. substitution principle). Jest to, w pewnym sensie, idealny sposb traktowania dziedziczenia. W takich przypadkach okrelamy czsto relacj pomidzy klas pochodn i klas podstawowjako relacj typujest, poniewa moemy wwczas stwierdzi np. okrgjest figur". Sprawdzianem rozumienia dziedziczeniajest umiejtno opisania w logiczny sposb relacji typu ,jest" pomidzy klasami. Zdarzajsijednak przypadki, w ktrych koniecznejest dodanie do typu pochodnego nowych elementw interfejsu, powodujc tym samym rozszerzenie tego interfejsu oraz utworzenie nowego typu. Typ podstawowy moe by nadal zastpiony typem pochodnym, ale zamiana ta niejestju tak doskonaa, poniewa nowe funkcje nie s dostpne z poziomu typu podstawowego. Mona t sytuacj opisa za pomoc relacji jestpodobny do nowy typ posiada interfejs starego typu, lecz zawiera rwnie inne funkcje; nie moemy zatem powiedzie, e typy te s dokadnie takie same. Jako przykad rozwamy klimatyzator. Zamy, e twj dom jest wyposaony w instalacj

36

Thinking in C++. Edycja polska

zawierajc wszystkie urzdzenia niezbdne do sterowania chodzeniem czyli posiada umoliwiajcy to interfejs. Wyobra sobie, e klimatyzator zepsu si i zastpujesz go pomp ciepa urzdzeniem, ktre moe zarwno ogrzewa, jak i chodzi. Pompa c\ep}'djestpodobna do klimatyzatora, ale zapewnia wicej moliwoci. Poniewa instalacja w twoim domu zostaa zaprojektowana wycznie w celu sterowania chodzeniem, moe si ona komunikowa jedynie z chodzc czci nowego urzdzenia. Interfejs nowego obiektu zosta rozbudowany, lecz istniejcy system nie zna niczego poza interfejsem oryginalnym.

Oczywicie, po przyjrzeniu si projektowi stanie si jasne, e klasa podstawowa system chodzenia" nie jest dostatecznie oglna i powinna zosta przemianowana na system sterowania temperatur" w taki sposb, by obejmowaa rwnie ogrzewanie, co umoliwi funkcjonowanie zasady zastpowania. Jednake powyszy diagram ilustruje to, co moe zdarzy si zarwno w czasie projektowania,jak i w rzeczywistym wiecie. Po zapoznaniu si z zasad zastpowania atwo jest odnie wraenie, e takie podejcie (czyste zastpowanie) jest jedynym sposobem postpowania oraz e projekt powinien by zbudowany w taki wanie sposb. Przekonamy si jednak, e zdarzaj si sytuacje, w ktrych zachodzi konieczno dodania nowych funkcji do interfejsu klasy pochodnej. Po bliszym przyjrzeniu si oba powysze przypadki powinny sta si dostatecznie oczywiste.

Zastpowanie obiektw przy uyciu polimorfizmu


Przy posugiwaniu si hierarchi typw czsto zdarza si, e chcemy traktowa obiekt w taki sposb, jakby nie by on obiektem jakiego szczeglnego typu, ale typu podstawowego. Pozwala to na napisanie kodu, ktry nie bdzie zaleny od konkretnych typw. W przykadzie dotyczcym figur funkcje operuj na dowolnych figurach geometrycznych, niezalenie od tego, czy s one okrgami, kwadratami, trjktami itd. Wszystkie figury mog zosta narysowane, usunite i przesunite, wic funkcje te przesyaj po prostu komunikat do obiektu figura", nie uwzgldniajc tego, w jaki sposb obiekt ten komunikat obsuy.

zdzia 1. Wprowadzenie do obiektw

37

Na napisany w taki sposb kod nie ma wpywu dodawanie nowych typw, bdce najczstszym sposobem rozbudowy programu obiektowego w celu dostosowania go do obsugi nowych sytuacji. Na przykad moemy wyprowadzi nowy podtyp figury . piciokt" nie modyfikujc funkcji majcych do czynieniajedynie z ogln" postaci figur. Moliwo atwego rozszerzania programu poprzez wyprowadzanie nowych typw jest istotna, poniewa znacznie podnosi ona jako projektw, obniajc zarazem koszt pielgnacji oprogramowania. Pojawia si jednak problem zwizany z prb traktowania obiektw typu pochodnego jako obiektw ich typu podstawowego (okrgwjako figur, rowerwjako pojazdw, kormoranw jako ptakw itd.)- Jeeli funkcja zamierza poleci oglnej" figurze, by si narysowaa, oglnemu" pojazdowi, by si przemieci albo oglnemu" ptakowi, by si poruszy, to kompilator nie moe w czasie kompilacji programu dokadnie wiedzie, ktry fragment kodu zostanie wykonany. To wanie stanowi istot kiedy wysyany jest komunikat, programista nie chce wiedzie, ktry fragment kodu zostanie wykonany. Funkcja rysowania moe by bowiem zastosowana zarwno do okrgu, jaki i kwadratu lub trjkta, a obiekt wykona waciwy kod, w zalenoci od swojego konkretnego typu. Jeeli nie musimy wiedzie, ktry fragment kodu zostanie wykonany, to po dodaniu nowego podtypu wykonywany przez niego kod moe by inny, bez koniecznoci dokonywania zmian w wywoaniu funkcji. Jak zatem dziaa kompilator, nie wiedzc dokadnie, ktry fragment kodu jest wykonywany? Na przykad na przedstawionym poniej diagramie obiekt KontroIerPtaka pracuje po prostu z oglnymi" obiektami typu Ptak, nie znajc ich typw. Jest to wygodne z punktu widzenia obiektu KontroIerPtaka, poniewa nie potrzebuje on adnego specjalnego kodu, ktry okrelabyjakiego dokadnie typujest Ptak, z ktrym wsppracuje, i w j a ki sposb si ten Ptak zachowuje. Dlaczego zatem, gdy wywoywana jest funkcja przemie( ), pomimo nieznajomoci konkretnego typu obiektu Ptak, podejmowane jest waciwe dziaanie (Ge biegnie, leci lub pynie, aPingwin biegnie lub pynie)?

Odpowied stanowi zasadniczy zwrot zwizany z programowaniem obiektowym kompilator nie moe wykona wywoania funkcji w tradycyjny sposb. Wywoanie funkcji, wygenerowane przez kompilator jzyka niebdcego jzykiem obiektowym, powoduje tzw. wczesne wizanie (ang. early binding}. Pojcie to niejest znane tym wszystkim, ktrzy traktuj wywoania funkcji jedynie w tradycyjny sposb. Oznacza ono, e kompilator generuje wywoanie funkcji o pewnej nazwie, a program czcy (ang. linker} zamienia je w okrelony adres bezwzgldny kodu, ktry ma zosta wywoany. W programowaniu obiektowym program nie moe okreli adresu kodu, dopki nie zostanie uruchomiony, a zatem potrzebny jest jaki inny mechanizm, umoliwiajcy wysanie komunikatu do oglnego" obiektu.

38

Thinking in C++. Edycja polska

Do rozwizania tego problemu w jzykach obiektowych jest wykorzystana koncepcja pnego wizania (ang. lale binding). Kiedy do obiektu wysyany jest komunikat, wywoywany kod nie jest okrelony a do momentu uruchomienia programu. Kompilator gwarantuje, e funkcja istnieje, dokonujc kontroli typwjej argumentw oraz zwracanej wartoci Qezyki, w ktrych nie ma to miejsca, s nazywane jzykami o sabej kontroli typw), nie wie jednak, jaki dokadnie kod zostanie wykonany. W celu przeprowadzenia pnego wizania kompilator C++ wstawia zamiast wywoania bezwzgldnego specjalny kod. Kod ten wyznacza adres ciaa funkcji, wykorzystujc informacj zapisan wewntrz obiektu (proces ten zosta szczegowo opisany w rozdziale 15.). A zatem kady obiekt moe zachowywa si rnie, zalenie od treci zawartego w nim specjalnego kodu. Po wysaniu do obiektu komunikatu obiekt domyla si", co naley z nim zrobi. Aby okreli, e funkcja ma posiada elastyczno zwizan z pnym wizaniem, naley uy sowa kluczowego virtual (wirtualny). Nie trzeba rozumie dziaania funkcji wirtualnych, by ich uywa, ale nie sposb bez tego programowa obiektowo w C++. W jzyku C++ naley pamita o dodaniu sowa kluczowego virtual, poniewa domylnie funkcje skadowe nie s dynamicznie wizane. Funkcje wirtualne pozwalaj na wyraenie rnic wdziaaniu klas nalecych do jednej rodziny. Rnice tesprzyczynzachowaniapolimorficznego.

''"'
1

Rozwamy przypadek figur geometrycznych. Rodzina klas (z ktrych wszystkie bazujna tym samym, jednolitym interfejsie) zostaa przedstawiona w pocztkowej czci rozdziau. Aby zademonstrowa polimorfizm, napiszemy kod, ktry bdzie ignorowa szczegy zwizane z konkretnym typem, odwoujc si wycznie do klasy podstawowej. Kod ten zostanie oddzielony od informacji dotyczcej konkretnego typu, dziki czemu bdzie atwiejszy do napisania i do zrozumienia. Ponadto jeeli za pomoc metody dziedziczenia do typu Ksztatt zostanie dodany nowy typ np. Szeciokt to napisany kod bdzie dziaa w nim rwnie dobrze jak w przypadku ju istniejcych typw. Dziki temu mona rozbudowywa istniejcy program. Napiszemy w jzyku C++ funkcj (wkrtce dowiesz si, jak to zrobi): void zrbCo(Figura& f) { f.usu(); // ... f.narysuj(); } Funkcja ta odwouje si do dowolnego obiektu typu Figura, jest wic niezalena od konkretnego typu obiektu, ktry jest rysowany i usuwany (znak &" oznacza pobierz adres obiektu przekazanego funkcji zrbCo()", ale nie jest jeszcze konieczne rozumienie dokadnie wszystkich szczegw). Jeeli wjakiej innej czci programu zostanie uyta funkcja zrbCo():
Okrg o; Trjkt t; Linia l; zrbCo(o); zrbCo(t): zrbCo(l):

Rozdzia 1. Wprowadzenie do obiektw

39

Wywoania funkcji zrbCo( ) automatycznie dziaaj poprawnie, niezalenie od konkretnego typu obiektu. W istocie to zdumiewajca sztuczka. Wemy pod uwag wiersz:
zrbCo(o);

W powyszym przypadku obiekt Okrg zosta przekazany funkcji oczekujcej obiektu typu Figura. Poniewa Okrg jest Figur, to moe by traktowany przez funkcj zrbCo( )jako obiekt typu Figura. Oznacza to, e Okrg akceptuje wszystkie komunikaty, jakie funkcja zrbCo( ) moe wysya do obiektu Figura. Jest to zupenie bezpieczne i cakowicie logiczne. Proces traktowania klasy pochodnej w taki sposb, jakby by on typem podstawowym, nazywamy rzutowaniem w gr (ang. upcasting). Termin rzutowanie zosta uyty w znaczeniu przenoszenia na jak paszczyzn" (lub dopasowania si do pewnej formy"), natomiast w gr" pochodzi ze sposobu, w jaki zazwyczaj jest uoony diagram dziedziczenia z typem podstawowym na grze i klasami pochodnymi rozwijajcymi si poniej. A zatem rzutowanie w kierunku typu podstawowego przesuwa si w diagramie dziedziczenia do gry i std nosi nazw rzutowania w gr.

Programy obiektowe zawieraj jakie rzutowania w gr, poniewa jest to sposb, wjaki uniezaleniamy si od precyzyjnej wiedzy na temat typu, z ktrym pracujemy. Przyjrzyjmy si kodowi funkcji zrbCo():
f.usu(); // . . . f.narysuj(); . , ,

Zauwamy, e powyszy kod nie zawiera polece: jeelijeste typu Okrg, zrb to, ajeelijeste typu Kwadrat, zrb tamto itd.". W przypadku gdy piszemy kod sprawdzajcy wszystkie moliwe typy, ktrymi moe by w danej c h w i l i Figura, staje si on nieelegancki i musimy zmienia go za kadym razem, gdy dodajemy nowy rodzaj obiektu Figura. W naszym przykadzie stwierdzamy natomiast: ,jestes figur i wiem, e potrafisz wykona funkcje usu() i narysuj(), wic zrb to, pamitajc o wszystkich szczegach". Tym, co robi wraenie w kodzie funkcji zrbCo( ), jest fakt, e w jaki sposb dziaa on wanie tak, jak powinien. Wywoanie funkcji rysuj( ) w stosunku do okrgu powoduje wykonanie innego kodu ni wywoaniejej w stosunku do kwadratu czy linii.

40

Thinking in C++. Edycja polska

lecz gdy komunikat rysuj( ) jest wysyany do anonimowego obiektu typu Figura, to podejmowanejest dziaanie waciwe dlajego rzeczywistego typu. To zdumiewajce, poniewa, jak wspomniano wczeniej, w czasie kompilacji kodu funkcji zrbCo( ) kompilatorjzyka C++ nie moe wiedzie dokadnie, z j a k i m i typami ma do czynienia. A zatem spodziewalibymy si wywoania funkcji usu( ) i narysuj( ) typu Figura, a nie funkcji zdefiniowanych dla specyficznych typw: Okrg, Kwadrat czy - . ; : Linia. Jednake dziki polimorfizmowi dzieje si to, co powinno. Szczegami zaj,. muje si kompilator oraz system obsugujcy wykonywanie programu wystarczy -. ( X ,.. tylko wiedzie, e tak si dzieje i, co waniejsze, umie wykorzysta ten fakt podczas , )f-.\ projektowania. Jeeli funkcja skadowajest funkcj wirtualn (oznaczon specyfikatorem virtual), to po wysaniu obiektowi komunikatu wykona on waciw czynno, nawet wwczas, gdy zastosowane bdzie rzutowanie w gr.

Tworzenie i niszczenie obiektw


Z technicznego punktu widzenia do dziedziny programowania obiektowego nale: tworzenie abstrakcyjnych typw danych, dziedziczenie i polimorfizm. Wystpuj w niej jednak rwnie inne, nie mniej wane kwestie. W niniejszym podrozdziale przedstawiono ich przegld. Szczeglnie wany jest sposb, w jaki obiekty s tworzone i niszczone. Gdzie przechowywane s dane obiektw i w jaki sposb kontrolowany jest czas ich ycia? W rnych jzykach programowania przyjmuje si w takich przypadkach odmienne zaoenia. W j z y k u C++, w ktrym najistotniejszkwestijest kontrola efektywnoci, programista ma moliwo wyboru. W celu osignicia maksymalnej szybkoci wykonania sposb przechowywania danych oraz czas ycia mog by okrelone w czasie pisania programu przez umieszczenie obiektw ria stosie lub w obszarze danych statycznych. Stos jest obszarem pamici, uywanym bezporednio przez mikroprocesor do przechowywania danych w czasie wykonywania programu. Zmienne znajdujce si na stosie nazywane s czasami zmiennymi automatycznymi lub lokalnymi. Obszar danych statycznych jest z kolei ustalonym fragmentem pamici, przydzielonym przed rozpoczciem pracy programu. Uywanie stosu lub obszaru danych statycznych oznacza pooenie nacisku na szybko przydzielania i zwalniania pamici, ktra moe by poyteczna w niektrych sytuacjach. Jednake tracisz w ten sposb elastyczno, poniewa musisz w czasie pisania programu zna dokadnie liczb, czas ycia oraz typ obiektw. Jeeli prbujesz rozwiza bardziej oglny problem, taki jak np. projektowanie wspomagane komputerowo, program zarzdzajcy magazynem lub kontrol ruchu powietrznego, stanowi to nadmierne ograniczenie. Drugim podejciemjest dynamiczne tworzenie obiektw w obszarze pamici zwanym stert (ang. heap). W ujciu tym liczba potrzebnych obiektw, ich czas ycia" oraz dokadne typy nie s znane a do uruchomienia programu. Decyzje te s podejmowane spontanicznie, w czasie wykonywania programu. Jeeli potrzebujesz nowego obiektu, to po prostu tworzysz go na stercie, uywajc do tego sowa kluczowego new. Kiedy zajmowana przez niego pami nie jest ci ju potrzebna, musisz zwolni j za pomoc sowa kluczowego delete.

Rozdzia 1. Wprowadzenie do obiektw

41

Poniewa zarzdzanie pamici odbywa si dynamicznie, w czasie wykonywania programu, czas potrzebny do przydzielenia pamici na stercie jest znacznie wikszy ni w przypadku utworzenia obszaru pamici na stosie (ktre jest czsto pojedyncz instrukcj mikroprocesora, polegajc na przesuniciu wskanika stosu w d, oraz drug, przemieszczajc go z powrotem w gr). W podejciu dynamicznym przyjmuje si, logiczne zazwyczaj, zaoenie, e obiekty maj tendencj do bycia zoonymi, a wic dodatkowy narzut, polegajcy na znalezieniu wolnej pamici oraz jej zwolnieniu, nie bdzie mia istotnego wpywu na tworzenie obiektw. Ponadto wiksza elastyczno jest niezbdna w przypadku rozwizywania oglnych problemw programistycznych. Jednake pozostaje jeszcze kwestia czasu ycia obiektw. Jeeli obiekt tworzony jest na stosie lub w obszarze danych statycznych, to kompilator okresla,jak dugo powinien on istnie i moe automatycznie go zniszczy. Jeeli jednak obiekt zostanie utworzony na stercie, kompilator nie posiada informacji na temat jego czasu ycia. W jzyku C++ musimy okreli za pomoc metod programowych, kiedy obiekt powinien zosta zniszczony, a nastpnie dokona tego, uywajc sowa kluczowego delete. Alternatywnie, rodowisko moe posiada mechanizm zwany zbieraczem mieci (ang. garbage collector), automatycznie rozpoznajcym nieuywaneju obiekty, a nastpnieje niszczcym. Oczywicie, pisanie programw uywajcych zbieracza mieci jest znacznie wygodniejsze, wymaga jednak, by wszystkie toleroway jego obecno oraz narzut zwizany z odmiecaniem. Poniewa nie jest to zgodne z wymaganiami projektowymi jzyka C++, zbieracz mieci nie zosta do niego wczony, chocia dostpne s wersje przeznaczone dla C++, dostarczane przez niezalene firmy.

Obsluga wyjtkw sposb traktowania bdw


Od pocztku istnienia jzykw programowania obsuga bdw naleaa do najtrudniejszych zagadnie. Poniewa trudno jest zaprojektowa dobry system obsugi bdw, wiele jzykw programowania po prostu ignoruje ten temat, przerzucajc problem na projektantw bibliotek. Ci ostatni tworzprodki, sprawdzajce si w wielu sytuacjach, ale atwe do ominicia zazwyczaj monaje po prostu zignorowa. Zasadniczym problemem zwizanym z wikszoci systemw obsugi bdw jest fakt, e polegaj one na czujnoci programisty, wyraajcej si w postpowaniu zgodnym z ustalon konwencj, ktra niejest wspierana przezjzyk. Jeeli programista nie zachowa czujnoci, co si czsto zdarza, gdy si spieszy, moe atwo o takim systemie zapomnie. Obsluga wyjtkw (ang. exception handling) wie obsug bdw bezporednio zjzykiem programowania, a czasami nawet z systemem operacyjnym. Wyjtekjest obiektem zgoszonym" (lub wyrzuconym") w miejscu, w ktrym wystpi bd. Nastpnie bd ten moe by wyapany" przez odpowiedni procedur obsugi wyjtku (ang. exception handler), przeznaczondo obsugi odpowiadajcego mu rodzaju bldu. Funkcjonuje to w taki sposb, jak gdyby obsuga wyjtkw bya oddzieln, rwnoleg ciek wykonywania programu, ktra moe by wykorzystana w razie

42

Thinking in C++. Edycja polska pojawienia si usterek. Poniewa obsuga wyjtkw wykorzystuje oddzieln ciek Avykonywania programu, nie musi ona ingerowa w kod, odpowiadajcyjego normalnej pracy. Dziki temu jest on atwiejszy do napisania, nie musimy bowiem bez przerwy sprawdza, czy nie wystpiyjakie bdy. Ponadto zgoszony wyjtek nie : przypomina kodu bdu zwrconego przez funkcj ani znacznika ustawionego przez ni w celu zasygnalizowania powstania bdu, ktre mog by po prostu zignorowa'' ne. Wyjtku nie mona zignorowa istnieje wic gwarancja, e w ktrym miejscu zostanie on obsuony. Wyjtki zapewniaj wreszcie niezawodny sposb wychodze' nia z kryzysw. Zamiast zakoczy dziaanie programu, jestemy czsto w stanie dokona naprawy i przywrci dziaanie programu, co umoliwia tworzenie znacznie bardziej niezawodnych systemw. Wartonadmieni,emechanizmobsugiwyjtkwniejestwyczncechjzykw obiektowych, mimo e wyjtki s w nich zazwyczaj reprezentowane w postaci obiektw. Istniaonjeszczeprzedpowstaniemjzykwobiektowych.

'

'''

' ^'*i.--\> > : W tym tomie ksiki obsuga wyjtkw jest przedstawiona i uywana jedynie w po>; wierzchowny sposb. Dokadny opis wyjtkw znajduje si w tomie drugim (dostp1 ".-, nym w witrynie: http:/flielion.pl/online/thinking/index.html).

Analiza i projektowanie
Model obiektowy stanowi nowy, odmienny sposb mylenia o programowaniu i wiele osb ma pocztkowo kopoty z rozpoczciem pracy nad projektem obiektowym. Kiedy jednak przekonasz si, e niemal wszystko jest obiektem i nauczysz si myle w sposb bardziej obiektowy", bdziesz mg przystpi do tworzenia dobrych" projektw, wykorzystujcych wszystkie zalety programowania obiektowego. Metoda (zwana czsto metodyk) jest zbiorem procedur i heurystyk uywanych do zmniejszenia zoonoci problemu programistycznego. Wiele spord metod programowania obiektowego zostao sformuowanych, zanim jeszcze powstao programowanie obiektowe. W niniejszym podrozdziale przedstawiono korzyci, ktre mona osign, uywajc metodyki. v! w dziedzinie metodyki dokonuje si wielu eksperymentw szczeglnie w programowaniu obieklowym istotne wic jest zrozumienie, jaki problem prbuje rozwiza dana metoda, zanimjeszcze zacznie si bra pod uwagjej zastosowanie. Odnosi si to w szczeglnoci do C++, gdy sam jzyk zosta opracowany z myl o zmniejszeniu zoonoci (w porwnaniu z jzykiem C) zwizanej z wyraaniem programu. Dziki temu moliwe jest ograniczenie koniecznoci tworzenia coraz bardziej zoonych metodyk. Zamiast nich, o wiele prostsze metodyki mog si okaza wystarczajce do rozwizania w jzyku C++ znacznie liczniejszej klasy problemw ni ta, z ktrmona sobie poradzi, uywajc prostych metodyk wjzykach proceduralnych. Istotne jest rwnie zrozumienie, e termin metodyka" jest czsto uywany nieco na wyrost. Wszystko, co czynisz obecnie, projektujc i piszc programy, jest metod. Moe to by twoja wasna metoda, ktrej stosowania moesz nie by wiadomy, lecz

Rozdzia 1. Wprowadzenie do obiektw v

43

jest to proces, przez ktry przechodzisz w czasie tworzenia. Jeeli jest on efektywny, to wykorzystanie go w C++ moe wymaga lylko nieznacznych poprawek. Jeeli na ; tomiast nie jeste zadowolony z wydajnoci swojej pracy oraz sposobu, w jaki po, wstaj lwoje programy, to moesz rozway przyjcie jakiej formalnej metody postpowania lub wybra elementy, nalece do wielu metod formalnych. r,: W trakcie procesu projektowania najwaniejsze jest to, aby si nie zagubi. Mona w cel atwo osign. Wikszo metod analizy i projektowania zostaa opracowana z myl o rozwizywaniu najistotniejszych problemw. Naley pamita, e wikszo projektw nie naley do tej kategorii, mona wic na og z powodzeniem przeprowadzi analiz i wykona projekt, wykorzystujc jedynie wzgldnie may podzbir zalece zwizanych z dan metod7. Jednak niektre sposoby postpowania, niezalenie od tego, jak bd ograniczone, pozwol ci na prac w znacznie lepszym j i . stylu ni gdyby po prostu rozpocz kodowanie. atwo jest rwnie utkn, wpadajc w puapk analitycznego paraliu". Odnosisz wwczas wraenie, e nie moesz posun si naprzd, poniewa nie zostay ukor ' czone wszystkie detale dotyczce biecego etapu. Pamitaj, e bez wzgldu na to, ile v wykonasz analiz, s takie kwestie dotyczce systemu, ktre nie ujawni si a do rozpoczcia projektowania i jeszcze liczniejsze takie, ktre pozostan niewidoczne a do rozpoczcia kodowania albo nawet do chwili, gdy program bdzie ju ukoczony i uruchomiony. Z tego powodu kluczow kwestijest stosunkowo szybkie przejcie etapu analizy i projektowania, a nastpnie implementacja testw zaproponowanego systemu. Ten punktjest warty podkrelenia. Z uwagi na dowiadczenia zjzykami proceduralnymi, godne pochway jest to, e zesp bdzie chcia postpowa ostronie, rozumiejc kady najdrobniejszy szczeg, zanim przystpi do projektowania i implementacji. Z pewnoci podczas tworzenia systemu zarzdzania baz danych warto zrozumie dokadnie potrzeby klienta. Jednake zarzdzanie baz danych naley do klasy problemw dobrze postawionych i gruntownie poznanych w wielu takich programach problemem do rozwizania jest struktura bazy danych. Klasa problemw programistycznych omawianych w biecym rozdziale naley do kategorii nieprzewidywalne" 8 ; w ich przypadku rozwizanie nie polega na prostym przeksztaceniu jakiego dobrze znanego rozwizania. Zawiera natomiast jeden lub wicej nieprzewidywalnych czynnikw" elementw, ktre nie posiadaj dobrze poznanych 9 wczeniejszych rozwiza i dla ktrych niezbdne jest przeprowadzenie bada . Prba gruntownej analizy nieprzewidywalnego problemu, dokonana przed przystpieniem do projektowania i implementacji, koczy si analitycznym paraliem z uwagi Doskonaym tego przykademjest ksika Martina Fowlera UML Distilled (Addison-WesIey, 2000), sprowadzajca, niejednokrotnie przytaczajce, metodyjezyka UML, do dajcego si ogarn podzbioru. W oryginale wystpuje w tym miejscu wprowadzony przez autora termin wild-card", oznaczajcy rzecz, ktrej wasnoci snieznane lub nieprzewidywalne-pr;\p. ttum. Moja wskazwka dotyczca szacowania takich projektwjest nastpujca: Jeeli projekt zawiera wicej nijeden element nieprzewidywalny, nie prbuj nawet planowa, ile czasu zabierzejego wykonanie ani ile bcdzie ono kosztowao, dopki nie stworzyszjego dziaajcego prototypu. Wystpuje w nim zbyt wieIe stopni swobody".

Thinking in C++. Edycja polska

na brak informacji pozwalajcych na rozwizanie tego rodzaju problemu w fazie analizy. Do jego rozwizania konieczne jest powtarzanie caego cyklu, ktre z kolei wymaga podejmowania ryzykownych dziaa (co jest uzasadnione, bowiem prbujemy zrobi co nowego i potencjalne profity maj wiksz warto). Wydaje si, e ryzykowne jest popieszne" przystpienie do wstpnej implementacji, ale moe ona zmniejszy zagroenie nieprzewidywalnego projektu, dajc sposobno wczesnego przekonania si o tym, czy przyjte zaoenia s moliwe do zrealizowania. Opracowywanie produktwjest zarzdzaniem ryzykiem. Czsto proponuje si, by stworzy co z gry przeznaczonego do wyrzucenia". W czasie programowania obiektowego mona nadal usun cz kodu, ale poniewa jest on zamknity wewntrz klas, ju za pierwszym podejciem powstanie z pewnoci kilka uytecznych projektw klas i opracowanych zostanie par wartociowych pomysw, dotyczcych projektu systemu, ktrych nie trzeba bdzie wyrzuca. Tak wic pierwsze, pobiene ujcie problemu nie tylko dostarcza istotnych informacji, przydatnych dla kolejnych cykli analizy, projektowania i implementacji, ale w rezultacie zostaje rwnie utworzony kod, stanowicy dla nich podstaw. Jak ju wspomniano, nawet mimo wykorzystania metodyki zawierajcej ogromn liczb szczegw, zalecajc wykonanie wielu krokw oraz sporzdzenie wielu dokumentw, nadal trudno jest okreli, na czym naley poprzesta. Trzeba wic pamita o tym, czego prbujemy si dowiedzie: 1. Zjakimi obiektami mamy do czynienia (wjaki sposb podzieli projekt na czci skadowe)? 2. Jakie s ich interfejsy ^akie komunikaty trzeba wysya do kadego z obiektw)? Nawet jeeli utworzysz tylko obiekty oraz ich interfejsy, to i tak bdziesz ju mg napisa program. Z rozmaitych powodw moesz potrzebowa wikszej liczby opisw oraz dokumentw ni te, ktre zostay przedstawione, ale na pewno nie poradzisz sobie z mniejsz ich liczb. Proces mona przeprowadzi w piciu etapach, przy czym etap zerowy bdzie stanowi tylko wstpn zgod na pewien sposb organizacji pracy.

Etap 0. Przygotuj plan


Najpierw naley zdecydowa, z jakich etapw bdzie skadaa si praca. Wydaje si to oczywiste (podobnie jak wszystko, co zostao tu przedstawione), a mimo to czsto nie podejmuje si tej decyzji przed przystpieniem do kodowania. By moe twj zamiar polega na tym, aby ,^przystapic od razu do kodowania" (w przypadku dobrego poznania problemu jest to waciwe podejcie). W porzdku ale przynajmniej przyjmij, e taki jest wanie twj plan. Na tym etapie moesz rwnie doj do wniosku, e niezbdna jest jaka dodatkowa forma organizacji pracy, ale nie decyduj jeszcze o wszystkim. Jest zrozumiae, e niektrzy programici preferuj prac na luzie", w trybie nienarzucajcym adnych rozwiza organizacyjnych bdzie gotowe,<jak skocz". Przez pewien

Rozdzia 1. Wprowadzenie do obiektw

45

czas moe to si wydawa atrakcyjne, ale odkryem, e wyznaczenie k i l k u ,,kamieni milowych" pomaga skupi si i zintensyfikowa wysiki zwizane z ich realizacj, zamiast tkwi w deniu do realizacji jedynego celu: ukoczenia projektu". Ponadto powoduje to podzia projektu na atwiejsze fragmenty, dziki czemu sprawia on wraenie przystpniejszego (a poza tym kamienie milowe daj wicej okazji do witowania). Kiedy rozpoczynaem nauk budowy opowiada (dziki czemu napisz kiedy powie), byem przeciwny idei konstrukcji; piszc po prostu przelewaem swoje myli na papier. Pniej jednak zrozumiaem, e kiedy pisz na temat komputerw, konstrukcja jest wystarczajco przejrzysta, abym nie musia si nad ni zastanawia. Wcijednak organizuj swojprac, cho robi tojedynie podwiadomie. Zatemjeeli wydaje ci si, e twj plan polega jedynie na tym, aby rozpocz kodowanie, nadal w pewien sposb pokonujesz kolejne etapy, zadajc sobie pewne pytania i odpowiadajc na nie.

Misja
Kady tworzony system, niezalenie od tego,jakjest skomplikowany, majakie podstawowe zastosowanie, zadanie, suy zaspokojeniu jakiej potrzeby. Abstrahujc od interfejsu uytkownika, szczegw sprztowych i systemowych, zakodowanych algorytmw oraz problemw efektywnoci, odnajdziesz prostracjjego istnienia. Podobnie jak ogln koncepcj hollywoodzkiego filmu, moesz opisa j w kilku zdaniach. Ten czysty opis stanowi punkt wyjcia. Oglna koncepcjajest bardzo wana, bowiem nadaje ton twojemu projektowi jest jego misj. Nie musisz od razu uchwyci caej idei (moesz realizowa kolejne etapy projektu, zanim stanie si ona zupenie oczywista), ale podejmuj prbyjej zrozumienia, dopki nie dotrzesz do sedna. Na przykad analizujc system kontroli ruchu powietrznego moesz zacz od oglnej koncepcji, koncentrujc si na konstruowanym systemie ,,program wiey ledzi ruch samolotu". Rozwa jednak, co si stanie, kiedy ograniczysz system do bardzo maego lotniska prawdopodobnie to czowiek jest na nim kontrolerem ruchu powietrznego lub nie ma ono w ogle adnego kontrolera. Bardziej uyteczny model bdzie nie tyle dotyczy tworzonego rozwizania, ile opisywa sam problem: samolot przylatuje, jest rozadowywany, naprawiany, ponownieadowanyiodlatuje".

Etap 1. Co tworzymy?
W poprzedniej generacji metod projektowania programw (nazywanej projektowaniem proceduralnym) etap ten nazywano tworzeniem analizy wymaga i specyfikacji systemu". atwo si w tym pogubi dokumenty o gronie brzmicych nazwach, ktre mogy rozrasta si do rozmiarw duych projektw, rzdzcych si wasnymi prawami. Jednake intencje byy dobre. Analiza wymaga zakada: przygotuj list wskazwek, dziki ktrym dowiemy si, kiedy praca zostanie ukoczona, a wymagania klienta zaspokojone". Specyfikacja systemu gosi natomiast: oto opis tego, co program bdzie robi (nie wjaki sposb), aby sprosta postawionym wymaganiom". Analiza wymaga jest w rzeczywistoci umow pomidzy

16

Thinking in C++. Edycja polska

tob a klientem (nawet jeeli klient ten pracuje w twojej firmie albo jest on jakim innym obiektem lub systemem). Specyfikacja systemu jest perspektywicznym spojrzeniem na problem" oraz, w pewnym sensie, sposobem ustalenia, czy mona 1 go zrealizowa i ile zajmie to czasu. Poniewa obie te kwestie wymagaj uzgodnie pomidzy ludmi (i zazwyczaj zmieniaj si one w trakcie realizacji projektu), dla oszczdnoci czasu najlepiej zachowaje w najprostszej moliwej formie w idealnym przypadku w postaci list i prostych diagramw. By moe istniejjakie dodatkowe uwarunkowania, wymagajce sporzdzenia obszerniejszych dokumentw, ' T ale dziki utrzymaniu pierwotnej dokumentacji w krtkiej i zwizej postaci moliwe jest jej utworzenie w cigu k i l k u sesji burz mzgw przez prowadzcego spotkanie, tworzcego opracowanie na bieco. Nie tylko umoliwia to uwzgldnienie wszystkich opinii, ale pozwala rwnie na wczenie si do pracy wszyst: kich czonkw zespou jednoczenie, a take sprzyja wzajemnemu zrozumieniu midzy nimi. I, co by moe najwaniejsze, pozwala rozpocz projekt z dudoz entuzjazmu. Konieczne jest skupienie uwagi na istocie tego, co prbujemy osign w biecym etapie: okreleniu, jak powinien zadziaa system. Najbardziej przydatnym do tego i narzdziem jest zbir tzw. przypadkw uycia" (ang. use cases). Przypadki uycia : :,,. identyfikuj kluczowe cechy systemu, ujawniajc niektre z podstawowych klas, kt l re zostan uyte. S one gwnie opisowymi odpowiedziami na pytania w rodzaju 10 : 4 Kto bdzie uywa systemu?" 4 Co aktorzy mog robi z systemem?" 4 W jaki sposb dany aktor wykonuje w systemie dan czynno?" 4 ,,Jak inaczej mogaby dziaa dana operacja, gdyby bya wykonywana przez kogo innego lub gdyby ten sam aktor mia odmienny cel (odkrywanie wariacji)?" 4 Jakie problemy mog si pojawi podczas wykonywania w systemie danej operacji (odkrywanie wyjtkw)?" Jeeli, na przykad, projektujesz bankomat, to przypadek uycia dla jakiego konkretnego aspektu funkcjonowania systemu moe opisywa, co robi bankomat w kadej moliwej sytuacji. Kada z tych sytuacji" jest nazywana scenariuszem, natomiast kady przypadek uycia moe by traktowany jako zbir scenariuszy. Mona uj scenariuszjako pytanie rozpoczynajce si sowami: co zrobi system,jezeli...?". Na przykad, co zrobi bankomat, jeeli klient w cigu ostatnich 24 godzin zdeponowa czek, a rodki znajdujce si najego rachunku, przed uwzgldnieniem tego czeku, nie wystarczajdo wypacenia danej kwoty?". Diagramy przypadkw uycia s celowo proste po to, by zapobiec przedwczesnemu ugrzniciu w szczegach implementacji systemu:

'^Dzikuj za pomoc Jw*MW H. Janettowi.

Rozdzia 1. Wprowadzenie do obiektw

47

Kady patykowaty ludzik reprezentuje ,,aktora", ktrym jest na og czowiek albo jaki inny niezaleny agent (moe by nim nawet inny system komputerowy tak jak w przypadku ATM). Prostokt okrela zakres naszego systemu. Elipsy symbolizuj przypadki uycia, bdce opisami istotnych zada, ktre mona wykona, uywajc systemu. Linie pomidzy aktorami i przypadkami uycia reprezentuj natomiast interakcje. Nie ma znaczenia, w jaki sposb system jest naprawd zaimplementowany, dopki wydaje si taki z punktu widzenia uytkownika. Przypadki uycia nie musz by wyjtkowo skomplikowane, nawet gdy reprezentuj one zoony system. Ich celem jest jedynie prezentacja systemu w taki sposb, jak wyglda on z punktu widzenia uytkownika. Na przykad:

Przypadki uycia tworz specyfikacje wymaga, okrelajc wszystkie moliwe interakcje uytkownika z systemem. Prbujemy zatem okreli peny zbir przypadkw uycia systemu, a gdy t o j u uczynimy, otrzymamy istot tego, co system powinien robi. Korzy wynikajca ze skupienia si na przypadkach uycia polega na tym, e zawsze sprowadzaj nas one do istoty rzeczy, powstrzymujc od zagbiania si w kwestie nieistotne dla wykonania pracy. Oznacza to, e majc peny zbir przypadkw uycia mona opisa system i przej do nastpnego etapu projektowania. Prawdopodobnie nie uda ci si jeszcze zrozumie wszystkiego za pierwszym podejciem, ale nie ma to znaczenia. Wszystko wyjani si w swoim czasie jeeli na tym etapie chcesz uzyska doskona specyfikacj systemu, to z pewnoci na tym utkniesz.

Thinking in C++. Edycja polska Jeeli zdarzyo ci si utkn, to moesz ruszy z miejsca, posugujc si metod umoliwiajc zgrabne przyblienie problemu: opisz system w kilku akapitach, a nastpnie przyjrzyj si rzeczownikom i czasownikom. Rzeczowniki mog odpowiada aktorom, kontekstom przypadkw uycia (np. hol wejciowy") lub przedmiotom przetwarzanym w przypadkach uycia. Czasowniki mog natomiast opisywa interakcje zachodzce pomidzy aktorami i przypadkami uycia, a take okrela kolejne kroki realizacji przypadkw uycia. Zauwaysz rwnie, e obiekty i komunikaty s w fazie projektowania oznaczane za pomoc rzeczownikw i czasownikw. Zwr jednak uwag na to, e przypadki uycia opisuj interakcje pomidzy podsystemami, a zatem technika ,/zeczownikw i czasownikw" moe by uywana jedynie w charakterze instrumentu mylowego ze wzgldu na to, e nie umoliwia ona tworzenia przypadkw uycia". Granica pomidzy przypadkiem uycia i aktorem moe wskazywa na istnienie interfejsu uytkownika, ale nie definiuje ona tego interfejsu. Proces definiowania i tworzenia interfejsw uytkownika zosta opisany w ksice Larry'ego Constantine'a i Lucy Lockwood: Sofnvarefor Use (Addison Wesley Longman, 1999) oraz pod adresem: www.ForUse.com. Chocia jest to czarna magia, istotne jest wprowadzenie w tej fazie jakiego podstawowego harmonogramu. Wiesz ju w przyblieniu, co budujesz, a zatem jeste prawdopodobnie w stanie okreli, ile zajmie to czasu. Odgrywa w tym rol wiele czynnikw. Jeeli przewidujesz dugi termin realizacji, to firma moe zrezygnowa z budowy systemu (i tym samym spoytkowa swoje zasoby na jaki rozsdniejszy cel). Moe si rwnie zdarzy, e kierownik zdecydowa ju, ile powinien trwa projekt i bdzie prbowa wpyn na twoje oszacowanie. Jednak najlepiej jest mie od pocztku rzetelnie przygotowany harmonogram i wczenie upora si z trudnymi decyzjami. Podejmowano wiele prb opracowania technik tworzenia dokadnych harmonogramw (podobnie jak technik przewidywania notowa giedowych), ale prawdopodobnie najlepszym wyjciem jest poleganie na wasnym dowiadczeniu i intuicji. Zastanw si powanie, ile naprawd zajmie to czasu, a nastpnie podwj otrzyman warto i powiksz j o 10 procent. Twoje pocztkowe przekonanie jest prawdopodobnie trafne moesz otrzyma w tym czasie co, co dziaa. Podwojenie czasu spowoduje jednak, e uzyskasz produkt przyzwoitej jakoci, a dodatkowe 10 procent pozwoli na nadanie ostatniego szlifu i uporanie si ze szczegami 1 '. Jakkolwiek zamierzasz to wyjani i niezalenie od lamentw i prb manipulacji, ktre nastpi po przedstawieniu takiego harmonogramu, po prostu wydaje si, e jest on zgodny z prawd.

Wicej informacji na temat przypadkw uycia mona znale w ksikach: Shneider. Winters: Applying Use Cases, Addison-Wesley, 1998 oraz Rosenberg: Use Case Driven Object Modeling with UML, Addison-Wesley, 1999.
1

^Moj wasny pogld na ten temat ulegt ostatnio zmianie. Podwojenie czasu i powikszenie go o 10 procent zapewni ci przyblienie z rozsdndoktadnoci (zakadajc, e w problemie nie wystpuje zbyt wiele czynnikw nieprzewidywalnych), ale nadal bdziesz musial do intensywnie pracowa, aby skoczy prac w tym czasie. Jeeli potrzebujesz czasu, by efekt tej pracy by elegancki, i chcesz si dobrze bawi podczasjej realizacji, to sdz, e waciwym mnonikiem czasu bdzie raczej trzy albo cztery.

Rozdzia 1. Wprowadzenie do obiektw

49

Etap 2. Jak to zrobimy?


Na tym etapie powinnimy utworzy projekt opisujcy, jak wygldaj klasy i w jaki sposb bd one ze sob wsppracowa. Doskonaym narzdziem, umoliwiajcym okrelenie koniecznych klas oraz interakcji pomidzy nimi, s karty CRC (ang. ClassResponsibility-Collaboration klasa-obowizek-wsplpraca). Jedn z zalet tej metodyjest fakt, ejest ona nieskomplikowana technologicznie zaczynamy od zbioru czystych kart o rozmiarach 3 na 5 cali (ok. 7,6x12.7 cm), a nastpnie zapisujemy na nich informacje. Kada karta reprezentuje pojedyncz klas i zapisujemy na niej: 1. Nazw klasy. Powinna by ona zwizana z istot tego, co dana klasa robi tak aby na pierwszy rzut oka wygldao to logicznie. 2. Obowizki" klasy, czyli to, co powinna ona robi. Na og monaje podsumowa, wymieniajc po prostu nazwy funkcji skadowych klasy (poniewa w dobrym projekcie nazwy te powinny by opisowe), ale nie wyklucza to sporzdzenia dodatkowych notatek. Jeeli musisz rozpocz ten proces, spjrz na zagadnienie z punktu widzenia leniwego programisty: jakie obiekty powinny pojawi si w czarodziejski sposb, aby rozwiza twj problem? 3. Wsppraca" klasy, czyli jej interakcja z innymi klasami. Uycie w tym miejscu szerokiego pojcia ,,interakcji"jest celowe moe ona oznacza zarwno czenie klas albo po prostu istnieniejakiego innego obiektu, wiadczcego usugi na rzecz obiektu danej klasy. Wsppraca powinna rwnie uwzgldnia obserwatorw" tej klasy. Na przykadjeeli utworzymy .... klas Fajerwerk, to ktoj bdzie obserwowa: Chemik czy Widz? Pierwszy ';>- z nich bdzie chcia si dowiedziec,jakich chemikaliw uywa si dojego '' konstrukcji, drugi natomiast bdzie zainteresowany kolorami i ksztatami powstaymi podczas eksplozji. By moe wydaje ci si, e karty powinny by wiksze, biorc pod uwag wszystkie informacje, ktre chciaby na nich umieci. S one jednak celowo mae, nie tylko po to, by zachowa niewielkie rozmiary klas, ale rwnie po to, by uniemoliwi ci zbyt wczesne zagbianie si w szczegy. Jeeli nie potrafisz zmieci wszystkich niezbdnych informacji na temat klasy na malej kartce, oznacza to, e klasa ta jest zbyt zoona (opisae j zbyt szczegowo albo te powiniene utworzy wicej ni jedn klas). Idealna klasa powinna by zrozumiaa na pierwszy rzut oka. Ide kart CRC jest pomoc w uzyskaniu pierwszego przyblienia rozwizania umoliwiajcego ogarnicie caoci projektu, a nastpniejego dopracowanie. Jedn z istotnych korzyci kart CRC jest komunikacja. Najlepiej uzyskuje si j na bieco w grupie, bez uycia komputerw. Kada osoba bierze na siebie odpowiedzialno za kilka klas (ktre pocztkowo nie posiadaj nazw; nie ma rwnie na ich temat adnych innych informacji). Uruchamiamy symulacj na ywo", znajdujc za kadym razem rozwizanie jednego scenariusza podejmujemy decyzje, jakie komunikaty s wysyane do poszczeglnych obiektw w celu jego realizacji. W trakcie tego procesu poznajemy niezbdne klasy, wraz z ich zadaniami i wspprac pomidzy nimi, oraz wypeniamy poszczeglne karty. Po przejciu przez wszystkie przypadki uycia powinnimy uzyska pierwsze pene przyblienie projektu.

50

Thinking in C++. Edycja polska Pamitam swoje najbardziej udane dowiadczenia konsultingowe zwizane z tworzeniem wstpnego projektu, zanim rozpoczem uywanie kart CRC. Staem przed zespoem, ktry nigdy wczeniej nie realizowa projektu obiektowego, rysujc obiekty na tablicy. Rozmawialimy na temat tego, w jaki sposb powinny komunikowa si ze sob obiekty, wymazujc niektre z nich i zastpujc je innymi. W rezultacie posugiwaem si ,,kartami CRC" na tablicy. Zesp (ktry wiedzia, jakie zadania ma realizowa system) w rzeczywistoci tworzy projekt by on w wikszym stopniu wasnoci" zespou ni zosta mu podarowany. Moja rola ograniczaa si do kierowania tym procesem poprzez zadawanie waciwych pyta, testowanie zaoe oraz wykorzystywanie uwag zespou do ich modyfikacji. Prawdziwa zaleta tego podejcia polegaa na tym, e zesp uczy si realizacji projektu obiektowego nie za pomoc przegldania abstrakcyjnych przykadw, ale pracujc nad projektem, ktry by dla niego wwczas najbardziej interesujcy czyli swoim wasnym. Kiedy rozpoczynasz prac z zestawem kart CRC, moesz pokusi si o bardziej forrnalny opis swojego projektu, wykorzystujc notacj UML13. Nie musisz uywa jzyka UML, ale moe on by pomocny szczeglnie gdy zamierzasz powiesi na cianie diagram, ktry kady mgby przemyle, co jest dobrym pomysem. Rozwizaniem alternatywnym w stosunku do UML s tekstowe opisy obiektw oraz ich interfejsw, albo w zalenoci od uywanego przez ciebie jzyka programowania sam kod programu 14 . Jzyk UML pozwala rwnie na stosowanie dodatkowej notacji w postaci diagramw, umoliwiajcych opis dynamicznego modelu systemu. Jest ona przydatna w przypadkach, w ktrych przejcia pomidzy stanami systernu lub podsystemu s na tyle znaczce, e konieczne jest przygotowanie dla nich odrbnych diagramw (na przykad w systemie sterujcym). Moe ponadto wystpi konieczno opisania struktur danych w systemach lub podsystemach, w przypadku ktrych dane s czynnikiem dominujcym (na przykad w bazach danych). Zakoczysz drugi etap, kiedy opiszesz obiekty oraz ich interfejsy. A przynajmniej wikszo z nich jest bowiem zazwyczaj kilka takich, o ktrych zapomniano i ktre ujawni si dopiero w trzecim etapie. Ale nie ma si czym przejmowa interesuje ci tylko to, by znale w kocu wszystkie obiekty. Dobrze jest zrobi to na wczesnych etapach projektowania, ale programowanie obiektowe udostpnia konstrukcje, dziki ktrym nie jest rwnie problemem pniejsze ich odkrycie. W istocie projektowanie obiektw odbywa si zazwyczaj w piciu etapach, w czasie caego procesu tworzenia programu.

, /

., i .,; i.

Wf etapw projektowania obiektw


Czas projektowania obiektu nie jest ograniczony do okresu, w ktrym pisanyjest program. Projektowanie to przebiega natomiast w kolejnych etapach. Dziki takiej perspektywie przestaniesz oczekiwa natychmiastowego osignicia ideau uwiadomisz sobie, e zrozumienie tego, co robiobiekty ijak powinny one wyglda, przychodzi

''

Pocztkujcym polecam wymienionju wczeniej pozycj UML Distilled.


14

Pyton (www.Python.org))est czsto wykorzystywany w charakterze wykonywalnego pseudokodu".

Rozdzia 1. * Wprowadzenie do obiektw

51

dopiero po pewnym czasie. Taka obserwacja dotyczy rwnie projektowania rnego rodzaju programw wzorcowa posta okrelonego rodzaju programu powstaje w wyniku podejmowanych wielokrotnie prb rozwizania problemu (wzorce projektowe zostay omwione w drugim tomie ksiki). Rwnie obiekty majswoje wzorce, ujawniajce si poprzez ich zrozumienie i wielokrotne uywanie. 1. Znajdowanie obiektw. Ten etap nastpuje w czasie pocztkowej analizy programu. Obiekty mogzosta znalezione dziki poszukiwaniu: zewntrznych czynnikw i ogranicze, powtarzajcych si elementw systemu oraz najmniejszychjednostek pojciowych. Niektre z obiektw soczywiste, jeeli dysponujemyju zbiorem bibliotek klas. Podobiestwa midzy klasami, wskazujce naobecno klas podstawowych oraz dziedziczenia, mogpojawi si wczeniej lub pniej w trakcie procesu projektowania. 2. Skadanie obiektw. W czasie tworzenia obiektu odkryjesz potrzeb dodania do niego skadowych, niewidocznjeszcze w chwilijego znalezienia. Wewntrzne wymagania obiektu mog wymusza wspomaganie go przez inne klasy. 3. Konstrukcja systemu. Na tym etapie ponownie mog si pojawi dodatkowe wymagania dotyczce obiektu. W miarjak powiksza si twoja wiedza, zmieniasz stopniowo swoje obiekty. Potrzeba komunikacji i wzajemnych powiza z innymi obiektami wystpujcymi w systemie moe powodowa zmian wymaga twojej klasy lub pociga za sob konieczno tworzenia nowych klas. Na przykad moesz odkry potrzeb utworzenia klas pomocniczych, takichjak lista wizana, zawierajcych niewiele informacji $^y o stanie (lub w ogle ich niezawierjcych) i wspomagajcych po prostu dziaanie innych klas. 4. Rozbudowa systemu. W trakcie dodawania do systemu nowych funkcji moesz zauway, e twj wczeniejszy projekt nie umoliwia atwej rozbudowy systemu. Bogatszy o t wiedz, moesz przebudowa cz systemu, na przykad dodajc do niego nowe klasy lub hierarchi klas. 5. Wielokrotne wykorzystywanie obiektw. Jest to prawdziwa prba wytrzymaociowa kIasy. Jeeli kto prbuje wykorzysta klas w zupenie nowych warunkach, to prawdopodobnie odkryjejakiejej wady. W miar jak bdziesz modyfikowa t klas, przystosowujcjdo kolejnych programw, jej oglne podstawy stan si coraz bardziej przejrzyste, a w kocu uzyskasz typ naprawd odpowiedni do wielokrotnego wykorzystania. Nie naleyjednak spodziewa si, e wikszo obiektw wchodzcych w skad projektu systemu bdzie nadawaa si do ponownego uytku jest oczywiste, e wiele obiektw zostanie opracowanych w sposb specyficzny dla konkretnego systemu. Typy nadajce si do wielokrotnego wykorzystania wystpujrzadziej, a ponadto po to, by mona ich wielokrotnie uywa, musz one rozwizywa bardziej oglne problemy.

Wskazwki dotyczce projektowania obiektw


Przedstawione poniej etapy zawieraj wskazwki, ktre przydadz ci si w chwili, gdy zaczniesz si zastanawia nad projektowaniem klas:

52

Thinking in C++. Edycja polska

1. Niech konkretny problem spowoduje utworzenie klasy, a nastpnie pozwl jej rosn i dojrzewa, rozwizujc inne problemy. 2. Pamitaj o tym, e odkrywanie niezbdnych k!as (oraz ich interfejsw) stanowi gwncz projektowania systemu. Gdyby miaju te klasy, byby to atwy projekt. 3. Nie zmuszaj si do zrozumienia wszystkiego ju na samym pocztku ucz si w trakcie pracy. W kocu i tak to nastpi. i;" 4. Zacznij programowa stwrz co dziaajcego, dziki czemu bdziesz mg potwierdzi poprawno swojego projektu lub wykaza jego bdno. Nie obawiaj si, e skoczy si to utworzeniem popltanego kodu, napisanego w stylu proceduralnym klasy dzielbowiem problem na czci, pozwalajc zapanowa zarwno nad anarchi, jak i entropi. Ze klasy nie psuj dobrych klas. 5. Zawsze zachowuj prostot. Lepsze s mae i proste obiekty, o oczywistym przeznaczeniu, ni wielkie i skomplikowane interfejsy. Kiedy przychodzi czas podejmowania decyzji, uywaj brzytwy Ockhama rozwa wszystkie moliwoci i wybierz t, ktrajest najprostsza, poniewa proste klasy s niemal w kadym przypadku najlepsze. Rozpocznij od maej i prostej klasy jej interfejs bdziesz mg rozwin, gdyju zrozumieszjlepiej, jednake, w miar upywu czasu, coraz trudniej bdzie usun z klasy jakiekolwiek elementy.

Etap 3. Budujemy jdro


Jest to wstpne przejcie ze zgrubnego projektu do kompilacji i wykonania gwnej czci kodu, ktry bdzie mona testowa, dziki czemu w szczeglnoci potwierdzona lub zanegowana zostanie poprawno przyjtej architektury. Nie jest to proces jednoprzebiegowy, a raczej pocztek serii krokw, ktre, jak si przekonamy w czwartym etapie, umoliwi iteracyjn budow systemu. Twoim celem jest znalezienie jdra architektury systemu, ktre musi zosta zaimplementowane w celu utworzenia dziaajcego systemu, niezalenie od tego, jak bardzo jest on niekompletny na obecnym, pocztkowym etapie. Tworzysz szkielet, ktry bdzie nastpnie w trakcie kolejnych iteracji stopniowo rozbudowywany. Przeprowadzisz pierwsze z wielu integracji i testw systemu, a take poinformujesz udziaowcw, jak bdzie wyglda ich system i jak zaawansowane s prace nad nim. W idealnym przypadku ujawnisz rwnie niektre z istotnych zagroe projektu. Prawdopodobnie odkryjesz take modyfikacje oraz udoskonalenia, ktre mog by wprowadzone do pierwotnej architektury systemu kwestie, o ktrych nie dowiedziaby si, gdyby go nie zaimplementowa. Elementem budowy systemu jest rzeczywiste potwierdzenie jego zgodnoci z analiz wymaga oraz specyfikacj (niezalenie od tego, w jakiej istniej one postaci), uzyskane z wyniku testw. Upewnij si, e testy zweryfikuj rwnie wymagania i przypadki uycia. Gdy jdro twojego systemu bdzie ju stabilne, mona przej do nastpnego etapu i zwikszy niecojego funkcjonalno.

Rozdzia 1. Wprowadzenie do obiektw

53

Etap 4. lteracje przez przypadki uycia


Kiedy dziaaju zasadniczy szkielet systemu, dodanie do niego kadego zbioru cech jest samo w sobie niewielkim projektem. Zbir cech dodawany jest do systemu w czasie iteracji wzgldnie krtkiego procesu twrczego. Jak dugo trwa iteracja? W idealnym przypadku kada iteracja zajmuje od jednego do trzech tygodni (czas ten moe si rni w zalenoci od jzyka implementacji). Po zakoczeniu tego okresu uzyskujemy zintegrowany, przetestowany system, o funkcjonalnoci wikszej ni uprzednio. Jednake najbardziej interesujc kwesti jest podstawa kadej iteracji pojedynczy przypadek uycia. Kady przypadek uycia jest pakietem powizanych ze sob funkcji, ktre wbudowuje si w system rwnoczenie, w trakcie pojedynczej iteracji. Pozwala to nie tylko wypracowa lepszy pogld na temat tego, jaki powinien by zakres przypadkw uycia, ale stanowi rwnie potwierdzenie susznoci ich idei z uwagi na to, e nie tylko nie zostaj one odrzucone, po zakoczeniu etapw analizy i projektowania, ale staj si wrcz fundamentalnjednostk rozwoju w procesie tworzenia oprogramowania. Iteracje kocz si po osigniciu docelowej funkcjonalnoci albo gdy nadejdzie narzucony z gry ostateczny termin wykonania pracy, a klient moe zadowoli si aktualn wersj systemu (pamitaj, e oprogramowanie jest biznesem opartym na subskrypcji). Poniewa proces ten jest iteracyjny, istnieje wiele sposobnoci do wydania produktu klientowi nie jest to jaki jedyny punkt kocowy. Projekty typu opensource funkcjonuj wycznie w rodowisku iteracyjnym, o silnym sprzeniu zwrotnym, cojest z pewnocirdem ich sukcesu. Iteracyjny proces rozwoju oprogramowania jest korzystny z wielu powodw. Umoliwia wczesne ujawnienie i rozwizanie istotnych zagroe projektu, klienci maj wiele okazji do zmiany swoich zamierze, wiksza jest satysfakcja programistw, a projektem mona kierowa z wiksz precyzj. Jednake istotn dodatkow korzycijest sprzenie zwrotne z udziaowcami, ktrzy widzc aktualny stan produktu, dokadnie wiedz, najakim etapie znajduje sijego realizacja. Moe to zmniejszy lub zupenie wyeliminowa potrzeb organizowania jaowych spotka, dotyczcych stanu zaawansowania pracy, a take zwikszy zaufanie i wsparcie ze strony udziaowcw.

Etap 5. Ewolucja
Ten etap cyklu projektowego jest tradycyjnie okrelany mianem ,,pielegnacji" (ang. maintenance) sowem-wytrychem, ktry moe oznacza waciwie wszystko poczwszy od doprowadzenia do dziaania zgodnie z pierwotnymi zaoeniami" przez ,,dodanie funkcji, o ktrych klient zapomnia nadmieni" po bardziej tradycyjne poprawianie ujawnionych bdw" oraz dodawanie, w miar potrzeby, nowych funkcji". Tak wiele bdnych znacze przypisywano pojciu pielgnacji", e nie oddaje ono trafnie istoty rzeczy czciowo z tego powodu, e sugeruje, i w rzeczywistoci zosta napisany program i teraz trzeba jedynie wymienia w nim czei, oliwi go i pilnowa, aby nie zardzewia". By moe istnieje wic lepszy termin, okrelajcy to, z czym mamy w rzeczywistoci do czynienia.

54
15

Thinking in C++. Edycja polska W tej ksice posu si terminem ewolucja . Oznacza on: nie zrobisz od razu wszystkiego dobrze, wic pozostaw sobie moliwo, e gdy nabierzesz dowiadczenia, powrcisz i wprowadzisz zmiany". By moe bdziesz musia wprowadzi wiele zmian, w miar uczenia si i gbszego zrozumienia problemu. Elegancja, ktr osigniesz a do uzyskania dobrego rozwizania, opaci si zarwno w perspektywie krtkoterminowej, jak i w dugoterminowej. W trakcie ewolucji twj program z dobrego zamienia si we wspaniay, a zagadnienia, ktrych nie rozumiae przy pierwszym podejciu, staj si jasne. Jest to rwnie proces, w czasie ktrego twoje klasy mog ewoluowa z moliwych do wykorzystania w pojedynczym projekcie do zasobw nadajcych si do wielokrotnego uycia. Zrobienie tego dobrze" nie opisujejedynie sytuacji, w ktrej pogram dziaa zgodnie z wymaganiami oraz przypadkami uycia. Oznacza to rwnie, e wewntrzna struktura kodu jest logiczna, a jej poszczeglne elementy wydaj si dobrze do siebie pasowa, nie zawiera ona niezgrabnych struktur, przeronitych obiektw albo niezrcznie obnaonych fragmentw kodu. Musisz mie rwnie wraenie, e struktura programu przetrwa zmiany, ktre niewtpliwie nastpi w okresie jego istnienia, oraz e modyfikacje te bd mogy zosta wprowadzone bez trudnoci. Nie jest to atwe zadanie. Musisz nie tylko zrozumie, co tworzysz, ale rwnie to, w jaki sposb program bdzie si rozwija (co nazywam wektorem zmianib). Na szczcie obiektowe jzyki programowania s szczeglnie predysponowane do wspomagania tego rodzaju cigych modyfikacji granice tworzone przez obiekty s tym, co powstrzymuje struktur przed rozpadem. Umoliwiaj one rwnie wprowadzanie zmian takich, ktre wydawayby si drastyczne w programie proceduralnym bez wywoywania trzsienia ziemi w twoim kodzie. W istocie wsparcie dla ewolucji jest by moe najwaniejsz zalet programowania obiektowego. W trakcie ewolucji tworzysz co, co przynajmniej stanowi przyblienie pocztkowego zamysu. Nastpnie sprawdzasz jeszcze wszystko dokadnie, porwnujesz z wymaganiami i wykrywasz wszelkie niedoskonaoci. Nastpnie cofasz si i dokonujesz poprawek ponownie projektujc i implementujc fragmenty programu, ktre nie dziaajdobrze 1 7 . Obecnie do rozwizania problemu (lubjego czci) moesz potrzebowa wielu podej, zanim wpadniesz na waciwe rozwizanie (na og pomocne w tym przypadku jest zapoznanie si z wzorcami projektowymi, opisanymi w drugim tomie ksiki). ;

' Przynajmniej jeden z aspektw ewolucji zosta opisany w ksice Martina Flowera Refactoring: improving the design ofexisting code (Addison-Wesley, 1999). Uprzedzam, e ksika wykorzystuje przykady napisane wycznie wjzyku Java. Pojcie tojest omawiane szczegowo w rozdziale Design Paiterns, znajdujcym si w drugim tomie ksiki. Jest to co w rodzaju szybkiego przygotowywania prototypw", w przypadku ktrego miaby zamiar zbudowa naprdce wersj umoliwiajcpoznanie systemu, a nastpnie wyrzuci przygotowany w ten sposb prototyp i zbudowa gojak naley. Problem z szybkim przygotowywaniem prototypw polega na tym, e zamiast wyrzuci prototyp, budowano na nim system. W poczeniu z brakiem struktury w programowaniu strukturalnym, prowadzio to do stworzenia niechlujnych systemw,' ktrych pielgnacja bya kosztowna.

Rozdzia 1. Wprowadzenie do obiektw

55

Ewolucja zachodzi rwnie wtedy, gdy budujesz system, sprawdzasz, czy spenia on wymagania, a nastpnie odkrywasz, e nie spenia on zaoe. Gdy przeanalizujesz system w dziaaniu, zauwaasz, e w istocie chciae rozwiza zupenie inny problem. Jeeli mylisz, e ewolucja przebiega bdzie w taki wanie sposb, to powiniene zbudowa swoj pierwsz wersj tak szybko, jak to moliwe, dziki czemu przekonasz si, czy odpowiada oczekiwaniom. Warto przede wszystkim zapamita, e domylnie z definicji, w rzeczywistoci jeeli modyfikujesz klas, to jej klasy podstawowe i klasy pochodne bd nadal dziaa. Nie musisz obawia si modyfikacji (zwaszcza gdy dysponujesz wbudowanym zbiorem testw pozwalajcych na sprawdzenie poprawnoci modyfikacji). Modyfikacja niekoniecznie musi oznacza zepsucie programu, a ponadto wszelkie zmiany bd w rezultacie ograniczone do klas podrzdnych i (lub) specyficznej wsppracy pomidzy zmienianymi klasami.

Planowanie si optaca
Oczywicie, nie zbudowaby domu bez wielu starannie nakrelonych planw. Jeeli budujesz taras lub bud dla psa, twoje plany nie bd tak szczegowe, ale prawdopodobnie rozpoczniesz prac od jakiego rodzaju szkicu, ktrym bdziesz si kierowa. Projektowanie oprogramowania popadao ze skrajnoci w skrajno. Przez dugi czas programowanie nie miao specjalnych ram organizacyjnych, ale wwczas zaczy upada due projekty. W rezultacie skoczylimy na metodykach, ktre miay gron liczb struktur i szczegw, pomylanych przede wszystkim z myl o tych obszernych projektach. Te metodyki s zbyt odstrczajce mona by odnie wraenie, e masz spdzi cay swj czas piszc dokumentacj i nie majc chwili na programowanie (czsto do tego dochodzio). Sugeruj poredni drog; uyj podejcia, ktre odpowiada twoim potrzebom (i osobicie tobie). Niezalenie od tego, jak maym postanowisz go uczyni, jakikolwiek pIan bdzie stanowi znaczne udoskonalenie twojego projektu w stosunku do braku planu w ogle. Pamitaj, e wedug wikszoci szacunkw przeszo 50 procent projektw upada (niektre oszacowania wskazuj 70 procent)! Postpujc zgodnie z planem najlepiej prostym i zwizym i tworzc struktur projektu przed rozpoczciem kodowania, odkryjesz, e rzeczy cz si ze sob znacznie atwiej ni gdy rzucisz si na gbok wod i zaczniesz pisanie kodu. Sprawi ci to rwnie wielk satysfakcj. Z mojego dowiadczenia wynika, e uzyskanie eleganckiego rozwizania jest gboko satysfakcjonujce na zupenie nowej paszczynie. Wydaje si blisze sztuce ni technologii. I wreszcie elegancja zawsze si opaca niejest tylko sztukdla sztuki. Taki programjest nie tylko atwiej zbudowa i usun z niego bdy, ale rwnie zrozumie i pielgnowa, a to przynosi zawsze zyski.

Programowanie ekstremalne
Uczyem si technik analizy i projektowania od czasu studiw, cho z przerwami. Koncepcja programowania ekstremalnego (ang. extreme programming XP) jest

Thinking in C++. Edycja polska

najbardziej radykaln i inspirujc ze znanych mi technik. Jej opis mona znale w ksice Kenta Becka Extreme Programming Explained (Addison-Wesley, 2000) lub w Internecie pod adresem www.xprogramming.com. Programowanie ekstremalne jest zarwno filozofi dotyczc sposobu pracy programisty,jak i zbiorem wskazwek, wjaki sposb naleyjwykonywa. Niektre z tych wskazwek znajduj odzwierciedlenie w innych, wczeniejszych metodykach, ale dwoma, moim zdaniem najistotniejszymi i najbardziej wyrniajcymi si, s: najpierw napisz testy" oraz programowanie w parach". Beck, mimo e kadzie duy nacisk na cay proces, wskazuje, e przyjcie tylko tych dwch elementw spowoduje znaczne zwikszenie produktywnoci i niezawodnoci.

Najpierw napisz testy


Testowanie jest tradycyjnie zepchnite na ostatni etap projektu, gdy wszystko ju dziaa, ale trzeba jeszcze to zrobi tak dla pewnoci". Wynika z tego jego niski priorytet, a osoby specjalizujce si w testowaniu s traktowane po macoszemu czsto zsyia si ich gdzie do piwnicy, z dala od prawdziwych programistw". Zespoy testerw odpacaj piknym za nadobne, ubierajc si na czarno i rechoczc z uciechy za kadym razem, gdy znajdjaki bd (szczerze mwic, sam miaem to samo uczucie znajdujc bdy w kompilatorach C++). Programowanie ekstremalne rewolucjonizuje koncepcj testowania poprzez przyznanie mu rwnego (a nawet wyszego) priorytetu ni kodowaniu. W rzeczywistoci, testy spisane zam'mjeszcze powstanie kod, ktry bdzie testowany, i zostajna trwale do niego przypisane. Testy musz by wykonane poprawnie ilekro jest przeprowadzana integracja projektu (czasami czciej ni raz dziennie). Rozpoczcie pracy od pisania testw pociga za sobdwa bardzo istotne skutki. Po pierwsze wymusza przejrzyst definicj interfejsu klasy. Czsto proponuj, by jako narzdzie pomocne przy prbie projektowania systemu wyobrazi sobie doskona klas, rozwizujc konkretny problem". Strategia testowania w programowaniu ekstremalnym posuwa si jeszcze dalej okrela precyzyjnie, jak musi wyglda klasa z punktu widzenia jej uytkownika i jak dokadnie ma ona dziaa. I robi to w przejrzysty sposb. Mona napisa cae opowiadanie lub narysowa dowolne diagramy, opisujce, w jaki sposb zachowuje si klasa i jak powinna ona wyglda, ale nic nie jest tak rzeczywiste, jak zestaw testw. Te pierwsze s tylko listami ycze, natomiast testy stanowi kontrakt uwierzytelniony przez kompilator i dziaajcy program. Trudno wyobrazi sobie bardziej konkretny opis klasy ni testy. W czasie pisania testw jeste zmuszony do gruntownego przemylenia klasy, dziki czemu czsto odkryjesz takie konieczne do jej dziaania funkcje, ktre mona przeoczy podczas eksperymentw mylowych z diagramami UML, kartami CRC, przypadkami uycia itp. Druga istotna korzy, zwizana z tworzeniem testw na samym pocztku, wynika z ich uruchamianiapo kadej kompilacji programu. Stanowione uzupenienie testw przeprowadzanych przez kompilator. Jeeli spojrzysz z tej perspektywy na ewolucj

j
ii

Rozdzia 1. Wprowadzenie do obiektw

57

jzykw programowania, zauwaysz, e istotne udoskonalenia dotyczce technologii jzykw s w istocie zwizane z testowaniem. Asembler sprawdza jedynie skadni, ale jzyk C narzuci pewne ograniczenia semantyczne, zapobiegajce popenianiu okrelonych typw bdw. Jzyki obiektowe naoyyjeszcze wiksz lic/b ogranicze semantycznych, ktre jeli si nad nimi zastanowi s w istocie pewnymi rodzajami testw. Pytania w rodzaju ,,Czy ten typ danych jest uywany waciwie?", Czy ta funkcja jest wywoywana poprawnie?" s testami, przeprowadzanymi przez kompilator lub system uruchomieniowy. Widzimy rezultaty wbudowania tych testw w jzyk ludzie s w stanie pisa bardziej zoone systemy i uruchamia je znacznie szybciej, wkadajc w to o wiele mniej wysiku. amaem sobie gow, dlaczego tak jest, ale teraz rozumiem, e dzieje si tak dziki testom jeli popeniasz bd, a siatka zabezpieczajca, utworzona przez wbudowane testy, informuje ci, e pojawi si problem i wskazuje miejsce, w ktrym wystpi. Jednake wbudowane testy, moliwe do przeprowadzenia na podstawie projektu jzyka, dochodz tylko do tego miejsca. W pewnym momencie musisz wkroczy i doda pozostae testy, by w poczeniu z kompilatorem i systemem uruchomieniowym tworzyy one peny zestaw, sprawdzajcy poprawno caego programu. I czy majc taki kompilator, zagldajcy ci przez rami, nie chciaby, aby testy te rwnie pomagay ci od samego pocztku? To wanie dlatego zaczynasz od pisania testw, a nastpnie uruchamiasz je automatycznie podczas kadej kompilacji systemu. Twoje testy stajsi rozszerzeniem siatki zabezpieczajcej, udostpnianej przezjzyk. W kwestii uywania coraz bardziej efektywnych jzykw programowania odkryem rwnie i to, e jestem skonny do przeprowadzania coraz mielszych eksperymentw, poniewa wiem, e jzyk ustrzee mnie przed strat czasu na tropienie bdw. Schemat testowania programowania ekstremalnego wykonuje to samo w stosunku do caego projektu. Poniewa wiesz, e napisane przez ciebie testy zawsze wychwyc wszystkie problemy, ktre sam wprowadzisz (zastanawiajc si nad nimi, regularnie uzupeniasz je o nowe testy), moesz dokonywa wielkich zmian, kiedy tylko zajdzie taka potrzeba, nie martwic si przy tym o to, e pogrysz cay swj projekt w zupenym chaosie. Jest to niewiarygodnie potne narzdzie.

Programowanie w parach
Programowanie w parach koliduje ze skrajnym indywidualizmem, ktry wpajano nam od koyski, poprzez szko (kiedy samodzielnie osigalimy sukcesy lub odnosilimy poraki, a wsplna praca z ssiadem bya traktowana jako oszustwo") oraz media szczeglnie hollywoodzkie filmy, ktrych bohater walczy zazwyczaj przeciwko bezmylnemu konformizmowi 1 8 . Programici rwnie s uwaani za wzorcowych indywidualistw kowboi kodowania", jak mawia o nich Larry Constantine. Nawet zwolennicy programowania ekstremalnego, ktrzy sami tocz batali przeciwko konwencjonalnemu myleniu, prezentuj pogld, e kod powinien by pisany przez dwie osoby, pracujce przy tym samym komputerze. Sugeruj take, e praca powinna odbywa si w pomieszczeniu, w ktrym znajduje si grupa stacji roboczych bez
18.

Mimo ejest to by moe raczej amerykaski punkt widzenia, hollywoodzkie opowieci docieraj wszdzie.

>8

Thinking in C++. Edycja polska

cianek dziaowych, tak bardzo lubianych przez projektantw wntrz. Beck twierdzi :, .>; nawet, e pierwsz czynnoci zwizan z wdroeniem programowania ekstremalnego jest wzicie do rki rubokrta oraz klucza francuskiego i zdemontowanie wszystkiego, co zawadza'^. Wymaga tojednak obecnoci szefa, ktry potrafi odeprze ataki dziau administracyjnego. Korzyci wynikajc z programowania w parach jest to, e jedna z osb zajmuje si pisaniem kodu programu, podczas, gdy druga go analizuje. Myliciel" ma na uwadze obraz caoci nie tylko aktualnie rozwizywany problem, ale rwnie zalecenia programowania ekstremalnego. Kiedy dwie osoby pracuj razem, jest mniej prawdopodobne, e jedna z nich zamie zasady, postanawiajc na przykad: nie chc zaczy;.! na od pisania testw". Poza tym, gdy osoba piszca kod utknie na czym, moe zamieni si miejscami z mylicielem". Jeeli obaj nie bd sobie mogfi, z czym poradzi, to ich uwagi zostan by moe przypadkowo usyszane przez kogo pracujcego w tym samym pomieszczeniu, kto bdzie potrafi im pomc. Praca w parach nadaje czynnociom projektowym waciwy kierunek. I, co prawdopodobnie waniej; ' sze, czyni programowanie zajciem znacznie bardziej towarzyskim i zabawnym. Zaczem wykorzystywa programowanie w parach w trakcie wicze na jednym --..,- ..:; z moich seminariw i wydaje mi si, e znacznie wzbogacio to dowiadczenia r r wszystkichjegouczestnikw.

Maczego C++ odnosi sukcesy?


Jednym z powodw sprawiajcych, e jzyk C++ cieszy si takim powodzeniem jest fakt, e nie powsta on jedynie w celu przeksztacenie jzyka C w jzyk obiektowy (chocia od tego si wanie zaczo), ale rwnie rozwizania wielu innych problemw, z ktrymi zmagajsi dzi projektanci szczeglnie ci, ktrzy zainwestowali ju sporo w C. Jzyki obiektowe prezentoway tradycyjnie podejcie, zgodnie z ktrym naley odrzuci ca swoj dotychczasow wiedz i zacz od zera posugujc si nowym zestawem poj i now skadni, argumentujc, e lepiej jest w duszej perspektywie pozby si balastu dowiadcze wyniesionych z jzykw proceduralnych. Niewykluczone, e na dusz met jest to prawd. Jednake w krtszej perspektywie wiele z tych dowiadcze okazuje si przydatne. By moe najbardziej wartociowymi elementami nie jest wcale fundament istniejcego kodu (ktry, uywajc odpowiednich narzdzi, mona przetumaczy), lecz istniejcy fundament intelektualny. Jeeli czynnie programujc w C, musisz odrzuci wszystko to, co wiesz o tymjzyku, po to, by przyswoi sobie nowy jzyk. Stajesz si natychmiast znacznie mniej produktywny na okres wielu miesicy dopki twj umys nie dostosuje si do nowego schematu. Natomiast w przypadku, gdy moesz jako punkt wyjcia wykorzysta swojobecnwiedz na tematjzyka C,jestes w stanie zarwno posugujc si

Wtaczajc to (w szczeglnoci) system naganiajcy. Pracowaem kiedy w firmie, ktra nalegaia na naganianie rozmw telefonicznych, przychodzcych do kadego kierownika, co bez przerwy przeszkadzato nam w pracy (ale szefom nie przyszo do gowy wyczenie czego tak wanego, jak system naganiajcy). W kocu, kiedy nikt nie patrzy, poprzecinaem przewody gonikowe.

Rozdzia 1. Wprowadzenie do obiektw

59

t wiedz nadal wydajnie pracowa, jak i rwnoczenie przechodzi do wiata programowania obiektowego. Jako e kady ma wasny model programowania, przejcie to jest ju i tak wystarczajco chaotyczne nawet bez dodatkowego obcienia w postaci rozpoczynania od nauki nowego modelu jzyka od podstaw. Tak wic, w najwikszym skrcie, powodem, ktry sprawia, e C++ cieszy si powodzeniem, jest ekonomia przejcie do programowania obiektowego nadal kosztuje, ale C++ moe okaza si mniej kosztowny 20 . Celem C++jest zwikszenie produktywnoci. Monaj osign na wiele sposobw, alejzyk zaprojektowano w taki sposb, by pomaga ci i zarazemjak najmniej przeszkadza nieuzasadnionymi reguami czy te koniecznoci wykorzystywania okrelonego zbioru cech. Jzyk C++ opracowano po to, by by uyteczny. Decyzje projektowe, ktre legy u podstaw C++, miay na celu udostpnienie programicie maksymalnych korzyci (przynajmniej z punktu widzeniajzyka C).

Lepsze C
Natychmiast zyskujesz nawet jeeli nadal piszesz kod w jzyku C poniewa C++ domyka wiele luk zawartych w jzyku C, a take zapewnia lepsz kontrol typw oraz analiz dokonywan w czasie kompilacji. Jeste zmuszony do deklarowania funkcji, dziki czemu kompilator moe nadzorowa ich uywanie. Konieczno wykorzystywania preprocesora zostaa praktycznie wyeliminowana w stosunku do podstawiania wartoci i makr, co usuno grup trudnych do wykrycia bdw. C++ posiada mechanizm nazywany referencjami, umoliwiajcy wygodniejsz obsug adresw argumentw funkcji oraz zwracanych wartoci. Obsuga funkcji zostaa udoskonalona dziki przecianiu funkcji, pozwalajcemu na uywanie tej samej nazwy w stosunku do rnych funkcji. Cecha nazywana przestrzeniami nazw poprawia rwnie kontrol nazw. Istnieje rwnie szereg drobnych udoskonale, zwikszajcych bezpieczestwo C.

Zaczte si ju uczy
Problemem z nauk nowego jzyka jest produktywno. adna firma nie moe sobie pozwoli na gwatowny spadek produktywnoci inyniera zajmujcego si programowaniem, tylko dlatego, e uczy si on nowegojzyka. C++jest rozszerzeniemjzyka C, a nie zupenie now skadni i modelem programowania. Pozwala to na dalsze tworzenie uytecznego kodu i jednoczesne stopniowe wprowadzanie nowych cech w miar ich poznawania i rozumienia. By moe jest to jeden z najwaniejszych powodw sukcesu C++. W dodatku wikszo z caego kodu napisanego przez ciebie w jzyku C nadal dziaa w C++, lecz z uwagi na to, e kompilator C++jest bardziej drobiazgowy, czsto ponowne skompilowanie programu w C++, ujawnia znajdujce si w nim ukryte bdyjzyka C.
0

p rowiedziaem ,,moze", poniewa, z uwagi na zoono C++, w rzeczywistoci tasze moe okaza si przejcie najzyk Java. Jednake na decyzje, ktryjezyk wybra, wpywa wiele czynnikw i w ksice zakadam, e wybrae C++.

60

Thinking in C++. Edycja polska

Efektywno
! Czasami dobrze jest zrezygnowa z szybkoci wykonania programu na rzecz produktywnoci programisty. Na przykad model finansowy moe by uyteczny jedynie przez krtki okres, dlatego te waniejsze jest jego szybkie powstanie ni szybkie dziaanie. Jednake wikszoci aplikacji wymaga wjakim stopniu efektywnoci, dlatego te C++ zawsze opowiada si po jej stronie. Poniewa programici piszcy w j z y k u C s na og bardzo na ni wyczuleni, stanowi to rwnie dobry sposb na wytrcenie im z rki argumentw, e jzyk jest zbyt przeadowany i powolny. Wiele spord waciwoci C++ zaprojektowano w ten sposb, by umoliwi popraw wydajnoci w przypadku, gdy generowany kod nie jest dostatecznie efektywny.

\ Dysponujesz nie tylko t sam niskopoziomow kontrol, jak w przypadku jzyka C (wczajc w to moliwo bezporedniego uywania jzyka asemblera wewntrz programu napisanego w C++), ale praktyka dowodzi, e szybko obiektowego programu w C++ waha si w granicach 10% w stosunku do programu napisanego w C, a czstojest mujeszcze blisza 2 1 . Projekt przygotowany pod ktem programu obiektowego moe by w rzeczywistoci bardziej efektywny nijego odpowiednik w C.

Systemy s tatwiejsze do opisania i do zrozumienia


Klasy projektowane pod ktem konkretnego problemu na og lepiej go okrelaj. Oznacza to, e piszc kod, opisujesz swoje rozwizanie, posugujc si raczej pojciami pochodzcymi z przestrzeni problemu (wrzu uszczelk do kosza") ni waciwymi komputerowi, stanowicemu przestrze rozwizania (ustaw bit procesora, oznaczajcy zamknicie obwodu przekanika"). '' Masz do czynienia z pojciami wyszego poziomu, dziki czemu moesz dokona znacznie wicej w pojedynczym wierszu programu. Inn korzyci wynikajc z tej atwoci opisu jest pielgnacja kodu, ktra (o ile wierzy raportom) pochania lwicz kosztw w okresie uytkowania programu. Jeeli programjest przyslpniejszy, tojest on zarazem atwiejszy w pielgnacji. Dziki temu mona rwnie obniy nakady na utworzenie i pielgnacj dokumentacji.

Maksymalne wykorzystanie bibliotek


Najszybszym sposobem napisania programujest uycieju napisanego kodu biblioteki. Gwnym celem jzyka C++ jest uatwienie korzystania z bibliotek. Uzyskuje si to poprzez rzutowanie bibliotek na nowe typy danych (klasy), dziki czemu doczenie biblioteki oznacza dodanie dojzyka nowych typw. Poniewa kompilator C++ czuwa nad sposobem uycia biblioteki gwarantujc waciw inicjalizacj i ,^przatanie", a take zapewniajc, e funkcje s poprawnie wywoywane mona skupi si na tym, do czego biblioteka ma suy, zamiast zastanawia si, wjaki sposb naleyjej uywa.

;. .

" Jednake warto przejrze rubryk Dana Saksa w C/C++ User's Journal, w ktrej mona znale istotne rozwaania dotyczce wydajnoci bibliotek C++.

Rozdzia 1. Wprowadzenie do obiektw

61

Poniewa uywane nazwy mog by przydzielone do poszczeglnych fragmentw programu za pomoc przestrzeni nazw C++, mona posuy si dowoln liczb bibliotek, unikajc popadania w znane zjzyka C kolizje nazw.

Wielokrotne wykorzystywanie kodu dziki szablonom


Istnieje znaczca kategoria typw, ktre wymagaj modyfikacji kodu rdowego po to, by mona ich byo w efektywny sposb uy powtrnie. Szablony w jzyku C++ dokonuj automatycznej modyfikacji kodu rdowego, co czyni je szczeglnie efektywnym narzdziem, umoliwiajcym wielokrotne wykorzystywanie kodu bibliotek. Typy projektowane za pomoc szablonw bd bez problemw dziaa z wieloma innymi typami. Szablony s szczeglnie eleganckim rozwizaniem z uwagi na to, e ukrywajprzed klientem-programist (uytkownikiem biblioteki) zoono, zwizan z takim sposobem wielokrotnego uywania kodu.

ObstagabJdw
Obsuga bdw w Cjest dobrze znanym i czsto ignorowanym problemem. W czasie tworzenia duego i zoonego programu nie moe przytrafi si nic gorszego, ni pojawienie si ukrytego gdzie bdu, co do ktrego brakujejakichkolwiek wskazwek, czym moe on by spowodowany. Obsluga wyjtkw w C++ (wprowadzona w tym, a opisana w peni w drugim tomie ksiki ktry mona pobra z witryny http:/flielion.pl/online/thinkirtg/index.html) gwarantuje, e bd zostanie zauwaony, ; awwynikujegowystpieniazostaniepodjtejakiedziaanie. 'v-:j,;-.

Programowanie na wielk skal


Wiele spord tradycyjnych jzykw ma wbudowane ograniczenia, dotyczce wielkoci i zoonoci programw. Na przykad BASIC moe doskonale nadawa si do sklecenia szybkich rozwiza dla pewnych klas problemw. Jednak w przypadku gdy program rozronie si powyej kilku stron kodu lub wykroczy poza waciw temu jzykowi dziedzin problemw, programowanie zacznie przypomina pywanie w coraz bardziej gstniejcej cieczy. Jzyk C rwnie posiada takie ograniczenia. Na przykad gdy wielko programu przekracza okoo 50 000 wierszy, problemem zaczynaj by kolizje nazw praktycznie kocz si nazwy zmiennych oraz funkcji. Innym, szczeglnie uciliwym, problemem s niewielkie luki w jzyku C bdy ukryte w wielkim programie mog by wyjtkowo trudne do znalezienia. Nie ma wyranej granicy, okrelajcej kiedy uywany jzyk zaczyna zawodzi, a nawet, gdyby bya, to i tak by j zignorowa. Nie powiesz przecie: mj program w BASIC-u wanie zrobi si zbyt duy bd musia przepisa go w C!". Zamiast tego sprbujesz upchn do programu jeszcze kiIka wierszy, aby doda do niego kolejn now funkcj. W ten sposb coraz realniejsze staje si widmo dodatkowych kosztw. Jzyk C++ zaprojektowano w ten sposb, by wspomaga on programowanie na wielk skal, czyli usun niewidoczne granice zoonoci pomidzy maymi a wielkimi

62

Thinking in C++. Edycja polska

programami. Piszc program narzdziowy typu ,,witaj, wiecie!", z pewnoci nie musisz uywa programowania obiektowego, szablonw, przestrzeni nazw ani obsugi wyjtkw ale moesz wykorzysta je wtedy, gdy bdziesz ich potrzebowa. Natomiast kompilator jest rwnie agresywny w wykrywaniu bdw zarwno w stosunku do maego, jak i w wielkiego programu.
' '

Strategie przejcia
Kiedy ju zdecydujesz si na programowanie obiektowe, z pewnoci zadasz pytanie: Wjaki sposb nakoni mojego szefa (kolegw, wydzia lub wsppracowViikw) do uywania obiektw?" Zastanw si. jak ty sam niezaleny programista powiniene rozpocz nauk nowego jzyka oraz modelu programowania. Robie to ju wczeniej. Zaczyna si od nauki i przykadw. Nastpnie przychodzi czas na prbny projekt, pozwalajcy wyczu podstawowe konstrukcje jzyka bez koniecznoci podejmowania zbyt skomplikowanych dziaa. Wreszcie nadchodzi czas na rzeczywisty" projekt, ktry ma peni jak uyteczn rol. Podczas jego realizacji kontynuujesz nauk czytajc, zadajc pytania ekspertom i wymieniajc si poradami zprzyjacimi. Jest to rwnie metoda zalecana przechodzcym z C do C++ przez wielu dowiadczonych programistw. Dokonanie takiego przejcia w caej firmie wie si, oczywicie, z pewnymi aspektami psychologii oddziaywa w obrbie grupy, ale na kadym kroku przydatna jest wiedza na temat tego, jak robiaby to jedna osoba.

*"*'

Wskazwki
obiektowegoorazC++:

Oto gar wskazwek, wartych rozwaenia przy przechodzeniu do programowania


"'' '**"' s * " - ' ' l 5 ' i ; '*.''* - . V - t ' . - ' ' ; v ' . ; ; s - < . "l

LSzkolenie
''
f

'

'"

Pierwszym krokiem jest jaka forma edukacji. Miej na uwadze inwestycje dokonane przez firm w kod w czystym C i postaraj si nie pogry wszystkiego w chaosie na okres szeciu do omiu miesicy, kiedy to wszyscy bd gowili si nad tym, jak dziaa wielokrotne dziedziczenie. Do szkolenia wybierz magrup, najlepiej zoon z ludzi dociekliwych, dobrze ze sob wsppracujcych i mogcych wspiera si podczas nauki C++. Czasami sugerowanejest podejcie alternatywne, polegajce na nauczaniu prowadzonym rwnoczenie na wszystkich szczeblach firmy, obejmujcym zarwno kursy przegldowe, przeznaczone dla menederw zajmujcych si strategi, jak i szkolenia z zakresu projektowania i programowania, ktrych uczestnikami s twrcy projektw. Jest to szczeglnie korzystne w przypadku maych firm, dokonujcych zasadniczego zwrotu w swoim sposobie dziaania, oraz na poziomie poszczeglnych oddziaw wikszych firm. Poniewajednak wie si to z wikszymi wydatkami, mona rozpocz od szkolenia na poziomie projektowym, zrealizowa projekt pilotaowy (o ile to moliwe z udziaem zewntrznego doradcy), a nastpnie pozwoli, by uczestnicy projektu zostali nauczycielami pozostaych pracownikw firmy.

" ' sr >':>t.*r

Rozdzial. * Wprowadzenie do obiektw

63

2. Projekt niewielkiego ryzyka


Rozpocznij od projektu o niewielkim ryzyku i dopu moliwo popeniania bdw. Kiedy ju zyskasz pewne dowiadczenie, bdziesz mg albo przydzieli czonkom pierwotnego zespou inne projekty, albo wykorzysta ich w charakterze personelu technicznego, wspierajcego programowanie obiektowe. Pierwszy projekt moe nie dziaa od razu poprawnie, dlatego te nie powinien on mie dla firmy kluczowego znaczenia. Winien by prosty, niezaleny i pouczajcy, czyli wiza si z utworzeniem klas, ktre przydadz si innym pracujcym w firmie programistom, kiedy rozpoczn nauk C++.

3. Bierz przykted z sukcesw


Zanim zaczniesz od zera, poszukaj przykadw dobrych projektw obiektowych. Istnieje due prawdopodobiestwo, e kto ju rozwiza twj problem. Nawet jeeli rozwizanie niejest dokadnie tym, o co chodzi, moesz przypuszczalnie wykorzysta to, czego nauczye si na temat abstrakcji, do zmodyfikowania istniejcego projektu w taki sposb, by odpowiada on twoim potrzebom. Jest to oglna idea wzorcw projektowych, opisanych w drugim tomie ksiki.

4.Uzywajistniejacychbibliotekklas
Podstawow motywacj ekonomiczn przejcia na C++ jest atwo wykorzystania ju istniejcego kodu w postaci bibliotek klas (w szczeglnoci standardowych bibliotek C++, opisanych szczegowo w drugim tomie ksiki). Najwiksze skrcenie cyklu projektowego nastpi wwczas, gdy nie trzeba bdzie pisa niczego, oprcz main( ), a tworzone i uywane obiekty bd pochodziy z powszechnie dostpnych bibliotek. Jednake niektrzy pocztkujcy programici nie rozumiej tego nie s wiadomi istnienia bibliotek klas, albo, ulegajc fascynacji jzykiem, pragn napisa klasy, ktre by moe ju istniej. Najwikszy sukces zwizany z programowaniem obiektowym i C++ moesz odnie wtedy, gdy na wczesnym etapie przejcia zadasz sobie trud znalezienia i wykorzystania kodu napisanego przez innych.

5. Nie thimacz istniejcego kodu na C++


Chocia skompilowanie kodu napisanego w C za pomoc kompilatora C++ zazwyczaj przynosi (niekiedy ogromne) korzyci, wynikajce ze znalezienia bdw zawartych w starym kodzie, to tumaczenie istniejcego i dziaajcego kodu na C++ nie jest na og najlepszym sposobem wykorzystania czasu jeeli musisz przeksztaci kod napisany w jzyku C w program obiektowy, wystarczy, e opakujesz" go w klasy C++). Korzyci przyrastaj stopniowo szczeglnie gdy planuje si powtrne wykorzystanie kodu. Jest jednak cakiem moliwe, e nie bdziesz w stanie zauway spodziewanego znaczcego zwikszenia wydajnoci w swoich pierwszych kilku projektach, chyba e bdzie to zupenie nowy projekt. Jzyk C++ oraz programowanie obiektowe przynosz najbardziej spektakularne rezultaty wwczas, gdy obejmuj cao projektu od pomysu dojego realizacji.

64

Thinking in C++. Edycja polska

Problemyzzarzdzaniem

Jeeli jeste menederem, twoja praca polega na zdobywaniu rodkw dla zespou, pokonywaniu przeszkd, ktre stoj na drodze do jego sukcesu i, oglnie, na prbach zapewnienia najbardziej wydajnego i przyjaznego rodowiska, dziki ktremu zesp bdzie mg dokona wszystkich wymaganych od niego wyczynw. Przejcie na C++ odpowiada wszystkim trzem powyszym kategoriom i wszystko byoby wspaniale, gdyby nie kwestia kosztw. Mimo e przejcie na C++ moe by tasze dla zespou programujcego w C (i prawdopodobnie w innych jzykach proceduralnych) zalenie od twoich uwarunkowa 22 ni w przypadku alternatywnych jzykw obiektowych, nie odbywa si ono za darmo i istniejprzeszkody, na ktre powiniene zwrci uwg.

Kotzty pocztkowe
Koszt przejcia na C++ to wicej ni tylko zakup kompilatorw C++ Qeden z najlepszych kompilatorw, GNU C++, jest darmowy). rednio- i dugoterminowe koszty zostan zminimalizowane, jeeli zainwestujesz w szkolenie (i prawdopodobnie nadzr nad pierwszym projektem), a takejeeli zidentyfikujesz i zakupisz biblioteki klas rozwizujce twj problem, zamiast prbowa zbudowa je samodzielnie. S to koszty, ktre musz zosta uwzgldnione w realistycznym planie. Istniej ponadto ukryte koszty, zwizane z utrat wydajnoci w trakcie uczenia si nowego jzyka, a take, prawdopodobnie, nowego rodowiska programowania. Szkolenie i doradztwo mogje z pewnoci/minimalizowa, lecz czonkowie zespou muszprzezwyciy swoje problemy zwizane ze zrozumieniem nowej technologii. W czasie tego procesu bd oni popeniali wicej bdw (to zaleta, poniewa zauwaone bdy s najszybsz metod uczenia si) i bd mniej wydajni. Jednak nawet wwczas, borykajc si z rozmaitymi problemami zwizanymi z programowaniem, majc waciwe klasy i odpowiednie rodowisko programowania, mona by bardziej produktywnym uczc si C++ (nawet uwzgldniwszy wiksz liczb bdw i pisanie mniejszej liczby wierszy kodu dziennie) ni pozostajc przy C. ,

Kivestiewydajnosci
Czsto zadawane jest pytanie: czy programowanie obiektowe nie spowoduje, e moje programy stan si automatycznie znacznie wiksze i wolniejsze?". Odpowied nie jest jednoznaczna. Wikszo tradycyjnych jzykw obiektowych bya projektowana raczej z myl o eksperymentowaniu i szybkim przygotowywaniu prototypw ni oszczdnym wykorzystaniu zasobw w czasie dziaania. Dlatego te, z praktycznego punktu widzenia, powodujone znaczne powikszenie programw oraz zmniejszenie szybkoci ich pracy. Jednakejzyk C++ zaprojektowano z myl o produkcji oprogramowania. Kiedy interesuje ci szybkie przygotowanie prototypu, moesz byskawicznie poczy ze sobjego poszczeglne elementy, pomijajc zupenie kwestie efektywnoci. Jeeli uywasz bibliotek dostarczanych przez niezalene firmy, to s one ju zazwyczaj zoptymalizowane przez ich producentw w kadym razie nie jest to problemem, gdy twoim celem jest szybkie opracowanie prototypu. Kiedy uzyskujesz system, o ktry ci chodzio, i okazuje si, e jest on wystarczajco maly i szybki,
Z uwagi na udoskonalenia zwizane z wydajnoci, naley w tym miejscu rwnie wzi pod uwag jzyk Java.

Rozdzia 1. Wprowadzenie do obiektw

65

przedsiwzicie dobiega kresu. W przeciwnym przypadku rozpoczynasz optymalizacj za pomoc narzdzia profilujcego, szukajc w pierwszej kolejnoci atwych moliwoci przyspieszenia programu, zwizanych z wykorzystaniem wbudowanych wasnocijzyka C++. Jeeli to nie pomaga, potrzebujesz modyfikacji, ktdrych mona dokona w wewntrznej implementacji klas, dziki czemu nie ma koniecznoci zmianyjakiegokolwiek uywajcego ich kodu. Dopiero gdy wszystko zawiedzie, bdziesz musia zmieni projekt. Informacja, e wydajno jest tak istotna w tej czci projektu jest wskazwk, e musi ona stanowi element pierwotnych zaoe projektowych. Korzyci wynikajc z szybkiego projektowaniajest odkrycie tego faktu na wczesnym etapie realizacji systemu. Jak ju wspomniano, najczciej podawan wartoci okrelajc rnice wielkoci i szybkoci dziaania programw napisanych w C i C++jest 10%, a czsto programy te sdo siebiejeszcze bardziej zblione. Uywajc C++ zamiast C mona nawet osign znaczne polepszenie wielkoci i szybkoci dziaania programu z uwagi na to, e projekt przygotowywany pod ktem C++ moe by zupenie odmienny od projektu tworzonego dlajzyka C. Rezultat porwnania wielkoci i szybkoci dziaania programw w jzykach C i C++ wynika raczej z praktyki i zapewne takim pozostanie. Niezalenie od liczby osb sugerujcych realizacj tego samego projektu w C i C++, prawdopodobnie adna firma nie decyduje si na takie trwonienie pienidzy, chyba ejest ona bardzo dua i zainteresowana tego rodzaju projektami badawczymi. Ale nawet w takim przypadku wydaje si, e istnieje lepszy sposb wydawania pienidzy. Niemal wszyscy programici, ktrzy przeszli od C (lub jakiego innego jzyka proceduralnego) do C++ (albo innego jzyka obiektowego), znacznie zwikszyli swoj programistyczn wydajno i jest to z pewnoci najbardziej przekonujcy argument,jaki mona przytoczy.

Typowe btdy projektowe


Kiedy zesp rozpoczyna przygod z programowaniem obiektowym oraz C++, programici popeniaj zazwyczaj szereg typowych bdw projektowych. Dzieje si tak czsto z uwagi na zbyt mae sprzenie zwrotne z ekspertami na etapie projektowania i implementacji wczesnych projektw w firmie nie pracujjeszcze eksperci, a istnieje by moe opr zwizany z angaowaniem zewntrznych konsultantw. atwo jest przedwczenie odnie wraenie, e ju rozumie si programowanie obiektowe i zabrn w lep uliczk. Problem oczywisty dIa osoby dowiadczonej moe si sta dIa nowicjusza przedmiotem wielkiej rozterki. Wikszoci z tych bolesnych problemw mona unikn, korzystajc z usug w zakresie szkolenia i doradztwa, wiadczonych przez dowiadczonego zewntrznego eksperta. Z drugiej jednak strony fakt, e tak atwo jest popenia tego typu bdy projektowe, wiadczy o gwnej wadziejzyka C++ jego wstecznej zgodnoci z C (oczywicie, jest to rwniejego gwna zaleta). Aby wywiza si z zadania zdolnoci kompilacji kodu C, jzyk musia przyj pewne rozwizania kompromisowe, czego wynikiem jest zawarta w nim pewna liczba sabych punktw". Wikszo z nich mona napotka w trakcie nauki jzyka. W ksice, a take jej drugim tomie (oraz innych ksikach, opisanych w dodatku C), prbuj przedstawi wikszo puapek, z ktrymi prawdopodobnie bdziesz mia do czynienia uywajc jzyka C++. Powiniene mie zawsze wiadomo, e siatka zabezpieczajaca"jestjednak nieco dziurawa.

Thinking in C++. Edycja polska

Podsumowanie
W niniejszym rozdziale podjem prb przedstawienia szerokiego zakresu zagadnie dotyczcych programowania obiektowego oraz C++. Wskazaem, co wyrnia programowanie obiektowe, a w szczeglnoci co wyrnia jzyk C++, omwiem koncepcje metodyk programowania obiektowego oraz, w kocowej czci, problemy, z jakimi moesz mie do czynienia, dokonujc w swojej firmie przejcia do programowania obiektowego oraz C++. Programowanie obiektowe i C++ mog nie sprosta niektrym oczekiwaniom. Nalezy okreli swoje potrzeby i podj decyzj, czy C++ optymalnieje zaspokoi, czfy tez lepiej bdzie zdecydowa si na inny system programowania (wczajc w to system uywany obecnie). Jeli wiesz, e twoje potrzeby bd w dajcej si przewidzie przyszoci nietypowe i jeeli twoje specyficzne uwarunkowania nie mog by zaspokojone przez C++, to warto zainwestowa w rozwizania alternatywne 31 . Nawet jeeli w kocu zdecydujesz si na jzyk C++, to przynajmniej dowiesz si, jakie miae moliwoci i wyrobisz sobie jasno okrelony pogld, dlaczego wybrae wanie ten kierunek. Wiadomo, jak wyglda program proceduralny skada si z definicji danych i wywoa funkcji. Aby pozna znaczenie takiego programu, musisz si troch natrudzi, analizujc wywoania funkcji i pojcia niskiego poziomu po to, by utworzy w swoim umyle jego model. Jest to powd, dla ktrego w trakcie projektowania programu proceduralnego potrzebne s reprezentacje porednie programy takie s same w sobie skomplikowane, poniewa stosowane rodki wyrazu s ukierunkowane raczej na komputer ni rozwizywany problem. Poniewa C++ dodaje do jzyka C wiele nowych poj, oczywiste moe ci si wydawa zaoenie, e funkcja main( ) w programie napisanym w C++ bdzie znacznie bardziej skomplikowana ni w przypadku jego odpowiednika w jzyku C. Bdziesz mile zaskoczony dobrze napisany program w C++ jest na og znacznie prostszy i atwiej go zrozumie ni jego odpowiednik w jzyku C. Zobaczysz definicje obiektw reprezentujcych koncepcje w przestrzeni problemu (a nie kwestie reprezentacji komputerowej) oraz komunikaty wysyane do tych obiektw, reprezentujce dziaania w tej przestrzeni. Jedn z atrakcyjnych cech programowania obiektowego jest to, ze czytajc kod dobrze zaprojektowanego programu mozna go atwo zrozumie. Zazwyczaj program ten jest znacznie krtszy, poniewa wiele problemw mozna rozwiza, wykorzystujc kod istniejcych bibliotek.

,i-, r

~ W szczeglnoci polecam przyjrzenie sijzykonu tova {http://java.sun.com) oraz Python (http://www.Python.org).

Tworzenie i uywanie obiektw


W rozdziale tym zostay wprowadzone pojcia skadni C++ oraz konstrukcji programu w stopniu umoliwiajcym pisanie i uruchamianie prostych programw obiektowych. W nastpnym rozdziale znajduje si szczegowy opis podstawowej skadni C oraz C++. Czytajc ten rozdzia poznasz podstawy programowania z uyciem obiektw w C++, a take odkryjesz niektre z powodw, dla ktrych jzyk ten spotyka si z entuzjastycznym przyjciem. Powinno to wystarczy do przejcia do rozdziau 3., ktry moe by nieco wyczerpujcy, gdy zawiera wikszo szczegowych informacji na tematjzyka C. Zdefiniowany przez uytkownika typ danych czyli klasa jest tym, co odrnia C++ od tradycyjnych jzykw proceduralnych. Klasa jest nowym typem danych, utworzonym przez ciebie (lub kogo innego) w celu rozwizania okrelonego rodzaju problemw. Po utworzeniu klasy moe jej uywa kady, nie znajc szczegw jej funkcjonowania ani nawet nie wiedzc, jak zbudowane s klasy. W rozdziale tym klasy traktowane s w taki sposb, jakby byy jeszcze jednym wbudowanym typem danych, ktrego mona uywa w programach. Klasy utworzone przez innych s na og poczone w biblioteki. W rozdziale przedstawiono wiele spord bibliotek klas dostarczanych wraz z wszystkimi implementacjami C++. Szczeglnie wan bibliotek standardowjest biblioteka strumieni wejcia-wyjcia, ktra (midzy innymi) pozwala na odczytywanie danych z plikw oraz klawiatury, a take zapisywanie ich w plikach oraz na ekranie. Poznasz rwnie bardzo przydatn klas string oraz kontener vector, pochodzce ze standardowej biblioteki C++. Koczc lektur tego rozdziau przekonasz sie,jak atwojest uywa predefiniowanych bibliotek klas. Aby utworzy swj pierwszy program, musisz najpierw pozna narzdzia uywane do budowy aplikacji.

Rozdzia 2.

68

Thinking in C++. Edycja polska

Proces tumaczenia jzyka


Wszystkie jzyki komputerowe s tumaczone z postaci, ktra jest zazwyczaj atwa do zrozumienia przez czowieka (kodu rdowego), do postaci wykonywanej przez komputer (rozkazy komputera). Translatory zaliczane s tradycyjnie do jednej z dwch klas interpreterw i kompilatorw.

Interpretery
Interpreter przekada kod rdowy na czynnoci (mog one skada si z grupVozkazow komputera), ktre natychmiast wykonuje. Przykadem popularnego jzyka interpretowanego jest BASIC. Tradycyjne interpretery BASIC-a tumacz i wykonujjednorazowojeden wiersz programu, a nastpnie zapominaj, e zosta on przetumaczony. To czyni je wolnymi, poniewa musz one ponownie tumaczy kady powtarzajcy si kod. Dla zwikszenia szybkoci BASIC moe by rwnie kompilowany. Nowoczeniejsze interpretery, takie jak na przykad interpretery jzyka Python, tumacz cay program na jzyk poredni, ktry jest nastpnie wykonywany przez znacznie szybszy interpreter'. Interpretery maj wiele zalet. Przejcie od pisania kodu do jego wykonania nastpuje niemal natychmiastowo, natomiast kod rdowyjest dostpny przez cay czas, dziki czemu w przypadku wystpienia bdu interpreter moe by znacznie bardziej precyzyjny. Czsto przytaczanymi zaletami interpreterw s: atwo interakcji oraz szybkieprojektowanie(alejuzniekonieczniewykonywanie)programow. ... Jzyki interpretowane zawieraj czsto szereg ogranicze zwizanych z tworzeniem duych projektw (Python wydaje si tu by wyjtkiem). Interpreter (albo jego zredukowana wersja) musi zawsze podczas wykonywania kodu pozostawa w pamici. Ponadto wykorzystanie nawet najszybszych interpreterw moe si wiza z niemoliwymi do zaakceptowania ograniczeniami szybkoci. Wikszo interpreterw wymaga przedstawienia do interpretacji od razu caego kodu rdowego. Powoduje to nie tylko ograniczenia zwizane z pamici, ale moe by rwnie przyczyn bardziej zoonych bdw, gdy jzyk nie dostarcza mechanizmw umoliwiajcych lokalizacj oddziaywania rnych fragmentw kodu.

Kompilatory
Kompilator tumaczy kod rdowy bezporednio na jzyk asemblera lub rozkazy komputera. Ostatecznym produktem kocowymjest plik lub pliki zawierajcejzyk , wewntrzny komputera. Jest to zoony proces, ktry zazwyczaj odbywa si w kilku etapach. W przypadku kompilatorw droga od napisania kodu do jego uruchomienia jest znacznie dusza. Granica pomidzy kompilatorami a interpreterami zaczyna si nieco rozmywa szczeglnie w przypadkujezyka Python, posiadajcego wiele cech oraz siejzykw kompilowanych, ale zarazem szybko uruchamianiajzykw interpretowanych.

Rozdzia 2. Tworzenie i uywanie obiektw

69

Programy generowane przez kompilator wymagaj zazwyczaj zalenie od pomysowoci autorw mniej lub wicej pamici i uruchamiaj si o wiele szybciej. Mimo e wielko i szybko programw s prawdopodobnie najczciej przytaczanymi powodami uywania kompilatorw, w wielu przypadkach nie s one przyczynami najwaniejszymi. Niektrejzyki (takiejak C) zostay zaprojektowane w sposb umoliwiajcy niezalen kompilacj poszczeglnych czci programu. Czci te s ostatecznie czone w jeden program wykonywalny za pomoc narzdzia zwanego programem lczcym (ang. linker). Proces ten nosi nazw rozlcznejkompilacji. Rozczna kompilacja przynosi wiele korzyci. Program, ktry kompilowany od razu w caoci mgby przekroczy ograniczenia kompilatora albo rodowiska kompilacji, moe by skompilowany w oddzielnych czciach. Programy mona tworzy i testowa partiami. Kiedyjaka cz programuju dziaa, moe by zachowana i traktowanajako budulec dlajego kolejnych czci. Zbiory przetestowanych i dziaajcych elementw mona poczy w biblioteki przeznaczone do uywania przez innych programistw. W trakcie tworzenia kadej czci programu zoono jego pozostaych czci pozostaje ukryta. Wszystkie te cechy wspomagajtworzenie wielkich programw". Stopniowo w kompilatorach znacznie udoskonalano mechanizmy uatwiajce usuwanie bdw. Wczesne kompilatory generoway jedynie jzyk wewntrzny komputera, a programista wstawia do programu instrukcje print, by zorientowa si, co si dzieje. Nie zawsze postpowanie to jest efektywne. Nowoczesne kompilatory mog wstawia do wykonywalnego programu informacje dotyczce kodu rdowego. Informacje te s wykorzystywane przez potne programy uruchomieniowe dzialajce na poziomie kodu rdowego (ang. source-level debuggers) do prezentacji tego, co rzeczywicie wydarzyo si w programie ledzcjego przebieg w kodzie rdowym. Niektre kompilatory radz sobie z problemem szybkoci kompilacji, dokonujc kompilacji wpamici. Wikszo kompilatorw pracuje z plikami czytajc i zapisujc je na kadym etapie procesu kompilacji. Urzdzenia wykonujce kompilacj w pamici przechowuj kompilowany program w pamici RAM. Dziki temu uruchamianie niewielkich programw moe si wydawa rwnie szybkie, jak w przypadku interpreterw.

Proces kompilacji
Aby programowa w C i C++, trzeba zrozumie poszczeglne kroki, a take sposb dziaania narzdzi uywanych w procesie kompilacji. Niektrejzyki (w szczeglnoci C i C++) rozpoczynaj kompilacj od uruchamieniapreprocesora kodu rdowego. Preprocesorjest prostym programem, zamieniajcym wzorce napotkane w kodzie rdowym na inne wzorce, zdefiniowane przez programist (za pomoc dyrektyw preprocesora). Dyrektywy preprocesora s stosowane po to, aby oszczdzi sobie trudu pisania oraz zwikszy czytelno kodu (w dalszej czci ksiki dowiesz si, jak w projekcie C++ zapobiega si uywaniu preprocesora w wikszoci przypadkw z uwagi na moliwo popenienia trudno uchwytnych bdw). Przetworzony wstpnie kodjest zwykle zapisywany w pliku porednim.
ython znowujest tutaj wyjtkiem, umoliwia bowiem rwnie rozcznkompilacje.

70

Thinking in C++. Edycja polska

'..

Kompilatory wykonuj zazwyczaj swoj prac w dwch przebiegach. W pierwszym przebiegu dokonywana jest analiza skladniowa (ang. parsing) przetworzonego wstpnie kodu. Kompilator rozbija kod rdowy na mae jednostki i porzdkuje go, uywajc struktury nazywanej drzewem. W wyraeniu A + B" elementy A", +" i B" s limi drzewa skadniowego. Czasami pomidzy pierwszym a drugim przebiegiem kompilacji uywany jest optymalizator globalny (ang. global optimizer), umoliwiajcy uzyskanie krtszego i szybszego kodu. ~ * W drugim przebiegu generator kodu przechodzi przez drzewo skadniowe, tworzc dla poszczeglnych wzw drzewa kod w jzyku asemblera albo w jzyku wewntrznym komputera. Jeeli generator tworzy kod w jzyku asemblera, to w nastpnej kolejnoci musi zosta uruchomiony asembler. Kocowym rezultatem jest w obu przypadkach program wynikowy (plik, ktry ma na og rozszerzenie .o lub .obj). W drugim przebiegu uywany jest czasem optymalizator lokalny (ang. peephole optimizer), poszukujcy fragmentw kodu zawierajcych nadmiarowe instrukcjejzyka asemblera. Program czcy (ang. linker) czy ze sob list programw wynikowych w program wykonywalny, ktry moe by zaadowany i uruchomiony przez system operacyjny. Kiedy funkcja w jakim programie wynikowym odwouje si do funkcji lub zmiennej znajdujcej si w innym programie wynikowym, odwoania te s rozwizywane przez program czcy sprawdza on, czy istniej wszystkie zewntrzne funkcje i dane, ktrych istnienia zadano w czasie kompilacji. Program czcy dodaje rwnie specjalny kod, wykonujcy pewne dziaania w czasie uruchamiania programu.

.,

""~'

Program czcy, rozwizujc wszystkie odwoania, moe przeszukiwa specjalne pliki nazywane bibliotekami (ang. libraries). Biblioteka skada si ze zbioru programw wynikowych, poczonych w pojedynczy plik. Biblioteki s tworzone i obsugiwane za pomoca_programow zarzdzajcych bibliotekami (ang. librarians}.

Statyczna kontrola typw


W czasie pierwszego przebiegu kompilator przeprowadza kontrol typw. Polega ona na sprawdzeniu poprawnoci uycia argumentw funkcji oraz zapobiega wielu rodzajom bdw programistycznych. Poniewa kontrola typw odbywa si w czasie kompilacji, a nie w czasie wykonywania programu, jest nazywana statyczn kontrol typw. ': Niektre jzyki obiektowe (szczeglnie Java) dokonuj pewnego rodzaju kontroli wczasie pracy programu (nazywa si to dynamiczn kontrol typw). Dynamiczna kontrola typw jest w poczeniu ze statyczn kontrol typw bardziej skuteczna ni sama tylko statyczna. Jednake wie si ona z pewnym dodatkowym obciniem w czasie wykonania programu. C++ wykorzystuje statyczn kontrol typw, poniewajzyk nie moe zaoy adnego konkretnego wsparcia bdnych operacji w czasie wykonywania programu. Statyczna kontrola typw informuje programist o niewaciwym uyciu typw w czasie kompilacji, maksymalizujc szybko wykonania programu. W miar poznawania C++ przekonasz si, e decyzje projektowe jzyka wspieray w wikszoci ten sam rodzaj szybkiego, ukierunkowanego na produkcj programowania, z ktrego syniejzyk C.

Rozdzia 2. # Tworzenie i uywanie obiektw

71

Mona wyczy statyczn kontrol typw w jzyku C++. Mona rwnie samodzielnie przeprowadzi dynamiczn kontrol typw wystarczy tylko napisa odpowiedni kod.

Narzdzia do rozcznej kompilacji


Rozczna kompilacja jest szczeglnie wana w razie budowy duych projektw. W przypadku jzykw C oraz C++ program moe zosta utworzony w postaci maych, moliwych do ogarnicia, niezalenie testowanych moduw. Najbardziej podstawowym narzdziem, pozwalajcym na rozbicie programu na moduy, jest moliwo tworzenia nazwanych procedur lub podprogramw. W jzykach C i C++ podprogramy s nazywane funkcjami, stanowicymi fragmenty kodu, ktry moe by umieszczony w rnych plikach, dziki czemu moliwa jest rozczna kompilacja. Innymi sowy funkcje stanowi elementarn jednostk kodu, poniewa nie jest moliwa sytuacja, w ktrej rne fragmenty tej samej funkcji znajdowayby si w rnych plikach. Funkcja musi by w caoci umieszczona w pojedynczym pliku (cho pliki mogzawiera wicej nijednfunkcj). W czasie wywoywania funkcji na og przekazywane s do niej pewne argumenty bdce wartociami, z ktrymi ma ona pracowa wwczas, gdy bdzie wykonywana. Po zakoczeniu funkcji przekazuje ona zazwyczaj warto zwracan, czyli warto, ktrajest przesyana z powrotemjako rezultatjej dziaania. Moliwejest rwnie napisanie funkcji, ktra nie pobiera argumentw ani nie zwraca adnej wartoci. Podczas tworzenia programu skadajcego si z wielu plikw funkcje wjednym pliku musz mie dostp do funkcji i danych znajdujcych si w innym pliku. W trakcie kompilacji pliku kompilator C lub C++ musi rwnie wiedzie o funkcjach i danych znajdujcych si w innych plikach w szczeglnoci o tym, jakie s ich nazwy i poprawne uycie. Kompilator upewnia si, e funkcje oraz dane s uywane poprawnie. Proces informowania kompilatora" o nazwach zewntrznych funkcji i danych oraz o tym, jak powinny one wyglda, nazywany jest deklaracj. Po zadeklarowaniu funkcji l u b zmiennej kompilator wie, w j a k i sposb sprawdzi, czyjest ona waciwie uywana.

Deklaracje i definicje
Wane jest, by rozumie rnic pomidzy deklaracjami i definicjami, poniewa pojcia te bd uywane precyzyjnie w dalszej czci ksiki. Na og wszystkie programy napisane w C i C++ wymagaj deklaracji. Zanim napiszesz swj pierwszy program, musisz zapozna si z poprawnym zapisem deklaracji. Deklaracja przedstawia kompilatorowi nazw identyfikator. Komunikuje ona kompilatorowi: ta funkcja lub zmienna istnieje w j a k i m miejscu i powinna tak wanie wyglda". Z kolei definicja nakazuje: ,,utworz w tym miejscu zmienn" albo ,,utworz tutaj t funkcj". Przydziela ona nazwie pami. Nastpuje to niezalenie od tego, czy wchodzi w gr zmienna czy funkcja w kadym przypadku w miejscu

72

Thinking in C++. Edycja polska wystpienia definicji kompilator przydziela pami. W przypadku zmiennej, kompilator okrela, jaka jesl jej wielko i powoduje utworzenie w pamici miejsca, przeznaczonego do przechowywaniajej danych. W przypadku funkcji kompilator tworzy kod, ktry w kocu rwnie zajmuje miejsce w pamici. Zmienna lub funkcja mog by zadeklarowane w wielu rnych miejscach, ale zarwno w C, jak i w C++ moe wystpi tylko jedna definicja Qest to czasami nazywane regujednej definicji). Kiedy program czcy scala wszystkie programy wynikowe, to na og zgasza bd w sytuacji, gdy napotka wicej nijednde/inicj tej samej funkcji lub zmiennej. X Definicja moe by rwnie deklaracj. Jeeli kompilator nie napotka wczeniej nazwy x, a ty wprowadzisz definicj int x;, kompilator potraktuje t nazw jako deklaracjirwnoczenieprzydzielizmiennejpami.

'"' > ; c

Sktadnia deklaracji funkcji


ii ui
.w:

Deklaracja funkcji w C oraz C++ zawiera nazw funkcji oraz typy przekazywanych jej argumentw i zwracanej wartoci. Na przykad poniej znajduje si deklaracja funkcji o nazwie funcl(), ktra pobiera dwa cakowite argumenty (liczby cakowite s oznaczane w C i C+4- za pomoc sowa kluczowego int) oraz zwraca warto cakowit:
int funcl(int,int);

r . . -.

'

'

'' ' ':';

Pierwsze widoczne sowo kluczowejest zwracan wartoci: int. Argumenty zawarte s w nawiasie, znajdujcym si po nazwie funkcji, w kolejnoci, w ktrej s uywane. rednik sygnalizuje koniec instrukcji w tym przypadku komunikuje kompilatorowi: toju wszystko, definicja funkcji si skoczya!". Deklaracje jzykw C i C++ prbuj naladowa sposb uycia obiektu. Na przykad jeeli ajest zmienncakowit, to powysza funkcja moe by uyta w nastpujcysposb: a - funcl(2.3); ' ' ,,
;

Poniewa funcl() zwraca warto cakowit, kompilatory C i C++ sprawdz sposb uycia funcl(), by upewni si, e a moe przyj zwracan warto, a argumenty s podane waciwe. Argumenty wymienione w deklaracjach funkcji mog mie nazwy. Kompilator je ignoruje, lecz mog one by pomocne, przypominajc uytkownikowi znaczenie parametrw. Na przykad moemy zadeklarowa funkcj funcl() w nieco innym stylu, majcym jednak to samo znaczenie:
int funcl(int dlugosc, int szerokosc);

Putepka
argumentw.WjzykuCdeklaracja:
int func2();

Istnieje zasadnicza rnica pomidzy C i C++ w przypadku funkcji z pustymi listami


.'

Rozdzia 2. Tworzenie i uywanie obiektw

73

oznacza: funkcj z dowoln liczb argumentw dowolnego typu". Uniemoliwia to kontrol typw, dlatego te w C++ zapis taki oznacza ,,funkcje bez argumentw".

Definicje funkcji
Definicja funkcji przypominajej deklaracj, z tjednak rnic, e zawiera ona tre (ciao) funkcji. Ciao funkcji jest zbiorem instrukcji, zamknitym w nawiasie klamrowym. Nawiasy klamrowe symbolizuj pocztek oraz koniec bloku kodu. Aby okreli dla funkcji funcl( ) definicj, bdc pustym ciaem (ciaem niezawierajcym kodu), naley napisa:
int funcl(int dlugosc, int szerokosc} { }

Zwr uwag na to, e nawiasy klamrowe w definicji funkcji zastpuj rednik. Poniewa nawiasy klamrowe zawieraj instrukcj lub grup instrukcji, rednik nie jest potrzebny. Warto rwnie zauway, e jeeli chcesz uywa argumentw w ciele funkcji, to musz one posiada nazwy (w powyszym przypadku, poniewa nigdy nie s uywane, maj charakter opcjonalny).

Sktednia deklaracji zmiennej


Pojciu deklaracja zmiennej" przypisywano w przeszoci sprzeczne i mylce znaczenia, dlatego wane jest zrozumienie waciwej definicji, co umoliwi poprawne odczytywanie kodu. Deklaracja zmiennej informuje kompilator o tym,jak ta zmienna wyglda. Oznacza ona: wiem, e jeszcze nie spotkae tej nazwy, ale z pewnoci ona gdzie istnieje ijest zmienn typu X". Wraz z deklaracj funkcji podaje si jej typ (zwracan warto), nazw, list argumentw i koczy si j rednikiem. To wystarcza kompilatorowi, by zrozumia, e jest to deklaracja, i wiedzia, jak powinna wyglda ta funkcja. Mona z tego wysnu wniosek, e deklaracja zmiennej jest typem, po ktrym nastpuje nazwa. Na przykad:
int a:

mogoby, zgodnie z powysz logik, stanowi deklaracj zmiennej a bdcej liczb cakowit. Wystpuje tu jednak konflikt powyszy kod zawiera wystarczajco duo informacji, aby kompilator zarezerwowa pami dla zmiennej cakowitej o nazwie a, i do takiej wanie sytuacji dochodzi. Aby rozwiza ten dylemat, zarwno w przypadku C,jak i C++, koniecznejest sowo kluczowe oznaczajce: to tylko deklaracja, zmienna zostaa zdefiniowana w innym miejscu". Tym sowem kluczowym jest extern. Moe ono oznacza, e albo definicja znajduje si w innym pIiku. albo wystpuje ona wjego dalszej czci. Deklaracja zmiennej bez jej definiowania oznacza uycie sowa kluczowego extern przed opisem tej zmiennej, takjak poniej:
extern int a;

Sowo kluczowe extern moe by rwnie zastosowane w stosunku do deklaracji funkcji. W przypadku funkcji funcl( ) wyglda to nastpujco:
extern int funcl(int dlugosc. int szerokosc);

74

Thinking in C++. Edycja polska

Instrukcja ta jest rwnowana poprzednim deklaracjom funcl( ). Poniewa nie wystpuje tutaj ciao funkcji, kompilator musi traktowa jjako deklaracj, a nie definicj funkcji. Dlatego te sowo kluczowe extern jest w przypadku deklaracji funkcji nadmiarowe i zarazem opcjonalne. Chyba nie najlepiej si stao, e twrcy jzyka C nie wymusili uycia sowa kluczowego extern w przypadku deklaracji funkcji zapewnioby to wiksz spjno i przejrzysto (ale wymagaoby wicej pisania, co zapewne wyjania ich decyzj). Poniej przedstawiono nieco wicej przykadw deklaracji: / / : C02:Declare.cpp // Przykady deklaracji i definicji extern int i: // Deklaracja bez definicji extern float f(float); // Deklaracja funkcji float b; // Deklaracja i definicja float f(float a) { // Definicja return a + 1.0; } int i; // Definicja int h(int x) { // Deklaracja i definicja return x + 1; } int main() { b = 1.0: i = 2; f(b): h(i); } ///:~ W deklaracjach funkcji identyfikatory ich argumentw s opcjonalne. Natomiast w definicjach s one konieczne (identyfikatory argumentw wymagane s jedynie w C, a nie w C++). t

Dcriczanie nagtwkw
Wikszo bibliotek zawiera pokan liczb funkcji i zmiennych. Aby uatwi prac i zapewni spjno podczas tworzenia zewntrznych deklaracji tych elementw, jzyki C oraz C++ uywaj mechanizmu nazywanego plikiem nagwkowym (ang. headerfile). Plik nagwkowy jest plikiem zawierajcym zewntrze deklaracje biblioteki tradycyjnie rozszerzeniem nazwy tego p l i k u j e s t h", takjak w przypadku p l i k u headerfile.h (w starszych programach uywa si innych rozszerze, takich jak .hxx lub .hpp, ale wystpuj one rzadko). Programista tworzcy bibliotek udostpnia p l i k nagwkowy. Aby zadeklarowa funkcje oraz zewntrzne zmienne znajdujce si w bibliotece, uytkownik docza plik nagwkowy. Aby to uczyni, naley uy dyrektywy preprocesora #include. Nakazuje ona preprocesorowi, by otworzy wymieniony plik nagwkowy i wstawi jego zawarto w miejscu, w ktrym znajduje si instrukcja #include. W dyrektywie #include nazw pliku mona poda na dwa sposoby w nawiasie ktowym (< >) lub w cudzysowie.

Rozdzia 2. Tworzenie i uywanie obiektw

75

Podanie nazw plikw w nawiasie ktowym, takjak w przykadzie poniej:

#include <neader>
powoduje, e preprocesor poszukuje pliku w sposb zaleny od implementacji, ale zazwyczaj wie si to zjakim rodzajem cieki wyszukiwania", okrelonej w rodowisku lub podanej kompilatorowi w wierszu polecenia. Mechanizm okrelania cieki wyszukiwania zaley od rodzaju komputera, systemu operacyjnego oraz implementacji C++ i moe wymaga sprawdzenia. Podanie nazwy pliku w cudzysowie.jak w przykadzie:
#include "local.h"

informuje preprocesor, by poszuka pliku w (zgodnie ze specyfikacj) sposb zdefiniowany w implementacji". Oznacza to zazwyczaj poszukiwanie pliku w stosunku do biecego katalogu. Jeeli plik nie zostanie znaleziony, to dyrektywa doczenia jest przetwarzana ponownie w taki sposb, jak gdyby wystpowa w niej nawias ktowy, a nie znaki cudzysowu. Aby doczy plik nagwkowy iostream", naley napisa:

#include <iostream>
Preprocesor znajdzie plik nagwkowy iostream" (przewanie w katalogu o nazwie include") i doczy go.

Standardowy format doczania plikw w C++


W miar rozwojujzyka C++ rni producenci kompilatorw nadawali nazwom p l i kw rne rozszerzenia. Ponadto rozmaite systemy operacyjne nakaday na nazwy plikw odmienne ograniczenia, w szczeglnoci dotyczce ich dugoci. Powodowao to problemy z przenonoci kodu rdowego. Aby zniwelowa te rnice, standard stosuje format dopuszczajcy nazwy plikw dusze ni niesawne osiem znakw oraz eliminuje ich rozszerzenia. Na przykad zamiast dawnego stylu doczania pliku iostream.h, ktry wyglda nastpujco:
#include <iostream.h> mona obecnie pisa:

#include <iostream>
Translator moe implementowa instrukcj doczania w sposb odpowiadajcy potrzebom okrelonego kompilatora i systemu operacyjnego, w razie potrzeby skracajc nazw i dodajc do niej rozszerzenie. Oczywicie, jeeli chcesz uywa takiego stylu, zanim producent kompilatora zapewni dla niego wsparcie, to moesz rwnie skopiowa pliki nagwkowe udostpnione przez producenta, usuwajc z ich nazw rozszerzenia. Biblioteki odziedziczone z C s nadal dostpne pod nazwami z tradycyjnym rozszerzeniem ,,.h". Moesz ich jednak rwnie uywa za pomoc bardziej nowoczesnego stylu doczania C++, poprzedzajc ich nazwy liter c". A zatem dyrektywy:

#include <stdio.h> #include <stdlib.h>

76

Thinking in C++. Edycja polska

mona zastpi przez: finclude <cstdio> #include <cstdlib> I tak dalej, dla wszystkich standardowych plikw nagwkowych C. Pozwala to czytelnikowi na eleganckie rozrnienie, czy uywane s biblioteki C czy C++. Rezultat zastosowania nowego formatu doczania plikw nie jest tosamy z\uzyciem starego formatu posugiwanie si rozszerzeniem ,,.h" powoduje doczenie dawnej wersji, pozbawionej szablonw, natomiast pominicie ,,.h" docza now wersj, wykorzystujc szablony. Prba poczenia obu tych form w pojedynczym programie powoduje na og kopoty.

czenie
Program czcy zestawia ze sob programy wynikowe (czsto z rozszerzeniami nazw plikw ,,.o" lub ,,.obj"), utworzone przez kompilator, czego wynikiemjest program wykonywalny, ktry system operacyjny moe zaadowa i uruchomi. Jest to ostatni etap procesu kompilacji. Cechy programw czcych zmieniaj si w zalenoci od systemu. Na og, aby program czcy wykona swe zadanie, wystarczy mu po prostu poda nazwy programw wynikowych oraz bibliotek, ktre majby ze sob poczone, a take nazw pliku wykonywalnego. Niektre systemy wymagaj od uytkownika samodzielnego uruchomienia programu czcego. W wikszoci pakietw C++ program czcy jest wywoywany za porednictwem kompilatora C++. W wielu przypadkach program czcyjest uruchamiany w sposb niewidoczny dla uytkownika. Niektre ze starszych programw szukaj programw wynikowych i bibliotek tylko jednokrotnie, przeszukujc podan list plikw od lewej do prawej strony. Oznacza to, e kolejno programw wynikowych oraz bibliotek moe mie znaczenie. Jeeli napotkae jaki tajemniczy problem, ktry nie ujawni si a do etapu czenia, to jedn z moliwych przyczyn jest kolejno, w jakiej pliki zostay podane programowi czcemu.

Uywanie bibliotek
Po zapoznaniu si z podstawow terminologijeste w stanie zrozumie, w j a k i sposb uywa bibliotek. Aby korzysta z biblioteki, naley: 1. Doczy plik nagwkowy biblioteki. 2. Uy funkcji i zmiennych, zawartych w bibliotece. 3. Doczy bibliotek do programu wykonywalnego. Te same kroki obowizuj rwnie w przypadku, gdy programy wynikowe nie s scalone w bibliotek. Doczenie pliku nagwkowego oraz poczenie programw wynikowych spodstawowymi etapami rozcznej kompilacji, zarwno w C,jak i w C++.

Rozdzia 2. Tworzenie i uywanie obiektw

77

Jak program ^czcy przeszukuje bibliotek?


Kiedy odwoujesz si w j z y k u C lub C++ do zewntrznej funkcji lub zmiennej, program czcy, napotykajc to odwoanie, moe zadziaa dwojako. Jeeli do tej pory nie spotka definicji funkcji lub zmiennej, to dodaje jej identyfikator do listy nieustalonych odwoa". Jezeli natomiast program czcy napotka ju definicj, odwoaniejest uznawane za ustalone. Jeeli program czcy nie moze znale definicji na licie programw wynikowych, przeszukuje biblioteki. Biblioteki s w j a k i sposb poindeksowane; program czcy nie musi wic przeglda wszystkich programw wynikowych zawartych w bibliotece, lecz tylko jej indeks. Kiedy program czcy znajdzie w bibliotece definicj, wwczas cay program wynikowy (a nie tylko definicja funkcji) zostaje doc/ony do programu wykonywalnego. Zwr uwag na to, e niejest doczana caa biblioteka, a tylko zawarty w niej program wynikowy, zawierajcy dan definicj (w przeciwnym przypadku programy byyby niepotrzebnie due). Jeeli chcesz zmniejszy wielko programu wykonywalnego, to mozesz rozway umieszczenie poszczeglnych funkcji w oddzielnych plikach kodu na etapie tworzenia wasnych bibliotek. Wymaga to wikszych nakadw na edycj3, lecz moze by pomocne dla uytkownika. Poniewa program czcy przeszukuje plikj w kolejnoci, wjakiej zostay one podane, moesz wykluczy uywanie funkcji znajdujcej si w bibliotece, umieszczajc na licie przed wystpieniem nazwy biblioteki p l i k zawierajcy wasn funkcj o tej samej nazwie. Zanim program czcy przeszuka bibliotek, ustali on wszelkie odwoania do tej funkcji, wykorzystujc twojfunkcj. Zostanie ona uzyta w miejsce funkcji zawartej w bibliotece. Zwr uwag na to, e wystpienie takiej sytuacji moe by rwnie bdem; chroniprzed niprzestrzenie nazw C++.

Sekretne dodatki
Kiedy tworzony jest wykonywalny plik C lub C++, w niejawny sposb doczane s do niego pewne elementy. Jednym z nich jest modu startowy, zawierajcy procedury inicjalizujce, ktre muszby wykonane za kadym razem, gdy uruchamianyjest program C lub C++. Procedury te przygotowuj stos i inicjuj w programie pewne zmienne. Program czcy zawsze poszukuje w standardowej bibliotece skompilowanej wersji kadej standardowej" funkcji, wywoywanej w programie. Poniewa standardowa biblioteka jest zawsze przeszukiwana, mozna uzy jej dowolnego elementu, doczajc po prostu do swojego programu odpowiedni plik nagwkowy bez potrzeby informowania o koniecznoci przeszukiwania standardowej biblioteki. Na przykad funkcje strumieni wejcia-wyjcia znajduj si w standardowej bibliotece C++. Aby ich uy, wystarczy doczy do swojego programu plik nagwkowy <iostream>. Jeeli natomiast uywasz jakiej dodatkowej biblioteki, to musisz wyranie umieci jej nazw na licie plikw, przekazywanej programowi czcemu.

Polecabym uzycie Perla lub Pythona, w celu zautomatyzowania tego zadania jako czci procesu tworzenia bibliotek (patrz wwH.Perl.org lub w ww.Pylhon org).

78

Thinking in C++. Edycja polska

Uywanie bibliotek napisanych w czystym C


To, e piszesz kod w jzyku C++ nie oznacza wcale, e nie moesz uywa funkcji bibliotecznych C. W rzeczywistoci biblioteka C jest w caoci wczona do standardowej biblioteki C++. Przygotowujc te funkcje wykonano ogromn prac, dziki czemu pozwalaj one na zaoszczdzenie mnstwa czasu. W ksice uywane bd funkcje standardowej biblioteki C++ (a zatem rwni standardowej biblioteki C), kiedy to bdzie wygodne, ale, dla zapewnienia przenonoci programw, stosowane bdjedynie standardowe funkcje biblioteczne. W niektrych przypadkach, kiedy naley uy funkcji bibliotecznych nieznjdujcych si w standardzie C++, dooono wszelkich stara, by wykorzystane byy funkcje zgodne z POSIX. POSIXjest standardem bazujcym na wysikach standaryzacyjnych Uniksa, zawierajcym funkcje wykraczajce poza zakres biblioteki C++. Na og mona spodziewa si obecnoci funkcji standardu POSIX na platformach uniksowych (w szczeglnoci w Linuksie), a czsto dostpne sone rwnie w systemach DOS i Windows. Na przykadjeeli uywasz wielowtkowoci, to korzystajc z biblioteki wtkw POSIX jeste w lepszej sytuacji, poniewa dziki temu twj kod jest atwiejszy do zrozumienia, przeniesienia i pielgnacji (a biblioteka wtkw POSIX bdzie zazwyczaj wykorzystywaa wasnoci wielowtkowoci systemu operacyjnego, o ile s one przez system udostpniane).

Twj pierwszy program w C++


Dysponujeszju niemal wszystkimi podstawowymi wiadomociami, niezbdnymi do utworzenia i skompilowania programu. Program bdzie uywa klas strumieni wejcia-wyjcia (ang. iostream input-output stream), nalecych do standardu C++. Umoliwiaj one odczytywanie i zapisywanie plikw, a take standardowego" wejcia i wyjcia (ktre s zwykle zwizane z konsol, ale mog by rwnie przekierowane do plikw lub innych urzdze). W naszym prostym programie do wywietlenia komunikatu na ekranie zostanie uyty obiekt bdcy strumieniem.

Uywanie klasy strumieni wejcia-wyjcia


Aby zadeklarowa funkcje oraz zewntrzne dane zawarte w klasie strumieni wejciawyjcia, naley doczy plik nagwkowy, uywajc instrukcji:

#include <iostream>
Pierwszy program uywa pojcia standardowego wyjcia, ktre oznacza miejsce oglnego przeznaczenia, suce do wysyania informacji wyjciowych". Pniej zostan przedstawione przykady, w ktrych standardowe wyjcie jest uyte w odmienny sposb, ale w tym wypadku informacje bd po prostu przekazywane na konsol. Pakiet strumieni wejcia-wyjcia automatycznie definiuje zmienn (obiekt) o nazwie cout, ktry przyjmuje wszystkie dane skierowane na standardowe wyjcie.

Rozdzia 2. * Tworzenie i uywanie obiektw

79

Do wyprowadzania danych na standardowe wyjcie uywany jest operator . Programujcy wjzyku C znaj ten operatorjako bitowy operator przesunicia w lewo", ktry zostanie opisany w nastpnym rozdziale. Wystarczy wiedzie, e przesunicie bitw w lewo nie ma nic wsplnego z wyjciem. Jednake jzyk C++ pozwala na przecianie (ang. overloading) operatorw. Przeciajc operator, nadaje si mu nowe znaczenie w przypadku, gdy jest on uywany z obiektami okrelonego typu. W poczeniu z obiektami klasy strumieni wejcia-wyjcia operator oznacza wylij do". Na przykad polecenie:
cout "witaj!";

wysya napis witaj!" do obiektu o nazwie cout (co jest skrtem nazwy console oiitput", oznaczajcej po angielsku wyjcie konsoli). Przecianie operatorw opisano szczegowo w rozdziale 12.

Przestrzenie nazw
Jak ju wspomniano w rozdziale 1., jednym z gwnych problemw spotykanych wjzyku C jest wyczerpywanie si nazw" funkcji i identyfikatorw, gdy program osiga pewn wielko. Oczywicie, nazwy tak naprawd wcale si nie kocz jednak coraz trudniejsze staje si szybkie wymylanie nowych. Co waniejsze, gdy program osiga pewn wielko, jest na og dzielony na czci, z ktrych kada jest tworzona i utrzymywana przez inn osob lub grup. Poniewajzyk C ma w rzeczywistoci tylkojeden obszar, w ktrym funkcjonuj wszystkie identyfikatory i nazwy funkcji, oznacza to, e programici musz zachowa ostrono, by przypadkowo nie uy tych samych nazw w sytuacjach, w ktrych mog one ze sob kolidowa. Szybko staje si to uciliwe, czasochonne i w ostatecznym rozrachunku kosztowne. Standardowe C++ posiada mechanizm zapobiegajcy takim kolizjom sowo kluczowe namespace (od angielskich sw ,,naine space" przestrze nazw). Kady zbir definicji zawartych w bibliotece lub programie jest opakowany" w przestrze nazw; w sytuacji, gdy jaka inna definicja nosi t sam nazw, ale znajduje si w innej przestrzeni nazw, nie powoduje to kolizji. Przestrzenie nazw s wygodnym i pomocnym narzdziem, ale ich obecno powoduje, e naley o nich wiedzie przed podjciem prby napisaniajakikolwiek programu. Jeeli doczysz po prostu plik nagwkowy i uyjesz funkcji lub obiektw pochodzcych z tego pliku, to prawdopodobnie podczas prby kompilacji programu otrzymasz dziwnie wygldajce bdy, oznajmiajce, e kompilator nie znalaz adnych deklaracji obiektw, ktre doczye przecie w pliku nagwkowym! K i e d y j u zobaczysz ten komunikat kolejny raz,jego znaczenie stanie si dla ciebie oczywiste: doczye plik nagwkowy, ale wszystkie deklaracje znajduj si wewntrz przestrzeni nazw, a ty nie zapowiedziae kompilatorowi, e zamierzasz uywa deklaracji w tej wanie przestrzeni nazw". Istnieje sowo kluczowe, dziki ktremu mona powiedzie: chc uywa deklaracji i (lub) definicji, znajdujcych si w tej przestrzeni nazw". Tym waciwym sowem jest using. Wszystkie standardowe biblioteki C++ s zawarte w pojedynczej przestrzeni

80

Thinking in C++. Edycja polska

nazw, ktr jest std (skrt od standard"). Poniewa w ksice uywane s niemal wycznie standardowe biblioteki, niemal w kadym programie widnieje nastpujca dyrektywa using: using namespace std; Oznacza ona, e zamierzamy odsoni wszystkie elementy zawarte w przestrzeni nazw std. Po tej instrukcji nie trzeba si obawia o to, e jaki szczeglny skaonik biblioteki znajduje si wewntrz przestrzeni nazw. Dyrektywa using powoduje bowiem, i wymieniona w niej przestrze nazw jest dostpna a do koca pliku, w ktrym dyrektywa ta zostaa umieszczona. Odsonicie wszystkich elementw przestrzeni nazw po tym, jak kto zada sobie trud ich ukrycia, moe wydawa si szkodliwe i naley zachowa ostrono przed pochopnym ujawnianiem przestrzeni nazw (przekonasz si o tym w dalszej czci ksiki). Jednake dyrektywa using odsania te nazwy jedynie w obrbie biecego pliku, jej dziaanie nie jest wic tak radykalne, jak mogoby to si wydawa na pierwszy rzut oka (ale i tak powanie si zastanw, zanim lekkomylnie zrobisz to w pliku nagwkowym). Istnieje zwizek pomidzy przestrzeniami nazw i sposobem, w jaki doczane s pliki nagwkowe. Przed standaryzacj nowoczesnego stylu doczania plikw nagwkowych (bez kocowego ,,.h", jak w <iostream>) typowe byo doczanie plikw nagwkowych z rozszerzeniem ,,.h" jak np. <iostream.h>. Przestrzenie nazw rwnie nie byy wwczas elementem jzyka. Zapis dokonany w celu zapewnienia wstecznej zgodnoci z istniejcym kodem:
#include <iostream.h>

oznacza: #include <iostream> using namespace std; Jednake w ksice uywany bdzie standardowy format doczania plikw nagwkowych (bez ,,.h"), dlatego te konieczne bdzie stosowanie dyrektywy using. W rozdziale 10. temat przestrzeni nazw jest opisany znacznie bardziej gruntownie.

Podstawy struktury programu


Program w C lub w C++ jest zbiorem zmiennych, definicji funkcji oraz wywoa funkcji. Kiedy program rozpoczyna dziaanie, uruchamia kod inicjujcy i wywouje specjaln funkcj main()". W tym miejscu umieszczany jest gwny kod programu. Jak ju wspomniano wczeniej, definicja funkcji skada si ze zwracanego typu (ktry musi by okrelony w C++), nazwy funkcji, listy argumentw w nawiasie oraz kodu funkcji, zawartego w nawiasie klamrowym. Oto przykadowa definicja funkcji:
int functionO { // Tu znajduje si kod funkcji (to jest komentarz) }

Rozdzia 2. Tworzenie i uywanie obiektw


m^^"^^^^^^^^^^^^ " i i ^ ^ ^ ^ ^ " " " ^ ^ ^ ^ ^ ^ ^ ^ . w . ^ ^ ^ . . . ^ ^ ^ ^ ^ ^ ^ . ^ ^ ^ ^ ^ . ^ ^ ^ ... _ ,

81
,

Powysza funkcja posiada pust list argumentw oraz ciao, zawierajce jedynie komentarz. W obrbie definicji funkcji moe wystpowa wiele nawiasw klamrowych, ale przynajmniej jeden z nich musi obejmowa ciao funkcji. Poniewa main( ) jest funkcj, to musi rwnie stosowa si do tych zasad. W C++ funkcja main( ) zawsze zwraca warto typu int. C oraz C++ sjzykami o swobodnej strukturze. Kompilator (z kilkoma wyjtkami) ignoruje znaki nowego wiersza i odstpy, musi wic wjaki sposb okreli miejsca, w ktrym koczy si instrukcja. Instrukcje soddzielone rednikami. W jzyku C komentarze rozpoczynaj si znakami /*, a kocz par znakw */. Mog one zawiera znaki nowego wiersza. Jzyk C++ uywa komentarzy w stylu C, a ponadto ma dodatkowy typ komentarzy: //. Para znakw // rozpoczyna komentarz, ktry koczy si znakiem nowego wiersza. W przypadku komentarzy jednowierszowychjest to wygodniejsze ni/* */. Komentarze takie sczsto uywane w ksice.

,Witaj, wiecie!"
Przedstawiamy wreszcie pierwszy program: // Przywitanie za pomoca. C++ #include <iostream> // Deklaracje strumieni using namespace std: int main() { cout "Witaj, wiecie! Mam dzisiaj " 8 " urodziny!" endl; } IIIWiele argumentwjest przekazywanych obiektowi cout za pomoc operatorw ". Drukuje on te argumenty w kolejnoci od lewej do prawej. Specjalna funkcja strumieni wejcia-wyjcia endl wyprowadza wiersz oraz znak nowego wiersza. Korzystajc z klasy strumieni wejcia-wyjcia, mona poczy ze sob cig argumentw, takjak w powyszym przykadzie, co czyni t klas atw w uyciu. W jzyku C tekst zawarty w cudzysowie jest tradycyjnie nazywany acuchem" (ang. string). Poniewa jednak standardowa biblioteka C++ zawiera potn klas o nazwie string, umoliwiajcoperacje na tekstach, w stosunku do tekstu zawartego w cudzysowie bdzie uywany bardziej precyzyjny termin tablica znakowa. Kompilator tworzy miejsce w pamici, przeznaczone do przechowywania tablicy znakowej, zapisujc w nim kod ASCII kadego znaku. Kompilator automatycznie koczy t tablic dodatkow komrk pamici, zawierajc warto 0, oznaczajc koniec tablicy znakowej. Wewntrz tablicy znakowej mona umieszcza znaki specjalne, uywajc do tego celu sekwencji znakw specjalnych (ang. escape sequences). Skadaj si one z lewego
/ / : C02:He11o.cpp

82

Thinking in C++. Edycja polska

ukonika ftK P ktrym nastpuje specjalny kod. Na przykad ^n oznacza znak nowego wiersza. Instrukcja uywanego przez ciebie kompilatora, lub inny dostpny poradnik na temat C, zawiera pelny zbir sekwencji znakw specjalnych nale do nich m.in.: \t (tabulacja), \\ (lewy ukonik) oraz \b (znak cofania). Zwr uwag na to, e instrukcja moe przebiega przez wiele wierszy oraz e koczy si znakiem rednika. \ W powyszej instrukcji cout argumenty bdce tablicami znakowymi s wymieszane ze staymi liczbowymi. Poniewa operator , Uywany z cout, jest przeciony dla rozmaitych znacze, mona wysya do cout argumenty rnych typw, a on domyla si, co zrobi z komunikatem". Czytajc ksik zauwaysz, e pierwszy wiersz kadego pliku jest komentarzem, rozpoczynajcym si znakami pocztku komentarza (zazwyczaj //), po ktrych nastpuje dwukropek. Natomiast ostatni wiersz koczy si komentarzem, po ktrym s umieszczone znaki ,J:~". Jest to technika, ktrej uywam w celu atwego wycigania informacji z plikw rdowych (program, ktry to wykonuje, mona znale w drugim tomie ksiki, dostpnym w witrynie http:/flielion.pl/online/thinking/index.html). Pierwszy wiersz zawiera ponadto nazw i pooenie pliku, dziki czemu mona odwoywa si do niego w tekcie oraz w innych plikach, a take atwo odnale go w kodzie rdowym programw zawartych w ksice (mona go pobra z witryny http:// helion.pl/online/thinking/index.html).

Uruchamianie kompilatora
Po pobraniu i rozpakowaniu plikw rdowych odszukaj program w podkatalogu CO2. Wywoaj kompilator, podajc jako argument plik Hello.cpp. W przypadku prostych, jednoplikowych programw takich jak przedstawiony wikszo kompilatorw poprowadzi ci przez cay proces. Na przykad uywajc kompilatora GNU C++ (dostpny bezpatnie w Internecie), naley napisa:

g++ Hello.cpp
Inne kompilatory bd miay podobn skadni szczegowych informacji poszukaj w dokumentacji uywanego przez siebie kompilatora.

Wicej o strumieniach wejcia-wyjcia


Powyej zostay przedstawione tylko najbardziej elementarne waciwoci klasy strumieni wejcia-wyjcia. Wykorzystaniejej do formatowania wyjcia umoliwia takie operacje, jak formatowanie liczb dziesitnych, semkowych i szesnastkowych. Poniej znajduje sijeszczejeden przykad uycia strumieni wejcia-wyjcia:
/ / : C02:Stream2.cpp // Dodatkowe wasnoci strumieni wejcia-wyjcia #include <iostream> using namespace std;

t Tworzenie i uywanie obiektw int main() { // Okreslame formatu za pomoca. mampulator6w cout << "liczba dziesietna dec << 15 << endl, cout "osemkowa- " << oct << 15 << endl. cout "szesnastkowa " << hex << 15 << endl, cout "liczba zmiennopozycyjna. " 3 14159 << endl. cout < "znak medrukowalny (sterujacy)' " < char(27) << endl,

83

} lll-

W przykadzie zaprezentowano klas strumieni wejcia-wyjcia, drukujc liczby w systemie dziesitnym, semkowym i szesnastkowym z wykorzystaniem manipulatorw strumieni wejcia-wyjcia (ktre same nie drukuj niczego, ale zmieniaj stan strumienia wyjciowego). Formatowanie liczb zmiennopozycyjnych jest okrelane automatycznie przez kompilator. Ponadto do obiektu bdcego strumieniem mona wysa dowolny znak, wykorzystujc rzutowanie (ang. cast) na typ char (char jest typem danych, umoliwiajcym przechowywanie pojedynczego znaku). Rzutowanie to przypomina wywoanie funkcji char(), z argumentem bdcym kodem ASCII znaku. W powyszym programie instrukcja char(27) wysya do obiektu cout znak sterujcy".

czenie tablic znakowych


Wan wasnoci preprocesora jzyka C jest czenie tablic znakowych. Cecha ta jest wykorzystywana w niektrych przykadach zawartych w ksice. Jeeli dwie tablice znakowe przylegaj do siebie i nie ma pomidzy nimi adnych znakw przestankowych, kompilator skleja je ze sob, tworzc pojedyncz tablic znakow. Jest to szczeglnie przydatne, gdy wydruki, zawierajce kod programu, maj ograniczon szeroko: //: C02 Concat cpp // czenie tablic znakowych linclude <iostream> using namespace std. int mainO { cout "Ten tekst jest o wiele za dugi " "aby umieci go w jednym wierszu ale niozna " "go podzieli bez adnych ubocznych skutkw" "\ndopoki pomidzy ssiednimi tablicami " "znakowymi me bdzie adnych znakw " "przestankowych \n".

} /// -

Na pierwszy rzut oka powyszy kod wydaje si bdny, poniewa nie kady jego wiersz koczy si znajomym znakiem rednika. Pamitaj jednak, e C i C++ s jzykami o swobodnej strukturze i chocia na og na kocu kadego wiersza programu widnieje rednik, to w rzeczywistoci wymagane jest, by rednikami koczya si kada instrukcja. Ponadto jest jak najbardziej dopuszczalne, by obejmowaa ona kilka wierszy.

84

Thinking in C++. Edycja polska

Odczytywanie wejcia
Klasy strumieni wejcia-wyjcia zapewniaj rwnie moliwo odczytywania wejcia. Obiektem uywanym do obsugi standardowego wejcia jest cin (skrt od ang. console input wejcie konsoli). Zwykle cin spodziewa si danych wejciowych pochodzcych z konsoli, ale mona przekierowa do niego rwnie inne rda danych. Przykad przekierowania pokazano w dalszej czci rozdziau. Uywanym z cin operatorem strumienia wejcia-wyjcia jest . Operator oczekuje na rodzaj danych wejciowych, odpowiadajcych jego argumentowi. Na przykad jeeli zostanie mu podany argument calkowitoliczbowy, to bdzie oczekiwa na wprowadzenie z konsoli liczby cakowitej. Ilustruje to poniszy przykad:
/ / : C02:Numconv.cpp // Zamiana liczby dziesitnej na semkow i szesnastkow linclude <iostream> using namespace std;
int main() { int number;

cout < Wprowad liczb dziesitna: : ci n > number; cout < "warto w zapisie semkowym = O" < oct number endl: cout < "warto w zapisie szesnastkowym = Ox" < hex number endl; } ///:Program ten zamienia liczby wprowadzone przez uytkownika na ich reprezentacj semkow oraz szesnastkow.

Wywoywanie innych programw


Podczas gdy programy czytajce standardowe wejcie i zapisujce wyniki na standardowe wyjcie s zazwyczaj uywane w skryptach powoki Uniksa lub w plikach wsadowych DOS-u, z programu w C lub C++ mona wywoa dowolny inny program, uywajc funkcji system( ) bdcej standardow funkcj C, zadeklarowan w pliku nagwkowym <cstdlib>: // Wywoanie innego programu finclude <cstdlib> // Deklaracja funkcji "systemO" using namespace std:
/ / : C02:CallHello.cpp

int main() { system("Hello"); } III-.Aby uy funkcji system(), naley jej przekaza tablic znakow o treci, ktra byaby zwykle wpisana po znaku zachty systemu operacyjnego. Moe ona rwnie zawiera argumenty wiersza polece; mona j take utworzy w trakcie pracy programu (zamiast uywa statycznej tablicy znakw, jak w powyszym przykadzie). Polecenie jest wykonywane, a sterowanie powraca do programu.

Rozdzia 2. Tworzenie i uywanie obiektw

85

Program pokazuje, jak atwo jest uywa w jzyku C++ bibliotek, napisanych w czystym C wystarczy jedynie doczy plik nagwkowy i wywoa odpowiedni funkcj. Zgodno w gr" pomidzy C i C++ jest bardzo korzystna w sytuacji, gdy rozpoczynasz nauk jzyka C++, znajc ju C.

Wprowadzenie do acuchw
Mimo e tablice znakowe s cakiem uyteczne, to maj one pewne ograniczenia. S one po prostu zbiorem znakw w pamici, ale gdy zamierzasz wykona jakiekolwiek dziaania, musisz uwzgldni wszystkie szczegy. Na przykad wielko tablicy znakowej jest ustalana w trakcie kompilacji. Jeeli masz ju tablic znakw i chcesz doda do niej kilka kolejnych znakw, to bdziesz musia si cakiem sporo nauczy (wczajc w to dynamiczne zarzdzanie pamici, kopiowanie tablic znakowych oraz ich czenie), zanim osigniesz cel. Takimi wanie kwestiami powinny zajmowa si obiekty. Klasa string (ang. string cig znakw, acuch), naleca do standardu C++, zostaa zaprojektowana dla wszystkich niskopoziomowych operacji na tablicach znakowych (a take po to, je ukrywa), ktrymi zajmowali si wczeniej programici jzyka C. Od czasu powstania jzyka C z powodu tych operacji marnowano czas i popeniano bdy. Mimo e klasie string powicono cay rozdzia w drugim tomie ksiki, jest ona na tyle wana i tak bardzo usprawnia prac, e zostanie wprowadzona w tym miejscu i bdzie uywana w wielu pocztkowych rozdziaach ksiki. Aby uywa acuchw, naley doczy plik nagwkowy C++ <string>. Klasa string znajduje si w przestrzeni nazw std, konieczne jest wic zastosowanie dyrektywy using. Dziki przecieniu operatorw skadnia zwizana z uywaniem acuchw jest do intuicyjna: //: C02:Hel1oStrings.cpp // Podstawy standardowej klasy O+ string finclude <string> linclude <iostream> using namespace std;
int main() {

} III:-

string sl. s2; // Puste acuchy string s3 = "Witaj, wiecie."; // Inicjaizacja string s4("Mam dzisiaj"); // Rwnie imcja izacja s2 = "urodziny"; // Przypisanie acuchowi wartoci sl = s3 + " " -f s4; // czenie acuchw sl += " 8 "; // Doczanie do acucha cout sl + s2 + " ! " endl;

Pierwsze dwa acuchy sl i s2 s pocztkowo puste, podczas gdy na przykadzie acuchw s3 i s4 pokazano dwa rwnowane sposoby inicjalizacji obiektw klasy string za pomoc tablic znakowych (rwnie atwo mona dokona inicjalizacji obiektw klasy string za pomoc innych obiektw tej klasy).

86

Thinking in C++. Edycja polska

Moesz przypisa warto dowolnemu obiektowi klasy string, uywajc operatora =". Powoduje to automatyczn zmian poprzedniej zawartoci acucha na to, co znajduje si po prawej stronie. Do poczenia ze sob acuchw uywa si operatora +", ktry pozwala rwnie na scalenie tablic znakowych z acuchami. Jeeli chcesz doczy acuch lub tablic znakow do innego acucha, to moesz uy o(teratora +=". Zwr wreszcie uwag na to, e strumienie wejcia-wyjcia zawsze wiedz", co zrobi z acuchami, moesz wic po prostu wysa acuch (lub wyraenie, ktrego wynikiem jest acuch, jak np. sl + s2 + "!") bezporednio do cout po to, by go wydrukowa.

Odczytywanie i zapisywanie plikw


W jzyku C proces otwierania plikw i ich przetwarzania wymaga gruntownej wiedzy dotyczcej podstawjzyka, niezbdnej do wykonywania zoonych operacji. Jednake biblioteka strumieni wejcia-wyjcia C++ udostpnia prosty sposb operowania na plikach, dziki czemu zagadnienie to mona wprowadzi znacznie wczeniej ni w przypadku jzyka C. W celu umoliwienia otwierania plikw do odczytu i zapisu trzeba doczy plik <fstream>. Mimo e w takim przypadku automatycznie doczony zostanie rwnie plik <iostream>, to jeeli zamierzamy uywa obiektw cin, cout itd., na og roztropnie jest doczy jawnie plik <iostream>. Aby otworzy plik do odczytu, naley utworzy obiekt ifstream, ktry zachowuje si podobnie do cin. W celu otwarcia pliku do zapisu trzeba natomiast utworzy obiekt ofstream, ktry funkcjonuje podobnie do cout. Po. otwarciu pliku mona czyta z niego lub do niego zapisywa, tak jak w przypadku innych obiektw strumienia wejcia-wyjcia. To takie proste (i na tym, oczywicie, polega przeom). Jedn z najbardziej przydatnych funkcji spord zawartych w bibliotece strumieni wejcia-wyjcia jest getline(), pozwalajca na wczytanie pojedynczego wiersza (zakoczonego znakiem nowego wiersza) do obiektu string4. Pierwszym argumentem jest obiekt ifstream, z ktrego odbywa si czytanie, natomiast drugim argumentem obiekt string. Po zakoczeniu wywoania funkcji obiekt string bdzie zawiera wiersz. Poniej przedstawiono przykad prostego programu, kopiujcego zawarto jednego pliku do drugiego:

//: C02:Scopy.cpp // Kopiowanie jednego pliku do drugiego, po wierszu #include <string> #include <fstream> using namespace std;
W rzeczywistoci istnieje wiele wariantw funkcji getline( ), co zostanie gruntownie omwione w rozdziale powiconym strumieniom wejcia-wyjcia, znajdujcym si w drugim tomie ksiki.

> 2. Tworzenie i uywanie obiektw

87

int main() { ifstream in("Scopy.cpp"); // Otwarcie do odczytu ofstream out("Scopy2.cpp"); // Otwarcie do zapisu string s;

while(getline(in. s)) // Usuwa znak nowego wiersza out s "\n"; // . . . musi doda go z powrotem } ///Aby otworzy pliki, wystarczy po prostu przekaza obiektom ifstream i ofstream nazwy plikw, ktre maj by otwarte lub utworzone, jak w powyszym przykadzie. Wprowadzono tu rwnie nowe pojcie, jakim jest ptla while. Chocia zagadnienie to zostanie opisane szczegowo w nastpnym rozdziale, warto wiedzie, e jej podstawa dziaania polega na tym, e wyraenie znajdujce si w nawiasie po instrukcji while steruje wykonywaniem nastpnej instrukcji (ktr moe by rwnie wiele instrukcji, zawartych w nawiasie klamrowym). Dopki wyraenie w nawiasie (w tym przypadku getline(in, s)) zwraca wynik o wartoci prawda" (ang. true), wykonywana jest instrukcja kontrolowana przez while. Okazuje si, e funkcja getline( ) zwraca warto, ktra moe by interpretowana jako prawda", wwczas gdy pomylnie odczyta kolejny wiersz pliku, a fasz" (ang.false) po osigniciu koca pliku. Tak wic powysza ptla while odczytuje kolejno wszystkie wiersze pliku wejciowego, przesyajc kady z nich do pliku wyjciowego. Funkcja getline( ) wczytuje znaki znajdujce si w kadym wierszu, a do napotkania znaku nowego wiersza (znak kocowy mona zmieni, ale kwestia ta zostanie opisana w rozdziale powiconym strumieniom wejcia-wyjcia, znajdujcym si w drugim tomie ksiki). W kadym razie usuwa ona znak nowego wiersza i nie zapisuje go w docelowym obiekcie typu string. Tak wic jeeli chcemy, aby skopiowany plik mia posta pliku rdowego, musimy ponownie dopisa znaki nowego wiersza, jak przedstawiono w programie. Innym interesujcym przykadem jest kopiowanie caego pliku do pojedynczego obiektu typu string: / / : C02:Fil1String.cpp // Wczytanie caego pliku do pojedynczego acucha #include <string> #include <iostream> #include <fstream>

using namespace std;


int main() {

while(getline(in, lin)) s += lin + "\n";


cout s; } ///:-

ifstream inCTillString.cpp"); string s. line;

Z uwagi na dynamiczn natur typu string nie trzeba zaprzta sobie uwagi tym, ile pamici naley przydzieli acuchowi wystarczy dodawa do niego dane, a acuch bdzie powiksza si, by pomieci wszystko to, co w nim umieszczono.

88

Thinking in C++. Edycja polska

Jedn z korzyci, wynikajcych z umieszczenia w acuchu caego pliku, jest ta, e klasa string posiada wiele funkcji, sucych do przeszukiwania i przetwarzania acuchw, co umoliwia modyfikacj caego pliku jako pojedynczego acucha. Jednake wie si to z pewnymi ograniczeniami. Na przykad czsto wygodnie jest traktowa plikjako zbir wierszy, a niejakojeden wielki blok tekstu. atwiej dodasz numeracj wierszy, jeeli kady jego wiersz bdzie znajdowa si w oddzielnym obiekcie typu string. Aby to uczyni, niezbdnejest inne podejcie.

Wprowadzenie do wektorw
Za pomocacuchw moemy wypeni obiekt typu string, nie wiedzc, ile wymaga to pamici. Problem z wczytywaniem poszczeglnych wierszy pliku do oddzielnych obiektw typu string polega na tym, e na pocztku nie wiadomo, ile acuchw bdzie do tego potrzebnych okae si to dopiero po przeczytaniu caego pliku. Aby rozwiza ten problem, niezbdny jest jaki kontener, ktry bdzie si automatycznie powiksza, przechowujc dowolnliczb obiektw typu string. Dlaczego waciwie naley si ogranicza do przechowywania acuchw? Okazuje si, e nierzadko zdarzaj si problemy polegajce na tym, e w chwili pisania programu nie jest znana liczbajakich obiektw. Ponadto nazwa kontener" sugeruje, e byby on bardziej uyteczny, gdyby mg przechowywa w ogle dowolny rodzaj obiektow\ Na szczcie standardowa biblioteka C++ posiada gotowe rozwizanie, ktrym sstandardowe klasy kontenerowe. Tojedna z lokomotyw standardu C++. Kontenery i algorytmy, znajdujce si w standardowej bibliotece C++, s czsto mylone z STL. Standardowa biblioteka szablonw (ang. Standard Template Library STL) jest nazw uyt przez Alexa Stepanova (pracujcego wwczas w firmie Hewlett-Packard) podczas prezentacji Komitetowi Standaryzacyjnemu C++ opracowanej przez siebie biblioteki. Odbya si ona w ramach spotkania w San Diego (Kalifornia) wiosn 1994 roku. Nazwa ta przyja si i upowszechnia od czasu, gdy HewlettPackard zdecydowa si udostpni bibliotek publicznie. W tym czasie komitet doczy j do standardowej biblioteki C++, dokonujc w niej wielu zmian. Rozwj standardowej biblioteki szablonw kontynuowano w firmie Silicon Graphics (SGI patrz http://www.sgi.conrfTechnology/STL). Biblioteka SGI STL rni si od standardowej biblioteki C++ wieloma szczegami. A zatem, wbrew powszechnemu mniemaniu, standardowa biblioteka C++ nie zawiera" STL. Moe to by nieco mylce, poniewa kontenery i algorytmy zawarte w standardowej bibliotece C++ maj te same korzenie (i zazwyczaj takie same nazwy) co SGI STL. W ksice posu si terminami: standardowa biblioteka C++", kontenery standardowej biblioteki" itp., unikajc terminu STL". Mimo e implementacja kontenerw oraz algorytmw w standardowej bibliotece C++ stosuje pewne zaawansowane pojcia i jej peny opis zajmuje dwa due rozdziay drugiego tomu ksiki, biblioteka ta moe by skutecznie wykorzystana nawet przez osob, ktra nie posiada gruntownej wiedzy na jej temat. Jest tak przydatna, e najbardziej podstawowy standardowy kontener vector zosta wprowadzony ju

Rozdzia 2. Tworzenie i uywanie obiektw


M < ^ " * " " " ^ ~ ~ " ~

89

w biecym rozdziale i jest uywany w dalszej czci ksiki. Jak si przekonasz, moesz dokona bardzo wiele, uywajc po prostu podstawowych waciwoci klasy vector i nie zwaajc na jej wewntrzn implementacj (zapewne pamitasz, e jest to wana cecha programowania obiektowego). Kiedy dowiesz si nieco wicej na temat tego i innych kontenerw dziki lekturze rozdziaw powiconych bibliotece standardowej w drugim tomie ksiki, zrozumiesz zapewne, e w programach znajdujcych si w pocztkowych rozdziaach ksiki nie uyto wektorw dokadnie w taki sposb, jak zrobiby to dowiadczony programista C++. Przekonasz si, e w wikszoci przypadkw prezentowany tutaj sposb ich uycia jest w zupenoci wystarczajcy. Klasa vector jest szablonem, co oznacza, e moe ona by w efektywny sposb zastosowana do rnych typw. Moemy zatem utworzy wektor ksztatw, wektor kotw, wektor acuchw itp. Uywajc szablonu, mona utworzy klas czegokolwiek". Aby zgosi kompilatorowi, z jak klas bdzie on mia do czynienia (w naszym przypadku co bdzie przechowywa wektor), naley umieci nazw danego typu w nawiasie ktowym", czyli w nawiasie zoonym ze znakw <" i ,p>". A zatem wektor acuchw (obiektw klasy string) powinien by zapisany jako vector<string>. W rezultacie otrzymasz specjalizowany wektor, przechowujcy jedynie obiekty kasy string, i prba umieszczenia w nim innego elementu spowoduje zgoszenie przez kompilator komunikatu o bdzie. Poniewa klasa vector reprezentuje pojcie kontenera", musi istnie jaki sposb umieszczania w nim elementw i ich wydobywania. Aby doda na kocu wektora zupenie nowy element, naley uy funkcji skadowej push_back( ) (poniewa jest to funkcja skadowa, trzeba uy .", aby wywoa j dla konkretnego obiektu). Nazwa tej funkcji wydaje si nieco wymylna" push_back( ) (ang. push back do z tyu) w przeciwiestwie do prostszej, jak np. put" (ang. put umie) czego powodem jest fakt, e istniej rwnie inne kontenery i funkcje skadowe umoliwiajce umieszczanie w nich nowych elementw. Na przykad funkcja skadowa insert() (ang. insert wstaw) pozwala na wstawienie czego do rodka kontenera. Klasa vector rwnie j zawiera, ale zastosowanie tej funkcji jest nieco bardziej skomplikowane i dlatego wyjanimyje dopiero w drugim tomie ksiki. Istnieje rwnie funkcja push_front( ) (ang. pushfront do z przodu), nie naleca do klasy vector, ktra wstawia elementy na pocztku. Istnieje jeszcze o wiele wicej funkcji skadowych klasy vector i znacznie liczniejsze kontenery znajduj si w standardowej bibliotece C++ to niewiarygodne, ile mona dokona, wiedzc o kilku prostych waciwociach. Tak wic mona dodawa do wektora nowe elementy za pomoc funkcji push_ back( ), a l e j a k j e pniej z niego wydoby? Rozwizaniejest przemylniejsze i bardziej eleganckie dziki zastosowaniu przecienia operatora wektor przypomina tablic. Tablice (opisane dokadniej w nastpnym rozdziale) s typem danych, dostpnym praktycznie w kadymjzyku programowania, wic zapewne sci one znane. Tablice s agregatami, co oznacza, e skadaj si one z pewnej liczby poczonych ze sob elementw. Cech wyrniajc tablice jest to, e ich elementy s tej samej wielkoci i uporzdkowano je w taki sposb, e nastpuj kolejno po sobie. Co najwaniejsze elementy tablicy mog zosta wybrane przez indeksowanie", a zatem mona wskaza element o numerze n" i zostanie on udostpniony, zazwyczaj szybko.

90

Thinking in C++. Edycja polska

Mimo e istniej pod tym wzgldem wyjtki wrd jzykw programowania, indeksowanie uzyskuje si na og za pomoc nawiasw kwadratowych, tak wic jeeli masz np. tablic a i chcesz otrzyma jej pity element, to zapisujesz a[4] (zwr uwag na to, e indeksowanie rozpoczyna si zawsze od zera). Tak zwizy i efektywny zapis indeksowania uzyskano w klasie vector dziki przecieniu operatorw w sposb podobny do tego, w jaki operatory " i " zostay wprowadzone do strumieni wejcia-wyjcia. Kolejny raz okazuje si, e nie trzeba wiedzie, w jaki sposb zaimplementowano przecienia (temat ten przedstawiono w nastpnych rozdziaach), ale warto mie wiadomo, e kryj si za tym jakie czary, sprawiajce, e nawiasy kwadratowe dziaajrazem z klasavector. Majc to na uwadze, moemy ju zaprezentowa program wykorzystujcy wektory. Aby uywa obiektw klasy vector, naley doczy plik nagwkowy <vector>:
// Kopiowanie calego pliku do wektora acuchw #include <string> #include <iostream>
/ / : C02:Fillvector.cpp

#include <fstream> #include <vector> using namespace std;

int main() { vector<string> v; ifstream in("Fillvector.cpp"); string line; while(getline(in, line)) v.push_back(line); // Dodanie wiersza na kocu // Dodanie numerw wierszy: for(int i = 0; i < v.size(); i++) cout i ": " v[i] endl;
Wiksza cz powyszego programu jest podobna do jego poprzedniej wersji otwierany jest plik, a jego wiersze s kolejno wczytywane do acuchw (obiektw typu string). Te acuchy sjednak dopisywane na kocu wektora v. Po zakoczeniu ptli while cay plik znajduje si w pamici wewntrz obiektu v. Nastpna instrukcja programu nosi nazw ptli for. Jest ona podobna do ptli while, z wyjtkiem tego, e zapewnia ona pewn dodatkow kontrol. Po instrukcji for nastpuje nawias, zawierajcy wyraenie sterujce", podobnie jak w ptli while. Jednake wyraenie to skada si z trzech czci: pierwszej inicjalizujcej, drugiej sprawdzajcej, czy naley ju wyj z ptli, i trzeciej zmieniajcej co, na og po to, by przej przez sekwencj elementw. W programie uyto ptli for w najczciej spotykany sposb. Cz inicjalizujca, int i = 0, tworzy cakowit zmienn i, uywan w charakterze licznika ptli, i nadaje jej zerow warto pocztkow. Cz sprawdzajca oznacza, e aby pozosta w ptli, i powinno by mniejsze ni liczba elementw wektora v. Liczb t uzyskuje si za pomoc funkcji skadowej size( ). ktr niejako przemyciem" do programu, ale ma ona do oczywiste znaczenie (ang. size wielko, rozmiar). W ostatniej czci wykorzystano skrt stosowany wjzykach C i C++ operator automatycznej inkrementacji", zwikszajcy warto

Rozdzia 2. * Tworzenie i uywanie obiektw

91

zmiennej i o jeden. W rezultacie zapis i++ oznacza, pobierz warto i, zwiksz j ojeden i umie rezultat z powrotem w zmiennej i". Tak wic ostatecznym efektem dziaania ptli for jest utworzenie zmiennej i, a nastpnie zmiana jej wartoci od zera do liczby elementw wektora pomniejszonej o jeden. Dla kadej wartoci i wykonywanajest instrukcja cout, zapisujca wiersz zoony z wartoci zmiennej i (przeksztaconej w cudowny sposb w tablic znakow za pomoc instrukcji cout), dwukropka, spacji, wiersza pochodzcego z pliku i znaku nowego wiersza, wyprowadzanego przez endl. Po kompilacji i uruchomieniu programu przekonasz si, e efektem jego dziaaniajest dopisanie w pliku numerw poszczeglnych wierszy. Z uwagi na sposb, w jaki operator " dziaa ze strumieniami wejcia-wyjcia, mona atwo zmodyfikowa powyszy program, tak aby zamiast na wiersze dzieli on plik wejciowy na sowa, oddzielone od siebie odstpami: //: C02:GetWords.cpp // Podzia p l i k u na sowa oddzielone odstpami #include <string> #include <iostream> #include <fstream> #include <vector> using namespace std; int main() { vector<string> words; ifstream in("GetWords.cpp"); string word; while(in word) words.push_back(word); for(int i = 0; i <words.size(); i++) cout words[i] endl;
III-

Wyraenie: while(in word) pobiera z wejcia za kadym razem jedno sowo", a gdy ma ono warto fasz", oznacza to, e osignity zosta koniec pliku. Oddzielanie od siebie sw za pomoc odstpw jest do toporne, ale pomocne w prostym przykadzie. W dalszej czci ksiki zapoznasz si z bardziej wyrafinowanymi przykadami, pozwalajcymi na podzia danych wejciowych w dowolny sposb. Aby pokaza, jak atwo jest uywa wektorw dowolnego typu, w poniszym przykadzie zosta utworzony wektor liczb cakowitych (vector<int>): //: C02:Intvector.cpp // Tworzenie wektora zawierajcego liczby cakowite #include <iostream> #include <vector> using namespace std; int main() { vector<int> v; for(int i = 0; i < 10; i++) v.push_back(i}; for(int i = 0; i < v.size(); i++)

92

Thinking in C++. Edycja polska cout v[1] ". "; cout endl ; for(int 1 = 0; i < v.sizeO: i++) v[i] = v[i] * 10; // Przypisanie for(int i = 0; i < v.sizeO; i++) cout v[i] ", "; cout endl ; W celu utworzenia wektora przechowujcego inny typ danych naley poda ten typ jako argument szablonu (w nawiasie ktowym). Szablony oraz dobrze zaprojektowane biblioteki szablonw majw zamyle ich twrcw by atwe w uytku. Powyszy przykad ilustruje rwnie innwancech klasy vector. W wyraeniu:
v[i] = v[i] * 10;

uycie wektora nie jest ograniczone tylko do umieszczania w nim elementw i pobierania ich z powrotem. Mona take dokona przypisania (a wic i zmiany) do dowolnego elementu wektora, uywajc do tego celu rwnie operatora indeksowania w postaci nawiasu kwadratowego. Oznacza to, e wektor jest elastycznym notesem" oglnego przeznaczenia, umoliwiajcym prac z grupami obiektw. Oczywicie skorzystamy z tego w nastpnych rozdziaach.

Podsumowanie
Celem rozdziau byo pokazanie, jak atwe moe by programowanie obiektowe, pod warunkiem, e kto wykona za ciebie prac, polegajc na zdefiniowaniu obiektw. W takim przypadku musisz doczy plik nagwkowy,' utworzy obiekty, a nastpnie wysya do nich komunikaty. Jeeli uywane przez ciebie typy s efektywne i odpowiednio zaprojektowane, moesz oszczdzi sobie wiele pracy, a powstay program bdzie rwnie wydajny. Przy okazji prezentacji atwoci programowania obiektowego z uyciem bibliotek klas przedstawiono rwnie najbardziej podstawowe i uyteczne typy w standardowej bibliotece C++ rodzin strumieni wejcia-wyjcia (szczeglnie tych czytajcych z konsoli oraz plikw i zapisujcych tam informacje), klas string i szablon vector. Przekonae si ju, jak atwo mona ich uywa i zapewne moesz teraz wyobrazi sobie wiele projektw, ktre mgby z ich pomoc zrealizowa, a w rzeczywistoci 5 ich moliwoci s jeszcze wiksze . Mimo e w pocztkowych rozdziaach ksiki bdziemy wykorzystywa jedynie ograniczon funkcjonalno tych narzdzi, trzeba pamita, e i tak stanowi one duy krok naprzd w stosunku do prostoty nauki niskopoziomowego jzyka, jakim jest C (nauka niskopoziomowych cech C ma wprawdzie pewien walor edukacyjny, lecz jest rwnie czasochonna). Ostatecznie znacznie zwikszysz swoj wydajno, jeeli uyjesz obiektw do obsugi niskopoziomowych Jeeli jeste szczeglnie zainteresowany wszystkim, co mona zrobi z tymi oraz innymi skadnikami standardowej biblioteki, zajrzyj do drugiego tomu ksiki, dostpnego w witrynie www.BruceEckel.com, http://helion.pl/online/thinking/index.html, atake www.dinkwnware.com.

gozdzia 2. Tworzenie i uywanie obiektw

93

zagadnie. Mimo wszystko gwnym atutem programowania obiektowego jest ukrywanie szczegw, umoliwiajce pewien rozmach. Jednake poruszajc si nawet na tak wysokim poziomie, do jakiego aspiruje programowanie obiektowe, nie mona pomin pewnych podstawowych zagadnie jzyka C. I wanie one zostan opisane w nastpnym rozdziale.

wiczenia
Rozwizania wybranych wicze mona znale w dokumencie elektronicznym: The Thinking in C++ Annotated Solution Guide, dostpnego za niewielk opat w witrynie http://www. BruceEckel. com. 1. Zmodyfikuj program Hello.cpp w taki sposb, by drukowa twoje imi i wiek (lub rozmiar buta albo wiek twojego psa). Skompiluj i uruchom ten program. 2. Na podstawie programw Stream2.cpp oraz Numconv.cpp napisz program, ktry pyta o promie koa, a nastpnie drukuje jego powierzchni. Do wyznaczenia kwadratu promienia moesz uy operatora *". Nie prbuj wyprowadza wartocijako semkowej lub szesnastkowej (dziaajone wycznie z typami cakowitymi). 3. Napisz program, ktry otwiera plik i liczy zawarte w nim, oddzielone odstparni sowa. 4. Napisz program, ktry wyznacza liczb wystpie okrelonego sowa w pliku (uyj operatora ==" klasy string do znalezienia tego sowa). 5. Zmie program Fillvector.cpp w taki sposb, by drukowa wiersze od koca od ostatniego do pierwszego. 6. Zmie program Fillvector.cpp w taki sposb, by czy wszystkie elementy znajdujce si w wektorze w pojedynczy acuchjeszcze przedjego wydrukowaniem, ale nie prbuj dodawa numeracji wierszy. 7. Wywietl plikpojednym wierszu, oczekujc po kadym z nich nacinicia przez uytkownika klawisza Enter". 8. Utwrz wektor vector<float> i umie w nim 25 liczb zmiennopozycyjnych, uywajc do tego ptli for. Wywietl ten wektor. 9. Utwrz trzy obiekty vector<float> i wypenij dwa pierwsze z nich w taki sposb, jak w poprzednim wiczeniu. Napisz ptl for, ktra doda odpowiadajce sobie elementy pierwszych dwch wektorw, zapisujc wynik w odpowiednim elemencie trzeciego wektora. Wywietl zawarto wszystkich trzech wektorw. Utwrz wektor vector<float> i umie w nim 25 liczb, jak w poprzednich wiczeniach. Nastpnie podnie do kwadratu kad z liczb i umie wynik z powrotem w tym samym miejscu wektora. Wywietl zawarto wektora przed wykonaniem nuioenia i po nim.

94

Thinking in C++. Edycja polska

Jzyk C w C++
Z uwagi na to, e jzyk C++ w duej mierze bazuje na jzyku C, trzeba najpierw pozna skadni C, aby programowa w C++ podobnie jak naley nabra pewnej biegoci w algebrze, by zmierzy si z analiz matematyczn. Jeeli nigdy wczeniej nie zetkne si z jzykiem C, to w tym rozdziale znajdziesz podstawowe informacje dotyczce rodzaju jzyka C uywanego w C++. Jeeli znasz jzyk C, opisany w pierwszym wydaniu ksiki Kernighana i Ritchiego (okrelany czsto jako K&R C), to w C++ oraz w standardowym C odnajdziesz zarwno nowe waciwoci jzyka, jak i takie, ktre rni si od ju poznanych. Jeeli natomiast znasz ju standardowe C, warto przejrze ten rozdzia, poszukujc tematw zwizanych z C++. Zwr uwag na to, e niektre z wprowadzonych w rozdziale podstawowych waciwoci jzyka C++ s pokrewne waciwociom jzyka C. Cz z nich ilustruje rwnie zmian w ujciu pewnych kwestii w porwnaniu z podejciem stosowanym w jzyku C. Bardziej zaawansowane waciwoci jzyka C++ zostan przedstawione w dalszych rozdziaach ksiki. Rozdzia ten stanowi do krtki opis konstrukcji jzyka C i wprowadzenie do pewnych podstawowych konstrukcji C++. Przyjto zaoenie, e maszju dowiadczenie wprogramowaniu wjakim innymjzyku.

Rozdzia 3.

Tworzenie funkcji
W dawnym" C (przed powstaniem standardu jzyka) mona byo wywoa funkcj z dowoln liczb argumentw dowolnego typu, nie wywoujc sprzeciwu kompilatora. Wszystko wydawao si poprawne, dopki nie zosta uruchomiony program. Pojawiay si wwczas zagadkowe wyniki (lub gorzej program przerywa prac) i nie docieray adne wskazwki na temat tego, co moe by ich powodem. Brak pomocy w przekazywaniu argumentw oraz enigmatyczne bdy s prawdopodobnie jednym z powodw, dla ktrych jzyk C mianowano asemblerem wysokiego poziomu". Programici uywajcy C przed wprowadzeniem standardujzyka po prostu si do tego przyzwyczaili.

96

Thinking in C++. Edycja polska W standardowym C oraz C++ jest uywane prototypowanie funkcji (ang. function prototyping). Powoduje ono konieczno opisania typw argumentw funkcji, zarwno w j e j deklaracji,jak i w definicji. Opis ten stanowi wianie w prototyp". W czasie wywoania funkcji kompilator uywa prototypu, by upewni si, e postay jej przekazane waciwe argumenty, a zwracana warto jest poprawnie traktowana. Jeeli programista pomyli si podczas wywoania funkcji, bd ten zostanie wykryty przez kompilator. Wiadomoci o prototypowaniu funkcji znajduj si ju w poprzednim rozdziale (mimo e nie zostao ono w taki sposb nazwane), poniewa posta deklaracji funkcji w C++ wymaga wanie odpowiedniego prototypowania. W prototypie funkcji lista argumentw zawiera typy argumentw, ktre musz by przekazane funkcji, oraz (opcjonalnie w przypadku deklaracji) ich identyfikatory. Kolejno i typy argumentw musz odpowiada sobie w deklaracji, definicji i wywoaniu funkcji. Oto przykad prototypu funkcji zawartego w deklaracji: int translate(float x, float y, float z); Posta deklaracji zmiennych w prototypie funkcji nie jest taka sama, jak zwyka definicja zmiennych. Oznacza to, e nie mona w tym wypadku uy zapisu: float x, y, z. Naley oddzielnie okreli typ kadego jej argumentu. W deklaracji funkcji dopuszczalnajest rwnie nastpujca posta zapisu: int translate(float, float. float); Poniewa kompilator sprawdza w wywoaniu funkcji jedynie typy jej argumentw, identyfikatory sutylko do zwikszenia przejrzystoci kodu w przypadku, gdy kto go czyta. Nazwy argumentw s natomiast wymagane w definicji funkcji, poniewa odwoujemy si do nich w obrbie funkcji: int translate(float x. float y. float z) { x = y = z;

Okazuje si, e ta regua obowizuje tylko w stosunku do jzyka C. W C++ argumenty znajdujce si na licie argumentw definicji funkcji mog pozosta anonimowe. Poniewa nie maj one nazw, nie mona oczywicie uywa ich wewntrz funkcji. Anonimowe argumenty zostay wprowadzone po to, by umoliwi programicie rezerwacj miejsca na licie argumentw". Kady, kto uywa tej funkcji, musi wywoywa j z odpowiednimi argumentami. Jednake osoba, ktra utworzya funkcj, moe zastosowa zarezerwowany" argument w przyszoci, nie powodujc koniecznoci modyfikacji kodu wywoujcego funkcj. Moliwo zignorowania argumentu istnieje rwnie w przypadku, gdy pozostawi si jego nazw na licie argumentw. Powoduje to jednak zgaszanie, podczas kadej kompilacji funkcji, irytujcego ostrzeenia, dotyczcego niewykorzystanego argumentu. Usunicie nazwy argumentu wyeliminuje t niedogodno. W jzykach C oraz C++ s uywane dwa rne sposoby deklaracji listy argumentw. Je^ eli funkcja posiada pust list argumentw, mona zadeklarowa j w jzyku C++ jako <unc(). To informacja dla kompilatora, e funkcja ta nie przyjmuje w ogle argumentw.

3. Jzyk C w

C++

97

Natey zwrci uwag na to, e powyszy zapis oznacza pust list argumentw jedynie w jzyku C++. W jzyku C okrela on natomiast nieokrelon liczb argumentw" (co stanowi luk w tym jzyku, poniewa wycza, w takim przypadku, kontrol ich typw). Zarwno w C, jak i w C++ deklaracja func(void) okrela pust list argumentw, Sk>wo kluczowe void oznacza w tym przypadku nic" ^ak dowiesz si w dalszej czci rozdziau, w przypadku wskanikw moe ono rwnie oznacza brak typu"). fany przypadek, dotyczcy list argumentw, ma miejsce wtedy, gdy nie jest znana liczba albo typy argumentw, z ktrymi zostanie wywoana funkcja w takiej sytuacji mamy do czynienia ze zmienn list argumentw. Taka nieokrelona lista argumentw"jest oznaczana wielokropkiem (...)- Definiowanie funkcji o zmiennej licie argumentw jest znacznie bardziej skomplikowane ni definiowanie zwyczajnej funkcji. Zmiennej listy argumentw mona rwnie uywa w przypadku funkcji o ustalonej liczbie argumentw, jeeli (z jakiego powodu) naley zablokowa kontrol ich typw, wynikajc z prototypowania funkcji. Dlatego wanie powinno si ograniczy stosowanie zmiennej listy argumentw do jzyka C i unika uywania jej w C++ (w ktrym to jzyku, jak si pniej przekonasz, s dostpne znacznie lepsze rozwizania alternatywne). Opis obsugi zmiennych list argumentw mona znale w rozdziale powiconym bibliotekom w dowolnym poradniku dotyczcymjzyka C.

Wartoci zwracane przez funkcje


Wjzyku C++ prototyp funkcji musi okrela typ zwracanej przez ni wartoci (w C pominicie typu zwracanej wartoci powoduje domylne przyjcie typu int). Specyfikacja zwracanego typu poprzedza nazw funkcji. Aby okreli, e funkcja nie zwraca adnej wartoci, naley uy sowa kluczowego void. W przypadku prby zwrcenia przez funkcj wartoci spowoduje to komunikat o bdzie. Poniej znajduje si kilka kompletnych prototypw funkcji:
int fl(void); // Zwraca warto 1nt. nie przyjmuje adnych argumentw int f2O; // Podobnie, jak fl() w przypadku C++, ale nie w standardzie C! float f3(float. int, char, double); // Zwraca warto float void f4(void); // Nie przyjmuje argumentw i nic nie zwraca

Aby przekaza z funkcji jak warto, naley uy instrukcji return. Powoduje ona opuszczenie funkcji i przejcie do miejsca znajdujcego si bezporednio po jej wywoaniu. Jeeli instrukcja return posiadajaki argument, staje si on wartoci zwracan przez funkcj. Jeli funkcja zwraca argument okrelonego typu, to wszystkie zawarte w niej instrukcje return musz zwraca wartoci tego wanie typu. W definicji funkcji moe znajdowa si wicej nijedna instrukcja return: //: C03:Return.cpp // Uycie instrukcji "return" #include <iostream> using namespace std; char cfunc(int i) { if(i = 0) return 'a': if(i =- 1) return 'g';

98

Thinking in C++. Edycja polska

if(i 5) return ' z ' return 'c' ;

int main() { cout "wpisz liczbe cakowita: int val ; cin val ; cout cfunc(val) endl; W funkcji cfunc( ) pierwsza instrukcja if, ktrej wyraenie sterujce osignie warto true (prawda), spowoduje opuszczenie funkcji za pomoc instrukcji return. Zwr uwag na to, e deklaracja tej funkcji nie jest konieczna, poniewa definicja funkcji wystpuje w programie przedjej uyciem w funkcji main( ), dziki czemu kompilator poznajufunkcj, czytajcjej definicj.

Uywanie bibliotek funkcji jzyka C


Wszystkie funkcje znajdujce si w twojej lokalnej bibliotece funkcji C s dostpne w czasie programowania w jzyku C++. Warto zapozna si doWadnie z bibliotek, zanim zdefiniujesz wasn funkcj istnieje dua szansa, e kto rozwiza ju twj problem, powicajc prawdopodobnie znacznie wicej czasu na przemylenie odpowiedniej funkcji orazjej uruchomienie. Sowo przestrogi: wiele kompilatorw zawiera mnstwo dodatkowych, niezwykle przydatnych funkcji, nie nalecych jednak do standardowej biblioteki C. Jeeli masz pewno, e nigdy nie przeniesiesz aplikacji na innplatform (kt moe by tego pewien?), nie wahaj si uyj tych funkcji i uatw sobie za'danie. Jeeli jednak zaley ci na tym, by twoja aplikacja bya przenona, to naley ograniczy si do funkcji biblioteki standardowej. Jeeli musisz wykona dziaania specyficzne dla danej platformy, sprbuj umieci je w oddzielnym fragmencie kodu, dziki czemu, przy przenoszeniu programu na inn platform, bdzie go mona atwo zmieni. W C++ dziaania zalene od platformy mona czsto zamkn w obrbie klasy, co jest rozwizaniem idealnym. Zasada uywana funkcji bibliotecznych jest nastpujca: najpierw odszukaj funkcj w podrczniku programowania (wiele podrcznikw zawiera zarwno spis funkcji, z podziaem na kategorie, jak i alfabetyczny). Opis funkcji powinien znajdowa si w dziale prezentujcym skadni kodu. W grnej czci tego dziau znajduje si zazwyczaj przynajmniej jeden wiersz, zawierajcy dyrektyw #include, ktry przedstawia nazw pliku nagwkowego z prototypem funkcji. Skopiuj ten wiersz do swojego programu, dziki czemu funkcja bdzie prawidowo zadeklarowana. Teraz moesz wywoa funkcj w taki sposb, jak przedstawia to dzia podrcznika opisujcy skadni. Jeeli si pomylisz, kompilator wykryje to, porwnujc twoje wywoanie z prototypem funkcji zawartym w pliku nagwkowym, i powiadomi ci 0 bdzie. Program czcy domylnie przeszuka standardow bibliotek, dziki czemu jedynym dziaaniem, ktry musisz wykona, jest doczenie pliku nagwkowego 1 wywoanie funkcji.

Rozdzia 3. * Jzyk C w

C++

99

Tworzenie wtasnych bibliotek za pomoc programu zarzdzajcego bibliotekami


Moesz poczy napisane przez siebie funkcje, tworzc bibliotek. Wikszo pakietw programistycznych dostarczanych jest wraz z programem zarzdzajcym bibliotekami i grupami programw wynikowych. Kady program zarzdzajcy bibliotekami ma odrbny zbir polece, ale generalna zasada jest nastpujca: aby utworzy bibliotek, utwrz plik nagwkowy zawierajcy prototypy wszystkich funkcji zawartych w bibliotece. Plik ten naley umie w jakimkolwiek miejscu objtym ciek wyszukiwania preprocesora albo w lokalnym katalogu (dziki czemu bdzie mona go znale za pomoc dyrektywy #include "plik-nagtwkowy"), albo w katalogu zawierajcym doczane pliki nagwkowe (mona go odszuka przy uyciu dyrektywy #include <plik-nagft>wkowy>). Nastpnie przeka wszystkie programy wynikowe programowi zarzdzajcemu bibliotek wraz z nazw wynikowej biblioteki (wikszo programw bibliotecznych wymaga typowego rozszerzenia nazwy pliku, takiego jak .lib lub .a). Umie gotow bibliotek w miejscu, w ktrym znajduj si inne biblioteki, dziki czemu bdzie j mg znale program czcy. W czasie uywania swojej biblioteki musisz doda co do wiersza polece, dziki czemu program czcy bdzie mg odnale w tej bibliotece wywoywane przez ciebie funkcje. Szczegowych informacji musisz poszuka w dostpnej dokumentacji, poniewa rni si one w poszczeglnych systemach.

Sterowanie wykonywaniem programu


Rozdzia obejmuje zawarte w C++ instrukcje sterujce wykonywaniem programu. Musiszje pozna, zanim zaczniesz czyta i pisa programy w C lub w C++. Jzyk C++ wykorzystuje wszystkie instrukcje sterujce jzyka C. Obejmuj one instrukcje if-else, while, do-while, for oraz instrukcj wyboru, nazywan switch. C++ pozwala rwnie na uycie niesawnej instrukcji goto, celowo w niniejszej ksice pomijanej.

Prawda i fatez
W celu okrelenia cieki wykonania programu wszystkie instrukcje warunkowe uywaj pojcia prawdziwoci lub faszywoci wyraenia warunkowego. Przykadem wyraenia warunkowego jest: A == B. Wykorzystuje ono operator warunkowy == do sprawdzenia, czy zmienna Ajest rwnowana zmiennej B. Wyraenie ma warto logiczn true (prawda) lub false (fasz). S one sowami kluczowymi tylko w C++ w jzyku C wyraenie jest prawdziwe", jeeli ma niezerow warto. Innymi operatorami warunkowymi s: >, <, >= itd. Wyraenia warunkowe zostay opisane dokadniej w dalszej czci rozdziau.

100

Thinking in C++. Edycja polska

ifelse
Instrukcja if-else moe wystpowa w dwch postaciach z else^oraz bez niego. Wyglda ona nastpujco:
if(wyraenie) instrukcja

lub:
if(wyraenie) instrukcja else instrukcja

Wyraenie" daje warto true (prawda) lub fake (fasz). Instrukcja" oznacza natomiast albo instrukcj prost, zakoczon rednikiem, albo zoon, bedc grup instrukcji prostych, zamknitych w nawiasie klamrowym. Sowo instrukcja" oznacza zawsze instrukcj prost albo zoon. Zwr uwag na to, e instrukcj t moe by rwnie inna instrukcja if, dziki czemu mogby one poczone w acuch. //: C03:Ifthen.cpp
// Demonstracja instrukcji warunkowych if i if-else

#include <iostream> using namespace std; int main() {

cin i: if(i > 5)

cout "wpisz liczbe i nacisnij 'Enter'" endl;

int i:

cout "Jest wiksza niz 5" endl; else cout "Jest mniejsza ni 5 " endl: else cout "Jest rowna 5 " endl;

if(i < 5)

cout "wpisz liczbe i nacisnij 'Enter'" endl; if(i > 5) // "if" jest po prostu kolejna, instrukcj cout "5 < i < 10" endl; else cout "i <- 5" endl: else // Skojarzone z "if(i < 10)" cout "i >- 10" endl;

cin i; if(i < 10)

} III-

Zwyczajowo w instrukcji sterujcej wykonaniem programu stosuje si wcicia, dziki czemu czytelnik moe atwo okreli jego pocztek i koniec'.
Zwr uwag na to, e wszystkie konwencje wydaj si koczy na uznaniu, e naley stosowa jaki rodzaj wci. Walka pomidzy rnymi sposobami formatowania kodu nie ma koca. Dodatek A zawiera opis stylu kodowania uywanego w ksice.

Rozdzia 3. Jzyk C w

C++

101

while
Instrukcje while, do-while oraz for steruj ptlami. Instrukcja jest powtarzana a do momentu, gdy wyraenie kontrolne zwrci warto false. Posta instrukcji while jest nastpujca: while(wyraenie) instrukcja Warto wyraenia jest obliczana jednokrotnie na pocztku ptli, a nastpnie przed kadym kolejnym powtrzeniem instrukcji. W poniszym przykadzie program pozostaje wewntrz ptli whUe, a do wpisania tajnego numeru lub nacinicia Control-C. //: C03:Guess.cpp // Odgadywanie numeru (demonstracja instrukcji "while") #include <iostream> using namespace std; int mainC) { int secret = 15; int guess = 0; // "!=" jest warunkiem "nierwne": while(guess != secret) { // Instrukcja zoona cout "odgadnij numer: "; cin guess: cout "Zgadles!" endl; Warunkowe wyraenie instrukcji while nie jest ograniczone do prostego testu, jak w powyszym przykadzie moe by ono dowolnie skomplikowane, dopki zwraca warto true lub false. Moesz nawet zobaczy kod, w ktrym ptla nie ma ciaa, tylko sam rednik: while(/* Zrob duzo w tym miejscu */) W takim przypadku programista napisa wyraenie warunkowe nie tylko w celu jego sprawdzenia, ale rwnie po to, by wykonao ono jak prac.
}

do-while
Postaci instrukcji do-while jest: instrukcja while(wyraenie); Instrukcja do-while tym rni si od while, ejest zawsze wykonywana przynajmniej jednokrotnie, nawet jeeli warto wyraenia jest ju za pierwszym razem faszywa.
do

Thinking in C++. Edycja polska W przypadku zwykego while, jeeli warunek za pierwszym razem jest faszywy, instrukcja niejest nigdy wykonywana. Jeeli ptla do-while zostanie uyta w programie Guess.cpp, to zmiennej guess nie trzeba bdzie nadawa fikcyjnej wartoci pocztkowej. Zostanie ona bowiem zainicjowana instrukcj cin, zanim jej warto zostanie sprawdzona: / / : C03:Guess2.cpp // Program odgadujcy, wykorzystujcy ptl do-while #include <iostream> using namespace std; int main() {. int secret = 15; int guess; // Nie ma potrzeby inicjalizacji zmiennej
do {

cout "odgadnij numer: "; cin guess; // Tu ma miejsce inicjalizacja } while(guess != secret) ; cout "Zgadles!" endl; Wikszo programistw unika instrukcji do-while, uywajc instrukcji while.

Ptla for wykonuje inicjalizacj, poprzedzajcpierwsziteracj. Nastpnie dokonuje sprawdzenia warunku, a na kocu kadej iteracji wykonuje jaki rodzaj kroku". Posta instrukcji for przedstawiono poniej: for(inicjalizacja; warunek; krok) instrukcja Zarwno inicjalizacja, jak i warunek oraz krok mog by puste. Kod inicjalizacji wykonuje si na samym pocztku, tylko jeden raz. Warunek jest sprawdzany przed kadym przebiegiem ptli jeeli jego warto na pocztku bdzie faszywa, instrukcja nigdy nie zostanie wykonana). Na kocu kadej ptli zostanie wykonany krok. Ptle for s zazwyczaj uywane do zada zliczania":

// Wywietlanie wszystkich znakw ASCII // Demonstracja petli "for" #include <iostream> using namespace std;
int main() { for(int i = 0; i < 128; i - i + 1) if (i != 26) // Oczyszczenie ekranu terminalu ANSI cout " wartosc: " i " znak: " char(i) // Konwersja typu endl;

/ / : C03:Charlist.cpp

Rozdzia 3. Jzyk C w

C++

103

Zmienna i zostaa zdefiniowana w miejscu, w ktrym jest uywana, a nie na pocztku bIoku, oznaczonym klamrowym nawiasem otwierajcym {". Stanowi to rnic w stosunku do tradycyjnych jzykw proceduralnych (w tym C), wymagajcych deklaracji wszystkich zmiennych na pocztku bloku. Temat ten zostanie omwiony w dalszej czci rozdziau.

Sfowa kluczowe break i continue


Wewntrz dowolnej konstrukcji tworzcej ptl, takiej jak while, do-while lub for, mona sterowa jej przebiegiem za pomoc instrukcji break i continue. Instrukcja break powoduje opuszczenie ptli, bez wykonywania pozostaych, zawartych w niej instrukcji. Natomiast instrukcja continue zatrzymuje wykonanie aktualnej iteracji, powodujc powrt do pocztku ptli w celu rozpoczcia nowej iteracji. Przykadem uycia instrukcji break i continue jest program bdcy bardzo prostym systemem menu: //: C03:Menu.cpp // Prosty program menu, ilustrujcy // uycie instrukcji "break" i "continue" #include <iostream> using namespace std; int main() { char c; // Oo przechowywania odpowiedzi while(true) { cout "GLOWNE MENU:" endl; cout "1: lewe, p: prawe, w: wyjcie -> ";

cin c: if(c == 'w')

if(c == '1') {

break; // Wyjcie z "while(l)" cout "LEWE MENU:" endl; cout "wybierz a lub b: "; cout "wybrales 'a'" endl; continue; // Powrt do gwnego menu

cin c; if(c == 'a') { } if(c == 'b') { }

cout "wybrales 'b'" endl; continue; // Powrt do gwnego menu

else { cout "nie wybrae a ani b!" endl; continue; // Powrt do gwnego menu

if(c 'p') { cin c;

cout "PRAWE MENU:" endl; cout "wybierz c lub d: ";

L04

Thinking in C++. Edycja polska

} cout "musisz wpisac 1. p albo w!" endl;


}

if(c 'c') { cout "wybrales 'c'" endl; continue; // Powrt do gwnego menu } ifCc - 'd') { cout "wybrales 'd'" endl; continue; // Powrt do gwnego menu } else { cout "nie wybrales c ani d!" endl ; continue; // Powrt do gwnego menu }

} cout "wyjcie z menu. . ." endl ;


III-

Jeeli uytkownik wybierze w gwnym menu w", do wyjcia uywane jest sowo kluczowe break; w przeciwnym razie program bdzie dziaa bez koca. Po kadym z wyborw w podmenu nastpuje powrt na pocztek ptli while za pomoc sowa kluczowego continue. Instrukcja while(true) jest rwnowana poleceniu wykonuj t ptl w nieskoczono". Instrukcja break umoliwia przerwanie tej nieskoczonej ptli po wpisaniu przez uytkownika w".

switch
Instrukcja switch wybierajeden z fragmentw kodu na podstawie wartoci wyraenia cakowitego. Ma ona nastpujc posta: switch(selektor) { case wartosc-calkowital : instrukcja; break; case wartosc-calkowita2 : instrukcja; break; case wartosc-calkowita3 : instrukcja; break; case wartosc-calkowita4 : instrukcja; break; case wartosc-calkowita5 : instrukcja; break; default: instrukcja; Selektor jest wyraeniem dajcym warto cakowit. Instrukcja switch porwnuje warto selektora z kad wartoci calkowit. Jeeli znajdzie odpowiedni warto, wykona stosown instrukcj (prost lub zoon). Jeeli adna warto cakowita nie bdzie odpowiada wartoci selektora, zostanie wykonana instrukcja znajdujca si po sowie kluczowym default. W powyszej definicji kada instrukcja po sowie kluczowym case koczy si instrukcj break, powodujc przejcie wykonywania programu na koniec instrukcji switch (instrukcj t koczy klamra, zamykajca nawias). Jest to typowy sposb tworzenia instrukcji switch, ale uycie instrukcji break jest opcjonalne. Jeeli ona nie
(...)

Rozdzia 3. Jzyk C w

C++

105

wystpuje, program przeskakuje" do instrukcji znajdujcej si po nastpnym sowie kluczowym case. Oznacza to, e wykonywany jest kod kolejnych instrukcji case, a do napotkania sowa break. Mimo e zazwyczaj takie dziaanie instrukcji jest niepodane, to dla dowiadczonego programisty moe okaza si ono przydatne. Instrukcja switch jest przejrzystym sposobem implementacji wielokierunkowej selekcji (czyli wyboru jednej z wielu cieek wykonywania programu), ale wymaga uycia selektora, ktry w czasie kompilacji programu ma warto cakowit. Na przykad acuch wykorzystany w charakterze selektora nie bdzie dziaa z instrukcj switch. W przypadku selektora bdcego acuchem naleaoby zamiast tego uy cigu instrukcji if, porwnujc acuch w obrbie wyraenia warunkowego. Poprzedni program tworzcy menu stanowi szczeglnie wdziczny przykad uycia instrukcji switch:

// Menu utworzone za pomoc instrukcji switch #include <iostream> using namespace std;
int main() { bool quit - false; // Znacznik wyjcia wnile(quit == false) { cout "Wybierz a, b. c lub w, aby wyjsc: "; char response; cin response; switch(response) { case 'a' : cout "wybrales ' a ' " endl; break; case 'b' : cout " wybrales ' b ' " endl; break ; case 'c' : cout " wybrales ' c ' " endl; break ;

//: C03:Menu2.cpp

case 'w' : cout "wyjcie z menu" endl; quit = true; break; default : cout "Uzyj a.b,c lub w!" endl ;

Znacznik quitjest zmienn typu bool (skrt od ang. Boolean boole'owski, logiczny), bdcego typem spotykanym jedynie w C++. Przyjmuje on tylko wartoci okrelone sowami kluczowymi true (prawda) lub false (fasz). Wybranie w" powoduje ustawienie wartoci true znacznika quit. Kiedy selektor ptli jest obliczany ponownie, warunek quit == fake zwraca fase, co powoduje, e wntrze ptli niejestju wykonywane.

Uywanie i naduywanie instrukcji goto


Sowo kluczowe gotojest dostpne w C++, poniewa istnieje ono w C. Uywanie goto jest czsto odrzucane jako wiadectwo zego stylu programowania, zreszt zazwyczaj susznie. Ilekro uywasz goto, przyjrzyj si uwanie swojemu kodowi i sprawd,

L06

Thinking in C++. Edycja polska czy mona napisa go w inny sposb. W wyjtkowych przypadkach zastosowanie goto moe rozwiza problem, z ktrym nie mona sobie poradzi w inny sposb, ale naley dobrze przemyle ten wybr. Oto odpowiedni przykad:

//: C03:gotoKeyword.cpp // Niesawna instrukcja goto jest dostpna w C++ #include <iostream> using namespace std;
int main() { long val - 0; for(int i = 1; i < 1000; i++) { for(int j = 1; j < 100; j += 10) {
val = i * j;

if(val > 47000) goto bottom; // Instrukcja 'break' spowodowaaby jedynie // przejcie do bardziej zewntrznej instrukcji 'for'

bottom: // Etykieta cout val endl ; Alternatywne rozwizanie polegaoby na ustawieniu znacznika logicznego, ktrego warto byaby sprawdzana w zewntrznej ptli for, i na uyciu w wewntrznej ptli instrukcji break. Jednake w przypadku wielu poziomw ptli for lub while byoby to niewygodne.

Rekurencja
Rekurencja jest interesujc i niekiedy uyteczn technik programowania, polegajc na wywoaniu funkcji z jej wntrza. Oczywicie, jeeli jest to jedyne dziaanie, jakie ta funkcja wykonuje, to bdzie ona wywoywana a do chwili, gdy skoczy si pami. Musi zatem istnie sposb umoliwiajcy wycofanie si z rekurencyjnych wywoa. W poniszym przykadzie uzyskuje si to, okrelajc, e rekurencja koczy si po przekroczeniu przez argument cat znaku Z"2: / / : C03:CatsInHats.cpp // Prosty przykad rekurencji #include <iostream> using namespace std; void removeHat(char cat) { for(char c - ' A ' ; c < cat; c++) cout " "; if(cat <= ' Z ' ) { cout "kot " cat endl; removeHat(cat + 1); // Wywoanie rekurencyjne } else cout "MIAMU!!!" endl Dzikuj Krisowi C. Matsonowi za sugesti wykorzystania tego przykadu.

Rozdzia 3. Jzyk C w
int main() { removeHat( ' A ' ) ; } III-

C++

107

A zatem dopki catjest mniejsze od Z" w funkcji reraoveHat() jest ona wywoywana ze swojego wntrza, czego wynikiem jest rekurencja. Podczas kadego wywoania funkcji removeHat() argument cat jest wikszy o jeden w stosunku do jego aktualnej wartoci, co powodujejego stae zwikszanie. Rekurencja jest czsto uywana do rozwizywania szczeglnie zoonych problemw, poniewa nie ogranicza ona wielkoci" rozwizania funkcja zagbia si po prostu w rekurencj, dopki nie zostanie znalezione rozwizanie.

Wprowadzenie do operatorw
Operatory mog by traktowane jako szczeglny rodzaj funkcji (przekonasz si, e przecianie operatorw w C++ powoduje, e s one ujmowane wanie w taki sposb). Operator dziaa na jednym lub kilku argumentach, a wynikiem jego dziaania jest zupenie nowa warto. Argumenty maj inn posta ni zwyke wywoania funkcji, ale rezultatjest w obu przypadkach taki sam. Jeeli masz ju jakie dowiadczenie programistyczne, to uywane do tej pory operatory powinny by ci znajome. Pojcia dodawania (+), odejmowania i zmiany znaku (-), mnoenia (*), dzielenia (f} i przypisania (=) maj zasadniczo to samo znaczenie w kadym jzyku programowania. Peny zbir operatorw zosta wymieniony w dalszej czci rozdziau.

Priorytety
Priorytety operatorw okrelaj, w jakiej kolejnoci s obliczane wyraenia zawierajce kilka rnych operatorw. W jzykach C oraz C++ wystpuj specjalne reguy wyznaczajce kolejno oblicze. Najatwiej zapamita, e mnoenie i dzielenie s wykonywane przed dodawaniem i odejmowaniem. Poza tym jeeli masz problemy z interpretacj danego wyraenia, to prawdopodobnie bdzie ono rwnie nieprzejrzyste dla kadej innej osoby czytajcej twj kod. Naley zatem uywa nawiasw, by wyranie okreli kolejno wykonywania operacji. Na przykad wyraenie:
A = X + Y - 2/2 + Z;

ma zupenie inne znaczenie ni to samo wyraenie, w ktrym nastpujco uyto nawiasw:


A = X + (Y - 2 ) / ( 2 + Z ) ;

(sprbuj obliczy wynik dla X = 1, Y = 2 i Z = 3).

98

Thlnking in C++. Edycja polska

Automatyczna inkrementacja i dekrementacja


Jzyk C, a zatem rwnie C++, jest peen skrtw. Znacznie uatwiaj one wpisywanie kodu, aIe czasami wyranie zmniejszaj jego czytelno. By moe projektanci jzyka C uznali, e atwiej zrozumie skomplikowane fragmenty kodu, jeeli nie trzeba bdzi wzrokiem po zbyt wielkich wydrukach. Do najbardziej przydatnych skrtw nale operatory automatycznej inkrementacji i dekrementacji. Czsto uywa si ich do zmiany wartoci zmiennych sterujcych liczb wykona ptli. Operatorem automatycznej dekrementacji jest ", co oznacza ,^mniejszenie o jedn jednostk". Operatorem automatycznej inkrementacji jest ++" i oznacza ,^wiekszenie ojednjednostk". Na przykadjeeli Ajest zmienncakowit, to wyraenie ++Ajest rwnowane zapisowi (A = A + 1). Operatory automatycznej inkrementacji i dekrementacji zwracaj jako wynik warto zmiennej. Jeeli operator znajduje si przed zmienn (np. ++A), to najpierw wykonywana jest operacja, a nastpnie wyznaczana zwracana warto. Jeeli natomiast operator znajduje si za zmienn (np. A++), to zwracanajest aktualna warto zmiennej, a nastpnie wykonywana operacja. Na przykad:

//: C03:AutoIncrement.cpp // Prezentacja uycia operatorw automatycznej // inkrementacji i dekrementacji #include <iostream> using namespace std; int main() { int i = 0; int j - 0; cout ++i endl; cout j++ endl; cout --i endl; cout j-- end1;

// // // //

Preinkrementacja Postinkrementacja Predkrementacja Postdekrementacja

Teraz ju zapewne w peni rozumiesz znaczenie nazwy C++" oznacza ona krok naprzd w stosunku do C".

Wprowadzenie do typw danych


Typy danych okrelaj sposb uycia pamici w pisanym przez ciebie programie. Okrelajc typ danych, przekazujesz kompilatorowi informacj, jak utworzy okrelony fragment pamici, a take wjaki sposb wykonywa na nim operacje. Rozrniamy wbudowane i abstrakcyjne typy danych. Wbudowane typy danych to te, ktre wewntrznie ,jozumie" kompilator s one w nim bezporednio zdefiniowane. W jzykach C i C++ wystpuj niemal identyczne wbudowane typy danych. Inaczej jest z typami zdeGniowanymi przez uytkownika, bdcymi klasami utworzonymi przez ciebie albo innego programist. Czsto okrela si je mianem abstrakcyjnych

Rozdzia 3. Jzyk C w

C++

109

typw danych. Kompilator zna wbudowane typy danych od momentu uruchomienia, natomiast uczy si" obsugi abstrakcyjnych typw danych, czytajc pliki nagwkowe zawierajce deklaracje klas (te zagadnienia zostay przedstawione w nastpnym rozdziale).

Podstawowe typy wbudowane


Specyfikacja standardu jzyka C, dotyczca wbudowanych typw danych (ktre dziedziczy C++), nie zawiera informacji o tym, z ilu bitw musi skada si kady typ. Okrela ona natomiast wymagania dotyczce minimalnej i maksymalnej wartoci, ktre musi przechowa kady wbudowany typ danych. W przypadku gdy komputer pracuje w systemie dwjkowym, mona bezporednio przeoy t maksymaln warto na minimaln liczb bitw, niezbdnych do jej przechowywania. Jeeli jednak komputer uywa do reprezentowania liczb na przykad liczb dziesitnych kodowanych dwjkowo (ang. binary-coded decimal BCD), to zajrnowany obszar pamici, niezbdny do przechowywania maksymalnych wartoci kadego typu, bdzie mia inn wielko. Maksymalne i minimalne wartoci, moliwe do wyraenia w rozmaitych typach danych, zdefiniowano w systemowych plikach nagwkowych Iimits.h i float.h (w dyrektywie #include jzyka C++ odwoania do nich umiecisz zazwyczaj w postaci <climits> i <cfloat>). W jzykach C oraz C++ istniej cztery podstawowe wbudowane typy danych, opisane w niniejszej ksice dla komputerw dziaajcych w systemie dwjkowym. Typ char suy do przechowywania znaku i wykorzystuje przynajmniej 8 bitw (bajt) pamici, chocia moe by rwnie wikszy. Typ int przechowuje liczb cakowit i uywa co najmniej dwch bajtw pamici. Typy float i double su do przechowywania liczb zmiennopozycyjnych, zazwyczaj w formacie zmiennopozycyjnym IEEE. Typ float suy do przechowywania liczb zmiennopozycyjnych pojedynczej precyzji, a typ double liczb zmiennopozycyjnych podwjnej precyzji. Jakju wspomniano, zmienne mogby definiowane w dowolnym miejscu ich zasigu i mona je rwnoczenie definiowa i inicjalizowa. Oto przykady definicji zmiennych wykorzystujcych cztery podstawowe typy danych:
/ / : C03:BasiC.Cpp

// Definiowanie czterech podstawowych // typw danych C i C++

int main() { // Definicja bez inicjalizacji: char protein; int carbohydrates: float fiber; double fat; // Rwnoczesna definicja i inicjalizacja: char pizza = 'A'. pop = 'Z'; int dongdings = 100. twinkles = 150. heehos - 200;
float chocolate = 3.14159; // Zapis wykadniczy: double fudge_ripple - 6e-4;

1 1 0

Thinking in C++. Edycja polska W pierwszej czci programu zdefiniowane s zmienne czterech podstawowych typw danych, bez ich inicjalizacji. Zgodnie ze standardem jzyka, warto niezainicjowanej zmiennej jest nieokrelona (zazwyczaj oznacza to, e zawiera ona mieci). W drugiej czci programu zmienne s rwnoczenie zdefiniowane i zainicjowane (ilekroto moliwe, najlepiejjest podawa warto inicjalizujczmiennej w miejscu jej definicji). Zwr uwag na zapis wykadniczy, uyty w stalej 6e-4, oznaczajcy 6 razy 10 podniesione do potgi minus czwartej).

bool, true i false


Zanim do standardu C++ wczono typ bool, stosowano przerne techniki, aby zasymulowa zachowanie zmiennych logicznych. Skutkowao to problemami z przenonoci i mogo stanowi rdo trudno uchwytnych bdw. W standardowym C++ typ bool moe posiada dwa stany, opisywane wbudowanymi staymi: true (ktrmona przeksztaci w warto cakowitrwnjeden) oraz false (ktr mona przeksztaci w warto cakowit rwn zero). Zarwno bool, jak i true oraz false s sowami kluczowymi. Ponadto do typu bool przystosowane zostay niektre elementy jzyka:
Element Wykorzystanie typu bool

&& || ! , ~ ~

Dziaajnaargumentachtypubool,awynikiemjestwartotypu bool. Wynikiemdziaaniajestwartotypubool.


Wartoci wyrae warunkowych sprzeksztacane w wartoci typu bool.

if, for, while, do

? :

Pierwszy argument jest zamieniany w warto typu bool.

Poniewa istnieje ju mnstwo programw, w ktrym do reprezentowania znacznikw jest uywany typ int, kompilator zawsze przeksztaca typ int w typ bool (wartoci niezerowe zamieniane s w true, a zero w false). W idealnym przypadku kompilator powinien zgosi ostrzeenie, sugerujce wprowadzenie poprawek. Praktyk nalec do kategorii zego stylu programowania" jest uywanie operatora ++ do nadawania znacznikowi wartoci true. Jest to wprawdzie nadal dopuszczalne, ale niewskazane, co oznacza, e w bliszej lub dalszej przyszoci zostanie uznane za niedozwolone. Problem polega na tym, e w takim przypadku dokonywane jest niejawne przeksztacenie typu bool w int, zwikszenie wartoci o jeden (by moe wykraczajce poza normalny zakres wartoci bool, czyli zero i jeden), a nastpnie ponowne niejawne przeksztacenie do typu bool. Eto typu bool s rwnie, w razie potrzeby, automatycznie przeksztacane wskaniki (ktre zostan wprowadzone w dalszej czci rozdziau).

Rozdzia 3. Jzyk C w

C++

1 1 1

Specyfikatory
Specyfikatory zmieniaj znaczenie podstawowych typw wbudowanych, znacznie powikszajc ich zbir. Istniejcztery specyfikatory: long, short, signed i unsigned. Specyfikatory long (dugi) i short (krtki) zmieniaj maksymalne i minimalne wartoci, ktre mog by przedstawione za pomoc typu. Zwykly typ int musi by co najmniej wielkoci short. Hierarchia wielkoci dla typw cakowitych jest nastpujca: short int, int, long int. Mona sobie wyobrazi, e wielko wszystkich tych typw bdzie taka sama, dopki bd one spenia wymg dotyczcy wartoci maksymalnych i minimalnych. Na przykad w komputerze, ktrego sowo jest 64-bitowe, wszystkie typy danych mog mie dugo 64 bitw. Hierarchia typw zmiennopozycyjnych jest nastpujca: float, double i long double. long float" nie jest dopuszczalnym typem danych. Nie ma rwnie krtkich" (ang. short) liczb zmiennopozycyjnych. Specyfikatory signed (ze znakiem) i unsigned (bez znaku) okrelaj, w jaki sposb naley traktowa bit znaku w typach cakowitych i znakowych. Liczba typu unsigned nie pamita znaku, w zwizku z czym moe wykorzysta jeden dodatkowy bit, dziki ktremu bdzie w stanie przechowywa dwukrotnie wiksze liczby dodatnie ni liczba typu signed. Specyfikator signed jest przyjmowany domylnie i jego uycie jest konieczne jedynie w przypadku typu char. Typ ten moe by domylnie (lecz nie musi) liczb ze znakiem (signed). Zapis signed char wymusza wykorzystanie bitu znaku. Zamieszczony poniej przykadowy program wywietla wyraon w bajtach wielko poszczeglnych typw danych, uywajc do tego operatora sizeof, ktry zostanie opisany w dalszej czci rozdziau:
/ / : C03:Specify.cpp // Demonstracja uycia specyfikatorw |include <iostream> using namespace std; int main() { char c;

unsigned char cu; unsigned int iu: short int is; short iis; // To samo. co short int unsigned short int isu; unsigned short iisu: long int i l : long i i l ; // To samo. co long int unsigned long int ilu: unsigned long iilu; float f: double d: long double ld: cout

int i:

TWnking in C++. Edycja polska "\n char= " sizeof(c) "\n unsigned char = " sizeof(cu) "\n int = " sizeof(i) "\n unsigned int = " sizeof(iu) "\n short = " sizeof(is) "\fi unsigned short = " sizeof(isu) "\n long = " sizeof(il) "\n unsigned long = " sizeof(ilu) "\n float - " sizeof(f) "\n double = " sizeof(d) "\n long double = " sizeof(ld) endl ; Zwr uwag na to. e wyniki uzyskane po uruchomieniu programu bd prawdopodobnie rne dla poszczeglnych komputerw, systemw operacyjnych i kompilatorw, poniewa ^ak ju wspomniano) jedynym wymaganiem jest, by kady z tych typw by w stanie przechowa wartoci minimalne i maksymalne, okrelone w standardzie. W przypadku modyfikacji liczby cakowitej za pomoc specyfikatora short lub long uycie sowa kluczowego int jest opcjonalne, o czym wiadczy powyszy program.

Wprowadzenie do wskanikw
Uruchamiany program jest najpierw adowany (przewanie z dysku) do pamici komputera. Tak wiec wszystkie ekmenty programu s umieszczane w pamici. Pami jest zorganizowana na og w postaci cigu komrek, do ktrych zazwyczaj odwoujemy si jako do omiobitowych bajtw. W rzeczywistoci jednak wielko kadej z nich zaley od architektury komputera i jest nazywana dugoci slowa. Kade miejsce w pamici mona jednoznacznie odrni od pozostaych za pomoc adresu. W niniejszej ksice zaoymy, e wszystkie komputery wykorzystuj bajty, ktrych adresy rozpoczynaj si od zera i wzrastaj a do wielkoci pamici, w jak wyposaony jest komputer. Poniewa program od chwili uruchomienia znajduje si w pamici, kady jego element ma okrelony adres. Zacznijrny od prostego programu:
/ / : C03:YourPetSl.cpp #include <iostream> using namespace std;
int dog, cat. bird. fish;

void f(int pet) { cout "numer zwierzaka: " pet endl; int main() { int i. j. k;

Rozdzia 3. Jzyk C w

C++

113

W czasie wykonywania programu kadyjego element, nawet funkcja, zajmujejakie miejsce w pamici. Okazuje si, e rodzaj obiektu i sposb jego zdefiniowania okrelaj na og obszar pamici, w ktrym bdzie on umieszczony. W jzykach C i C++ istnieje operator zwracajcy adres elementu. Operatorem tym jest &". Wystarczy poprzedzi znakiem &" nazw identyfikatora, a zwrci on jego adres. Mona zmodyfikowa program YourPetsl.cpp w taki sposb, by drukowa on adresy wszystkich swoich elementw: //: C03:YourPets2.cpp #include <iostream> using namespace std; int dog, cat, bird, fish; void f(int pet) { cout "numer zwierzaka: " pet endl; int main() {

cout "f(): " (long)&f endl; cout "dog: " (long)&dog endl; cout "cat: " (long)&cat endl; cout "bird: " (long)&bird end1; cout "fish: " (long)&fish endl; cout "i: " (long)&i endl: cout "j: " (long)&j endl: cout "k: " (long)&k endl:
III-

int i. j. k;

Zapis (long) oznacza rzutowanie. Oznacza on: nie traktuj tego tak, jakby byo zwykym typem, leczjako typ long". Rzutowanie niejest w tym przypadku konieczne, ale gdyby go nie byo, adresy zostayby wydrukowane w postaci szesnastkowej. Rzutowanie na typ long zwiksza zatem nieco ich czytelno. Rezultat dziaania programu zaley od komputera, systemu operacyjnego i wielu innych czynnikw, ale i tak dostarczy ciekawych wskazwek. Uruchomienie go na moim komputerze dao nastpujce wyniki: f(): 4198736 dog: 4323632 cat: 4323636 bird: 4323640 fish: 4323644 i: 6684160 j: 6684156 k: 6684152 Zmienne zdefiniowane wewntrz funkcji main() znajduj si w innym obszarze pamici ni zmienne zdefiniowane poza t funkcj. Zrozumiesz, dlaczego tak si dzieje, kiedy dowiesz si wicej na temat jzyka. Wyglda rwnie na to, e funkcja f() zajmuje odrbny obszar pamici kodjest w niej zazwyczaj oddzielony od danych.

1 1 4

Thinking in C++. Edycja polska Warto rwnie wspomnie, e kolejno definiowane po sobie zmienne wydaj si umieszczone w cigym obszarze pamici. S one oddzielone od siebie liczb bajtw, wymagan przez ich typ danych. W powyszym przypadku jedynym typem danych jest int, a cat znajduje si cztery bajty dalej ni dog, bird cztery bajty dalej ni cat itd. A zatem w tym komputerze typ int zajmuje cztery bajty. Czy poza interesujcym eksperymentem prezentujcym organizacj pamici mona jeszcze co zrobi z uzyskanym adresem? Najwaniejsz czynnoci, ktr mona wykona, jest zapisanie go w jakiej zmiennej w celu pniejszego uycia. Jzyki C oraz C++ posiadaj specjalny typ zmiennych, przechowujcych adresy. Zmienne te s nazywane wskanikami. Operatorem definiujcym wskanik jest ten sam znak, ktry jest uywany do oznaczania mnoenia *". Kompilator wie", e nie jest to mnoenie, z kontekstu, w ktrym znak ten zosta uyty. Definiujc wskanik, musisz okreli typ wskazywanej przez niego zmiennej. Naley zacz od podania nazwy typu, a nastpnie, zamiast od razu poda identyfikator zmiennej, wstawi gwiazdk pomidzy nazw typu i identyfikator zmiennej, co oznacza: chwileczk to jest wskanik". A zatem wskanik do zmiennej typu int jest nastpujcy: int* 1p: // ip wskazuje na zmienn typu int Poczenie gwiazdki z nazw typu wydaje si logiczne i czytelne, lecz bywa nieco mylce. Moe powodowa skonno do traktowania wskanika do liczby cakowitej" jako oddzielnego typu. Jednake uywajc typu int, lub innego podstawowego typu danych, mona zapisa:

int a. b, c:
natomiast za pomoc wskanikw chciaby zapewne napisa: int* ipa, ipb. ipc; Skadnia jzyka C (odziedziczona rwnie w C++) nie pozwala na taki logiczny zapis. W powyszej definicji tylko ipa jest wskanikiem, ipb i ipc s za zwykymi liczbami cakowitymi (mona by powiedzie, e znak *" jest bardziej zwizany z identyfikatorem zmiennej). A zatem najlepszy rezultat mona osign umieszczajc w kadym wierszu tylko jedn definicj. Dziki temu uzyskuje si wygldajc logicznie skadni, unikajc wprowadzajcego w bd zapisu:
int* ipa; int* ipb; int* ipc;

Wedug generalnego zalecenia dotyczcego programowania w C++, nakazujcego inicjalizowanie zmiennych w miejscu ich definicji, zapis takijest lepszy. Na przykad powysze zmienne nie zostay zainicjowane i zawierajmieci. Znacznie lepiejjest napisa:
int a = 47; int* ipa - &a;

Teraz zmienna a oraz ipa zostay zainicjowane, a wskanik ipa zawiera adres zmiennej a.

Rozdzia 3. Jzyk C w

C++

115

Najprostszym dziaaniem, jakie mona wykona z zainicjowanym wskanikiem, jest zmiana wskazywanej przez niego wartoci. Aby za pomoc wskanika uzyska dostp do zmiennej, naley dokona operacji wyluskania (ang. dereference), uywajc tego samego operatora, ktry posuy dojego zdefiniowania, jak w poniszym przykadzie: *ipa = 100; Teraz zmienna a ma warto 100, a nie 47. S to podstawy dziaania wskanikw za ich pomoc mona zapamita adres, uywajc go nastpnie do zmiany wartoci wskazywanej zmiennej. Pozostaje jednak pytanie: po co modyfikowa wartojednej zmiennej, dokonujc tego za porednictwem drugiej? Na potrzeby niniejszego wprowadzenia odpowiemy na to pytanie, podajc dwa rozlege obszary zastosowa wskanikw: 1. Zmiana obiektw zewntrznych" z wntrza funkcji. Jest to, by moe, najbardziej podstawowe wykorzystanie wskanikw i zostanie ono omwione poniej. 2. Uzyskanie wielu innych przemylnych technik programistycznych, opisanych w rozdziaach w dalszej czci ksiki.

Modyfikacja obiektw zewntrznych


W czasie przekazywania argumentu funkcji zwykle wewntrz funkcji jest tworzona jego kopia. Dziaanie to nosi nazw przekazywania przez warto (ang. pass by value). Zostao ono zaprezentowane w poniszym programie: / / : C03:PassByVa1ue.cpp finclude <iostream> using namespace std: void f(int a) { cout "a = " a endl; a = 5; cout "a = " a endl;

int main() { int x = 47; cout "x = " x endl ; f(x); cout "x = " x endl ;
Zmienna ajest w funkcji f( ) zmienn lokaln, czyli istnieje tylko w czasie wywoania funkcji f( ). Poniewajest to argument funkcji, warto zmiennej ajest inicjowana za pomoc argumentu przekazywanego w trakcie wywoania funkcji. W funkcji main( ) tym argumentem jest zmienna x o wartoci 47, zatem ta wanie warto jest kopiowana do zmiennej a w czasie wywoania funkcji f( ).

1 1 6

Thinking in C++. Edycja polska Po uruchomieniu programu wywietlane snastpujce wyniki:


x a a x = = = =
47 47 5 47

Pocztkowo warto zmiennej x wynosi, oczywicie, 47. Podczas wywoywania funkcji f() jest przydzielany tymczasowy obszar pamici, przechowujcy warto zmiennej a. Nastpnie zmienna a jest inicjowana za pomoc wartoci zmiennej x, co potwierdza wydruk jej wartoci. Mona, oczywicie, zmieni warto zmiennej a i pokaza, e ulega ona zmianie. Jednake po zakoczeniu funkcji f() tymczasowy obszar pamici przydzielony zmiennej a przestaje istnie i, jak wida, jedynym zwizkiem pomidzy zmiennymi a i x byo skopiowanie wartoci zmiennej x do zmiennej a. Z punktu widzenia wntrza funkcji f() zmienna x jest obiektem zewntrznym (termin uywany przez autora). Modyfikacja zmiennej lokalnej nie powoduje, oczywicie, zmiany obiektu zewntrznego, poniewa znajduj si one w pamici pod dwoma rnymi adresami. Jak jednak postpi w sytuacji, gdy chcemy zmieni warto obiektu zewntrznego? W tym wanie celu pomocne s wskaniki. W pewnym sensie wskanikjest synonimem innej zmiennej. A zatem przekazujc do funkcji wskanik, a nie zwyk warto, przesyamy jej synonim zewntrznego obiektu, co pozwala funkcji najego modyfikacj, jak w poniszym przykadzie: //: C03:PassAddress.cpp #include <iostream> using namespace std; void f(int* p) { cout "p = " p endl; cout "*p = " *p endl: *p 5; cout "p = " p endl; int main() { int x = 47; cout "x = " x endl; cout "&x = " &x endl; f(&x); cout "x = " x endl;
} lll:~

Teraz f() pobiera jako swj argument wskanik, dokonujc, w czasie przypisania, operacji wyuskania wskazywanej zmiennej, co powoduje modyfikacj zewntrznego obiektu x. Wyniki dziaania programu s nastpujce:
x = 47 &x = 0065FEOO p = 0065FEOO *p = 47 p = 0065FEOO x-5

Rozdzia 3. Jzyk C w

C++

117

Warto zwrci uwag na to, e warto zmiennej p jest taka sama, jak adres zmiennej x czyli wskanik p faktycznie wskazuje na zmienn x. Jeeli nie wydaje si to dostatecznie przekonujce, wystarczy zauway, e po przypisaniu wartoci 5 zmiennej wyuskanej ze wskanika p warto x zostaje rwnie zmieniona na 5. A zatem przekazanie funkcji wskanika umoliwiajej modyfikacj obiektu zewntrznego. W dalszej czci ksiki zaprezentowano jeszcze wiele innych sposobw uycia wskanikw, ale zmiana wartoci obiektw zewntrznych jest prawdopodobnie ich najbardziej podstawowym i najczciej spotykanym zastosowaniem.

Wprowadzenie do referencji
Zarwno w C, jak i w C++ wskaniki dziaaj podobnie, ale jzyk C++ udostpnia dodatkowy sposb przekazywania funkcjom adresw, zwany przekazywaniem przez referencj (ang. pass by reference). Wystpuje on w wielu innych jzykach programowania, niejest zatem wynalazkiem C++. Na pierwszy rzut oka moe si wydawa, e referencje s niepotrzebne i mona napisa kady program, wcale ich nie uywajc. Na og jest to prawd z wyjtkiem kilku istotnych sytuacji, przedstawionych w dalszej czci ksiki. Oglna zasada dziaania referencji jest taka sama, jak wskanikw prezentowanych w poprzednim przykadzie dziki niej mona przekaza funkcji adres argumentu. Rnica pomidzy uyciem referencji i wskanikw polega na tym, e wywolanie funkcji przyjmujcej referencje jest, pod wzgldem skadni, bardziej przejrzyste ni wywoanie funkcji odbierajcej wskaniki (wanie te rnice skadniowe czyni referencje niezbdnymi w pewnych przypadkach). Jeeli program PassAddress.cpp zostanie zmodyfikowany w taki sposb, by uywa referencji, to bdzie widoczna rnica w sposobie wywoania funkcji f() w obrbie funkcji main(): / / : C03:PassReference.cpp #include <iostream> using namespace std; void f(int& r) { cout "r = " r endl; cout "&r = " &r endl; r = 5; cout "r = " r endl; int main() {

cout "x = " x endl ; cout "&x = " &x endl ; f(x); // Wygllda, jak przekazanie przez warto. // ale jest przekazaniem przez referencje cout "x = " x endl ; W celu przekazania do funkcji f( ) referencji naley zamiast int*, uywanego do przekazania wskanika poda n a j e j licie argumentw int&. Odwoanie si do r" (ktre zwracaoby adres, gdyby zmienna r bya wskanikiem) wewntrz funkcji f( )

int x = 47;

118

Thinking in C++. Edycja polska pozwala na uzyskanie wartoci zmiennej, do ktrej rjest referencj. Jeeli natomiast zmiennej r zostanie przypisana warto, to take zostanie ona przypisana zmiennej, do ktrej jest referencj. W rzeczywistoci jedynym sposobem uzyskania adresu zawartego w zmiennej rjest uycie operatora &". Zasadniczy efekt zastosowania referencji mona wykry wewntrz funkcji main( ) w skadni wywoania funkcji f(), ktra ma posta f(x). Mimo e wyglda to na zwyke przekazanie argumentu przez warto, w wyniku zastosowania referencji pobierany i przekazywanyjest do funkcji adres zmiennej, a nie kopiajej wartoci. Wyniki dziaania programu s nastpujce:

x = 47 &x = 0065FEOO
&r = 0065FEOO

r = 47

r=5 x=5

A zatem przekazywanie argumentw przez referencj pozwala funkcji na modyfikacj obiektu zewntrznego w taki sam sposb, jak zastosowanie wskanikw (mona rwnie zauway, e referencja ukrywa fakt przekazywania adresu zostanie to omwione w dalszej czci ksiki). Tak wic, na potrzeby niniejszego uproszczonego wprowadzenia, mona zaoy, e referencje s odmiennym skadniowo sposobem (czasami okrela si je mianem cukierka skadniowego") osignicia tego samego celu, ktry uzyskuje si za pomoc wskanikw pozwalaj one funkcji na zmian obiektw zewntrznych.

Wskaniki i referencje jako modyfikatory


Powyej zostay przedstawione podstawowe typy danych char, int, float i double oraz specyfikatory signed, unsigned, short i long, ktre mogby uywane w niemal wszystkich kombinacjach wraz z podstawowymi typami danych. Teraz dodalimy wskaniki i referencje, niezalene od podstawowych typw danych i specyfikatorw, dziki czemu liczba moliwych kombinacji ulega potrojeniu: / / : C03:AllDefinitions.cpp // Wszystkie moliwe kombinacje podstawowych typw // danych, specyfikatorw. wskanikw i referencji #include <iostream> using namespace std; void fl(char c. int i, float f, double d); void f2(short int si. long int li, long double ld); void f3(unsigned char uc, unsigned int ui, unsigned short int us1, unsigned long int uli): void f4(char* cp. int* ip, float* fp, double* dp): void f5(short int* sip, long int* lip. long double* ldp); void f6(unsigned char* ucp, unsigned int* uip. unsigned short int* usip, unsigned long int* ulip); void f7(char& cr, int& ir, float& fr, double& dr); void f8(short int&-sir. long int& lir. long double& ldr);

Rozdzia 3. Jzyk C w

C++

119

void f9(unsigned char& ucr, unsigned int& uir. unsigned short int& usir. unsigned long int& ulir); int main() {} l/l-

Wskaniki i referencje dziaajzarwno podczas przekazywaniu obiektw do,jak i na zewntrz funkcji przekonasz si o tym wjednym z nastpnych rozdziaw. Istnieje jeszcze jeden typ, ktrego mona uy ze wskanikami void. Okrelenie typu wskanikajako void* oznacza, e mona do niego przypisa dowolny rodzaj adresu (podczas gdy w przypadku np. typu int* mona przypisa do niego jedynie adres zmiennej typu int). Na przyk}ad: //: C03:VoidPointer.cpp int main() { void* vp; char c; int i; float f; // Do wskanika typu void* mona // przypisa adres DOWOLNEGO typu: vp = &c; vp = &i; vp = &f; vp = &d; } ///:Po przypisaniu do wskanika typu void* tracona jest informacja o typie. Oznacza to, e przed uyciem wskanika koniecznejestjego rzutowanie na odpowiedni typ:
/ / : C03:CastFromVoidPointer.cpp int main() { int i = 99; void* vp = &i;

double d;

// Nie mona dokona wyuskania ze wskanika void*: // *vp = 3; // Bad kompilacji // Przed wyuskaniem, trzeba rzutowa // z powrotem na typ int: *((int*)vp) = 3; } ///:Rzutowanie (int*)vp pobiera wskanik void* i informuje kompilator, by traktowa go jako int*, dziki czemu mona dokona pomylnie operacji wyuskania. Nietrudno zauway, e skadnia tego wyraenia jest nieelegancka, co gorsza wskanik void* tworzy wyom w systemie typwjzyka. Umoliwiaon traktowaniejednego typu tak,jakby by on innym typem, a nawet temu sprzyja. W powyszym przykadzie typ int zosta potraktowany jako int poprzez rzutowanie wskanika vp na typ int*. Nic jednak nie stoi na przeszkodzie, aby rzutowa go na char* albo double*, co spowodowaoby zmian innego obszaru pamici ni przydzielonego zmiennej typu int i w konsekwencji prawdopodobnie zaamanie wykonywania programu. Na og naley unika wskanikw void*, stosujcje tyIko w szczeglnych sytuacjach, przedstawionych w dalszych partiach ksiki. Nie mona stosowa referencji typu void z powodw, ktre zostan wyjanione w rozdziale 11.

Thinking in C++. Edycja polska

Reguy zasigu okrelaj, gdzie zmienna jest dostpna, tworzona i niszczona (czyli gdzie koczy si jej zasig). Zasig zmiennej rozciga si od miejsca, w ktrym zostaa ona zdefiniowana, do pierwszego klamrowego nawiasu zamykajcego nawias rozpoczynajcy si w najbliszym miejscu poprzedzajcym definicj zmiennej. Oznacza to, e zasig jest okrelony przez najblisz" par nawiasw klamrowych. Ilustruje to poniszy przykad: //: C03:Scope.cpp // Zasig zmiennych int main() { int scpl; // scpl jest widoczna { // scpl jest nadal widoczna // ..... int scp2; // scp2 jest widoczna // scpl i scp2 s nadal widoczne

//. .

int scp3; // scpl, scp2 i scp3 s widoczne } // <-- scp3 w tym miejscu przestaa istnie // scp3 nie jest dostpna // scpl i scp2 s nadal widoczne } // <-- scp2 w tym miejscu przestaa istnie // scp3 i scp2 nie s w tym miejscu dostpne // scpl jest nadal widoczna // <-- scpl w tym miejscu przestaa istnie Powyszy przykad pokazuje, kiedy zmienne s widoczne, a kiedy nie s one dostpne (tzn. znajduj si poza. zasigiem). Zmienne mog by uywane jedynie w obrbie swojego zasigu. Zasigi mog by zagniedone, o czym wiadcz nawiasy klamrowe wewntrz innych takich nawiasw. Zagniedanie oznacza, e dostp do zmiennej istnieje w zasigu zawartym w aktualnym zasigu. W powyszym przykadzie zmienna scpl jest dostpna wewntrz wszystkich zasigw, natomiast zmienna scp3 wycznie w najbardziej wewntrznym zasigu.
//. .
// .. . // . ..

Definiowanie zmiennych w locie"


Jak ju wspomniano we wczeniejszej czci rozdziau, pomidzy C i C++ istnieje zasadnicza rnica w sposobie definiowania zmiennych. Oba jzyki wymagaj, by zmienna zostaa zdefiniowana przed uyciem, ale C (i wiele innych jzykw proceduralnych) wymusza definicj wszystkich zmiennych na pocztku zasigu, dziki czemu kompilator tworzc blok moe przydzieli tym zmiennym pami.

Rozdzia 3. Jzyk C w

C++

121

W czasie czytania programu napisanego w jzyku C pierwsz rzecz widoczn na pocztku zasigu jest zazwyczaj blok definicji zmiennych. Deklarowanie wszystkich zmiennych na pocztku bloku wymaga od programisty tworzenia kodu w sposb wymuszony szczegami implementacyjnymi jzyka. Przed napisaniem kodu zwykle nie wiadomo, jakie zmienne zostan uyte. Powoduje to konieczno cigego przeskakiwania na pocztek bloku, co jest nie tylko niewygodne, ale moe by rwnie przyczyn bdw. Takie definicje zmiennych nie dostarczaj na og adnych informacji osobie czytajcej program i s zazwyczaj mylce, poniewa wystpuj z dala od kontekstu, w ktrym zostay uyte. Jzyk C++ (ale nie C) pozwala na definicj zmiennych w dowolnym miejscu ich zasigu, dziki czemu monaje definiowa tu przed uyciem. Ponadto w miejscu definicji zmiennej mona dokona jej inicjalizacji, co pozwala na uniknicie niektrych rodzajw bdw. Taki sposb definiowania zmiennych powoduje, e program jest znacznie atwiejszy do napisania, a take ogranicza bdy, spowodowane nieustannym przemieszczaniem si w obrbie zasigu. Uatwia to zrozumienie kodu, poniewa definicje zmiennych s widoczne w takim kontekcie, wjakim zmienne te s stosowane. Jest to szczeglnie wane w przypadku rwnoczesnej definicji i inicjalizacji zmiennej znaczenie wartoci inicjujcej jest widoczne przy okazji uycia zainicjowanej zmiennej. Zmienne mona rwnie definiowa w obrbie wyrae sterujcych ptli for i while, w warunku instrukcji if oraz w selektorze instrukcji switch. Poniej zamieszczono przykady definiowania zmiennych w locie": //: C03:OnTheFly.cpp // Definiowanie zmiennych "w locie" #include <iostream> using namespace std; int main() { //.. { // Pocztek nowego zasigu int q = 0; // Jzyk C wymaga definicji w tym miejscu //.. // Definicja w miejscu uycia: for(int i = 0; i < 100; i++) { q++; // q pochodzi z szerszego zasigu // Definicja na kocu zasigu: int p = 12; } int p = 1; // Inne p } // Koniec zasigu zawierajcego q i zewntrzne p cout "Wpisz znaki:" endl; while(char c = cin.get() != 'q') { cout c " to nie to" endl; if(char x = c == 'a' | | c = 'b') cout "Wpisales a lub b" endl; cout "Wpisales " x endl; } cout "Wpisz A. B lub C" endl; switch(int i = cin.getO) {

else

122

Thinking in C++. Edycja polska case ' A ' : cout "Bach" endl; break; case ' B ' : cout "Trach" endl; break; case ' C ' : cout "Bum" endl; break; default: cout "To nie jest A, B ani C ! " endl;

Zmienna p jest zdefiniowana w obrbie najbardziej wewntrznego zasigu, tu przed jego kocem definicja ta jest wic zupenie bezuyteczna (ale wiadczy o tym, e zmienne rzeczywicie mog by definiowane w dowolnym miejscu). Ta sama sytuacja ma miejsce w przypadku definicji zmiennej p, znajdujcej si w bardziej zewntrznym zasigu. Definicja zmiennej i w wyraeniu kontrolnym ptli for jest przykadem moliwoci zdefiniowania zmiennej dokadnie w miejscu, w ktrym jest ona potrzebna (mona to zrobi tylko w C++). Zasig zmiennej i jest zasigiem wyraenia kontrolowanego przez ptl for, dziki czemu mona ponownie uy tej zmiennej w nastpnej ptli for. Jest to wygodny i powszechnie stosowany styl programowania w jzyku C++ i jest tradycyjn nazw licznika ptli i nie ma potrzeby wymylania dla niego nowych nazw. Mimo e w prezentowanym przykadzie wystpuj rwnie zmienne definiowane w instrukcjach while, if oraz switch, to definicje takie nie s rwnie popularne, jak definicje znajdujce si w wyraeniach instrukcji for prawdopodobnie z uwagi na ograniczenia skadniowe. Na przykad niemoliwe jest uywanie w nich nawiasw nie mona zatem napisa: while((char c = cin,getO) != ' q ' ) Dodatkowe nawiasy mog wyglda na uyteczne, ale z uwagi na brak moliwoci ich uycia, rezultat wyraenia rni si od spodziewanego. Przyczyn problemu jest to, e operator !=" ma wyszy priorytet ni =", a zatem zmiennej znakowej c przypisywana jest warto typu bool, przeksztacona do typu char. Podczas jej wydruku na wielu terminalach pojawia si znak przedstawiajcy umiechnit buzi. Naley traktowa moliwo definiowania zmiennych w instrukcjach while, if oraz switch jako wynikajc z konsekwencji, ale jedynym miejscem, w ktrym rzeczywicie stosuje si takie definicje (zresztdo czsto), jest ptla for.

Specyfikacja przydziau pamici


Podczas tworzenia zmiennej dostpnychjest wiele opcji okrelajcych czasjej ycia, sposb przydziau pamici, a take to, wjaki sposbjest ona traktowana przez kompilator.

Zmienne globalne
Zmienne globalne s definiowane na zewntrz cia wszystkich funkcji i s dostpne dla wszystkich czci programu (nawet znajdujcych si w innych plikach). Na zmienne globalne nie ma wpywu zasig i istniej one przez cay czas (tj. czas ycia

Rozdzia 3. Jzyk C w C++ zmiennych globalnych dobiega kresu dopiero wraz z zakoczeniem pracy programu). Jeeli zmienna globalna, znajdujca si w jakim pliku, zostaa w innym pliku zadeklarowana przy uyciu sowa kluczowego extern (ang. external zewntrzny), to moe ona by stosowana w pliku zawierajcym t deklaracj. A oto przykad uycia zmiennych globalnych:

//{L} Global2 // Demonstracja zmiennych globalnych finclude <iostream> using namespace std;

//: C03:Global.cpp

int globe; void func(): int main() { globe = 12;

cout globe endl ; func(); // Modyfikuje zmienna globe cout globe endl ;

Poniej przedstawiono tre pliku, ktry odwouje si do zmiennej globe, zadeklarowanej jako zmienna zewntrzna (extern): / / : C03:Global2.cpp {0} // Dostep do zewntrznych zmiennych globalnych extern int globe; // (Odwoanie zostanie okrelone przez program aczacy) void func() { globe = 47;
}
III-

Pami jest przydzielana zmiennej globe na podstawie definicji, znajdujcej si w pliku Global.cpp, i ta sama zmienna jest dostpna w kodzie zawartym w pliku Global2.cpp Poniewa kod zawarty w pliku Global2.cpp jest kompilowany niezalenie od kodu znajdujcego si w pliku Global.cpp, kompilator musi by poinformowany, e zmienna ta istnieje wjakim miejscu. Naley w tym celu uy deklaracji:

extern int globe;


Po uruchomieniu programujest oczywiste, e wywoanie funkcji func( ) rzeczywicie modyfikuje pojedynczy globalny egzemplarz zmiennej globe. W pliku Global.cpp znajduje si specjalny, wymylony przeze mnie, komentarz:

//{L} Global2
Informuje on, e do utworzenia programu konieczne jest doczenie programu wynikowego o nazwie Global2 (nie podano tu rozszerzenia nazwy pliku, poniewa w przypadku programw wynikowych s one rne w rnych systemach). Komentarz znajdujcy si w pierwszym wierszu pliku Global2.cpp zawiera inny specjalny znacznik {O}, ktry znaczy mniej wicej tyle: nie prbuj na podstawie tego pliku tworzy pIiku wykonywalnego; jest on kompilowany po to, by mg by wczony do jakiego innego pliku wykonywalnego". Program ExtractCode.cpp, zawarty w drugim tomie ksiki (mona go pobra z witryny http:Melion.pUonline/thinking/index.html),

124

Thinking in C++. Edycja polska

odczytuje te znaczniki, tworzc odpowiedni plik makefile, dziki czemu wszystko kompiluje si we waciwy sposb (informacje na temat plikw makefile znajduj si na kocu niniejszego rozdziau).

Zmienne lokalne
Zmienne lokalne wystpujw obrbiejakiego zasigu sone lokalne" w stosunku do funkcji. Czsto nazywa si je zmiennymi automatycznymi, poniewa s one automatycznie tworzone przy wejciu do zasigu, a nastpnie automatycznie usuwane po jego opuszczeniu. Uycie sowa kluczowego auto okrela to wyranie, ale zmienne lokalne s zawsze domylnie zmiennymi automatycznymi, nigdy wic nie ma potrzeby deklarowania czegokolwiek za pomoc specyfikatora auto.

Zmienne rejestrowe
Zmienna rejestrowajest rodzajem zmiennej lokalnej. Sowo kluczowe register nakazuje kompilatorowi, aby dostp do tej zmiennej by moliwiejak najszybszy". Sposb zwikszenia szybkoci dostpu zaley od kompilatora, ale, jak wskazuje samo sowo kluczowe, polega on czsto na umieszczeniu zmiennej w rejestrze. Nie ma adnych gwarancji, e zmienna zostanie ulokowana w rejestrze, ani nawet, e zwikszy si szybko dostpu do niej. Jest tojedynie wskazwka dla kompilatora. Uywanie zmiennych rejestrowych podlega pewnym ograniczeniom. Mog by one deklarowane jedynie w obrbie bloku (nie ma globalnych ani statycznych zmiennych rejestrowych). Zmiennych rejestrowych mona jednak uywa jako argumentw formalnych funkcji (tj. mogone wystpowa najej licie argumentw). Na og nie naley prbowa wyrcza optymalizatora kompilatora, poniewa prawdopodobnie sam dokona on lepszej optymalizacji. Dlatego te najlepiej jest unika uywania sowa kluczowego register.

static
Sowo kluczowe static (statyczny) ma wiele rnych znacze. Zazwyczaj zmienne zdefiniowane jako lokalne w stosunku do funkcji przestaj istnie, gdy koczy si jej zasig. Ilekro funkcja jest wywoywana, zmiennym przydzielana jest pami, a ich wartoci s ponownie inicjalizowane. Jeeli potrzebna jest warto, ktra bdzie istniaa przez cay okres pracy programu, mona zdefiniowa zmienn lokaln funkcji jako statyczn i nada jej warto pocztkow. Inicjalizacja ta jest dokonywana tylko podczas pierwszego wywoania funkcji, a dane zachowuj swoje wartoci pomidzy wywoaniami funkcji. Dziki temu funkcja moe pamita" pewne informacje pomidzy swoimi wywoaniami. Nasuwa si pytanie, czy nie mona by uy w takim przypadku zmiennych lokalnych. Urok zmiennych statycznych polega na tym, e nie s one dostpne poza zasigiem funkcji, dziki czemu ich wartoci nie mona przypadkowo zmieni. Pozwala to na ograniczenie zasigu bdw.

Rozdzia 3. Jzyk C w C+ Poniej zamieszczono przykad uycia zmiennych statycznych:


/ / : C03:Static.cpp // Uycie w funkcji zmiennych statycznych #include <iostream> using namespace std; void func() { static int i - 0; cout "i = " ++i endl; int main() { for(int x = 0; x < 10; x++) func(); } III-

125

Za kadym razem, gdy funkcja func( ) jest wywoywana w ptli for, drukuje ona inn warto. Gdyby nie uyto sowa kluczowego static, warto ta wynosiaby zawsze 1". Drugie znaczenie sowa statyczny" jest zwizane z jego pierwszym znaczeniem w zakresie niewidocznoci poza pewnym zasigiem". Kiedy sowo kluczowe static zostanie uyte w stosunku do nazwy funkcji lub zmiennej, znajdujcej si na zewntrz wszystkich funkcji, oznacza ono: ta nazwa niejest dostpna poza biecym plikiem". Nazwa funkcji lub zmiennej jest w takim przypadku lokalna w stosunku do pliku ma ona zasig pliku. Ilustruje to nastpujcy przykad skompilowanie i poczenie poniszych dwch plikw wywoa zgoszenie komunikatu o bdzie przez program czcy:
/ / : C03:FileStatic.cpp // Demonstracja zasigu pliku. Skompilowanie // i poczenie pliku wraz z plikiem FileStatic2.cpp

// wywoa bld programu czcego

// Zasig pliku oznacza dostpno wycznie // w biecym pliku: static int fs;
int main() { fs = 1: } IIIMimo e zmienna fs zostaa w nastpnym pliku okrelona jako zmienna zewntrzna (extern), program czcy nie odnajdziejej, poniewa w pIiku FileStatic.cpp zostaa ona zadeklarowanajako zmienna statyczna.
/ / : C03:FileStatic2.cpp {0} // Prba odwoania do zmiennej fs extern int fs: void func() { fs = 100;

Specyfikator static moe zosta rwnie uyty w obrbie kIasy. Zostanie to jednak wyjanione dopiero w dalszej czci ksiki po przedstawieniu zagadnie dotyczcych tworzenia klas.

126

Thinking in C++. Edycja polska

extern
Slowo kluczowe extern zostao ju pokrtce opisane i zaprezentowane. Informuje ono kompilator, e funkcja lub zmienna istnieje nawetjeeli kompilator nie napotka jej do tej pory w aktualnie kompilowanym pliku. Ta zmienna lub funkcja moe by zdefiniowana w innym pliku lub w dalszej czci biecego pliku. Poniej znajduje si przykad drugiego z wymienionych przypadkw: / / : C03:Forward.cpp // Wyprzedzajce deklaracje funkcji i danych #include <iostream> using namespace std; // Ponisze deklaracje nie dotycz w rzeczywistoci // obiektw zewntrznych, ale kompilator musi // wiedzie, e gdzie one istniej: extern int i ; extern void func() ; int main() { func(); int i; // Definicja danych void func() {
i++;

i = 0;

cout i ; Kiedy kompilator napotyka deklaracj ,,extern int i", dowiaduje si dziki niej, e gdzie musi si znajdowa definicja zmiennej globalnej i. Kiedy dochodzi on do definicji zmiennej i, nie jest widoczna adna inna deklaracja; wie" wic, e znalaz to samo i, zadeklarowane we wczeniejszej czci pliku. Gdyby zmienna i zostaa zadeklarowana jako statyczna (static), oznaczaoby to informacj dla kompilatora, e zmienna ta zostaa zdefiniowanajako globalna (za pomocaextern). Jednake ma ona zarazem zasig ograniczony do pliku (przez uycie static), kompilator zgosiby zatem bd.

czenie
Aby zrozumie dziaanie programw napisanych w C oraz C++, trzeba pozna pojcie lczenia. W wykonywanym programie identyfikator jest reprezentowany przez miejsce pamici, przechowujce zmienn lub skompilowan tre funkcji. czenie opisuje to miejsce z punktu widzenia programu czcego. Istniejdwa rodzaje czenia: tczenie wewntrzne i czenie zewntrzne. czenie wewntrzne oznacza, e miejsce w pamici jest tworzone w celu reprezentowania identyfikatora wycznie na potrzeby kompilowanego pliku. Inne pliki mog wykorzystywa t sam nazw identyfikatora do czenia wewntrznego lub jako nazw zmiennej globalnej, nie powodujc zgaszania konfliktw przez program czcy dla kadego identyfikatorajest bowiem tworzone odrbne miejsce w pamici. czenie wewntrznejest w jzyku C i C++ okrelane za pomoc sowa kluczowego static.

Rozdzia 3. Jzyk C w

C++

127

czenie zewntrzne oznacza, e w pamici tworzone jest pojedyncze miejsce, reprezentujce identyfikator w stosunku do wszystkich kompilowanych plikw. Miejsce to jest tworzone tylko jeden raz, a program czcy musi poradzi sobie z wszystkimi odnoszcymi si do niego odwoaniami. Zmienne globalne oraz nazwy funkcji s czone zewntrznie. Sone dostpne w innych plikach, po zadeklarowaniu ich za pomoc sowa kluczowego extern. Zmienne zdefiniowane poza ciaami funkcji (z wyjtkiem staych w C++) oraz definicje funkcji s domylnie czone zewntrznie. Mona wymusi, by byy one czone wewntrznie, uywajc sowa kluczowego static. Monajawnie okreli, e dany identyfikatorjest czony zewntrznie, definiujc go za pomoc sowa kluczowego extern. W jzyku C nie jest konieczne uywanie sowa kluczowego extern w celu definiowania zmiennych czy funkcji, ale moe okaza si ono nieodzowne w przypadku staych w C++. Zmienne automatyczne (lokalne) istniej tylko tymczasowo na stosie, w czasie wywoania funkcji. Program czcy nie wie nic na temat zmiennych automatycznych, dlatego te nie s one w ogle tczone.

State
W dawnym" C (przed powstaniem standardu jzyka) do utworzenia staej trzeba byo uy preprocesora:

#define PI 3.14159
Kade wystpienie PI byo zastpowane przez preprocesor wartoci 3.14159 (nadal mona uywa tej metody zarwno w C, jak i w C++). W przypadku uycia do tworzenia staych preprocesora, kontrola nad nimi znajduje si poza zasigiem kompilatora. W stosunku do nazwy PI nie jest dokonywana adna kontrola typw, nie sposb rwnie okreli adresu PI (nie mona zatem przekaza wskanika ani referencji do PI). PI nie moe by zmienn typu okrelonego przez uytkownika. Znaczenie PI rozciga si od miejsca, w ktrym zostao ono zdefiniowane, do koca pliku preprocesor nie rozrnia bowiem zasigw. Wjzyku C++ zostao wprowadzone pojcie staych nazwanych, dziaajcych tak samo, jak zmienne z wyjtkiem tego, e ich wartoci nie mog by zmieniane. Modyfikator const informuje kompilator, e nazwa reprezentuje sta. W charakterze staej moe by wykorzystany dowolny typ danych zarwno wbudowany, jak i zdefiniowany przez uytkownika. Jeeli podejmie si prb modyfikacji wartoci czego, co zostao zdefiniowanejako staa, spowoduje to zgoszenie przez kompilator komunikatu o bdzie. Typ staej musi by okrelony, jak pokazano w przykadzie poniej:
const int x - 10:

W standardowym C oraz C++ mona uywa staych nazwanych na listach argumentw, nawet jeeli odpowiadajce im argumenty s wskanikami lub referencjami (oznacza to, e mona okreli adres staej). Stae maj swj zasig, taki sam jak w przypadku zwyczajnych zmiennych, co pozwala na ukrycie" staej wewntrz funkcji i zapewnia, ejej nazwa nie bdzie miaa wpywu na pozostacz programu.

128

Thinking in C++. Edycja polska

Stae pochodz z jzyka C++ i zostay one zaadaptowane na potrzeby standardu C w troch odmienny sposb. W jzyku C kompilator traktuje stae zupenie tak samo jak zmienne, zaopatrzone w specjalny znacznik, oznaczajcy nie zmieniaj mnie". W przypadku definiowania staych w jzyku C kompilator rezerwuje dla nich pami. Dlatego te, jeeli zdefiniuje si w dwch rnych plikach dwie stae o tej samej nazwie (albo umieci definicj staej w pliku nagwkowym), program czcy zgosi bd wynikajcy z konfliktu nazw. Zaoenia dotyczce stosowania staych w jzyku C rni si nieco od zaoe dotyczcych ich wykorzystania w jzyku C++ (krtko mwic, w C++ wygodniej si ich uywa).

Wartoci stafych
W jzyku C++ staa musi zawsze posiada inicjujcj warto (zasada ta nie obowizuje w jzyku C). W przypadku typw wbudowanych wartoci staych mog by wyraone jako liczby dziesitne, semkowe, szesnastkowe, zmiennopozycyjne (liczby dwjkowe nie zostay, niestety, uznane za wane) albojako znaki. W przypadku braku jakichkolwiek dodatkowych wskazwek kompilator zakada, e warto jest liczb dziesitn. Liczby 47, 0 i 1101 s wic traktowane jako liczby dziesitne. Staa rozpoczynajca si od 0 (zero) jest traktowana jako liczba semkowa (o podstawie 8). Liczby semkowe mog si skada wycznie z cyfr od 0 do 7 kompilator interpretuje pozostae cyfry jako bdne. Przykadem prawidowej liczby semkowej jest 017 (odpowiada ona w zapisie dziesitnym liczbie 15). Staa rozpoczynajca si od Oxjest traktowanajako liczba szesnastkowa (o podstawie 16). Liczby szesnastkowe skadaj si z cyfr od 0 do 9 oraz liter od a do f (albo od A do F). Przykadem poprawnej liczby szesnastkowej jest 0x1fe (co odpowiada liczbie 510 w zapisie dziesitnym). Liczby zmiennopozycyjne mog zawiera kropki dziesitne3 oraz wykadniki potgi (reprezentowane przez e, oznaczajce 10 do potgi"). Zarwno kropka dziesitna, jak i e, s opcjonalne. Jeeli do zmiennej zmiennopozycyjnej zostanie przypisana staa, kompilator dokona zamiany jej wartoci na liczb zmiennopozycyjn (proces ten jest jedn z postaci tzw. niejawnej konwersji typw). Dobrym zwyczajem jest jednak uywanie kropki dziesitnej albo litery e w celu przypomnienia osobie czytajcej kod, e uywana jest liczba zmiennopozycyjn wskazwki takiej wymagaj rwnie niektre ze starszych wersji kompilatorw. Poprawnymi liczbami zmiennopozycyjnymi s: le4, 1.0001, 47.0, 0.0 i -1.159e-77. Aby wymusi typ liczby zmiennopozycyjnej, mona do niej dopisa przyrostek. Przyrostki f oraz F oznaczaj typ float, a L oraz 1 typ long double. W kadym innym przypadku liczba jest typu double.

Wjzykach C i C++ cz uamkowa liczby oddzielonajest kropk, a nie stosowanym w Polsce przecinkiem przyp. ttum.

Rozdzia 3. Jzyk C w C++

Stae znakowe s znakami umieszczonymi pomidzy znakami apostrofu, np. 'A', '0', ' '. Zwr uwag na zasadnicz rnic midzy znakiem '0' (kod ASCII 96) i wartoci 0. Znaki specjalne s zapisywane w postaci ukonikowych sekwencji specjalnych", rozpoczynajcych si znakiem lewego ukonika: \n' (nowy wiersz), M' (tabulacja), W (lewy ukonik), V (powrt karetki), V" (cudzysw), V (apostrof) itd. Stae znakowe mona rwnie zapisywa w postaci semkowej: M7' lub szesnastkowej: ^xfF.

volatile
Podczas gdy modyfikator const informuje kompilator: ta warto nigdy si nie zmienia" (co umoliwia mu dokonanie dodatkowej optymalizacji), modyfikator volatile (ulotny) komunikuje kompilatorowi: nigdy nie wiadomo, kiedy zmienia si ta warto", powstrzymujc go przed dokonywaniem jakichkolwiek optymalizacji, zakadajcych stabilno zmiennej. Tego sowa kluczowego naley uywa podczas odczytu wartoci znajdujcych si poza kontrol programu np. rejestru elementu urzdzenia komunikacyjnego. Zmienna oznaczona modyfikatorem volatile jest odczytywana zawsze wtedy, gdy potrzebna jest jej warto, nawet jeeli bya ju odczytywana w poprzednim wierszu programu. Szczeglny przypadek obszaru pamici znajdujcego si poza kontrol programu" wystpuje w programie wielowtkowym. Jeeli obserwujesz jaki znacznik, modyfikowany przez inny wtek lub proces programu, to znacznik ten powinien zosta oznaczony jako volatile po to, by kompilator nie zakada, e moe zoptymalizowa nastpujce po sobie odczytyjego wartoci. Zwr uwag na to, e uycie sowa kluczowego volatile moe nie mie adnego skutku, jeli kompilator nie dokonuje optymalizacji. Moe natomiast uchroni ci przed powanymi bdami, kiedy zaczniesz optymalizowa swj kod (a kompilator rozpocznie poszukiwanie nadmiarowych odczytw wartoci zmiennych). Sowa kluczowe const i volatile zostan opisane dokadniej w jednym z nastpnych rozdziaw.

Operatory i ich uywanie


W rozdziale opisano wszystkie operatory wystpujce w C i C++. Wszystkie operatory zwracaj wartoci na podstawie swoich argumentw. Warto ta jest wyznaczana bez modyfikacji argumentw, z wyjtkiem operatorw: przypisania, inkrementacji i dekrementacji. Modyfikacja argumentu jest nazywana skutkiem ubocznym (ang. side effect). Najczstsze zastosowanie operatorw modyfikujcych swoje argumenty polega na wywoaniu skutku ubocznego, ale naley zapamita, e uzyskan za ich pomoc warto mona wykorzysta tak samo, jak w przypadku operatorw pozbawionych skutkw ubocznych.

130

Thinking in C++. Edycja polska

Przypisanie
Przypisanie jest realizowane za pomoc operatora =. Oznacza ono: praw stron (czsto nazywanp-wartoc/) skopiuj na lewstron (nazywanczsto l-wartosciq)". P-warto jest dowoln sta, zmienn lub wyraeniem zwracajcym warto, ale 1warto musi by pojedyncz, nazwan zmienn (czyli fizycznym miejscem, przeznaczonym do przechowywania danych). Na przykad mona przypisa zmiennej sta warto (A = 4;), ale nie wolno przypisa niczego staej wartoci nie moe ona by l-wartoci (bdnyjest wic zapis: 4 = A;).

Operatory matematyczne
Podstawowe operatory matematyczne s takie same, jak te dostpne w wikszoci jzykw programowania: dodawanie (+), odejmowanie (-), dzielenie (0, mnoenie (*) oraz modulo (%, zwracajcy reszt z cakowitego dzielenia). Cakowite dzielenie obcina cz uamkow wyniku (nie jest on zaokrglany). Operator modulo nie moe by uywany w stosunku do liczb zmiennopozycyjnych. Jzyki C i C++ wykorzystuj rwnie skrcony zapis, umoliwiajcy rwnoczesne wykonanie danej operacji i przypisania. Jest on oznaczany jako operator, po ktrym nastpuje znak rwnoci, i obowizuje dla wszystkich operatorw dostpnych w jzyku (dla ktrych taka operacja ma sens). Na przykad aby doda 4 do zmiennej x i wynik tej operacji przypisa zmiennej x, naley napisa: x+ = 4;. Poniszy przykad stanowi ilustracj sposobu uywania operatorw matematycznych:
/ / : C03:MathopS.Cpp // Operatory matematyczne #include <iostream> using namespace std; // Makroinstrukcja, wywietlajca acuch i warto #define PRINT(STR, VAR) \ cout STR " " VAR endl

int main() { int i. j. k;

PRINT("j".j); PRINT("k",k); i - j + k; PRINT("j + k".i); i - j - k; PRINT("j - k".i); i = k / j; PRINT("k / j".i); i = k * j; PRINT("k * j",i); i - k % j; PRINT("k % j".i); // Nastpne dziaaj tylko z liczbami cakowitymi: j %= k; PRINT("j %= k", j); cout "Wprowad liczbe zmiennopozycyjna: "; cin v;

float u. v. w; // Odnosi sie. rwnie do liczb double cout "wprowad liczbe cakowita: "; cin j; cout "wprowad jeszcze jedna liczbe cakowita: "; cin k:

Rozdzia 3. Jzyk C w

C++

133

cout "Wprowad jeszcze jedna liczbe zmiennopozycyjna:"; cin w;


PRINT("v".v); PRINT("w".w): u = v + w; PRINT("v + w", u) u - v - w; PRINT("v - w", u) u = v * w; PRINT("v * w", u) u = v / w; PRINT("v / w", u)

PRINT("u". u); PRINT("v". v); u += v; PRINT("u += v", u); u -= v; PRINT("u -= v", u);

// Nastpne dziaaj z liczbami cakowitymi, // znakami i liczbami double:

u *= v; PRINT("u *= v", u);

u /= v; PRINT("u /= v". u); Oczywicie, p-wartocl wszystkich przypisa, mog by bardziej skomplikowane.

Wprowadzenie do makroinstrukcji preprocesora


Zwr uwag na uycie makroinstrukcji PRINT( ), pozwalajcej oszczdzi na pisaniu (i chronicej przed popenianiem pomyek!). Makroinstrukcje preprocesora s tradycyjnie zapisywane wielkimi literami, dziki czemu rzucaj si w oczy. Mog by one niebezpiecznym (ale take niezwykle uytecznym) narzdziem. Argumenty znajdujce si w nawiasie po nazwie makroinstrukcji s zastpowane w caym kodzie, nastpujcym po nawiasie zamykajcym. Preprocesor usuwa nazw PMNT i zastpuje j tym kodem w kadym miejscu wywoania makroinstrukcji, co powoduje, e kompilator nie jest w stanie wygenerowa adnego komunikatu zawierajcego nazw makroinstrukcji. Nie przeprowadza rwnie adnej kontroli typw dotyczcej jej argumentw (to ostatnie moe by zalet, jak zobaczymy na przykadzie makroinstrukcji uruchomieniowych, przedstawionych na kocu rozdziau).

Operatory relacji
Operatory relacji okrelaj zwizek pomidzy wartociami argumentw. Zwracaj one warto logiczn (oznaczan w jzyku C++ sowem kluczowym bool) true jeeli relacjajest prawdziwa Iub false jeeli jest ona faszywa. Operatorami relacji s: mniejsze ni (<), wiksze ni (>), mniejsze Iub rwne (<=), wiksze lub rwne (>=), rwne (==) i nierwne (!=). Mog one by uywane z wbudowanymi typami danych zarwno w C, jak i w C++. W jzyku C++ mona przypisa im rwnie specjalne definicje, umoliwiajce ich stosowanie wraz z typami zdefiniowanymi przez uytkownika (zagadnienie to zostanie przedstawione w rozdziale 12., opisujcym przecianie operatorw).

Operatory logiczne
Operatory iloczynu logicznego (&&) i sumy logicznej (||) zwracaj wartoci true lub fabe na podstawie logicznej relacji swych argumentw. Naley pamita, e w jzykach

Thinking in C++. Edycja polska C i C++ wyraenie jest prawdziwe, jeeli ma ono warto niezerow, a faszywe, kiedy jest one rwne zeru. Podczas wydruku wartoci typu bool zazwyczaj pojawiaj si znaki '1' (dla true) i '0' (dla false). W poniszym przykadzie zostay wykorzystane operatory relacji i operatory logiczne: / / : C03:Boolean.cpp // Operatory relacji 1 operatory logiczne finclude <iostream> using namespace std;

int main() { int 1 .j; cout "Wprowad liczbe cakowita: "; cin 1 ; cout "Wprowad jeszcze jedna liczbe cakowita: cin > j ; cout 'i > j wynosi " (i > j) endl; cout < "i < j wynosi " (i < j) endl: cout < "i >= j wynosi " (i >= j) endl; cout < "i <= j wynosi " (i <- j) endl; cout < "i == j wynosi " (i == j) endl; cout "i != j wynosi " (i !- j) endl; cout "i && j wynosi " (i && j) endl; cout "i || j wynosi " (i || j) endl; cout " (i < 10) && (j < 10) wynosi " ((i < 10) && (j < 10)) endl;
W powyszym przykadzie mona wymieni definicje typu zmiennych int, typu float lub double. Naley jednak wiedzie o tym, e porwnanie liczb zmiennopozycyjnej z zerem jest dokadne liczba rnica si od innej liczby w najmniejszej choby czcijestjej nierwna". Liczbazmiennopozycyjna, ktrej nawet najmniej znaczcy bit jest niezerowy, jest natomiast traktowana jako prawdziwa".

Operatory bitowe
Operatory bitowe umoliwiaj operacje na poszczeglnych bitach liczby (poniewa liczby zmiennopozycyjne wykorzystuj specjalny wewntrzny format zapisu informacji, operatory bitowe dziaaj tylko z typami cakowitymi: char, int i long). Operatory bitowe zwracaj wartoci, wykonujc operacje logiczne na odpowiadajcych sobie bitach argumentw. Bitowy operator koniunkcji (&) zwraca bit o wartoci jeden, gdy oba bity wejciowe maj warto jeden, a zero w przeciwnym przypadku. Bitowy operator alternatywy (|) zwraca bit o wartoci jeden, gdy dowolny z bitw wejciowych jest rwny jeden, a bit o wartoci zero tylko wwczas, gdy oba bity wejciowe s rwne zeru. Bitowy operator rnicy symetrycznej (^) zwraca warto jeden, gdy dowolny z bitw wejciowych wynosi jeden, ale nie oba rwnoczenie. Bitowy operator negacji (~, nazywany rwnie uzupenieniem do jedynki) jest operatorem jednoargumentowym (wszystkie pozostae operatory bitowe s operatorami dwuargumentowymi). Operator ten zwraca odwrotno bitu wejciowego jeden, gdy wynosi on zero, a zero, gdyjest on rwnyjeden.

Rozdzia 3. Jzyk C w

C++

133

Operatory bitowe mona zestawia ze znakiem =, czc w ten sposb wykonanie operacji z przypisaniem poprawnymi operacjami s wic: &=, |= i ^= (poniewa ~ jest operatoremjednoargumentowym, nie mona czy go ze znakiem =).

Operatory przesuni
Operatory przesuni rwnie dziaaj na bitach. Operator przesunicia w Iewo () zwraca warto argumentu po jego lewej stronie, przesunit w lewo o liczb bitw okrelon przez argument po jego prawej stronie. Operator przesunicia w prawo () zwraca warto argumentu znajdujcego si po jego lewej stronie, przesunit w prawo o liczb bitw okrelon przez argument po jego prawej stronie. Jeeli warto argumentu po prawej stronie operatorajest wiksza ni liczba bitw argumentu pojego lewej stronie, wynik operacji jest nieokrelony. Jeeli argument po lewej stronie operatora przesunicia jest liczb bez znaku, to przesunicie w prawo jest przesuniciem logicznym (najstarsze bity s wypeniane zerami). Jeeli natomiast argument po lewej stronie operatora jest liczb ze znakiem, to przesunicie w prawo moe, ale wcale nie musi, by przesuniciem logicznym (czyli zachowanie operatorajest niezdefiniowane). Operatory przesuni mog by poczone ze znakiem rwnoci (= i =). W takim przypadku w wyniku operacji l-wartojest zastpowana l-wartoci przesunit 0 liczb bitw okrelon przez p-warto. Zaprezentowany poniej przykad ilustruje uycie wszystkich operatorw dziaajcych na bitach. Na pocztku zdefiniowano funkcj oglnego przeznaczenia, drukujc bajty w zapisie dwjkowym. Funkcja ta znajduje si w oddzielnym pIiku, dziki czemu mona bdzie mona w atwy sposb ponownie j wykorzysta. Zadeklarowano j w pliku nagwkowym:

// Wywietl bajt w zapisie dwjkowym


A oto implementacja tej funkcji:

/ / : C03:printBinary.h

void printBinary(const unsigned char val); ///:-

/ / : C03:printBinary.cpp {0} #include <iostream> void printBinary(const unsigned char val) { for(int i = 7; i >= 0: i--) if(val & (1 i)) std::cout "1": else Std::cout "0"; } III-

Funkcja printBinary() przyjmuje jako argument pojedynczy bajt i wywietla go, bit po bicie. Wyraenie:
(1 i)

zwracajedynk na kadej kolejnej pozycji bitu w zapisie dwjkowym: 00000001, 00000010 itd. Jeeli wyznaczy si koniunkcj tego bitu z wartoci zmiennej val 1 uzyskany wynik bdzie niezerowy, bdzie to oznaczao obecno jedynki na odpowiadajcej temu bitowi pozycji zmiennej vaI.

Thinking in C++. Edycja polska

Ostatecznie funkcja zostaa uyta w przykadzie, prezentujcym operatory dziaajce na bitach: / / : C03:Bitwise.cpp / / { L } printBinary

// Demonstracja operacji na bitach finclude "printBinary.h"

#include <iostream> using namespace std; // Makroinstrukcja, pozwalajca oszczdzi na pisaniu: #define PR(STR, EXPR) \ cout STR; printBinary(EXPR); cout endl; int main() { unsigned int getval ; unsigned char a, b; cout "Wprowad liczbe z zakresu od 0 do 255: "; cin getval ; a = getval ; PR("a w zapisie dwjkowym: ". a); cout " Wprowad liczbe z zakresu od 0 do 255: "; cin getval ; b = getval ; PR("b w zapisie dwjkowym: ". b);

a |= c; PR("a |= c; a = ", a); b &= c; PR("b &= c; b = ". b); b ^= a: PR("b ^= a; b = ". b);

PR("-a = ". -a); PR("-b = ", -b); // Interesujca sekwencja bitw: unsigned char c = 0x5A; PR("c w zapisie dwjkowym: ". c);

PR("a | b = ". a | b); PR("a & b = ", a & b); PR("a ^ b = ", a ^ b);

Ponownie, w celu oszczdzenia sobie pisania, zostaa uyta makroinstrukcja. Drukuje ona dowolny acuch, a nastpnie dwjkowreprezentacj wyraenia i znak nowego wiersza. Zmienne w funkcji main( ) s typu unsigned, poniewa w przypadku operacji na bitach znaki nie s na og potrzebne. Zmienna getval musi by zdefiniowana jako zmienna typu int, a nie char z uwagi na to, e w przeciwnym przypadku instrukcja cin " potraktowaaby pierwszcyfrjako znak. Podczas przypisywania zmiennym a i b wartoci zmiennej getval jest ona przeksztacana do pojedynczego bajtu (przez odcicie pozostaych bitw). Operatory i umoliwiaj przesuwanie bitw, ale w przypadku gdy bity zostan przesunite poza granice liczby, s one tracone (czsto powiada si, e trafiaj one do zbiornika na bity miejsca, w ktrym znajduj si odrzucone bity, dziki czemu mogone by przypuszczalnie z powrotem uyte...). W czasie wykonywania operacji na bitach mona rwnie dokona obrotu, co oznacza, e bity, ktre wychodz"

Rozdzia 3. Jzyk C w C++ zjednego koca liczby s wstawiane na jej drugi koniec tak jakby obracay si w kko. Mimo e wikszo procesorw znajdujcych si w komputerach posiada sprztowe rozkazy obrotw (s one dostpne w jzyku asemblera), to jzyki C oraz C++ nie zapewniaj bezporedniego wsparcia dla instrukcji obrotu". Przypuszczalnie projektanci jzyka C uznali za usprawiedliwione pominicie tej instrukcji (koncentrujc si, jak twierdzili, na ,jezyku minimalnym"), poniewa mona utworzy wasne polecenia, realizujce obroty. Poniej zamieszczono przykadowe funkcje, dokonujce obrotw w lewo oraz w prawo: //: C03:Rotation.cpp {0} // Wykonywanie obrotw w lewo i w prawo unsigned char rol(unsigned char val) { int highbit; if(val & 0x80) // 0x80 jest najstarszym bitem highbit = 1; else highbit = 0; // Przesunicie w lewo (najmodszy bit // otrzymuje warto 0): // Wstawienie najstarszego bitu na najmodszej pozycji: val |= highbit; return val ; unsigned char ror(unsigned char val) { int lowbit; if(val & 1) // Sprawdzenie wartoci najmodszego bitu lowbit = 1; else lowbit = 0; val = 1: // Przesunicie w prawo o jedn pozycj // Wstawienie najmodszego bitu na najstarszej pozycji: val |= (lowbit 7); return val ; Sprbuj uy tych funkcji w programie Bitwise.cpp. Zwr uwag na to, e definicje (a przynajmniej deklaracje) funkcji rol( ) i ror( ) musz by widoczne dla kompilatora w pliku Bitwise.cpp, zanimjeszcze funkcje te zostanwykorzystane. Stosowanie funkcji operujcych na bitach jest na og wyjtkowo efektywne, poniewa przekadaj si one bezporednio na instrukcje asemblera. W niektrych przypadkach pojedyncza instrukcja jzyka C lub C++ generuje pojedynczy wiersz kodu wjzyku asemblera.
val - 1:

Operatory jednoargumentowe
Bitowy operator negacji nie jest jedynym operatorem dziaajcym na pojedynczym argumencie. Jego odpowiednik, operator negacji logicznej (!), dla argumentu o wartoci true zwraca warto fa!se. Jednoargumentowe operatory: minus (-) i plus (+) wygldaj tak samo, jak dwuargumentowe operatory odejmowania i dodawania kompilator

136

Thinking in C++. Edycja polska

okrela, wjakim znaczeniu zostay one uyte, na podstawie tego, jak zostao zapisane wyraenie. Na przykad instrukcja:
x = -a;

maoczywiste znaczenie. Kompilator potrafi rwnie okreli znaczenie instrukcji:


x = a * -b;

ale czytajcaje osoba moe by nieco zdezorientowana, dlatego te bezpieczniej jest napisa:
x = a * (-b);

Jednoargumentowy operator minus zwraca ujemn warto argumentu. Jednoargumentowy operator plus istnieje dla symetrii, chocia w rzeczywistoci nie wykonuje adnych dziaa. Operatory inkrementacji i dekrementacji (++ i --) zostay ju wprowadzone we wczeniejszej czci rozdziau. S one jedynymi operatorami (z wyjtkiem operatorw zawierajcych przypisanie), ktre posiadajskutki uboczne. Operatory te zwikszaj lub zmniejszaj zmienn o jednjednostk, cho pojcie ,jednostki" ma rne znaczenia, w zalenoci od typu danych szczeglnie w przypadku wskanikw. Ostatnimi 'operatorami jednoargumentowymi s w jzykach C i C++ operatory: adresu (&), wyuskania (* i ->) oraz rzutowania, a w jzyku C++ dodatkowo new i delete. Operatory adresu i wyuskania uywane s ze wskanikami i zostay opisane w niniejszym rozdziale. Rzutowanie zostanie omwione w dalszej czci rozdziau, natomiast operatory new i delete wprowadzono w rozdziale 4.

Operator trjargumentowy
Operator trjargumentowy if-else jest operatorem o niezwykych wasnociach, poniewa dziaa on na trzech argumentach. Jest on naprawd operatorem, poniewa w przeciwiestwie do zwykej instrukcji if-eke zwraca warto. Skada si on z trzech wyrae: jeeli pierwsze z nich (znajdujce si przed znakiem ?) daje warto true, to wyznaczanajest warto wyraenia nastpujcego po ? i staje si ona zarazem wartoci zwracan przez cay operator. Jeeli natomiast pierwsze wyraenie daje warto false, to wyznaczana jest warto trzeciego wyraenia (znajdujcego si po znaku :) i to ona staje si wartocizwracanprzez operator. Operator warunkowy moe by uywany z uwagi na swoje skutki uboczne lub zwracan warto. Poniszy przykad ilustruje oba te zastosowania:
a = --b ? b : (b = -99);

W przykadzie tym operator warunkowy zwracap-warto. Jeeli rezultat dekrementacji zmiennej bjest niezerowy, tojest on przypisywany zmiennej a. Jeeli natomiast zmienna b osiga warto zerow% to zarwno zmiennej a, jak i b jest przypisywana warto -99. Zmienna b jest dekrementowana zawsze, ale warto -99 przypisywana jest jej tylko wwczas, gdy w wyniku dekrementacji zmienna ta osignie warto 0. Podobna instrukcja moe by uywana bez ,# =" wycznie z uwagi na swoje skutki uboczne:

Rozdzial3. Jzyk C w
--b ? b : (b = -99);

C++

137

W powyszym przykadzie drugie wystpienie zmiennej b jest nadmiarowe, gdy warto zwracana przez operator nie jest do niczego uywana. Poniewa jednak midzy ? a : wymagane jest wyraenie, w tym przypadku moe ono by po prostu sta, dziki czemu program zadziaa nieco szybciej.

Operator przecinkowy
Uycie przecinka nie jest ograniczone wycznie do oddzielenia od siebie nazw w definicji wielu zmiennych,jak w przykadzie poniej:
int i. j, k;

Oczywicie, jest on rwnie uywany na listach argumentw funkcji. Jednake przecinek moe by take uywany jako operator oddzielajcy od siebie wyraenia w takim przypadku zwraca onjedynie warto ostatniego z nich. Wszystkie pozostae wyraenia, odseparowane od siebie przecinkami, s obliczane wycznie z uwagi na swoje skutki uboczne. W poniszym przykadzie inkrementowana jest lista zmiennych, a ostatnia z nich jest uywana jako p-warto: II: C03:CommaOperator.cpp |1nclude <iostream> using namespace std; int main() { int a = 0. b = 1. c = 2. d = 3. e = 4; a - (b++, c++, d++, e++); cout "a = " a endl; // Powyszy nawias jest konieczny. Bez niego // instrukcja bylaby wykonywana jako: (a = b++), c++, d++, e++: cout "a = " a endl; } IIINajlepiej jest unika uywania przecinka inaczej ni jako separatora, poniewa zazwyczaj nie postrzega si go wjako operatora.

Najczstsze putepki zwizane z uywaniem operatorw


Jak wynika z powyszego przykadu, jedn z puapek zwizanych z uywaniem operatorw jest prba pozbycia si nawiasw w przypadku choby najmniejszej wtpliwoci dotyczcej sposobu obliczania wyraenia (kolejno wykonywania operacji mona znale w dowolnym podrczniku C). Inny, wyjtkowo czsto spotykany bd jest nastpujcy: // Pomyki w uywaniu operatorw
/ / : C03:Pitfall.cpp

.38
int main() { int a - 1, b - 1; while(a = b) {

Thinking in C++. Edycja polska

Instrukcja a = b bdzie zwraca warto true zawsze wtedy, gdy b ma warto niezerow. Zmiennej a jest przypisywana warto zmiennej b i jest ona rwnie zwracana przez operator =. Na og wewntrz wyrae warunkowych zamierzamy uy operatora porwnania ==, a nie przypisania. Dao si to we znaki niejednemu programicie jednake niektre kompilatory sygnalizujtakie problemy, cojest niezwykle pomocne). Podobnym problemem jest uycie bitowych operatorw koniunkcji i alternatywy zamiast ich logicznych odpowiednikw. Operatory bitowej koniunkcji i alternatywy s oznaczane pojedynczymi znakami (& i |), podczas gdy logiczny iloczyn i suma skadaj si z dwch znakw (&& i | ). Podobniejak w przypadku == i =, atwo zamiast dwch znakw wpisa jeden. Uyteczn technik mnemotechniczn moe by spostrzeenie, e bity s mniejsze, wic nie potrzebujatylu znakw w swoich operatorach".

Dperatory rzutowania
Termin rzutowanie uywany jest tu w znaczeniu przenoszenia na inn paszczyzn". Kompilator automatycznie przeksztacajeden typ danych w inny, jeeli jest to moliwe do zaakceptowania. Na przykad jeeli zmiennej zmiennopozycyjnej zostanie przypisana warto cakowita, kompilator w niejawny sposb wywouje funkcj (albo, co bardziej prawdopodobne wstawia kod) przeksztacajc typ int w float. Rzutowanie pozwala na uczynienie takiej konwersji jawn albo wymuszenie jej w przypadkach, w ktrych nie zostaaby ona wykonana. Aby dokona rzutowania, naley umieci docelowy typ danych (cznie ze wszystkimi modyfikatorami) w nawiasie, po lewej stronie rzutowanej wartoci. Warto ta moe by zmienn, sta, wynikiem wyraenia albo wartoci zwracan przez funkcj. Oto przykad rzutowania:
/ / : C03:SimpleCast.cpp int main() { int b = 200: unsigned long a (unsigned long int)b; } III-

Rzutowanie jest skutecznym narzdziem, ale moe ono nastrczy problemw, poniewa w pewnych sytuacjach wymusza na kompilatorze traktowanie danych w taki sposb, jakby byy one (na przykad) wiksze ni w rzeczywistoci. Zajm one zatem wikszy obszar pamici co z kolei moe naruszy inne dane. Sytuacje takie zdarzaj si zazwyczaj podczas rzutowania wskanikw, a nie zwykego rzutowania takiego, jak przedstawione powyej. W jzyku C++ istnieje dodatkowa skadnia operacji rzutowania, naladujca sposb wywoania funkcji. W zapisie tym nie docelowy typ danych, ale argument znajduje si w nawiasie podobniejak podczas wywoania funkcji.

Rozdzia

3.

Jzyk

C++

139

/ / : C03:FunctionCallCast.cpp int main() { float a = float(200); // Jest to rwnowane: float b = (float)200; Oczywicie, w powyszym przykadzie niejest naprawd potrzebne rzutowanie mona napisa po prostu 200f (w istocie kompilator to wanie zrobi z powyszym wyraeniem). Rzutowaniejest na og czciej uywane w stosunku do zmiennych ni do funkcji.

Jawne rzutowanie w C++


Rzutowanie powinno by stosowane ostronie, poniewa w rzeczywistoci nakazuje ono kompilatorowi: pomi kontrol typw traktuj to jak zupenie inny typ". Oznacza to, e tworzysz luk w systemie typw C++, powstrzymujc kompilator przed poinformowaniem ci, e twoje dziaania dotyczce typw s niewaciwe. Co gorsza, kompilator nie przeprowadza adnych dodatkowych kontroli, ktre pozwoliyby na wyapanie bdw. Zaczynajc uywa rzutowa, naraasz si na wszelkiego rodzaju problemy. W rzeczywistoci, kady program, ktry zawiera wiele rzutowa, powinien by traktowany podejrzliwie, niezalenie od argumentw autora, e naleao go napisa wanie w ten sposb. Na og rzutowanie powinno by stosowane rzadko i ograniczone do rozwizywania wyjtkowych problemw. Kiedy staniesz oko w oko z programem penym bdw, twoimi gwnymi podejrzanymi bd zapewne operacje rzutowania. W jaki jednak sposb odnajdziesz rzutowania, zapisane w stylu uywanym w jzyku C? S one zwykymi nazwami typw ujtymi w nawiasy i gdy zaczniesz na nie polowa", przekonasz si, e czsto nieatwo odrni je od reszty programu. Standard jzyka C++ zawiera skadni jawnego rzutowania, umoliwiajc cakowit rezygnacj ze stylu uywanego w C (oczywicie, nie mona zabroni stosowania rzutowania w stylu C nie amic zasad, ale autorzy kompilatorw mogliby bez trudu zaznaczy uytkownikowi rzutowania realizowane w starym" stylu). Skadnia jawnego rzutowania umoliwia atwe odnalezienie miejsc, w ktrych je zastosowano, dziki uywanym przez nie nazwom: static_cast Rzutowania, ktre dobrze si zachowuj" i stosunkowo dobrze si zachowuj", wczajc w to operacje, ktre mona przeprowadzi w ogle bez uycia rzutowania (np. automatyczn konwersj typw). Rzutowania usuwajce modyfikatory const i (lub) volatile. Rzutowanie z cakowit zmian znaczenia. Istotne jest, e bezpieczne wykorzystywanie takiego rzutowania wymaga ponownego rzutowania z powrotem do pierwotnego typu. Typ, do ktrego dokonywane jest rzutowanie, jest zazwyczaj uywany wycznie do operacji niskopoziomowych albojakich innych celw. Jest to najbardziej niebezpieczny rodzaj rzutowania. Do bezpiecznego rzutowania w d" (ten rodzaj rzutowania zostanie opisanywrozdzialel5.). _

const_cast reinterpret_cast

dynamic_cast

140 i

Thinking in C++. Edycja polska ^______^^^^^^^^^.^^^

Pierwsze trzy jawne rzutowania zostan obszerniej opisane w nastpnych podrozdziaach, natomiast ostatnie bdzie zaprezentowane dopiero w rozdziale 15.

static_cast
Rzutowanie static_cast jest uywane w przypadku wszelkich dobrze zdefiniowanych rzutowa. Obejmuj one bezpieczne" konwersje,ktore kompilator powinien dopuci bez rzutowania, a take przeksztacenia nieco mniej bezpieczne, ale dobrze okrelone. Rodzaje rzutowa, okrelonych przez static_cast, obejmuj: typowe konwersje niewymagajce rzutowania, przeksztacenia zmniejszajce rozmiar danych (z utrat informacji), wymuszenie rzutowania typu void*, niejawne konwersje typw oraz statyczne poruszanie si w obrbie hierarchii klas (z uwagi na to, e klasy oraz ich dziedziczenie nie zostay do tej pory przedstawione, przypadek ten zostanie opisany w rozdziale 15.).

//: C03:static_cast.cpp void func(int) {} int main() { int i = 0x7fff; // Maksymalna dodatnia warto = 32767 long 1; float f; // (1) Typowe konwersje bez uycia rzutowania: 1 - 1; f = 1: // Dziaaj rwnie: 1 = static_cast<long>(i); f = static_cast<float>(i); // (2) Konwersje zawajce: i = 1; // Moliwo utraty cyfr i = f; // Moliwo utraty informacji // Powiedzenie "wiem o tym" eliminuje ostrzeenia: i = static_cast<int>(l); i = static_cast<int>(f): char c - static_cast<char>(i); // (3) Wymuszenie konwersji z typu void* : void* vp = &i; // Stary sposb powoduje niebezpieczn konwersj: float* fp - (float*)vp; // Nowy jest rwnie niebezpieczny: fp = static_cast<float*>(vp); // (4) Niejawne konwersje typw, dokonywane // zazwyczaj przez kompilator: double d = 0.0: int x = d; // Automatyczna konwersja typu x = static_cast<int>(d): // Bardziej jawna konwersja func(d): // Automatyczna konwersja typu func(static_cast<int>(d)): //' Bardziej jawna konwersja

Rozdzia 3. Jzyk C w

C++

141

W sekcji (1) widoczne s sposoby konwersji stosowane w jzyku C z zastosowaniem rzutowania oraz bez niego. Zmiana wartoci int na long albo float nie jest problemem, poniewa w tych przypadkach typy docelowe s zawsze w stanie przechowa kad warto reprezentowan przez int. Mimo e nie jest to konieczne, mona uy sowa kluczowego static_cast, sygnalizujcego dokonane konwersje. W sekcji (2) przedstawiono konwersje w przeciwnym kierunku. W ich przypadku istnieje moliwo utraty danych, poniewa typ int nie jest tak duy", jak typy long czy float nie jest bowiem w stanie przechowywa rwnie duych liczb. Dlatego te s one nazywane konwersjami zawajcymi. Kompilator nadal jest w stanie je wykona, ale najczciej zgosi w takich przypadkach ostrzeenie. Mona unikn wywietlania ostrzeenia, wskazujc za pomoc rzutowania, e wanie tak operacj zamierzao si wykona. W jzyku C++ (w odrnieniu od C) przypisanie wartoci typu void* nie jest moliwe bez uycia rzutowania. Jest ono niebezpieczne i w zwizku z tym wymaga, by programista zachowa ostrono. Uycie sowa kluczowego static_cast zapewnia, e gdy przyjdzie pora na szukanie bdw, miejsce, w ktrym dokonano rzutowania, bdzie przynajmniej atwiejsze do znalezienia ni w przypadku standardowego starego" rzutowania. Sekcja (4) programu prezentuje niejawne konwersje typw, dokonywane na og automatycznie przez kompilator. Poniewa s one wykonywane automatycznie, nie wymagaj rzutowania. Jednake uycie static_cast umoliwia sygnalizacj podejmowanych dziaa, uatwiajc ich zrozumienie lub pniejsze odszukanie.

const_cast
Jeeli potrzebnajest konwersja z typu oznaczonego modyfikatorem const lub volatile do typu nieposiadajcego takiego modyfikatora, naley uy do tego ceIu sowa kluczowego const_cast. S to jedyne rodzaje przeksztace, ktrych mona dokona za pomocrzutowania const_cast jeeli wykonywane sjakiekolwiek inne konwersje, trzeba uy do nich oddzielnych wyrae, bo w innym przypadku spowoduje to zgoszenie bdu kompilacji. //: C03:const_cast.cpp 1nt main() { const int i = 0; int* j = (int*)&i; // Posta niewskazana j - const_cast<int*>(&i); // Posta preferowana // Nie mona wykona rwnoczenie dodatkowego rzutowania: //! long* 1 = const_cast<long*>(&i); // Bd volatile int k = 0; int* u = const_cast<int*>(&k); Podczas pobierania adresu obiektu typu const tworzonyjest wskanik do staej, ktrego nie mona bez rzutowania przypisa do zmiennej niebdcej wskanikiem do staej. Mona to zrobi, uywajc starego" stylu rzutowania, ale lepszym rozwizaniem jest zastosowanie rzutowania const_cast. Ta sama uwaga odnosi si do modyfikatora volatile.

142

Thlnking ln C++. Edycja polska

reinterpret_cast
Jest to najmniej bezpieczny mechanizm rzutowania, bdcy zarazem najbardziej prawdopodobn przyczyn bdw. Rzutowanie reinterpret_cast zakada, e obiekt jest zbiorem bitw, ktry moe by traktowany (dla jakich niejasnych celw) tak, jakby by obiektem zupenie innego typu. Jest to wanie taka operacja niskopoziomowa, z jakich niechlubnie synie jzyk C. Waciwie zawsze istnieje konieczno ponownego rzutowania zmiennej do pierwotnego typu (albo przynamniej traktowania jej tak,jakby bya takiego typu), zanim wykona sijakiekolwiek inne dziaania. / / : C03:reinterpret_cast.cpp #include <iostream> using namespace std; const int sz = 100; struct X { int a[sz]; }; void print(X* x) { + for(int i = 0; i < sz; i+ ) cout x->a[i] ' '; cout endl "

" endl;

int main() { X x; print(&x); int* xp - reinterpret_cast<int*>(&x); for(int* i = xp; i < xp + sz; i++) *i = 0;

// Nie mona w tym miejscu uywac zmiennej xp tak. // jakby bya typu X*, dopki nie dokona si z powrotem // jej rzutowania; print(reinterpret_cast<X*>(xp)); // W tym przykadzie mona rwnie uyc // oryginalnego identyfikatora: print(&x);

W powyszym, prostym przykadzie struktura X zawiera tablic liczb cakowitych, ale gdy jest ona tworzona na stosie jako X x, kada ze znajdujcych si w tej tablicy liczb zawiera mieci (zostao to pokazane za pomoc uycia funkcji print( ), wywietlajcej zawarto struktury). W celu zainicjowania tych wartoci adres struktury jest rzutowany na wskanik do typu int, ktry nastpnie przechodzi przez ca tablic, przypisujc kademu jej elementowi warto 0. Zwr uwag na to, e grna granica zmiennej i jest wyznaczana przez dodanie" sz do xp kompilator wie, e w rzeczywistoci chodzi ci o sz wskazywanych pozycji powyej xp i samodzielnie przeprowadza odpowiednie obliczenia na wskanikach. Gdy stosuje si rzutowanie reinterpret_cast, uzyskuje si rezultat tak odmienny, e nie moe on zosta uyty zgodnie z pocztkowym przeznaczeniem typu, dopki nie dokona si jego rzutowania z powrotem na pierwotny typ. W powyszym przykadzie widoczne jest rzutowanie ponownie na typ X*, ale oczywicie, poniewa nadal dysponujemy oryginalnym identyfikatorem, to moemy uy go w tym przypadku. Jednak zmienna xpjest uyteczna wycznie jako int*, co stanowi rzeczywicie reinterpretacj" pierwotnego typu X.

Rozdzia

3.

Jzyk

C++

143

Rzutowanie reinterpret_cast oznacza czsto niezalecany i (lub) nieprzenony styl programowania, alejest dostpnejeli uwaasz, e musisz go uy.

sizeof samotny operator


Operator sizeofjestjedynym operatorem w swoim rodzaju, poniewa zaspokaja on niezwyk potrzeb. Udziela informacji, dotyczcej wielkoci pamici przydzielonej jednostkom danych. Jakju wspomniano we wczeniejszej czci rozdziau, sizeof informuje o liczbie bajtw, zajmowanych przez dowoln zmienn. Moe on rwnie poda wielko typu danych (bez nazwy zmiennej): / / : C03:sizeof.cpp #include <iostream> using namespace std; int main() { cout "sizeof(double) = " sizeof(double); cout ". sizeof(char) = " sizeof(char); Zgodnie z definicj, wielko dowolnego typu znakowego char (signed, unsigned lub zwykego) wynosi zawsze jeden, niezalenie od tego, czy pami zorganizowana jest rzeczywicie w taki sposb, e znaki przechowywane s w pojedynczych bajtach. Dla wszystkich pozostaych typw zwracan wartocijest wielko wyraona w bajtach. Warto zwrci uwag na to, e sizeofjest operatorem, a nie funkcj. Jeeli zostanie on zastosowany do typu, naley posuy si widoczn powyej notacj nawiasow, jednak w odniesieniu do zmiennych mona go uywa bez nawiasw: / / : C03:sizeofOperator.cpp int main() {
int x;

int i = sizeof x; Operator sizeof moe rwnie podawa wielko typw danych, zdefiniowanych przez uytkownika. Zostanie to wykorzystane w dalszej czci ksiki.

Stowo kluczowe asm


Jest to proteza, umoliwiajca wstawienie do programu, napisanego w jzyku C++, kodu asemblerowego, obsugujcego uywany sprzt. W programie asemblerowym istnieje czsto moliwo odwoywania si do zmiennych zdefiniowanych w C++. Uatwia to komunikacj z programem napisanym w C++, ograniczajc zarazem konieczno uywania jzyka asemblera do minimum niezbdnego z uwagi na efektywno lub moliwo wykorzystania specjalnych instrukcji procesora. Szczegowy opis skadni uywanej podczas pisania programu w asemblerze zaley od kompilatora; mona go odnale w doczonej do niego dokumentacji.

144

Thinking in C++. Edycja polska

Operatory dosfowne
S to sowa kluczowe okrelajce operacje bitowe i logiczne. Programistw, ktrych klawiatury nie posiadaj takich znakw, jak &, |, ^ itd., zmuszano w jzyku C do uywania okropnych trjznakw, ktre byy nie tylko irytujce przy wpisywaniu, ale rwnie zupenie niezrozumiae w czasie czytania programu. W jzyku C++ usunito t niedogodno, wprowadzajc dodatkowe sowa kluczowe: sk>wo kluczowe and or not not_eq bitand and_eq bitor or_eq xor xor_eq compl znaczenie && (iloczyn logiczny) || (suma logiczna) ! (negacja logiczna) != (nierwno logiczna) & (koniunkcja bitowa) &= (koniunkcja bitowa z przypisaniem) | (alternatywa bitowa) |= (alternatywa bitowa z przypisaniem) A (bitowa rnica symetryczna) ^= (bitowa rnica symetryczna z przypisaniem) ~ (uzupenienie do jedynki)

Jeeli uywany przez ciebie kompilator jest zgodny ze standardem C++, to bdzie obsugiwa powysze sowa kluczowe.

Tworzenie typw zoonych


Podstawowe typy danych oraz ich odmiany s niezbdne, ale raczej prymitywne. Jzyki C i C++ udostpniaj narzdzia pozwalajce na tworzenie na podstawie typw podstawowych bardziej skomplikowanych typw danych. Najwaniejszym z nich jest struktura (struct), stanowica w jzyku C++ podstaw klasy (class). Jednake najprostszym sposobem tworzenia bardziej zoonych typw jest przypisanie nazwie typu innej nazwy za pomoc sowa kluczowego typedef.

Nadawanie typom nowych nazw za pomoc typedef


Nazwa sowa kluczowego typedefjest nieco mylca sugeruje bowiem definicj typu" (ang. type deflnition), podczas gdy bardziej stosownym, i oddajcym istot rzeczy, okreleniem byoby nadanie nowej nazwy". Jego skadnia jest nastpujca: typedef opis-istniejcego-typu nowa-nazwa Sowa kluczowego typedef uywa si czsto w sytuacjach, gdy typy danych staj si zbyt skomplikowane po to, by unikn wpisywania dugich nazw. Typowe uycie typedef ma nastpujc posta:

Rozdzia 3. jzyk C w

C++

145

typedef unsigned long ulong; A zatem gdy kompilator napotka w twoim programie typ ulong, bdzie wiedzia, e chodzio ci o unsigned long. Cho wydaje si, e to samo mona atwo osign za pomoc dyrektyw preprocesora, zdarzaj si sytuacje, w ktrych kompilator musi wiedzie, e traktujesz nazw w taki sposb, jakby bya typem, i w zwizku z tym uycie typedefjest niezbdne. Jednym z miejsc, w ktrych wygodnie jest uywa typedef, s typy wskanikowe. Jakju wspomniano, definicja: int* x, y; oznacza w rzeczywistoci, e zmienna x jest typu int*, a zmienna y typu int (nie int*). Oznacza to, e znak *" jest wizany prawostronnie, a nie lewostronnie. Jeeli jednak uyje si sowa kluczowego typedef: typedef int* IntPtr; IntPtr x, y; to zarwno x, jak i y bd typu int*. Mona twierdzi, e w przypadku prostych typw danych lepiej jest unika stosowania typedef, dziki czemu zachowana zostanie przejrzysto, a zatem rwnie czytelno programu. Rzeczywicie, uycie wielu deklaracji typedef znacznie zmniejsza czytelno programu. Jednake wjzyku C stosowanie typedefjest szczeglnie wane w odniesieniu do struktur.

czenie zmiennych w struktury


Sowo kluczowe struct umoliwia poczenie grupy zmiennych w struktur. Po utworzeniu struktury mona zdefiniowa wiele egzemplarzy zmiennych ustanowionego przez ni nowego" typu danych. Na przykad: //: C03:SimpleStruct.cpp struct Structurel { char c: int i: float f, double d: int main() { struct Structurel sl. s2; sl.c = ' a ' : // Wybor elementu za pomoca. sl.i - 1: sl.f = 3.14: sl.d = 0.00093; s2.c = ' a ' ; s2.i = 1: s2.f = 3.14; s2.d = 0.00093;

Thinking in C++. Edycja polska Deklaracja struct musi koczy si rednikiem. W funkcji main( ) utworzono dwa egzemplarze struktury Structurel sl i s2. Kady z nich posiada odrbne wersje elementw c, i, f i d. A wic sl i s2 reprezentuj dwie grupy zupenie niezalenych od siebie zmiennych. Do wyboru poszczeglnych elementw w obrbie sl i s2 uywany jest znak ." podobnie jak w przykadach stosowania obiektw klas w C++, zaprezentowanych w poprzednim rozdziale. Poniewa klasy powstay ze struktur, wic wywodzi si z nich rwnie ta skadnia. Zwraca uwag niewygodny sposb uywania struktury Structurel ^ak si okae, dotyczy to wycznie jzyka C, a nie C++). W jzyku C, podczas definiowania zmiennych, nie mona napisa Structurel trzeba natomiast dokona zapisu: struct Structurel. Jest to przypadek, w ktrym w jzyku C szczeglnie przydaje si deklaracja typedef: //: C03:SimpleStruct2.cpp // Uycie typedef w stosunku do struktury typedef struct { char c: int i ; float f; double d; } Structure2; int main() { Structure2 sl, s2; sl.c = 'a' ; sl.i = 1; sl.f = 3.14; sl.d = 0.00093; s2.c = 'a'; s2.i = 1; S2.f - 3.14; s2.d = 0.00093; Uywajc w taki sposb deklaracji typedef (przynajmniej w C w jzyku C++ sprbuj usun sowo typedef), mona, podczas definiowania zmiennych sl i s2, udawa, e Structure2 jest typem wbudowanym, podobnie jak int czy float (zawiera on tylko wycznie dane, czyli cechy, a nie zachowanie, co miaoby miejsce w C++ w przypadku prawdziwych obiektw). Nietrudno zauway, e na pocztku deklaracji pominito identyfikator struktury, poniewa jej celem byo jedynie nadanie typowi nazwy. Moe jednak zaistnie potrzeba odwoania si do struktury z wntrza jej definicji. W takich przypadkach mona powtrzy t sam nazw zarwno jako nazw struktury, jak i w deklaracji typedef: / / : C03:SelfReferential.cpp // Struktura odwoujca si do siebie samej typedef struct SelfReferential { SelfReferential* sr; // Niesamowite, prawda? } SelfReferential;
int i ;

Rozdzia 3. Jzyk C w 1nt main() { SelfReferential srl. sr2; srl.sr = &sr2; sr2.sr = &srl; srl.i = 47; sr2.i = 1024;

C++

147

atwo zauway, e zmienne srl i sr2 wskazuj na siebie nawzajem, a take kada z nich zawierajakie dane. Nazwa zdefiniowana przez struct nie jest t sam nazw, ktra zostaa utworzona przez typedef, ale zazwyczaj taki sposbjej uycia stanowi uatwienie.

Wskaniki i struktury
Wszystkimi strukturami, zawartymi w poprzednich przykadach, operuje si w taki sam sposb, jak obiektami. Jednak, podobnie jak w przypadku dowolnego obszaru pamici, mona rwnie pobra adres struktury ^ak to pokazano w programie SelfReferential.cpp). Jak wskazano powyej, do wyboru elementu obiektu, bdcego struktur, uywa si znaku .". Jednak w przypadku wskanika do struktury, do wyboru elementu uywa si operatora: ->". Oto przykad: / / : C03:SimpleStruct3.cpp // Uywanie wskanikw do struktur typedef struct Structure3 { char c; float f; double d; } Structure3;
int i ;

int main() { Structure3 sl. s2; Structure3* sp = &sl;


sp->c sp->i sp->f sp->d = = 'a' ; 1; 3.14; 0.00093; 'a' ; 1; 3.14; 0.00093;

sp->c sp->i sp->f sp->d

sp = &s2; // Wskazuje na inna struktur


= = = =

} ///:-

W funkcji main( ) zmienna sp, bdca wskanikiem, wskazywaa najpierw na struktur sl, ktrej skadowe zostay zainicjowane za pomocoperatora ->" (tego samego operatora mona uy do odczytania ich wartoci). Nastpnie sp wskazywaa na struktur s2, ajej skadowe zostay zainicjowane w taki sam sposb, jak skadowe sl. A zatem dodatkowa korzy wynikajca z zastosowania wskanikw polega na tym, e ich wartoci mog by dynamicznie zmieniane, dziki czemu wskazuj one rne obiekty. Umoliwia to wikszelastyczno w czasie pisania programu.

148

Thinking in C++. Edycja polska Oto wszystkie niezbdne informacje na temat struktur; dziki lekturze dalszej czci ksiki nabierzesz wprawy w ich uywaniu (a szczeglnie w stosowaniu ich bardziej efektywnych nastpcw klas).

Zwikszanie przejrzystoci programw za pomoc wylicze


Wyliczeniowy typ danych umoliwia powizanie nazw z liczbami, uatwiajc tym samym zrozumienie programu czytajcej go osobie. Sowo kluczowe enum (pochodzce z jzyka C) automatycznie numeruje list identyfikatorw, nadajc im wartoci 0, 1, 2 itd. Mona rwnie zadeklarowa zmienne wyliczeniowe (ktre zawsze s reprezentowane przez liczby cakowite). Deklaracja typu wyliczeniowego przypomina deklaracj struktury. Wyliczeniowe typy danych s przydatne w przypadku zapamitywania pewnego rodzaju cech:

//: C03:Enum.cpp // Zapamitywanie ksztatw

enum ShapeType { circle. square, rectangle }; // Musi koczy si rednikiem, tak jak struct
int main() { ShapeType shape = circle; // Jakie operacje.,. // A teraz dziaania zalene od ksztatu: switch(shape) { case circle: /* obsuga okregu */ break: case square: /* obsuga kwadratu */ break: case rectange: /* obsuga prostokta */ break:

Zmienna shape jest zmienn typu wyliczeniowego ShapeType, a jej warto jest porwnywana z wartociami znajdujcymi si na licie elementw wyliczenia. Jednak poniewa zmienna shape jest w rzeczywistoci zmienn cakowit, moe ona przyjmowa dowolne wartoci typu int (w tym liczby ujemne). Z wartociami znajdujcymi si na licie elementw wyliczenia mog by rwnie porwnywane zmienne typu int. Program wykorzystujcy zaprezentowany w powyszym przykadzie sposb wyboru dziaania moe sprawia problemy. Jzyk C++ umoliwia znacznie lepszy sposb wykonywania takich operacji, ale jego opis zostanie przedstawiony dopiero w dalszej czci ksiki.

Rozdzia 3. Jzyk C w

C++

14!

Jeeli nie odpowiada ci sposb, w jaki kompilator przypisuje poszczeglne wartoci moesz zrobi to sam, takjak w przykadzie poniej:
enum ShapeType { circle - 10, square = 20, rectangle = 50 }:

Jeeli wartoci zostan nadane tyIko niektrym nazwom, kompilator uyje w stosunku do pozostaych nazw kolejnych wartoci cakowitych. Na przykad w przypadki deklaracji: enum snap { crackle = 25. pop }; kompilator nada elementowi pop warto 26. Nietrudno zauway, e program wykorzystujcy typy wyliczeniowe jest znacznie bardziej przejrzysty. Jednak, w pewnej mierze, jest to jedynie prba (w jzyku C) osignicia tego, co mona uzyska, wykorzystujc klasy w jzyku C++. Dlatego te deklaracja enumjest w C++ uywana rzadziej ni wjzyku C.

Kontrola typw w wyliczeniach


Wyliczenia s realizowane w jzyku C w do prosty sposb przypisuj one wartoci cakowite nazwom, nie dokonujc przy tym adnej kontroli typw. W jzyku C++, jak mona si tego spodziewa na podstawie dotychczasowej wiedzy, pojcie typu, podobnie zresztjak wylicze, ma charakter podstawowy. Tworzc wyliczenie o okrelonej nazwie, w rzeczywistoci generuje si nowy typ danych, tak samo jak w przypadku klas. Nazwa wyliczenia staje si sowem zastrzeonym w obrbie biecejjednostki kompilacji. Wyliczenia s w jzyku C++ objte dokadniejsz kontrol typw ni w C. Mona to zauway na przykad w przypadku egzemplarza zmiennej a, bdcej typu wyliczeniowego color (kolor). W jzyku C moliwy jest zapis a++, ale nie mona zastosowa go w C++. Wynika to z faktu, e inkrementacja zrniennej wyliczeniowej wymaga dokonania dwch konwersji typw, z ktrychjednajest wjzyku C++ dopuszczalna, a druga ju nie. Warto zmiennej wyliczeniowej jest najpierw rzutowana z typu color na typ int, nastpnie jest ona inkrementowana, a na koniec rzutowana z powrotem z typu int na typ color. W jzyku C++ to niedozwolone, poniewa typ color jest zupenie innym typem danych, niebdcym rwnowanikiem typu int. Wydaje si to logicznie; skd mona bowiem, na przykad, wiedzie, jaki kolor uzyska si w wyniku inkrementacji wartoci blue (niebieski)? Jeeli konieczna jest moliwo inkrementacji wartoci typu color, to powinien on by klas (posiadajc zdefiniowany operator inkrementacji), a nie wyliczeniem. Klas tak mona bowiem utworzy w sposb zapewniajcy znacznie wiksze bezpieczestwo. Podczas kadej prby niejawnej konwersji do typu wyliczeniowego kompilator bdzie sygnalizowa takie, rzeczywicie niebezpieczne, dziaanie. Podobn, dodatkowkontrol typw wjzyku C++ posiadajopisane poniej unie.

150

Thinking in C++. Edycja polska

Oszczdzanie pamici za pomoc unii


Zdarza si czasami, e program obsuguje rne typy danych, uywajc do tego celu tych samych zmiennych. W takim przypadku istniej dwa moliwe rozwizania: utworzenie struktury, zawierajcej te wszystkie typy danych, ktrych przechowywanie moe by potrzebne, albo wykorzystanie unii. Unia lokuje wszystkie swoje dane w tym samym miejscu wyznacza wielko pamici niezbdn do przechowania najwikszego umieszczonego w niej elementu. Wielko ta staje si zarazem wielkociunii. Unie suywane w celu oszczdzania pamici. Kada warto umieszczona w unii zajmuje miejsce rozpoczynajce si zawsze na pocztku unii, ale obejmujce tylko niezbdn ilo pamici. A zatem tworzy si w ten sposb superzmienn", zdoln do przechowywania dowolnej spord zmiennych unii. Adresy wszystkich zmiennych, wchodzcych w skad unii, s takie same (w przypadku klas lub struktur rne). Poniej przedstawiono prosty przykad uycia unii. Przeprowad eksperymenty z usuwaniem rnych elementw unii, obserwujc, jaki bdzie to miao wpyw na jej wielko. Zwr rwnie uwag na to, e nie ma sensu deklaracja wicej nijednego egzemplarza kadego z typw danych, wchodzcych w skad unii (chyba tylko po to, by odwoywa si do nich, uywajc rnych nazw). / / : C03:Un1on.cpp // Wielko uni1 1 jej proste wykorzystanie #include <iostream> using namespace std; union Packed { // Deklaracja podobna do klasy char 1 ; short j; long 1 ; float f; double d; // Un1a bedzie. miala wielko typu double, // poniewa jest to jej najwikszy element }; // rednik koczy deklaracj unii, podobnie jak struktury
int main() { 1nt k;

cout "sizeof(Packed) = " sizeof(Packed) endl; Packed x;


x.i = 'c' :

cout x.i endl : x.d = 3.14159; cout x.d endl ; Kompilator dokonuje odpowiedniego przypisania, zalenie od wybranej skadowej unii. Kompilator, po dokonaniu przypisania, pomija to, co dzieje si dalej z uni. W poniszym przykadzie mona przypisa zmiennej x warto zmiennopozycyjn: x.f = 2.222;

Rozdzia 3. Jzyk C w

C++

151

a nastpnie wyprowadzije na wyjcie w taki sposob,jakby bya wartocicakowit:


cout x.i;

Spowoduje to pojawienie si na wyjciu mieci.

Tablice
Tablice stanowi rodzaj typu zoonego, poniewa pozwalaj one poczy ze sob wiele zmiennych pod pojedyncz nazw, w taki sposb, e bd one wystpoway kolejno po sobie. Zapis: i n t a[10]; tworzy miejsce w pamici, przechowujce 10 zmiennych cakowitych, umieszczonych jedna za drug, bez przypisywania odrbnej nazwy kadej z nich wszystkie one uywaj natomiast pojedynczej nazwy a. Aby uzyska dostp do elementw tablicy, naley zastosowa t sam skadni (wykorzystujcnawiasy kwadratowe), ktra zostaa uyta do zdefiniowania tablicy:
a[5] = 47;

Naleyjednak pamita, e cho rozmiarem tablicy ajest 10, indeksyjej elementw rozpoczynaj si od zera ^est to czasami nazywane indeksowaniem zerowym). A zatem mona odwoywa si tylko do elementw o numerach od 0 do 9, tak jak to pokazano w poniszym przykadzie:
/ / : C03:Arrays.cpp #include <iostream> using namespace std;
int main() { int a[10]; for(int i = 0; i < 10: i++) { a[i] - i * 10;

cout "a[" i "] = " a[i] endl;

Dostp do tablic jest wyjtkowo szybki. Jednake nie ma tu adnej siatki zabezpieczajcej", chronicej przed przekroczeniem indeksu koca tablicy w takim przypadku wkracza si bowiem w obszar innych zmiennych. Inn niedogodnoci jest wymg zdefiniowania wielkoci tablicy na etapie kompilacji w przypadku koniecznoci zmiany rozmiaru tablicy w czasie pracy programu, nie mona tego zrobi przy uyciu pokazanej powyej skadni Qezyk C umoliwia dynamiczne tworzenie tablic, aIe jest to rozwizanie znacznie mniej eleganckie). Zaprezentowane w poprzednim rozdziale wektory, ktre mona wykorzysta w C++, udostpniaj obiekty podobne do tablic, automatycznie zmieniajce swj rozmiar. Stanowi wic na og znacznie lepsze rozwizanie w sytuacji, gdy wielko tablicy nie moe by z gry okrelona w czasie kompilacji.

152
1

Thinking in C++. Edycja polska Mona tworzy tablice dowolnego typu nawet tablice struktur:
/ / : C03:StructArray.cpp // Tablica struktur typedef struct { int 1. j. k; } ThreeDpoint; int main() { ThreeDpoint p[10]; for(int i = 0; i < 10; i++) { p[i].i = i + 1; p[i].j = i + 2: p[i].k = i + 3;

Zwr uwag na to, e identyfikator i, bdcy skadow struktury, jest niezaleny od zmiennej i, stanowicej licznik ptli for. Aby przekona si, e elementy tablicy s przechowywane w pamici jeden za drugim, mona wydrukowa ich adresy, takjak w przykadzie poniej:
/ / : C03:ArrayAddresses.cpp #include <iostream> using namespace std;

int main() { int a[10]; cout "sizeof(int) = " sizeof(int) endl; for(int i = 0; i < 10; i++) cout "&a[" i "] - " (long}&a[i] endl ; Po uruchomieniu programu mona zauway, e kady element tablicy znajduje si w odlegoci od poprzedniego elementu odpowiadajcej wielkoci liczby cakowitej (int). Oznacza to, e sone umieszczone po kolei w pamici.

Wskaniki i tablice
Identyfikatory tablic rni si od identyfikatorw zwykych zmiennych. Jednym z powodwjest to, e nie sone l-wartociami nie mona zatem przypisa im wartoci. W istocie s one punktem zaczepienia skadni, wykorzystujcej nawiasy kwadratowe podajc nazw tablicy bez nawiasw kwadratowych, uzyskuje si adres pocztku tablicy:
/ / : C03:ArrayIdentifier.cpp #include <iostream> using namespace std; int main() { int a[10]; cout "a = " a endl ; cout "&a[0] =" &a[0] endl;

Rozdzia 3. Jzyk C w

C++

153

Po uruchomieniu programu mona zauway, e oba adresy (ktre zostan wydrukowane szesnastkowo, nie wystpuje tu bowiem rzutowanie na typ long) s takie same. A zatem identyfikator tablicy mona traktowa rwniejako wskanik dojej pocztku, dostpny wycznie do odczytu. I chocia nie sposb zmieni wartoci identyfikatora tablicy, by wskazywa jakie inne miejsce, to mona utworzy inny wskanik, uywajc go do poruszania si w obrbie tablicy. W rzeczywistoci notacj wykorzystujc nawiasy kwadratowe mona zastosowa rwnie w stosunku do zwykych wskanikw:

//: C03:PointersAndBrackets.cpp int main() {


int a[10]; int* ip * a; for(int i - 0; i < 10; i++) i * 10;

To, e nazwa tablicy zwraca adres jej pocztku, okazuje si do istotne w przypadku przekazywania tablic funkcjom. Podczas deklaracji w charakterze argumentu funkcji tablicy jest deklarowany wskanik. A zatem, w poniszym przykadzie, listy argumentw funkcji funcl() i func2( ) s w istocie takie same:

//: C03:ArrayArguments.cpp #include <iostream> #include <string> using namespace std;


void funcl(int a[], int size) { for(int i = 0; i < size; i++)
a[i] = i * i - i;

void func2(int* a, int size) { for(int i = 0; i < size; i++)


a[i] - i * i + i ;

void print(int a[], string name, int size) for(int i = 0; i < size; i++) cout name "[" i "] - " a[i] endl ; int main() { // Tablice prawdopodobnie zawieraj mieci: print(a, "a", 5); print(b. "b", 5); // Inicjalizacja tablic: funcl(a. 5); funcl(b, 5); print(a, " a " , 5); print(b, "b", 5); // Tablice s zawsze modyfikowane:
int a[5]. b[5];

Thinking in C++. Edycja polska func2(a. func2(b. print(a. print(b, 5); 5); "a", 5); "b". 5);

Mimo e funkcje funcl( ) i func2( ) rni si deklaracjami swoich argumentw, sposb uycia argumentw w obrbie obu funkcji jest taki sam. W powyszym przykadzie ujawniy si jeszcze inne kwestie tablice nie mog by przekazywane 4 przez warto , to znaczy nigdy nie jest tworzona lokalna kopia tablicy, ktra zostaa przekazana funkcji. A zatem gdy podlega zmianie tablica, zawsze modyfikowany jest obiekt zewntrzny. Jeeli spodziewasz si, e zwyke" argumenty powinny by przekazywane przez warto, moe to by na pierwszy rzut oka nieco mylce. Zwr uwag na to, e funkcja print( ) uywa nawiasw kwadratowych w stosunku do argumentw bdcych tablicami. Wprawdzie gdy przekazywane argumenty s tablicami, ujmowanie ich jako wskanikw jest praktycznie rwnowane stosowaniu notacji wykorzystujcej nawiasy kwadratowe, jednak uywanie nawiasw informuje osob czytajcprogram o tym, e argumentjest traktowanyjako tablica. Warto rwnie zauway, e w kadym przypadku funkcji przekazywany jest argument size. Przekazanie samego adresu tablicy nie jest wystarczajc informacj funkcja musi zawsze zna wielko tablicy po to, aby nie przekroczy jej zakresu. Tablice, w tym tablice wskanikw, mog by dowolnego typu. W rzeczywistoci jzyki C i C++ posiadaj specjaln list argumentw funkcji main( ), umoliwiajc przekazywanie programowi argumentw w wierszu polece. Lista ta ma nastpujc posta: int main(int argc, char* argv[]) { // . . . Pierwszy z argumentw jest liczb elementw zawartych w tablicy, bdcej drugim argumentem. Drugi argument jest zawsze tablic elementw typu char*, poniewa argumenty podane w wierszu polece s przekazywane zawsze jako tablice znakowe (a, jak pamitamy, tablice mog by przekazywane wycznie w postaci wskanikw). Kada oddzielona odstpami grupa znakw znajdujca si w wierszu polece jest traktowana jako argument i zapisywana w oddzielnej tablicy. Poniszy program drukuje wszystkie argumenty, podane w wierszu polece przy jego uruchomieniu, przechodzc przez tablic argv: / / : C03:CommandLineArgs.cpp #include <iostream> using namespace std;

Chyba e przyjmiesz dosowninterpretacj twierdzenia, e wszystkie argumenty wjzykach C i C++ sprzekazywane przez warto, a wartoci tablicyjest przekazywana przezjej identyfikator czyli adres tablicy". Z punktu widzenia jzyka asemblera moe wydawa si to prawd, ale nie sdz, by mogo uatwi prac z pojciami wyszego poziomu. Dodanie wjzyku C++ referencji powoduje, e argument dotyczcy przekazywania wszystkiego przez warto" wprowadza jeszcze wikszy baagan dotego stopnia, e sdz, i lepiej jest myle o przekazywaniu przez warto", jako przeciwiestwie przekazywania adresu".

Rozdzia 3. Jzyk C w int main(int argc, char* argv[]) { cout "argc = " argc endl; for(int 1 = 0; 1 < argc; i++) cout "argv[" i "] = " argv[i] endl; } ///:-

C++

155

Jak zauwaysz, argv[0] jest ciek dostpu i nazw programu. Pozwala to programowi na uzyskanie informacji o sobie samym. Powoduje rwnie dodanie jeszcze jednego elementu do tablicy argumentw, dlatego te typowym bdem przy pobieraniu argumentw wiersza polece jest uycie argumentu argv[0] w sytuacji, gdy potrzebny jest argument argv[l]. Nie ma koniecznoci uywania w funkcji main( ) identyfikatorw o nazwach argc oraz argv ich nazwy wynikaj tylko ze stosowanej konwencji (ale inni mog by zdezorientowani, jeeli zostan wykorzystane odmienne nazwy). Istnieje rwnie inny sposb zadeklarowania argumentu argv: int main(int argc, char** argv) { // . . . Oba sposoby s rwnowane, ale wersja uywana w ksice jest najbardziej intuicyjna i czytelna, poniewa oznacza ona wprost: ,jest to tablica wskanikw do znakw". Z wiersza polece mona odczyta jedynie tablice znakw jeeli chcesz traktowa argumenty w taki sposb, jakby byy jakich innych typw, musisz dokona ich konwersji w programie. W standardowej bibliotece jzyka C istniej funkcje, zadeklarowane w <cstdlib>, uatwiajce konwersj znakw na liczby. Najatwiejszymi do uycia funkcjami s atoi(), atol( ) oraz atof( ), dokonujce konwersji tablicy znakw ASCII odpowiednio do typw: int, long i zmiennopozycyjnego typu double. Poniej znajduje si przykad z wykorzystaniem funkcji atoi( ) (dwie pozostae funkcje wywouje si w taki sam sposb): / / : C03:ArgsToInts.cpp // Konwersja argumentw wiersza polece // na liczby cakowite #include <iostream> #include <cstdlib> using namespace std; int main(int argc. char* argv[]) { for(int i = 1; i < argc: i++) cout atoi(argv[i]) endl;
} ///:-

Podczas wywoania programu w wierszu polece mona umieci dowoln liczb argumentw. Ptla for rozpoczyna si od wartoci 1 po to, by pomin nazw programu znajdujc si w argv[0]. Wpisanie w wierszu polece liczby zmiennopozycyjnej, zawierajcej kropk dziesitn, spowoduje, e funkcja atoi( ) uwzgldni wycznie cyfry znajdujce si przed kropk. Jeeli zostan natomiast podane znaki niebdce cyframi, to funkcja atoi() zwrci warto zero.

156

Thinking in C++. Edycja polska

Format zmiennopozycyjny
Wprowadzona we wczeniejszej czci rozdziau funkcja printBinary() jest wygodnym narzdziem, pozwalajcym na penetracj wewntrznej struktury rnych typw danych. Najbardziej interesujcym z nichjest format zmiennopozycyjny, umoliwiajcy w jzykach C i C++ reprezentacj zarwno bardzo duych, jak i bardzo maych wartoci, do czego wykorzystuje ograniczon ilo pamici. Mimo e nie sposb w niniejszym rozdziale przedstawi wszystkich szczegw, warto wiedzie, e bity w liczbach typw float i double s podzielone na trzy czci: wykadnik, mantys i bit znaku, a zatem przechowuj one wartoci wykorzystujc notacj wykadnicz. Poniszy program umoliwia zabaw, polegajcna drukowaniu dwjkowych wzorcw liczb zmiennopozycyjnych. Pozwala ona na wycignicie wnioskw dotyczcych formatu zmiennopozycyjnego, stosowanego przez kompilator (zazwyczaj jest to standard liczb zmiennopozycyjnych IEEE, ale uywany przez ciebie kompilator moe si do niego nie stosowa): / / : C03:FloatingAsBinary.cpp / / { L } printBinary #include "printBinary.h" #include <cstdlib> #include <iostream> using namespace std:
/ / { T } 3.14159

int main(int argc, char* argv[]) { if(argc !- 2) { cout "Musisz podac liczbe" endl; exit(l): 1 double d - atof(argv[l]); unsigned char* cp =

reinterpret_cast<unsigned char*>(&d); for(int i = sizeof(double); i > 0 ; i -- 2) { printBinary(cp[i-l]); printBinary(cp[i]);


}
III-

Najpierw program upewnia si, e zosta podany argument sprawdzajc warto argc, ktra wynosi dwa, gdy podano jeden argument ^esli nie podano w ogle argumentw, wynosi ona jeden, poniewa nazwa programu jest zawsze pierwszym elementem argv). Jeeli wynik sprawdzenia jest niepomylny, drukowany jest komunikat, a nastpnie wywoywana jest standardowa funkcja biblioteczna C, exit(), powodujca zakoczenie programu. Program pobiera argument z wiersza polece i za pomoc funkcji atof( ) przeksztaca znaki w liczb typu double. Nastpnie liczba tajest potraktowanajako tablica bajtw jej adresjest rzutowany na typ unsigned char*. Kady z tych bajtw zostaje przekazany funkcji printBinary() w celu wydrukowania. Przykad zosta przygotowany w taki sposb, by na moim komputerze bajty byy drukowane poczynajc od bitu znaku. Twj komputer moe by inny, jest wic moliwe, e zechcesz zmieni kolejno wydruku. Warto rwnie podkreli, e format zmiennopozycyjny nie jest trywialny. Na przykad wykadnik oraz mantysa nie s zazwyczaj

Rozdzia 3. Jzyk C w

C++

157

wyrwnane do granicy bajtw rezerwuje si dla nich okrelon liczb bitw, ktre snastpnie umieszczane w pamici moliwiejak najbliej. Aby naprawd wiedzie, co si dzieje, naley odgadn wielko poszczeglnych czci liczby (bit znaku jest zawsze pojedynczym bitem, lecz wykadnik i manlysa mog by rnych rozmiarw) i oddzielnie wydrukowa bity skadajce si na kadz nich.

Arytmetyka wskanikw
Gdyby jedyn rzecz moliw do zrobienia ze wskanikiem wskazujcym tablic byo traktowanie go w taki sposb, jakby by synonimem nazwy tej tablicy, wskaniki do tablic nie zasugiwayby na szczegln uwag. S one jednak bardziej elastyczne, poniewa mona zmieni ich warto, tak by wskazyway jakie inne miejsce (naley jednak pamita, e nie da si w taki sposb modyfikowa identyfikatorw tablic). Pojcie arytmetyki wskanikw odnosi si do zastosowania wybranych operatorw matematycznych w stosunku do wskanikw. Arytmetyka wskanikw stanowi temat odrbny w stosunku do zwykej arytmetyki, poniewa aby wskaniki zachowyway si prawidowo, musz podlega pewnym ograniczeniom. Na przykad operatorem czsto uywanym w stosunku do wskanikw jest ++, ktry zwiksza wskanik ojeden". Naprawd oznacza to, e warto wskanika zostaje tak zmieniona, by przesun si on do nastpnej pozycji" (cokolwiek by to znaczyo). Oto przykad: //: C03:PointerIncrement.cpp #include <iostream> using namespace std; int main() { int i[10]; double d[10]; int* ip = i ; double* dp = d; cout "ip - " ip++; cout "ip = " cout "dp = " dp*+; cout "dp = "

Oong)ip endl: (long)ip endl; (long)dp endl; (long)dp endl;

Program, uruchomiony na moim komputerze, daje nastpujce wyniki: ip - 6684124 ip = 6684128 dp = 6684044 dp = 6684052 Mimo e operacja ++ przebiega tak samo zarwno w przypadku typu int*, jak i double*, mona zauway, e wskazywany adres zmieni si tyIko o 4 bajty dla wskanika typu int*, natomiast a o 8 bajtw dla wskanika typu double*. Nie jest przypadkiem, e wanie takie s rozmiary typw int i double w moim komputerze. I to jest wanie sztuczka zwizana z arytmetyk wskanikw kompilator wyznacza wielko, ojak powinien zmieni si wskanik tak, by wskazywa on nastpn pozycj tablicy (arytmetyka wskanikw ma znaczenie tylko w obrbie tablic). Dziaa to w taki sposb nawet w przypadku tablic struktur:

Thinking in C++. Edycja polska / / : C03:PointerIncrement2.cpp #include <iostream> using namespace std; typedef struct { char c; short s; int i ; long 1 ; float f; double d; long double ld; } Primitives; int main() { Primitives p[10]; Primitives* pp = p; cout "sizeof(Primitives) = " sizeof(Primitives) endl; cout "pp - " (long)pp endl; pp++; cout "pp = " (long)pp endl ; Po uruchomieniu programu na moim komputerze uzyskaem nastpujce wyniki: sizeof(Pnmitives) pp = 6683764 pp = 6683804
40

A zatem kompilator obsuguje odpowiednio wskaniki do struktur (a take wskaniki do klas i unii). Arytmetyka wskanikw odnosi si rwnie do operatorw , + i -, lecz dwa ostatnie maj ograniczony zakres. Nie mona doda do siebie dwch wskanikw, a w przypadku odjcia od siebie wskanikw uzyskuje si w wyniku liczb znajdujcych si pomidzy nimi elementw. Mona jednak dodawa i odejmowa od wskanikw wartoci cakowite. Poniej znajduje si przykad ilustrujcy arytmetyk wskanikw: / / : C03:PointerArithmetic.cpp finclude <iostream> using namespace std; #define P ( E X ) cout #EX EX endl:

int main() { int a[10]; for(int i 0: i < 10: i++) a[i] = i // Przypisz wartoci indeksw int* ip = a: P(*ip): P(*++ip); P(*(ip + 5)): int* ip2 = ip + 5; P(*ip2); P(*(ip2 - 4)); P(*--ip2); P(ip2 - ip); // Wynikiem jest liczba elementw

Rozdzia 3. Jzyk C w

C++

159

Program rozpoczyna si jeszcze jedn makroinstrukcj, ale wykorzystuje ona cech preprocesora nazywan lacuchowaniem (zaimpIementowan w postaci znaku #", poprzedzajcego wyraenie), ktra zmienia dowolne wyraenie w acuch znakw. Jest to bardzo wygodne, pozwala bowiem na wydrukowanie wyraenia, po ktrym nastpuje przecinek, a nastpnie warto tego wyraenia. Sposb wykorzystania tego wygodnego skrtu widocznyjest w funkcji main(). Mimo e w stosunku do wskanikw poprawne s zarwno przedrostkowe, jak i przyrostkowe wersje operatorw ++ i --, w przykadzie uyto wycznie ich wersji przedrostkowych. W powyszych wyraeniach operacje s bowiem wykonywane jeszcze przed wyuskaniem wskanikw, dziki czemu moliwa jest obserwacja efektu dziaania operatorw. Zwr uwag, e dodawane i odejmowane od wskanikw s wycznie wartoci cakowite kompilator nie pozwoli na dodawanie lub odejmowanie od siebie dwch wskanikw. A oto wyniki dziaania powyszego programu:
*ip: 0 ++ * i p: 1 *dp + 5): 6 *ip2: 6 *(ip2 - 4): 2 *--ip2: 5 ip2 - ip: 4

We wszystkich przypadkach, dziki arytmetyce wskanikw, wskanik jest zmieniany w taki sposb, by wskazywa waciwe miejsce", w zalenoci od rozmiaru elementw. Nie przejmuj si, jeeli arytmetyka wskanikw wydaje ci si na pierwszy rzut oka nieco przytaczajca. Najczciej jedynie tworzy si tablice i indeksuje je przy uyciu nawiasw kwadratowych, a najbardziej wyrafinowanymi operacjami wykonywanymi na wskanikach s ++ i --. Arytmetyka wskanikw jest zarezerwowana dla bardziej zoonych i wyrafinowanych programw wiele spord kontenerw, znajdujcych si w standardowej bibliotece C++, ukrywa przed tob wikszo ze swoich przemylnie zrealizowanych szczegw, dziki czemu nie musisz si o nie martwi.

Wskazwki dotyczce Uruchamiania programw


Idealne rodowisko programistyczne zawieraoby wspaniay program uruchomieniowy (ang. debugger), dziki ktremu dziaanie programu stawaoby si zupenie przejrzyste, co z kolei pozwolioby na szybkie znalezienie znajdujcych si w nim bdw. Niestety, wikszo dostpnych programw uruchomieniowych ma jakie sabe strony i wymaga umieszczenia w programie fragmentw kodu uatwiajcych prac programicie. Mona rwnie pracowa w rodowisku (na przykad takim, jak systemy wbudowane, w ktrych latami nabywaem dowiadczenia), w ktrym nie s dostpne

160

Thinking in C++. Edycja polska

adne programy uruchomieniowe, a moliwoci komunikacji zwrotnej programu z uytkownikiem s nader ograniczone (sprowadzajce si, na przykad, do wykorzystania jednowierszowego wywietlacza LED). W takich przypadkach przydaje si pomysowo, dotyczca sposobw wyszukiwania i wywietlania informacji o przebiegu wykonania programu. W niniejszym rozdziale opisano niektre przydatne w tym wzgldzie techniki.

Znaczniki uruchomieniowe
Wprowadzenie kodu uruchomieniowego na stae do programu moe spowodowa problemy. Zaczyna pojawia si zbyt wiele informacji, co sprawia trudnoci z wyodrbnieniem z nich bdw. Kiedy wszystko wskazuje na to, e zosta ju znaleziony bd, rozpoczyna si wyrzucanie z programu kodu uruchomieniowego po to, by pniej doj do wniosku, e trzeba umieci go tam z powrotem. Mona rozwiza te problemy, uywajc dwch rodzajw znacznikw znacznikw uruchomieniowych preprocesora oraz znacznikw sprawdzanych w czasie pracy programu.

Znaczniki uruchomieniowe preprocesora


Wykorzystujc dyrektyw preprocesora #define do zdefiniowania jednego lub wikszej liczby znacznikw (najlepiej w pliku nagwkowym), mona nastpnie testowa go za pomoc dyrektywy #ifdef, warunkowo wczajc do programu kod uruchomieniowy. Kiedy uruchamianie programu wydaje si ju zakoczone, mona po prostu uniewani definicj znacznika dyrektyw #undef, co powoduje automatyczne usunicie kodu uruchomieniowego (a zarazem zmniejszenie narzutu zwizanego z jego wykonywaniem oraz wielkoci programu). Warto okreli nazwy znacznikw uruchomieniowych jeszcze przed przystpieniem do realizacji projektu, co pozwoli na zachowanie ich spjnoci. Znaczniki preprocesora szwyczajowo zapisywane wielkimi literami, co odrniaje od zmiennych. Czsto uywan nazw znacznika jest po prostu DEBUG (ale naley uwaa, by nie uy nazwy NDEBUG, bdcej nazw zastrzeon w jzyku C). Kolejno uytych dyrektyw moe by nastpujca:
#define DEBUG // Prawdopodobnie w pliku nagwkowym

#ifdef DEBUG // Sprawdzenie, czy zdefiniowano znacznik /* kod uruchomieniowy */ #endif // DEBUG
Wikszo implementacji jzykw C i C++ pozwala rwnie na definiowanie i uniewanianie znacznikw z poziomu wiersza polece kompilatora, dziki czemu mona dokona powtrnej kompilacji kodu i umieszczenia w nim informacji uruchomieniowej za pomocjednego polecenia (najlepiej uywajc do tego makefile narzdzia, ktre zostanie krtko opisane). Szczegy na ten temat mona znale w dokumentacji kompilatora.

//...

Rozdzia 3. Jzyk C w

C++

161

Znaczniki uruchomieniowe sprawdzanie w czasie pracy programu


W pewnych przypadkach wygodniej jest wycza i wcza znaczniki uruchomieniowe w czasie pracy programu, w szczeglnoci ustawiajc je w momencie uruchamiania w wierszu polece. Powtrna kompilacja programu tylko po to, aby wstawi do niego kod uruchomieniowy, jest natomiast nuca. Aby dynamicznie wcza i wycza kod uruchomieniowy, naley utworzy znaczniki typu bool: / / : C03:Dynam1cDebugFlags.cpp #include <iostream> #include <string> using namespace std; // Znaczniki uruchomieniowe nie musza // by koniecznie globalne: bool debug = false; int main(int argc. char* argv[]) { for(int i = 0: i < argc: i++) if(string(argv[i]) == "--debug=on") debug = true: bool go = true: while(go) { if(debug) { // Kod uruchomieniowy cout "Program uruchomieniowy wlaczony!" endl: } else { cout "Program uruchomieniowy wylaczony." endl: } cout "Zmien stan programu [wlacz/wylacz/wyjdz]: "; string reply; cin reply; if(reply == "wlacz") debug = true: // wcz go if(reply == "wylacz") debug = false: // Wylacz if(reply == "wyjdz") break: // Wyjcie z petli 'while'

Program pozwala na wczanie i wyczanie znacznika uruchomieniowego, a do momentu, kiedy zostanie wpisane polecenie wyjd", powodujce opuszczenie programu. Zwr uwag na to, e program wymaga wpisywania caych sw, a nie pojedynczych liter (moesz go jednak zmieni w taki sposb, by przyjmowa litery). Do wczenia kodu uruchomieniowego na pocztku programu mona rwnie uy opcji podanej w wierszu polece moe ona wystpi w dowolnym miejscu wiersza polece, poniewa kod znajdujcy si na pocztku funkcji main( ) przeglda wszystkie argumenty. Dziki wyraeniu: string(argv[i]) sprawdzenie jej obecnoci jest do atwe do przeprowadzenia. Pobiera ono tablic znakow argv[i] i tworzy acuch. Powyszy program poszukuje caego acucha -debug=on. Mona rwnie szuka acucha -^lebug=, a nastpnie sprawdza, co znajduje si w jego dalszej czci, udostpniajc w ten sposb wicej opcji. Drugi tom ksiki (dostpny w witrynie hltp:/Aielion.pUonline/thinking/index.html) zawiera rozdzia powicony klasie string, nalecej do standardu C++.

162

Thinking in C++. Edycja polska Mimo e znaczniki uruchomieniowe stanowi jeden z nielicznych przypadkw, w ktrych jest uzasadnione uycie zmiennych globalnych, to niekoniecznie musi tak by. Zwr rwnie uwag na to, e zmienna debug zostaa zapisana malymi literami, co przypomina osobie czytajcej program, e niejest ona znacznikiem preprocesora.

Przeksztateanie zmiennych i wyrae w tecuchy


Podczas tworzenia kodu uruchomieniowego niewygodne jest wpisywanie wyrae drukujcych, skadajcych si z tablic znakowych (zawierajcych nazwy zmiennych), a nastpnie samych zmiennych. Na szczcie standard jzyka C zawiera operator acuchowania #", ktry wystpi ju we wczeniejszej czci rozdziau. Umieszczenie znaku # przed argumentem makroinstrukcji preprocesora powoduje przeksztacenie tego argumentu w tablic znakow. Skojarzenie tego z faktem, e tablice znakowe, pomidzy ktrymi nie wystpuj znaki przestankowe, s czone w pojedyncz tablic znakow, pozwala na napisanie bardzo wygodnej makroinstrukcji, drukujcej warto zmiennej w trakcie uruchamiania programu:

#define PR(x) cout #x " = " x "\n";


Wywoanie makroinstrukcji PR(a) w celu wydrukowania wartoci zmiennej a przyniesie taki sam rezultat, jak kod:
cout "a = " a "\n";

To samo dotyczy caych wyrae. Poniszy program wykorzystuje makroinstrukcj w celu utworzenia skrtu drukujcego wyraenie zamienione w tablic znakow, a nastpnie wyznaczajcego i drukujcego warto tego wyraenia:
/ / : C03'.StringizingExpressions.cpp #include <iostream> using namespace std; #define P(A) cout #A " : " (A) endl: int main() { int a - 1. b = 2. c - 3; P(a); P(b); P(c): P(a + b): P((c - a)/b):

Nietrudno zauway, w jaki sposb techniki mog szybko sta si niezastpione, szczeglnie gdy nie dysponujesz programem uruchomieniowym (lub musisz korzysta z wielu rodowisk projektowych). Moesz uy rwnie dyrektywy #ifdef, dziki ktrej makroinstrukcja P(A) zostanie zdefiniowanajako nic", gdy zamierzasz usun kod uruchomieniowy.

Makroinstrukcja assert( ) jzyka C


W standardowynvpliku nagwkowym <cassert> znajduje si wygodna makroinstrukcja uruchomieniowa assert( ). Uywajc makroinstrukcji assert( ), naley poda

Rozdzia 3. Jzyk C w

C++

163

jej argument, bdcy wyraeniem, ktre deklaruje sijako prawdziwe". Preprocesor generuje kod, weryfikujcy prawdziwo wyraenia. Jeeli nie jest ono prawdziwe, program zatrzyma si, wytwarzajc komunikat o bdzie, informujcy o postaci wyraenia oraz o tym, e niejest ono prawdziwe. Prezentuje to poniszy, prosty przykad: / / : C03:Assert.cpp // Uycie makroinstrukcji uruchomieniowej assert() #include <cassert> // Zawiera makroinstrukcje using namespace std;
int main() {
int i - 100;

assert(i != 100); // Niepowodzenie } lll-.~ Makroinstrukcja zostaa napisana w standardowym C, jest wic rwnie dostpna w pliku nagwkowym assert.h. Po zakoczeniu uruchamiania programu mona usun kod tworzony przez makroinstrukcj, umieszczajc w programie, przed wczeniem pliku <cassert>, wiersz: #define NDEBUG lub definiujc znacznik NDEBUG w wierszu polece kompilatora. NDEBUG jest znacznikiem uywanym w pliku <cassert> do zmiany sposobu, w jaki generowany jest kod makroinstrukcji. W dalszej czci ksiki zostan przedstawione bardziej wyrafinowane rozwizania, alternatywne w stosunku do makroinstrukcji assert().

Adresy funkcji
Funkcja skompilowana i zaadowana do komputera w celu wykonania zajmuje pewien obszar pamici. Obszar ten, a wic rwnie funkcja, posiadajjaki adres. Jzyk C nigdy nie by jzykiem uniemoliwiajcym programicie podejmowania potencjalnie niebezpiecznych dziaa. Adresw zmiennych mona uywa za pomoc wskanikw, podobnie jak w przypadku adresw zmiennych. Deklaracja i sposb wykorzystywania wskanikw do funkcji wygldaj na pierwszy rzut oka troch nieprzejrzycie, ale s one zgodne z konwencj stosowan w innych konstrukcjach jzyka.

Definicja wskanika do funkcji


Aby zdefiniowa wskanik do funkcji, ktra nie posiada argumentw i nie zwraca wartoci, naley napisa:
void (*funcPtr)O;

164

Thinking in C++. Edycja polska W przypadku tak skomplikowanej definicji najlepiej jest rozpocz jej analiz od rodka, a nastpnie przesuwa si stopniowo na zewntrz. Rozpoczcie od rodka" oznacza rozpoczcie od nazwy zmiennej, ktrjest funcPtr. Przesuwanie si na zewntrz" to natomiast poszukanie najbliszego obiektu po prawej stronie (w tym przypadku nie ma tam niczego poszukiwania kocz si bowiem szybko nawiasem zamykajcym), a nastpnie po lewej stronie (gdzie znajduje si gwiazdka, oznaczajca wskanik), pniej znw po prawej (gdzie znajduje si pusta lista argumentw, okrelajca funkcj nieprzyjmujc adnych argumentw), a nastpnie znowu po lewej (sowo kluczowe void, oznaczajce, e funkcja nie zwraca-adnej wartoci). Tego typu przegldanie deklaracji z lewa na prawo tam i z powrotem sprawdza si w wikszoci przypadkw. Podsumujmy: rozpoczynamy od rodka" (,JuncPtrjest..."), przesuwamy si w prawo (nic tam nie ma zatrzymujemy si na nawiasie zamykajcym), przesuwamy si w lewo i napotykamy *" (...wskanikiem do..."), przesuwamy si w prawo i znajdujemy pust list argumentw (...funkcji, ktra nie przyjmuje argumentw..."), przesuwamy si w lewo i napotykamy sowo void (funcPtr jest wskanikiem do funkcji, ktra nie przyjmuje argumentw i zwraca warto void"). By moe dziwisz si, dlaczego *funcPrt znajduje si w nawiasie. Gdyby go nie byo, kompilator zobaczyby tekst: void *funcPtr(); zmiennej, zadeklarowano by funkcj (zwracajc warto void*). Mona sobie wyobrazi, e kompilator, prbujc okreli, jakiego rodzaju jest dana deklaracja lub definicja, przebywa tak sam drog, jak opisana powyej,. Nawias jest mu potrzebny do tego, by odbi si w przeciwnym kierunku", i przesuwajc si w lewo znale *" zamiast, poruszajc si dalej w prawo, napotka pustlist argumentw.

Skomplikowane deklaracje i definicje


Zapoznawszy si ze skadni deklaracji w jzykach C i C++, mona tworzy bardziej skomplikowane definicje. Na przykad takie: / / : Cff3:ComplicatedDefinitions.cpp /* 1. */ /* 2. */ /* 3. */ /* 4. */ void * (*(*fpl)(int))[10]; float (*(*fp2)(int.int.float))(1nt): typedef double (*(*(*fp3)O)[10])O: fp3 a; int (*(*f4())[10])O;

int main() {} lll:~ Aby j zrozumie, przejd przez kad z nich, przesuwajc si raz w prawo, raz w lewo. Pierwsza definicja gosi, e ,Jpl jest wskanikiem do funkcji przyjmujcej cakowity argument i zwracajcej wskanik do tablicy, zawierajcej 10 wskanikw do typu void".

Rozdzia 3. Jzyk C w

C++

165

Wedug drugiej definicji: ,,fp2 jest wskanikiem do funkcji, przyjmujcej trzy argumenty (typw int, int i float) i zwracajcej wskanik do funkcji, przyjmujcej argument cakowity i zwracajcej warto typu float". Tworzc du liczb definicji, mona uy deklaracji typedef. Trzeci przykad pokazuje, w jaki sposb uycie typedef moe oszczdzi koniecznoci wpisywania za kadym razem skomplikowanych opisw. Oznacza on: ,fp3 jest wskanikiem do funkcji nieprzyjmujcej argumentw i zwracajcej wskanik do tablicy 10 wskanikw do funkcji, ktre nie przyjmuj argumentw i zwracaj wartoci typu double". Nastpnie okrela, e zmienna a jest typu fp3". Deklaracja typedef jest na og przydatna do tworzenia skomplikowanych opisw, na podstawie prostych. Czwarty przykad nie jest definicj zmiennej, lecz deklaracj funkcji. Oznacza ona: ,/4jest funkcj zwracajc wskanik do tablicy 10 wskanikw do funkcji, zwracajcych wartoci cakowite". Rzadko (o ile w ogle) spotrzebne rwnie skomplikowane deklaracje i definicje, jak te przedstawione powyej. Jeeli jednak wykonasz wiczenie, majce na celu ich zrozumienie, bez trudu poradzisz sobie z prostszymi przypadkami.

Wykorzystywanie wskanikw do funkcji


Po zdefiniowaniu wskanika do funkcji, jeszcze przedjego uyciem, trzeba przypisa mu adres. Podobnie jak adres tablicy arr[10] jest zwracany przez jej nazw, podan bez nawiasu kwadratowego (arr), adres funkcji func( ) uzyskuje si za pomoc nazwy funkcji, pozbawionej listy argumentw (func). Mona rwnie uy w tym celu bardziej jednoznacznej skadni: &func(). Aby wywoa funkcj, trzeba dokona operacji wyuskania wskanika, w taki sam sposb, w jaki zosta on zadeklarowany (naley pamita, e wjzykach C i C++ definicje szawsze zblione do sposobu, wjaki uywane s definiowane obiekty). Poniszy przykad przedstawia sposb zdefiniowania i pniejszego uycia wskanika do funkcji: //: C03:PointerToFunction.cpp // Definicja i wykorzystanie wskanika do funkcji #include <iostream> using namespace std; void func() { cout "func() wywoana..." endl; int main() { void (*fp)O; // Definicja wskanika do funkcji fp = func: // Inicjalizacja wskanika C*fp)O: // Wyuskanie powoduje wywoanie funkcji void (*fp2)O = func; // Definicja i inicjalizacja (*fp2)O;

166

Thinking in C++. Edycja potska

Po zdefiniowaniu fp jako wskanika do funkcji, za pomoc instrukcji fp = func (zwr uwag na to, e nazwa funkcji wystpuje tu bez listy argumentw) zostaje mu przypisany adres funkcji func(). Drugi przypadek prezentuje rwnoczesn definicj i inicjalizacj.

Tablice wskanikw do funkcji


Jedn z najciekawszych konstrukcji, jakie mona utworzy, s tablice wskanikw do funkcji. Aby wybra funkcj, wystarczy wybra indeks tablicy i dokona wyuskania wskanika. Odpowiada to koncepcji programu sterowanego tabel (ang. table-driven code) zamiast instrukcji warunkowych lub instrukcji wyboru, na podstawie zmiennej stanu (lub kombinacji wielu zmiennych stanu) okrela si funkcj, ktra ma zosta wykonana. Taki sposb projektowania moe by przydatny, gdy czsto dodaje si lub usuwa funkcje zawarte w tabeli (albo zamierza tworzy lub zmienia taktabel dynamicznie). Nastpny przykad tworzy kilka lepych" funkcji, uywajc do tego makroinstrukcji preprocesora, a nastpnie przygotowuje tablic wskanikw do tych funkcji, wykorzystujc automatyczn inicjalizacj agregatow. A zatem mona atwo dodawa i usuwa funkcje znajdujce si w tabeli (czyli zmienia tym samym funkcjonowanie programu), modyfikujc tylko niewielkcz kodu: //: C03:FunctionTable.cpp // Wykorzystanie tablicy wskanikw do funkcji #include <iostream> using namespace std; // Makroinstrukcja, definiujca "lepe" funkcje: #define DF(N) void NO { \ cout "funkcja " #N " wywoana..." endl; } DF(a); DF(b): DF(c): DF(d): DF(e); DF(f); DF(g); void (*func_table[])O = { &. b, c. d, e. f. g }: int main() { while(l) { cout "nacisnij klawisz od 'a' do 'g' " "albo w, aby wyj z programu" endl: char c. cr; cin.get(c): cin.get(cr): // drugi dla CR

if ( c == 'w' )

if ( c < 'a' | | c > 'g' )


continue: (*func table[c - 'a'])O:

break: // ... wyjcie z while(l)

By moe zdajesz sobie teraz spraw, jak bardzo przydatna moe by ta technika do tworzenia niektrych rodzajw interpreterw lub programw przetwarzajcych listy.

Rozdzia 3. * Jzyk C w

C++

167

Make zarzdzanie rozczn kompilacj


W trakcie roztcznej kompilacji (po podzieleniu kodu na pewn liczb jednostek translacji) potrzebny jest jaki sposb, umoliwiajcy automatyczn kompilacj kadego pliku i informujcy program czcy, by scali wszystkie fragmenty w tym odpowiednie biblioteki i kod inicjujcy w program wykonywalny. W przypadku wikszoci kompilatorw mona tego dokona, uywajc pojedynczej instrukcji wiersza polece. Dla kompilatora GNU C++ mona na przykad napisa:
g++ PlikZrodlowyl.cpp PlikZrodlowy 2.cpp

Problemem wynikajcym z takiego podejcia jest to, e kompilator skompiluje najpierw oddzielnie kady z plikw, niezalenie od tego, czy wymaga on powtrnej kompilacji, czy te nie. Jeli projekt skada si z wielu plikw, konieczno ponownej kompilacji wszystkiego w sytuacji, gdy zmieni si tylkojeden z plikw, moe okaza si zbyt kosztowna. Rozwizaniem tego problemu, opracowanym w systemie Unix, ale dostpnym w takiej czy innej postaci w kadym systemie, jest program o nazwie make. Zarzdza on wszystkimi plikami wchodzcymi w skad projektu, zgodnie z instrukcjami zawartymi w pliku tekstowym o nazwie makefile. Po dokonaniu zmian w niektrych plikach tworzcych projekt i wpisaniu polecenia make, program ten wykorzystuje wskazwki zawarte w pliku makefile, porwnujc daty plikw rdowych z odpowiednimi plikami docelowymi. W przypadku gdy data pliku rdowego jest pniejsza ni data pliku docelowego, wywouje on dla tego pliku rdowego kompilator. Program make ponownie kompiluje tylko te pliki rdowe, ktre ulegy zmianie, oraz pliki rdowe, na ktre miay wpyw dokonane zmiany. Dziki programowi make nie ma potrzeby powtrnej kompilacji wszystkich plikw wchodzcych w skad projektu, ilekro dokonane zostanjakie zmiany, ani te sprawdzania, czy wszystko zostao wykonane poprawnie. Plik makefile zawiera wszystkie polecenia niezbdne do zoenia projektu w cao. Jeeli nauczysz si uywania polecenia make, pozwoli ci to na zaoszczdzenie mnstwa czasu i problemw. Zauwaysz rwnie, e program make jest zazwyczaj uywany do instalacji nowych programw w komputerach pracujcych pod kontrol systemw operacyjnych Linux i Unix (chocia ich pliki makefile s na og znacznie bardziej zoone ni te prezentowane w ksice, a ponadto czsto w ramach procesu instalacji pliki te mog zosta automatycznie utworzone dla danego komputera). Poniewa program make jest dostpny w takiej czy innej postaci dla kadego kompilatora C++ (a nawet gdy nie jest, to z kadym kompilatorem mona uy jego bezpatnie dostpnych wersji), bdzie on narzdziem uywanym przeze mnie w caej ksice. Producenci kompilatorw opracowali jednak rwnie wasne narzdzia, suce do tworzenia projektw. Narzdzia te pytaj programist o to, ktre pliki wchodz w skad projektu, a nastpnie same okrelaj wzajemne relacje pomidzy nimi. Uywaj one plikw podobnych do makefile, nazywanych na og plikami projektw, ktre sjednak nadzorowane przez rodowisko programistyczne. Sposb konfiguracji i wykorzystywania plikw projektw zaley od rodowiska projektowego.

L68

Thinking in C++. Edycja polska

dlatego informacji o ich uywaniu naley poszuka w odpowiedniej dokumentacji (chocia narzdzia suce do obsugi plikw projektw, dostarczane przez producentw kompilatorw, s zazwyczaj tak proste, e mona nauczy si ich moj ulubion metod nauki prb i bdw). Pliki makefile, wykorzystywane w ksice, powinny dziaa rwnie wtedy, gdy uywasz jakiego specyficznego narzdzia sucego do tworzenia projektw, dostarczonego przez producenta kompilatora.

Dziatenie programu make


Po wpisaniu polecenia make (albo innej nazwy, pod ktr dostpne jest to narzdzie) program poszukuje w biecym katalogu pliku o nazwie makeflle, ktry zosta utworzony przez autora projektu. Plik ten zawiera list zalenoci pomidzy plikami rdowymi. Program make sprawdza daty modyfikacji plikw. Jeeli plik zaleny posiada wczeniejsz dat ni plik, od ktrego on zaley, program make wykonuje regu, podan po tej zalenoci. Wszystkie komentarze znajdujce si w plikach makeflle rozpoczynaj si znakiem # i rozcigaj si a do koca wiersza. W prostym przypadku zawarto pliku makeflle programu o nazwie hello" moe wyglda nastpujco:
f Komentarz hello.exe: hello.cpp mojkompilator hello.cpp

Oznacza to, e plik hello.exe (docelowy) jest zaleny od pliku hello.cpp. W przypadku gdy plik hello.cpp ma pniejsz dat modyfikacji ni plik hello.exe, program make wykonuje regu" mojkompilator hello.cpp. Plik makeflle moe zawiera liczne zalenoci i reguy. Wiele programw make wymaga, by wszystkie reguy rozpoczynay si tabulacj. W przypadku innych, odstpy s generalnie pomijane, mona wic sformatowa plik w taki sposb, by by on czytelny. Reguy nie s ograniczone wycznie do wywoania kompilatora program make moe wywoa dowolny program. Tworzc grupy wzajemnie powizanych zalenoci i regu, mona zmodyfikowa pliki rdowe, wpisa polecenie make i mie pewno, e wszystkie zalene od nich pliki zostanodpowiednio przebudowane.

Makrodefmicje
Pliki makeflle mog zawiera makrodefinicje (nie s to makroinstrukcje preprocesora, dostpne w C i C++). Makrodefmicje pozwalaj na wygodn zamian acuchw. Pliki makeflle zawarte w ksice uywaj makrodefinicji do wywoania kompilatora C++. Na przykad:
CPP = mojkompilator hello.exe: hello.cpp S(CPP) hello.cpp

Rozdzia 3. Jzyk C w

C++

169

Znak = jest uywany do zdefiniowania CPP jako makrodefinicji, natomiast znak $ oraz nastpujca po nim para nawiasw powoduje jej rozwinicie. W powyszym przypadku rozwinicie oznacza, e wywoanie makrodefinicji $(CPP) zostanie zastpione acuchem mojkompilator. Chcc zmieni wykorzystywany kompilator na inny, o nazwie cpp, naley po prostu zmodyfikowa powysz makrodefinicj, tak by miaa ona posta:
CPP = cpp

Mona rwnie dopisa do tej makrodefinicji znaczniki kompilatora lub uy w tym celu oddzielnych makrodefinicji.

Regufy przyrostkowe
Przekazywanie programowi make informacji o tym, w jaki sposb powinien on wywoa kompilator, oddzielnie dla kadego pliku cpp zawartego w projekcie, jest niewygodne szczeglnie gdy wiadomo, e za kadym razem wywoanie to wyglda dokadnie tak samo. Poniewa program make powsta po to, by oszczdzi na czasie, umoliwia on stosowanie skrtw podejmowanych dziaa pod warunkiem, e s one zalene od przyrostkw nazw plikw. Skrty te s nazywane reguami przyrostkowymi. Regua przyrostkowa jest sposobem poinformowania programu make o tym, wjaki sposb przeksztaci plik o okrelonym rozszerzeniu nazwy (np. .cpp) w plik o innym rozszerzeniu (np. .obj Iub .exe). Po przekazaniu programowi make regu, dotyczcych tworzeniajednych rodzajw plikw z innych, wystarczy poda informacj dotyczc zalenoci pomidzy plikami. Kiedy program make znajdzie plik o dacie modyfikacji wczeniejszej ni pliku, od ktrego jest on zaleny, wykorzysta t regu do utworzenia nowego pliku. Regua przyrostkowa informuje program make, e nie potrzebuje on szczegowych regu okrelajcych sposb tworzenia kadego pliku, ale moe okreli to na podstawie rozszerzenia nazwy pliku. W tym przypadku oznacza ona: aby utworzy plik o nazwie zakoczonej na exe na podstawie pliku, ktrego nazwa koczy si na cpp, naley wywoa nastpujce polecenie". Otojak wyglda to dla powyszego przykadu: CPP = mojkompilator .SUFFIXES: .exe .cpp .cpp.exe: S(CPP) $< Dyrektywa .SUFFIXES informuje program make, e powinien on zwrci uwag na pliki o podanych rozszerzeniach nazw, poniewa w obrbie biecego pliku makefile maj one szczeglne znaczenie. Nastpnie widoczna jest regua przyrostkowa .cpp.exe, goszca: w taki sposb mona przeksztaci kady plik o rozszerzeniu nazwy cpp w plik o rozszerzeniu exe" (w przypadku gdy plik cpp by modyfikowany pniej ni plik exe). Podobniejak poprzednio uywanajest makrodefinicja $(CPP), ale pojawia si rwnie co nowego: $<. Poniewa sekwencja ta rozpoczyna si znakiem $", jest ona makrodefinicj, ale o specjalnym charakterze wbudowan makrodefinicjprogramu make. Moe ona by uywana wycznie w reguach przyrostkowych i oznacza: co, co byo przyczyn zastosowania reguy" (nazywane rwnie elementem decvdujacym), co w tym przypadku przekada si na: plik cpp, ktry musi zosta skompilowany".

170

Thinking in C++. Edycja polska Po zdefiniowaniu regu przyrostkowych mona wyda polecenie w rodzaju make Union.exe", a zostanie zastosowana odpowiednia regua przyrostkowa, mimo e w pliku makefile nie wystpuje nigdzie sowo Union".

Domylne pliki wynikowe


Po makrodefinicjach i reguach przyrostkowych program make poszukuje w pliku pierwszego pliku wynikowego" i tworzy go, chyba e okreli si inaczej. W przypadku poniszego pliku makefile: CPP = mojkompilator .SUFFIXES: .exe .cpp .cpp.exe: S(CPP) $< targetl.exe: target2.exe: wpisanie po polecenia make" spowoduje utworzenie pliku targetl.exe (przy uyciu domylnej reguy przyrostkowej), poniewa jest to pierwszy plik docelowy, napotkany przez program make. Aby utworzy plik target2.exe, trzeba uy polecenia make target2.exe". Jest to niewygodne, wic zazwyczaj tworzy si lepy" plik wynikowy, ktry zaley od wszystkich pozostaych plikw wynikowych, jak w poniszym przykadzie: CPP = mojkompilator .SUFFIXES: .exe .cpp .cpp.exe: $(CPP) $< all: targetl.exe target2.exe W powyszym przykadzie all" nie istnieje i nie ma rwnie pliku o tej nazwie, wic po wpisaniu polecenia make program ten wykrywa plik all" jako pierwszy plik wynikowy na licie (a wic zarazem domylny plik wynikowy). Nastpnie zauwaa, e plik all" nie istnieje, wic najlepiej utworzy go, sprawdzajc wszystkie zalenoci. Analizuje wic plik targetl.exe i (uywajc reguy przyrostkowej) sprawdza najpierw, czy plik targetl.cpp istnieje, a nastpnie, czy by on modyfikowany pniej ni targetl.exe. Jeeli tak jest, program make wykonuje dziaanie zgodne z regu przyrostkow jeeli okreli si wyranie regu dla konkretnego pliku wynikowego, to zostanie ona uyta zamiast reguy przyrostkowej). Nastpnie przechodzi on do kolejnego pliku, wymienionego na licie domylnych plikw wynikowych. Tak wic jeeli utworzy si list domylnych plikw wynikowych (okrelan tradycyjnie all", ale mona nada jej dowoln nazw), to mona spowodowa utworzenie wszystkich plikw wykonywalnych wchodzcych w skad projektu, wpisujc polecenie make". Mona ponadto utworzy listy plikw docelowych, niebdcych plikami domylnymi, wykonujce inne dziaania. Na przykad mona je zorganizowa w taki sposb, aby wpisanie polecenia make debug" spowodowao przebudowanie wszystkich plikw, z wczonym kodem uruchomieniowym.

Rozdzia 3. Jzyk C w

C++

171

Pliki makefile uywane w ksice


Wszystkie pliki przykadowych programw zostay automatycznie utworzone, na podstawie wydrukw zawartych w tekstowej wersji ksiki, za pomoc programu ExtractCode.cpp (pochodzcego z jej drugiego tomu). Nastpnie zostay one umieszczone w podkatalogach o nazwach odpowiadajcych poszczeglnym rozdziaom. Ponadto program ExtractCode.cpp tworzy w kadym podkatalogu szereg plikw makeflle (posiadajcych rne nazwy), dziki ktrym mona przej do takiego podkatalogu i uy polecenia make -f mojkompilator.makef!le, zastpujc mojkompilator" nazw uywanego kompilatora (opcja ,,-f' okrela natomiast, e wymieniony po niej plik ma zosta uyty jako makeflle). Ostatecznie program ExtractCode.cpp tworzy nadrzdny" plik makeflle, znajdujcy si w gwnym katalogu, do ktrego zostay rozpakowane pliki zawierajce programy rdowe. Ten nadrzdny" plik makeflle powoduje przejcie do kadego podkatalogu i wywoanie w nim programu make z odpowiednim plikiem makeflle. W ten sposb mona skompilowa wszystkie programy zawarte w ksice, uywajc do tego ceIu pojedynczego polecenia make, a proces ten zostanie przerwany, gdy kompilator nie bdzie potrafi poradzi sobie z jakim plikiem (kompilator zgodny z C++ powinien by w stanie skompilowa wszystkie programy zawarte w ksice). Poniewa implementacje programu make rni si w poszczeglnych systemach, w plikach makeflle zostay wykorzystanejedynie najbardziej podstawowe, najczciej uywane polecenia.

Przyktadowy plik makefile


Jak ju wspomniano, program ExtractCode.cpp, tworzcy pliki programw na podstawie tekstu ksiki, automatycznie utworzy dla kadego rozdziau pliki makeflle. Dlatego te pliki te nie zostay zamieszczone w ksice (wszystkie pliki makeflle zostay spakowane razem z programami rdowymi i mona je pobra pod adresem: ftp://ftp.helion.pUprzykla.dy/thicpp.zip}- Poytecznie bdziejednak zobaczy przykad pliku makeflle. Zamieszczony poniej plik stanowi skrcon wersj pliku, ktry zosta automatycznie utworzony dla biecego rozdziau przez program ExtractCode.cpp. W kadym podkatalogu znajduje si wiksza liczba plikw makeflle (maj one rne nazwy i naley wywoa je za pomoc polecenia make -f'). Przedstawiony poniej plikjest przeznaczony dla GNU C++:

CPP = g++ OFLAG = -o .SUFFIXES : .o .cpp .c .cpp.o : $(CPP) S(CPPFLAGS) -c $< .C.o : $(CPP) S(CPPFLAGS) -c $<
all: \ Return \ Declare \ Ifthen \ Guess \ Guess2 # Nie pokazano pozostaych plikw, zawartych w rozdziale

Thinking in C++. Edycja polska

Return: Return,o $(CPP) S(OFLAG)Return Return.o Declare: Declare.o $(CPP) S(OFLAG)Declare Declare.o Ifthen: Ifthen.o S(CPP) t(OFLAG)Ifthen Ifthen.o Guess: Guess.o $(CPP) $(OFLAG)Guess Guess.o Guess2: Guess2.o $(CPP) $(OFLAG)Guess2 Guess2.o Return.o: Return.cpp Declare.o: Declare.cpp Ifthen.o: Ifthen.cpp Guess.o: Guess.cpp Guess2.o: Guess2.cpp MakrodeFinicja CPP okrela nazw kompilatora. Aby uy innego kompilatora, naley albo dokona modyfikacji w pliku makeflle, albo zmieni warto makrodefinicji w wierszu polece, jak w poniszym przykadzie:
make CPP=cpp

Warto jednak zwrci uwag na to, e program ExtractCode.cpp zawiera mechanizm umoliwiajcy automatyczne utworzenie plikw makeflle dla dodatkowych kompilatorw. Druga makrodefinicja, OFLAG, jest opcj uywan do okrelenia nazwy pliku wyjciowego. Mimo e wiele kompilatorw automatycznie 'przyjmuje, e nazwa pliku wyjciowego posiada t sam nazw (z wyjtkiem rozszerzenia), co plik wejciowy, to niektre kompilatory tego nie robi (na przykad kompilatory w systemach Linux i Unix, ktre domylnie tworzplik o nazwie a.out). W przedstawionym pliku mona rwnie zauway dwie reguy przyrostkowe jedn dla plikw cpp, a drug dla plikw .c (w przypadku koniecznoci kompilacji kodu rdowego jzyka C). Domylnym plikiem wynikowym jest all, a kady ze zwizanych z nim wierszy jest kontynuowany" za pomoc lewego ukonika, a do pliku Guess2, ostatniego na licie, i dlatego znajdujcego si w wierszu pozbawionym lewego ukonika. W rozdziale zawarto znacznie wicej plikw, ale z uwagi na zwizo w przykadzie wymieniono tylko kilka z nich. Reguy przyrostkowe dotycz tworzenia plikw wynikowych (z rozszerzeniem .o) na podstawie plikw cpp, ale na og trzeba jawnie okreli reguy tworzenia plikw wynikowych. W typowym przypadku pliki wykonywalne powstaj bowiem w rezultacie poczenia ze sobwielu rnych plikw wynikowych i program make niejest w stanie odgadn ich nazw. W powyszym przykadzie (w systemie Linux lub Unix) nie istnieje rwnie standardowe rozszerzenie nazw plikw wykonywalnych, wic reguy przyrostkowe nie bd w takim przypadku dziaa. Dlatego te w przykadowym pliku widoczne sjasno okrelone reguy tworzenia wszystkich plikw wykonywalnych.

Rozdzia 3. Jzyk C w

C++

173

Przedstawiony powyej plik makeflle wykorzystuje w najbezpieczniejszy sposb moliwie jak najmniejszy zbir polece programu make uywa jedynie jego podstawowych poj plikw wynikowych i zalenoci oraz makrodefinicji. Takie podejcie gwarantuje dziaanie z najwiksz moliw liczb wersji polecenia make. W wyniku powstaj co prawda na og wiksze pliki makefile, ale nie jest to problemem, poniewa sone automatycznie generowane przez program ExtractCode.cpp. Istnieje wiele dodatkowych polece programu make, ktre nie s uywane w ksice; s rwnie nowsze i przemylniejsze wersje i odmiany tego programu, zawierajce zaawansowane skrty, ktre pozwalaj na oszczdzenie mnstwa czasu. Dokumentacja doczona do uywanej przez ciebie wersji programu make moe zawiera opis dodatkowychjego cech. Wicej informacji na temat programu moesz znale w ksice Managing Projects with Make, napisanej przez Orama i Talbotta (O'Reilly, 1993). Jeeli natomiast producent stosowanego przez ciebie kompilatora w ogle nie dostarcza programu make, albo uywa jego niestandardowej wersji, moesz znale jego wersje GNU przeznaczone dla dowolnej istniejcej platformy, poszukujc w Internecie archiww programw GNU (ktrych jest wiele).

Podsumowanie
Rozdzia stanowi do bogaty przegld wszystkich podstawowych cech skadni jzyka C++, z ktrych wikszo wywodzi si z jzyka C i wystpuje w obu tych jzykach (w rezultacie jzyk C++ moe poszczyci si wsteczn zgodnoci z jzykiem C). Mimo e w przegldzie zaprezentowano rwnie pewne cechyjzyka C++, to zosta on przygotowany przede wszystkim z myl o czytelnikach, ktrzy majju pewne dowiadczenie w programowaniu i potrzebuj wprowadzenia obejmujcego podstawy skadni jzykw C i C++. Jeeli programujesz ju w C, to zapewne, oprcz cech jzyka C++, ktre s w wikszoci dla ciebie nowe, znalaze tu niewiele nieznanych ci informacji dotyczcych jzyka C. Jeeli jednak zawarto rozdziau wydaje ci si zbyt przytaczajca, to zapoznaj si z kursem Thinking in C: Foundations for C+ + andJava (skadajcym si z wykadw, wicze i wskazwek dotyczcych ich rozwiza), dostpnym w witrynie www.BruceEckel.com.

wiczenia
Rozwizania wybranych wicze mona znale w dokumencie elektronicznym The Thinking in C++ Annotated Solution Guide, dostpnym za niewielk opat z witryny http://www. BruceEckel. com. 1. Utwrz plik nagwkowy (z rozszerzeniem .h). W pliku tym zadeklaruj grup funkcji poprzez zmian list argumentw i zwracanych przez nie wartoci, wybierajc je spord typw: void, char, int oraz float. Nastpnie utwrz plik .cpp, do ktrego doczanyjest utworzony uprzednio plik nagwkowy, i utwrz w nim definicje wszystkich tych funkcji. Kada z definicji powinna

Thinking in C++. Edycja polska drukowa nazw funkcji, list argumentw oraz zwracan warto, dziki czemu wiadomo, e zostaa ona wywoana. Utwrz drugi plik .cpp, do ktrego doczany bdzie twj plik nagwkowy. Ponadto bdzie si w nim znajdowaa definicja funkcji int main(), zawierajca wywoania wszystkich utworzonych uprzednio funkcji. Skompiluj i uruchom ten program. 2. Napisz program, wykorzystujcy dwie zagniedone ptle for i operator moduu (%) do znalezienia i wydrukowania liczb pierwszych (liczb cakowitych podzielnych jedynie przez 1 i same siebie). 3. Napisz program, wykorzystujcy ptl whUe do wczytywania sw ze standardowego wejcia (cin) do acucha (string). Bdzie to nieskoczona" ptla while, ktr przerwiesz (i opucisz program) za pomoc instrukcji break. Przypisz kademu przeczytanemu sowu warto cakowit, uywajc do tego sekwencji instrukcji if, a nastpnie zastosuj instrukcj switch, wykorzystujc t warto w charakterze selektora (zaproponowana kolejno niejest przykadem dobrego stylu programowania ma ona umoliwi ci wiczenie zwizane z przepywem sterowania). W kadym przypadku case wydrukuje co sensownego. Musisz dokona wyboru ,jnteresujacych" sw i ich znaczenia, a take zdecydowa, jakie sowo bdzie poleceniem zakoczenia programu. Przetestuj program, przekierowujc najego standardowe wejcie plik jeeli chcesz oszczdzi sobie pisania, plikiem tym moe by plik rdowy programu). 4. Zmodyfikuj program Menu.cpp tak, by zamiast instrukcji if uywa on instrukcji switch. 5. Napisz program, ktry bdzie oblicza dwa wyraenia, zamieszczone w podrozdziale ,J>riorytety". 6. Zmodyfikuj program YourPets2.cpp w taki sposb, by wykorzystywa wiele rnych typw danych (char, int, float, double i ich wariantw). Uruchom program i stwrz map zawartoci jego pamici. Jeeli masz dostp do rnych komputerw, systemw operacyjnych lub kompilatorw, wykonaj eksperyment w takiej liczbie ich kombinacji, wjakiej jeste w stanie to zrobi. 7. Utwrz dwie funkcje, z ktrychjedna bdzie przyjmowaa argument typu string*, a druga typu string&. Kada z tych funkcji powinna modyfikowa zewntrzny obiekt string we waciwy sobie sposb. Utwrz i zainicjuj w funkcji main() obiekt typu string, wydrukuj jego warto, a nastpnie przeka go kolejno kadej z dwch funkcji, drukujc za kadym razem wynik ich dziaania. 8. Napisz program z zastosowaniem wszystkich trjznakw, aby przekona si, czy uywany przez ciebie kompilator je obsuguje. 9. Skompiluj i uruchom program Static.cpp. Usu z programu sowo kluczowe static, skompiluj i uruchom go ponownie, a nastpnie wyjanij to, co si stao. 10. Sprbuj skompilowa i dokona czenia plikw FileStatic.cpp i FileStatic2.cpp. Co oznacza zgoszony komunikat o bdzie? U.. Zmodyfikuj program Boolean.cpp tak, by zamiast z wartociami cakowitymi dziaa z wartociami typu double.

Rozdzia 3. * Jzyk C w

C++

175

12. Zmodyfikuj programy Boolean.cpp i Bitwise.cpp w taki sposb, by uyway operatorw dosownych jeeli twj kompilatorjest zgodny ze standardem C++, to bdzieje obsugiwa). Z3. Zmodyfikuj program Bitwise.cpp tak, by uywa funkcji zawartych w programie Rotation.cpp. Upewnij si, e wywietlaszjego wyniki w taki sposb, e wida, co dzieje si w czasie obrotw. 14. Zmodyfikuj program Ifthen.cpp w taki sposb, by uywa on operatora trjargumentowego if-else (?:). 15. Utwrz struktur przechowujcdwa obiekty typu stringijedn liczb cakowit. Utwrz egzemplarz tej struktury, zainicjuj wszystkie trzyjego wartoci, a nastpnieje wydrukuj. Pobierz adres obiektu i przypisz go wskanikowi do typu twojej struktury. Uywajc wskanika, zmie wszystkie trzy wartoci obiektu i wydrukuj je. 16. Napisz program, wykorzystujcy wyliczenie kolorw. Utwrz zmienntypu wyliczeniowego i wydrukuj liczby odpowiadajce nazwom poszczeglnych kolorw, uywajc do tego ptli for. 17. Poeksperymentuj z programem Union.cpp, usuwajc rne elementy unii i obserwujcJaki ma to wpyw najej wielko. Sprbuj dokona przypisania dojednego z elementw unii (posiadajcegojaki typ), a nastpnie wydrukuj warto innego elementu (ojakim innym typie), wchodzcego wjej skad, i zobacz, co si stanie. 18. Napisz program definiujcy dwie tablice liczb cakowitychJednobok drugiej. Dokonaj przypisania do elementu pierwszej tablicy, przekraczajc wartojej maksymalnego indeksu. Wydrukuj zawarto drugiej tablicy i zaobserwuj spowodowane przez to przypisanie zmiany. Nastpnie pomidzy tymi dwoma tablicami umieci definicj zmiennej typu char i powtrz eksperyment. Aby uatwi sobie kodowanie, moesz utworzy funkcj drukujc tablice. 19. Zmodyfikuj program ArrayAddresses.cpp w taki sposb, by dziaa z typami danych char, long int, float i double. 20. Zastosuj sposb pokazany w programie ArrayAddresses.cpp do wydrukowania wielkoci struktury i adresw elementw tablicy, zawartych w programie StructArray.cpp. 21. Utwrz tablic obiektw typu string i przypisz kademujej elementowi acuch. Wydrukuj zawarto tablicy, uywajc ptli for. 22. Na podstawie programu ArgsToInts.cpp napisz dwa nowe programy, uywajce odpowiednio funkcji atol() i atof(). 23. Zmodyfikuj program PointerIncrement2.cpp w taki sposb, by zamiast struktury uywa unii. 24. Zmodyfikuj program PointerArithmetic.cpp w taki sposb, by dziaa z typami long i long double.

Thinking in C++. Edycja polska

25. Zdefiniuj zmienntypu float. Pobierzjej adres, dokonaj jego rzutowania na typ unsigned char*, a nastpnie przypisz go wskanikowi do typu unsigned char. Za pomoc tego wskanika i nawiasw kwadratowych dokonaj indeksowania w obrbie zmiennej float (przesuwajc si od 0 do sizeof(float)), drukujc map" tej zmiennej przy uyciu opisanej w rozdziale funkcji printBinary(). Zmie warto zmiennej typu float i przekonaj si, czy potrafisz wyjani rezultat (zmienna float zawiera ,^aszyfrowane" dane). 26. Zdefiniuj tablic liczb cakowitych. Pobierz adres pocztku tej tablicy i uyj rzutowania static_cast w celu przeksztacenia go do typu void*. Napisz funkcj, ktra pobiera argument typu void*, liczb (oznaczajc liczb bajtw) i warto (okrelajc warto, ktra powinna zosta przypisana kademu bajtowi). Funkcja powinna przypisa kademu bajtowi okrelon warto z podanego zakresu. Wyprbuj dziaanie funkcji na zdefiniowanej uprzednio tablicy liczb cakowitych. 27. Utwrz sta(const) tablic liczb typu double i tablic liczb typu double, opatrzonmodyfikatorem volatile. Przejd przez kadtablic, uywajc rzutowa const_cast w celu przeksztacenia kadego z ich elementw na typy pozbawione modyfikatorw const i volatile oraz nadajc tym elementom wartoci. 28. Napisz funkcj, pobierajc wskanik do tablicy liczb typu double, oraz warto, okrelajc wielko tablicy. Funkcja ta powinna wydrukowa wszystkie elementy tablicy. Utwrz tablic liczb typu double, zainicjuj kady jej element wartocirwnzero, a potem uyj swojej funkcji do wydrukowania zawartoci tablicy. Nastpnie uyj rzutowania reinterpret_cast do przeksztacenia adresu tablicy do typu unsigned char* i przypisz kademu bajtowi takiej tablicy warto 1 (wskazwka: jest potrzebny operator sizeof do okrelenia liczby bajtw skadajcych si na typ double). Nastpnie uyj napisanej wczeniej funkcji do wydrukowania zawartoci tablicy. Jak mylisz, dlaczego jej elementy nie maj wartoci 1.0? 29. (Trudne) Zmodyfikuj program FloatingAsBinary.cpp w taki sposb, by drukowa on kadcz liczby typu double w postaci oddzielnej grupy bitw. Aby to zrobi, naley zastpi wywoanie funkcji printBinary() wasnym, napisanym specjalnie w tym celu, kodem (ktry moesz opracowa na podstawie funkcji printBinary()). Trzeba rwnie odszuka i zrozumie informacje dotyczce formatu zmiennopozycyjnego i kolejnoci bajtw, wykorzystywanych przez posiadany przez ciebie kompilator (to jest wanie trudna cz tego wiczenia). 30. Utwrz plik makefile, ktry nie tylko skompiluje pliki YourPetsl.cpp i YourPets2.cpp (uywajc posiadanego przez ciebie kompilatora), ale rwnie, w ramach dziaania zwizanego z domylnymi plikami wynikowymi, uruchomi oba te programy. Upewnij si, e zostay uyte reguy przyrostkowe. 3- Zmodyfikuj program StringizingExpressions.cpp w taki sposb, aby P(A) bya zdefiniowana warunkowo, co pozwoli na automatyczne usunicie kodu uruchomieniowego za pomoc znacznika, podanego w wierszu polece

Rozdzia 3. Jzyk C w

C++

177

kompilatora. Naley sprawdzi w dokumentacji uywanego przez ciebie kompilatora, wjaki sposb definiuje si i uniewania wartoci preprocesora w wierszu polece kompilatora. 32. Zdefiniuj funkcj przyjmujcargument typu double i zwracajc warto typu int. Utwrz i zainicjuj wskanik do tej funkcji, a nastpnie wywoaj j, uywajc wskanika. 33. Zadeklaruj wskanik do funkcji przyjmujcej argument typu int i zwracajcej wskanik do funkcji, ktra przyjmuje argument typu char i zwraca warto typu float. 34. Zmodyfikuj program FunctionTable.cpp w taki sposb, by kada funkcja zwracaa acuch (zamiast drukowa komunikat), ktrego warto zostanie wydrukowana wewntrz funkcji main(). 35. Utwrz p!ik makefile dlajednego z poprzednich wicze (dowolnie wybranego), ktry umoliwia wpisanie polecenia make, tworzcego gotowy program wykonywalny, oraz polecenia make debug, tworzcego program zawierajcy informacje uruchomieniowe.

L78

Thinking in C++. Edycja polska

Abstrakcja danych
Jzyk C++ jest narzdziem zwikszajcym wydajno. Z jakiego innego powodu warto by podejmowa wysiek ^est to niewtpliwie trud, niezalenie od tego, jak atwym prbujemy uczyni przejcie) zmianyjzyka, ktryju znasz i z powodzeniem wykorzystujesz, na taki, za ktrego pomoc osigniesz przez pewien czas mniejsz wydajno, dopki biegle go nie opanujesz? Odpowied jest oczywista: wykorzystujc nowe narzdzie, osigniesz due korzyci. W terminologii stosowanej w programowaniu komputerw wydajno oznacza, e mniej liczna grupa osb moe tworzy wiksze i znacznie bardziej skomplikowane programy w krtszym czasie. Kiedy naley dokona wyboru jzyka programowania, wane sz pewnocirwnie inne kwestie, takiejak efektywno (czy naturajzyka nie spowoduje spowolnienia programu i rozdcie jego kodu?), bezpieczestwo (czy jzyk zapewni, e program bdzie zawsze wykonywa to, co zamierzono, i elegancko obsuy bdy?) oraz pielgnacj (czy dziki wykorzystaniu jzyka powstanie kod atwy do zrozumienia, modyfikacji i rozszerzania?). Te z pewnoci istotne czynniki zostan przeanalizowane w niniejszej ksice. Zwyka wydajno oznacza, e program, ktrego napisanie zajmowao poprzednio trzem osobom tydzie, zabiera teraz jednej osobie kilka dni. Zagadnienie to dotyka wielu aspektw ekonomii. Odczuwasz satysfakcj, poniewa dokonae jakiego dziea, twj klient (lub szef) jest zadowolony, gdy produkty s tworzone szybciej, przez mniejsz liczb Iudzi, a klient rwnie ma powody do radoci, bowiem nabywa produkt taniej. Jedynym sposobem uzyskania duego wzrostu wydajnoci jest wykorzystanie kodu napisanego przez innych. Oznacza to konieczno uywania bibliotek. Bibliotek tworz fragmenty kodu, napisane przez kogo i poczone w cao. Najmniejsze pakiety skadaj si czsto z pIiku o rozszerzeniu takim jak Ub oraz jednego lub wikszej liczby plikw nagwkowych, informujcych kompilator o zawartoci biblioteki. Program czcy wie, w jaki sposb przeszukiwa plik biblioteki i wydoby z niej odpowiedni, skompilowany kod. Jest to jednak tylko jedna z metod dostarczania bibliotek. Na platformach obejmujcych wiele rnorodnych architektur, takich jak Linux czy Unix, czsto jedynym rozsdnym sposobem dostarczania biblioteki jest doczenie do niej kodu rdowego, co pozwala na jej przekonfigurowanie i powtrn kompilacj w nowym rodowisku.

Rozdzia 4.

180

Thinking in C++. Edycja polska

A zatem biblioteki s prawdopodobnie najwaniejszym sposobem zwikszenia wydajnoci, a jednym z gwnych celw projektowych jzyka C++ byo uatwienie korzystania z bibliotek. Wynika z tego, e istnieje przeszkoda utrudniajca wykorzystywanie bibliotek w jzyku C. Zrozumienie tego umoliwi ci pojcie, jak zosta zaprojektowany jzyk C++, a zatem rwniejak naley go uywa.

Miniaturowa biblioteka w stylu C


Biblioteki powstaj na og jako zbiory funkcji, ale jeeli uywasz niezalenych bibliotek jzyka C, to wiesz, e proces ten jest zazwyczaj bardziej skomplikowany, poniewa ycie nie skada si tylko z zachowa, dziaa i funkcji. Srwnie waciwoci (niebieski, kilogramy, faktura,jasnosc), reprezentowane za pomocdanych. Kiedy zaczynasz zajmowa si w jzyku C zbiorem waciwoci, wygodne jest poczenie ich w struktur, szczeglnie gdy zamierzasz opisywa za ich pomoc wicej nijeden element przestrzeni problemu. Dziki temu moesz nastpnie utworzy zmienn bdcegzemplarzem struktury, oddzielnie dla kadego elementu. Tak wic wikszo bibliotekjzyka C zawiera zbir struktur oraz zbir dziaajcych na nich funkcji. Jako przykad takiego systemu rozwaymy narzdzie programistyczne dziaajce jak tablica, ale ktrego wielko moe by okrelona w czasie pracy programu, kiedy jest ono tworzone. Zostanie mu nadana nazwa CStash. Mimo e zostao ono napisane w C++, przypomina napisane wjzyku C. // Plik nagwkowy biblioteki w stylu C // Tablicopodobny twr. tworzony w czasie // pracy programu typedef struct CStashTag { int size; // Wielko kadego elementu int quantity; // Liczba elementw pamici int next; // Nastpny pusty element // Dynamicznie przydzielana tablica bajtw: unsigned char* storage: } CStash; void initialize(CStash* s. int size); void cleanup(CStash* s); int add(CStash* s, const void* element); void* fetch(CStasn* s. int index); int count(CStash* s};
/ / : C04:CLib.h

void inflate(CStash* s, int increase);

Takie identyfikatory, jak CstashTag, s na og uywane w strukturach, w ktrych istnieje konieczno odwoania si do struktury z jej wntrza. Na przykad podczas tworzenia listy powizanej (kady element takiej listy zawiera wskanik do nastpnego elementu) niezbdny jest wskanik do nastpnej zmiennej bdcej struktur, dlatego te potrzebny jest sposb okrelenia typu takiego wskanika wewntrz ciaa struktury. Rwnie stosowanie deklaracji typedef w stosunku do struktur (przedstawione powyej) jest niemal powszechne w bibliotekach jzyka C. Dziki temu mona traktowa struktury w taki sposob,jakby byy nowymi typami, i definiowa zmienne ich typw:

Rozdzia 4. Abstrakcja danych


CStash A. B, C;

181

Wskanik do pamici jest typu unsigned char*. Najmniejszym fragmentem pamici, obsugiwanym przez kompilatorjzyka C, jest typ unsigned char, chocia w przypadku niektrych komputerw moe on by tej samej wielkoci, co najwikszy fragment. Zaley to od konkretnej implementacji, ale czsto ma on wielko jednego bajtu. Moe si wydawa, e skoro CStash jest projektowany w taki sposb, by przechowywa zmienne dowolnego typu, to bardziej odpowiedni byby typ void*. Jednake celem nie jest w tym przypadku traktowanie pamici jako bloku jakiego nieokrelonego typu, leczjako bloku kolejnych bajtw. Kod rdowy pliku zawierajcego implementacj (ktrego moesz nie uzyska w przypadku, gdy nabywasz komercyjn bibliotek zapewne otrzymasz jedynie skompilowane pliki o rozszerzeniach obj, lib, dll itp.) wyglda nastpujco: //: C04:CLib.cpp {0} // Implementacja przykadowej biblioteki w stylu C // Deklaracje struktury i funkcji: #include "CLib.h" #include <iostream> #include <cassert> using namespace std: // Liczba elementw dodawanych // w przypadku powikszenia pamici: const int increment = 100: void initialize(CStash* s, int sz) { s->size = sz; s->quantity = 0: s->storage = 0: s->next = 0: int add(CStash* s, const void* element) { if(s->next >= s->quantity) //Czy wystarczy pamici? inflate(s. increment); // Kopiowanie elementu do pamici. // poczwszy od nastpnego wolnego miejsca: int startBytes = s->next * s->size; unsigned char* e = (unsigned char*)e1ement; for(int i " 0; i < s->size; i++) s->storage[startBytes + i] = e[i]; s->next++: return(s->next - 1); // Numer indeksu void* fetch(CStash* s. int index) { // Kontrola zakresu indeksu assert(0 <= index); if(index >= s->next) return 0; // Oznaczenie koca // Tworzenie wskanika do danego elementu: return &(s->storage[index * s->size]);

Thinking in C++. Edycja polska int count(CStash* s) { return s->next: // Liczba elementw w CStash void inflate(CStash* s. int increase) { assert(increase > 0); int newQuantity = s->quantity + increase; int newBytes newQuantity * s->size; int oldBytes - s->quantity * s->size; unsigned char* b = new unsigned char[newBytes]; for(int i = 0; i < oldBytes; i++) b[i] = s->storage[i]; // Kopiowanie starego obszaru do nowego delete [](s->storage); // Stary obszar pamici s->storage - b; // Wskanik do nowego obszaru s->quantity = newQuantity; void cleanup(CStash* s) { if(s->storage !- 0) { cout "zwalnianie pamici" endl; delete []s->storage;

Funkcja initialize( ) dokonuje niezbdnej konfiguracji struktury CStash, przypisujc jej wewntrznym zmiennym odpowiednie wartoci. Pierwotnie wskanik storage jest ustawiany na zero, poniewa nie zostaajeszcze przydzielona adna pami. Funkcja add( ) wstawia do CStash element, umieszczajc go na nastpnej wolnej pozycji. Najpierw sprawdza, czyjestjeszcze dostpna pami. Jeeli nie, powiksza obszar pamici, uywajc do tego funkcji inflate( ), ktra zostanie opisana pniej. Poniewa kompilator nie zna dokadnego typu przechowywanych zmiennych (wszystkie funkcje przyjmuj wskaniki typu void*), nie mona zwyczajnie dokona przypisania, co z pewnoci byoby najwygodniejsze. Trzeba natomiast kopiowa zmienn, bajt po bajcie. Najprostsz metod dokonania tego kopiowania jest wykorzystanie indeksowania tablicy. Zazwyczaj w obszarze pamici wskazywanym przez storage znajduj si ju jakie dane, o czym wiadczy warto zmiennej next. Aby rozpocz od przesunicia o waciw liczb bajtw, zmienna next jest mnoona przez wielko kadego elementu (wyraon w bajtach) i daje w wyniku warto startBytes. Nastpnie argument element jest rzutowany na typ unsigned char*, dziki czemu bdzie si mona do niego odwoywa bajt po bajcie, kopiujc go do wolnego obszaru pamici, wskazywanej przez zmienn storage. Zwikszana jest zmienna next, co sprawia, e okrela ona nastpny wolny fragment pamici, a take zwracany jest numer indeksu", pod ktrym zostaa zapisana warto. Pozwoli to na jej pniejsze odczytanie, po podaniu tego indeksu funkcji fetch(). Funkcja fetch( ) sprawdza, czy podany indeks nie wykracza poza dopuszczalne granice, a nastpnie zwraca adres danej zmiennej, wyznaczajc go na podstawie argumentu index. Poniewa argument index okrela liczb elementw, o jak naley przesun si w obrbie CStash, musi by on pomnoony przez liczb bajtw zajmowan przez kady element. W wyniku uzyskuje si liczb bdc wielkoci przesunicia.

Rozdzia 4. Abstrakcja danych

183

wyraon w bajtach. Kiedy przesunicie to jest uywane do tablicowego indeksowania wskanika storage, jego wynikiem nie jest adres, tylko bajt, znajdujcy si pod tym adresem. W celu uzyskania adresu naley zatem uy operatora adresu &. Funkcja count( ) moe si wyda dowiadczonemu programicie nieco osobliwa. Wyglda na to, e zadano sobie wiele trudu po to, by wykona co, co prawdopodobnie mona by zrobi znacznie atwiej ,jecznie". Jeli na przykad dysponuje si struktur typu CStash, o nazwie intStash, znacznie prostsze wydaje si okrelenie zawartej w niej liczby elementw za pomoc odwoania intStash.next, zamiast wywoania funkcji (ktre wie si z narzutem) w postaci count(&intStash). Jeeli jednak zamierza si zmieni wewntrzn reprezentacj struktury CStash, a zatem rwnie sposb, w jaki wyznaczana jest liczba zawartych w niej elementw, to interfejs w postaci wywoania funkcji zapewnia jej niezbdn elastyczno. Niestety, wikszo programistw nie wemie pod uwag informacji o wyszoci takiego podejcia do projektu biblioteki. Po przyjrzeniu si strukturze bd bezporednio odczytywa warto zmiennej next, a by moe nawet zmienia j bez twojego pozwolenia. Gdyby tylko istnia jaki sposb, umoliwiajcy projektantowi biblioteki sprawowanie nad tym wikszej kontroli! Jak si pniej przekonamy, sposb taki istnieje.

Dynamiczny przydzia pamici


Nigdy nie wiadomo, ile pamici bdzie potrzebowaa zmienna typu CStash, wic pami wskazywana przez zmienn storage jest przydzielana ze sterty (ang. heap). Stertajest duym blokiem pamici, z ktrego przydzielane s w czasie pracy programu mniejsze fragmenty. Sterty uywa si w przypadku, gdy w czasie pisania programu nie jest znana wielko potrzebnej pamici. Na przykad tylko w czasie pracy programu mona okreli, e potrzebna jest pami przechowujca 200, a nie 20 zmiennych Samolot. W standardzie C funkcjami sucymi do dynamicznego przydziau pamici byy: malloc(), calIoc(), realloc() i free(). W jzyku C++ zastpiono wywoania funkcji bibliotecznych bardziej wyrafinowanym (cho prostszym w uyciu) rozwizaniem dotyczcym obsugi pamici dynamicznej, wczonym do jzyka w postaci sw kluczowych new i delete. Funkcja inflate( ) uywa sowa kluczowego new do przydzielenia typowi CStash wikszego fragmentu pamici. W takiej sytuacji zwikszamy jedynie przydzielon pami, nigdy jej nie zmniejszajc, a funkcja assert() zapewnia, e funkcji inflate( ) nigdy nie zostanie przekazana ujemna warto parametru increase. Liczba elementw, ktre bd mogy by przechowywane w pamici (po zakoczeniu funkcji inflate()), jest wyznaczana jako newQuantity, a nastpnie mnoona przez liczb bajtw przypadajcych na element. W rezultacie daje ona warto newBytes, bdc liczb przydzielanych bajtw. Aby byo wiadomo, iIe bajtw naley skopiowa z poprzednio przydzielonego obszaru pamici, na podstawie poprzedniej wartoci zmiennej quantity wyznaczana jest warto oldBytes. Faktyczny przydzia pamici zachodzi w wyraeniu new, ktre zawiera sowo kluczowe new:

new unsigned char[newBytes];

Thinking in C++. Edycja polska

Oglna posta wyraenia newjest nastpujca:


new typ;

gdzie typ opisuje typ zmiennej, ktra ma zosta przydzielona na stercie. W naszym przypadku potrzebna jest tablica elementw typu unsigned char, o wielkoci newBytes, i dlatego tworz one w przedstawionym programie typ. Mona rwnie dokona przydziau czego tak prostegojak liczba cakowita, zapisujc:
new int;

i chocia przypadek taki zdarza si rzadko, to jest oczywiste, e posta wyraenia jest spjna. Wyraenie new zwraca wskanik do obiektu dokadnie takiego typu, o jakiego przydzielenie zosta poproszony. A zatem, piszc new typ, uzyskuje si w wyniku wskanik do typu. Jeeli zapiszemy new int, otrzymamy wskanik do liczby cakowitej. Jeli potrzebna jest tablica znakw, uzyskany wskanik bdzie wskazywa pierwszy element tej tablicy. Kompilator zagwarantuje, e warto zwrcona przez wyraenie new zostanie przypisana wskanikowi odpowiedniego typu. Oczywicie, zawsze moe si zdarzy, e jeli nie bdzie ju wolnej pamici, danie jej przydziau zakoczy si niepowodzeniem. Jak si przekonasz, jzyk C++ posiada mechanizmy, uruchamiane w sytuacji, gdy operacja przydziau pamici zakoczy si niepowodzeniem. Po przydzieleniu nowego obszaru pamici musz zosta skopiowane do niego dane znajdujce si w starym obszarze pamici. Jest to dokonywane za pomoc indeksowania tablicowego w ptli, bajt po bajcie. Po skopiowaniu danych stary obszar danych musi zosta zwolniony, dziki czemu mona go wykorzysta w innych partiach programu, kiedy bdone potrzeboway przydzielenia pamici. Sowo kluczowe delete jest przeciwiestwem sowa new i musi by ono uyte do zwolnienia kadego obszaru pamici, przydzielonego za pomoc new jeeli zapomnisz o uyciu operatora delete, pami ta pozostanie niedostpna; jeli zdarzy si wicej tzw. wyciekw pamici, w kocujej zabraknie). Ponadto w przypadku usuwania tablic stosowanajest specjalna skadnia. Ma ona tak posta, jakby trzeba byo przypomnie kompilatorowi, e wskanik nie wskazuje pojedynczego obiektu, tylko tablic przed usuwanym wskanikiem umieszcza si pusty nawias kwadratowy:
delete []mojaTablica;

Po usuniciu starego obszaru pamici wskanikowi storage mona przypisa adres nowego obszaru; aktualizowana jest informacja o jego wielkoci i funkcja inflate( ) koczy swprac. Zwr uwag na to, e meneder sterty jest do prosty. Udostpnia on fragmenty pamici, a nastpnie zabiera je z powrotem, po uyciu operatora delete. Nie wbudowano w niego adnych mechanizmw umoliwiajcych upakowanie sterty, ktre dokonaaby jej kompresji, dziki czemu na stercie byyby dostpne wiksze fragmenty pamici. Jeeli program przydziela i zwalnia przez pewien czas pami na stercie, moe to doprowadzi dojejfragmentacji. Polega ona na tym, e na stercie znajduje si jeszcze duo wolnej pamici, ale aden jej fragment nie jest dostatecznie duy, by

Rozdzia 4. * Abstrakcja danych

185

mona przydzieli dan w danej chwili wielko. Program dokonujcy upakowania sterty sprawia, e program si komplikuje, poniewa przesuwa on przydzielone obszary pamici, co powoduje, e wartoci wskanikw przestaj by aktualne. Niektre rodowiska operacyjne zawieraj wbudowane mechanizmy pakowania sterty, ale wymagaj uycia, zamiast wskanikw, specjalnych uchwytw pamici (ktre mog by czasowo przeksztacane we wskaniki, po zablokowaniu ich w pamici w taki sposb, aby program pakujcy stert nie mg ich przesun). Mona rwnie utworzy wasny system pakujcy stert, ale niejest to atwe zadanie. Jeeli jaka zmienna tworzona jest w czasie kompilacji na stosie, to pami dla niej jest automatycznie przydzielana i zwalniana przez kompilator. Kompilator wie dokadnie, ile pamici potrzeba, a take dziki zasigowi zna czas ycia zmiennej. Jednak w przypadku dynamicznego przydziau pamici, kompilator nie zna wielkoci wymaganej pamici ani okresu, w ktrym bdzie ona potrzebna. Oznacza, to, e pami nie jest automatycznie zwalniana. A zatem naley j zwolni za pomoc operatora delete, informujcego menedera sterty, e pami ta moe zosta wykorzystana podczas nastpnego uycia operatora new. Logiczne jest, by w bibliotece zwalnianie pamici odbywao si w obrbie funkcji cleanup() (ang. clecmup sprztanie), poniewa dokonywane s w niej wszystkie kocowe operacje porzdkowe. W celu przetestowania biblioteki tworzone s dwie zmienne typu CStash. Pierwsza z nich suy do przechowywania liczb cakowitych, a druga tablic 80-znakowych:
/ / : C04:CLibTeSt.Cpp / / { L } CLib // Test biblioteki w stylu C #include "CLib.h" #include <fstream> #include <iostream> #include <string> #include <cassert> using namespace std;

int main() { // Definicje zmiennych znajduj si // na pocztku bloku, tak jak w jzyku C: CStash intStash. stringStash; int i; char* cp; ifstream in; string line; const int bufsize = 80; // Trzeba pamita o inicjalizacji zmiennych: initialize(&intStash, sizeof(int)); for(i - 0; i < 100; i++) add(&intStash, &i); for(i = 0; i < count(&intStash); i++) cout "fetch(&intStash. " i ") - " *Ont*)fetch(&intStash. i) endl; // Przechowywanie 80-znakowych acuchw: initialize(&stringStash, sizeof(char)*bufsize); in.open("CLibTest.cpp"); assert(in);

186

Thinking in C++. Edycja polska

whileCgetline(in. line))

add(&stringStash. line.c_strO); i - 0; wMle((cp = (char*)fetch(&str1ngStash.i++))!=0) cout "fetch(&str1ngStash, " i ") = " cp endl; cleanup(&intStash): cleanup(&sthngStash); Zgodnie z wymaganiem jzyka C, wszystkie zmienne s tworzone na pocztku zasigu funkcji main(). Oczywicie, naley pamita o pniejszej inicjalizacji zmiennych typu CStash za pomoc wywoania funkcji initiaUze(). Jednym z problemw zwizanych z bibliotekami jzyka C jest to, e trzeba przekaza jej uytkownikom informacje o tym, jak wane s funkcje inicjalizujce i porzdkujce. Jeeli nie zostan one wywoane, spowoduje to mnstwo kopotw. Niestety, uytkownicy nie zawsze zastanawiaj si nad tym, czy inicjalizacja i sprztanie s konieczne. Wiedz, co sami chc osign, i nie przejmuj si zbytnio poradami w rodzaju: ,JPoczekaj, musisz najpierw koniecznie zrobi to!". Niektrzy uytkownicy s nawet znani z tego, e inicjalizuj elementy struktury na wasn rk. Z pewnoci nie ma w jzyku C mechanizmu, ktry by temu zapobiega. Jak si pniej przekonamy, mechanizm taki istnieje jednak w C++. Zmienna iniStash jest wypeniana liczbami cakowitymi, a stringStash tablicami znakowymi. Tablice znakowe powstaj w wyniku otwarcia pliku rdowego CLibTest.cpp i wczytania znajdujcych si w nim wierszy do zmiennej acuchowej line, a nastpnie utworzenia za pomoc funkcji skadowej c_str( ) wskanika do znakowej reprezentacji tej zmiennej. Po wypenieniu iniStash i stringStash wywietlana jest ich zawarto. Zawarto intStash jest drukowana za pomoc ptli for, wykorzystujcej funkcj count() do okrelenia zakresu licznika ptli. Natomiast do wydruku stringStash suy instrukcja while, przerywana wtedy, gdy funkcja fetch() zwraca warto zero, oznaczajc przekroczenie zakresu indeksu elementu. Warto rwnie zwrci uwag na dodatkowe rzutowanie w instrukcji:

cp - (char*)fetch(&stringStash,i++)
Zastosowano je z powodu dokadniejszej kontroli typw w jzyku C++, niepozwalajcej na przypisanie wartoci typu void* zmiennej adnego innego typu (w jzyku C jest to dozwolone).

B^dne zatoenia
Jest jeszcze jedna, bardziej istotna kwestia, o ktrej naley wspomnie, zanim omwimy oglne problemy zwizane z tworzeniem bibliotek jzyka C. Zwr uwag na to, e plik nagwkowy CLib.h musi by doczony do kadego pliku, ktry odwouje si do pliku CStash, poniewa kompilator niejest w stanie dowiedzie si, jakajest posta struktury. Jednake kompilator moe domyli si, jak wygldaj funkcje wyglda to na zalet, ale okazuje si byjednzgwnych puapekjzyka C.

Rozdzia 4. Abstrakcja danych

187

Mimo e naley zawsze deklarowa funkcje, doczajc odpowiedni plik nagwkowy, deklaracje funkcji nie s w jzyku C konieczne. W jzyku C (ale nie w C++) mona wywoa funkcj, ktra nie zostaa zadeklarowana. Dobry kompilator powinien zgosi ostrzeenie, informujce o tym, e funkcj naley najpierw zadeklarowa, ale nie wynika to z wymaga standardu jzyka C. Jest to niebezpieczna praktyka, poniewa kompilator C moe zaoy, e lista argumentw funkcji, wywoanej z argumentem bdcym liczb cakowit, zawiera liczb cakowit, nawet jeeli w rzeczywistoci znajduje si tam liczba zmiennopozycyjna. Jak si przekonasz, wywouje to niekiedy bardzo trudne do wykrycia bdy. Wjzyku C kady plik zawierajcy implementacj (czyli o rozszerzeniu .c) nazywany jestjednostk translacji. Oznacza to, e kompilatorjest uruchamiany oddzielnie dla kadej jednostki translacji i w czasie pracy zwraca uwag jedynie na biecjednostk. Tak wic informacja dostarczona przez doczenie pliku nagwkowego jest bardzo istotna, poniewa zaley od niej waciwe rozumienie przez kompilator reszty programu. Szczeglnie wane s deklaracje zawarte w pliku nagwkowym, poniewa gdy doczony zostanie plik nagwkowy, kompilator bdzie wiedzia dokadnie, jakie dziaania ma wykonywa. Na przykad gdy plik nagwkowy bdzie zawiera deklaracj void func(float), wwczas kompilator bdzie wiedzia, e jeeli funkcja ta zostanie wywoana zargumentem typu cakowitego, to powinien w czasie przekazywania argumentu przeksztaci typ int we float ftest to nazywane promocj). Bez tej deklaracji, kompilatorjzyka C zaoyby po prostu, e istnieje funkcja func(int), nie dokonaby promocji, a funkcji func() zostayby po cichu" przekazane niewaciwe dane. Kompilator tworzy dla kadej jednostki translacji odrbny plik wynikowy, posiadajcy rozszerzenie .o, .obj lub podobne. Pliki wynikowe wraz z kodem, niezbdnym do uruchomienia programu, musz zosta poczone w wykonywalny program za pomoc programu czcego. W trakcie czenia naley okreli wszystkie zewntrzne odwoania. Na przykad w pliku CLibTest.cpp s zadeklarowane (czyli kompilator wie, jak maj posta) i uywane takie funkcje, jak initialize() i fetch(), ale nie zostay one w tym pliku zdefiniowane. S natomiast zdefiniowane gdzie indziej w pliku CLib.cpp. A zatem odwoania do funkcji znajdujcych si w pliku CLib.cpp s odwoaniami zewntrznymi. czc z sob pliki wynikowe, program czcy musi okreli aktualne adresy wszystkich nierozstrzygnitych odwoa zewntrznych. Adresy te s umieszczane w programie wykonywalnym, zastpujc zewntrzne odwoania. Warto podkreli, e w jzyku C zewntrzne odwoania, ktrych poszukuje program czcy, spo prostu nazwami funkcji, poprzedzonymi na og znakiem podkrelenia. Tak wic program czcyjedynie dopasowuje nazw funkcji wystpujcej w miejscu wywoana do jej ciaa, znajdujcego si w pliku wynikowym. Jeeli przypadkowo wywoa si funkcj, ktra zostanie zinterpretowana przez kompilator jako funkcja func(int), a wjakim innym pliku wynikowym znajdzie si ciao funkcji func(float), to program czcy wykryje w obu tych miejscach nazw _func i uzna, e wszystko jest w porzdku. W miejscu wywoania func( ) umieci na stosie warto typu int, a ciao funkcji func( ) bdzie oczekiwao, e na stosie znajduje si warto typu float. Jeeli funkcja czytajedynie t warto, nie zmieniajcjej, nie spowoduje to zniszczenia stosu. W istocie warto typu float, odczytana ze stosu, moe si nawet wydawa poprawna. Jednake sytuacja jest wwczas jeszcze gorsza, poniewa bd taki znacznie trudniej znale.

.88

Thinking in C++. Edycja polska

Na czym polega problem?


Potrafimy si przystosowa nawet do sytuacji, do ktrych by moe nie powinnimy. Styl, w jakim napisano bibliotek CStash, by chlebem powszednim dla programujcych w jzyku C, ale jeeli przyjrze si mu bliej, moe si okaza, e jest on... niewygodny. Uywajc biblioteki CStash, trzeba przekazywa adres struktury kadej funkcji wchodzcej w skad biblioteki. Podczas czytania programu mechanizmy obsugujce bibliotek myl si z wywoywanymi funkcjami, co jest irytujce w przypadku, gdy prbuje si zrozumie, jak to wszystko dziaa. Jednak jedn z najwikszych trudnoci, wicych si z uywaniem w jzyku C bibliotek, jest problem kolizji nazw. Jzyk C posiada tylko jedn przestrze nazw funkcji, tj. kiedy program czcy poszukuje nazwy funkcji, posuguje si jedn gwn list. Ponadto podczas przetwarzania jednostki translacji kompilator moe pracowa tylko zjednfunkcjo podanej nazwie. Zamy, e zamierzasz naby dwie biblioteki, dostarczane przez dwie rne firmy; kada z nich zawiera struktur, ktr trzeba zainicjowa i po ktrej trzeba posprzta. Obie firmy uznay, e odpowiednimi nazwami dla tych czynnoci s initialize() i cleanup( ). Co zrobi kompilator jzyka C, jeeli doczysz pliki nagwkowe obu tych bibliotek do pojedynczej jednostki translacji? Na szczcie, kompilator zgosi bd, informujc o tym, e wystpuje niezgodno typw dwch rnych list argumentw zadeklarowanych funkcji. Nawetjeli oba pliki nagwkowe nie zostan wczone do tej samej jednostki translacji, kopoty bdzie mia nadal program czcy. Dobry program czcy zauway, e w takim przypadku wystpuje konflikt nazw. Jednake niektre programy czce przyjm pierwsz napotkan nazw funkcji, przeszukujc listy plikw wynikowych w kolejnoci podanej na licie czonych moduw (dziaanie takie moe by nawet traktowane jako zaleta z uwagi na moliwo zastpienia funkcji bibliotecznej przygotowan samodzielnie wersj). W adnym z tych przypadkw nie sposb uywa dwch bibliotek jzyka C, zawierajcych funkcje o takich samych nazwach. Aby rozwiza ten problem, producenci bibliotek czsto poprzedzaj wszystkie nazwy funkcji unikatowymi cigami znakw. W taki sposb funkcje initialize() i cleanup( ) mog sta si funkcjami CStashinitialize( ) oraz CStash_cleanup( ). Jest to dziaanie logiczne, poniewa uzupenia nazw struktury, na ktrej dziaa funkcja, nazwtej funkcji. Pora na pierwszy krok w kierunku tworzenia klas w jzyku C++. Nazwy zmiennych zawartych w strukturach nie koliduj z nazwami zmiennych globalnych. Dlaczego wic nie wykorzysta tego w przypadku nazw funkcji, ktre operuj na tych wanie strukturach? Innymi sowy, dlaczego nie uczyni funkcji skadow struktury?

Podstawowy obiekt
To wanie pierwszy krok. Funkcje w jzyku C++ mog by umieszczone wewntrz strukturjako funkcjeskadowe". Poniej pokazano,jak wyglda to w przypadku konwersji zrealizowanej w jzyku C wersji struktury CStash na napisan w C++ struktur Stash:

Rozdzia 4. * Abstrakcja danych

189

//: C04:CppLib.h // Biblioteka w stylu C. przeniesiona do C++ struct Stash { int size; // Wielko kadego elementu int quantity; // Liczba elementw pamici int next; // Nastpny pusty element // Dynamicznie przydzielana tablica bajtw: unsigned char* storage; // Funkcje! void initialize(int size); void cleanup(); int add(const void* element); void* fetch(int index); int count(); void inflate(int increase); Po pierwsze, naley zwrci uwag na to, e nie ma tu deklaracji typedef. Kompilator C++, zamiast wymaga stosowania deklaracji typedef, przeksztaca na potrzeby programu nazw struktury w nazw nowego typu (tak jak nazwami typw s int, char, float i double). Wszystkie dane skadowe struktury pozostay takie same, jak poprzednio, aIe w jej ciele pojawiy si dodatkowo funkcje. Ponadto naley zwrci uwag na to, e z funkcji zawartych w bibliotece, napisanej wjzyku C, usunito pierwszy argument. Wjzyku C++ nie wymaga si od programisty przekazywania adresu struktury wszystkim operujcym na niej funkcjom, lecz wykonuje to za niego po kryjomu kompilator. Obecnie jedyne argumenty przekazywane funkcjom zwizane s z tym, co te funkcje robi, a nie z mechanizmem ich dziaania. Trzeba zdawa sobie spraw z tego, e kod funkcji jest w rzeczywistoci taki sam, jak w przypadku wersji biblioteki napisanej w C. Liczba argumentw jest identyczna (nawet jeeli nie jest widoczny przekazywany funkcjom adres struktury, to nadal tam istnieje), a ponadto kada funkcja ma tylkojedno ciao. Dzieje si tak, poniewa zapis: Stash A, B, C; nie oznacza wcale, e dla kadej zmiennej istniej odrbne wersje funkcji add( ). A zatem generowany kod jest niemal identyczny z tym, ktry naleaoby napisa dIa wersji biblioteki przygotowanej w jzyku C. Co ciekawsze, zawiera on rwnie ozdobne nazwy", ktre naleaoby wykorzysta, tworzc Stash_initiaIize( ), Stash_cleanup( ) itd. Kompilator robi praktycznie to samo, gdy nazwa funkcji znajduje si wewntrz struktury. Tak wic funkcja initiaIize( ), zawarta w strukturze Stash, nie bdzie kolidowaa z funkcj o nazwie initialize( ), znajdujc si wewntrz innej struktury, ani nawet z globaln funkcj o tej samej nazwie. W wikszoci przypadkw nie trzeba si przejmowa uzupenieniami nazw funkcji uywa si po prostu zwykych nazw. Czasami jednak zachodzi konieczno okrelenia, e funkcja initialize( ) naley do struktury Stash, a nie do jakiej innej. W szczeglnoci podczas definiowania funkcji naley w peni oznaczy, ktrjest ona funkcj. Aby to uzyska, stosuje si wjzyku C++ operator (::), nazywany operatorem zasigu (z uwagi na to, e nazwy mog obecnie nalee do rnych zasigw zasigu globalnego lub

Thinking in C++. Edycja polska zawartego w strukturze). Na przykad chcc wskaza funkcj initialize(), nalec do struktury Stash, naley napisa Stash::initialize(int size). Sposb zastosowania operatora zasigu ujawnia si w definicjach funkcji: // Biblioteka w jzyku C. przeniesiona do C++ // Deklaracje struktury i funkcji: #include "CppLib.h" #include <iostream> #include <cassert> using namespace std; // Liczba elementw dodawanych // w przypadku powikszenia pamici: const int increment - 100; void Stash::initialize(int sz) { size = sz; quantity = 0; storage = 0; next = 0; int Stash::add(const void* element) { if(next >= quantity) //Czy wystarczy pamici? inflate(increment); // Kopiowanie elementu do pamici. // poczwszy od nastpnego wolnego miejsca: int startBytes = next * size; unsigned char* e - (unsigned char*)element; for(int i = 0; i < size; i++) storage[startBytes + i] = e[i]; next++; return(next - 1); // Numer indeksu void* Stash::fetch(int index) { // Check index boundaries: assert(0 <= index); if(index >= next) return 0; // Oznaczenie koca // Tworzenie wskanika do za,danego elementu: return &(storage[index * size]); int Stash: :count() { return next; // Liczba elementw w Stash void Stash::inflate(int increase) { assert(increase > 0); int newQuantity - quantity + increase; int newBytes - newQuantity * size; int oldBytes - quantity * size; unsigned char* b = new unsigned char[newBytes]; for(int i - 0: i < oldBytes; i++) b[i] = storage[i]; // Kopiowanie starego obszaru do nowego delete []storage; // Stary obszar pamici
/ / : C04:CppLib.cpp {0}

Rozdzia 4. Abstrakcja danych

191

storage = b; // Wskanik do nowego obszaru quantity - newQuantity;

}
void Stash::cleanup() { if(storage !- 0) { cout "zwalnianie pamici" endl; delete []storage;

Istnieje szereg innych rnic pomidzy C i C++. Po pierwsze, kompilator wymaga deklaracji zawartych w plikach nagwkowych. W jzyku C++ nie mona wywoa niezadeklarowanej uprzednio funkcji, poniewa spowoduje to zgoszenie bdu przez kompilator. Jest to istotna metoda zapewnienia spjnoci pomidzy wywoaniem funkcji a jej definicj. Wymuszajc deklaracj funkcji przed jej wywoaniem, kompilator w praktyce zapewnia, e deklaracja ta zostanie dokonana przez doczenie pliku nagwkowego. Jeeli w miejscu, w ktrym zdefiniowane s funkcje, doczony zostanie ten sam plik nagwkowy, kompilator upewni si, e zawarte w pliku nagwkowym deklaracje s zgodne z definicjami funkcji. Oznacza to, e plik nagwkowy staje si uwierzytelnion skadnic deklaracji funkcji, gwarantujc tym samym, e funkcje te bd wykorzystywane w jednolity sposb we wszystkich jednostkach translacji projektu. Oczywicie, funkcje globalne mog by nadal deklarowane rcznie" w kadym miejscu, w ktrym s one definiowane i wykorzystywane Qest to na tyle niewygodne, e w rezultacie staje si niezwykle rzadkie). Jednake struktury musz by zawsze zadeklarowane, zanim bd definiowane lub uywane, a najwygodniejszym miejscem, w ktrym mona umieci definicj struktury, jest plik nagwkowy z wyjtkiem tych struktur, ktre scelowo ukryte w pliku. Jak mona zauway, wszystkie funkcje skadowe wygldaj niemal tak samo jak wwczas, gdy byy funkcjami jzyka C, z wyjtkiem wyszczeglnienia zasigu oraz faktu, e pierwszy parametr, wystpujcy w wersji biblioteki napisanej w jzyku C, nie jest ju przekazywany jawnie. Oczywicie, parametr ten nadal istnieje, poniewa funkcje musz dziaa na konkretnej zmiennej bdcej struktur. Naley jednak zwrci uwag na to, e w obrbie funkcji skadowych pominito rwnie wybr elementw skadowych struktury! A zatem zamiast s->size = sz piszemy size = sz pomijajc irytujcy przedrostek s->, ktry tak naprawd nie wnosi adnego nowego znaczenia. Kompilatorjzyka C++ najwyraniej nas w tym wyrcza. W rzeczywistoci tworzy on ukryty" pierwszy argument funkcji (zawierajcy adres struktury, ktry by uprzednio przekazywany jawnie) i stosuje selektor skadowej przy kadym odwoaniu do zawartych w strukturze elementw danych. Oznacza to, e bdc wewntrz funkcji skadowej jakiej struktury mona odwoywa si do jej skadowych (wczajc w to skadowe bdce funkcjami) poprzez podanie ich nazw. Zanim kompilator zacznie poszukiwa nazwy wrd nazw globalnych, przeszuka najpierw lokalne nazwy struktury. Przekonasz si, e dziki temu nie tylko atwiej napiszesz programy, ale bdone rwnie znacznie prostsze w czytaniu.

Thinking in C++. Edycja polska

Co jednak zrobi w przypadku, gdy chce si uy adresu struktury? W napisanej wjzyku C wersji biblioteki byo to proste, poniewa pierwszym argumentem kadej funkcji by wskanik CStasch* o nazwie s. W jzyku C++ okazuje si to jeszcze bardziej spjne. Istnieje specjalne sowo kluczowe this, zwracajce adres struktury. Jest to odpowiednik argumentu ,s", uywanego w wersji biblioteki w jzyku C. Dlatego te mona powrci do stylu jzyka C, piszc:
th1s->size - Size:

Kod wygenerowany przez kompilator jest dokadnie taki sam, nie ma wic potrzeby uywania w takich przypadkach sowa kluczowego this. Mona niekiedy natrafi na program, ktrego autor wszdzie stosuje konstrukcj this->, ale nie wnosi ona niczego nowego do treci programu i wskazuje czsto na brak dowiadczenia programisty. Sowo kluczowe this, na og uywane rzadko, jest dostpne w przypadku, gdy zachodzi potrzeba jego uycia (sowo this jest wykorzystywane w niektrych przykadach w dalszej czci ksiki). Zostaajeszczejedna kwestia, o ktrej trzeba wspomnie. Wjzyku C mona przypisa warto typu void* kademu innemu typowi, jak w przykadzie poniej:
int i = 10; void* vp = &i; // Moliwe zarwno w C. jak i w C++ int* ip = vp; // Dopuszczalne tylko w C

i nie spowoduje to sprzeciwu kompilatora. Jednake w jzyku C++ wyraenie takie nie jest dopuszczalne. Dlaczego? Poniewa jzyk C nie cechuje drobiazgowo w sprawach typw, pozwala on na przypisanie wskanika nieokrelonego typu wskanikowi, ktrego typ zosta okrelony. W jzyku C++ nie jest to moliwe. Poniewa pojcie typu jest w jzyku C++ pojciem kluczowym, kompilator zgosi swj sprzeciw w przypadku naruszenia w jakikolwiek sposb informacji o typie. Byo to zawsze wane, lecz w jzyku C++ ma szczeglne znaczenie, poniewa struktury zawieraj funkcje skadowe. Gdyby w jzyku C++ mona byo bezkarnie zmienia typy wskanikw do struktur, mogoby si to nawet skoczy katastrof wywoaniem funkcji skadowej struktury, ktrej w ogle w tej strukturze nie ma! Dlatego te, mimo e jzyk C++ dopuszcza przypisanie dowolnego typu wskanika wskanikowi typu void* (by to pierwotny cel wprowadzenia typu void* o takim rozmiarze, by mg przechowa wskanik dowolnego typu), nie pozwoli on na przypisanie wskanika typu void* wskanikowi jakiegokolwiek innego typu. W takich przypadkach, dla zasygnalizowania czytelnikowi oraz kompilatorowi, e naprawd chce si traktowa wskanik jako wskanik typu docelowego, trzeba zawsze uywa rzutowania. Wie si z tym pewna interesujca kwestia. Jednym z istotnych celw jzyka C++ jest moliwo kompilacji jak najwikszej czci istniejcego kodu jzyka C, co pozwala na atwe przejcie do nowego jzyka. Nie oznacza to jednak, e kada konstrukcja dostpna w jzyku C bdzie automatycznie dopuszczalna w C++. Istnieje wiele takich dziaa, akceptowanych przez kompilator jzyka C, ktre s w istocie niebezpieczne i mog by przyczyn bdw (zostan opisane w dalszej czci ksiki). W takich sytuacjach kompilator C++ zgasza bdy i ostrzeenia. Jest to w wikszym stopniu korzyci ni utrudnieniem. Istnieje bowiem wiele sytuacji, w ktrych na prno prbuje sj wytropi przyczyn bdu w programie napisanym w jzyku C, dopki nie skompiluje si powtrnie programu w C++ i kompilator nie wskae rda

Rozdzia 4. Abstrakcja danych

193

problemu! W jzyku C czsto okazuje si, e program mona co prawda skompilowa, ale w nastpnej kolejnoci trzeba doprowadzi do tego, by dziaa. Program, ktry kompiluje si poprawnie w C++, zazwyczaj rwnie dziaa! Dzieje si tak, poniewajzyk tenjest znacznie bardziej bezwzgldny w stosunku do typw. W programie testowym, prezentujcym wykorzystanie napisanej w C++ wersji biblioteki Stash, mona dostrzec wiele nowych elementw: //: C04:CppLibTest.Cpp //{L} CppLib // Test biblioteki w C++ #include "CppLib.h" #include "../require.h" #include <fstream> #include <iostream> #include <string> using namespace std; int main() { Stash intStash; intStash.initialize(sizeof(int)); for(int i = 0; i < 100; i++) intStash.add(&i); for(int j = 0; j < intStash.count(); j++) cout "intStash.fetch(" j ") = " *dnt*)intStasn.fetch(j) endl . // Przechowywanie 80-znakowych acuchw: Stash stringStash; const int bufsize = 80: stringStash.initialize(sizeof(char) * bufsize); ifstream in("CppLibTest.cpp"); assure(in. "CppLibTest.cpp"); string line; while(getline(in, line)) stringStash . add ( 1 i ne . c_str() ) ; int k = 0; char* cp; while((cp =(char*)stringStash.fetch(k++)) != 0) cout "stringStash.fetch(" k ") = " cp endl ; intStash.cleanup(); stringStash.cleanup() ; atwo dostrzec, e wszystkie zmienne zostay zdefiniowane w locie" (co opisano w poprzednim rozdziale). Oznacza to, e s one zdefiniowane w dowolnym miejscu zasigu, a miejsce ich definicji niejest ograniczone dojego poczatku,jak w przypadkujzyka C. Kod programu jest do podobny do kodu zawartego w pliku CLibTest.cpp, lecz w czasie wywoywania funkcji uywany jest operator selekcji skadowej .", wystpujcy po nazwie zmiennej. Skadni mona uzna za wygodn, poniewa naladuje ona wybr zmiennej, stanowicej skadow struktury. Rnica polega na tym, e jest to funkcja, wic posiada ona list argumentw.

94

Thinking in C++. Edycja polska Oczywicie, wywoanie generowane przez kompilator, w rzeczywistoci przypomina bardziej wywoanie funkcji w wersji biblioteki, napisanej w C. A zatem, uwzgldniajc uzupenienia nazwy funkcji i przekazanie parametru this, wywoanie funkcji intStash.initialize(sizeof(int), 100) staje si czym w rodzaju Stash_initiaUze(&intStash, sizeof(int), 100). Aby pozna, jaki kryje si pod tym mechanizm, pamitaj, e cfront oryginalny kompilator jzyka C++ firmy AT&T tworzy w wyniku kompilacji kod w jzyku C, ktry by nastpnie kompilowany przez kompilator tego jzyka. Oznacza to, e kompilator cfront moe by szybko przeniesiony na dowolny komputer posiadajcy kompilator jzyka C, przyczyniajc si w ten sposb do szybkiego rozprzestrzeniania si technologii kompilacji C++. Poniewajednak kompilator C++ musi by w stanie generowa program w jzyku C, istnieje jaki sposb umoliwiajcy prezentacj skadni jzyka C++ w jzyku C (niektre kompilatory nadal pozwalaj na generacj kodu wjzyku C). Istnieje jeszcze jedna rnica w stosunku do kodu zawartego w CLibTest.cpp jest ni doczenie pliku nagwkowego require.h. Jest to plik utworzony przez autora na potrzeby ksiki, dokonujcy bardziej wyrafinowanej kontroli bdw ni ta realizowana przez funkcj assert(). Zawiera on szereg funkcji, midzy innymi zastosowan tutaj funkcj assure(), uywan w stosunku do plikw. Funkcja ta sprawdza, czy plik zosta pomylnie otwarty i w przypadku niepowodzenia wywietla na standardowym wyjciu komunikat, informujcy, e pliku nie mona otworzy (dlatego te wymaga podania nazwy tego plikujako drugiego argumentu), oraz powoduje zakoczenie pracy programu. Funkcje zawarte w pliku nagwkowym require.h bd uywane w dalszej czci ksiki, szczeglnie do upewnienia si, e program zosta wywoany z odpowiedni liczb argumentw oraz e uywane przez niego pliki zostay poprawnie otwarte. Zastpuj one powtarzajcy si i irytujcy kod, sprawdzajcy wystpienie bdw, dostarczajc ponadto naprawd uytecznych komunikatw o bdach. Funkcje te zostanszczegowo opisane w dalszej czci ksiki.

Czym s obiekty?
Po prezentacji wstpnego przykadu pora zrobi krok wstecz i przyjrze si pewnym zagadnieniom, zwizanym z terminologi. Wczenie funkcji do struktur jest istot wkadu, wniesionego przez jzyk C++ do jzyka C. Zarazem zosta wprowadzony zupenie nowy sposb ujmowania strukturjako poj, W jzyku C struktury stanowijedynie zbiorowiska danych sposb upakowania, umoliwiajcy ich czne traktowanie. Trudno jednak myle o nich inaczej ni jako o konwencji stosowanej przez programistw. Funkcje przetwarzajce te struktury znajduj si wszdzie. Jednake gdy razem z danymi spakowane s funkcje, struktura staje si nowym tworem, zdolnym do opisu zarwno cech Qak robi to struktury w jzyku C), jak i zachowa. Koncepcja obiektw samodzielnych, ograniczonychjednostek, potraficych pamita i dziata, nasuwa si sama. W jzyku C++ obiekty s po prostu zmiennymi, a ich najprostsz definicjjest obszar pamici" (co jest bardziej precyzyjn form stwierdzenia, e kady obiekt musi mie unikatowy identyfikator", ktrym w przypadku C++ jest unikatowy adres pamici). To miejsce, w ktrym mogby przechowywane dane, z czego porednio wynika, e istniejrwnie operacje, ktre mona na tych danych wykona.

Rozdzia 4. Abstrakcja danych

195

Niestety, w dziedzinie tych poj nie ma cakowitej zgodnoci wrd jzykw programowania, mimo e s one raczej powszechnie akceptowane. Mona si spotka z rozbienymi opiniami na temat tego, czym jest obiektowy jzyk programowania, chocia wydaje si to obecnie do dobrze okrelone. Istniej jzyki bazujce na obiektach, co oznacza, e posiadaj one podobnie jak jzyk C+H struktury zawierajce funkcje, z czym zetknlimy si do tej pory. Jest to jednak jedynie wycinek perspektywy, obejmujcej jzyki obiektowe i jzyki, ktre poprzestaj na umieszczeniu funkcji w strukturach danych. Okrelane s jako jzyki bazujce na obiektach, a nie jzyki obiektowe.

Tworzenie abstrakcyjnych typw danych


Moliwo czenia danych z funkcjami pozwala na utworzenie nowych typw danych. Czsto nazywa sijkapsukowaniem1 (ang. encapsulation). Istniejce typy danych mog skada si z wielu poczonych ze sob informacji. Na przykad liczba typu float zawiera wykadnik, mantys oraz bit znaku. Mona zada dodania jej do innej liczby zmiennopozycyjnej, liczby cakowitej itd. Posiada wic ona zarwno pewne dane,jak i okrelone zachowanie. Definicja struktury Stash tworzy nowy typ danych. Mona wykonywa na nim operacje add(), fetch() oraz inflate(). Egzemplarz tego typu tworzy si wpisujc Stash s, podobnie jak piszc float f tworzy si zmienn zmiennopozycyjn. Typ Stash posiada zarwno cechy, jak i zachowanie. Mimo e zachowuje si on jak prawdziwy, wbudowany typ danych, okrelamy go mianem abstrakcyjnego typu danych, prawdopodobnie z uwagi na to, e pozwala na przedstawienie w przestrzeni rozwizania abstrakcyjnej postaci pojcia, pochodzcego z przestrzeni problemu. Ponadto kompilatorjzyka C++ traktuje gojak nowy typ danych i w gdy funkcja wymaga argumentu typu Stash, kompilator upewnia si, e funkcji zostaa przekazana taka wanie warto. A zatem w stosunku do abstrakcyjnych typw danych (nazywanych czasami typami zdefiniowanymi przez uytkownika) kontrola typw przeprowadzana jest na takim samym poziomie, jak w przypadku typw wbudowanych. Rnicajest natomiast natychmiast widoczna w sposobie, wjaki wykonywane soperacje na obiektach. Zapis obiekt.funkcjaSkiadowa(IistaArgurnentow) oznacza: wywoaj funkcj skadow obiektu". Jednak w uywanej potocznie terminologii obiektowej jest to rwnie okrelane wysyaniem komunikatu do obiektu". A zatem dla zmiennej, zdefiniowanej jako Stash s, instrukcja s.add(&i) oznacza wysanie do zmiennej s komunikatu, by wykonaa na sobie operacj add()". Waciwie programowanie obiektowe mona streci w kilku sowach jako wysylanie komunikatw do obiektw. I faktycznie tworzymy grup obiektw, a nastpnie wysyamy do nich komunikaty. Caa sztuka polega oczywicie na tym, by okreli, jakie s te obiekty i te komunikaty, ale kiedy po wykonaniu tego implementacja programu wjzyku C++ okazuje si zaskakujco atwa.
Pojcie to wywouje spory. Niektrzy uywaj go w sposb przedstawiony w tym miejscu, podczas gdy inni stosuj go w odniesieniu do kontroli dostpu, omawianej w nastpnym rozdziale.

196

Thinking in C++. Edycja polska

Szczegy dotyczce obiektw


W trakcie seminariw czsto pada pytanie: ,jak due sobiekty i jak one wygldaj?". Odpowied brzmi: spodobne do strukturjzyka C". W istocie kod generowany przez kompilator jzyka C w przypadku struktur (bez dodatkw waciwych jzykowi C++) wyglda zazwyczaj dokladnie tak samo, jak kod wygenerowany przez kompilator C++. Uspokaja to tych programistw jzyka C, ktrzy uzaleniaj swj kod od szczegw dotyczcych wielkoci oraz rozmieszczenia danych w pamici i z jakiego powodu, zamiast uywa identyfikatorw, odwouj si bezporednio do poszczeglnych bajtw tworzcych struktur (poleganie na okrelonej wielkoci i rozmieszczeniu danych w pamici prowadzi do tworzenia nieprzenonych programw). Wielko struktury jest czn wielkoci wszystkich jej skadowych. Czasami, gdy struktury umieszczane s w pamici, dodawane s do nich dodatkowe bajty, pozwalajce na odpowiednie rozmieszczenie dzielcych ich granic, dziki czemu moliwe jest uzyskanie wikszej szybkoci wykonania programu. W rozdziale 15. pokaemy, jak w pewnych przypadkach do struktur dodawane s ukryte" wskaniki, ale tymczasem kwestia ta zostanie pominita. Wielko struktury mona okreli za pomoc operatora sizeof. Oto krtki przykad: / / : C04:S1zeof.cpp // Wielkoci struktur #include "CLib.h" #include "CppLib.h" #include <iostream> using namespace std; struct A { int i[100]; struct B { void f():

int main() { cout "wielkosc struktury A = " sizeof(A) " bajtow" endl ; cout "wielkosc struktury B = " sizeof(B) " bajtow" endl ; cout "wielkosc CStash w C = " sizeof(CStash) " bajtow" endl; cout "wielkosc Stash w C++ = " sizeof(Stash) " bajtow" endl;
W przypadku mojego komputera (wyniki uzyskane na innych komputerach mog by odmienne) pierwsza instrukcja wydrukowaa warto 200, poniewa kada liczba cakowita zajmuje dwa bajty. Struktura B stanowi pewn anomali, gdy nie zawiera danych skadowych. To niedopuszczalne w jzyku C, w przeciwiestwie do C++, w ktrym

Rozdzia 4. Abstrakcja danych

197

jest potrzebna moliwo utworzenia struktury speniajcej jedyne zadanie okrelenia zasigu nazw funkcji. Moe by jednak zaskoczeniem, e informacja wydrukowana przez drug instrukcj nie jest zerem. We wczesnych wersjach jzyka wielko tego typu struktur bya zerowa, ale prowadzio to do kopotliwych sytuacji, zwizanych z tworzeniem obiektw tego typu posiaday one taki sam adres, jak obiekty utworzone bezporednio po nich i w zwizku z tym niczym si nie rniy. Jedn z podstawowych zasad dotyczcych obiektw jest ta, e musz one posiada unikatowy adres. Obiekty niezawierajce skadowych bdcych danymi maj zawsze pewnminimaln, niezerowwielko. Dwie ostatnie instrukcje sizeof pokazuj, e wielko struktury w jzyku C++ jest taka samajak wielko odpowiadajcej jej wersji w C. Jzyk C++ stara si nie dokada adnego niepotrzebnego narzutu.

Zasady uywania plikw nagwkowych


Gdy tworzysz struktur zawierajc dane skadowe, tworzysz zarazem nowy typ danych. Zazwyczaj chcesz, by typ ten by dostpny zarwno dla ciebie, jak i dla innych. W dodatku zamierzasz oddzieli interfejs (deklaracje) od implementacji (definicji funkcji skadowych), tak by mona byo zmieni implementacj bez koniecznoci powtrnej kompilacji caego systemu. Osigniesz to, umieszczajc deklaracje dotyczce tworzonego typu w pliku nagwkowym. Kiedy zaczynaem uczy si programowania wjzyku C, pliki nagwkowe byy dla mnie czym tajemniczym. Autorzy wielu ksiek na temat C zdawali si nie przywizywa do nich wagi, a kompilatory nie wymagay deklaracji funkcji. Pliki nagwkowe wydaway si zatem w wikszoci przypadkw nieobowizkowe z wyjtkiem sytuacji, gdy deklarowane byy struktury. W jzyku C++ uywanie plikw nagwkowych staje si zupenie oczywiste. S one niezbdne do atwego tworzenia programw i umieszcza si w nich cile okrelone informacje deklaracje. Pliki nagwkowe informuj kompilator o zawartoci bibliotek. Biblioteki mona uywa nawet wwczas, gdy posiada si jedynie plik nagwkowy oraz plik bdcy programem wynikowym lub bibliotek nie jest potrzebny do tego plik cpp, zawierajcy kod rdowy. W plikach nagwkowych przechowywanajest specyfikacja interfejsu. Mimo e nie jest to wymuszane przez kompilator, najlepszym sposobem budowania wjzyku C duych projektwjest wykorzystanie bibliotek poczenie zwizanych ze sobfunkcji wjeden plik wynikowy lub bibliotek oraz uycie pliku nagwkowego, zawierajcego wszystkie deklaracje funkcji. W jzyku C++ jest to konieczne do biblioteki w C mona powrzuca rne funkcje, ale abstrakcyjne typy danych jzyka C++ okrelaj funkcje powizane ze sob moliwoci wsplnego dostpu do danych zawartych w strukturze. Kada funkcja skadowa musi zosta zadeklarowana w deklaracji struktury nie mona umieci jej w adnym innym miejscu. A zatem o ile uywanie bibliotek funkcji byo w jzyku C wspierane, o tyle jest ju ono w jzyku C++ zinstytucjonalizowane.

198

Thinking in C++. Edycja polska

Znaczenie plikw nagtwkowych


W przypadku uywania funkcji pochodzcej z biblioteki jzyk C daje moliwo pominicia pliku nagwkowego i zadeklarowania tej funkcji na wasn rk. W przeszoci wielu programistw tak robio po to, by przyspieszy cho troch dziaanie kompilatora dziki unikniciu koniecznoci otwierania i doczania pliku (nie ma to znaczenia w przypadku nowoczesnych kompilatorw). Poniszy przykad prezentuje deklaracj funkcji printf() w jzyku C (pochodzcej z pliku <stdio.h>), bdc wynikiem skrajnego lenistwa:
printf(...);

Wielokropek okrela zmienn list argumentw2, co oznacza: funkcja printf() posiadajakie argumenty, z ktrych kadyjestjakiego typu, ale naley to zignorowa, akceptujc w jej wywoaniu dowolne argumenty. Uywajc takiej deklaracji, zawiesza si wszelkkontrol typw argumentw funkcji. Taka praktyka moe by rdem trudnych do uchwycenia problemw. Jeeli funkcje deklarowane s ,jecznie", to w jednym z plikw moe zdarzy si pomyka. Poniewa kompilator wykrywa w tym pliku jedynie wprowadzon rcznie" deklaracj, to moe dostosowa si do popenionego bdu. Program zostanie co prawda poczony poprawnie, lecz uycie funkcji w tym wanie pliku bdzie bdne. Jest to trudny do znalezienia bd, ktrego mona atwo unikn, uywajc pliku nagwkowego. Umieszczenie deklaracji wszystkich funkcji w pliku nagwkowym, a nastpnie doczenie tego pliku wszdzie tam, gdzie funkcje te s uywane, a take w miejscu ich definicji, zapewnia stosowanie w caym systemie jednolitych deklaracji. Doczenie pliku nagwkowego do pliku, zawierajcego definicje funkcji, gwarantuje rwnie zgodno deklaracji z definicjami. W przypadku deklaracji struktury w pliku nagwkowym w jzyku C++ plik ten musi zosta doczony wszdzie tam, gdzie struktura ta jest uywana, oraz w miejscu, w ktrym zostay zdefiniowane funkcje skadowe struktury. Kompilator jzyka C++ zgosi bd w przypadku prby wywoania zwykej funkcji, a take wywoania lub definicji funkcji skadowej, jeeli nie zostay one uprzednio zadeklarowane. Wspierajc waciwe stosowanie plikw nagwkowych, jzyk zapewnia spjno w obrbie bibliotek, a take ogranicza liczb bdw, wymuszajc stosowanie w kadym miejscu ujednoliconego interfejsu. Plik nagwkowyjest kontraktem pomidzy toba uytkownikiem biblioteki. Opisuje on twoje struktury danych, a take okrela argumenty wywoywanych funkcji oraz wartoci przez nie zwracane. Gosi on: oto, co robi moja biblioteka". Niektre informacje zawarte w pliku nagwkowym potrzebne s uytkownikowi opracowujcemu wasn aplikacj, a wszystkie z nich niezbdne s kompilatorowi do wygenerowania poprawnego kodu. Uytkownik struktury docza po prostujej plik nagwkowy, tworzy obiekty (egzemplarze) tej struktury i docza do swojego programu odpowiedni modu wynikowy lub bibliotek (tj. skompilowany kod).
Aby napisa definicj funkcji, uywajcej zmiennej liczby argumentw, trzeba uy zbioru makroinstrukcji varargs, ale w jzyku C++ powinno si tego unika. Szczegy dotyczce makroinstrukcji varargs mona znale w dokumentacji kompilatora.

Rozdzia 4. Abstrakcja danych

199

Kompilator wspomaga ten kontrakt, wymagajc od ciebie zadeklarowania wszystkich struktur i funkcji, zanim zostanone uyte oraz w przypadku funkcji skadowych zanim zostan zdefiniowane. Naley zatem umieci deklaracj w pliku nagwkowym i doczy go do pliku, w ktrym zdefiniowane zostay funkcje skadowe, a take do pliku (plikach), w ktrych s one uywane. Poniewa w caym systemie doczanyjest ten sam plik nagwkowy, opisujcy twoj bibliotek, kompilator moe zapewni spjno systemu i zapobiec bdom. Aby poprawnie zorganizowa swj kod i utworzy odpowiednie pliki nagwkowe, naley zwrci uwag na niektre kwestie. Pierwsz z nich jest decyzja, jakie informacje naley umieci w plikach nagwkowych. Podstawowa regua gosi, e powinny si w nich znajdowa wycznie deklaracje, czyli informacje dla kompilatora; nie mog one natomiast zawiera niczego, co przydzielaoby pami generujc kod lub tworzc zmienne. Jest to istotne, poniewa pliki nagwkowe s zazwyczaj doczane do wielujednostek translacji, wchodzcych w skad projektu i w przypadku gdy pami dla tego samego identyfikatora jest przydzielana w wicej ni jednym miejscu, powoduje to zgoszenie przez program czcy bdu wielokrotnej definicji ^est to regula jednej definicji jzyka C++ kad rzecz mona zadeklarowa dowoln liczb razy, ale w programie moe wystpi tylko jedna jej definicja). Od reguy tej istniejjednak odstpstwa. Jeeli zdefiniuje si w pliku nagwkowym zmienn, ktrajest statyczna w obrbie pliku" ^est widoczna tylko wjednym pliku), to w projekcie powstanie wiele jej egzemplarzy, ale podczas czenia nie wystpi kolizja nazw zmiennych3. Na og nie naley umieszcza w pliku nagwkowym niczego, co powodowaoby dwuznaczno podczas czenia.

Problem wielokrotnych deklaracji


Druga kwestia, zwizana z plikami nagwkowymi, polega na tym, e w przypadku zoonego programu, po umieszczeniu deklaracji w pliku nagwkowym, istnieje moliwo, e plik ten zostanie doczony wicej ni jednokrotnie. Dobrym tego przykadem s strumienie wejcia-wyjcia. Do deklaracji struktury, realizujcej funkcje wejcia-wyjcia, moe by doczony jeden lub wiksza liczba plikw nagwkowych, zawierajcych deklaracje strumieni. Jeeli plik cpp, nad ktrym pracujesz, wykorzystuje wiksz liczb struktur (doczajc pliki nagwkowe kadej z nich), istnieje moliwo doczenia pliku nagwkowego <iostream> wicej ni jeden raz i tym samym niebezpieczestwo powtrnej deklaracji strumieni. Kompilator traktuje powtrne deklaracje struktur (dotyczy to zarwno struktur, jak i kIas) jako bd, poniewa w przeciwnym przypadku pozwolioby to na uywanie tej samej nazwy w odniesieniu do rnych typw. Aby zapobiec bdom zwizanym z wielokrotnym doczaniem plikw nagwkowych, naley wbudowa w nie pewn doz inteligencji, uywajc do tego celu preprocesora (standardowe pliki nagwkowe C++, takiejak <iostream>, posiadajju t inteligencj").

Jednak w standardzie C++, uywanie elementw statycznych w obrbie pliku nie jest wskazane.

200

Thinking in C++. Edycja polska Zarwno jzyk C, jak i C++ pozwalaj na powtrn deklaracj funkcji, pod warunkiem, e obie deklaracje s ze sob zgodne; nie zezwalajjednak wcale na powtrn deklaracj struktur. W jzyku C++ zasada ta jest szczeglnie wana z uwagi na to, e gdyby kompilator pozwoli na powtrn deklaracj struktury, to jeliby deklaracje te rniy si od siebie, nie wiedziaby, ktrej z nich ma uy. Problem zwizany z powtrndeklaracjzyskujejeszcze na znaczeniu wjzyku C++, poniewa wszystkie typy danych (struktury zawierajce funkcje) posiadaj na og wasne pliki nagwkowe. W przypadku tworzenia nowego typu danych na podstawie typu ju istniejcego konieczne jest doczenie pliku nagwkowego typu podstawowego do pliku nagwkowego tworzonego typu. W kadym z tworzcych projekt plikw cpp moliwe jest doczenie wielu plikw, doczajcych z kolei te same pliki nagwkowe. Podczas pojedynczej kompilacji kompilator odczytuje wielokrotnie te same pliki nagwkowe. Jeeli temu si nie zapobiegnie, kompilator bdzie napotyka powtrne deklaracje tej samej struktury, zgaszajc bdy. Aby rozwiza ten problem, trzeba poszerzy wiadomoci na temat preprocesora.

Dyrektywy preprocesora #define, #ifdef i #endif


Dyrektywy preprocesora #define mona uy do utworzenia znacznikw dostpnych w czasie kompilacji. Istniej dwie moliwoci: mona po prostu poinformowa preprocesor, e definiowanyjest znacznik nieposiadajcy adnej okrelonej wartoci: #define FLAG lub nada mu warto (cojest wjzyku C typowym sposobem tworzenia staych):

#define PI 3.14159
W kadym z powyszych przypadkw preprocesor moe sprawdzi, czy etykieta zostaa zdefiniowana:
#ifdef FLAG

Wyraenie to zwrci warto prawdziw, w wyniku czego kod nastpujcy po dyrektywie #ifdef zostanie wczony do kodu przekazywanego kompilatorowi. Wczanie to koczy si w chwili napotkania przez preprocesor dyrektywy: #endif

lub

#endif // FLAG
Niedozwolone jest dopisanie po dyrektywie #endif w tym samym wierszu czego, co niejest komentarzem, mimo e niektre kompilatory mogto akceptowa. Pary dyrektyw #ifdef/#endif mog by wewntrz siebie zagniedane. Przeciwiestwem #define jest #undef, w wyniku ktrego dyrektywa #ifdef, uywajca tego samego identyfikatora, zwrci warto negatywn. Dyrektywa #undef moe rwnie spowodowa zaprzestanie uywania przez kompilator makroinstrukcji. Przeciwiestwem

Rozdzia 4. Abstrakcja danych

201

#ifdef jest #ifndef, zwracajca warto pozytywn, gdy podana etykieta nie zostaa zdefiniowana ^ej wanie bdziemy uywa w plikach nagwkowych). Srwnie inne uyleczne waciwoci preprocesorajzyka C. Ich peny wykaz znajduje si w dokumentacji uywanego kompilatora.

Standard plikw nagtwkowych


W przypadku kadego pliku nagwkowego zawierajcego struktur naley najpierw ustali, czy plik ten nie zostaju doczony do biecego pliku cpp. Aby to uczyni, trzeba sprawdzi warto znacznika preprocesora. Jeeli znacznik ten nie jest zdefiniowany, oznacza to, e biecy plik nagwkowy nie zostajeszcze doczony. Naley zatem zdefiniowa znacznik (dziki czemu struktura nie bdzie moga zosta powtrnie zadeklarowana) i zadeklarowa struktur. Jeeli natomiast znacznik jest ju zdefiniowany, oznacza to, e typ ten zostaju zadeklarowany, wic mona po prostu pomin deklarujcy go kod. PIik nagwkowy powinien wic wyglda nastpujco:
#ifndef HEADER_FLAG #define HEADER_FLAG // Deklaracja typu... #endif // HEADER_FLAG

Gdy plik nagwkowyjest doczany po raz pierwszy,jego zawarto (wczajc w to deklaracj typu) zostanie doczona przez preprocesor. We wszystkich nastpnych doczeniach pliku w obrbie tej samej jednostki kompilacji deklaracja typu zostanie zignorowana. Nazwa HEADER_FLAG moe by dowoln unikatow nazw, ale niezawodnym standardem, ktry warto naladowa, jest zapisanie nazwy pliku wielkimi literami i zastpienie wystpujcych w niej kropek znakami podkrelenia jednake znajdujce si na pocztku nazwy znaki podkrelenia s zarezerwowane dla nazw systemowych). Ilustruje to poniszy przykad:
/ / : C04:Simple.h // Prosty nagwek, zapobiegajcy powtrnej definicji #ifndef SIMPLE_H #define SIMPLE_H struct Simple { int i.j.k; initialize() { i = j = k = 0: } }:

#endif // SIMPLE_H ///:-

Mimo e tekst SIMPLE_H, znajdujcy si po dyrektywie #endif, jest oznaczonyjako komentarz i tym samym jest on ignorowany przez preprocesor, przydaje si do celw dokumentacyjnych. Przedstawione powyej dyrektywy preprocesora, uniemoliwiajce wielokrotne doczanie plikw nagwkowych, okrelane s czsto mianem stranikw doczania.

:02

Thinking in C++. Edycja polska

*rzestrzenie nazw w plikach nagtwkowych


Nietrudno zauway, e niemal we wszystkich plikach, zawartych w ksice, uywanajest dyrektywa using, wystpujca zazwyczaj w postaci:

using namespace std;


Poniewa std jest przestrzeni nazw, obejmujc cal standardow bibliotek C++, taki sposb wykorzystania dyrektywy using pozwala na uywanie bez kwalifikatorw nazw zawartych w standardowej bibliotece C++. Jednake dyrektywa using nie wystpuje nigdy w plikach nagwkowych (przynajmniej nie zewntrz zasigw). Dyrektywa using wyklucza bowiem ochron danej przestrzeni nazw, ajej dziaanie rozciga si do koca biecej jednostki kompilacji. Jeeli dyrektyw t umieci si poza jakimkolwiek zasigiem, w pliku nagwkowym, bdzie to oznaczao, e utrata ochrony przestrzeni nazw" nastpi w kadym pliku, do ktrego zostanie doczony ten plik nagwkowy, a zatem czsto rwnie w innych plikach nagwkowych. Tak wic umieszczenie dyrektywy using w plikach nagwkowych moe bardzo atwo doprowadzi do wyczenia" przestrzeni nazw praktycznie w kadym pliku, likwidujc korzyci z nimi zwizane. Krtko mwic w plikach nagwkowych nie naley umieszcza dyrektyw using.

Wykorzystywanie plikw nagtwkowych w projektach


Podczas budowy projektw na og czy si ze sob wiele rnych typw (struktur danych wraz z towarzyszcymi im funkcjami). Zazwyczaj deklaracje kadego typu lub grupy powizanych ze sob typw znajduj si w oddzielnych plikach nagwkowych, a nastpnie definiuje si funkcje tych typw w poszczeglnych jednostkach translacji. Podczas uywania typu trzeba doczy odpowiedni plik nagwkowy, co zapewnia waciwe dokonanie deklaracji. W ksice wystpuj przykady przygotowane zgodnie z opisanymi powyej wzorcami, ale czciej zdarza si, e prezentowane przykady s bardzo mae. Wszystkie elementy deklaracje, struktury, definicje funkcji oraz funkcja main( ) mog zatem znajdowa si w pojedynczym pliku. Warto jednak pamita, e w praktyce korzystnejest uywanie oddzielnych plikw oraz plikw nagwkowych.

Zagniedone struktury
Wygoda, wynikajca z usunicia nazw danych funkcji z globalnej przestrzeni nazw, dotyczy rwnie struktur. Mona umieci struktur wewntrz innej struktury, utrzymujc w ten sposb razem powizane ze sob elementy. Skadnia takiej konstrukcji jest zgodna z oczekiwaniami, o czym wiadczy przedstawiona poniej struktura, stanowica implementacj rozwijanego w d stosu, zrealizowanego za pomoc prostej listy powizanej, dziki czemu nigdy" nie wykracza on poza dostpnpami:

Rozdzia 4. Abstrakcja danych

203

//: C04:Stack.h // Zagniedone struktury, tworzce list powizan powizanej #ifndef STACK_H #define STACK_H struct Stack { struct Link { void* data; Link* next; void initialize(void* dat, Link* nxt); }* head; void initialize(); void push(void* dat); void* peek(); void* pop(); void cleanup(); };

#endif // STACK_H ///:-

Zagniedona struktura nosi nazw Link i zawiera wskanik do nastpnej struktury Link, znajdujcej si na licie, oraz wskanik do danych przechowywanych w strukturze. Jeeli wartoci wskanika nextjest zero, oznacza to osignicie koca listy. Zwr uwag na to, e wskanik head zosta zdefiniowany bezporednio po deklaracji struktury Link, a nie w oddzielnej definicji o postaci Link* head. Jest to skadnia wywodzca si z jzyka C, ale podkrela ona znaczenie rednika, wystpujcego po deklaracji struktury rednik oznacza koniec listy oddzielonych przecinkami definicji zmiennych tego typu (zazwyczaj ta Iistajest pusta). Podobniejak inne struktury prezentowane do tej pory, zagniedona struktura posiada funkcj initialize(), zapewniajc odpowiedni inicjalizacj. Struktura Stack posiada zarwno funkcje initialize( ) oraz cleanup(), jak i dwie funkcje. Funkcja push() pobiera wskanik do danych, ktre maj by przechowywane (zakada ona, e dane zostay umieszczone na stercie); z kolei pop() zwraca wskanik danych (data) zawarty w elemencie znajdujcym si na szczycie stosu i usuwa ten element ze stosu (po pobraniu elementu ze stosu uytkownikjest odpowiedzialny za usunicie obiektu wskazywanego przez data). Funkcja peek() rwnie zwraca wskanik danych zawarty w elemencie znajdujcym si na szczycie stosu, ale pozostawia ten element na stosie. Poniej przedstawiono definicje funkcji skadowych: //: C04:Stack.cpp {0} // Lista powizana z zagniedaniem #include "Stack.h" #include "../require.h" using namespace std; void Stack:;Link;:initialize(void*dat. Link*nxt) {

data - dat;
next = nxt;

void Stack::initialize() { head - 0;

Thinking in C++. Edycja polska void Stack::push(void* dat) { Link* newLink = new Link; newLink->initialize(dat, head); head - newLink; void* Stack::peek() { require(head !- 0, "Stos jest pusty"); return head->data; void* Stack: :pop() { if(head == 0) return 0; void* result = head->data: Link* oldHead = head; head = head->next; delete oldHead; return result; void Stack: :cleanup() { require(head == 0. "Stos nie jest pusty"); Pierwsza z definicji jest szczeglnie interesujca, poniewa prezentuje ona sposb, w jaki definiowana jest skadowa zagniedonej struktury. Uywa si w tym celu dodatkowego poziomu zasigu, okrelajcego nazw zawierajcej go struktury. Funkcja Stack::Link:: initiaUze( ) pobiera argumenty, przypisujc swoim zmiennym ich wartoci. Funkcja Stack::initialize() ustawia zerow warto wskanika head, dziki czemu obiekt wie, e listajest pusta. Funkcja Stack::push( ) pobiera argument bdcy wskanikiem do zmiennej, ktra ma zosta zapamitana, i umieszcza go na stosie. Najpierw wykorzystuje operator new do przydzielenia pamici strukturze Link, ktr ulokuje na szczycie stosu. Nastpnie wywouje funkcj initialize( ) struktury Link w celu przypisania odpowiednich wartoci skadowym struktury Link. Zwr uwag na to, e wskanikowi next jest przypisywana aktualna warto wskanika head, a nastpnie wskanikowi head przyporzdkowanyjest wskanik do nowo utworzonej struktury Link. W praktyce powoduje to umieszczenie struktury Link na szczycie stosu. Funkcja Stack::pop( ) pobiera wskanik danych (data) znajdujcych si na szczycie stosu, a nastpnie przesuwa wskanik head do nastpnej pozycji i usuwa stary szczyt stosu, zwracajc ostatecznie pobrany wskanik danych. Gdy funkcja pop( ) usunie ostatni element stosu, wskanik head ponownie osignie warto zerow, oznaczajc, e stos jest pusty. Funkcja Stack::cleanup( ) w rzeczywistoci niczego nie porzdkuje. Okrela ona natomiast polityk firmy", ktra sprowadza si do tego, e klient-programista, uywajcy tego stosu, jest odpowiedzialny za pobranie z niego wszystkich elementw oraz ich usunicie". Do zasygnalizowania, e w przypadku gdy stos nie jest pusty. wystpi bd programistyczny, uywanajest funkcja require( ).

Rozdzia 4. Abstrakcja danych

205

Dlaczego wszystkimi obiektami, ktre klient pozostawi na stosie, nie mgby si zaj destruktor stosu? Problem polega na tym, e na stosie przechowywane s wskaniki typu void* i, jak przekonamy si w rozdziale 13., uycie w stosunku do takiego wskanika operatora delete nie powoduje poprawnego usunicia obiektu. Problem kto jest odpowiedzialny za pami" nie jest nawet tak prosty, o czym dowiesz si dziki lekturze nastpnych rozdziaw. Poniej znajduje si przykadowy program, testujcy struktur Stack: //: C04:StackTest.cpp //{L} Stack //{T} StackTest.cpp // Test zagniedonej listy powizanej #include "Stack.h" #include ".,/require.h" #include <fstream> #include <iostream> #include <string> using namespace std; int main(int argc, char* argv[]) { requireArgs(argc. 1); // Argumentem jest nazwa pliku ifstream in(argv[l]); assure(in, argv[l]); Stack textlines; textlines.initialize(); string line; // Odczytanie pliku i zapamitanie wierszy na stosie: while(getline(in. line)) textlines.push(new stnng(line)); // Pobranie wierszy ze stosu i wydru<owanie ich: string* s; while((s = (string*)textlines.popO) != 0) { cout *s endl; delete s; } textlines.cleanup(); } ///:Program jest to podobny do przedstawionego w poprzednim przykadzie, ale umieszcza wiersze odczytane z pliku Q'ako wskaniki do acuchw) na stosie, a nastpnie pobieraje z niego, w rezultacie czego wiersze pliku s drukowane w odwrotnej kolejnoci. Warto zwrci uwag na to, e funkcja skadowa pop() zwraca wskanik void*, ktry przed wykorzystaniem musi by z powrotem rzutowany na typ string*. W celu wydrukowania acucha dokonuje si wyuskania wskanika. Podczas wypeniania stosu textlines zawarto zmiennej Une jest dla kadej operacji push() klonowana" za pomoc wyraenia new string(Iine). Warto zwracana przez wyraenie new jest wskanikiem do nowo utworzonego acucha, do ktrego kopiowana jest warto zmiennej Une. Gdyby przekazywa funkcji push po prostu adres zmiennej line, doprowadzioby to do wypenienia stosu identycznymi adresami, wskazujcymi na zmienn line. W dalszej czci ksiki znajduje si wicej informacji na temat takiego klonowania".

206

Thinking in C++. Edycja polska

Nazwa plikujest pobierana z wiersza polece. Do upewnienia si, e w wierszu polece podano odpowiedni liczb argumentw, uyto funkcji requireArgs( ), pochodzcej z pliku nagwkowego require.h. Funkcja ta porwnuje argument argc z liczb oczekiwanych argumentw, drukujc odpowiedni komunikat o bdzie i koczc prac programu w przypadku, gdy nie podano odpowiedniej ich liczby.

Zasig globalny
Operator zasigu pozwala na wyjcie z sytuacji, w ktrych domylnie wybrana przez kompilator (najblisza") nazwa nie jest dan. Zamy, e mamy struktur zawierajc identyfikator lokalny a, natomiast wewntrz funkcji skadowej zamierzamy uy globalnego identyfikatora a. Kompilator powinien wybra domylnie lokalny identyfikator, naley wic okreli to w jaki inny sposb. Chcc wskaza nazw globaln, okrelajc jej zasig, naley uy operatora zasigu, przed ktrym nic si nie znajduje. Poniej zamieszczono przykad prezentujcy wybr zasigu globalnego zarwno w przypadku zmiennej, jak i funkcji:
// Okrelenie zasigu globalnego int a; void f() {} struct S { int a; void f();

//: C04:Scoperes.cpp

void S::f() {

} int main() { S s; f(); } lll:~

::f(); // Inaczej wywoanie byoby rekurencyjne! ::a++; // Wybr zmiennej globalnej a a--; // Zmienna a w zasigu struktury

Gdyby w funkcji S::f( ) nie okrelono zasigu, kompilator wybraby domylnie wersje f( ) i a, bdce skadowymi struktury.

Podsumowanie
W rozdziale przedstawiono zasadniczy zwrot", dokonany dziki jzykowi C++ moliwo umieszczania funkcji wewntrz struktur. Takie nowe rodzaje struktur s nazywane abstrakcyjnymi typami danych, a zmienne utworzone za pomoc tych struktur obiektami lub egzemplarzami tych typw. Wywoywanie funkcji skadowych obiektw okrela si mianem wysytania do nich komunikatw. Wysyanie komunikatw do obiektw jest podstawowym dziaaniem zwizanym z programowaniem obiektowym.

Rozdzia 4. Abstrakcja danych

207

Mimo e poczenie danych i funkcji przynosi istotn korzy z punktu widzenia organizacji kodu, uatwia korzystanie z bibliotek, zapobiega kolizjom nazw, ukrywajc je, tojednak mona zrobi znacznie wicej, by uczyni programowanie w C++ bezpieczniejszym. W nastpnym rozdziale zostanie przedstawiony sposb pozwalajcy na takochron niektrych skadowych struktury, by tylkojej autor mg wykonywa na nich operacje. Wyznacza to wyran granic pomidzy tym, co moe zmieni uytkownik danej struktury, i tym, co moe zmodyfikowa wycznie programista.

wiczenia
Rozwizania wybranych wicze mona znale w dokumencie elektronicznym: The Thinking in C++ Annotated Solution Guide, ktry mona pobra za niewielk opat z witryny http://www.BruceEckel.com. . W standardowej bibliotecejzyka C funkcja puts( ) drukuje na konsoli tablic znakowa(dzieki czemu mona napisa puts("witaj")). Napisz program wjzyku C, ktry wykorzystuje funkcj puts(), ale nie docza pliku nagwkowego <stdio.h> ani nie deklaruje tej funkcji w aden inny sposb. Skompiluj program za pomoc kompilatorajzyka C (niektre kompilatory jzyka C++ peni rwnoczenie funkcj kompilatorwjzyka C w takim przypadku musisz odszuka parametr wiersza polece, wymuszajcy kompilacjjzyka C). Nastpnie skompiluj program za pomoc kompilatora C++ i zaobserwuj rnice. 2. Utwrz deklaracj struktury posiadajcej pojedyncz funkcj skadow, a nastpnie utwrz definicj tej funkcji. Utwrz obiekt nowego typu danych i wywoaj funkcj skadow. 3. Zmodyfikuj rozwizanie poprzedniego wiczenia w taki sposb, by struktura zostaa zadeklarowana w odpowiednio strzeonym" pliku nagwkowym, z definicjwjednym pliku cpp i funkcjmain() w drugim. 4. Utwrz struktur skadajc si z pojedynczej skadowej typu cakowitego oraz dwie funkcje globalne, z ktrych kada przyjmuje wskanik do tej struktury. Pierwsza funkcja powinna posiada drugi argument typu cakowitego i przypisywajego warto skadowej cakowitej struktury. Druga funkcjapowinna wywietla warto tej skadowej. Przetestuj obie funkcje. 5. Powtrz poprzednie wiczenie, zmieniajc funkcje w taki sposb, aby stanowiy skadowe struktury, i ponownie przetestuj dziaanie programu. 6. Za pomocsowa kluczowego this (odpowiadajcego adresowi biecego obiektu) utwrz klas, ktra bdzie (nadmiarowo) dokonywaa wyboru danych skadowych i wywoywaa funkcje skadowe. 7. Zmodyfikuj struktur Stash w taki sposb, aby przechowywaa liczby typu double. Umie na nim 25 wartoci typu double, a nastpnie wydrukuj je na konsoli.

!08

Thinking in C++. Edycja polska

8. Powtrz poprzednie wiczenie, dokonujc modyfikacji struktury Stack. 9. Za pomocfunkcji printf(), zawartej w pliku nagwkowym <stdio.h>, utwrz plik zawierajcy funkcj f(), przyjmujcargument cakowity i drukujcyjego warto na konsoli. Uyj zapisu: printf("%dW, i), gdzie ijest drukowanwartocicakowit. Utwrz oddzielny plik, zawierajcy funkcj main(), i zadeklaruj w nim funkcj f( )jako funkcj przyjmujc argument typu float. Wywoaj funkcj f() z wntrza funkcji main(). Sprbuj skompilowa i dokona czenia programu za pomoc kompilatora C++ i sprawd, co si stanie. Nastpnie skompiluj i dokonaj czenia programu, uywajc do tego kompilatorajzyka C. Zobacz, co si stanie, gdy uruchomisz program. Wyjanij jego dziaanie. 10. Dowiedz si, wjaki sposb wygenerowa na wyjciu kompilatorw C i C++ program w asemblerze. Napisz wjzyku C funkcj, a w j z y k u C++ struktur, zawierajcjednfunkcj skadow. Dla kadego z tych plikw wygeneruj programwjzyku asemblera. W utworzonych plikach poszukaj nazw utworzonych dla nazwy funkcji wjzyku C i funkcji skadowej w C++, aby zobaczy, jak nazwy te zostay uzupenione przez kompilator. 11. Napisz program kompilujcy warunkowo kod, zawarty w funkcji main(), w taki sposb, by w razie zdefiniowania okrelonego symbolu preprocesora drukowany by jaki komunikat, a w przeciwnym przypadku by drukowany by inny komunikat. Skompiluj ten kod, eksperymentujc z dyrektywami #define, zawartymi w programie, a nastpnie dowiedz si, jak podaje si definicje preprocesora w wierszu polece kompilatora, i rwnie przeprowad eksperymenty. 12. Napisz program, uywajcy makroinstrukcji assert() z argumentem, ktryjest zawsze faszywy (zerowy), i zobacz, co si stanie, gdy go uruchomisz. Nastpnie skompiluj go, wczajc do programu dyrektyw #define NDEBUG, uruchom go ponownie i zwr uwag na rnice. 13. Utwrz abstrakcyjny typ danych, reprezentujcy kaset w wypoyczalni wideo. Zastanw si nad danymi oraz operacjami, ktrych moe wymaga typ Video, by dziaa prawidowo w systemie zarzdzania wypoyczalni wideo. Docz funkcj skadow print(), wywietlajc informacje o zmiennej typu Video. 14. Utwrz obiekt Stack, przechowujcy obiekty typu Video, pochodzce z poprzedniego wiczenia. Utwrz kilka obiektw typu Video, umie je w obiekcie Stack, a nastpnie wydrukuj, uywajc funkcji Video::print(). 15. Napisz program, ktry drukuje na twoim komputerze wielkoci wszystkich podstawowych typw danych, uywajc do tego operatora sizeof. 16. Zmodyfikuj struktur Stash w taki sposb, by jako podstawowej struktury danych uywaa ona typu vector<char>. 17. Uywajc operatora new, dynamicznie przydziel obszary pamici nastpujcych typw: int, long, tablic o 100 znakach i tablic 100 wartoci typu float. Wydrukuj adresy tych obszarw, a nastpnie zwolnij przydzielon pami, uywajc operatora delete.

Rozdzia 4. Abstrakcja danych

209

18. Napisz funkcj przyjmujcparametr typu char*. Uywajc operatora new, przydziel dynamicznie pami tablicy znakw o takiej samej wielkoci, jak tablica znakowa przekazana funkcji. Wykorzystujc indeksowanie tablic, skopiuj znaki argumentu do dynamicznie przydzielonej tablicy (nie zapomnij o zerze, koczcym tablic znakow) i zwr wskanik do utworzonej w ten sposb kopii. Przetestuj napisan funkcj w obrbie funkcji main( ), przekazujcjej statyczntablic znakowznajdujcsi w cudzysowie, a zwrcon warto przeka z powrotem swojej funkcji. Wydrukuj oba acuchy i oba wskaniki w taki sposb, aby byo wida, e s to rne obszary pamici. Zwolnij caprzydzielondynamicznie pami, uywajc do tego operatora delete. 19. Zaprezentuj przykad struktury zadeklarowanej wewntrz innej struktury (czyli struktury zagniedonej). W obu strukturach zadeklaruj dane skadowe, a nastpnie zadeklaruj i zdefiniuj w nich funkcje skadowe. Napisz funkcj main( ) testujc utworzone przez ciebie typy. 20. Napisz kod drukujcy wielkoci rnych struktur. Utwrz struktury zawierajce wycznie dane skadowe i takie, ktre skadaj si zarwno z danych, jak i z funkcji. Nastpnie utwrz struktur, ktra w ogle nie ma skadowych. Wydrukuj wielko wszystkich utworzonych struktur. Wyjanij wyniki uzyskane dla struktury nieposiadajcej skadowych. 21. Jak wiadomo, w przypadku strukturjzyk C++ automatycznie podejmuje dziaanie rwnowane uyciu sowa kluczowego typedef. Czyni tak rwnie w przypadku wylicze i unii. Napisz niewielki program, ktry to demonstruje. 22. Utwrz stos (Stack) przechowujcy elementy typu Stash. Kady element typu Stash powinien zawiera pi wierszy pliku wejciowego i naley go utworzy za pomocoperatora new. Wczytaj plik, zapamitujc go na stosie, a nastpnie wydrukuj w pierwotnej postaci, pobierajc dane ze stosu. 23. Zmodyfikuj poprzednie wiczenie, tworzc struktur zawierajc stos obiektw typu Stash. Jej uytkownik powinienjedynie dodawa i pobiera wiersze, uywajc do tego funkcji skadowych, ale wewntrz struktury powinny by stosowane typy Stack i Stash. 24. Utwrz struktur przechowujc liczb cakowit i wskanik do innego egzemplarza tej samej struktury. Napisz funkcj pobierajcadresjednej z takich struktur oraz liczb cakowit, oznaczajcdugo listy, ktr zamierzasz utworzy. Funkcja ta bdzie tworzya cay acuch takich struktur (listepowiazanq), rozpoczynajc od argumentu (glowy listy)', kada z tych struktur bdzie wskazywaa na nastpn. Do ich tworzenia uyj operatora new, zapisujc licznik (numer porzdkowy obiektu) w skadowej, bdcej liczb cakowit. W ostatniej strukturze na licie przypisz wskanikowi nastpnej struktury warto zerow, oznaczajc koniec listy. Napisz drug funkcj, pobierajcgow listy i przesuwjcsi wzdu niej a do koca. Funkcja ta drukuje dla kadego elementu zarwno warto wskanika, jak i zapamitan w elemencie warto cakowit. 25. Powtrz poprzednie wiczenie, umieszczajc wszystkie funkcje wewntrz struktury (zamiast uywania oddzielnych" struktur i funkcji).

210

Thinking in C++. Edycja polska

Rozdzia 5.

Ukrywanie implementacji
Typowa biblioteka skada si w jzyku C ze struktury i kilku doczonych funkcji, wykonujcych na niej operacje. Zapoznalimy si ze sposobem, w jaki jzyk C++ grupuje funkcje, zwizane ze sob pojciowo, i czy je literalnie. Dokonuje tego umieszczajc deklaracje funkcji w obrbie zasigu struktury, zmieniajc sposb, wjaki funkcje te swywofywane w stosunku do tej struktury, eliminujc przekazywanie adresu struktury jako pierwszego argumentu i dodajc do programu nazw nowego typu (dziki czemu nie trzeba uywa sowa kluczowego typedef w stosunku do identyfikatora struktury). Wszystko to zapewnia wiksz wygod pozwala lepiej zorganizowa kod i uatwia zarwno jego napisanie, jak i przeczytanie. Warto jednak poruszy jeszcze inne wane zagadnienia, zwizane z atwiejszym tworzeniem bibliotek w jzyku C++ w szczeglnoci sto kwestie, dotyczce bezpieczestwa i kontroli. W niniejszym rozdziale zapoznamy si bliej z kwestiami ogranicze dotyczcych struktur.

Okrelanie ogranicze
W kadej relacji istotnejest okrelenie granic, respektowanych przez wszystkie zaangaowane w ni strony. Tworzc bibliotek, ustanawiasz relacj z klientem-programist, uywajcym twojej biblioteki do zbudowania aplikacji lub utworzenia innej biblioteki. W strukturach dostpnych w jzyku C, podobnie jak w wikszoci elementw tego jzyka, nie obowizujadne reguy. Klienci-programici mogpostpi dowolnie ze struktur i nie ma adnego sposobu, by wymusi na nich jakiekolwiek szczeglne zachowania. Na przykad mimo wanoci funkcji o nazwach initialize() i cleanup() (wskazanej w poprzednim rozdziale), klient-programista moe w ogle ich nie wywoa (lepsze rozwizanie tej kwestii zaprezentujemy w nastpnym rozdziale). W jzyku C nie ma adnego sposobu zapobieenia temu, by klienci-programici operowali bezporednio na niektrych skadowych struktur. Wszystko ma charakterjawny.

212

Thinking in C++. Edycja polska Istniej dwa powody wprowadzenia kontroli dostpu do skadowych struktur. Przede wszystkim programistom naley uniemoliwi stosowanie narzdzi niezbdnych do wykonywania wewntrznych operacji zwizanych z typem danych, lecz niebdcych czci interfejsu potrzebnego klientom-programistom do rozwizania ich wasnych problemw. Jest to w rzeczywistoci pomoc udzielana klientom-programistom, poniewa dziki temu mogatwo odrni kwestie istotne od pozostaych. Drugim powodem wprowadzenia kontroli dostpu jest umoliwienie projektantowi biblioteki zmiany wewntrznych mechanizmw struktury z pominiciem wpywu na klienta-programist. W przykadzie ze stosem, przedstawionym w poprzednim rozdziale, z uwagi na szybko mona by przydziela pami duymi porcjami zamiast tworzy kolejny jej obszar, ilekro dodawany jest nowy element. Jeeli interfejs oraz implementacja s wyranie od siebie oddzielone i chronione, mona tego dokona, wymagajc od klienta-programisty jedynie przeprowadzenia ponownego czenia moduw wynikowych.

Kontrola dostpu w C++


Jzyk C++ wprowadza trzy nowe sowa kluczowe, pozwalajce na okrelenie granic w obrbie struktur: public (publiczny), private (prywatny) i protected (chroniony). Sposb ich uycia oraz znaczenie wydaj si do oczywiste. S one specyfikatorami dostpu (ang. access specifiers}, uywanymi wycznie w deklaracjach struktur, zmieniajcymi ograniczenia dla wszystkich nastpujcych po nich definicji. Specyfikator dostpu zawsze musi koczy si rednikiem. Specyfikator public oznacza, e wszystkie nastpujce po nim deklaracje skadowych s dostpne dla wszystkich. Skadowe publiczne s takie same, jak zwyke skadowe struktur. Na przykad ponisze deklaracje struktur sidentyczne:
/ / : C05:Public.cpp // Specyfikator public przypomina zwyke // struktury jzyka C
struct A { int i; char j; float f; void func();

void A::func() {}

struct B { public: char j; float f; void func();

int i;

Rozdzia 5. Ukrywanie implementacji


void B::func() {} int main() { A a; B b; a.i = b.i = 1; a.j = b.j = ' c ' ; a.f = b.f = 3.14159; a.func(); b.func();

213

Z kolei sowo kluczowe private oznacza, e do skadowych struktury nie ma dostpu nikt, oprcz ciebie, twrcy typu, i tojedynie w obrbie funkcji skadowych tego typu. Specyfikator private stanowi barier pomidzy tob i klientem-programist kady, kto sprbuje odwoa si do prywatnej skadowej klasy, otrzyma komunikat o bdzie ju na etapie kompilacji. W powyszej strukturze B mgby chcie na przykad ukry cz jej reprezentacji (tj. danych skadowych), dziki czemu byfyby one dostpne wycznie dla ciebie:
/ / : C05:Private.cpp // Okrelanie granicy struct B { private: char j; float f; public: int i ; void func();

void B: :func() i = 0; J - '0'; f = 0.0;

int main() { B b; b.i = 1; // OK, skadowa publiczna //! b.j = '1'; // Niedozwolone, skadowa prywatna //! b.f = 1.0; // Niedozwolone, skadowa prywatna Mimo e funkcja func( ) ma dostp do kadej skadowej struktury B (poniewa jest ona rwnie skadow struktury B, wic automatycznie uzyskuje do tego prawo), to zwyka funkcja globalna, takajak main( ), nie posiada takich uprawnie. Oczywicie, do tych skadowych nie maj dostpu rwnie funkcje skadowe innych struktur. Wycznie funkcje, ktre zostafy wyranie wymienione w deklaracji struktury (kontrakt"), maj dostp do prywatnych skadowych struktury. Nie istnieje okrelony porzdek, w jakim powinny wystpowa specyfikatory dostpu; mog one rwnie wystpowa wicej ni jednokrotnie. Dotycz one wszystkich zadeklarowanych po nich skadowych, a do napotkania nastpnego specyfikatora dostpu.

214

Thinking in C++. Edycja polska

Specyfikator protected
Ostatnim specyfikatorem dostpujest protected. Jego znaczenie jest zblione do specyfikatora private, z jednym wyjtkiem, ktry nie moe zosta jeszcze teraz wyjaniony struktury dziedziczce" (ktre nie posiadaj dostpu do skadowych prywatnych) maj zagwarantowany dostp do skadowych oznaczonych specyfikatorem protected. Zostanie to wyjanione w rozdziale 14., przy okazji wprowadzenia pojcia dziedziczenia. Tymczasowo mona przyj, e specyfikator protected dziaa podobnie do specyfikatora private.

Przyjaciele
Co zrobi w sytuacji, gdy chcemy jawnie udzieli pozwolenia na dostp funkcji, niebdcej skadowbiecej struktury? Uzyskuje si to, deklarujc funkcj za pomoc sowa kluczowego friend przyjaciel) wewntrz deklaracji struktury. Wane jest, by deklaracja friend wystpowaa w obrbie deklaracji struktury, poniewa programista (i kompilator), czytajc deklaracj struktury, musi pozna wszystkie zasady dotyczce wielkoci i zachowania tego typu danych. A niezwykle wan zasad, obowizujc w kadej relacji, stanowi odpowied na pytanie: Kto ma dostp do mojej prywatnej implementacji?". To sama struktura okrela, ktry kod ma dostp do jej skadowych. Nie ma adnego magicznego sposobu wamania si" z zewntrz, jeeli nie jest si przyjacielem" nie mona zadeklarowa nowej klasy, twierdzc: Cze, jestem przyjacielem Boba!" i spodziewajc si, e zapewni to dostp do prywatnych i chronionych skadowych klasy Bob. Wolno zadeklarowa jako przyjaciela" funkcj globalnaj moe nim by rwnie skadowa innej struktury albo nawet caa struktura zadeklarowana z uyciem sowa kluczowego friend. Poniej zamieszczono przykad:
/ / : C05:Friend.cpp // Sowo kluczowe friend daje specjalne // prawa dostpu
// Deklaracja (niekompletna specyfikacja typu): struct X; struct Y { void f(X*);

struct X { // Definicja private: int i; public: void initialize(); friend void g(X*, int); // Przyjaciel globalny friend void Y::f(X*); // Przyjaciel bdcy skadow struktury friend struct Z; // Caa struktura jako przyjaciel friend void h();

Rozdzia 5. Ukrywanie implementacji void X: :initialize() { i = 0; void g(X* x, int i) { x->i = i; void Y : : f ( X * x) { x->i = 47; struct Z { private: int j; public: void initialize(); void g(X* x); void Z::initialize() J = 99; void Z : : g ( X * x) { x->i += j; void h() { X x; x.i = 100; // Bezporednia operacja na skadowej int main()
X x; Z z;

215

z.g(&x);

Struktura Y posiada funkcj skadow f( ), modyfikujc obiekt typu X. Jest to nieco zagadkowe, poniewa kompilator jzyka C++ wymaga zadeklarowania kadej rzeczy przed odwoaniem si do niej. Naley wic zadeklarowa struktur Y, zanim jeszcze jej skadowa Y::f(X*) bdzie moga zosta zadeklarowanajako przyjaciel struktury X. Jednake, aby funkcja Y::f(X*) moga zosta zadeklarowana, najpierw naley zadeklarowa struktur X! Oto rozwizanie. Zwr uwag na to, e funkcja Y::f(X*) pobiera adres obiektu X. Jest to istotne, poniewa kompilator zawsze wie, wjaki sposb przekaza adres bdcy staej wielkoci, niezalenie od przekazywanego za jego porednictwem obiektu i nawet jeeli nie posiada penej informacji dotyczcej jego wielkoci. Jednake w przypadku prby przekazania caego obiektu kompilator musi widzie ca definicj struktury X, aby poznajej wielko i wiedzie, wjaki sposbjprzekaza, zanim pozwoli na deklaracj funkcji w rodzaju Y::g(X).

216

Thinking in C++. Edycja polska

Przekazujc adres struktury X, kompilator pozwala na utworzenie niepenej specyfikacji typu X, umieszczonej przed deklaracj Y::f(X*). Uzyskuje si j za pomoc deklaracji:
struct X;

Deklaracja ta informuje kompilator, e istnieje struktura o podanej nazwie, wic mona odwoa si do niej, dopki nie jest na jej temat potrzebna adna dodatkowa wiedza, poza nazw. Potem funkcja Y::f(X*) w strukturze X moe by ju bez problemu zadeklarowana jako przyjaciel". W razie prby zadeklarowaniajej, zanim kompilator napotka pen specyfikacj klasy Y, nastpioby zgoszenie bdu. Jest to cecha zapewniajca bezpieczestwo i spjno, a take zapobiegajca bdom. Zwr uwag na dwie pozostae funkcje zadeklarowane z uyciem sowa kluczowego friend. Pierwsza z nich deklaruje jako przyjaciela zwyk funkcj globaln g(). Funkcja ta nie zostaa jednak wczeniej zadeklarowana w zasigu globalnym! Okazuje si, e taki sposb uycia deklaracji friend moe zosta wykorzystany do rwnoczesnego zadeklarowania funkcji i nadaniajej statusu przyjaciela. Dotyczy to rwnie caych struktur. Deklaracja:
friend struct Z;

jest niepenspecyfikacjtypu struktury Z, nadajcrwnoczenie caej tej strukturze status przyjaciela.

Zagniedeni przyjaciele
Utworzenie struktury zagniedonej nie zapewnia jej automatycznie prawa dostpu do skadowych prywatnych. Aby to osign, naley postpi w szczeglny sposb: najpierw zadeklarowa (nie definiujc) struktur zagniedon, nastpnie zadeklarowa j, uywajc sowa kluczowego friend, a na koniec zdefiniowa struktur. Definicja struktury musi by oddzielona od deklaracji friend, bo w przeciwnym przypadku kompilator nie uznaby jej za skadow struktury. Poniej zamieszczono przykad takiego zagniedenia struktury:
/ / : C05:NestFriend.cpp // Zagniedeni "przyjaciele" #include <iostream> #include <cstring> // memset() using namespace std; const int sz = 20;

struct Holder { private: int a[sz]; public: void initialize();


struct Pointer; friend Pointer; struct Pointer {

Rozdzia 5. Ukrywanie implementacji private: Holder* h; int* p; public: void initialize(Holder* h); // Poruszanie si w obrbie tablicy: void next(); void previous(); void top(); void end(); // Dostp do wartoci: int read(); void set(int i);

217

void Holder::initialize() { memset(a, 0, sz * sizof(int)); void Holder::Pointer::initialize(Holder* rv) h = rv; p = rv->a; voi d Holdr::Poi ntr::next() { if(p < &(h->a[sz - 1])) p++; voi d Holdr::Poi ntr::previ ous() if(p > &(h->a[0])) p--; void Holder::Pointer::top() p = &(h->a[0]); void Holder::Pointer::end() p = &(h->a[sz - 1]); int Holder::Pointer::read() { rturn *p; } void Holder::Pointer::set(int i) {

int main() { Holdr h; Holdr::Pointer hp, hp2; int i; h.initializ(); hp.initialize(&h); hp2.initialize(&h);

218
for(i = 0; i < sz; i++) { hp.set(i); hp.next(); } hp.top(); hp2.end(); for(i = 0; i < sz; i++) { cout "hp = " hp.read() ", hp2 = " hp2.read() endl; hp.next(); hp2.previous();

Thinking in C++. Edycja polska

Po zadeklarowaniu struktury Pointer deklaracja:


friend Pointer;

zapewnia jej dostp do prywatnych skadowych struktury Holder. Struktura Holder zawiera tablic liczb cakowitych, do ktrych dostp jest moliwy wanie dziki strukturze Pointer. Poniewa struktura Pointerjest cile zwizana ze strukturHolder, rozsdne jest uczynienie z niej skadowej struktury Holder. Poniewa jednak Pointer stanowi oddzieln struktur w stosunku do struktury Holder, mona utworzy w funkcji main() wiksz liczb jej egzemplarzy, uywajc ich nastpnie do wyboru rnych fragmentw tablicy. Pointer jest struktur z niezwykrym wskanikiem jzyka C, gwarantuje wic zawsze poprawne wskazania w obrbie struktury Holder. Funkcja memset( ), wchodzca w skad standardowej biblioteki jzyka C (zadeklarowana w <cstring>), zostaa dla wygody wykorzystana w powyszym programie. Poczwszy od okrelonego adresu (bdcego pierwszym argumentem) wypenia ona capami odpowiedniawartoscia(podanajako drugi argument), wypeniajc n kolejnych bajtw (n stanowi trzeci argument). Oczywicie, mona przej przez kolejne adresy pamici, uywajc do tego ptli, ale funkcja memset()jest dostpna, starannie przetestowana (wic jest mao prawdopodobne, e powoduje bdy) i prawdopodobnie bardziej efektywna ni kod napisany samodzielnie.

Czy jest to czyste"?


Definicja klasy udostpnia dziennik nadzoru", dziki ktremu analizujc klas mona okreli funkcje majce prawo do modyfikacji jej prywatnych elementw. Jeeli funkcja zostaa zadeklarowana z uyciem sowa kluczowego friend, oznacza to, e nie jest ona funkcj skadow, ale mimo to chcemy da jej prawo do modyfikacji prywatnych danych. Musi ona widnie w definicji klasy, by wszyscy wiedzieli, e naley do funkcji uprzywilejowanych w taki wanie sposb. Jzyk C++ jest hybrydowym jzykiem obiektowym, a nie jzykiem czysto obiektowym. Sowo kluczowe friend zostao do niego dodane w celu pominicia problemw, ktre zdarzaj si w praktyce. Mona sformuowa zarzut, e czyni to jzyk mniej czystym". C++ zosta bowiem zaprojektowany w celu sprostania wymogowi uytecznoci, a nie po to, by aspirowa do miana abstrakcyjnego ideahi.

Rozdzia 5. Ukrywanie implementacji

219

Struktura pamici obiektw


W rozdziale 4. napisano, e struktura przygotowana dla kompilatora jzyka C, a nastpnie skompilowana za pomoc kompilatora C++, nie powinna ulec zmianie. Odnosi si to przede wszystkim do ukadu pamici obiektw tej struktury, to znaczy okrelenia, jak w pamici przydzielonej obiektowi rozmieszczone s poszczeglne zmienne, stanowice jego skadowe. Gdyby kompilator jzyka C++ zmienia ukad pamici strukturjzyka C, to nie dziaaby aden program napisany w C, ktry (co nie jest zalecane) wykorzystywaby informacj o rozmieszczeniu w pamici zmiennych tworzcych struktur. Rozpoczcie stosowania specyfikatorw dostpu zmieniajednak nieco posta rzeczy, przenoszc nas cakowicie w domenjzyka C++. W obrbie okrelonego bloku dostpu" (grupy deklaracji, ograniczonej specyfikatorami dostpu), gwarantowany jest zwarty ukad zmiennych w pamici, tak jak w jzyku C. Jednake poszczeglne bloki dostpu mog nie wystpowa w obiekcie w kolejnoci, w ktrej zostay zadeklarowane. Mimo e kompilator zazwyczaj umieszcza te bloki w pamici dokadnie w takiej kolejnoci, w jakiej s one widoczne w programie, nie obowizuje w tej kwestii adna regua. Niektre architektury komputerw i (lub) rodowiska systemw operacyjnych mog bowiem udziela jawnego wsparcia skadowym prywatnym i chronionym, co moe z kolei wymaga umieszczenia tych blokw w specjalnych obszarach pamici. Specyfikacjajzyka nie ma na celu ograniczenie moliwoci wykorzystania tego typu korzyci. Specyfikatory dostpu stanowi skadniki struktur i nie wpywaj na tworzone na ich podstawie obiekty. Wszelkie informacje dotyczce specyfikacji dostpu znikaj, zanim jeszcze program zostanie uruchomiony na og dzieje si to w czasie kompilacji. W dziaajcym programie obiekty staj si obszarami pamici" i niczym wicej. Jeeli naprawd tego chcesz, moesz zama wszelkie regufy, odwohijc si bezporednio do pamici, tak jak w jzyku C. Jzyka C++ nie zaprojektowano po to, by chroni ci przed popenianiem gupstw. Stanowi on jedynie znacznie atwiejsze i bardziej wartociowe rozwizanie alternatywne. Na ogl poleganie podczas pisania programu na czymkolwiek, co jest zalene od implementacji, nie jest dobrym pomysem. Jeeli musisz uy czego, co zaley od implementacji, zamknij to w obrbie struktury, dziki czemu zmiany zwizane z przenoszeniem programu bd skupione w jednym miejscu.

Klasy
Kontrola dostpu jest czsto okrelana mianem ukrywania implementacji. Umiesz1 czenie funkcji w strukturach (czsto nazywane kapsukowaniem ) tworzy typy danych, posiadajce zarwno cechy, jak i zachowanie. Jednake kontrola dostpu wyJakju wspomniano, kapsukowaniemjest rwnie czsto nazywana kontrola dostpu.

220

Thinking in C++. Edycja polska znacza ograniczenia w obrbie tych typw danych, wynikajce z dwch istotnych powodw. Po pierwsze, okrelaj one, co moe, a czego nie moe uywa klientprogramista. Mona wbudowa w struktur wewntrzne mechanizmy, nie martwic si o to, e klienci-programici uznaj te mechanizmy za cz interfejsu, ktrego powinni uywa. Prowadzi to bezporednio do drugiego z powodw, ktrymjest oddzielenie interfejsu od implementacji. Jeeli struktura jest uywana w wielu programach, lecz klienciprogramici mogjedynie wysya komunikaty do jej publicznego interfejsu, to mona w niej zmieni wszystko co jest prywatne, bez potrzeby zmiany kodu wykorzystujcych jprogramw. Kapsukowanie i kontrola dostpu, traktowane cznie, tworz co wicej ni struktury dostpne w jzyku C. Dziki nim wkraczamy do wiata programowania obiektowego, w ktrym struktury opisujklasy obiektw w taki sposob,jakbysmy opisywali klas ryb albo klas ptakw kady obiekt, nalecy do tych klas, bdzie posiada takie same cechy oraz rodzaje zachowa. Tym wanie staa si deklaracja struktury opisem, w jaki sposb wygldaj i funkcjonuj wszystkie obiekty jej typu. W pierwszymjzyku obiektowym, Simuli-67, sowo kluczowe class shiyo do opisu nowych typw danych. Najwyraniej zainspirowao to Stroustrupa do wyboru tego samego sowa kluczowego dla jzyka C++. wiadczy to o tym, e najwaniejsz cech caego jzyka jest tworzenie nowych typw danych, bdce czym wicej ni strukturami jzyka C zaopatrzonymi w funkcje. Z pewnoci wydaje si to wystarczajcym uzasadnieniem wprowadzenia nowego sowa kluczowego. Jednake sposb uycia sowa kluczowego class w jzyku C++ powoduje, e jest ono niemal niepotrzebne. Jest identyczne ze sowem kluczowym struct pod kadym wzgldem, z wyjtkiem jednego: skadowe klasy s domylnie prywatne, a skadowe struktury domylnie publiczne. Poniej przedstawiono dwie struktury, dajce takie same rezultaty:
/ / : C05:Class.cpp // Podobiestwo struktur i klas struct A { private: i nt i, j, k; public: int f(); void g(); }: int A : : f ( ) { return i + j + k;

void A : : g ( ) { i = j = k = 0;
// Taki sam rezultat uzyskuje si za pomoc:

Rozdzia 5. Ukrywanie implementacji class B { public:


}:

221

i nt i, j, k;
int f();

void g(); int B::f() { return i + j + k;


}

void B::g() {
i = j = k = 0; }

int main() {
A a; B b;

} ///:-

a.f(); a.g(); b.f(); b.g();

Klasa jest w jzyku C++ podstawowym pojciem zwizanym z programowaniem obiektowym. Jest jednym ze sw kluczowych, ktre nie zostay zaznaczone w ksice pogrubion czcionk byoby to irytujce w przypadku sowa powtarzanego tak czsto jak class". Przejcie do klas jest tak istotnym krokiem, e podejrzewam, i Stroustrup miaby ochot wyrzuci w ogle sowo kluczowe struct. Przeszkod stanowi jednak konieczno zachowania wstecznej zgodnoci jzyka C++ zjzykiem C. Wiele osb preferuje styl tworzenia klas bliszy strukturom ni klasom. Nie przywizujone wagi do domylnie prywatnego" zachowania klas, rozpoczynajc deklaracje klas od ich elementw publicznych: class X { public: void funkcja_interfejsu(); private: void funkcja_prywatna(); int wewnetrzna_reprezentacja;
}:

Przemawia za tym argument, e czytelnikowi takiego kodu wydaje si bardziej logiczne czytanie najpierw interesujcych go skadowych, a nastpnie pominicie wszystkiego, co zostao oznaczone jako prywatne. Faktycznie, wszystkie pozostae skadowe naley zadeklarowa w obrbie klasy jedynie dlatego, e kompilator musi zna wielkoci obiektw, by mg przydzieli im we waciwy sposb pami. Istotna jest take moliwo zagwarantowania spjnoci klasy. Jednak w przykadach wystpujcych w ksice skadowe prywatne bdznajdowafy si na pocztku deklaracji klasy,jak poniej: class X { void funkcja_prywatna(); int wewnetrzna_reprezentacja; public: void funkcja interfejsu();

222

Thinking in C++. Edycja polska

Niektrzy zadaj sobie nawet trad uzupeniania swoich prywatnych nazw:


class Y { public: void f(); private: int mX; // Uzupeniona nazwa }:

Poniewa zmienna mX jest ju ukryta w zasigu klasy Y, przedrostek m (od ang. member czonek, skadowa) jest niepotrzebny. Jednak w projektach o wielu zmiennych globalnych (czego naley unika, ale co w przypadku istniejcych projektw jest czasami nieuniknione) wan rol odgrywa moliwo odrnienia, ktre dane sdanymi globalnymi, a ktre skadowymi klasy.

Modyfikacja programu Stash, wykorzystujca kontrol dostpu


Modyfikacja programu z rozdziahi 4., dokonana w taki sposb, by uywa on klas oraz kontroli dostpu, wydaje si racjonalna. Zwr uwag nato, wjaki sposb cz interfejsu, przeznaczona dla klienta-programisty, zostaa obecnie wyranie wyrniona. Dziki temu nie istnieje ju moliwo, e przypadkowo bdzie on wykonywa operacje na nieodpowiedniej czci klasy:
/ / : C05:Stash.h // Zmieniony w celu wykorzystania kontroli dostpu #ifndef STASH_H #define STASH_H class Stash { int size; // Wielko kadego elementu int quantity; // Liczba elementw pamici int next; // Nastpny pusty lmnt // Dynamicznie przydzielana tablica bajtw: unsignd char* storag; void inflate(int incras); public: void initialize(int siz); void clanupO; int add(void* lmnt); void* fetch(int index); int count(); }: #ndif // STASH_H / / / : -

Funkcja inflate( ) zostaa okrelona jako prywatna, poniewa jest ona uywana wycznie przez funkcj add( ); stanowi zatem cz wewntrznego mechanizmu funkcjonowania klasy, a nie jej interfejsu. Oznacza to, e w przyszoci mona bdzie zmieni wewntrzn implementacj, uywajc innego systemu zarzdzania pamici. Powysza zawarto pliku nagwkowego jako jedyna pozajego nazw ulega modyfikacji w powyszym przykadzie. Zarwno plik zawierajcy implementacje,jak i plik testowy pozostafy takie same.

Rozdzia 5. Ukrywanie implementacji

223

Modyfikacja stosu, wykorzystujca kontrol dostpu


W drugim przykadzie w klas zostanie przeksztacony program tworzcy stos. Zagniedona struktura danych jest obecnie struktur prywatn, co wydaje si korzystne, poniewa gwarantuje, e klient-programista nigdy nie bdzie musia si jej przyglda ani nie uzaleni on swojego programu od wewntrznej reprezentacji klasy Stack: / / : C05:Stack2.h // Zagniedone struktury, tworzce list powizan #ifndef STACK2_H #define STACK2_H clss Stck { struct Link { void* dt; Link* next; void initialize(void* dat, Link* nxt); }* hed; public: void initilizeO; void push(void* dat); void* peek(); void* pop(); void cleanup(); #endif // STACK2_H / / / : Podobnie jak poprzednio, implementacja nie ulega w tym przypadku zmianie, nie zostaa wic w tym miejscu powtrnie przytoczona. Plik zawierajcy program testowy rwnie si nie zmieni. Zostaa jedynie zmodyfikowana moc, uzyskana dziki interfejsowi klasy. Istotn korzyci, wynikajc z kontroli dostpu, jest uniemoliwienie przekraczania granic podczas tworzenia programu. W rzeczywistoci jedynie kompilator posiada informacje dotyczce poziomu zabezpiecze poszczeglnych skadowych klasy. Nie istnieje adna informacja umoliwiajca kontrol dostpu, ktra byaby doczana do nazwy skadowej klasy, a nastpnie przekazywana programowi czcemu. Caa kontrola zabezpiecze jest dokonywana przez kompilator i nie zostaje przerwana w czasie wykonywania programu. Zwr uwag na to, e interfejs prezentowany klientowi-programicie rzeczywicie odpowiada teraz rozwijanemu w d stosowi. Jest on obecnie zaimplementowany w postaci powizanej listy, lecz mona to zmieni, nie modyfikujc elementw wykorzystywanych przez klienta-programist, a zatem (co waniejsze) rwnie anijednego wiersza napisanego przez niego kodu.
}:

Klasy-uchwyty
Kontrola dostpu w jzyku C++ pozwala na oddzielenie interfejsu od implementacji, jednak ukrycie implementacji jest tylko czciowe. Kompilator musi nadal widzie deklaracje wszystkich elementw obiektu po to, by mg poprawnie je tworzy i odpowiednio

224

Thinking in C++. Edycja polska obsugiwa. Mona wyobrazi sobie jzyk programowania, ktry wymagaby okrelenia jedynie publicznego interfejsu obiektu, pozwalajc na ukrycie jego prywatnej implementacji. Jednake jzyk C++ dokonuje kontroli typw statycznie (w czasie kompilacji), zawsze gdy jest to tylko moliwe. Oznacza to, e programista jest powiadamiany o bdach moliwie jak najszybciej, a take to, e program jest bardziej efektywny. Jednak doczenie prywatnej implementacji pociga za sob dwa skutki implementacja jest widoczna, nawet jeeli nie ma do niej atwego dostpu, a ponadto moe ona wywofywa niepotrzebnie powtrnkompilacj programu.

Ukrywanie implementacji
W przypadku niektrych projektw nie wolno dopuci do tego, by ich implementacja bya widoczna dla klienta-programisty. Plik nagwkowy biblioteki moe zawiera informacje o znaczeniu strategicznym, ktrych firma nie zamierza udostpnia konkurentom. By moe pracujesz nad systemem, w ktrym istotn kwesti stanowi bezpieczestwo na przykad algorytm szyfrowania i nie chcesz umieszcza w pliku nagwkowym informacji, ktre mogfyby uatwi zamanie kodu. Albo zamierzasz umieci swoj bibliotek we wrogim" rodowisku, w ktrym programici i tak bd odworywa si do prywatnych skadowych klasy wykorzystujc wskaniki i rzutowanie. We wszystkich takich przypadkach lepiej skompilowa rzeczywist struktur klasy wewntrz pliku, zawierajcegojej implementacj, ni ujawniajw pliku nagwkowym.

Ograniczanie powtrnych kompilacji


Meneder projektu, dostpny w uywanym przez ciebie rodowisku programistycznym, spowoduje powtrnkompilacj kadego pliku, jeli zosta on zmodyfikowany, lub jeeli zosta zmieniony plik, od ktrego jest on zaleny czyli doczony do niego plik nagwkowy. Oznacza to, e ilekro dokonywana jest zmiana dotyczca klasy (niezalenie od tego, czy dotyczy ona deklaracji jej publicznego interfejsu, czy te skadowych prywatnych), jeste zmuszony do powtrnej kompilacji wszystkich plikw, do ktrych doczony jest plik nagwkowy tej klasy. Czsto jest to okrelane mianemproblemu wraliwej klasypodstawowej. W przypadku wczesnych etapw realizacji duych projektw moe to by irytujce, poniewa wewntrzna implementacja podlega czstym zmianom gdy projekt taki jest bardzo obszerny, czas potrzebny na kompilacje niekiedy uniemoliwia szybkie wprowadzanie w nim zmian. Technika rozwizujca ten problem nazywana jest czasami klasami-uchwytami (ang. 2 handle classes} lub kotem z Cheshire" wszystko, co dotyczy implementacji znika i pozostaje tylko pojedynczy wskanik umiech". Wskanik odnosi si do struktury, ktrej definicja znajduje si w pliku zawierajcym implementacj, wraz z wszystkimi definicjami funkcji skadowych. Tak wic dopki nie zmieni si interfejs, dopty plik nagwkowy pozostaje niezmieniony. Implementacja moe by dowolnie zmieniana w kadej chwili, powodujc jedynie konieczno ponownej kompilacji i powtrnego poczenia z projektem pliku zawierajcego implementacj. Nazwa tajest przypisywana Johnowi Carolanowi,jednemu z pionierw programowania w C++ i, oczywicie, Lewisowi Carollowi. Monajrwnie postrzegajako form pomostowego" wzorca projektowego, opisanego w drugim tomie ksiki.

Rozdzia 5. Ukrywanie implementacji

225

Poniej zamieszczono prosty przykad, demonstrujcy wykorzystanie tej techniki. Plik nagwkowy zawiera wycznie publiczny interfejs klasy oraz wskanik do (nie w peni okrelonej) klasy: / / : C05:Handle.h // Klasy-uchwyty #ifndef HANDLE_H #define HANDLE_H class Handle { struct Cheshire; // Tylko deklaracja klasy Cheshire* smile; public: void initialize(); void cleanup(); int read(); void change(int); #endif // HANDLE_H / / / : To wszystko, co widzi klient-programista. Wiersz: struct Cheshire; stanowi niepen specyfikacj typu albo deklaracj klasy {definicja klasy zawieraaby jej ciao). Informuje ona kompilator, e Cheshire jest nazw struktury, lecz nie dostarcza adnych szczegw najej temat. Informacja wystarczajedynie do utworzenia wskanika do tej struktury nie mona utworzy obiektu, dopki nie zostanie udostpnione jej ciao. W przypadku zastosowania tej techniki ciao to jest ukryte w pliku zawierajcym implementacj: / / : C05:Handle.cpp {0} // Implementacja uchwytu #include "Handle.h" #include "../require.h" // Definicja implementacji uchwytu: struct Handle::Cheshire {
int i;
}:

void Handle::initialize() smile = new Cheshire; smile->i = 0; void Handle::cleanup() delete smile; int Handle::read() return smile->i; void Handle::change(int x) smile->i = x;

226

Thinking in C++. Edycja polska

Cheshire jest struktur zagniedon, musi wic ona zosta zdefiniowana w zasigu klasy:
struct Handle::Cheshire {

W funkcji Handle::initialize() strukturze Cheshire przydzielanajest pami, ktra jest pniej zwalniana przez funkcj Handle::cleanup(). Pami ta jest uywana zamiast wszystkich elementw danych, ktre s zazwyczaj umieszczane w prywatnej czci klasy. Po skompilowaniu pliku Handle.cpp definicja tej struktury zostaje ukryta w pliku wynikowym i nie jest ona dla nikogo widoczna. Jeeli nastpuje zmiana elementw struktury Ceshire, to jedynym plikiem, ktry musi zosta powtrnie skompilowany, jest Handle.cpp, poniewa plik nagwkowy pozostanie niezmieniony. Sposb uycia klasy Handle przypomina wykorzystywanie kadej innej klasy naley doczyjej plik nagwkowy, utworzy obiekty i wysya do nich komunikaty:
/ / : C05:UseHandle.cpp / / { L } Handle // Uywanie klasy-uchwytu #include "Handle.h" int main() { Handle u; u.initialize(); u.read(); u.change(l); u.cleanup();

Jedynrzecz, do ktrej ma dostp klient,jest publiczny interfejs klasy. Dopki wic zmienia si jedynie jej implementacja, powyszy plik nie bdzie nigdy wymaga powtrnej kompilacji. Tak wic, mimo e nie jest to doskonafy sposb ukrycia implementacji, stanowi on w tej dziedzinie ogromny krok naprzd.

Podsumowanie
Kontrola dostpu w jzyku C++ zapewnia programicie cisy nadzr nad utworzon przez siebie klas. Uytkownicy klasy widz w przejrzysty sposb, czego moguywa, a co powinni zignorowa. Jeszcze waniejszajest moliwo gwarancji, e aden klient-programista nie bdzie uzaleniony od jakiejkolwiek czci kodu tworzcego wewntrzn implementacj klasy. Dziki temu twrca klasy moe zmieni wewntrzn implementacj, wiedzc e aden z klientw-programistw nie zostanie zmuszony do wprowadzania jakichkolwiek modyfikacji w swoim programie, poniewa nie ma on dostpu do tej czci klasy. Majc moliwo zmiany wewntrznej implementacji, mona nie tylko udoskonali swj projekt, ale rwnie pozwoli sobie na popenianie bdw. Bez wzgldu na to, jak skrupulatnie zaplanuje si wszystko i wykona, i tak dojdzie do pomyek. Wiedza, e popenianie takich bdw jest stosunkowo bezpieczne, umoliwia eksperymentowanie, efektywniejsznauk i szybsze zakoczenie projektu.

Rozdzia 5. Ukrywanie implementacji

227

Publiczny interfejs klasy jest tym, co widzi klient-programista, naley wic we waciwy sposb przemyle go w trakcie analizy i projektowania. Lecz nawet on pozostawia pewn moliwo wprowadzania zmian. Jeeli posta interfejsu nie zostanie od razu gruntownie przemylana, mona uzupeni go o nowe funkcje, pod warunkiem, e nie zostanz niego usunite te spord funkcji, ktre zostayju uyte przez klientw-programistw.

wiczenia
Rozwizania wybranych wicze znajduj si w dokumencie elektronicznym: The Thinking in C++ AnnotatedSolution Guide, ktry mona pobra za niewielkopat z witryny http://www.BruceEckel.com. 1. Utwrz klas posiadajc dane skadowe publiczne, prywatne oraz chronione. Utwrz obiekt tej klasy i zobacz,jakie komunikaty kompilatora uzyskasz, prbujc odwoa si do wszystkich danych skadowych klasy. 2. Utwrz struktur o nazwie Lib, zawierajctrzy obiekty bdce acuchami (string): a, b oraz c. W funkcji main( ) utwrz obiekt o nazwie x i przypisz wartoci skadowym x.a, x.b oraz x.c. Wydrukuj te wartoci. Nastpnie zastp skadowe a, b i c tablic, zdefiniowanjako string s[3]. Zauwa, e w rezultacie dokonanej zmiany przestanie dziaa kod, zawarty w funkcji main( ). Teraz utwrz klas o nazwie Libc, zawierajcprywatne skadowe, bdce acuchami a, b i c, a take funkcje skadowe seta( ), geta( ), setb( ), getb( ), setc( ) i getc( ), umoliwiajce ustawianie i pobieranie wartoci skadowych. W podobny sposb jak poprzednio napisz funkcj main( ). Teraz zastp prywatne skadowe a, b i c, prywatntablicstring s[3]. Zauwa, e mimo dokonanych zmian, kod zawarty w funkcji main( ) nie przesta dziaa poprawnie. 3. Utwrz klas i globalnfunkcj, bdcjej przyjacielem", operujc na prywatnych danych tej klasy. 4. Utwrz dwie klasy, tak by kada z nich posiadaa funkcj skadow, przyjmujcwskanik do obiektu drugiej klasy. W funkcji main( ) utwrz egzemplarze obu obiektw i wywoaj w kadym z nich wymienione wczeniej funkcje skadowe. 5. Utwrz trzy klasy. Pierwsza z nich powinna zawiera dane prywatne, a take wskazajako swoich ,,przyjaciol" cadrugklas oraz funkcj skadow trzeciej klasy. Zademonstruj w funkcji main( ), e wszystko dziaa poprawnie. 6. Utwrz klas Hen. Umie wewntrz niej klas Nest. Wewntrz klasy Nest ulokuj klas Egg. Kada z klas powinna posiada funkcj skadowdisplay( ). W funkcji main( ) utwrz obiekty kadej z klas i wywolaj dla kadego z nich funkcj display( ). 7. Zmodyfikuj poprzednie wiczenie w taki sposb, aby klasy Nest i Egg zawieray dane prywatne. Okrel przyjaci" tych klas, tak aby do ich danych prywatnych miay dostp klasy, w ktrych sone zagniedone.

228

Thinking in C++. Edycja polska

8. Utwrz klas, ktrej dane skadowe zawarte bd w regionach: publicznym, prywatnym i chronionym. Dodaj do klasy funkcj skadow showMap( ), drukujcnazwy oraz adresy kadej z tych danych skadowych. Jeeli to moliwe, skompiluj i uruchom program, uywajc rnych kompilatorw, komputerw i systemw operacyjnych. Obserwuj, czy zmienia si ukad danych skadowych w pamici. 9. Skopiuj pliki zawierajce implementacje i testy programu Stash, zawartego w rozdziale 4., tak abyje skompilowa i uruchomi razem z zawartym w biecym rozdziale plikiem Stash.h. 10. Zapamitaj obiekty klasy Hen, utworzonej w 6. wiczeniu, uywajc do tego klasy Stash. Pobierzje ponownie, anastpnieje wydrukuj Oeelijeszcze nie zostao to wykonane, naley utworzy funkcj skladowaHen::print( )). 11. Skopiuj pliki zawierajce implementacje i testy programu Stack, zawartego w rozdziale 4., tak abyje skompilowa i uruchomi razem z zawartym w biecym rozdziale plikiem Stack2.h. 12. Zapamitaj obiekty klasy Hen, utworzonej w 6. wiczeniu, uywajc do tego klasy Stack. Pobierzje z powrotem ze stosu, a nastpnie wydrukuj ^eelijeszcze nie zostao to wykonane, naley utworzy funkcj skladowaHen::print( )). 13. Zmodyfikuj struktur Cheshire, zawart w pliku Handle.cpp, i sprawd, czy twj meneder powtrnie skompiluje i poczyjedynie ten plik, nie kompilujc ponownie pliku UseHandle.cpp. 14. Utwrz klas StackOflnt (stos przechowujcy wartoci cakowite) w klasie o nazwie StackImp. Uyj do tego celu techniki kota z Cheshire", ukrywajcej niskopoziomowe struktury danych, stosowane do przechowywania elementw. Zaimplementuj dwie wersje klasy StackImp wykorzystujctablic liczb cakowitych o starym rozmiarze i stosujctyp vector<int>. Ustaw maksymaln wielko stosu, by nie uwzgldnia powikszania tablicy w pierwszym z tych przypadkw. Zwr uwag na to, e opis klasy, zawarty w pliku StackOflnt.h, nie zmienia si podczas dokonywania zmian w klasie StackImp.

lnicjalizacja i kocowe porzdki


W rozdziale 4. osignlimy znaczcy postp w zakresie uywania bibliotek, czc rozproszone komponenty typowej biblioteki jzyka C i zamykajc je w strukturze (abstrakcyjnym typie danych, nazywanym klas). Dziki temu nie tylko jest udostpniane jedyne, ujednolicone wejcie do komponentu biblioteki, ale ukrywa si rwnie nazwy poszczeglnych funkcji w obrbie nazwy klasy. W rozdziale 5. zostao wprowadzone pojcie kontroli dostpu (ukrywania implementacji). Umoliwia ona projektantowi klasy zdefiniowanie wyranych granic, okrelajcych, czym moe si posugiwa klient-programista, a co znajduje si poza jego zasigiem. Oznacza to, e wewntrzne mechanizmy dziaania typu danych pozostaj pod kontrol projektanta klasy i ich posta jest zalena od jego woli, natomiast dla klientw-programistw jest zupenie oczywiste, na ktre skadowe klasy mog i powinni oni zwrci uwag. Kapsukowanie wraz z kontrol dostpu stanowi istotny krok na drodze do uatwie w dziedzinie wykorzystywania bibliotek. Tworzone przez nie pojcie nowego typu danych" jest pod pewnymi wzgldami lepsze ni wbudowane typy danych, dostpne wjzyku C. Kompilatorjzyka C++ moe obecnie zapewni kontrol typw w odniesieniu do takich nowych typw danych", gwarantujc zarazem bezpieczestwo podczas ich wykorzystywania. W sprawach dotyczcych bezpieczestwa kompilatorjzyka C++ zapewnia znacznie wiksze moliwoci ni jzyk C. W tym i w nastpnych rozdziaach zostan przedstawione dodatkowe cechy jzyka C++, dziki ktrym bdy zawarte w kodzie niemal samoczynnie si ujawniaj czasami nawet zanim jeszcze skompiluje si program, lecz najczciej w postaci komunikatw o bdach i ostrzee kompilatora. Dziki temu ju wkrtce przyzwyczaisz si do nieprawdopodobnego scenariusza, wedug ktrego program w jzyku C++, ktry si kompiluje, czsto od razu dziaa poprawnie. Dwoma kwestiami zwizanymi z bezpieczestwem s: inicjalizacja i sprztanie". Przyczyn licznych bdw wjzyku C byo to, e programista zapomnia o inicjalizacji

Rozdzia 6.

230

Thinking in C++. Edycja polska

zmiennej lub o przeprowadzeniu kocowych porzdkw". Zdarza si to szczeglnie w przypadku bibliotekjzyka C, gdy klient-programista nie wie, w j a k i sposb zainicjalizowa struktur, albo nawet nie jest wiadomy tego, e powinien to zrobi (biblioteki czsto nie zawieraj funkcji inicjalizujcych, co sprawia, e klientprogramista jest zmuszony do samodzielnego inicjalizowania struktur). Kocowe porzdki stanowi szczeglny problem, poniewa programici jzyka C s przyzwyczajeni do zapominania o zmiennych, ktre ju nie istniej. W zwizku z tym wszelkie sprztanie, ktre mogoby by konieczne w przypadku zawartych w bibliotekach struktur, jest czsto pomijane. W jzyku C++ pojcia inicjalizacji i sprztania s niezbdnym elementem atwego korzystania z bibliotek oraz sposobem eliminacji wielu trudno uchwytnych bdw, wystpujcych wwczas, gdy klient-programista zapomni o wykonaniu tych dziaa. W rozdziale omwiono waciwoci jzyka C++, pomocne w zapewnieniu odpowiedniej inicjalizacji i sprztania.

Konstruktor gwarantuje inicjalizacj


Klasy Stash i Stack, zdefiniowane w poprzednim rozdziale, posiadaj funkcj initialize( ), ktr, jak wskazuje na to jej nazwa, naley wywoa, zanim w jakikolwiek sposb zostanie uyty obiekt. Niestety, oznacza to, e waciw inicjalizacj musi w tym przypadku zapewni klient-programista. Jednake klienci-programici, dc do rozwizania swoich problemw za pomoc twojej fantastycznej biblioteki, maj tendencj do zapominania o takich drobiazgach, jak inicjalizacja. W jzyku C++ inicjalizacjajest zbyt istotna, by pozostawi j klientowi-programicie. Projektant klasy moe zapewni inicjalizacj kadego obiektu, dostarczajc specjaln funkcj, nazywan konstruktorem. Jeeli klasa posiada konstruktor, kompilator automatycznie wywouje go w miejscu, w ktrym tworzony jest obiekt, zanim jeszcze klientprogramista bdzie mg podj jakiekolwiek dziaania z nim zwizane. Wywoanie konstruktora nie zaley w aden sposb od woli klienta-programisty jest dokonywane przez kompilator w miejscu, w ktrym definiowanyjest obiekt. Kolejnym problememjest wybr nazwy tej funkcji. Wisi z tym dwie kwestie. Po pierwsze, kada nazwa moe potencjalnie kolidowa z nazwami, uywanymi w charakterze skadowych klas. Po drugie, skoro kompilator jest odpowiedzialny za wywoanie konstruktora, to zawsze musi wiedzie, ktr funkcj powinien wywoa. Rozwizanie wybrane przez Stroustrupa wydaje si najprostsze i najbardziej logiczne nazwa konstruktorajest taka sama, jak nazwa klasy. Logicznejest, e taka funkcja zostanie wywoana automatycznie w czasie inicjalizacji. Poniej zamieszczono przykad prostej klasy posiadajcej konstruktor:
class X {

int i; public: X ( ) ; // Konstruktor

Rozdzia 6. lnicjalizacja i kocowe porzdki

231

Obecnie, gdy obiektjest definiowany:


void f() { X a;

zachodzi to samo, co w przypadku gdyby zmienna a bya liczb cakowit obiektowi przydzielanajest pamJ. Lecz kiedy program dochodzi do punktu sekwencyjnego (miejsca wykonania), w ktrym zdefiniowana jest zmienna a, nastpuje automatyczne wywoywanie konstruktora. Oznacza to, e w miejscu definicji kompilator po cichu" wstawia wywoanie funkcji X::X( ) dla obiektu a. Podobnie jak w przypadku kadej funkcji~skladowej, pierwszym (niewidocznym) argumentem konstruktorajest wskanik this adres obiektu, dla ktrego zosta on wywoany. Jednak w przypadku konstruktora wskanik this wskazuje na niezainicjowany blok pamici, a jego prawidowa inicjalizacjajest wanie zadaniem konstruktora. Podobnie jak w przypadku kadej innej funkcji, konstruktor moe posiada argumenty penice rozmaite funkcje: umoliwiajce okrelenie, w jaki sposb tworzony jest obiekt, przekazujce wartoci inicjujce itd. Argumenty konstruktora s sposobem gwarantujcym, e wszystkie elementy obiektu s inicjowane odpowiednimi wartociami. Na przykadjeeli klasa Tree (drzewo) zawiera konstruktor, pobierajcy pojedynczy argument cakowity, oznaczajcy wysoko drzewa, to jej obiekt musi zosta utworzony w nastpujcy sposb:
Tree t(12); // 12-metrowe drzewo

Jeeli Tree(int) jest jedynym konstruktorem, kompilator nie dopuci do utworzenia obiektu w aden inny sposb (w nastpnym rozdziale zostan przedstawione wielokrotne konstruktory, a take rne sposoby wywoania konstruktorw). Oto caa wiedza o konstruktorach s to funkcje o specjalnych nazwach, wywoywane automatycznie przez kompilatordla kadego obiektu, w miejscujego tworzenia. Pomimo swojej prostoty, s one wyjtkowo cenne, pozwalaj bowiem na wyeliminowanie duej klasy problemw, powodujc rwnoczenie, e kodjest atwiejszy zarwno do napisania, jak i do przeczytania. Na przykad w przedstawionym powyej fragmencie kodu nie sposb dostrzec jawnego wywoania adnej funkcji initialize( ), ktre byoby oderwane pojciowo od inicjalizacji. Wjzyku C++ definicja i inicjalizacja spojciami poczonymi ze sob nie mona uywajednego z nich w oderwaniu od drugiego. Zarwno konstruktor, jak i destruktor s osobliwymi rodzajami funkcji nie zwracaj one adnej wartoci. Jest to czym zupenie innym ni zwracanie wartoci typu void; w tym przypadku funkcja co prawda niczego nie zwraca, ale zawsze istnieje moliwo zmiany tej sytuacji. Konstruktory i destruktory nie zwracaj niczego i nie mona tego zmieni. Dziaania polegajce na umieszczeniu obiektu w programie, a pniej jego usuniciu s czym szczeglnym, podobnie jak narodziny i mier, i kompilator zawsze sam wywouje funkcje konstruktora i destruktora, by mie pewno, e zostay one wykonane. Gdyby zwracay one jak warto i mona byo j dowolnie wybra, to kompilator musiaby skd wiedzie, co ma zrobi z t wartoci; albo te klient-programista byby zmuszony dojawnego wywoywania konstruktorw i destruktorw, co z kolei wyeliminowaoby zwizane z nimi bezpieczestwo.

32

Thinking in C++. Edycja polska

Destruktor gwarantuje sprztanie


Jako programista jzyka C pewnie czsto zastanawiae si nad tym, jak wana jest inicjalizacja, mniej natomiast interesowao ci sprztanie. W kocu, na czym miaoby polega sprztanie po liczbie cakowitej? mona o tym zapomnie! Jednake w przypadku bibliotek porzucenie" niepotrzebnego ju obiektu nie jest tak bezpieczne. Co si stanie, jeeli zmieni on stanjakiego elementu sprztowego, narysowa co na ekranie lub przydzieli pami na stercie? Jeeli o tym zapomnisz, usuwany obiekt nigdy nie zostanie w peni zamknity. W jzyku C++ sprztanie jest rwnie wane, jak inicjalizacja, i dlatego waniejest ono gwarantowane przez destruktor. Skadnia destruktora jest podobna do skadni konstruktora w charakterze nazwy funkcji uywanajest nazwa klasy. Jednake destruktor rni si od konstruktora tym, e jego nazw poprzedza znak tyldy (~). W dodatku destruktory nie wymagaj nigdy adnych argumentw. Poniej przedstawiono deklaracj destruktora:
class Y ( public: ~Y();

Destruktor jest wywoywany przez kompilator automatycznie, gdy koczy si zasig obiektu. O ile miejsce wywoania konstruktorajest widoczne Qest nim miejsce definicji obiektu), o tyle jedynym wiadectwem wywoania destruktora jest nawias klamrowy, zamykajcy zasig, w ktrym znajduje si obiekt. Mimo to destruktor jest wywoywany, nawet jeeli uyje si instrukcji goto, wychodzc z zasigu (instrukcja goto istnieje nadal wjzyku C++ w celu zapewnienia wstecznej zgodnoci zjzykiem C, a take z tego powodu, e zdarzaj si sytuacje, w ktrych okazuje si ona przydatna). Naley pamita o tym, e instrukcja dalekiego goto, zaimplementowana w postaci funkcji setjmp() oraz longjmp(), zawartych w standardowej bibliotece funkcji jzyka C, nie powoduje wywoania destruktorw (tak gosi jej specyfikacja nawet jeeli uywany przez ciebie kompilator wywouje w takim przypadku destruktory, to poleganie na waciwoci, ktra nie zostaa uwzgldniona w specyfikacji, oznacza, e napisany w taki sposb programjest nieprzenony). Poniej zamieszczono przykadowy program, demonstrujcy przedstawione do tej pory waciwoci konstruktorw i destruktorw:
/ / : C06:Constructorl.cpp // Konstruktory i destruktory finclude <iostream> using namespace std:

class Tree { int height; public: Tree(int initialHeight); // Konstruktor ~Tree(); // Destruktor
void grow(int years); void printsize();

Rozdzia 6. Incjalizacja i kocowe porzdki

233

Tree::Tree(int initialHeight) height - initialHeight; Tree: :~TreeO { cout "wewntrz destruktora drzewa" endl; printsize(); void Tree::grow(int years) { height += years: voidTree::printsizeC) { cout "Wysokosc drzewa wynosi " height endl; int main() { cout "przed klamrowym nawiasem otwierajcym" endl; { Tree t(12);

cout "po utworzeniu drzewa" endl; t.printsize(); t.grow(4); cout "przed klamrowym nawiasem zamykajcym" endl;

} cout "po klamrowym nawiasie zamykajcym" endl;


A oto wyniki dziaania powyszego programu:
przed klamrowym nawiasem otwierajcym po utworzeniu drzewa Wysokosac drzewa wynosi 12 przed klamrowym nawiasem zamykajcym wewntrz destruktora drzewa Wysokosc drzewa wynosi 16 po klamrowym nawiasie zamykajcym

Destruktor jest zatem wywoywany automatycznie w miejscu klamrowego nawiasu, zamykajcego zasig, w ktrym zdefiniowano obiekt.

Eliminacja bloku definicji


W jzyku C wszystkie zmienne musiaty by zawsze zdefiniowane na pocztku bloku, po klamrowym nawiasie otwierajcym. Nie jest to jakie wyjtkowe wymaganie wrd jzykw programowania;jakojego uzasadnienie czsto podaje si argument, ejest to dobry styl programowania". Autor niniejszej ksiki ma jednak co do tego wtpliwoci. Jako programista zawsze uwaaem, e jest niewygodne przeskakiwanie do pocztku bloku po to, by utworzy jak now zmienn. Zauwayem rwnie, e kod programu jest bardziej czytelny, gdy definicje zmiennych znajduj si blisko miejsca, w ktrym s uywane.

:34

Thinking in C++. Edycja polska By moe s to argumenty stylistyczne. Jednak w jzyku C++ wystpuje powany problem, zwizany z definicj wszystkich zmiennych na pocztku zasigu. Jeeli istnieje konstruktor, to musi on zosta wywoany podczas tworzenia obiektu. Jednak, jeeli ten konstruktor przyjmujejeden lub wicej argumentw, sucych do inicjalizacji, to skd wiadomo, e informacja potrzebna do inicjalizacji bdzie znana ju na pocztku zasigu? Na og nie bdzie ona znana. Poniewa w jzyku C nie wystpuje pojcie prywatnoci, oddzielenie definicji i inicjalizacji nie stanowi problemu. Jednak jzyk C++ gwarantuje, e tworzony obiekt jest rwnoczenie inicjalizowany. Dziki temu w systemie nie ma nigdy adnych niezainicjowanych obiektw. Jzyk C nie zwraca na to uwagi co wicej, wspiera praktyk tworzenia niezainicjowanych zmiennych, wymagajc zdefiniowania ich na pocztku bloku, zanimjeszcze dostpna jest informacja niezbdna do ich inicjalizacji'. Generalnie jzyk C++ nie pozwala na utworzenie obiektu, zanim nie zostan przygotowane informacje, potrzebne konstruktorowi do jego inicjalizacji. Z uwagi na to jzyk, ktry wymuszaby definicje zmiennych na pocztku zasigu, nie byby moliwy do zrealizowania. W rzeczywistoci styl jzyka wydaje si zachca do definiowania obiektw w miejscu moliwie bliskim ich uycia. W C++ kada regua dotyczca obiektw" automatycznie odnosi si rwnie do obiektw wbudowanych typw. Oznacza to, e zarwno obiekty dowolnego typu, jak i zmienne wbudowanych typw, mog by definiowane w dowolnym miejscu zasigu. Ponadto z definicjami zmiennych mona poczeka do miejsca, w ktrym bd dostpne inicjujce je wartoci, dziki czemu zawsze bdzieje mona rwnoczenie definiowa i inicjowa: / / : C06:DefineInitialize.cpp // Definiowanie zmiennych w dowolnych miejscach #include "../require.h" #include <iostream> #include <sthng>

using namespace std;


int i;

class G { public: G(int ii);

G::G(int ii) { i = ii; } int main() { cout "wartosc inicjujca? "; int retval = 0; cin retval; require(retval != 0); int y = retval + 3;
} G g(y); III-

Wykonywany jest zatem pewien kod, po czym jest definiowana i inicjalizowana zmienna retval, nastpnie wykorzystywana do pobrania wartoci wpisanej przez Jezyk C99, zaktualizowana wersja standardu C, pozwala na definicje zmiennych w dowolnym miejscu zasigu, podobnie jak C++.

Rozdzia 6. lnicjalizacja i kocowe porzdki

235

uytkownika. W dalszej kolejnoci definiowane szmienne y oraz g. Z drugiej strony jzyk C nie pozwala na definiowanie zmiennych w jakimkolwiek innym miejscu ni na pocztku zasigu. Generalnie naley definiowa zmienne moliwie jak najbliej miejsca ich wykorzystania i zawsze inicjalizowa je podczas definicji (w przypadku typw wbudowanych, ktrych inicjalizacjajest opcjonalna, tojedynie sugestia stylistyczna). Kwestia ta wie si rwnie z bezpieczestwem. Ograniczajc obszar, w ktrym zmienna jest dostpna w zasigu, zmniejsza si prawdopodobiestwo jej nieprawidowego uycia wjakim innym miejscu zasigu. Dodatkowo uzyskuje si wikszczytelno kodu, poniewa nie trzeba ju przeskakiwa do pocztku zasigu i z powrotem, by dowiedzie si, jakiego typu jest zmienna.

Ptle for
W jzyku C++ czsto wystpuje licznik ptli for, zdefiniowany bezporednio w wyraeniu ptli: f o r ( i n t j - 0; j < 100; j++) { cout "j - " j endl; } for(int i = 0; i < 100; i++) cout "1 = " i endl; Powysze wyraenia s wanymi przypadkami szczeglnymi, wprawiajcymi czsto w zakopotanie pocztkujcych programistw C++. Zmienne i oraz j zostay zdefiniowane bezporednio w wyraeniu ptli for (czego nie mona wykona w jzyku C), mona wic ich uy w ptli for. To bardzo wygodna konstrukcja, poniewa kontekst sprawia, e rozwiewaj si wszelkie wtpliwoci dotyczce przeznaczenia zmiennych i i j; nie ma wic potrzeby uywania tak niezgrabnych nazw, jak i_licznik_petli. Moesz si jednak rozczarowa, jeli sdzisz, e czas ycia zmiennych i i j wykracza poza zasig ptli for jest on ograniczony zasigiem ptli 2 . W rozdziale 3. wspomniano, e instrukcje while i switch rwnie pozwalaj na definiowanie obiektw w obrbie swoich wyrae kontrolnych, chocia takie ich zastosowanie wydaje si znacznie mniej istotne ni w przypadku ptli for. Naley zwrci uwag na zmienne lokalne, ktre zasaniaj zmienne znajdujce si w bardziej zewntrznym zasigu. Na og uycie tych samych nazw dla zmiennej zagniedonej i zmiennej globalnej w stosunku do jej zasigu jest mylce i moe by 3 przyczyn bdw . Wczeniejsza wersja projektu standardujzyka C++ gosia, e czas ycia tej zmiennej rozciga si do koca zasigu, w ktrym zawartajest ptla for. Niektre kompilatory nadal dziaaj w taki sposb, ale nie jest to poprawne. Dlatego te twj kod bdzie przenony tylko wwczas, gdy ograniczysz zasig zmiennej do ptli for. Jzyk Java uznaje to za na tyle zy pomys, e oznacza taki kod jako bdny.

Thinking in C++. Edycja polska Autor ksiki zauway, e niewielkie zasigi s cech dobrych projektw. Jeeli kod pojedynczej funkcji zajmuje wiele stron, to by moe zakres jej dziaania jest zbyt duy. Bardziej rozdrobnione funkcje s nie tylko bardziej uyteczne, ale atwiej w nich rwnie znale bdy.

>rzydzielanie pamici
Zmienne mog by obecnie definiowane w dowolnym miejscu zasigu. Wydawaoby si wiec, e rwnie ich pami moe nie by okrelona, a do miejsca wystpienia definicji. Obecnie jest jednak bardziej prawdopodobne, e kompilator zadziaa zgodnie z praktyk stosowan w jzyku C, przydzielajc ca pami dla zasigu w miejscu, w ktrym znajduje si kwadratowy nawias otwierajcy, rozpoczynajcy ten zasig. Nie ma to adnego znaczenia, poniewa programista i tak nie ma dostpu do tego obszaru pamici (znanego 4 rwnie pod nazwobiektu), dopki nie zostanie on zdefiniowany . Mimo e pamijest przydzielana na pocztku bloku, wywoanie konstruktora nastpuje dopiero w punkcie sekwencyjnym, w ktrym definiowanyjest obiekt, poniewajego identyfikator niejest wczeniej dostpny. Kompilator sprawdza nawet, czy definicja obiektu (a zatem i wywoanie konstruktora) nie zostaa umieszczona w miejscu, w ktrym sterowanie tylko warunkowo przechodzi przez punkt sekwencyjny, jak na przykad w instrukcji switch lub w takim, ktre moe przeskoczy instrukcja goto. Usunicie komentarzy, w ktrych umieszczone sinstrukcje poniszego programu, spowoduje zgoszenie ostrzeenia lub bdu:

// N1e mona przeskakiwa konstruktorw class X { public: X();


X::X() {}

//: C06:Nojump.cpp

void f(int i) { if(i < 10) { //! goto jumpl; // Bd: goto pomija inicjalizacj } X xl: // Wywoanie konstruktora jumpl: switch(i) { case 1 : X x2: // Wywoanie konstruktora Preak: //! case 2 : // Bd: case pomija inicjalizacj X x3; // Wywoanie konstruktora
break:

int main() f(9): f(ll):

W porzdku mona by to zrobi, bawic si wskanikami, ale byoby to niezwykle nierozsdne.

Rozdzia 6. lnicjalizacja i kocowe porzdki

237

W powyszym kodzie zarwno instrukcja goto, jak i switch mog potencjalnie przeskoczy przez punkt sekwencyjny, w ktrym wywoywany jest konstruktor. Obiekt taki znajdowaby si w zasigu, nawet jeeli nie byby wywoany jego konstruktor; kompilator zgasza wic komunikat o bdzie. Ponownie gwarantuje to, e obiekty nie mog by tworzone, jeeli nie s one rwnie inicjalizowane. Cae omawiane tutaj przydzielanie pamici odbywa si, oczywicie, na stosie. Pami jest przydzielana przez kompilator przez przesunicie wskanika stosu w d" (to pojcie wzgldne, ktre moe oznacza zwikszanie lub zmniejszanie aktualnej wartoci wskanika stosu, zalenie od rodzaju uywanego komputera). Obiekty mogby rwnie tworzone na stercie za pomoc operatora new, co zostanie omwione dokadniej wrozdziale 13.

Klasa Stash z konstruktorami i destruktorami


W przykadach z poprzednich rozdziaw s przedstawione funkcje, ktre w oczywisty sposb odpowiadaj konstruktorom i destruktorom: initiaIize() i cIeanup(). Poniej zaprezentowano plik nagwkowy klasy Stash, uywajcy konstruktorw i destruktorw: //: C06:Stash2.h // Z konstruktorami i destruktorami #ifndef STASH2_H
#define STASH2_H

class Stash { int size: // Wielko kadego elementu int quantity: // Liczba elementw pamici int next: // Nastpny pusty element // Dynamicznie przydzielana tablica bajtw: unsigned char* storage: void inflate(int increase); public: Stash(int size): ~Stash(): int add(void* element): void* fetch(int index): int countO: }: #endif // STASH2_H IIIJedynymi definicjami funkcji skadowych, ktre ulegy zmianie, s initialize() i cleanup(), zastpione przez konstruktor i destruktor:
/ / : C06:Stash2.cpp {0} // Konstruktory i destruktory #include "Stash2.h" #include "../require.h" #include <iostream>

Thinking in C++. Edycja polska

#include <cassert> using namespace std; const int increment - 100;


Stash::Stash(int sz) { size = sz; quantity = 0; storage - 0; next = 0;

int Stash::add(void* element) { if(next >- quantity) // Czy wystarczy pamici? inflate(increment): // Kopiowanie elementu do pamici. // poczwszy od nastpnego wolnego miejsca: int startBytes = next * size; unsigned char* e - (unsigned char*)element; for(int i - 0; i < size; i++) storage[startBytes + i] - e[i]; next++; return(next - 1); // Numer indeksu void* Stash:;fetch(int index) { require(0 <= index, "Stash::fetch indeks ma wartosc ujemna"); if(index >= next) return 0; // Oznaczenie koca // Tworzenie wskanika do danego elementu: return &(storage[index * size]); int Stash::countO { return next: // Liczba elementw w Stash void Stash::inflate(int increase) { require(increase > 0, "Stasn::inflate increase ma wartosc zerowa lub ujemna"); int newQuantity - quantity + increase; int newBytes = newQuantity * size; int oldBytes = quantity * size; unsigned char* b - new unsigned char[newBytes]; for(int i = 0; i < oldBytes; i++) b[i] = storage[i]; // Kopiowanie starego obszaru do nowego delete [](storage); // Stary obszar pamici storage = b; // Wskanik do nowego obszaru quantity - newQuantity; Stash::~Stash() { if(storage !- 0) { cout "zwalnianie pamieci' endl; delete []storage;

Rozdzia 6. * lnicjalizacja i kocowe porzdki

239

Do ledzenia bdw programisty, zamiast funkcji assert(), zostay uyte funkcje zawarte w pIiku require.h. Informacje wywietlane w przypadku bdu przez funkcj assert() nie s tak uyteczne, jak informacje wywietlane przez funkcje pochodzce z require.h (zostanone zaprezentowane w dalszej czci ksiki). Poniewa funkcja inflate( ) jest skadow prywatn, jedynym przypadkiem, w ktrym wywoanie require() mogoby zakoczy si niepowodzeniem, jest przypadkowe przekazanie funkcji inflate() niepoprawnej wartoci przez inn funkcj skadow klasy. Jeeli masz pewno, e nie moe si to zdarzy, mona rozway usunicie wywoania require(). Naley jednak pamita, e dopki tre klasy nie zostanie ustalona, istnieje zawsze moliwo dodania do niej jakiego nowego kodu, ktry stanie si przyczyn bdw. Koszt wywoania require() jest niewielki (a poza tym wywoanie to moe by automatycznie usunite za pomoc preprocesora), natomiast korzyci wynikajce z uzyskania niezawodnego kodu znaczne. Zwr uwag na to, e definicje obiektw klasy Stash, widoczne w poniszym programie, wystpuj bezporednio przed miejscami, w ktrych obiekty te s potrzebne, a take na to, e inicjalizacja wydaje si elementem definicji, znajdujcym si na licie argumentw konstruktora: //: C06:Stash2Test.cpp //{L} Stash2 // Konstruktory i destruktory #include "Stash2.h" #include "../require.h" #include <fstream> #include <iostream> #include <string> using namespace std: int main() { Stash intStash(sizeof(int)); for(int i = 0; i < 100; i++) intStash.add(&i); for(int j = 0; j < intStash.count(); j++) cout "intStash.fetch(" j ") = " *Unt*)intStash.fetch(j) endl; const int bufsize = 80; Stash stringStash(sizeof(char) * bufsize); ifstream in("Stash2Test.cpp"); assure(in. " Stash2Test.cpp"); string line; while(getline(in. line)) stringStash.add((char*)line.c_strO); int k = 0; char* cp; while((cp = (char*)stringStash.fetch(k++))!=0) cout "stringStash.fetch(" k ") - " cp endl;

:40

Thinking in C++. Edycja polska Zwr rwnie uwag na to, w jaki sposb wyeliminowano wywoania funkcji cleanup(). Jednake destruktory s nadal wywoywane automatycznie, gdy koczy si zasig obiektw iniStash i stringStash. Warto podnie pewn kwesti dotyczc klasy Stash. Autor ksiki zwrci szczegln uwag na to, by uywa jedynie wbudowanych typw danych to znaczy typw nieposiadajcych destruktorw. Podjcie prby skopiowania do klasy Stash obiektw innych klas spowodowaoby lawin wszelkich moliwych problemw i program nie dziaaby poprawnie. Standardowa biblioteka jzyka C++ obecnie poprawnie kopiuje obiekty zawarte w swoich kontenerach, ale jest to raczej proces kopotliwy i skomplikowany. W zamieszczonym poniej przykadzie wykorzystania klasy Stack zastosowano wskaniki w celu uniknicia tego problemu. W nastpnym rozdziale rwnie klasa Stash zostanie zmodyfikowana w taki sposb, by uywaa wskanikw.

Klasa Stack z konstruktorami i destruktorami


Powtrna implementacja listy powizanej (zawartej w klasie Stack), wykorzystujca konstruktory i destruktory, pokazuje, jak elegancko dziaaj one z operatorami new i delete. Poniej przedstawiono zmodyfikowany plik nagwkowy: //: C06:Stack3.h // Z konstruktorami i destruktorami #ifndef STACK3_H |define STACK3_H class Stack { struct Link { void* data; Link* next; Link(void* dat. Link* nxt); -Link(); }* head: public: Stack(); ~Stack(); void push(void* dat); void* peek(); void* pop(); endif // STACK3_H ///:Klasa Stack posiada nie tylko konstruktor i destruktor, ale rwnie zagniedon klas Link:
//: C06;Stack3.cpp {0} // Konstruktory i destruktory #include "Stack3.h" include "../require.h" using namespace std;

}:

Rozdzia 6. lnicjalizacja i kocowe porzdki

241

Stack::Link::Link(void*dat. Link*nxt) {

data = dat;
next = nxt;

}
Stack::Link::~Link() { } Stack::Stack() { head = 0; }

void Stack::push(void* dat) { head - new Link(dat.head); void* Stack::peek() { require(head != 0, "Stos jest pusty"); return head->data; void* Stack: :pop() { if(head == 0) return 0; void* resu1t = head->data; Link* oldHead - head; head = head->next; delete oldHead; return result;
Stack::~Stack() { require(head =- 0, "Stos nie jest pusty");

Konstruktor Link( )::Link( ) inicjaIizuje wskaniki data i next, wic wiersz:


head = new Link(dat.head);

zawarty w funkcji Stack::push( ), nie tylko przydziela pami nowemu obiektowi klasy Link (wykorzystujc dynamiczne tworzenie obiektw za pomoc sowa kluczowego new, opisane w rozdziale 4.), ale rwnie w elegancki sposb inicjaIizuje jego wskaniki. Mona by zada pytanie, dlaczego destruktor struktury Link nie wykonuje adnych dziaa w szczeglnoci, dlaczego nie usuwa za pomoc operatora delete obszaru pamici wskazywanego przez data? Wynika to z dwch problemw. W rozdziale 4., w ktrym zosta po raz pierwszy przedstawiony program Stack, wyjaniono, e nie sposb poprawnie usun obszaru wskazywanego przez wskanik typu void*, jeeli wskazuje on obiekt (twierdzenie to zostanie udowodnione w rozdziale 13.). Ponadto gdyby destruktor struktury Link usuwa obszar wskazywany data, to w efekcie funkcja pop( ) zwrciaby wskanik do usunitego obiektu, co z pewnoci byoby bdem. Jest to czasami nazywane problemem prawa wasnoci: struktura Link, a wic rwnie klasa Stack, przechowujjedynie wskaniki, ale nie odpowiadajza usunicie wskazywanych przez nie obiektw. Trzeba zatem zwrci szczegln uwag na to, by wiedzie, kto jest za to odpowiedzialny. Na przykad jeeli nie pobierzesz za pomoc funkcji pop( ) wszystkich wskanikw znajdujcych si na stosie i nie usuniesz za pomoc delete wskazywanych przez nie obiektw, to nie zostan one usunite automatycznie przez destruktor klasy Stack. Moe by to zoony problem,

:42

Thinking in C++. Edycja polska prowadzcy do wyciekania" pamici. Dlatego te rnica pomidzy dobrze i le dziaajcym programem moe sprowadza si do wiedzy na temat tego, kto odpowiada za usunicie obiektu. Z tego wanie powodu funkcja Stack::~Stack() wywietla komunikat o bdzie, jeli obiekt klasy Stack nie jest w czasie destrukcji pusty. Z uwagi na to, e tworzenie i usuwanie obiektw struktury Link jest ukryte wewntrz klasy Stack a wic stanowi cz jej wewntrznej implementacji nie zdoasz dostrzec operacji w programie testowym. To jednak ty ponosisz odpowiedzialno za usunicie obiektw, wskazywanych przez wartoci zwracane przez funkcj pop(): //: C06:Stack3Test.cpp //{L} Stack3 //{T} Stack3Test.cpp // Konstruktory 1 destruktory

#include "Stack3.h" #include " . ./require.h" #include <fstream>

#include <iostream>
#include <string>

using namespace std: int main(int argc. char* argv[]) { requireArgs(argc. 1); // Argumentem jest nazwa pliku ifstream in(argv[l]); assure(in. argv[l]); Stack textlines; string line; // Odczytanie pliku i zapamitanie wierszy na stosie: while(getline(in. line)) textlines.push(new string(line)); // Pobranie wierszy ze stosu i wydrukowanie ich: string* s; while((s = (string*)textlines.popO) != 0) {

cout *s endl; delete s;

W powyszym przykadzie wszystkie wiersze, znajdujce si na stosie textlines, zostay pobrane i usunite. Jednak gdyby si tak nie stao, to wywoanie require() wywietlioby komunikat oznaczajcy, e nastpi wyciek pamici.

lnicjalizacja agregatowa
Agregat (ang. aggregate) jest dokadnie tym, co to sowo oznacza grup poczonych ze sob elementw. Definicja ta obejmuje agregaty mieszanych typw, takie jak np. struktury czy klasy. Tablice s agregatami elementw jednego typu. lnicjalizacja agregatw moe stanowi rdo bdw i jest zajciem nucym. Znacznie bezpieczniejszajest, dostpnawjzyku C++, inicjalizacja agregatowa (ang. aggregate initialization). Podczas tworzenia obiektu bdcego agregatem naleyjedynie dokona przypisania, a inicjalizacja zostanie przeprowadzona przez kompilator.

Rozdzia 6. lnicjalizacja i kocowe porzdki

243

W zalenoci od rodzaju agregatu, przypisanie to moe mie wiele postaci, ale zawsze elementy zawarte w przypisaniu muszznajdowa si w nawiasie klamrowym. W przypadku tablicy elementw wbudowanego typu jest to do proste:
int a[5] = { 1. 2. 3. 4, 5 };

W razie podania wikszej liczby inicjatorw ni wynosi liczba elementw tablicy, kompilator zgosi komunikat o bdzie. Co jednak si stanie w przypadku, gdy podanych inicjatorw bdzie zbyt malol Na przykad:

int b[6] = {0};


Wwczas kompilator uyje pierwszego inicjatora do zainicjowania pierwszego elementu tablicy, a nastpnie wartoci zerowej dla wszystkich elementw, dla ktrych nie podano inicjatorw. Naley pamita, e dziaanie takie nie ma miejsca w przypadku, gdy tablica zostaa zdefiniowana bez podania Iisty inicjatorw. Tak wic powysze wyraeniejest zwizym sposobem inicjalizacji wszystkich elementw tablicy wartoci zerow, bez potrzeby uywania do tego ptli for i bez moliwoci popenienia bdu, polegajcego na niewaciwym indeksowaniu elementw tablicy (w zalenoci od kompilatora, sposb ten moe by rwnie znacznie bardziej efektywny ni wykorzystanie ptli for). Drugim skrtem, stosowanym w przypadku tablic, jest automatyczne zliczanie (ang. automatic counting), polegajce na tym, e kompilator wyznacza rozmiar tablicy na podstawie liczby inicjatorw:
int c[] = { 1, 2, 3. 4 };

Jeeli zdecydujesz si teraz na dodanie do tablicyjeszczejednego elementu, to naley jedynie dopisa jeszcze jeden inicjator. Napisanie kodu w taki sposb, by dokonywane zmiany dotyczyy tylko jednego miejsca, zmniejsza prawdopodobiestwo popenienia bdu w czasiejego modyfikacji. Jakjednak uzyska informacj na temat wielkoci takiej tablicy? Umoliwia to sztuczka dziaajca w sposb niezaleny od zmian wielkoci tablicy, czyli zastosowanie wyraenia sizeof c / sizeof *c (wielko caej tablicy, podzielona przez wielkojej pierwszego elementu)5:

for(int i = 0; i < sizeof c / sizeof *c: i++) c[i]++;


Struktury s rwnie agregatami, a zatem mog by inicjaIizowane w podobny sposb. Poniewa struktura, zdefiniowana w stylu jzyka C, posiada wszystkie skadowe publiczne, mona im bezporednio przypisa wartoci:

struct X { int i: float f; char c:


X xl = { 1. 2.2. 'c' }:
' W drugim tomie ksiki (dostpnym bezpatnie w witrynie www.BruceEckel.com) przedstawiony zostanie bardziej zwizy sposb wyznaczenia wielkoci tablicy za pomocszablonw.

l__

M4

Thnking In C++. Edycja polska

W przypadku tablicy takich obiektw mona je zainicjowa, uywajc dla kadego obiektu zagniedonego nawiasu klamrowego:
X x2[3] - { {1, 1.1, ' * ' } . {2. 2.2, 'b } };
1

W powyszym przykadzie trzeci element tablicy zosta zainicjowany wartoci zerow. Jeeli ktrakolwiek skadowa jest prywatna (co jest typowe w przypadku dobrze zaprojektowanych klas w jzyku C++) lub nawet gdy wszystkie dane skadowe s publiczne, ale zdefiniowano konstruktor, inicjalizacja odbywa si w inny sposb. W powyszych przykadach inicjatory s przypisywane bezporednio elementom agregatu, natomiast konstruktory s sposobem wymuszenia inicjalizacji obiektu za porednictwem oficjalnego" interfejsu. W celu dokonania inicjalizacji musz wic zosta wywoane konstruktory. A zatem w przypadku struktury o nastpujcej postaci: struct Y { float f: int i; Y(int a); }:

naley wskaza wywoania konstruktorw. Najlepiej zrobi to w jawny sposb, jak w poniszym przykadzie:
Y yl[J = { Y ( 1 ) , Y ( 2 ) , Y ( 3 ) };

Wystpuj tu trzy obiekty i trzy wywoania konstruktorw. Jeli zdefiniowany jest konstruktor, niezalenie od tego, czy jest to struktura z wszystkimi skadowymi publicznymi, czy te klasa o prywatnych danych skadowych, inicjalizacja musi odbywa si za porednictwem konstruktora nawet gdyjest wykorzystywana inicjalizacja agregatowa. Poniej zamieszczono drugi przykad, prezentujcy konstruktory posiadajce wiele argumentw:

// Konstruktory z wieloma argumentami. // uywane do inicjalizacji agregatowej finclude <iostream> using namespace std; class l { int i, j; public: Z(int ii. int jj): void print(); Z::Z(int ii. int jj) { i = ii; j - JJ: void 1::print() { cout "i = " << i ", j - " j endl;

//: C06:Multiarg.cpp

Rozdzia 6. # lnicjalizacja i kocowe porzdki


int main() { Z zz[] = { Z(1.2). Z(3.4). Z(5.6). Z(7,8) }; for(int 1 = 0; i < sizeof zz / sizeof *zz; i++) zz[i].print(); } III-

245

Wyglda to w taki sposb, jakby konstruktor byl wywoywany jawnie dla kadego obiektu znajdujcego si w tablicy.

Konstruktory domylne
Konstruktorem domylnym (ang. default constructor) jest konstruktor, ktry moe by wywoany bez argumentw. Suy on do tworzenia zwykych obiektw", ale peni rwnie funkcj w przypadku, gdy kompilator ma za zadanie utworzenie jakiego obiektu, ale nie zostan podane mu adne dodatkowe informacje. Na przykad jeeli struktury Y, zdefiniowanej w poprzednim przykadzie, uyje si w nastpujcej definicji:
Y y2[2] = { Y(1) }:

to kompilator zgosi, e nie moe odnale domylnego konstruktora tej struktury. Drugi obiekt, znajdujcy si w tablicy, ma zosta utworzony bez argumentw, wic kompilator poszukuje jego domylnego konstruktora. W istocie, jeeli zdefiniuje si po prostu tablic obiektw struktury Y:
Y y3[7];

to kompilator zgosi bd, poniewa do zainicjowania kadego obiektu, znajdujcego si w tablicy, potrzebny jest konstruktor domylny. Ten sam problem wystpuje w przypadku tworzenia pojedynczych obiektw, jak np.:
Y y4;

Pamitaj, e jeeli istnieje konstruktor, kompilator gwarantuje, e zostanie on wykonany zawsze, niezalenie od sytuacji. Domylny konstruktor odgrywa wan rol jeeli (i tylko jeeli) struktura (lub klasa) nie posiada w ogle konstruktorw, kompilator automatycznie tworzy konstruktor domylny. Dziki temu dziaa zamieszczony poniej przykad:

//: C06:AutoDefaultConstructor.cpp // Automatycznie tworzony konstruktor domylny class V { int i; // skadowa prywatna }: // Brak konstruktora int main() { V v. v2[10];

Thinking in C++. Edycja polska

Gdyby jednak zostay zdefiniowane jakiekolwiek konstruktory i nie byoby wrd nich konstruktora domylnego, prba utworzenia widocznych powyej obiektw klasy V spowodowaaby wystpienie bdw kompilacji. Mona by odnie wraenie, e konstruktor utworzony przez kompilator powinien dokona jakiej inteligentnej inicjalizacji, polegajcej na przykad na wyzerowaniu caego obszar pamici przydzielonej obiektowi. Tak jednak nie jest stanowioby to dodatkowy narzut, znajdujcy si poza kontrol programisty. Aby pami obiektu zostaa wyzerowana, naley uczyni to samodzielnie w jawny sposb tworzc konstruktor domylny. Mimo e kompilator sam tworzy konstruktor domylny, to jego dziaanie rzadko odpowiada rzeczywistym potrzebom. Naley traktowa t waciwo jzyka jako siatk zabezpieczajc", wykorzystujcjjednak z umiarem. Na og trzebajawnie zdefiniowa konstruktory, nie pozwalajc na to, by zrobi to za ciebie kompilator.

Podsumowanie
Pozornie skomplikowane mechanizmy, dostpne w jzyku C++, powinny dostarczy ci wyranych wskazwek, dotyczcych szczeglnej roli inicjalizacji i sprztania. Jedn z pierwszych obserwacji dotyczcych wydajnoci programowania w jzyku C, ktre dokona Stroustrup podczas projektowania jzyka C++, bya ta, e wiele problemw programistycznych jest spowodowanych nieprawidow inicjalizacj zmiennych. Bdy tego typu strudne do wykrycia, a podobne problemy zwizane srwnie z nieprawidowym sprztaniem". Poniewa konstruktory i destruktory gwarantuj dokonanie waciwej inicjalizacji i sprztania (kompilator nie dopuszcza do utworzenia i zniszczenia obiektu bez wywoania odpowiedniego konsu^uktora i destruktora), uzyskuje si dziki nim pen kontrol nad programem oraz bezpieczestwo. Inicjalizacj agregatowa zostaa zaprojektowana w podobnym duchu zapobiega ona typowym pomykom, zwizanym z inicjalizacj agregatw wbudowanych typw, czynic kod programu bardziej zwizym. Bezpieczestwo zwizane z pisaniem kodu jest w jzyku C++ niezwykle istotn kwesti. Jego wanym elementem jest inicjalizacj i sprztanie, ale w dalszej czci ksiki zapoznasz si rwnie z innymi problemami dotyczcymi bezpieczestwa

wiczenia
Rozwizania wybranych wicze znajduj si w dokumencie elektronicznym: The Thinking in C+ + Annotated Solution Guide, ktry mona pobra za niewielk opat z witryny http://www.BruceEckel.com.

Rozdzia 6. lnicjalizacja i kocowe porzdki

247

1. Utwrz prost klas o nazwie SimpIe, posiadajc konstruktor drukujcy cokolwiek po to, by byo wiadomo, e zosta wywoany. Utwrz obiekt tej klasy w funkcji main(). 2. Do klasy, utworzonej w poprzednim wiczeniu, dodaj destruktor, drukujcy cokolwiek aby byo wiadomo, e zosta on wywoany. 3. Zmodyfikuj poprzednie wiczenie w taki sposb, aby kIasa zawieraa skadow cakowit. Zmodyfikuj konstruktor tak, aby pobiera on argument cakowity i zapisywa go w skadowej klasy. Zarwno konstruktor, jak i destruktor powinny drukowa warto tej skadowej w swoich komunikatach, umoliwiajc obserwacj tworzonych i niszczonych obiektw. 4. Wyka, e destruktorjest wywoywany nawet w przypadku, gdy do opuszczenia ptlijest uywana instrukcja goto. 5. Utwrz dwie ptle for drukujce wartoci od zera do dziesiciu. Zdefiniuj licznik pierwszej ptli przed instrukcjfor, a drugiej w wyraeniu sterujcym ptli. W ramach drugiej czci wiczenia zmie identyfikator licznika drugiej ptli w taki sposb, aby mia on tak sam nazw, jak licznik pierwszej ptli. Przeled, co zrobi kompilator. 6. Zmodyfikuj pliki Handle.h, Handle.cpp, i UseHandle.cpp, znajdujce si na kocu 5. rozdziau w taki sposb, by wykorzystyway konstruktory i destruktory. 7. Uyj inicjalizacji agregatowej do utworzenia tablicy elementw typu double, w ktrej definicji okrelisz wielko tablicy, ale nie dostarczysz dostatecznej liczbyjej inicjatorw. Wydrukuj zawarto tablicy, stosujc operator sizeof do uzyskania informacji ojej wielkoci. Nastpnie utwrz tablic wartoci typu dobule, uywajc inicjalizacji agregatowej oraz automatycznego zliczania. Wydrukuj zawarto tablicy. 8. Uyj inicjalizacji agregatowej do utworzenia tablicy obiektw typu string (acuchw). Utwrz klas Stack (stos), przechowujcacuchy, a nastpnie przejd przez wszystkie elementy tablicy acuchw, umieszczajc je kolejno na stosie. Wreszcie pobierz wszystkie acuchy ze stosu za pomoc funkcji pop( ), drukujc kady z nich. 9. Zademonstruj automatyczne zliczanie i inicjalizacj agregatow tablicy obiektw klasy, utworzonej w wiczeniu 3. Dodaj do tej klasy funkcj skadow, drukujc komunikat. Oblicz wielko tablicy, a nastpnie przejd przez wszystkie znajdujce si w niej obiekty, wywoujc dla kadego z nich dodan funkcj skadow. 10. Utwrz klas pozbawion wszelkich konstruktorw i wyka, e wykorzystujc domylny konstruktor, moesz utworzy jej obiekty. Nastpnie utwrz dla tej klasy konstruktor niebdcy domylnym konstruktorem (posiadajcyjaki argument) i sprbuj skompilowa program ponownie. Wyjanij, co si stao.

48

Thinking in C++. Edycja polska

Przecianie nazw funkcji i argumenty domylne


Jednz istotnych cech kadegojzyka programowaniajest wygodne uywanie nazw. Tworzc obiekt (zmienn), nadaje si nazw okrelonemu obszarowi pamici. Funkcja jest nazw pewnego dziaania. Okrelajc samodzielnie nazwy, opisujce system, tworzymy program, ktryjest atwiejszy do zrozumienia i do modyfikacji. Przypomina to pisanie proz gwnym celemjest komunikacja z czytelnikami. Problemy pojawiaj si, gdy usiujemy odda subtelnoci poj jzyka naturalnego wjzyku programowania. To samo sowo, w zalenoci od kontekstu, oznacza czsto zupenie co innego. Jedno sowo majce wiele znacze okrelamy mianem przecionego (ang. overloaded}. To bardzo wygodne, zwaszcza gdy dotyczy prostych rnic. Mwimy: umyj samochd, umyj rce". Byoby bezsensowne, gdybymy byli zmuszeni do mwienia: ,,samochod_umyj samochd, rece_umyj rce" po prostu dlatego, e suchacz nie musi dokonywa adnego rozrnienia pomidzy wykonywanymi czynnociami. Jzyki naturalne zawieraj w sobie pewn nadmiarowo, wic nawet w przypadku opuszczenia kilku sw, moemy nadal okreli znaczenie wypowiedzi. Nie potrzebujemy unikatowych identyfikatorw potrafimy wywnioskowa znaczenie z kontekstu. Jednak wikszo jzykw programowania wymaga nadania unikatowego identyfikatora kadej funkcji. Jeeli zamierzasz wydrukowa wartoci trzech rnych typw danych: int, char i float, to na og musisz utworzy trzy rne nazwy funkcji, np. print_int(), print_char() oraz print_float( ). Wymaga to sporego nakadu dodatkowej pracy dla ciebie, w czasie pisania programu, a take dla osoby, ktra bdzie prbowaa go zrozumie. W jzyku C++ jest jeszcze jeden czynnik, wymuszajcy przecianie nazw funkcji konstruktor. Z uwagi na to, e nazwa konstruktora jest zdeterminowana nazw klasy, wydawaoby si, e klasa moe zawiera tylko jeden konstruktor. Co jednak zrobi w przypadku, gdy chcemy utworzy obiekt w wicej nijeden sposb? Na przykad zamy, e tworzymy klas, ktra moe si albo zainicjowa sama, w standardowy

Rozdzia 7.

250

Thinking in C++. Edycja polska

sposb, albo czytajc informacje z pliku. Potrzebne s dwa konstruktory jeden, niepobierajcy argumentw (konstruktor domylny), oraz drugi, ktry pobiera argument typu string, bdcy nazw pliku inicjujcego obiekt. Oba s konstruktorami, musz wic posiada t sam nazw nazw klasy. Tak wic przecianie nazw funkcji jest niezbdne, by ta funkcja o tej samej nazwie w tym przypadku konstruktor moga by uywana z rnymi typami argumentw. Mimo e przecianie nazw funkcji jest w przypadku konstruktorw konieczne, to stanowi ono uatwienie o charakterze oglnym, ktre moe by uywane z kad funkcj, a nie tylko z funkcjami skadowymi klas. Ponadto przecianie nazw funkcji oznacza, e jeeli dwie rne biblioteki posiadaj funkcje o tych samych nazwach, to nie bd one ze sob kolidoway, pod warunkiem, e rni si listami argumentw. W niniejszym rozdziale przyjrzymy si bliej wszystkim tym czynnikom. Tematem rozdziau jest wygodne uywanie nazw funkcji. Przecianie ich pozwala na stosowanie tej samej nazwy dla rnych funkcji, jestjednakjeszcze jeden sposb, uatwiajcy wywoywanie funkcji. Jak postpi w przypadku, gdybymy chcieli wywoa t sam funkcj na rne sposoby? Jeeli funkcja ma dug list argumentw, a wikszo z nich jest taka sama dla wszystkich jej wywoa, to wpisywanie wywoa funkcji staje si uciliwe (a program trudny w czytaniu). Powszechnie stosowan waciwocijzyka C++ sdomylne argumenty. Domylny argumentjest argumentem wstawianym przez kompilator w przypadku, gdy nie zosta on okrelony w wywoaniu funkcji. A zatem wywoania: f("czesc"), f("hej", 1) i f("witaj", 2, 'c') mog by wywoaniami tej samej funkcji. Mog one stanowi rwnie wywoania trzech rnych funkcji o przecionych nazwach, ale jeli listy argumentw s podobne, oczekujemy zazwyczaj rwnie podobnego zachowania, wymagajcego pojedynczej funkcji. Przecianie nazw funkcji oraz domylne argumenty nie ^a naprawd niczym bardzo skomplikowanym. Po zapoznaniu si z tym rozdziaem bdzieju wiadomo, kiedy ich uywa i jak dziaa wewntrzny mechanizm, odpowiedzialny za ich obsug w czasie kompilacji i czenia.

Dalsze uzupenienia nazw


W rozdziale 4. zostao wprowadzone pojcie uzupelnie nazw (ang. name decoration). W poniszym przykadzie:

class X { void f(); }:


funkcja f(), znajdujca si wewntrz zasigu klasy X, nie koliduje z globaln wersj funkcji f(). Kompilator uwzgldnia ten zasig, tworzc rne wewntrzne nazwy dla globalnej wersji funkcji f() oraz dla funkcji X::f(). W rozdziale 4. zasugerowano, e nazwy te s nazw kIasy, uzupenion nazw funkcji, a zatem wewntrzne nazwy, uywane przez kompilator, mog mie posta: _f i _X_f. Okazuje si jednak, e uzupenienie nazwy funkcji zawiera co wicej ni tylko nazw klasy.

void f();

Rozdzia 7. Przecianie nazw funkcji i argumenty domylne

251

Oto przyczyna. Zamy, e zamierzamy przeciy dwie nazwy funkcji:


void print(char); void print(float);

Nie ma znaczenia, czy obie funkcje znajduj si wewntrz klasy, czy te w zasigu globalnym. Kompilator nie moe utworzy unikatowych wewntrznych identyfikatorw, uywajc jedynie zasigu nazw funkcji, bo w obu przypadkach byaby to nazwa _print. Ide uywania przecionych nazw funkcji jest stosowanie takich samych ich nazw, ale rnych list argumentw. A zatem, aby przecianie dziaao poprawnie, kompilator musi uzupenia nazwy funkcji nazwami typw argumentw. W przypadku powyszych funkcji, zdefiniowanych w zasigu globalnym, wewntrzne nazwy, utworzone przez kompilator, mog mie posta: _print_char i _print_float. Warto w tym miejscu nadmieni, e nie ma standardowego sposobu, w jaki musz by uzupeniane nazwy, wic dla rnych kompilatorw mona uzyska rne rezultaty (aby zobaczy, jak to dziaa, naley nakaza kompilatorowi generowanie kodu w asemblerze). Oczywicie, powoduje to problemy w przypadku zamiaru nabycia skompilowanych bibliotek, przeznaczonych dla konkretnego kompilatora i programu czcego. Jednak nawet gdyby uzupenienia nazw miay posta standardow, i tak pozostaaby bariera, spowodowana sposobem, w jaki rne kompilatory generuj kod. To ju naprawd wszystko, jeeli chodzi o przecianie nazw funkcji mona uywa tej samej nazwy dla rnych funkcji, pod warunkiem, e rni si one listami argumentw. Kompilator uzupenia nazwy, wykorzystujc zasig i listy argumentw tworzy w ten sposb wewntrzne nazwy, wykorzystywane przez siebie oraz przez program czcy.

Przecianie na podstawie zwracanych wartoci


Do powszechn reakcj jest zdziwienie: Dlaczego tylko zasigi i listy argumentw? Dlaczego nie zwracane wartoci?". Dodatkowe uzupenianie wewntrznych nazw funkcji zwracanymi wartociami wydaje si rozsdne na pierwszy rzut oka. Mona by wwczas przecia nazwy funkcji rwnie na podstawie zwracanych wartoci:
void fO; int f():

Dziaa to wspaniale, jeli kompilator jest w stanie jednoznacznie okreli znaczenie na podstawie kontekstu, jak np. w instrukcji int x = f();. Jednake wjzyku C mona byo zawsze wywoa funkcj, ignorujc zwracan przez ni warto (tj. wywoa funkcj dlajej skutkw ubocznych). Wjaki sposb kompilator ma odrni, o wywoanie ktrej funkcji w takim przypadku chodzi? By moe jeszcze gorsze jest to, e osoba czytajca kod te bdzie miaa trudnoci z ustaleniem, ktra funkcja jest wywoywana. Przecianie wycznie na podstawie zwracanej wartoci jest zbyt trudno uchwytne i dlatego niejest dozwolone wjzyku C++.

52

Thinking in C++. Edycja polska

.czenie bezpieczne dla typw


Z wszystkich, opisanych powyej, uzupenie nazw wynika dodatkowa korzy. Przyczyn szczeglnie skomplikowanych problemw w jzyku C byo niepoprawne zadeklarowanie funkcji przez klienta-programist lub, co gorsza, wywoanie funkcji w ogle bez jej uprzedniej deklaracji, co powodowao, e kompilator wnioskowa o deklaracji funkcji na podstawie sposobu, w jaki bya ona wywoana. Czasami taka deklaracja bya poprawna, ale w przeciwnym przypadku moga stanowi trudny do wykrycia bd. Poniewa w jzyku C++ wszystkie funkcje musz zosta przed uyciem zadeklarowane, moliwo zaistnienia takiego bdu jest bardzo ograniczona. Kompilator jzyka C++ odmawia automatycznego deklarowania funkcji za programist, jest wic prawdopodobne, e do programu zostanie doczony odpowiedni plik nagwkowy. Jeeli programista z jakiego powodu zdoa jednak bdnie zadeklarowa funkcj, dziaajc samodzielnie lub doczajc niewaciwy plik nagwkowy (by moe po prostu nieaktualny), uzupenienia nazw zapewni siatk zabezpieczajc", nazywan czsto czeniem bezpiecznym dla typw (ang. type-safe linkage). We pod uwag nastpujcy scenariusz. W pierwszym pliku znajduje si definicja funkcji:
/ / : C07:Def.Cpp {0} // Definicja funkcji void f(int) {} III-

W drugim natomiast funkcjajest niepoprawnie zadeklarowana, a nastpnie wywoana:

//{L} Def // Bdna deklaracja funkcji void f(char);

//: C07:Use.Cpp

int main() { //! f(l); // Powoduje bd programu czcego


Mimo e funkcja ma w rzeczywistoci posta f(int), to kompilator o tym nie wie, poniewa poinformowano go za pomoc jawnej deklaracji e jest to funkcja f(char). Dlatego te kompilacja przebiega poprawnie. W jzyku C czenie rwnie zakoczyoby si pomylnie, ale nie w jzyku C++. Poniewa kompilator uzupenia nazwy, definicja uzyska posta w rodzaju f_int, podczas gdy uycie funkcji ma posta f_char. Kiedy program czcy prbuje ustali odwoanie do f_char, moe odnale jedynie f_int i zgasza komunikat o bdzie. Jest to wanie czenie bezpieczne dla typw. Chocia problem ten nie wystpuje czsto, to kiedy si pojawi, moe by niewiarygodnie trudny do wykrycia szczeglnie w duych projektach. Jest to jeden z przypadkw, w ktrym mona w atwy sposb odnale powany bd w programie napisanym w jzyku C, kompilujc go za pomoc kompilatora C++.

Rozdzia 7. Przecianie nazw funkcji i argumenty domylne

253

Przykadowe przecienie
Moemy obecnie zmodyfikowa wczeniejsze przykady, tak by zawieray one przecienia nazw funkcji. Jak ju napisano powyej, miejscem, w ktrym od razu przydaj si przecienia, s konstruktory. Mona si o tym przekona w zamieszczonej poniej wersji klasy Stash: //: C07:Stash3.h // Przeciganie nazw funkcji #ifndef STASH3_H #define STASH3_H class Stash { int snze; // Wielko kadego elementu int quantity; // Liczba elementw pamici int next; // Nastpny pusty element // Dynamicznie przydzielana tablica bajtw: unsigned char* storage; void inflate(int increase); public: Stash(int size); // Zerowa liczba elementw Stash(int size, int initQuantity); -Stash(); int add(void* element); void* fetch(int index); int count(); }:

#endif // STASH3_H ///:-

Pierwszy konstruktor Stash( ) jest taki sam, jak poprzednio, lecz drugi posiada argument Quantity, okrelajcy pocztkow liczb elementw, dla ktrych przydzielana jest pami. Jak mona zobaczy w definicji, warto zmiennej quantity jest ustawiana na zero, podobnie jak wskanika storage. W drugim konstruktorze wywoanie inflate(initQuantity) zwiksza warto quantity do wielkoci przydzielonego obszaru pamici:
/ / : C07:Stash3.cpp {0} // Przecianie nazw funkcji #include "Stash3.h" #include " . ./require.h" #include <iostream> #include <cassert> using namespace std; const int increment = 100; Stash: :Stash(int sz) { size = sz; quantity = 0; next = 0: storage = 0;

Stash::Stash(int sz. int initQuantity) size = sz; quantity - 0;

Thinking in C++. Edycja polska next - 0; storage = 0; inflate(initQuantity); Stash::~Stash() { if(storage ! 0) { cout "zwalnianie pamici" endl; delete []storage;

int Stash::add(void* element) { if(next >= quantity) // Czy wystarczy pamici? inflate(increment); / / Kopiowanie elementu do pamici, // poczwszy od nastpnego wolnego miejsca: int startBytes = next * size; unsigned char* e = (unsigned char*)element; for(int i = 0; i < size; i++) storage[startBytes + i] = e[i]; next++; return(next - 1); // Numer indeksu void* Stash::fetch(int index) { require(0 <- index, "Stash::fetch indeks ma wartosc ujemna"); if(index >- next)

return 0; // Oznaczenie koca // Tworzenie wskanika do zqdanego elementu; return &(storage[index * size]);

int Stash: :count() { return next; // Liczba elementw w Stash void Stash;:inflate(int increase) { assert(increase >- 0); if(increase =- 0) return; int newQuantity = quantity + increase; int newBytes - newQuantity * size; int oldBytes = quantity * size; unsigned char* D = new unsigned char[newBytes]; for(int i = 0; i < oldBytes; i++) b[i] = storage[i]; // Kopiowanie starego obszaru do nowego delete [](storage); // Zwolnienie starego obszaru pamici storage = b; // Wskanik do nowego obszaru quantity = newQuantity; // Aktualizacja liczby elementw W przypadku uycia pierwszego konstruktora wskanikowi storage nie zostaje przydzielona adna pami. Przydzielenie pamici odbywa si wwczas, gdy za pomoc funkcji add( ) po raz pierwszy dodawanyjest obiekt, a take ilekro wewntrz funkcji add( ) zostanie przekroczona aktualna wielko bloku pamici.

Rozdzia 7. Przecianie nazw funkcji i argumenty domylne Oba konstruktory zostay uyte w programie testowym: / / : C07:Stash3Test.cpp / / { L } Stash3 // Przecianie funkcji #include "Stash3.h" #include ". ./require.h" #include <fstream> #include <iostream> #include <string> using namespace std; int main() { Stash intStash(sizeof(int)); for(int i = 0; i < 100; i++) intStash.add(&i); + for(int j = 0; j < intStash.count(); j+ ) cout "intStash.fetch(" j ") - " *dnt*)intStash.fetch(j) endl ; const int bufsize = 80; Stash stringStash(sizeof(char) * bufsize. 100); ifstream in("Stash3Test.cpp"); assure(in, "Stash3Test.cpp"); string line; while(getline(in. line)) stringStash.add((char*)line.c_strO); int k = 0; char* cp; while((cp = (char*)stringStash.fetch(k++))!=0) cout "stringStash.fetch(" k ") = " cp endl ;

255

Wywoanie konstruktora dla obiektu stringStash wykorzystuje drugi argument przypuszczalnie na temat specyfiki rozwizywanego problemu wiadomo co istotnego, co pozwala na okrelenie pocztkowej wielkoci klasy Stash.

Unie
Jak ju wspomniano, w jzyku C++ jedyn rnic pomidzy strukturami i klasami jest to, e skadowe struktury s domylnie publiczne, a skadowe struktury domylnie prywatne. Jak si rwnie mona spodziewa, struktury mog posiada konstruktory i destruktory. Okazuje si jednak, e take unie mog posiada konstruktory, destruktory, funkcje skadowe, a nawet kontrol dostpu. W poniszym przykadzie mona ponownie przeledzi uycie przeciania oraz wynikajce z niego korzyci: / / : C07:UnionClass.cpp // Unie z konstruktorami i funkcjami skadowymi #include<iostream>

using namespace std:

56

Thinking in C++. Edycja polska union U { private: // Kontrola dostpu! int i;


float f; publlC:

-U();

U(int a); U(float b); int read_int(); float read float();

U::U(int a) { i = a; } U::U(float b) ( f - b;} U::-U() { cout "U::-U()\n"; } int U::read_int() { return i; } float U::read_float() { return f; } int main() { cout X.read_int() endl; cout Y.read_float() endl;
IIIU X C 1 2 ) , Y(1.9F);

Na podstawie powyszego programu moesz wycign wniosek, e jedyna rnica pomidzy uniami i klasami sprowadza si do sposobu przechowywania danych (tj. skadowe typw int i float s umieszczone w pokrywajcych si obszarach pamici). Unie nie mog by jednak uyte w charakterze klas podstawowych podczas dziedziczenia, co z punktu widzenia programowania obiektowego jest istotnym ograniczeniem (dziedziczenie zostanie przedstawione w rozdziale 14.). Mimo e funkcje skadowe czyni dostp do unii bardziej cywilizowanym", to po inicjalizacji unii nadal nie ma sposobu na to, by powstrzyma klienta-programist przed wyborem niewaciwego elementu. W powyszym przykadzie mona uy funkcji X.read_float(), cho jest to niepoprawne. Jednake, bezpieczna" unia moe zosta zamknita w klasie. Zwr uwag na to, wjaki sposb uycie enum zwiksza przejrzysto kodu ijak przydatnejest przecianie konstruktorw: / / : C07:SuperVar.cpp // Superzmienna #include <iostream> using namespace std; class SuperVar { enum { character, integer. floating_point } vartype; // Definicja zmiennej union { // Anonimowa unia char c;

Rozdzia 7. Przecianie nazw funkcji i argumenty domylne


int i;
}:

257

float f;
public:

SuperVar(char ch); SuperVar(int ii); SuperVar(float ff); void print();

SuperVar::SuperVar(char ch) vartype = character:


c = ch;

SuperVar::SuperVar(int vartype = integer;

ii)

SuperVar::SuperVar(float ff) { vartype - floating_point;


f - ff;

void SuperVar::print() { switch (vartype) { case character: cout "znak: " c endl; break; case integer: cout "liczba cakowita: " i endl; break; case floating_point: cout "liczba zmiennopozycyjna: " f endl; break:

int main() { SuperVar A('c'). B(12), C(1.44F); A.print(); B.print(); C.print(); W powyszym programie deklaracja enum nie zawiera nazwy typu Qest wyliczeniem nienazwanym). Jest to dopuszczalne, jeeli zamierza si, jak w tym przypadku, od razu zdefiniowa egzemplarz zmiennej wyliczeniowej. Nie ma potrzeby odwoywania si do nazwy typu wyliczenia w przyszoci, wic podaniejejjest opcjonalne. W przypadku unii nie podano ani nazwy typu, ani nazwy zmiennej. Konstrukcja taka jest nazywana uni anonimow (ang. anonymous union). Rezerwuje ona pami dla unii, nie wymaga jednak podawania nazwy zmiennej i kropki przy dostpie do jej elementw. Poniej zamieszczono przykad wykorzystania unii anonimowej:

258
/ / : C07:AnonymousUnion.cpp int main() { union {

Thinking in C++. Edycja polska

int i ; float f:

// Dostp do skadowych bez uycia kwalifikatorw:


i - 12; f = 1.22;

}:

Zwr uwag, e do skadowych unii anonimowej mona si odwoywa, tak jakby byy zwykymi zmiennymi. Jedyna rnica polega na tym, e obie te zmienne zajmuj ten sam obszar pamici. Jeeli anonimowa unia znajduje si w zasigu pliku (na zewntrz wszystkich funkcji i klas), to musi by zadeklarowanajako statyczna, by bya czona wewntrznie. Mimo e klasa SuperVar jest obecnie bezpieczna, jej uyteczno jest nieco dyskusyjna, z uwagi na to, e zjednej strony uywa ona unii, w celu zaoszczdzenia pamici, natomiast z drugiej dodanie zmiennej vartype zajmuje pami w wielkoci porwnywalnej z wielkoci danych zawartymi w unii, co w efekcie niweluje oszczdnoci. Istnieje jednak wiele sposobw, dziki ktrym mogoby to dziaa zgodnie z oczekiwaniami. Gdyby zmienna vartype okrelaa typ wicej ni jednego egzemplarza unii ktre byyby w takim przypadku tego samego typu to dla caej grupy unii wystarczyaby ty!kojedna taka zmienna, a zatem nie zajmowaaby ona tyle miejsca. Bardziej praktycznym sposobem byoby umieszczenie kodu wykorzystujcego zmienn vartype w obrbie dyrektyw #ifdef, co zapewnioby, e wszystko dziaa prawidowo w trakcie pisania programu i jego testowania, a nastpnie pozwolioby na atwe pozbycie si zajmowanego miejsca i narzutu czasowego z gotowego kodu.

Argumenty domylne
Przyjrzyjmy si dwm konstruktorom klasy Stash(), zdefiniowanym w pliku Stash3.cpp. Nie wygldaj one na zupenie od siebie rne. W istocie, pierwszy z konstruktorw wydaje si szczeglnym przypadkiem drugiego, posiadajcym zerow warto argumentu initQuantity. Tworzenie i pielgnacja dwch odmiennych wersji podobnych do siebie funkcji stanowi do pewnego stopnia marnotrawstwo wysiku programisty. Lekarstwem na to s domylne argumenty, dostpne w jzyku C++. Domylnym argumentemjest warto, podana w deklaracji, ktrkompilator wstawi automatycznie, jeli w czasie wywoania funkcji nie zostanie dostarczony argument. W przypadku klasy Stash moemy zastpi dwie funkcje:

Stash(int size); // Zerowa liczba elementw Stash(int size. int initQuantity):


pojedyncz funkcj: Stash(int size, int initQuantity = 0 ) ;

Rozdzia 7. * Przecianie nazw funkcji i argumenty domylne

25S

Definicja konstruktora Stash(int) jest usuwana potrzebna jest wycznie jedna definicja funkcji Stash(int, int). Obecnie dwie zamieszczone poniej definicje obiektw:
Stash A(100). BC100. 0);

bd miay taki sam skutek. W obu przypadkach wywoany zostanie identyczny konstruktor. Jednake co do obiektu A, to drugi argument zostanie automatycznie podstawiony przez kompilator, gdy zauway on, e pierwszy argument jest typu int, i nie podano drugiego argumentu. Jeeli kompilator zosta wczeniej poinformowany o istnieniu argumentu domylnego, to wie, e bdzie mg wywoa funkcj, podstawiajc go jako jej drugi argument, czyli zrobi dokadnie to, czego da od niego programista, nadajc argumentowi charakter domylny. Argumenty domylne s udogodnieniem, podobnie jak przecianie nazw funkcji. Oba mechanizmy umoliwiaj wykorzystanie pojedynczej nazwy funkcji w rnych sytuacjach. Rnica polega na tym, e w przypadku argumentw domylnych kompilator podstawia argumenty w sytuacji, gdy nie chce ich poda programista. Przedstawiony poprzednio przykad stanowi ilustracj trafnego wykorzystania argumentw domylnych zamiast przecionych nazw funkcji w przeciwnym razie skoczyoby si bowiem na dwch lub wikszej liczbie funkcji, wygldajcych i dziaajcych podobnie. W przypadku gdy funkcje maj zupenie rne dziaanie, uywanie domylnych argumentw na og nie ma sensu (mona wwczas zapyta, czy dwie funkcje o zupenie rnym dziaaniu powinny nazywa si tak samo). Istniej dwie reguy, o ktrych trzeba pamita, uywajc domylnych argumentw. Po pierwsze, domylne mog by tylko kocowe argumenty. Oznacza to, e po domylnym argumencie nie moe nastpowa zwyczajny argument. Po drugie, gdy zacznie si uywa domylnych argumentw w wywoaniu funkcji, wszystkie nastpne argumenty na licie argumentw tej funkcji musz by argumentami domylnymi (wynika to z pierwszej reguy). Domylne argumenty s umieszczone wycznie w deklaracji funkcji (na og znajduj si one w pliku nagwkowym). Kompilator musi widzie warto domyln, zanim bdzie mg jej uy. Niektrzy dla celw dokumentacyjnych umieszczaj wartoci domylnych argumentw w definicji funkcji, w charakterze komentarzy:
void fn(1nt x /* - 0 */) { // ...

Argumenty-wype4niacze
W definicji funkcji argumenty mog by zadeklarowane bez identyfikatorw. Gdy s one uywane wraz z domylnymi argumentami, wygldaj naprawd zabawnie. W rezultacie mona uzyska: void f(int x. int - 0. float = 1.1): Wjzyku C++ identyfikatory nie srwnie konieczne w definicjach funkcji:
void f(int x. int. float flt) { /* ... */ }

50

Thinking in C++. Edycja polska

W ciele funkcji mona si odwoa do argumentw x oraz flt, ale nie do rodkowego argumentu, poniewa nie ma on nazwy. Wywoania funkcji muszjednak zawiera warto przeznaczon dla wypeniacza": f(l) lub f(l,2^.0). Taka konstrukcja pozwala na umieszczenie argumentu w charakterze wypeniacza, bez jego wykorzystywania. Pomys polega na tym, by mona byo w przyszoci zmieni definicj funkcji w taki sposb, by uywaa ona argumentu-wypetniacza, nie uciekajc si do zmiany kodu zawierajcego wywoania funkcji. Oczywicie mona osign to samo, wykorzystujc nazwany argument, ale w przypadku zdefiniowania argumentu, ktry niejest uywany w ciele funkcji, wikszo kompilatorw zgosi ostrzeenie, zakadajc, e zosta popeniony bd logiczny. Celowe pominicie nazwy argumentu zapobiegnie pojawieniu si ostrzeenia. Co waniejsze, jeeli rozpoczniesz uywanie argumentu funkcji, a pniej postanowisz, e nie jest on potrzebny, moesz usun go, nie generujc ostrzee i nie zakcajc w aden sposb kodu klienta, wywoujcego poprzedni wersj funkcji.

Przecianie kontra argumenty domylne


Zarwno przecianie nazw funkcji, jak i argumenty domylne stanowi udogodnienie w wywoywaniu nazw funkcji. Jednake czasami trudno okreli, ktrej techniki uy. Jako przykad rozwamy przedstawiony poniej program pomocniczy, przeznaczony do automatycznego zarzdzania blokami pamici: //: C07:Mem.h #ifndef MEM_H #define MEM_H typedef unsigned char byte; class Mem { byte* mem; int size;

void ensureMinSize(int minSize); public:

Mem():

~Mem();

Mem(int sz);

#endif // MEM_H / / / : -

}:

int msize(): byte* pointer(); byte* pointer(int minSize):

Obiekt Mem przechowuje blok bajtw, sprawdzajc, czy dostpna jest dostateczna ilo pamici. Domylny konstruktor nie przydziela jej, natomiast drugi konstruktor zapewnia, e w obiekcie Mem dostpna jest pami o wielkoci sz bajtw. Destruktor zwalnia pami, funkcja msize() informuje, ile bajtw znajduje si obecnie w obiekcie Mem, a funkcja pointer() zwraca wskanik do pocztku obszaru pamici (klasa Mem to narzdzie do niskiego poziomu). Istnieje przeciona wersja funkcji pointer(), dziki ktrej kuent-programista moe okreli, e potrzebny jest mu wskanik do bloku pamici o wielkoci przynajmniej minSize, a funkcja skadowa go dostarcza.

Rozdzia 7. Przecianie nazw funkcji i argumenty domylne

261

Zarwno konstruktor, jak i funkcja skadowa pointer() uywaj do zwikszenia wielkoci bloku pamici funkcji skadowej private ensureMinSize() (zwr uwag na to, e w przypadku, gdy wielko pamici ulega zmianie, przechowywanie wartoci zwracanej przez funkcj pointer() nie jest bezpieczne). Poniej przedstawiono implementacj klasy:
/ / : C07:Mem.cpp {0} finclude "Mem.h" #include <cstring> using namespace std; Mem::Mem() { mem = 0; size = 0; } Mem::Mem(int sz) { mem = 0; size = 0: ensureMinSize(sz);

Mem::~Mem() { delete []mem; } int Mem::msize() { return size; }


void Mem::ensureMinSize(int minSize) { if(size < minSize) { byte* newmem = new byte[minSize]; memset(newmem + size. 0, minSize - size); memcpy(newmem. mem. size); delete []mem; mem = newmem; size = minSize;

byte* Mem::pointer() { return mem; }

byte* Mem::pointer(int minSize) { ensureMinSize(minSize); return mem; } IIINie ulega wtpliwoci, e ensureMinSize() jest jedyn funkcj odpowiedzialn za przydzielanie pamici; jest ona wywoywana przez drugi konstruktor oraz przez drug, przecion posta funkcji pointer(). Jeeli aktualna wielko bloku pamici jest dostatecznie dua, to wewntrz funkcji ensureMinSize() nic si nie dzieje. Jeeli w celu zwikszenia bloku musi zosta przydzielony nowy obszar pamici (co ma rwnie miejsce po wywoaniu domylnego konstruktora, gdy blok ma wielko zerow), to dodatkowa" porcja pamici jest wypeniana zerami za pomoc funkcji memset(), nalecej do standardowej biblioteki jzyka C (funkcja ta zostaa przedstawiona w rozdziale 5.). Nastpnie wywoywana jest funkcja memcpy(), rwnie naleca do standardowej biblioteki jzyka C, ktra w tym przypadku kopiuje istniejce ju bajty z obszaru mem do newmem (zazwyczaj w efektywny sposb). Wreszcie poprzedni obszar pamici jest zwalniany, a jej nowy obszar oraz rozmiar s kopiowane do odpowiednich skadowych.

Thinking in C++. Edycja polska Klasa Mem zostaa zaprojektowana w taki sposb, by suya jako narzdzie wewntrz innych klas, upraszczajc ich zarzdzanie pamici (mona jej uy rwnie po to, by ukry bardziej wyrafinowany system zarzdzania pamici, udostpniany, na przykad, przez system operacyjny). Takie wanie zastosowanie klasy Mem zostao przetestowane poniej. Wchodzi ona w skad prostej klasy obsugujcej acuchy: / / : C07:MemTest.cpp // Test klasy Mem //{L} Mem #include "Mem.h" #include <cstring> #include <iostream> using namespace std; class MyString { Mem* t>uf; public: MyString(); MyString(char* str); ~MyStringO: void concat(char* str); void phnt{ostream& os): MyString::MyString() { buf = 0: } MySthng::MyString(char*str) { buf = new Mem(strlen(str) + 1); strcpy((char*)buf->pointer(), str); void MyString::concat(char* str) { if(!buf) buf - new Mem; strcat((char*)buf->pointer( buf->msize() + strlen(str) + 1). str); void MyString::print(ostream& os) if(!buf) return; os buf->pointer() endl; MyString::^yString() { delete buf; }

int main() { MyString s("Moj testowy lancuch"); s.print(cout); s.concat(" cos dodatkowego"); s.print(cout); MyString s2; s2.concat("Uzycie domylnego konstruktora"); s2.print(cout);

Rozdzia 7. Przecianie nazw funkcji i argumenty domylne

263

Wykorzystujc powysz klas, mona jedynie utworzy obiekt typu MyString, doczy tekst oraz drukowa do strumienia ostream. Klasa zawiera wycznie wskanik do obiektu Mem, naley jednak zwrci uwag na rnic pomidzy domylnym konstruktorem, ustawiajcym ten wskanik na zero, i drugim konstruktorem, tworzcym obiekt typu Mem i kopiujcym do niego dane. Przykadem korzyci wynikajcej z domylnego konstruktora jest to, e mona utworzy du tablic pustych obiektw klasy MyString niewielkim kosztem, poniewa wielkoci kadego obiektu jest tylko wielko pojedynczego wskanika, a jedynym narzutem, wnoszonym przez domylny konstruktor przypisanie wskanikowi wartoci zerowej. Koszt klasy MyString zaczyna wzrasta dopiero wwczas, gdy doczane s dane w tym momencie jest tworzony obiekt Mem, o ile nie stao si to wczeniej. Jeeli jednak zostanie wykorzystany domylny konstruktor i nigdy nie bdzie wykonywana operacja doczania danych, to wywoanie destruktora bdzie nadal bezpieczne. Wywoanie delete dla wartoci zerowej jest bowiem zdefiniowane w taki sposb, e nie usiuje zwalnia pamici ani nie wywouje innych niepodanych skutkw. Gdy przyjrzysz si tym dwm konstruktorom, to mog si one na pierwszy rzut oka wydawa gwnymi kandydatami do uycia argumentw domylnych. Jeeli jednak usuniesz domylny konstruktor i uzupenisz pozostay konstruktor o argument domylny:
MyString(char* str - "");

wszystko bdzie dziaa poprawnie. Utracisz wszake poprzedni efektywno konstruktora, poniewa obiekt Mem bdzie zawsze tworzony. Aby przywrci jego efektywno, naley zmodyfikowa konstruktor:
MyString::MyStnng(char* str) { if(!*str) { // Wskazuje pusty acuch buf = 0: return; } buf = new Mem(strlen(str) + 1); strcpy((char*)buf->pointer(). str);

W rezultacie oznacza to, e warto domylna stanie si znacznikiem, wywoujcym wykonanie innego kodu, jeli zostanie uyta warto inna ni domylna. Mimo e w przypadku tak niewielkich konstruktorw jak przedstawiony wyglda to do niewinnie, na og stosowanie takich praktyk moe powodowa problemy. Jeeli musisz sprawdza, czy argument nie zawiera wartoci domylnej, zamiast traktowa gojako zwyk warto, to w efekcie zostan utworzone dwie funkcje, zawarte w ciele pojedynczej funkcji jedna, obsugujca normalne przypadki, i druga, przeznaczona dla przypadku domylnego. Rwnie dobrze mona podzieli j na dwie rne funkcje, zezwalajc, by tego wyboru dokona kompilator. W rezultacie uzyskuje si niewielki (ale zazwyczaj niewidoczny) wzrost wydajnoci, poniewa nie jest w tym przypadku przekazywany dodatkowy argument i nie jest rwnie wykonywany dodatkowy kod, zwizany ze sprawdzeniem warunku. Co waniejsze, kod wykonywany przez dwie oddzielne funkcje jest rzeczywicie umieszczony w dwch oddzielnych funkcjach, a nie poczony w jedn za pomoc domylnego argumentu. Uatwia to jego pielgnacj zwaszcza gdy funkcje te s due.

Thinking in C++. Edycja polska Z drugiej strony warto rozway klas Mem. Analizujc definicje dwch konstruktorw i dwch funkcji pointer(), mona zauway, e wprowadzenie domylnych argumentw nie spowoduje w adnym z tych przypadkw koniecznoci zmiany definicji funkcji skadowej. Tak wic definicja klasy moe by atwo przeksztacona do postaci:

//: C07:Mem2.h #ifndef MEM2_H #define MEM2_H typedef unsigned char byte; class Mem { byte* mem; int size; void ensureMinSizeCint minSize); publi c: Mem(int sz = 0): ~Mem(); int msize();
byte* pointer(int minSize = 0);

#endif // MEM2_H IIIZwr uwag na to, e wywoanie ensureMinSize(0) zawsze bdzie do efektywne. Mimo e w obu tych przypadkach autor ksiki kierowa si pewnego rodzaju procesem decyzyjnym, uwzgldniajc kwestie efektywnoci, to naley uwaa, by nie wpa w puapk rozwaania wszystkiego wycznie w tych kategoriach (efektywno jest sama w sobie fascynujca). Najistotniejszym zagadnieniem, zwizanym z projektowaniem klasy, jest jej interfejs (czyli publiczne skadowe, widoczne dla klienta-programisty). Jeeli dziki niemu klasa bdzie atwa w uyciu i w ponownym wykorzystaniu, osigniesz sukces w razie potrzeby zawsze moesz poprawi jej efektywno. Natomiast klasa zaprojektowana le, poniewa programista nadmiernie skupi si na kwestii jej efektywnoci, jest niekiedy prawdziw zmor. Naley troszczy si gwnie o to, by interfejs klasy wydawa si logiczny tym, ktrzy go uywaj, oraz tym, ktrzy bd czyta powstay kod. Zwr uwag na to, e nie zmieni si sposb uycia klasy MyString w pliku MemTest.cpp bez wzgldu na to, czy uywaa ona domylnego konstruktora czy te nie, ajej efektywno bya maa czy dua.

}:

Podsumowanie
Nie naley uywa domylnego argumentu w charakterze znacznika, od ktrego zaley warunkowe wykonanie kodu. Natomiast jeeli moesz, rozbij funkcj na dwie lub wiksz liczb przecionych funkcji. Domylny argument powinien by wartoci, ktra byaby normalnie wpisana na swojej pozycji. Jest to warto, ktra wystpuje czciej ni inne, dziki czemu klient-programista moe j pomin albo uy jej tylko w przypadku, gdy chce j zmieni na inn. Domylne argumenty maj na celu uatwienie wywoywania funkcji, zwaszcza gdy funkcje te pobieraj wiele argumentw, posiadajcych typowe wartoci. Nie tylko

Rozdzia 7. Przecianie nazw funkcji i argumenty domylne

265

prociej jest napisa ich wywoania, ale atwiej rwnie przeczyta zawierajcy je kod przede wszystkim wwczas, gdy twrca klasy uporzdkowa argumenty w taki sposb, by najrzadziej modyfikowane znajdoway si na kocu listy. Szczeglnie istotnym sposobem wykorzystania domylnych argumentw jest przypadek, gdy po pewnym czasie uywania funkcji o okrelonej Hczbie argumentw okazuje si, e potrzebne sjej dodatkowe argumenty. Nadajc wszystkim nowym argumentom wartoci domylne, mona zapewni, e kod klienta, wykorzystujcy poprzedni interfejs, pozostanie nienaruszony.

wiczenia
Rozwizania wybranych wicze znajduj si w dokumencie elektronicznym: The Thinking in C++ Annotated Solution Guide, ktry mona pobra za niewielk opat z witryny http://www.BruceEckel.com. 1. Utwrz klas Text, ktra zawiera obiekt klasy string, przechowujcy tekst znajdujcy si w pliku. Zdefiniuj dwajej konstruktory konstruktor domylny oraz konstruktor przyjmujcy acuch, bdcy nazwpliku, ktry ma zosta otwarty. W przypadku uycia drugiego konstruktora otwrz plik i wczytaj jego zawarto do skadowej, bdcej obiektem klasy string. Dodaj funkcj skadow contetns(), zwracajc przechowywany acuch, dziki czemu bdzie go mona (na przykad) wydrukowa. Wykorzystujc klas Text, otwrz w funkcji main( ) plik i wydrukuj jego zawarto. 2. Utwrz klas Message (wiadomo), ktra zawiera konstruktor pobierajcy pojedynczy acuch, stanowicy wiadomo o okrelonej domylnej wartoci. Utwrz prywatnskadow, bdcacuchem typu string, a w konstruktorze przypisz tej skadowej warto jego argumentu. Utwrz dwie przecione funkcje skadowe o nazwach print() jedn, ktra nie przyjmuje argumentw, drukujc po prostu przechowywan wiadomo, i drug, przyjmujcjako argument acuch drukowany wraz z przechowywan wiadomoci. Czy takie podejcie, zamiast uywanych w konstruktorze wartoci domylnych, ma sens? 3. Dowiedz si, wjaki sposb wygenerowa na wyjciu kompilatora kod w asemblerze i przeprowad eksperymenty, majce na celu okrelenie sposobu uzupeniania nazw przez kompilator. 4. Utwrz klas, zawierajccztery funkcje skadowe, pobierajce odpowiednio: 0, 1, 2 i 3 argumenty cakowite. Utwrz funkcj main(), w ktrej tworzony jest obiekt twojej klasy, a nastpniejest wywoywana kolejno kada z funkcji skadowych. Teraz zmodyfikuj swoj klas w taki sposb, by zawieraa tylko jedn funkcj skadow o wszystkich argumentach domylnych. Czy zmieni to kod, znajdujcy si w funkcji main( )? 5. Utwrz funkcj, posiadajc dwa argumenty, a nastpnie wywoaj j z wntrza funkcji main(). Potem zmiejeden zjej argumentw w argument-wypeniacz"

36

Thinking in C++. Edycja polska

(nieposiadajcy identyfikatora) i sprawd, czy zmieni si sposb wywoania tej funkcji w funkcji main(). 6. Zmodyfikuj pliki Stash3.h i Stash3.cpp w taki sposb, by w konstruktor klasy uywa domylnych argumentw. Przetestuj konstruktor, tworzc dwie rne wersje obiektu Stash. 7. Utwrz now wersj klasy Stack (opisanej w rozdziale 6.), zawierajc, jak poprzednio, domylny konstruktor, a take drugi konstruktor, przyjmujcy jako argumenty tablic wskanikw do obiektw oraz wielko tej tablicy. Konstruktor powinien przej przez t tablic, umieszczajc na stosie kady z zawartych w niej wskanikw. Przetestuj klas, uywajc tablic acuchw. 8. Zmodyfikuj klas SuperVar w taki sposb, aby kod, wykorzystujcy zmienn vartype, znajdowa si w obrbie dyrektyw #ifdef,jak to opisano w czci powiconej deklaracji enum. Przekszta zmiennavartype w zwyke, publicznie dostpne wyliczenie (bez egzemplarza zmiennej) i zmodyfikuj funkcj print() w taki sposb, by wymagaa argumentu typu vartype, okrelajcego, jakie dziaania ma wykona. 9. Dokonaj implementacji klasy, ktrej definicja znajduje si w pliku Mem2.h, upewniajc si, e dziaa nadal z programem MemTest.cpp. 10. Wykorzystaj klas Mem do zaimplementowania klasy Stash. Zwr uwag na to, e poniewa implementacja ma charakter prywatny, dziki czemu jest ukryta przed klientem-programist, program testowy klasy nie wymaga adnych modyfikacji. 11. Do klasy Mem dodaj funkcj moved( ), ktra pobiera rezultat wywoania funkcji pointer() i zwraca warto bool, informujc, czy wskanik zosta przesunity (z powodu powtrnego przydziau pamici). Napisz funkcj main(), testujct nowfunkcj skadow. Czy uywanie funkcji w rodzaju moved( ) ma sens, czy te lepiej wywoywa funkcj pointer( ) ilekro potrzebnyjest dostp do pamici przechowywanej w obiekcie klasy Mem?

Stae

Rozdzia 8.

Pojcie stalej (wyraone za pomoc sowa kluczowego const) zostao utworzone po to, by umoliwi programistom wytyczenie granicy pomidzy tym, co si zmienia, i tym, co niezmienne. Zapewnia ono bezpieczestwo i kontrol w projektach programistycznychjzyka C++. Pojcie to, od momentu powstania, miao wiele rnych zastosowa. W tym czasie wchodzio ono stopniowo take do jzyka C, ale jego znaczenie ulego zmianie. Wszystko to moe wydawa si na pierwszy rzut oka nieco skomplikowane, ale na podstawie lektury rozdziau dowiesz si, kiedy, dlaczego i w jaki sposb uywa si sowa kluczowego const. Pod koniec rozdziau zamieszczono dyskusj na temat sowa kluczowego volatile, blisko spokrewnionego ze sowem const (oba dotycz zmian), posiadajcym identyczn skadni. Wydaje si, e najwaniejszym powodem wprowadzenia sowa kluczowego const byo zaprzestanie uywania dyrektywy preprocesora #define do zastpowania wartoci. Od tej pory sowo to jest wykorzystywane w stosunku do wskanikw, argumentw funkcji, zwracanych wartoci, obiektw klas i funkcji skadowych. Wszystkie one maj nieco rne, ale pojciowo zblione znaczenia i zostan omwione w kolejnych podrozdziaach, zawartych w rozdziale.

Podstawianie wartoci
W celu tworzenia makroinstrukcji i podstawiania wartoci podczas programowania wjzyku C czsto uywa si preprocesora. Poniewa preprocesor dokonuje tylko prostego zastpowania tekstu, nie posiadajc ani wiedzy na temat typw, ani moliwoci ich kontroli, podstawianie wartoci za pomoc preprocesora powoduje trudno uchwytne problemy, ktrych mona unikn, uywajc wjzyku C++ wartoci staych. Typowy sposb uywania preprocesora do podstawiania wartoci jest nastpujcy w jzyku C:
)Wefine BUFSIZE 100

68

TKinking in C++. Edycja polska

BUFSIZE jest nazw istniejc wycznie w trakcie pracy preprocesora, a zatem nie zajmuje ona pamici i moe zosta umieszczona w pliku nagwkowym, udostpniajc t sam warto wszystkim wykorzystujcym jjednostkom translacji. Dla pielgnacji kodu bardzo wane jest, by uywa podstawiania wartoci zamiast tak zwanych magicznych liczb". Jeeli w swoim programie uywasz magicznych liczb, to nie tylkojego czytelnik nie wie, skd si one wziy i co oznaczaj, ale rwnie w razie koniecznoci zmiany ktrej z nichjeste zdany na mudnedycj, nie majc adnej gwarancji, e nie zostaa opuszczona adna warto (albo przypadkowo nie zmienie niewaciwej). W wikszoci przypadkw BUFSIZE bdzie zachowywaa si jak zwyka zmienna, ale nie w kadym. W dodatku nie posiada ona adnej informacji, dotyczcychjej typu. Moe to doprowadzi do powstania ukrytych, bardzo trudnych do wykrycia bdw. Do eliminacji tego typu problemw jzyk C++ wykorzystuje sowo kluczowe const, przenoszce proces zastpowania wartoci do kompilatora. Dziki temu mona napisa:

const int bufsize = 100;


Staej bufsize mona uywa w kadym miejscu, w ktrym kompilator musi zna warto w czasie kompilacji. Kompilator moe stosowa wartoci bufsize do dokonania sktadania statych (ang. constantfolding), co oznacza, e uproci on zoone wyraenia, przeprowadzajc niezbdne obliczenia podczas kompilacji. Jest to szczeglnie wane w przypadku definicji tablic:

char buf[bufsize]:
Sowa kluczowego const mona uy w stosunku do wszystkich wbudowanych typw (char, int, float i double) oraz ich wariantw (podobnie jak i w stosunku do obiektw klas, co zostanie zaprezentowane w dalszej czci rozdziau). Z uwagi na trudno uchwytne bdy, ktre moe spowodowa uycie- preprocesora, zamiast podstawie za pomoc dyrektywy #define naley zawsze posugiwa si sowem kluczowym const.

State w plikach nagtwkowych


Aby zamiast dyrektywy #define uywa modyfikatora const, musi istnie moliwo umieszczenia definicji staej w pliku nagwkowym, tak jak w przypadku #define. Dziki temu mona umieci definicj staej w jednym miejscu, a nastpnie dostarczy j poszczeglnym jednostkom translacji, doczajc do nich odpowiedni plik nagwkowy. Stae s w jzyku C++ domylnie czone wewntrznie to znaczy s widoczne wycznie w pliku, w ktrym zostay zdefiniowane, a podczas czenia nie s dostpne dla innych jednostek translacji. W czasie definiowania staej trzeba zawsze nada jej warto, z wyjtkiem przypadku, gdy dokonuje si jawnej deklaracji staej za pomoc sowa kluczowego extern:
extern const int bufsize;

Zwykle kompilator C++ unika rezerwacji pamici, zawierajcej warto staej, prze^ chowujc jej definicj w tablicy symboli. Jednak w przypadku uycia w deklaracji const sowa kluczowego extern, wymusza si przydzielenie staej pamici (dzieje si

Rozdzia 8. State

269

tak rwnie w niektrych innych przypadkach, takich jak pobieranie adresu staej). Pami musi zosta przydzielona, poniewa sowo kluczowe extern nakazuje: uyj czenia zewntrznego". Oznacza to, e wiele jednostek translacji musi by w stanie odwoa si do tej wartoci, co wymaga przydzieleniajej pamici. Zazwyczaj gdy definicja nie zawiera sowa extern pami nie jest przydzielana. Kiedy uywane jest sowo kluczowe const, warto jest po prostu wstawiana do programu podczas kompilacji. Postulat, by nigdy nie przydziela staym pamici, zawodzi rwnie w przypadku zoonych struktur. Gdy kompilator musi przydzieli staej pami, nie dokonuje skadania staych (poniewa nie potrafi okreli wartoci tej staej gdyby j zna, nie musiaby przydziela jej pamici). Z uwagi na to, e kompilator nie zawsze zdoa unikn przydzielenia staej pamici, jej definicja musi by domylnie czona wewntrznie, to znaczy wycznie w obrbie danej jednostki translacji. W przeciwnym razie spowodowaoby to zgoszenie przez program czcy bdu w przypadku zoonych staych, poniewa w wielu plikach cpp zostaaby im przydzielona pami. Program czcy ,^aprotestowalby" z powodu tej samej definicji zawartej w wielu plikach wynikowych. Poniewa stae podlegaj domylnie czeniu wewntrznemu, program czcy nie prbuje kojarzy ich definicji pomidzy rnymi jednostkami translacji, co pozwala na uniknicie kolizji. W przypadku typw wbudowanych, wystpujcych w wikszoci wyrae zawierajcych stae, kompilator zawsze moe przeprowadzi skadanie staych.

Bezpieczestwo statych
Zastosowanie staych nie ogranicza si do zastpowania dyrektyw #define w wyraeniach staych. Gdy zmienna jest inicjalizowana wartoci, wyznaczan w trakcie pracy programu, i wiadomo, e warto tej zmiennej nie zmieni si w czasie jej ycia, to dobrym programistycznym zwyczajem jest uczynienie jej sta. Dziki temu kompilator zgosi komunikat o bdzie w razie przypadkowej prby zmiany jej wartoci. Poniej zamieszczono odpowiedni przykad: //: C08:Safecons.cpp // Wykorzystywanie staych dla bezpieczestwa #include <iostream> using namespace std; const int i - 100: // Typowa staa const int j - i + 10; // Warto wyraenia staego long address = (long)&j; // Wymuszenie przydziau pamici char buf[j + 10]; // Nadal jest to wyraenie stae int main() { cout "wpisz znak i nacisnij Enter:"; const char c = cin.get(): // Tej wartoci nie be^zie mona zmieni const char c2 = c + 'a'; cout c2;

!70

Thinking in C++. Edycja polska

A zatem i jest sta, okrelon w czasie kompilacji, lecz warto staej j jest wyliczana na podstawie wartoci i. Poniewa jednak i jest sta, warto staej j jest obliczana rwnie na podstawie wyraenia staego, w zwizku z tym jest ona rwnie sta o wartoci wyznaczonej podczas kompilacji. Ju w nastpnym wierszu wymagany jest adres staej j, co wymusza na kompilatorze przydzielenie jej pamici. Nie przeszkadza tojednak w uyciu staej j do okrelenia wielkoci tablicy buf kompilator wie bowiem, e j jest sta, ktrej warto jest poprawna, nawet jeli w jakim miejscu programu zostaa przydzielona pami przechowujca t warto. W funkcji main(), pod nazw c, zdefiniowano inny rodzaj staej, poniewajej warto nie moe by znana w trakcie kompilacji. Oznacza to, e przydzielana jest jej pami, a kompilator nie prbuje zapisywa niczego w swojej tablicy symboli (zachowuje si tak samo, jak w jzyku C). Inicjalizacja staej musi nadal wystpowa w miejscu jej definicji, ale od momentu inicjalizacji warto tej staej nie moe ju by zmieniana. Na podstawie wartoci staej c wyznaczana jest warto staej c2, a w przypadku staych obowizuj rwnie reguy dotyczce zasigw, co stanowi jeszcze jedn przewag staych nad dyrektywa#define. Z praktyki wynika, e jeeli warto nie powinna by zmieniana, to naley uczyni j sta. Stanowi to nie tylko ochron przed jej przypadkowymi zmianami, ale pozwala rwnie kompilatorowi na wygenerowanie bardziej efektywnego kodu dziki unikniciu przydzielania pamici i jej pniejszego odczytywania.

Agregaty
Moliwe jest uycie modyfikatora const w odniesieniu do agregatw, ale jest waciwie pewne, e kompilator nie zadziaa na tyle przemylnie, by umieci agregat w tablicy symboli, w zwizku z czym przydzieli mu pami. W takich przypadkach pojcie staej oznacza: obszar pamici, ktry nie moe by zmieniany". Jednak jego wartoci nie wolno uywa w czasie kompilacji, poniewa nie mona wymaga od kompilatora, aby zna zawarto pamici w czasie tego procesu. W zamieszczonym poniej programie zaznaczono instrukcje, ktre nie s dozwolone:

//: C08:Constag.cpp // Stae a agregaty const int 1[] - { 1. 2. 3. 4 );

struct S { int i. j; }; const S s[] - { { 1. 2 }. { 3. 4 } };


int main() {} / / / : -

//! float f[i[3]]; // Niedozwolone

//! double d[s[l].j]; // Niedozwolone

Aby ulokowa zdefiniowan tablic, kompilator musi by w stanie wygenerowa kod, przesuwajcy wskanik stosu. W obu niedozwolonych definicjach tablic (przedstawionych powyej) kompilator zgasza sprzeciw, poniewa nie potrafi znale w nich staego wyraenia.

Rozdzia 8. State

271

Rnice w stosunku do jzyka C


Stae zostay wprowadzone do wczesnych wersji jzyka C++, gdy specyfikacja standardujzyka C wcijeszcze nie bya ukoczona. Mimo e komitet standaryzacyjnyjzyka C zdecydowa pniej o wprowadzeniu do tegojzyka staych, to nada im znaczenie zwykych zmiennych, ktrych wartoci nie mog by zmieniane". W jzyku C stae zawsze zajmujpami, a ich nazwy s globalne. Kompilatorjzyka C nie moe traktowa staejjako staej dostpnej w trakcie kompilacji. Po zapisaniu wjzyku C: const int bufsize = 100: char buf[bufs1ze]; otrzymamy komunikat o bdzie, mimo e taki zapis wydaje si racjonalny. Poniewa staa bufsize znajduje si gdzie w pamici, kompilator C nie moe zna jej wartoci w czasie kompilacji. Opcjonalnie, mona napisa w jzyku C:

const int bufsize;


ale nie jest to dozwolone w jzyku C++. Kompilator jzyka C przyjmuje taki zapis jako deklaracj obszaru pamici, przydzielonego w jakim innym miejscu programu. To dopuszczalne, poniewa w jzyku C stae s domylnie czone zewntrznie. W jzyku C++ domylnie w stosunku do staych jest stosowane wewntrzne czenie; chcc zatem uzyska taki sam efekt w C++, naley jawnie zmieni sposb wizania na zewntrzny, uywajc do tego celu sowa kluczowego extern: extern const int bufsize; // Tylko deklaracja Zapis taki jest poprawny rwnie wjzyku C. W jzyku C++ stae nie zawsze zajmuj pami, w przeciwiestwie do jzyka C. O tym, czy pami jest w jzyku C++ rezerwowana dla staej, czy te nie, decyduje sposb uywania tej staej. Na ogjeeli jest ona uywana w celu zastpienia nazwy wartoci (w taki sposb, jak byaby uywana dyrektywa #define), to nie ma koniecznoci przydzielania staej pamici. Jeeli pami nie jest przydzielana (zaley to od zoonoci typu danych i przemylnoci kompilatora), wartoci mog zosta umieszczone dla uzyskania wikszej efektywnoci bezporednio w kodzie, po dokonaniu kontroli typw, a nie przed ni, jak w przypadku uycia dyrektywy #define. Jeeli jednak pobierany jest adres staej (nawet niejawnie, podczas wywoania funkcji pobierajcej argument przekazywany przez referencj) albo staa ta zostanie zdefiniowana z uyciem sowa kluczowego extern, wwczas przydzielanajestjej pami. Wjzyku C++ stae znajdujce si poza ciaami wszystkich funkcji posiadaj zasig ograniczony do pliku (nie s one poza tym plikiem widoczne). Oznacza to, e podlegaj one domylnie czeniu wewntrznemu. Jest to zupenie inaczej ni w przypadku wszystkich pozostaych identyfikatorw wjzyku C++ (i staych w C!), ktre sdomylnie czone zewntrznie. Tak wic, jeeli zadeklaruje si w dwch rnych plikach stae o tych samych nazwach i nie zostan wykorzystane ich adresy ani nie zadeklaruje si ich przy uyciu sowa kluczowego extern, to idealny kompilatorjzyka C++ nie przydzieli tym staym pamici, umieszczajc przypisane im wartoci bezporednio w kodzie programu. Poniewa stae maj domylnie zasig pliku, mona umieci je w pIiku nagwkowym programu wjzyku C++, nie powodujc konfliktw w czasie czenia.

72

Thinking in C++. Edycja polska Z uwagi na to, e stae s w jzyku C++ domylnie czone wewntrznie, nie mona zadeklarowa staej w jednym pliku, a nastpnie odwoywa si do niej jako do obiektu zewntrznego w drugim pliku. Dla zapewnienia staej czenia zewntrznego, umoliwiajcego odwoanie si do niej w innym pliku, trzeba jawnie j zdefiniowa przy uyciu sowa kluczowego extern, jak w poniszym przykadzie:
extern const int x = 1;

Zwr uwag, e przypisanie staej wartoci i zadeklarowanie jej jako zewntrznej wymusza utworzenie dla niej pamici (chocia kompilator nadal ma w takim przypadku moliwo dokonania skadania staych). Inicjalizacja okrela, e jest to definicja, a nie deklaracja. Deklaracja:
extern const int x;

oznacza bowiem w jzyku C++, e definicja staej znajduje si w jakim innym miejscu (w jzyku C niekoniecznie musi by to prawd). Wiadomo zatem, dlaczego jzyk C++ wymaga, by definicja staej posiadaa inicjator pozwala on na odrnienie deklaracji od definicji (w jzyku C jest to zawsze definicja, wic inicjator nie jest potrzebny). W przypadku deklaracji extern const kompilator nie moe dokona skadania staych, poniewa nie zna on jej wartoci. Ujcie staych, prezentowane w jzyku C, nie jest zbyt uyteczne. Jeeli wic zamierzasz uywa wewntrz wyraenia staego (takiego, ktre musi by obliczone podczas kompilacji) nazwy, posiadajcej przypisan warto, to jzyk C niemal zmusza ci do uywania dyrektywy #define preprocesora.

Wskaniki
Wskaniki rwnie mog by staymi. Majc do czynienia ze staymi, bdcymi wskanikami, kompilator nadal usiuje unikn przydzielenia im pamici i dokonuje skadania staych, ale waciwoci te wydaj si mie w tym przypadku mniejsze znaczenie. Waniejsze jest, e kompilator poinformuje ci o prbie zmiany staej, bdcej wskanikiem, co znacznie zwiksza poziom bezpieczestwa programu. Podczas stosowania modyfikatora const ze wskanikami istniej dwie moliwoci moe on by zastosowany do tego, co wskazuje wskanik, albo do adresu przechowywanego w samym wskaniku. W obu tych przypadkach skadnia wydaje si na pierwszy rzut oka do niejasna, ale w praktyce okae si ona wygodna.

Wskaniki do statych
Podobnie jak w kadej skomplikowanej definicji, rwnie w przypadku definicji wskanikw sztuka polega na tym, by odczytywa j zaczynajc od identyfikatora, a nastpnie przesuwa si stopniowo na zewntrz. Modyfikator const jest zwizany z tym, co znajduje si w definicji najbliej niego". Dlatego te, aby zapobiec wszelkim zmianom wskazywanego elementu, naley zapisa nastpujcdefinicj:

RozdziaS. State const int* u;

273

Rozpoczynajc od identyfikatora, odczytujemy: u jest wskanikiem, wskazujcym na element typu const int (sta cakowit)". W tym przypadku nie jest konieczna adna inicjaIizacja, poniewa wedug definicji wskanik u moe wskazywa dowolny element (wskanik nie jest stay), ale nie moe on by zmieniany. W tym miejscu zaczynajsi trudnoci. Moe si wydawa, e, aby uczyni wskanik samym w sobie niezmiennym, to znaczy, uniemoliwi zmian adresu, pamitanego wewntrz u, naley po prostu przenie sowo const na drug stron sowa kluczowegoint,jakponizej:
int const* v;

Odczytywanie takiego zapisu jako: v jest staym wskanikiem do liczby cakowitej" nie jest zupenie pozbawione logiki. Jednake w rzeczywistoci oznacza on: v jest zwykym wskanikiem do liczby cakowitej, ktrajest akurat sta". Wynika z tego, e modyfikator const zosta ponownie zwizany ze sowem kluczowym int i w rezultacie uzyskalimy identyczn definicj jak poprzednio. Fakt, e obie te definicje s takie same, jest z pewnoci irytujcy aby zapobiec podobnym odczuciom osoby czytajcej program, lepiej jest poprzesta na pierwszej wersji definicji.

State wskaniki
Aby wskanik sam w sobie uczyni staym, naley umieci modyfikator const po prawej stronie znaku *", jak w poniszym przykadzie:

int d - 1; int* const w - &d;


Obecnie odczytuje si ten zapis nastpujco: wjest wskanikiem bdcym sta, ktry wskazuje liczb cakowit". Poniewa sam wskanik jest obecnie sta, to kompilator wymaga, by zostaa mu nadana warto inicjujca, ktra pozostanie niezmieniona przez cay czas jego ycia. Mona jednak zmodyfikowa wskazywan przez niego warto, zapisujc:
*w = 2;

Mona rwnie utworzy stay wskanik, wskazujcy obiekt bdcy sta, uywajc jednej ze znajdujcych si poniej, dopuszczalnych postaci:

const int* const x = &d: // (1) int const* const x2 = &d: // (2)
Obecnie aden z tych wskanikw ani jakikolwiek ze wskazywanych przez nie obiektw nie moe ulec zmianie. Wedug niektrych opinii druga z wymienionych powyej postaci definicji jest bardziej spjna, poniewa sowo const jest umieszczone zawsze po prawej stronie tego, co modyfikuje. Musisz sam zdecydowa o tym, co jest bardziej przejrzyste z punktu widzenia uywanego przez ciebie stylu programowania.

int d = 1:

274

Thinking in C++. Edycja polska A oto powysze wiersze, umieszczone w moliwym do skompilowania pliku: //: C08:ConstPointers.cpp const int* u; int const* v; int d = 1; int* const w - &d; const int* const x = &d; // (1) int const* const x2 = &d; // (2) int main() {} ///:-

Formatowanie
W niniejszej ksice zosta pooony szczeglny nacisk na to, by w kadym wierszu kodu znajdowaa si tylkojedna definicja wskanika, a wskaniki byy inicjalizowane w miejscu definicji, ilekro to moliwe. Dziki temu moliwe jest zastosowanie stylu formatowania, polegajcego na doklejeniu" znaku *" do typu danych: int* u = & i : jak gdyby int* byo samo w sobie odrbnym typem danych. W rezultacie kod wydaje si atwiejszy do zrozumienia, ale nie jest to, niestety, sposb zgodny z rzeczywistoci. W istocie symbol *" jest zwizany z identyfikatorem, a nie z typem. Moe by umieszczony w dowolnym miejscu, pomidzy typem i identyfikatorem. Mona wic zapisa definicj:
i nt *u - &i, v 0;

tworzc, jak poprzednio, wskanik int* u oraz zmienn int v, niebdc wskanikiem. Poniewajest to mylce dla osoby czytajcej kod, lepiej uywa konwencji stosowanej w ksice.

Przypisanie a kontrola typw


Jzyk C++ jest bardzo skrupulatny pod wzgldem kontroli typw; obejmuje to rwnie operacje przypisania wskanikw. Mona przypisa wskanikowi staej adres obiektu niebdcego sta, poniewa przypisanie takie wie si po prostu z obietnic niezmieniania czego, co i tak moe by modyfikowane. Jednake nie wolno przypisa adresu obiektu bdcego sta do wskanika do obiektu niebdcego sta, poniewa oznaczaoby to moliwo modyfikacji obiektu za porednictwem wskanika. Oczywicie, zawsze mona wymusi takie przypisania, uywajc rzutowa, ale jest to przykad zego stylu programowania. Narusza si bowiem w ten sposb niezmienno obiektw bdcych staymi, a take bezpieczestwo zapewnione przez sowo kluczowe const. Na przykad:
/ / : C08:PointerAssignmeni.cpp

const int e - 2: int* u - &d: // W porzdku - d nie jest sta //! int* v - &e: // Niedozwolone - e jest stal int* w = (int*)&e; // Dozwolone, ale jest to z1y styl int main() {} ///:-

int d = 1:

Rozdzia 8. State

275

Mimo ejzyk C++ pomaga w zapobieganiu bdom, to nie uchroni ci przed konsekwencjami w przypadku, gdy chcesz naruszy mechanizmy bezpieczestwa.

Literaty napisowe
Miejscem, w ktrym niezmienno nie jest bezwzgldnie egzekwowana, s literay napisowe (literay bdce tablicami znakowymi). Mona wic zapisa:
char* cp = "czesc";

i kompilator przyjmie to bez protestu. Z technicznego punktu widzenia jest to bd, poniewa literay napisowe (w tym przypadku cze") s tworzone przez kompilator jako stae tablice znakowe, a wartoci zwracan przez tablic znakw ujtych w cudzysw jest adres jej pocztku w pamici. Modyfikacja jakiegokolwiek znaku, znajdujcego si w takiej tablicy, jest bdem wykonania programu, chocia nie wszystkie kompilatory egzekwujto w poprawny sposb. Tak wic literay napisowe s w rzeczywistoci staymi tablicami znakowymi. Oczywicie, kompilator pozwala traktowa je tak, jakby nie byy staymi, z uwagi na to, e wykorzystuje to znaczna ilo istniejcego kodu napisanego w jzyku C. Jeeli jednak sprbujesz zmieni wartoci zawarte w literale napisowym, to wynik takiej operacji jest niezdefiniowany, chocia bdzie ona prawdopodobnie dziaa w wielu komputerach. Jeeli jednak zamierzasz modyfikowa acuch, to umie go w tablicy:
char cp[] = "czesc";

Poniewa jednak kompilatory czsto nie wymuszaj takiego rozrnienia, nie bd nalega na uywanie tej drugiej postaci definicji, w zwizku z czym caa kwestia staje si do trudno uchwytna.

Argumenty funkcji i zwracane wartoci


Uycie modyfikatora const do specyfikacji argumentw funkcji i zwracanych przez nie wartoci jest jeszcze jedn sytuacj, w ktrej pojcie staych moe wydawa si niezrozumiae. W przypadku przekazywania obiektw przez warto okrelenie ich jako staych nie ma dla klienta funkcji adnego znaczenia (oznacza, e przekazywanego argumentu nie woIno zmienia wewntrz funkcji). Jeeli funkcja zwraca przez warto, jako sta, obiekt typu zdefiniowanego przez uytkownika, oznacza to, e zwracana warto nie moe by modyfikowana. Jeeli przekazywany Iub zwracany jest adres, to modyfikator const stanowi przyrzeczenie, e warto znajdujca si pod tym adresem nie bdzie zmieniana.

Przekazywanie statej przez warto


Przekazujc funkcji argumenty przez warto, mona okreli, e s one staymi, jak w poniszym przypadku:

276
void fl(const int i) { i++; // Niedozwolone - bd podczas kompilacji }

Thinking in C++. Edycja polska

C to jednak oznacza? Zoono obietnic, e oryginalna warto zmiennej nie zostanie zmodyfikowana przez funkcj fl(). Poniewajednak argumentjest przekazywany przez warto, natychmiast tworzonajest kopiajego oryginalnej wartoci, wic obietnica zoona klientowi jest i tak domylnie dotrzymywana. Natomiast modyfikator const wewntrz funkcji oznacza, e jej argument nie moe by zmieniany. Jest to wic w rzeczywistoci narzdzie suce twrcy funkcji, a nie komu, ktrajwywouje. Aby nie spowodowa dezorientacji osoby wywoujcej funkcj, mona uczyni argument sta wewntrz funkcji, a nie na licie jej argumentw. Mona uczyni to za pomoc wskanika, ale bardziej elegancki zapis uzyskuje si dziki referencji (temat referencji zostanie gruntownie opisany rozdziale 11.). Krtko mwic, referencja przypomina stay wskanik, na ktrym automatycznie dokonuje si wyuskania, dziki czemu w efekcie staje si on synonimem obiektu. Aby utworzy referencj, naley uy w definicji symbolu &. Tak wic niewywoujca dezorientacji definicja funkcji jest nastpujca:
void f2(int ic) { const int& i = ic; i++: // Niedozwolone - bd podczas kompilacji }

W czasie kompilacji zostanie ponownie zgoszony bd, ale tym razem bdzie on wynika z niezmiennoci lokalnego obiektu, niebdcego elementem sygnatury funkcji ma on znaczenie jedynie dla jej implementacji i dlatego wanie zosta ukryty przedjej klientem.

Zwracanie statej przez warto


Podobnie dzieje si w przypadku wartoci zwracanej przez funkcj. Jeeli napiszemy, e warto zwracana przez funkcj jest staa:
const int g():

stanowi to obietnic, e oryginalna zmienna (znajdujca si wewntrz funkcji) nie zostanie zmieniona. Poniewa jednak jest ona ponownie zwracana przez warto, wykonywanajestjej kopia, dziki czemu pocztkowa warto nie moe by nigdy zmieniona za porednictwem zwracanej wartoci. Na pierwszy rzut oka moe si wydawa, e okrelenie zwracanej przez funkcj wartoci jako staej jest pozbawione sensu. Pozorny brak efektu zwracania staej przez warto staej ilustruje poniszy przykad:
/ / : C08:Constval.cpp // Zwracanie staej przez warto nie ma // znaczenia w pnypadku typw wbudowanych

Rozdzia 8. State int f3() { return 1; } const int f4() { return 1; } int main() { const int j = f3(); // Dziaa znakomicie int k = f4(); // Ale to rwnie dziaa znakomicie!

27

W przypadku typw wbudowanych nie ma adnego znaczenia, czy zwracana wai to jest sta. Dlatego te naley unika wprawiania w zakopotanie klienta programist, pomijajc modyfikator const w przypadku, gdy funkcja zwraca war to wbudowanego typu. Zwracanie przez warto staych jest wane w przypadku typw zdefiniowanycl przez uytkownika. Jeeli funkcja zwraca przez warto obiekt jako sta, to zwrco na warto nie moe by l-wartoci (to znaczy nie mona do niej dokona przypisa nia ani zmodyfikowajej wjaki inny sposb). Na przykad: //: C08:ConstReturnValues.cpp // Staa zwracana przez warto // Wynik nie moe by uyty jako l-warto class X { int i : public: X(int ii = 0); void modify();
X : : X ( i n t ii) { i = ii; } void X : :modify() { i++; }

X f5() { return X ( ) ;

const X f6() { return X(); // Przekazanie przez referencje obiektu // nie bdcego sta: void f7(X& x) { x.modify(); int main() { f5() = X(1); // W porzdku - zwracana warto nie jest sta f5O.modifyO; // W porzdku // Instrukcje wywoujce bdy podczas kompilacji: //! f7(f5O); //! f6() = X(1); //! f6().modifyO; //! f7(f60);

78

Thinking in C++. Edycja polska

Funkcja f5() zwraca obiekt X niebdcy sta, natomiast funkcja f6() obiekt X bdcy sta. W charakterze l-wartoci moe by uyta jedynie taka zwracana warto, ktra nie jest sta. Tak wic istotne jest stosowanie modyfikatora const w przypadku, gdy obiekt jest zwracany przez warto i chcemy zapobiec uywaniu go jako l-wartoci. Modyfikator const nie ma znaczenia w przypadku typw wbudowanych dlatego, e kompilator nie dopuszcza do uywania ich jako l-wartoci (poniewa zawsze s one wartociami, a nie zmiennymi). Kwestia ta ma znaczenie dopiero w przypadku, gdy zwracane s przez warto obiekty typw zdefiniowanych przez uytkownika. Funkcja f7() pobiera argument niebdcy sta poprzez referencj (dodatkowy sposb obsugi adresw w j z y k u C++, opisany w rozdziale 11.). Jest to niemal rwnowane pobraniu wskanika do obiektu niebdcego sta rnica polegajedynie na skadni. Powodem, dla ktrego instrukcja ta nie kompiluje si w jzyku C++, jest tworzenie obiektu tymczasowego.

Obiekty tymczasowe
Czasami, w czasie obliczania wyraenia, kompilator jest zmuszony do utworzenia obiektw tymczasowych. S one podobne do innych zajmuj pami, a take musz by utworzone oraz zniszczone. Rnica polega na tym, e nie s one widoczne kompilator decyduje o tym, kiedy s one potrzebne, i zajmuje si szczegami dotyczcymi ich istnienia. Jest jednak pewna istotna kwestia, zwizana z obiektami tymczasowymi s one tworzone automatycznie jako stae. Z uwagi na to, e zazwyczaj nie istnieje moliwo wykonywania operacji na obiektach tymczasowych, nakazanie zrobienia czego, co zmienioby obiekt tymczasowy, jest z pewnoci pomyk, poniewa nie ma sposobu na to, by pniej wykorzysta t informacj. Dziki temu, e wszystkie obiekty tymczasowe staj si automatycznie staymi, kompilator ma moliwo sygnalizowania takich pomyek. W powyszym przykadzie funkcja f5() zwraca obiekt typu X niebdcy sta, Jednak; w wyraeniu: wvrazeniu:
f7(f50);

kompilator musi utworzy tymczasowy obiekt, przechowujcy warto, zwracan przez funkcj f5(), by mona j byo przekaza funkcji f7(). Problemu nie istniaby, gdyby funkcja f7() pobieraa argument przez warto w takim przypadku obiekt tymczasowy byby kopiowany w funkcji f7() i jego dalsze losy nie miayby znaczenia. Jednake funkcja f7() pobiera swj argument przez referencj, co w tym przypadku oznacza, e pobiera ona adres tymczasowego obiektu typu X. Poniewa argument pobierany przez funkcj f7() nie jest referencj do staej, funkcja ta ma moliwo zmiany obiektu tymczasowego. Jednak kompilator wie, e obiekt tymczasowy przestanie istnie zaraz po zakoczeniu obliczania wyraenia, a zatem wszelkie dokonane w nim zmiany zostan utracone. Dziki temu, e wszystkie obiekty tymczasowe s domylnie obiektami staymi, sytuacje tego typu powoduj bdy na etapie kompilacji, co umoliwia uniknicie pomyek trudnych do wykrycia. Warto jednak zwrci jeszcze uwag na wyraenia, ktre s dozwolone:

Rozdzia 8. State
f5() - X(1); f5().modify():

279

Mimo e s one poprawne z punktu widzenia kompilatora, to w rzeczywistoci budz one wtpliwoci. Funkcja f5() zwraca obiekt typu X, natomiast kompilator, aby zrealizowa powysze wyraenia, musi utworzy obiekt tymczasowy, przechowujcy zwracan warto. Tak wic w obu wyraeniach modyfikowane s obiekty tymczasowe, ktre s usuwane natychmiast po zakoczeniu zwizanych z tymi wyraeniami operacji. W rezultacie dokonane zmiany s tracone, a wic powyszy kod jest prawdopodobnie bdny aIe kompilator nie zgosi w tym przypadku adnych uwag. Wyraenia takie, jak widoczne powyej, s dostatecznie proste, aby mona byo samodzielnie odkry zwizane z nimi problemy. Jednak w bardziej skomplikowanych przypadkach mona przeoczy bdy. Sposb, w jaki chroniona jest niezmienno obiektw klas, zosta opisany w dalszej czci rozdziau.

Przekazywanie i zwracanie adresw


W przypadku przekazywania lub zwracania adresw (zarwno za pomoc wskanikw, jak i referencji) moliwe jest wykorzystanie ich przez klienta-programist do zmodyfikowania wskazywanej przez nie wartoci. Aby temu zapobiec, naley uczyni wskanik lub referencj staymi, co moe uchroni ci od wielu nieszcz. Waciwie ilekro przekazujesz funkcji adres, powiniene okreli, e zawiera on sta o ile jest to moliwe. W przeciwnym przypadku wykluczysz moliwo uywania takiej funkcji w stosunku do staych. Decyzja, czy funkcja powinna zwraca wskanik, czy te referencj do staej, zaley od tego, na jakie dziaania w stosunku do niej zamierzasz pozwoli klientowiprogramicie. Poniszy przykad prezentuje uycie wskanikw do staych zarwnojako argumentw funkcji,jak i zwracanych przez nie wartoci:

//: C08:ConstPointer.cpp // Wskaniki do staych jako argumenty funkcji // i zwracane przez nie wartoci void t(int*) {} void u(const int* cip) { //! *cip = 2; // Niedozwolone - modyfikacja wartoci int i = *cip; // W porzdku - kopiowanie wartoci //! int* ip2 = cip; // Niedozwolone - nie jest to stal
const char* v() { // Zwraca adres statycznej tablicy znakw: return "wynik funkcji v O " ;

const int* const wC) static int i ; return &i ;

280

Thinking in C++. Edycja polska int main() { int x = 0; int* ip = &x; const int* cip - &x; t(ip); // W porzdku / / ! t(cip); // le u(ip); // W porzdku u(cip); // Rwnie w porzdku / / ! char* cp = v ( ) ; // le const char* ccp = v(); // W porzdku / / ! int* ip2 = w(); // le const int* const ccip = w ( ) ; // W porzdku const int* cip2 = w(); // W porzdku //! *w() = 1; // le } lll:~

Funkcja t() pobiera jako argument zwyky wskanik (niewskazujcy staej), natomiast funkcja u() pobiera wskanik do staej. Jak mona dostrzec wewntrz funkcji u(), prba modyfikacji wskazywanej przez argument wartoci nie jest dozwolona, ale mona, oczywicie, przepisa wskazywanprzez niego warto do zwykej zmiennej. Kompilator zapobiega rwnie utworzeniu wskanika niebdcego wskanikiem do staej, ktry wykorzystuje adres przechowywany wewntrz wskanika do staej. Funkcje v() oraz w() umoliwiaj sprawdzenie znaczenia zwracanych wartoci. Funkcja v() zwraca warto typu const char*, utworzon na podstawie literau napisowego. Instrukcja ta zwraca w rzeczywistoci adres literau napisowego po tym, jak zostanie on utworzony przez kompilator i zapisany w obszarze danych statycznych. Jak ju wspomniano, tablica ta jest z technicznego punktu widzenia staa, co zostao prawidowo wyraone za pomocwartoci zwracanej przez funkcj v(). Warto zwracana przez funkcj w() okrela, e zarwno wskanik, jak i to, co on wskazuje, muszby staymi. Podobniejak w przypadku funkcji v(), warto zwracana przez funkcj w() jest poprawna po powrocie z tej funkcji tylko dlatego, e jest ona statyczna. Nie chcemy nigdy zwraca wskanikw do lokalnych zmiennych umieszczonych na stosie, poniewa po powrocie z funkcji i oczyszczeniu stosu nie s ju one dostpne (innym powszechnie uywanym wskanikiem, ktry mgby zosta uyty, jest adres obszaru przydzielonego na stercie, dostpny po powrocie z funkcji). Poszczeglne funkcje s testowane, w obrbie funkcji main(), z rnymi argumentami. Funkcja t() przyjmuje jako argumenty wycznie wskaniki niewskazujce staych, natomiast w przypadku przekazania funkcji wskanika do staej nie mona zagwarantowa, e funkcja ta pozostawi nienaruszon warto wskazywan przez argument, dlatego te kompilator zgasza komunikat o bdzie. Poniewa funkcja u() pobiera wskanik do staej, akceptuje oba rodzaje argumentw. Tak wic funkcja pobierajca wskanik do staej jest funkcj bardziej ogln ni taka, ktra pobiera jako argument zwyky wskanik. Zgodnie z oczekiwaniami, warto zwracana przez funkcj v() moe by przypisana tylko wskanikowi do staej. Mona si rwnie spodziewa, e kompilator odrzuci przypisanie wartoci zwracanej przez funkcj w() wskanikowi niewskazujcemu staej, natomiast zaakceptuje przypisanie jej zmiennej typu const int* const, a take, co moe stanowi pewn niespodziank, zaakceptuje przypisanie jej do zmiennej typu const int*, ktra nie

Rozdzia 8. State

28

odpowiada dokadnie zwracanemu typowi. Z uwagi na to, e kopiowanajest warto (kt r jest adres przechowywany we wskaniku), zapewnienie, e wskazywana przez ni warto pozostanie nienaruszona, jest automatycznie podtrzymywane. Tak wiec druj modyfikator const w specyfikacji const int* const ma znaczenie tylko w przypadku u) cia gojako l-wartoci, co zostaoby uniemoliwione przez kompilator.

Standardowy sposb przekazywania argumentw


W jzyku C typowe jest przekazywanie argumentw przez warto, a w przypadk zamiaru przekazania adresu jedynym dostpnym rozwizaniem jest uycie wskani kow'. Jednake adne z tych podej niejest preferowane wjzyku C++. Najlepsz> mi zamiast nich sposobami przekazywania argumentw jest przekazywanie ich prze referencj oraz przez referencj do staej. Z punktu widzenia klienta-programist uywana skadnia jest identyczna ze skadni przekazywania argumentw przez wai to, unika si wic zamieszania wywoanego wskanikami. Z punktu widzenia twr cy funkcji przekazywanie adresu jest zawsze bardziej efektywne ni caego obiekt klasy, natomiast przekazywanie referencji do staej oznacza, e funkcja nie zmien wskazywanego przez ni obiektu, wic w efekcie, z punktu widzenia klienta-programist) taki sposb przekazywania argumentu bdzie dokadnie tym samym, co przekazywa nie go przez warto (zapewni tylko wiksz efektywno). Z uwagi na skadni referencji (z punktu widzenia wywoujcego ma t sam posta< jak przekazywanie przez warto) moliwe jest przekazanie tymczasowego obiekti funkcji, ktra pobiera referencj do staej, podczas gdy nie ma nigdy moliwo przekazania obiektu tymczasowego funkcji pobierajcej wskanik w przypadki wskanika musi by bowiemjawnie podany adres. Tak wic przekazywanie przez re ferencj tworzy zupenie now sytuacj, ktra nie wystpowaa nigdy w jzyku ( Obiekt tymczasowy, bdcy zawsze sta, moe przekaza funkcji swj adres Dlatego wanie, aby obiekty tymczasowe mogy by przekazywane funkcji przez refe rencje,jej argument musi by referencjdo staej. Demonstruje to poniszy przykad:

//: C08:ConstTemporary.cpp // Obiekty tymczasowe s staymi class X {}; X f() { return X(); } // Wynik zwracany przez warto void gl(X&) {} // Przekazywanie przez referencje nie do staej void g2(const X&) {} // Przekazywanie przez referencje do staej
int main() { // Bd: staly obiekt tymczasowy tworzony przez f(): / / ! gl(fO); // W porzdku: g2 pobiera referencje do staej: g2(fO); } lll:~ Niektrzy posuwajsi do twierdzenia, e vwzysrtojestwjezyku C przekazywane przez warto, poniewa w przypadku przekazywania wskanika, tworzonajestjego kopia (a zatem wskanikjest przekazywany przez warto). Jakkolwiek wydawaoby si to precyzyjne, myl, e w rzeczywistoci wprowadza zamieszanie w tej caej kwestii.

)2

Thinking in C++. Edycja polska Funkcja f ( ) zwraca obiekt klasy X przez warto. Oznacza to, e jeeli bezporednio przekae si warto zwracan przez funkcj f() innej funkcji, jak w wywoaniu funkcji gl() i g2(), zostanie utworzony obiekt tymczasowy, i obiekt ten jest sta. Wywoanie funkcji gl() jest bdem, poniewa funkcja ta nie pobiera referencji do staej, natomiast wywoanie funkcji g2() jest poprawne.

(lasy
W podrozdziale opisano, w jaki sposb uywa modyfikatora const w stosunku do klas. Mona utworzy w obrbie klasy lokaln sta, wykorzystywan w staych wyraeniach, ktre bd wyliczane podczas kompilacji. Jednake znaczenie modyfikatora const wewntrz klas jest nieco odmienne, dlatego te, aby tworzy stae dane skadowe klasy, trzeba najpierw zrozumie wszystkie dostpne moliwoci. Mona rwnie uczyni cay obiekt sta Qak si przekonalimy, kompilator zawsze tworzy stae obiekty tymczasowe). Jednak kwestia zapewnienia niezmiennoci obiektw jest bardziej zoonym zagadnieniem. Kompilator moe zapewni niezmienno wbudowanego typu, ale nie jest w stanie ledzi zawioci klas. W celu zagwarantowania niezmiennoci obiektu klasy wprowadzono stae funkcje skadowe jedynie takie funkcje mogby wywoywane w stosunku do obiektu bdcego sta.

5tate w klasach
Jednym z miejsc, w ktrych z pewnoci zechcesz wykorzysta stae umieszczone w wyraeniach staych, s klasy. Typowym tego przykadem jest tablica tworzona w obrbie klasy; do okrelenia jej wielkoci zamiast dyrektywy #define wolno uy staej, wykorzystujc j rwnie w obliczeniach odwoujcych si do tej tablicy. Wielko tablicy jest czym, co mona by ukry wewntrz klasy. Dziki temu uywajc do jej okrelenia takiego identyfikatora, jak np. size, mona by zastosowa go rwnie w innych klasach, bez obawy wywoania konfliktw nazw. Preprocesor traktuje wszystkie symbole zdefiniowane za pomoc dyrektywy #define jako globalne od miejsca wystpienia ich definicji, wic uywajc ich nie mona uzyska podanego rezultatu. Zamy, e umieszczenie staej wewntrz klasy jest decyzj logiczn. Niestety, nie zapewnia ona uzyskania oczekiwanego efektu. W obrbie klas stae w pewnym stopniu powracaj do znaczenia, jakie miay one w jzyku C. Wewntrz kadego obiektu jest im przydzielana pami i reprezentujone wartoci, ktre sjednokrotnie inicjalizowane i nie mogju by pniej zmieniane. Uycie modyfikatora const wewntrz klasy oznacza: ,jest to warto staa przez cay czas ycia obiektu". Jednak wartoci tej staej w rnych obiektach mog by zrnicowane. Tak wic tworzc wewntrz klasy zwyk (nie statyczn) sta, nie mona jej nada wartoci pocztkowej. Oczywicie, inicjalizacjataka musi nastpi w konstruktorze, ale suy do tego specjalna cz konstruktora. Poniewa sta naley zainicjowa w miejscu, w ktrymjest tworzona, musi ona byju zainicjowana wewntrz ciaa konstruktora. W przeciwnym przypadku mona by wstrzyma si z inicjalizacj do jakiego

Rozdzia 8. State

283

miejsca w dalszej czci ciaa konstruktora, co oznaczaoby, e staa bya przez pewien czas niezainicjowana. W takim przypadku nic rwnie nie mogoby powstrzyma programisty przed zmian wartoci staej w rnych miejscach konstruktora.

Lista inicjatorw konstruktora


To szczeglne miejsce, w ktrym odbywa si inicjalizacja, nazywane jest list inicjatorw konstruktora i powstao pierwotnie z myl o dziedziczeniu (opisanym w rozdziale 14.). Lista inicjatorw konstruktora ktra, jak wynika z nazwy, wystpuje wycznie w definicji konstruktora jest list wywoa konstruktora", umieszczon po licie jego argumentw i dwukropku, a przed nawiasem klamrowym rozpoczynajcym ciao konstruktora. Przypomina to o tym, e inicjalizacje znajdujce si na licie odbywajsijeszcze przed wykonaniemjakiegokolwiek kodu, wchodzcego w skad konstruktora. Jest to miejsce, w ktrym lokuje si wszystkie inicjalizacje staych. Poniej przedstawiono przykad prawidowej inicjalizacji staych, znajdujcych si wewntrz klasy: / / : C08:ConstInitialization.cpp // Inicjalizacja staych w klasach #include <iostream> using namespace std; class Fred { const int size; public: Fred(int sz); void print(); Fred::Fred(int sz) void Fred::print() size(sz) {} cout size endl:

int main() { Fred a(l), b(2). c(3); a.print(), b.print(), c.print(); } IIIPrzedstawiona powyej posta listy inicjatorw konstruktora wyda ci si pocztkowo dziwna, poniewa nie znaszjeszcze typu wbudowanego, traktowanegojakby posiada konstruktor.

Konstruktory" typw wbudowanych


W miar rozwoju jzyka i wysikw czynionych w celu upodobnienia typw zdefiniowanych przez uytkownika do typw wbudowanych stao si oczywiste, e w niektrych sytuacjach typy wbudowane powinny dziaa podobnie do typw zdefiniowanych przez uytkownika. Na licie inicjatorw konstruktora typ wbudowany moe by traktowany w taki sposb, jakby mia konstruktor:
/ / : C08:BuiltInTypeConstructors.cpp #include <iostream> using namespace std:

Thinking in C++. Edycja polska

class B { int 1 ; public: B(int ii): void print();


B::B(int ii) : i(ii) {} void B::print() { cout i << endl: }

int main() { B a(l). b(2):

cout << pi << endl ; } /ll:~

float pi(3.14159); a.print(); b.printO;

Ma to szczeglne znaczenie podczas inicjalizacji danych skadowych, bdcych staymi, poniewa muszone zosta zainicjowanejeszcze przed wejciem do ciaa funkcji. Rozszerzenie tego konstruktora", przeznaczonego dla typw wbudowanych (w tym przypadku oznacza on przypisanie), na przypadki oglne wydaje si logiczne, dziki czemu definicja float pi(3.14159), widoczna w powyszym kodzie, dziaa prawidowo. Czsto przydatnejest zamknicie wbudowanego typu w klasie, co gwarantuje mu inicjalizacj za pomoc konstruktora. Poniszy przykad przedstawia utworzon w taki sposb klas Integer: //: C08:EncapsulatingTypes.cpp #include <iostream> using namespace std;

class Integer {

int i ; public: Integer(int ii - 0); void print();


Integer::Integer(int ii) : i(ii) {} void Integer::print() { cout << i << ' '; } int main( ) { Integer i[100];

for(int j - 0; j < 100: j++) i[j].print(): } III-

Wszystkie elementy tablicy obiektw klasy Integer, zdefiniowanej w funkcji main( ), s automatycznie inicjalizowane wartoci zerow. Inicjalizacja taka niekoniecznie musi by bardziej kosztowna ni przeprowadzona w ptli for lub za pomoc funkcji memset( ). Wiele kompilatorw bez trudu optymalizuje taki kod, dziki czemu cay proces odbywa si bardzo szybko.

Rozdzia 8. State

285

State o wartociach okrelonych podczas kompilacji, zawarte w klasach


Zaprezentowany powyej sposb uycia staych jest interesujcy i prawdopodobnie przydatny w wielu przypadkach. Jak jednak rozwiza pierwotny problem, zawarty w pytaniu: w jaki sposb utworzy wewntrz klas stae o wartociach okrelonych podczas kompilacji?". Odpowied wymaga uycia dodatkowego sowa kluczowego static, ktre zostanie wyczerpujco opisane w rozdziale 10. W tym przypadku sowo kluczowe static oznacza: istnieje tylko jeden egzemplarz, niezalenie od tego, ile utworzono obiektw tej klasy". O to nam wanie chodzio o skadow klasy, bdc sta, ktra ma tak sam warto dla wszystkich obiektw tej klasy. Tak wic statyczne stae wbudowanych typw danych mog by traktowane jako stae o wartociach okrelonych podczas kompilacji. Istnieje pewna niezwyka cecha staych statycznych, uywanych wewntrz klas warto inicjujca musi by im dostarczona w miejscu, w ktrym zostay one zdefiniowane. Dzieje si tak tylko w przypadku staych statycznych jeliby taki sposb inicjalizacji zosta uyty w stosunku do innych danych skadowych, nie bdzie on poprawny, poniewa wszystkie pozostae dane skadowe klasy musz by zainicjowane w konstruktorze lub w innych funkcjach skadowych. W poniszym przykadzie przedstawiono sposb utworzenia i wykorzystania statycznej staej size wewntrz klasy reprezentujcej stos wskanikw do acuchw : / / : C08:StringStack.cpp // Wykorzystanie stalej statycznej do utworzenia // wewna,trz klasy stalej o wartoci okrelonej // podczas kompilacji #include <string> #include <iostream> using namespace std; class StringStack { static const int size = 100; const string* stack[size]: int index; public: StringStack(); void push(const string* s); const string* pop(); StringStack::StringStack() : index(0) { memset(stack. 0. size * sizeof(string*)); void StringStack::push(const string* s) if(index < size) stack[index++] = s; Kledy pisany by niniejszy tekst, nie wszystkie kompilatory umoliwiay wykorzystywanie tej cechy.

J6

Thinking in C++. Edycja polska const string* StringStack::pop() { 1f(index > 0) { const string* rv - stack[--index]; stack[index] = 0: return rv; return 0; str1ng iceCream[] = { "bakal 1 owo - smi etankowe". "karmelowe", "migdaowe", "czarna porzeczka", "sorbet malinowy", "cytrynowe". "czekoladowe", "karmelowe w gorzkiej czekoladzie" const int iCsz sizeof iceCream / sizeof *iceCream; int main() { StringStack ss; for(int i = 0; i < iCsz; i++) ss.push(&iceCream[i]); const string* cp; while((cp = ss.popO) != 0) cout << *cp << endl ; Poniewa staa size zostaa uyta do okrelenia rozmiaru tablicy stack, jest ona rzeczywicie sta o wartoci okrelonej w czasie kompilacji, ale zostaa ukryta wewntrz klasy. Zwr uwag na to, e funkcja push( ) pobiera argument typu const string*, funkcja pop( ) zwraca warto typu const string*, a klasa StringStack przechowuje wartoci typu const string*. Gdyby tak nie byo, nie mona by uywa obiektu klasy StringStack do przechowywania wskanikw, zawartych w tablicy iceCream. Rwnoczenie uniemoliwia to zrobienie czegokolwiek, co zmienioby zawarto obiektw przechowywanych w StringStack. Oczywicie, nie wszystkie klasy-kontenery s projektowane z uwzgldnieniem takiego ograniczenia.
}

Wyliczeniowy wytrych" w starym kodzie


W starszych wersjachjzyka C++ stae statyczne nie mogy znajdowa si w klasach. Oznacza to, e staych nie dao si rwnie wykorzysta w wyraeniach staych, zawartych w klasach. Istniaa jednak potrzeba takiego stosowania staych, wic typowym rozwizaniem (nazywanym zazwyczaj wyliczeniowym wytrychem") byo uycie nienazwanego wyliczenia, nieposiadajcego egzemplarza zmiennej. Wyliczenie jest lokalne w stosunku do zawierajcej je klasy; jego wszystkie wartoci musz by okrelone w czasie kompilacji, a ponadto mona je wykorzysta w wyraeniach staych. Tak wic czsto wystpuje nastpujce zastosowanie wylicze:

Rozdzia 8. Stae //: C08:EnumHack.cpp #include <iostream> using namespace std; class Bunch { enum { size = 1000 }; int i[size];

287

};
int main() { cout << "sizeof(Bunch) = " << sizeof(Bunch) << ", sizeof(i[1000]) = " << sizeof(int[1000]) << endl; } ///:~ Prezentowany tutaj sposb uycia wyliczenia na pewno nie zajmuje pamici w obiekcie, a wszystkie wyliczenia s wyliczane w trakcie kompilacji. Mona rwnie w jawny sposb nada wartoci poszczeglnym elementom wyliczenia: enum { jeden = 1, dwa = 2, trzy }; W przypadku liczb cakowitych kompilator kontynuuje liczenie, zaczynajc od ostatniej nadanej wartoci, wic element wyliczenia trzy otrzyma warto 3. W zamieszczonym wczeniej pliku przykadowym StringStack.cpp wiersz: static const int size = 100; mgby mie posta:
enum { size = 100 };

Mimo e w starszych programach czsto mona spotka technik wykorzystujc wyliczenia, stae statyczne zostay dodane do jzyka wanie po to, aby rozwiza ten problem. Nie ma jednak adnego istotnego powodu, dla ktrego naleaoby uywa staych statycznych zamiast wytrychu wyliczeniowego". W przykadach zawartych w ksice jest on uywany tylko dlatego, e kiedy bya ona pisana, mechanizm ten obsugiwaa wiksza liczba kompilatorw.

Stae obiekty i funkcje skadowe


Funkcje skadowe klas mog by rwnie okrelone jako stae. C to oznacza? Aby to zrozumie, trzeba najpierw pozna pojcie staych obiektw. Stay obiekt typu zdefiniowanego przez uytkownika definiuje si tak samo, jak obiekt typu wbudowanego. Na przykad: const int i = 1; const blob b(2); Powysza instrukcja definiuje b jako stay obiekt typu blob. Jego konstruktor jest wywoywany z argumentem rwnym dwa. Aby kompilator mg zagwarantowa niezmienno obiektu, musi on zapewni, by w czasie ycia tego obiektu nie zostay

98

Thinking in C++. Edycja polska

zmienione adne jego dane skadowe. Nie sprawia to trudnoci w stosunku do publicznych danych klasy, ale skd wiadomo, ktre funkcje skadowe s bezpieczne" dla staych obiektw, a ktre zmieniaj ich dane? Deklarujc funkcj skadow z modyfikatorem const, informuje si kompilator, e funkcja ta moe zosta wywoana w stosunku do obiektu bdcego sta. Funkcja skadowa niezadeklarowana jawnie z tym modyfikatorem jest traktowana jako taka, ktra modyfikuje dane obiektu. Kompilator nie pozwoli na jej wywoanie w stosunku do staego obiektu. Na tym si jednak nie koczy. Ogloszenie, e funkcja skadowa jest staa", nie gwarantuje jeszcze, e bdzie ona dziaaa w taki wanie sposb. Kompilator wymusza wic powtrzenie modyfikatora const w definicji tej funkcji (sowo const staje si czci sygnatury funkcji, dziki czemu zarwno kompilator, jak i program czcy sprawdzaj jej stao"). Nastpnie kompilator wymusza nienaruszalno danych w obrbie definicji funkcji, zgaszajc bd w przypadku prby zmiany jakiejkolwiek skadowej lub wywoania funkcji nieoznaczonej modyfikatorem const. Tak wic istnieje gwarancja, e kada funkcja skadowa zadeklarowana jako staa" bdzie zachowywaa si w taki sposb w swojej definicji. Aby zrozumie skadni deklaracji staych funkcji skadowych, naley zwrci uwag na to, e poprzedzenie nazwy funkcji modyfikatorem const oznacza, e funkcja ta zwraca warto bdc sta, nie przynosi wic spodziewanego rezultatu. Zamiast tego naley umieci sowo kluczowe const po licie argumentw. Na przykad:

//: C08:ConstMember.cpp class X { int i ; public: X(int ii); int f() const; }:

X::X(int ii) : i(ii) {} int X::f() const { return i; } int main() { X xl(10); const X x2(20); xl.f(); x2.f();
Zwr uwag na to, e sowo kluczowe const musi by powtrzone w definicji funkcji, bowiem w przeciwnym razie kompilator uznajza definicj innej funkcji. Poniewa f( ) jest sta funkcj skadow, wic gdyby prbowaa ona w jakikolwiek sposb zmieni warto skadowej i lub wywoa inn funkcj skadow, ktra nie zostaa zadeklarowanajako staa, kompilator zgosiby bd. A zatem stae funkcje skadowe mog by wywoane zarwno w stosunku staych obiektw, jak i obiektw niebdcych staymi. Mona je zatem uzna za bardziej ogln posta funkcji skadowych (to niefortunne, e funkcje skadowe klas nie s

Rozdzia 8. State

288

domylnie funkcjami staymi). Kada funkcja, ktra nie modyfikuje danych, powinna zosta zadeklarowanajako funkcja staa, dziki czemu bdziejej mona uy w stosunku do staych obiektw klasy. Poniej zamieszczono przykad przedstawiajcy porwnanie staych i niestaych" funkcji skadowych:

//: C08:Quoter.cpp // Losowy wybr cytatu #include <iostream> #include <cstdlib> // Generator liczb losowych #include <ctime> // Oo inicjalizacji generatora liczb losowych using namespace std; class Quoter { int lastquote; public: Quoter(); int lastQuote() const: const char* quote(); }: Quoter::Quoter(){ lastquote = -1; srand(time(0)); // Inicjalizacja generatora liczb losowych int Quoter::lastQuote() const return lastquote: const char* Quoter:quote() { static const char* quotes[] = { "Czy mamy si juz smiac?". "Lekarze zawsze wiedza najlepiej", "Sowy nie sa tym. czym sie wydaja", "Strach ma wielkie oczy", "Nie ma naukowego dowodu " "na poparcie tezy " "ze zycie jest na serio", "To. co nas bawi, czyni nas mdrzejszymi", }: const int qsize = sizeof quotes/sizeof *quotes: int qnum = rand() % qsize: while(lastquote >= 0 && qnum == lastquote) qnum = rand() % qsize; return quotes[lastquote - qnum];
int main() { Quoter q; const Quoter cq; cq.lastQuoteO; // W porzdku / / ! cq.quote(): // le - nie jest to funkcja stala for(int i = 0; i < 20; i++) cout << q.quote() << endl^

30

Thinking in C++. Edycja polska

Ani konstruktor, ani destruktor nie mog by staymi funkcjami, poniewa zawsze dokonuj modyfikacji obiektu w czasie inicjalizacji oraz sprztania. Funkcja skadowa quote() rwnie nie jest staa, poniewa modyfikuje ona skadow lastquote (co wida w instrukcji return). Jednake funkcja lastQuote() nie dokonuje adnych zmian, wic moe ona by funkcj sta i mona j bezpiecznie wywoa dla staego obiektu cq.

nutable: niezmienno fizyczna kontra niezmienno logiczna


Co zrobi w przypadku, gdy zamierzasz utworzy sta funkcj sWadow, ale nadal chcesz zmienia w obiekcie pewne dane? Czasami okrela si to jako rnic pomidzy niezmiennoci fizyczn (zwan rwnie niezmiennoci bitow) i niezmiennoci logiczn. Pierwszy z wymienionych terminw oznacza, e kady bit obiektu jest niezmienny, dziki czemu obraz bitw obiektu nigdy si nie zmienia. Niezmienno logiczna oznacza natomiast, e chocia cay obiekt jest pojciowo stay, to jednak mog zmienia si niektre jego skadowe. Jednake jeeli okreli si, e obiekt jest stay, to kompilator bdzie strzeg go zazdronie, zapewniajc jego niezmienno fizyczn. Istniej dwa sposoby uzyskania niezmiennoci logicznej, umoliwiajce zmian danych skadowych wewntrz staych funkcji skadowych. Pierwsze podejcie ma charakter historyczny i nazywane jest odrzuceniem niezmiennoci. Uzyskuje si je w do dziwny sposb. Pobiera si wskanik this (sowo kluczowe, zwracajce adres biecego obiektu) i rzutuje si go na wskanik do obiektu biecego typu. Wydawaoby si, e this ju jest takim wskanikiem. Jednak wewntrz staej funkcji skadowej jest on w rzeczywistoci wskanikiem do staej, wic rzutujc go na zwyky wskanik, usuwa si niezmienno wskazywanego obiektu. Poniej zamieszczono odpowiedni przykad:

//: C08:Castaway.cpp // "Odrzucenie" niezmiennoci class Y { int i; public: Y(); void f() const;
0; }

void Y::f() const { //! i++; // Bd - staa funkcja skadowa ((Y*)this)->i++; // W porzdku - odrzucenie niezmiennoci // Lepsze rozwizanie - uycie skadni jawnego rzutowania C++ (const_cast<Y*>(this))->i++; int main() { const Y yy; yy.f(); // W rzeczywistoci zmienia obiekt!

Rozdzia 8. State

291

Podejcie takie jest skuteczne i wystpuje w starszych programach, ale nie jest to zalecana technika. Problem polega na tym, e brak niezmiennoci obiektu zosta ukryt> w definicji funkcji skadowej, i nie ma jakichkolwiek przesanek, zawartych w interfejsie klasy, wskazujcych na to, e dane obiektu s w rzeczywistoci modyfikowane, chyba e ma si dostp do kodu rdowego (a take jeli ze wzgldu na podejrzenie, e niezmienno obiektu zostaa naruszona, poszuka si w tym kodzie rzutowa). Aby poda to wszystko do oglnej wiadomoci, naley w deklaracji klasy uy sowa kluczowego mutable (podlegajcy zmianom"), oznaczajcego, e okrelona skadowa moe zosta zmodyfikowana wewntrz staego obiektu:

//: C08:Mutable.cpp // Sowo kluczowe "mutable" class l {


int i ; mutable int j; publ 1 c : ZO; void f() const:
Z : : Z ( ) : i ( 0 ) . j(0) {}

void Z: :f() const { / / ! i++: // Bd - staa funkcja skadowa j++; // W porzdku - skadowa moe by zmieniana int main() { const Z zz; zz.f(): // Funkcja modyfikuje obiekt!

Dziki temu uytkownik klasy wie na podstawie jej deklaracji, ktre skadowe mog by modyfikowane wewntrzjej staych funkcji skadowych.

Moliwo umieszczenia obiektu w pamici ROM


Jeeli obiekt zosta zdefiniowany jako staa, jest kandydatem do umieszczenia w pamici przeznaczonej tylko do odczytu (ROM), co jest czsto istotn okolicznoci w przypadku programowania systemw wbudowanych. Jednak nie wystarczy w tym przypadku zwyke uczynienie obiektu sta wymagania umoliwiajce umieszczenie obiektu w pamici ROM s znacznie ostrzejsze. Oczywicie, obiekt musi by niezmienny fizycznie, a nie logicznie. atwo to zauway, gdy niezmienno logiczna jest implementowana wycznie za pomoc sowa kluczowego mutable. Jednake przypuszczalnie kompilator nie jest w stanie wykry tego, e niezmienno jest odrzucana wewntrz staej funkcji skadowej. Dodatkowo: 1. Klasa lub struktura nie moeposiada zdefiniowanych przez uytkownika konstruktorw ani destruktorw. 2. Nie mog istnie adne klasy podstawowe (opisane w rozdziale 14.) ani obiekty skadowe, posiadajce zdefiniowane przez uytkownika konstruktory lub destruktory.

)2

Thinking in C++. Edycja polska Wynik operacji zapisywania, dotyczcej jakiejkolwiek czci obiektu staego, zapisanego w pamici ROM, nie jest zdefiniowany. Mimo e odpowiednio przygotowane obiekty mogby umieszczone w pamici ROM, to aden obiekt nigdy tego nie wymaga.

rolatile
Skadnia modyfikatora volatile (ulotny)jest identycznajak skadnia sowa kluczowego const, lecz volatile oznacza: te dane mog by modyfikowane bez wiedzy kompilatora". Czasami dane s modyfikowane przez rodowisko (prawdopodobnie za pomoc wielozadaniowoci, wielowtkowoci lub przerwa) i w takim przypadku sowo kluczowe volatile informuje kompilator, by nie przyjmowa adnych zaoe dotyczcych danych szczeglnie zwizanych z optymalizacj. Jeeli kompilator wczyta ju dane do rejestru i nie zmienia od tej pory jego zawartoci, to zwykle nie musiaby odczytywa tych danych ponownie. Jednak jeeli dane te zostay oznaczone modyfikatorem volatile, to kompilator nie moe ju robi takich zaoe, poniewa dane mogy by zmienione przez inny proces. W zwizku z tym kompilator musi odczyta te dane ponownie, nie dokonujc optymalizacji kodu, ktra polegaaby na usuniciu z tego, co zwykle byoby nadmiarowoperacjodczytu. Obiekty ulotne" s tworzone za pomoc tej samej skadni, ktrej uywa si w przypadku obiektw staych. Mona nawet generowa obiekty typu const volatile, ktre nie mogyby by modyfikowane przez klienta-programist, ale zamiast tego byyby zmieniane przez jaki czynnik zewntrzny. Oto przykad, mogcy stanowi klas zwizanzjakim urzdzeniem komunikacyjnym:

//: C08:Volatile.cpp // Sowo kluczowe volatile class Comm { const volatile unsigned char byte; volatile unsigned char flag; enum { bufsize - 100 }; unsigned char buf[bufsize]; int index; public: Conm();
void isr() volatile; char read(int index) const;

Comm::Comm() : index(0), byte(0), flag(0) {}

// To tylko demonstracja - funkcja w rzeczywistoci // nie bdzie dziaa jako procedura obsugi przerwa: void Comm::isr() volatile { flag = 0; buf[index++] = byte; // Przewiniecie do pocztku bufora:
if(index >= bufsize) index = 0;

Rozdzia 8. State char Comm::read(int index) const { if(index < 0 || index >= bufsize) return 0; return buf[index];

293

int main() { volatile Comm Port; Port.isr(); // W porzdku //! Port.read(0); // Bd, funkcja read() nie jest typu volatile
Podobnie jak w przypadku modyfikatora const, sowa kluczowego volatile mona uywa w stosunku do danych skadowych, funkcji skadowych oraz samych obiektw. W stosunku do obiektw, zdefiniowanych z modyfikatorem volatile, mona wywoywajedynie funkcje typu volatile. Powodem, dla ktrego funkcja isr( ) nie mogaby by w rzeczywistoci wykorzystana w charakterze procedury obsugi przerwa, jest to, e w funkcjom skadowym klas musi by przekazany, w ukryty sposb, wskanik biecego obiektu (this), natomiast procedury obsugi przerwa na og nie wymagaj adnych argumentw. Aby rozwiza ten problem, mona uczyni funkcj isr( ) statyczn funkcj skadow funkcje takie zostay opisane w rozdziale 10. Skadnia modyfikatora volatile jest taka sama, jak skadnia modyfikatora const, dlatego te oba modyfikatory s czsto omawiane razem. S one okrelane cznie mianem modyfikatora c-v.

Podsumowanie
Sowo kluczowe const umoliwia definiowanie obiektw, argumentw funkcji, zwracanych wartoci oraz funkcji skadowych jako staych, pozwalajc zarazem na rezygnacj z oferowanego przez preprocesor zastpowania nazw, bez utraty adnej z korzyci wynikajcych ze stosowania preprocesora. Wszystko to umoliwia istotn, dodatkow form kontroli typw, a take bezpieczestwo zwizane z programowaniem. Wykorzystanie tak zwanej poprawnoci stosowania statych3 (uywanie staych wszdzie, gdzie to moliwe) moe ocali niejeden projekt. Mimo e mona zignorowa sowo kluczowe const i uywa nadal dawnych praktyk, stosowanych w jzyku C, to istnieje ono po to, by ci pomc. Poczynajc od rozdziau 11. sintensywnie wykorzystywane referencje; dopiero wwczas przekonasz si w peni o tym, jak wane jest uywanie modyfikatora const w stosunku do argumentw funkcji.

W oryginale const correctness okrelenie bdce parafraz poprawnoci politycznej" Qx>litical corectness) pnyp. ttum.

(4

Thinking in C++. Edycja polska

rWiczenia
Rozwizania wybranych wicze znajduj si w dokumencie elektronicznym: The Thinking in C++ Annotated Solution Guide, ktry mona pobra za niewielk opat z witryny http://www.BruceEckel.com. 1. Utwrz trzy stale cakowite, nastpnie dodaj je do siebie w celu utworzenia wartoci, okrelajcej wielko tablicy (w definicji tej tablicy). Sprbuj skompilowa ten sam kod rdowy wjzyku C i zobacz, co si stanie (na og mona zmusi kompilator C++ do tego, by dziaa jako kompilator jzyka C, uywajc do tego parametru podanego w wierszu polece). 2. Udowodnij, e kompilatory jzykw C i C++ rzeczywicie rnie traktuj stae. Utwrz sta globaln i wykorzystaj j w globalnym wyraeniu staym. Nastpnie skompiluj ten program, uywajc zarwno kompilatora C, jak i C++. 3. Utwrz przykadowe definicje staych dla wszystkich typw wbudowanych oraz ich wariantw. Uyj ich w poczeniu z innymi staymi, tworzc nowe definicje staych. Upewnij si, e kompiluj si one poprawnie. 4. W pliku nagwkowym utwrz definicj staej. Nastpnie docz ten plik nagwkowy do dwch plikw .cpp, skompiluj oba pliki i dokonaj ich czenia. Nie powinno si otrzyma adnych komunikatw o bdach. Teraz sprbuj przeprowadzi ten sam eksperyment z jzykiem C. 5. Utwrz sta, ktrej warto jest okrelana w trakcie pracy programu, przez odczytanie podczas uruchamiania programu aktualnego czasu. (Naley uy standardowego pliku nagwkowego <ctime>). W dalszej czci programu sprbuj ponownie wczyta do swojej staej aktualny czas i zobacz, co si stanie 6. Utwrz sta tablic znakw, a nastpnie sprbuj zmieni jeden z nich. 7. Utwrz deklaracj staej, zadeklarowanej wjednym plikujako extern const, a nastpnie umie w tym pliku funkcj main(), drukujc warto tej staej. Umie definicj tej staej w drugim pliku, a nastpnie skompiluj i pocz ze soboba pliki. 8. Utwrz dwa wskaniki do typu const long, uywajc obu postaci deklaracji. Przypisz jednemu z nich adres tablicy wartoci long. Wyka, e moesz inkrementowa i dekrementowa wskanik, ale nie moesz zmieni wskazywanej przez niego wartoci. 9. Utwrz stay wskanik do typu double i przypisz mu adres tablicy wartoci typu double. Wyka, e moesz zmienia warto, wskazywan przez wskanik, ale nie moesz go inkrementowa ani dekrementowa. 10. Utwrz stay wskanik do staego obiektu. Wyka, e moesz jedynie odczyta wskazywanprzez niego warto, ale nie moesz zmienia anijego, ani wartoci przez niego wskazywanej. di. Usu komentarz w wierszu wywoujcym bd w programie PointerAssignment.cpp i zobacz, jaki bd zgosi twj kompilator.

Rozdzia 8. State

295

12. Utwrz litera napisowy oraz wskanik, ktry wskazuje na pocztek tego literau. Nastpnie uyj wskanika do modyfikacji obiektw, znajdujcych si w tablicy. Czy twj kompilator zasygnalizuje tojako bd? Jeeli nie, to co otym mylisz? 13. Utwrz funkcj, pobierajcprzez warto argument bdcy sta, a nastpnie sprbuj, w ciele funkcji, zmieni warto tego argumentu. 14. Utwrz funkcj, pobierajcprzez warto argument typu float. Wewntrz funkcji powi z tym argumentem przez referencj sta typu const float& i odtd uywaj wycznie tej referencji, by zagwarantowa, e argument funkcji nie zostanie zmieniony. 15. Zmodyfikuj program ConstReturnVaIues.cpp, usuwajc indywidualnie komentarze w wierszach wywoujcych bdy, by przekona si, jakie komunikaty o bdach zgasza twj kompilator. 16. Zmodyfikuj program ConstPointer.cpp, usuwajc indywidualnie komentarze w wierszach wywoujcych bdy, by zobaczyc,jakie komunikaty o bdach zgasza twj kompilator. 17. Utwrz now wersj pliku ConstPointer.cpp, noszc nazw ConstReference.cpp, ktra demonstruje uycie referencji zamiast wskanikw (by moe bdziesz musia w tym celu zajrze do rozdziau 11.). 18. Zmodyfikuj program ConstTemporary.cpp, usuwajc komentarz w wierszu wywoujcym bd, by zobaczyc,jakie komunikaty o bdach zgasza twj kompilator. 19. Utwrz klas zawierajc zarwno stae, jak i niestae" elementy typu float. Zainicjalizuj je, uywajc listy inicjatorw konstruktora. 20. Utwrz klas o nazwie MyString, zawierajcacuch (string), konstruktor inicjalizujcy ten acuch oraz funkcj print(). Zmodyfikuj plik StringStack.cpp w taki sposb, aby zawarty w nim kontener przechowywa obiekty klasy MyString, a funkcj main() w taki sposb, byje wydrukowaa. 21. Utwrz klas zawierajcskadowsta, ktra zostanie zainicjowana na licie inicjatorw konstruktora, a take nienazwane wyliczenie, ktrego uyjesz do okrelenia rozmiaru tablicy, 22. W pliku ConstMember.cpp, z definicji funkcji skadowej usu modyfikator const, aIe zostaw go w deklaracji, aby zobaczyc,jaki otrzymasz rodzaj komunikatu o bdzie. 23. Utwrz klas, zawierajczarwno stae funkcje skadowe,jak i funkcje skadowe niebdce staymi. Utwrz stae obiekty tej klasy oraz obiekty niebdce staymi, a nastpnie sprbuj wywoa rne rodzaje funkcji skadowych dla rnych typw obiektw. 24. Utwrz klas, zawierajczarwno stae funkcje skadowe.jak i funkcje skadowe niebdce staymi. Sprbuj wywoa funkcj skadow niebdc staze staej funkcji skadowej i zobacz,jaki otrzymasz rodzaj komunikatu o bdzie.

96

Thinking in C++. Edycja polska

25. W pliku Mutable.cpp usu komentarz w wierszu wywoujcym bd, aby zobaczy, jaki rodzaj komunikatu o bdzie zgosi twj kompilator. 26. Zmodyfikuj plik Quoter.cpp, czynic funkcj quote() stafunkcjskadow i dodajc do skadowej lastquote sowo kluczowe mutable. 27. Utwrz klas o danych skadowych, posiadajcych modyfikator volatile. Utwrz funkcje skadowe zarwno posiadajce modyfikator volatile, jak i go nieposiadajce ktre modyfikujdane skadowe typu volatile i zobacz, wjaki sposb zareaguje na to kompilator. Utwrz obiekty swojej klasy, zarwno z modyfikatorem volatile, jak i bez tego modyfikatora. Sprbuj w stosunku do nich wywoywa zarwno funkcje skadowe zadeklarowane z modyfikatorem volatile,jak i bez niego, by zobaczy, czy si to powiedzie, a takejakie komunikaty o bdach zgosi kompilator. 28. Utwrz klas o nazwie bird, zawierajcfunkcj skadowfly(), oraz klas rock, nieposiadajctakiej funkcji. Utwrz obiektklasy rock, pobierzjego adres i przypisz go wskanikowi typu void*. Nastpnie przypisz warto tego wskanika wskanikowi typu bird* (naley uy do tego celu rzutowania) i za pomoctego wskanika wywoaj funkcj fly( ). Czy terazju rozumiesz, dlaczego dostpna wjzyku C moliwo swobodnego przypisywania wartoci wskanika void* (bez rzutowania) stanowi w tymjzyku luk", ktra nie powinna by rozszerzana najzyk C++?

Rozdzia 9.

Funkcje inline

Do waniejszych cechjzyka C++, wywodzcych sizjzyka C, naley efektywno Gdyby bya ona znacznie nisza ni w przypadku jzyka C, liczni programici nie potrafiliby znale przekonujcych argumentw, przemawiajcych na korzy C++.

Jednym ze sposobw osignicia efektywnoci w jzyku C jest wykorzystanie makroinstrukcji. Pozwalaj one na utworzenie czego, co przypomina wywoanie funkcji, ale pozbawione jest zwizanego z tym zazwyczaj narzutu. Makroinstrukcje s implementowane za pomoc preprocesora, a nie waciwoci kompilatora. Preprocesoi zastpuje bezporednio wszystkie wywoania makroinstrukcji ich kodem, co pozwala na uniknicie kosztw zwizanych z umieszczeniem argumentw na stosie, wykonaniem instrukcji CALL asemblera, pobraniem zwracanych argumentw i wykonaniem instrukcji RETURN asemblera. Caa praca zostaje wykonana przez preprocesor, dziki czemu uzyskuje si wygod i czytelno, odpowiadajc wywoaniu funkcji, nie ponoszc adnych kosztw.

Z uyciem makroinstrukcji preprocesora w jzyku C++ wi si dwa problemy. Pierwszy z nich wystpuje rwnie wjzyku C makroinstrukcja wyglda co prawda identycznie jak wywoanie funkcji, ale nie zawsze dziaa tak samo. Moe to prowadzi do powstania trudnych do wykrycia bdw. Drugi problem jest zwizany zjzykiem C+H preprocesor nie ma prawa dostpu do danych skadowych klas. Oznacza to, e makroinstrukcje preprocesora nie mog zosta uyte w charakterze funkcji skadowych.

W celu utrzymania efektywnoci makroinstrukcji procesora i rwnoczenie zapewnienia bezpieczestwa i zasigu w obrbie klasy, waciwych prawdziwym funkcjom, udostpniono w jzyku C++ funkcje inline. W niniejszym rozdziale przeledzimy problemy zwizane z wykorzystaniem makroinstrukcji preprocesora w jzyku C++, atake opiszemy, w jaki sposb zostay one rozwizane za pomoc funkcji inline. Omwione zostanrwnie wskazwki oraz szczegy zwizane z dziaaniem funkcji inline.

>8

Thinking in C++. Edycja polska

>ulapki preprocesora
Istot problemw zwizanych z wykorzystaniem makroinstrukcji preprocesora jest Wdne mniemanie, e preprocesor zachowuje si identyczniejak kompilator. Oczywicie, intencj twrcw byo to, aby makroinstrukcje wyglday i dziatay tak, jak wywoania funkcji, wic do atwo mona nabra takiego przekonania. Problemy wynikaj wwczas, gdy ujawniaj si niewielkie rnice midzy nimi. Tytuem prostego przykadu przyjrzyjmy si definicji: #define F (x) (x + 1) Jeeli wykonamy wywoanie makroinstrukcji F:

F(1)
to preprocesor rozwinieje nieco niespodziewanie do postaci:

(x) (x + 1)(1)
Przyczyn pojawienia si problemu jest wystpujcy w definicji makroinstrukcji odstp pomidzy liter F i nawiasem otwierajcym. Po usuniciu z definicji odstpu mona wywoa makroinstrukcj, nawet wpisujc odstp przed nawiasem:

F (1)
i nadal bdzie ona rozwijana poprawnie do postaci:

(1 + 1)
Przedstawiony powyej przykad jest do banalny i zawarty w nim problem natychmiast by si ujawni. Prawdziwe problemy wystpuj wwczas, gdy w charakterze parametrw wywoania makroinstrukcji uywane s wyraenia. Wystpuj tu dwa problemy. Pierwszy z nich polega na tym, e wyraenie moe by rozwinite wewntrz makroinstrukcji, co powoduje, e kolejno wykonywania operatorw bdzie odbiegaa od spodziewanej. Rozwamy na przykad definicj:
#define FLOOR(x.b) x>=b?0:l

Jeeli teraz jako argumenty uyte zostan wyraenia: if(FLOOR(a&OxOf.Ox07)) / / . . . to makroinstrukcja zostanie rozwinita do postaci:
if(a&OxOf>=Ox07?0:l)

Priorytet operatora & jest niszy ni >=, wic sposb wyliczenia makroinstrukcji nie bdzie zgodny z oczekiwaniami. Po ustaleniu przyczyny problemu mona go rozwiza, umieszczajc w nawiasach wszystkie elementy, znajdujce si w definicji makroinstrukcji (warto stosowa ten sposb w przypadku tworzenia makroinstrukcji preprocesora). Naley wic zapisa: #define FLOOR(x.b) ((x)>-(b)?0:l)

Rozdzia 9. * Funkcje inline

299

Jednak odkrycie problemu jest niekiedy trudne i moe on pozostawa nie zauwaony, jeli przyjmie si poprawne dziaanie makroinstrukcji za pewnik. W pozbawionej nawiasw, poprzedniej wersji makroinstrukcji, wikszo wyrae bdzie obliczana prawidowo, poniewa priorytet operatora >= jest niszy ni wikszoci takich operatorw, jak +, /, , a nawet bitowych operatorw przesunicia. Tak wic atwo jest nabra przekonania, e makroinstrukcja dziaa prawidowo w przypadku wszystkich wyrae, w tym w razie uycia bitowych operatorw logicznych. Poprzedni problem mona rozwiza, zachowujc ostrono podczas programowania naley umieci w nawiasach wszystko to, co znajduje si w makroinstrukcji. Jednak drugi problem jest bardziej subtelny. W odrnieniu od zwykej funkcji, podczas kadego uycia argumentu w makroinstrukcji obliczana jest jego warto. Dopki makroinstrukcjajest wywoywana ze zwykymi zmiennymi, niejest to grone, lecz gdy argument posiada skutki uboczne, to rezultat okae si zaskakujcy i zupenie nie przypomina zachowania funkcji. Na przykad ponisza makroinstrukcja sprawdza, czy jej argument jest zawarty w pewnym zakresie wartoci: #define BANO(x) U(x)>5 && (x)<10) ? (x) : 0) Dopki uywane s zwyke" argumenty, makroinstrukcja dziaa jak prawdziwa funkcja. Kiedyjednak stracisz czujno i uwierzysz, e tojest prawdziwa funkcja, zaczn si kopoty. Na przykad: / / : C09:MacroSideEffects.cpp #include "../require.h" #include <fstream> using namespace std: #define BAND(x) (Ux)>5 && (x)<10) ? (x) : 0) int main() { ofstream out("macro.out"); assure(out. "macro.out"); for(int i = 4; i < 11; i++) { out << "a = " << a << endl << '\t'; out << "BAND(++a)=" << BAND(++a) << endl; out << "\t a = " << a << endl;
int a = i;

Zwr uwag na uycie wielkich liter w nazwie makroinstrukcji. To dobry zwyczaj, poniewa dziki temu czytelnik widzi, e ma do czynienia z makroinstrukcj, a nie z funkcj, wic w przypadku problemw bdzie mu atwiej przypomnie sobie o tym. A oto wyniki dziaania programu, znacznie odbiegajce od tego, czego mona by si spodziewa po prawdziwej funkcji:
a = 4 BAND(++a)=0 a-5 a-5 BAND(++a)=8

Thinking in C++. Edycja polska

a-6 BAND(++a)=9 a = 9 a -7 BAND(++a)=10 a - 10 a = 8 BAND(++a)=0 a = 10 a-9 BAND(++a)=0 a = 11 a - 10 BAND(++a)=0 a = 12

a-8

Kiedy warto zmiennej a wynosi cztery, wykonywana jest tylko pierwsza cz wyraenia warunkowego. A zatem wyraenie jest obliczane tylko jeden raz, a w wyniku skutkw ubocznych wywoania makroinstrukcji zmienna a uzyskuje warto pi. Zachowania takiego mona by si spodziewa po wywoaniu w tej samej sytuacji normalnej funkcji. Jednake gdy warto zmiennej znajduje si w przedziale rodkowym, sprawdzane s oba warunki, w wyniku czego dwukrotnie wykonywana jest jej inkrementacja. W czasie wyznaczania wyniku argument obliczany jest ponownie, czego skutkiem jest trzecia inkrementacja. Gdy warto argumentu przekracza grny zakres, nadal sprawdzane s oba warunki, wic wynikiem tego jest dwukrotne wykonanie inkrementacji. Skutki uboczne mog wic by rne, w zalenoci od wartoci argumentu. Z pewnoci nie jest to sposb zachowania oczekiwany od makroinstrukcji przypominajcej wywoanie funkcji. W takim przypadku oczywistym rozwizaniem jest utworzenie prawdziwej funkcji, ktra bdzie wizaa si z dodatkowym narzutem i ktra moe zmniejszy efektywno caego programu, -w przypadku gdy bdzie wielokrotnie wywoywana. Niestety, problem ten nie zawsze jest rwnie oczywisty; moesz niewiadomie wej w posiadanie biblioteki zawierajcej wymieszane ze sob funkcje i makroinstrukcje, wic problem, podobny do opisanego, moe kry w sobie trudne do wykrycia bdy. Na przykad makroinstrukcja putc(), zawarta w cstdio, moe dwukrotnie wyznaczy warto swojego drugiego argumentu. Zostao to wyszczeglnione w standardzie jzyka C. Rwnie nieuwana implementacja touper() jako makroinstrukcji moe wyliczy argument wicej ni jednokrotnie, co da nieprzewidziane wyniki wywoania tej makroinstrukcji w postaci toupper(*p++)'.

Makroinstrukcje a dostp
Oczywicie, uwane programowanie i uywanie makroinstrukcji preprocesora jest konieczne w jzyku C i z pewnoci moglibymy poradzi sobie stosujc te same metody w jzyku C++, gdyby nie pewien problem makrodefinicja nie zawiera pojcia zasigu, wymaganego w przypadku funkcji skadowych. Preprocesor dokonuje po prostu podstawie tekstu, nie mona wic na przykad napisa: Andrew Koenig opisuje to bardziej szczegtowo w swojej ksice C Traps & Pitfalh (AddisonWesley, 1989).

Rozdzia 9. Funkcje inline


class X {

301

int i; public: #define V A L ( X : : i ) // Bd

ani nawet wykona podobnego zapisu. Ponadto nie mona by wskaza, do ktrego obiektu nastpuje odwoanie. Nie ma bowiem sposobu na to, by w makroinstrukcji okreli zasig klasy. Poniewa programici nie dysponuj adnym rozwizaniem alternatywnym w stosunku do makroinstrukcji preprocesora, w imi efektywnoci bd zmuszeni do okrelenia niektrych danych skadowych jako publiczne, eksponujc w ten sposb wewntrzn implementacj i uniemoliwiajc jej zmiany, a take rezygnujc z ochrony, zapewnianej przez specyfikator private.

Funkcje inline
Zgodnie z przyjtym w jzyku C++ rozwizaniem problemu dostpu makroinstrukcji do prywatnych skadowych klasy usunite zostay wszystkie problemy zwizane z makroinstrukcjami preprocesora. Zostao to osignite przez umieszczenie pojcia makroinstrukcji pod kontrol kompilatora, gdzie w istocie jest jego miejsce. Jzyk C++ implementuje makroinstrukcj jako funkcj inline, bdc pod kadym wzgldem prawdziw funkcj. Dziaa ona dokadnie tak, jak mona tego oczekiwa po zwykych funkcjach. Jedyna rnica polega na tym, e funkcje inline s rozwijane w miejscach ich wywoania, podobniejak makroinstrukcje preprocesora; eliminowanyjest wic narzut zwizany z wywoaniem funkcji. Tak wic nie powinno si (niemal) nigdy uywa makroinstrukcji, a zamiast nich naley stosowa wycznie funkcje inline. Kada funkcja, definiowana w ciele klasy, jest automatycznie funkcj inline, lecz mona uczyni funkcjami inline rwnie funkcje niebdce skadowymi klas, poprzedzajc je sowem kluczowym inline. Aby wywoao to jednak jakikolwiek skutek, trzeba doczy do deklaracji ciao (tre) funkcji, bo w przeciwnym razie zostanie to przez kompilator potraktowane tak, jak zwyka deklaracja funkcji. Tak wic zapis:
nnline int plusOne(int x);

nie wywouje adnego skutku, poza tym, e deklaruje funkcj (ktra pniej moe, ale nie musi, posiada definicji inline). Waciwym rozwizaniem byoby okrelenie ciaa funkcji:
inline int plusOne(int x) { return ++x; }

Zwr uwag na to, e kompilator sprawdzi Qak zawsze to robi) poprawne wykorzystanie listy argumentw i zwracanej wartoci (dokonujc wszystkich niezbdnych konwersji), czego preprocesor w ogle nie jest w stanie dokona. Rwnie prba zapisania powyszej funkcji w postaci makroinstrukcji preprocesora spowodowaaby niepodany skutek uboczny. Definicje inline prawie zawsze musz by umieszczane w plikach nagwkowych. Gdy kompilator napotka tak definicj, umieszcza w tablicy symboli typ funkcji O^j sygnatur, poczon ze zwracan wartoci) oraz ciao funkcji. Kiedy funkcja taka

>2

Thinking in C++. Edycja polska

jest uywana, kompilator upewnia si, e jej wywoanie jest poprawne, a zwracana warto wykorzystywana prawidowo. Nastpnie podstawia w miejsce wywoania tre funkcji, eliminujc w taki sposb narzut. Kod funkcji inline zajmuje pami, ale w przypadku gdy funkcja jest niewielka, to w rzeczywistoci moe zaj mniej miejsca ni kod wykonujcy zwyke wywoanie funkcji (umieszczajcy argumenty na stosie i wykonujcy instrukcj CALL). Funkcja inline zawarta w pliku nagwkowym posiada szczeglny status. Plik nagwkowy zawierajcy funkcj oraz jej definicj musi by bowiem doczony w kadym pliku, w ktrym ta funkcja jest wykorzystywana. Nie wywouje to jednak bdu wynikajcego z wielokrotnej definicji funkcji (chocia jej definicja musi by identyczna we wszystkich miejscach, w ktrych doczonajest funkcja inline).

Funkcje inline wewntrz klas


Aby zdefiniowa funkcj inline, trzeba jedynie umieci na kocu jej definicji sowo kluczowe inline. Nie jest to jednak konieczne w obrbie definicji klasy. Kada funkcja definiowana wewntrz definicji klasy staje si automatycznie funkcj inline. Na przykad: / / : C09:Inline.cpp // Funkcje inline wewntrz klas #include <iostream> #include <string> using namespace std; class Point { int i , j, k; public: Point(): i(0). j(0). k(0) {} Point(int ii, int jj. int kk) : Kii). j(jj), k(kk) {} void print(const string& msg "") const { if(msg.size() !- 0) cout << msg << endl; cout << "i - " << i << ", " << "j - " << j << ". " << "k - " << k << endl ;

int main() { Point p. q(l,2.3); p.print("wartosc p"); q.print("wartosc q");


W tym przypadku zarwno konstruktor, jak i funkcja print( ) s domylnie funkcjami inline. Zwr uwag na to, e w funkcji main( ) nie ma ladu uywania funkcji inline, i tak wanie powinno by. Logiczne dziaanie funkcji musi by takie samo, niezalenie od tego, czy jest ona funkcj inline, czy te nie (w przeciwnym razie oznaczaoby to, e uszkodzonyjest kompilator). Jedyna widoczna rnica polega na wydajnoci.

Rozdzia 9. Funkcje inline

303

Oczywicie, kuszce jest uywanie funkcji inline w kadym miejscu deklaracji klasy, poniewa pozwalaj one na uniknicie dodatkowego kroku, polegajcego na utworzeniu zewntrznej definicji funkcji skadowej. Trzebajednak pamita, e ide funkcji inline jest zapewnienie kompilatorowi wikszych moliwoci dokonania optymalizacji. Jednak definiowanie duych funkcji jako funkcji inline spowoduje, e kod bdzie powtarzany w kadym miejscu, w ktrym ta funkcja jest wywoywana. Wywoa to rozdcie kodu, niewspmierne do korzyci wynikajcych ze zwikszenia jego szybkoci ^jedynym niezawodnym sposobem przekonania si o tym jest przeprowadzenie eksperymentw, pozwalajcych na sprawdzenie wynikw uzyskiwanych dziki wykorzystaniu funkcji inline za pomoc uywanego przez siebie kompilatora).

Funkcje udostpniajce
Jednym z najwaniejszych zastosowa funkcji inline w klasach sfunkcje udostpniajce. Te niewielkie funkcje pozwalaj na odczyt lub zmian stanu czci obiektu to znaczy wewntrznej zmiennej (lub zmiennych) skadowej. Powd, dla ktrego funkcje inline s takie wane dla funkcji udostpniajcych, mona pozna analizujc poniszy przykad: //: C09:Access.Cpp

// Funkcje inline jako funkcje udostpniajce class Access { int i ; public: int read() const { return i; } void set(int ii) { i = ii; }
int main() { Access A; A.set(100); int x = A.read();

W powyszym przykadzie uytkownik klasy nie ma nigdy bezporedniego kontaktu ze zmiennymi stanu, zawartymi w klasie, i mog one pozosta prywatne, znajdujc si pod kontrol projektanta klasy. Kady dostp do prywatnych danych skadowych moe by nadzorowany za pomoc interfejsu funkcji skadowych. Ponadto dostp ten jest niezwykle efektywny. Przyjrzyjmy si, na przykad, funkcji read( ). Gdyby nie zostaa uyta funkcja inline, kod wygenerowany dla funkcji read( ) obejmowaby umieszczenie wskanika this na stosie i wywoanie instrukcji CALL asemblera. W przypadku wikszoci maszyn kod taki zajmowaby wicej miejsca ni kod generowany dla funkcji inline, a czas jego wykonania z pewnoci byby wikszy. Gdyby nie byo funkcji inline, to zwracajcy uwag na efektywno projektant klasy byby zmuszony do uczynienia zmiennej i skadow publiczn. Dziki temu wyeliminowaby narzut, pozwalajc uytkownikowi na bezporedni dostp do tej zmiennej. Z punktu widzenia projektu wywoaoby to katastrofalne skutki, poniewa skadowa i staje si w ten sposb elementem publicznego interfejsu, co oznacza, e projektant

Thinking in C++. Edycja polska

klasy nie bdzie mg jej ju nigdy zmieni. Zostanie wic niejako skazany" na zmienncakowito nazwie i. Stanowi to problem, bowiem pniej moe si okaza, e znacznie lepiej reprezentowa informacje dotyczce stanu jako liczb typu float, a nie int, lecz z uwagi na to, e deklaracja zmiennej i jako cakowitej stanowi element publicznego interfejsu klasy, nie sposbjujej zmieni. Moe si rwnie zdarzy, e odczytujc lub zapisujc warto zmiennej i trzeba bdzie przeprowadzi jakie dodatkowe obliczenia, co nie bdzie moliwe w przypadku, gdy zmienna ta bdzie zmienn publiczn. Z drugiej strony jeeli do odczytu i zapisu informacji o stanie obiektu zawsze uywane s funkcje skadowe, to mona dowolnie zmienia jego wewntrzn reprezentacj. Ponadto wykorzystanie funkcji skadowych do nadzoru nad zmiennymi skadowymi pozwala na dodanie do funkcji skadowych kodu wykrywajcego zmiany danych, ktry moe okaza si bardzo pomocny w czasie uruchamiania programu. Natomiast gdy skadowa jest publiczna, kady moe j zmieni w dowolnej chwili, nie informujc o tym nikogo.

)bserwatory i modyfikatory
Niektrzy dokonuj dalszego podziau funkcji udostpniajcych na obserwatory (odczytujce informacje o stanie obiektu, zwane rwnie akcesorami) i modyfikatory (zmieniajce stan obiektu). Ponadto w celu umoliwienia uycia tej samej nazwy funkcji zarwno w stosunku do obserwatora, jak i modyfikatora mona zastosowa przecienie nazwy sposb wywoania funkcji zadecyduje o tym, czy informacja o stanie jest odczytywana, czy te modyfikowana. Ilustruje to poniszy przykad:

//: C09:Rectangle.cpp // Obserwatory i modyfikatory class Rectangle { int wide. high; public: Rectangle(int w = 0. int h = 0) : wide(w), high(h) {} int width() const { return wide; } // Odczyt void width(int w) { wide - w: } // Zapis int height() const { return nigh; } // Odczyt void height(int h) { high - n: } // Zapis
int main() { Rectangle r(19. 47); // Zmiana szerokoci (width) i wysokocf (height) : r.height(2 * r,widthO); r.width(2 * r.heightO); } ///:~ Konstruktor wykorzystuje list inicjatorw konstruktora (przedstawion krtko w rozdziale 8. i opisan wyczerpujco w rozdziale 14.) do inicjalizacji wartoci skadowych wide i high (uywajc pseudokonstruktorw w stosunku do typw wbudowanych).

Rozdzia 9. Funkcje inline

305

Funkcje skadowe nie mog uywa tych samych nazw, co dane skadowe, moe wic zaistnie konieczno ich odrnienia. Naley poprzedzi ich nazwy znakami podkrelenia. Jednak identyfikatory poprzedzone znakami podkrelenia s zastrzeone, wic nie naley ich uywa. Zamiast tego mona uy przedrostkw get" (pobierz) i set" (ustaw) do oznaczenia obserwatorw i modyfikatorw: //: C09:Rectangle2.cpp // Obserwatory i modyfikatory z przedrostkami "get" i "set" class Rectangle { int width. height; public: Rectangle(int w - 0, int h = 0) : width(w), height(h) {} int getWidth() const { return width; } void setWidth(int w) { width = w: } int getHeight() const { return height: } void setHeight(int h) { height = h: }

}:

int main() { Rectangle r(19, 47): // Change width & height: r.setHeight(2 * r.getWidthO); r.setWidth(2 * r.getHeightO): } ///:Oczywicie, obserwatory i modyfikatory nie musz by zwykymi pasami transmisyjnymi" zmiennych wewntrznych. Czasami mog one wykonywa bardziej skomplikowane obliczenia. W poniszym przykadzie wykorzystano do utworzenia prostej klasy Time funkcje zwizane z czasem, zawarte w standardowej bibliotecejzyka C: //: C09:Cpptime.h // Prosta klasa, reprezentujca czas #ifndef CPPTIME_H #define CPPTIME_H #include <ctime> #include <cstring> class Time { std::time_t t: std: :tm local ; char asciiRep[26]: unsigned char lflag, aflag: void updateLocal() { if(!lflag) { local - *std::localtime(&t); lflag++; void updateAscii() { if(!aflag) { updateLocal(); std: :strcpy(asciiRep,std: :asctime(&local)): aflag++;

Thinking in C++. Edycja polska

public: Time() { mark(); } void mark() { lflag = aflag = 0; std::time(&t); } const char* ascii() { updateAsci i(); return asciiRep; }

return local.tm_min; } int second() {

// Rnica w sekundach: int delta(Time* dt) const { return int(std::difftime(t, dt->t)); ) int daylightSavings() ( updateLocal(); return local .tm_isdst; } int dayOfYear() { // Liczba dni od 1 stycznia updateLocalO; return local.tm_yday; } int dayOfWeek() { // Liczba dni od niedzieli updateLocal(): return local.tm_wday; } int sincel900() { // Liczba lat od 1900 updateLocal(); return local.tm^year; } int month() ( // Liczba miesicy od stycznia updateLocal(); return local.tm_mon; } int dayOfMonth() { updateLocaH); return local.tm_mday; } int hour() { // Liczba godzin od pnocy (zegar 24-godzinny) updateLocal(); return local.tm_hour; } int minute() { updateLocal();

updateLocal (): return local.tm sec;

#endif // CPPTIME_H ///:Funkcje zawarte w standardowej bibliotece jzyka C zawieraj wiele reprezentacji czasu, z ktrych kada jest elementem klasy Time. Nie ma jednak potrzeby aktualizowania

Rozdzia 9. * Funkcje inline

307

ich wszystkich. Dlatego te zmienna t typu t_time jest wykorzystywana jako reprezentacja podstawowa, natomiast zmienne locaI typu tm oraz znakowa reprezentacja ASCII asciiRep posiadaj znaczniki informujce o tym, czy byy one aktualizowane zgodnie z biec wartoci zmiennej t. Dwie funkcje prywatne updateLocaI( ) oraz updateAscii( ) sprawdzaj wartoci tych znacznikw i warunkowo dokonuj aktualizacji. Konstruktor wywouje funkcj mark( ) (moe j rwnie wywoa uytkownik, wymuszajc, by obiekt reprezentowa biecy czas), ktra zeruje oba znaczniki w celu oznaczenia, e wartoci zmiennych locaI oraz asciiRep s nieaktualne. Funkcja ascii( ) wywouje funkcj updateAscii( ), ktra kopiuje do lokalnego bufora wynik zwracany przez funkcj asctime( ), pochodzc ze standardowej biblioteki jzyka C. Funkcja asctime( ) wykorzystuje bowiem statyczny obszar danych, zapisywany w przypadku, gdy jest ona wywoywana w jakim innym miejscu. Warto zwracana przez funkcj ascii()jest adresem tego lokalnego bufora. Wszystkie funkcje, poczynajc od daylightSavings( ), wykorzystuj funkcj updateLocal( ), co powoduje, e funkcje powstae w wyniku ich rozwinicia mog by stosunkowo due. Wydaje si to niepotrzebne, zwaszcza gdy wemie si pod uwag, e prawdopodobnie nie bd one wywoywane zbyt czsto. Jednak nie oznacza to, e wszystkie te funkcje inline powinno si zamieni w zwyke funkcje. Jeli dokonasz takiego przeksztacenia, to pozostaw jako inline przynajmniej funkcj updateLocal( ). Diki temu jej kod zostanie powtrzony w zwykych funkcjach skadowych, eliminujc dodatkowy narzut zwizanyzjej wywoaniem. Poniej znajduje si krtki program testowy:

//: C09:Cpptime.cpp // Test prostej klasy, reprezentujcej czas #include "Cpptime.h"


#include <iostream> using namespace std;

int mainC) { Time start; for(int i = 1; i < 1000; i++) {

if(iX10 0) cout << endl; } Time end; cout << endl ; cout << "pocztek = " << start.ascii(); cout << "koniec = " << end.asciiO; cout << "roznica - " << end.delta(&start);
Tworzonyjest obiekt klasy Time, nastpnie wykonywane sjakie operacje, trwajce przez pewien czas, po czym jest generowany drugi obiekt klasy Time, oznaczajcy czas zakoczenia operacji. Oba obiekty zostay uyte do wywietlenia czasw rozpoczcia i zakoczenia operacji oraz czasu, ktry pomidzy nimi upyn.

cout << i << ' ' ;

08

Thinking in C++. Edycja polska

(lasy Stash i Stack z funkcjami inline


Dziki funkcji inline moemy obecnie w taki sposb przeksztaci klasy Stash i Stack, aby stay si one bardziej efektywne:

//: C09:Stash4.h // Funkcje inline #ifndef STASH4_H #define STASH4_H #include ". ./require.h" class Stash { int size; // Wielko kadego elementu int quantity; // Liczba elementw pamici int next: // Nastpny pusty element // Dynamicznie przydzielana tablica bajtw: unsigned char* storage; void inflate(int increase); public: Stash(int sz) : size(sz). quantity(0). next(0), storage(0) {} Stash(int sz, int initQuantity) : size(sz). quantity(0), next(0). storage(0) { Stash::-Stash() { if(storage != 0) delete []storage; } int add(void* element); void* fetch(int index) const { require(0 <- index. "Stash::fetch indeks ma wartosc-ujemna"): if(index >= next) return 0: // Oznaczenie koca // Tworzeme wskanika do za,danego elementu: return &(storage[index * size]); } int count() const { return next; }
}
inflate(initQuantity);

}: #endif // STASH4_H ///:-

Oczywicie, niewielkie funkcje bddziaa dobrzejako funkcje inline. Zwrjednak uwag na to, e dwie najwiksze funkcje pozostay zwykymi funkcjami, poniewa przeksztacenie ich w funkcje inline nie miaoby prawdopodobnie adnego wpywu na zwikszenie wydajnoci: / / : C09:Stash4.cpp {0} #include "Stash4.h" #include <iostream> #include <cassert> using namespace std; const int increment = 100; int Stash::add(votd* element) { if(next >= quantity) // Czy wystarczy pamici?

Rozdzia 9. Funkcje inline inflate(increment); // Kopiowanie elementu do pamici. // poczwszy od nastpnego wolnego miejsca: int startBytes = next * size; unsigned char* e = (unsigned char*)element; for(int i = 0; i < size; i++) storage[startBytes + i] = e[i]; next++; return(next - 1); // Numer indeksu void Stash::inflate(int increase) {

30!

assert(increase >= 0);

int newQuantity = quantity + increase; int newBytes = newQuantity * size; int oldBytes - quantity * size; unsigned char* b = new unsigned char[newBytes]; for(int i = 0; i < oldBytes; i++) b[i] = storage[i]; // Kopiowanie starego obszaru do nowego delete [](storage); // Zwolnienie starego obszaru pamici storage = b; // Wskanik do nowego obszaru quantity = newQuantity; // Aktualizacja liczby elementw } lll-.~ Program testujcy ponownie sprawdza, czy wszystko dziaa poprawnie: //: C09:Stash4Test.cpp //{L} Stash4 #include "Stash4.h" #include "../require.h" #include <fstream> #include <iostream> #include <string> using namespace std; int mainC) { Stash intStash(sizeof(int)); for(int i = 0; i < 100; i++) intStash.add(&i); for(int j = 0; j < intStash.count(); j++) cout << "intStash.fetch(" << j << ") = " << *(int*)intStash.fetch(j) << endl; const int bufsize = 80;

if(increase == 0) return;

Stash stringStash(sizeof(char) * bufsize, 100); ifstream in("Stash4Test.cpp"); assure(in. "Stash4Test.cpp"); string line; while(getline(in, line)) stringStash.add((char*)line.c_strC)); int k = 0; char* cp; while((cp = (char*)stringStash.fetch(k++))!=0) cout << "stringStash.fetch(" << k << ") = " << cp << endl;

Thinking in C++. Edycja polska

Jest to ten sam program testowy, ktrego uyto ostatnim razem, wic wyniki jego dziaania powinny by zasadniczo identyczne. Klasa Stack wykorzystuje funkcje inline wjeszcze lepszy sposb: //: C09:Stack4.h // Z funkcjami inline #ifndef STACK4_H #define STACK4_H #include ". ./require.h" class Stack { struct Link { void* data; Link* next; Link(void* dat, Link* nxt): data(dat). next(nxt) {} }* head; public: Stack() : head(0) {} -Stack() { require(head =- 0, "Stos nie jest pusty"); void push(void* dat) { head = new Link(dat. head); void* peek() const { return head ? head->data : 0; void* pop() { if(head -- 0) return 0; void* result = head->data; Link* oldHead = head; head - head->next; delete oldHead; return result; #endif // STACK4_H IIIWarto zwrci uwag na to, e zosta usunity destruktor struktury Link, ktry istnia w poprzedniej wersji klasy Stack, ale by funkcj pust. Wyraenie delete oldHead, zawarte w funkcji pop(), zwalnia pami zajmowan przez obiekt typu Link (nie niszczy ono natomiast wskazywanego przez niego obiektu data). Wikszo funkcji zostaa okrelonajako funkcje inline w do elegancki i oczywisty sposb, szczeglnie w przypadku struktury Link. Nawet co do funkcji pop() wydaje si to usprawiedliwione, mimo e za kadym razem, gdy w funkcji wystpuj wyraenia warunkowe lub zmienne lokalne, nie wiadomo, czy zastosowanie funkcji inline bdzie tak korzystne. W tym przypadku funkcjajest na tyle maa, e prawdopodobnie nie stanowi to adnej przeszkody. Gdy wszystkie funkcje s funkcjami inline, wykorzystywanie biblioteki staje si do proste, poniewa nie wymaga ono czenia. Ilustruje to przykadowy program testowy (zwr uwag na to, e nie istnieje plik Stack4.cpp):

} } }

Rozdzia 9. Funkcje inline

311

//: C09:Stack4Test.cpp //{T} Stack4Test.cpp #include "Stack4.h" #include "../require.h" #include <fstream> #include <iostream> #include <string> using namespace std;
int main(int argc, char* argv[]) { requireArgs(argc. 1); // Argumentem jest nazwa pliku ifstream in(argv[l]); assure(in, argv[l]); Stack textlines; string line; // Odczytanie pliku i zapamitanie wierszy na stosie: while(getline(in, line)) textl ines.push(new string(line)); // Pobranie wierszy ze stosu i wydrukowanie ich: string* s; while((s (string*)textlines.popO) != 0) { cout << *s << endl; delete s;

Czasami tworzy si klasy, w ktrych wszystkie funkcje s funkcjami inline, dziki czemu klasy zawarte s w caoci w plikach nagwkowych (podczas lektury ksiki przekonasz si, e take jej autor niekiedy przekracza t granic). Podczas tworzenia programu praktyka taka nie powoduje na og wikszych szkd, chocia czasami prowadzi do wyduenia czasu kompilacji. Kiedy program ju nieco okrzepnie", warto powrci i zamieni funkcje inline w zwyke funkcje wszdzie tam, gdzie jest to uzasadnione.

Funkcje inline a kompilator


Aby rozumie, wjakich przypadkach korzystniejszejest wykorzystywanie funkcji inline, warto wiedzie, jak dziaa kompilator, napotykajc tak funkcj. Podobnie jak w przypadku kadej innej funkcji, kompilator zapamituje w tablicy symboli typ funkcji (to znaczy jej prototyp, uwzgldniajcy jej nazw i typy argumentw, poczone z wartoci zwracan przez funkcj). Ponadto gdy kompilator sprawdzi, e typ funkcji oraz jej ciao nie zawieraj bdw skadniowych, kod zawarty w tym ciele rwnie jest umieszczany w tablicy symboli. To, czy jest on zapisywany jako kod rdowy, skompilowane instrukcje asemblera, czy jeszcze w jakiej innej postaci, zaley od kompilatora. W przypadku wywoania funkcji inline kompilator upewnia si najpierw, e monaje poprawnie wykona. Oznacza to, e typy wszystkich argumentw musz albo dokadnie odpowiada typom znajdujcym si na licie argumentw funkcji, albo kompilator musi by w stanie przeprowadzi ich konwersj do odpowiednich typw, a take, e warto zwracana przez funkcj musi mie waciwy typ (albo musi istnie

il2

Thinking in C++. Edycja polska moliwo jej konwersji do waciwego typu) w wyraeniu, w ktrym si znajduje. Oczywicie, kompilator robi dokadnie to samo w przypadku kadej funkcji i zasadniczo rni si to od tego, co wykonuje preprocesor, ktry nie jest w stanie kontrolowa typw ani dokonywa konwersji. Jeeli wszystkie informacje dotyczce typu funkcji odpowiadaj kontekstowi, w ktrym zostaa wywoana, wywoanie funkcji jest bezporednio zastpowane jej kodem, dziki czemu wyeliminowany zostanie narzut, zwizany z wywoaniem funkcji i moliwe bdzie przeprowadzenie przez kompilator dalszej optymalizacji. W przypadku gdy funkcja inlinejest funkcjskadow, w odpowiednim miejscu umieszczanyjest rwnie adres obiektu (this), czego, oczywicie, rwnie nie potrafi zrobi preprocesor.

Ograniczenia
Istniej dwie sytuacje, w ktrych kompilator nie moe dokona rozwinicia funkcji. W przypadkach tych powraca on do normalnej postaci funkcji, pobierajc definicj funkcji inline i rezerwujc dla niej pami, tak jak w przypadku funkcji niebdcych funkcjami inline. Gdy musi zrobi to w wielu jednostkach translacji (co zwykle spowodowaoby zgoszenie bdu wielokrotnej definicji), informuje program czcy o tym, e powinien zignorowa wielokrotne definicje. Kompilator nie potrafi rozwin funkcji w przypadku, gdy jest ona zbyt zoona. Jest to uzalenione od konkretnego kompilatora, ale w sytuacjach, w ktrej poddaje si wikszo kompilatorw, rozwinicie funkcji prawdopodobnie i tak nie spowodowaoby adnego zwikszenia efektywnoci. Na og za zbyt skomplikowane, by je rozwija, uwaane swszelkie rodzaje ptli i,jesli si nad tym zastanowi, wykonywanie ptli zajmuje prawdopodobnie znacznie wicej czasu ni narzut zwizany z wywoaniem funkcji. Jeeli funkcja jest zbiorem prostych instrukcji, to prawdopodobnie kompilator nie napotka adnych trudnoci z jej rozwiniciem. Jednake jeli tych instrukcji bdzie bardzo wiele, narzut zwizany z wywoaniem funkcji bdzie znacznie mniejszy ni koszt wykonania jej ciaa. Trzeba rwnie pamita, e ilekro wywoywana jest dua funkcja inline, caa jej tre jest wstawiana w miejsce kadego wywoania funkcji. Z tego powodu atwo uzyska nadmiernie rozdty kod, nie uzyskujc w zamian adnej zauwaalnej poprawy jego wydajnoci (niektre z przykadw, zawartych w tej ksice, mog zawiera funkcje inline o wielkoci przekraczajcej rozsdne rozmiary, przygotowane w tej postaci w celu ograniczenia dugoci wydrukw). Kompilator nie moe rwnie przeprowadzi rozwijania funkcji w przypadku gdy bezporednio lub porednio pobierany jest jej adres. Jeli kompilator musi okreli adres funkcji, przydziela pami, przeznaczon na jej kod, i wykorzystuje uzyskany w ten sposb adres. Jednak w miejscach, w ktrych adres funkcji nie jest uywany, kompilator bdzie prawdopodobnie nadal wstawia jej rozwinity kod. Warto podkreli, e sowo kluczowe inline stanowi tylko wskazwk dla kompilatora nie jest on w ogle zmuszony do rozwijania czegokolwiek. Dobry kompilator bdzie rozwija niewielkie, proste funkcje, w inteligentny sposb ignorujc te funkcje inline, ktre s zbyt skomplikowane. Zapewni ci to podany rezultat semantyk rzeczywistego wywoania funkcji, poczonzefektywnocimakroinstrukcji.

Rozdzia 9. << Funkcje inline

31

Odwotenia do przodu

Jeli wyobrazi sobie, co robi kompilator, realizujc funkcje inline, mona nabra przekonania, e istnieje tu wicej ogranicze ni w rzeczywistoci. W szczeglno< moe si wydawa, e kompilator niejest w stanie obsuy funkcji inline, odwouj cej si .,do przodu" do funkcji, ktra nie zostaajeszcze zadeklarowana w obrbi klasy (niezalenie od tego, czyjest ona funkcj inline, czy te nie):
/ / : C09:Eva1uationOrder.cpp // Kolejno wyliczania funkcji inline class Forward {
int i; public:

Forward() : i(0) {}

// Wywoanie niezadeklarowanej funkcji: int f() const { return g() + 1; } int g() const { return i; } int main() { Forward frwd; frwd . f( ) ;

W funkcji f( ) wystpuje wywoanie funkcji g( ), mimo e funkcja g( ) nie zosta jeszcze zadeklarowana. Funkcjonuje to prawidowo, poniewa definicja jzyka okre la, e adne funkcje inline nie s wyliczane, dopki nie napotka si nawiasu klamrc wego, zamykajcego deklaracj klasy.

Oczywicie, gdyby funkcja g( ) wywoywaa z kolei funkcj f( ), skoczyoby si t na szeregu wywoa rekurencyjnych, ktre byyby dla kompilatora zbyt trudne d< rozwinicia w postaci funkcji inline (naleaoby rwnie wstawi sprawdzanie jakich warunkw w funkcji f( ) lub g( ), by umoliwi wycofanie si" z rekurencji, ktr w przeciwnym wypadku nie miaaby koca).

Dziatenia ukryte w konstruktorach i destruktorach

Konstruktory i destruktory sdwoma miejscami, co do ktrych mona nabra bdne go mniemania, e wykorzystanie funkcji inline jest bardziej efektywne ni w rzeczy wistoci. Konstruktory i destruktory mog wykonywa ukryte dziaania. Wynikaj; one z tego, e klasa moe zawiera obiekty podrzdne, wymagajce wywoania swo ich konstruktorw i destruktorw. Obiekty podrzdne te mog by obiektami skado wymi albo mog one istnie dziki dziedziczeniu (opisanemu w rozdziale 14.). Przy kadem niech bdzie klasa zawierajca obiekty skadowe: //: C09:Hidden.cpp // Ukryte dziaania w funkcjach inline #include <iostream> using namespace std;

L4 class Member {

Thinklng In C++. Edyc]a polska

public: Member(int x = 0) : i(x). j(x). k(x) {} ~Member() { cout << "~Member" << endl; }

int i. j, k;

class WithMembers { Member q, r. s; // Posiadaj konstruktory int i; public: WithMembers(int ii) : i(ii) {} // Banalne? ~WithMembers() { cout << "~WithMembers" << endl;

int main() { WithMembers wm(l); } IIIKonstruktor klasy Member jest wystarczajco prosty, by by funkcj inline, poniewa nie zachodzi w nim nic szczeglnego ani dziedziczenie, ani obiekty skadowe nie powoduj adnych dodatkowych dziaa. Jednak w klasie WithMembers dzieje si wicej ni tylko to, co wida. Konstruktory i destruktory obiektw skadowych q, r i s s wywoywane automatycznie; sone rwnie funkcjami inline, wic rni si znacznie od normalnych funkcji skadowych. Nie oznacza to wcale, e konstruktorw i destruktorw nie mona nigdy definiowa jako funkcji inline s przypadki, w ktrych jest to uzasadnione. Gdy tworzy si wstpny projekt programu, szybko piszc kod, uywanie funkcji inline jest czsto wygodniejsze. Warto si im jednak bliej przyjrze, kiedy dy si do efektywnoci.

Walka z baaganem
W ksice takiej,jak niniejsza, prostota i zwizo, wynikajce z umieszczenia funkcji inline w klasach, s bardzo uyteczne, gdy dziki nim wicej tekstu programu mieci si na stronie lub ekranie (w przypadku seminariw). Jednak, jak wykaza Dan 2 Saks , w prawdziwych projektach wywouje to w efekcie niepotrzebny baagan w interfejsie klasy, czynic j zarazem trudniejsz w uyciu. Odwouje si on do funkcji skadowych definiowanych wewntrz klas, uywajc aciskiego terminu in situ (w miejscu). Saks twierdzi, e dla zachowania przejrzystoci interfejsu wszystkie definicje powinny by umieszczone na zewntrz klasy. Optymalizacja jest natomiast, jak utrzymuje, odrbn kwesti. Jeeli jest potrzebna, naley uy sowa kluczowego inUne. W przypadku zastosowania takiego ujcia przedstawiony wczeniej program Rectangle.cpp miaaby nastpujcposta: / / : C09:Noinsitu.Cpp // Usuwanie funkcji deklarowanych in situ Wspautor wraz z Tomem Plumem ksiki C++ Programming Guidelines (Plum Hall, 1991).

Rozdzia 9. Funkcje inline class Rectangle { int width. height; public: Rectangle(int w = 0. int h - 0); int getWidth() const; void setWidth(int w); int getHeight() const; void setHeight(int h):

31

};
inline Rectangle:;Rectangle(int w. int h) ; width(w). height(h) {} inline int Rectangle::getWidth() const { return width; inline void Rectangle::setWidth(int w) width = w; inline int Rectangle::getHeightO const return height; inline void Rectangle::setHeight(int h) height - h; int main() { Rectangle r(19. 47); // Transpose width & height: int iHeight = r.getHeight();

r.setHeight(r.getWidthO); r.setWidth(iHeight); } III-

Obecnie, chcc porwna dziaanie funkcji inline z dziaaniem zwyczajnych funkcji wystarczy jedynie usun sowo kluczowe inUne (funkcje inline powinny by jednak umieszczone w plikach nagwkowych, podczas gdy zwyczajne funkcje musz znajdowa si w swoich jednostkach translacji). Jeeli chcesz ulokowa te funkcje w dokumentacji, wystarczy prosta operacja typu wytnij i wklej". Funkcje in situ wymagajs wicej pracy i atwiej jest w ich przypadku o bdy. Innym argumentem przemawiajcym za takim podejciemjest to, e dziki niemu mona zawsze zachowajednolity styl formatowania definicji funkcji, co nie zawsze si udaje w przypadku funkcji in situ.

Dodatkowe cechy preprocesora

Jakju wspomniano, niemal zawszejest preferowane uywanie funkcji inline zamiast makroinstrukcji preprocesora. Wyjtkami s sytuacje, w ktrych zamierzasz wykorzysta trzy specjalne funkcje preprocesora jzyka C (bdcego rwnoczenie preprocesoremjzyka C++) acuchowanie, czenie acuchw i sklejanie symboli. acuchowanie, wprowadzone we wczeniejszej czci ksiki, jest wykonywane za pomoci

L6

Thinking in C++. Edycja polska

dyrektywy #, pozwalajcej na zmian identyfikatora w tablic znakow. czenie acuchw zachodzi wtedy, gdy dwie ssiednie tablice znakowe nie s oddzielone od siebie znakami interpunkcyjnymi, dziki czemu nastpuje ich poczenie. Te dwie waciwoci s szczeglnie przydatne w czasie tworzenia kodu uruchomieniowego. Na przykad makroinstrukcja:
|define DEBUG(x) cout << fx " = " << x << endl

drukuje warto dowolnej zmiennej. Mona rwnie utworzy makroinstrukcj ledzc" aktualnie wykonywane instrukcje:
#define TRACE(s) cerr << #s << endl: s

Dyrektywa #s przeksztaca instrukcj w acuch, by go wydrukowa, natomiast drugie s powtarza instrukcj, dziki czemu jest ona wykonywana. Oczywicie, moe to powodowa problemy, szczeglnie w przypadku jednowierszowych ptli for:
for(int i = 0: i < 100; i++) TRACE(f(i));

Z uwagi na to, e makroinstrukcja TRACE() zawiera w rzeczywistoci dwie instrukcje, jednowierszowa ptla for wykonuje tylko pierwsz z nich. Rozwizaniem jest zamiana wystpujcego w makroinstrukcji rednika na przecinek.

Sklejanie symboli
Sklejanie symboli, zaimplementowane w postaci dyrektywy ##, jest bardzo przydatne podczas tworzenia kodu. Pozwala ono na sklejenie ze sob dwch symboli, tworzce w wyniku automatycznie nowy identyfikator. Na przykad:
#define FIELO(a) char* a##_string; int a##_size class Record { FIELO(one):
FIELD(two); FIELD(three);

Kade wywoanie makroinstrukcji FffiLD() utworzy pole skadajce si z dwu identyfikatorw przeznaczonego do przechowywania tablicy znakowej i sucego do przechowywania dugoci tej tablicy. Jest to nie tylko atwiejsze w czytaniu, ale pozwala rwnie na wyeliminowanie bdw zwizanych z kodowaniem, a take uatwia pielgnacj programu.

Udoskonalona kontrola bdw


Do tej pory byy uywane funkcje zawarte w pliku nagwkowym require.h, ale nie zostay one jeszcze zdefiniowane (chocia tam, gdzie to byo celowe, do wykrywania bdw programistycznych bya rwnie stosowana makroinstrukcja assert()). Nadszed czas na zdefiniowanie tego pliku nagwkowego. Funkcje inline s wygodne, poniewa

Rozdzia 9. Funkcje inline

317

pozwalaj na umieszczenie wszystkiego w pliku nagwkowym, co upraszcza wykorzystywanie pakietu. Docza si jedynie plik nagwkowy i nie trzeba si troszczy o doczenie pliku zawierajcego implementacj. Naley rwnie pamita, e wyjtki (opisane szczegowo w drugim tomie ksiki) udostpniaj znacznie bardziej efektywny sposb obsugi wielu rodzajw bdw szczeglnie tych, z ktrymi dobrze byoby sobie poradzi zamiast po prostu przerwa program. Zdarzenia obsugiwane przez funkcje, zawarte w pliku require.h, uniemoliwiajjednak kontynuacj programu jak np. niepodanie przez uytkownika, w wierszu polece, odpowiedniej liczby argumentw lub brak moliwoci otwarcia pliku. Tak wic jest do przyjcia, e powoduj one wywoanie funkcji exit( ), pochodzcej ze standardowej biblioteki jzyka C. Przedstawiony poniej plik nagwkowy zosta umieszczony w gwnym katalogu, do ktrego zostay rozpakowane pliki zawierajce programy rdowe ksiki. Dziki temu jest on atwo dostpny z poziomu wszystkich rozdziaw: / / : :require.h // Kontrola wystpienia bdw w programach // Lokalne "using namespace std" dla starych kompilatorw #ifndef REQUIRE_H #define REQUIRE_H #include <cstdio> #include <cstdlib> #include <fstream> #include <string> inline void require(bool requirement, const std::string& msg = "Warunek nie zostal spelniony"){ using namespace std; if (!requirement) { fputs(msg.c_str(), stderr); fputs("\n". stderr);

exit(l);

inline void requireArgs(int argc, int args, const std::string& msg = "Nalezy podac *d argumentw") { using namespace std; if (argc != args + 1) { fprintf(stderr. msg.c_str(). args); fputs("\n". stderr); exit(l);

inline void requireMinArgs(int argc, int minArgs. const std::string& msg = "Nalezy podac przynajmniej Xd argumentw") { using namespace std; if(argc < minArgs + 1) { fprintf(stderr. msg.c_str(). minArgs): fputs("\n". stderr);

exit(l):

18

Thinklng in C++. Edycja polska

inline void assure(std;:ifstream& in. const std;;string& filename = "") { using namespace std: if(!in) { fprintf(stderr, "Nie mozna otworzy pliku %s\n", filename.c_strO); exit(l);

inline void assure(std::ofstream& out, const std:;string& filename = "") { using namespace std; if(!out) { fprintf(stderr. "Nie mozna otworzy pliku %s\n". filename.c_strO); exit(l):

#endif // REQUIRE_H ///:Domylne wartoci zapewniaj rozsdne komunikaty, ktre mog by w razie potrzeby zmienione. Zamiast argumentw typu char* uywane sargumenty typu const string&. Pozwala to na stosowanie w charakterze argumentw tych funkcji zarwno tablic znakowych (char*), jak i acuchw (string), co powoduje ich wiksz uyteczno (moesz wykorzysta ten wzr w pisanym przez siebie kodzie). W definicjach funkcji requireArgs() i requireMinArgs() liczba argumentw, ktrych podanie jest wymagane w wierszu polece, powikszana jest o jeden, poniewa zmienna argc zawsze uwzgldnia nazw uruchamianego programu jako argument zerowy. Z tego powodu przyjmuje ona warto o jeden wiksz ni liczba argumentw faktycznie podanych w wierszu polece. Zwr uwag na lokalne uycie deklaracji using namespace std", znajdujce si wewntrz kadej funkcji. Wynika ono z faktu, e w momencie pisania ksiki niektre kompilatory nie doczay prawidowo funkcji biblioteki jzyka C w przestrzeni nazw std, wic jej jawna specyfikacja wywoaaby bd w trakcie kompilacji. Lokalne deklaracje umoliwiaj funkcjom, zawartym w pliku require.h, poprawne dziaanie, zarwno z prawidowymi, jak i nieprawidowymi bibliotekami. Nie zachodzi konieczno otwierania przestrzeni nazw std kademu, kto doczy ten plik nagwkowy. Poniej zamieszczono prosty program, testujcy funkcje zawarte w pliku require.h:

//: C09:ErrTest.cpp //{T} ErrTest.cpp // Test funkcji zawartych w pliku require.h #include ". ./require.h"
#include <fstream> using namespace std;

Rozdzia 9. Funkcje inline int main(int argc. char* argv[]) { int i = 1; require(i, "wartosc musi by niezerowa"): requireArgs(argc. 1); requireMinArgs(argc, 1); ifstream in(argv[l]); assure(in. argv[l]); // Wykorzystanie nazwy pliku ifstream nofile("nofile.xxx"); // Fails: / / ! assure(nofile); // Argument domylny ofstream out("tmp.txt"): assure(out); } ///:-

319

By moe pokusisz si o to, by pj w sprawie otwierania plikw o krok dalej, dodajc do pliku nagwkowego require.h makroinstrukcj: #define IFOPEN(VAR. NAME) \ ifstream VAR(NAME); \ assure(VAR, NAME); ktra powinna by nastpnie uywana w nastpujcy sposb: IFOPEN(in. argv[l]) Na pierwszy rzut oka moe to wyglda interesujco, poniewa oznacza oszczdno w pisaniu. Nie jest to jednak zalecane, cho nie stanowi zagroenia. Zwr ponownie uwag na to, e przedstawiona makroinstrukcja wyglda co prawda jak funkcja, ale dziaa inaczej w rzeczywistoci tworzy ona obiekt (in) o zasigu wykraczajcym poza makroinstrukcj. Dla pocztkujcych programistw i tych, ktrzy zajmuj si pielgnacj kodu, to kolejny twardy orzech do zgryzienia. Jzyk C++ jest dostatecznie skomplikowany, bez wprowadzania dodatkowych niejasnoci, wic, o ile moesz, unikaj uywania makroinstrukcji preprocesora.

Podsumowanie
Bardzo istotna jest moliwo ukrycia wewntrznej implementacji klasy, gdy w przyszoci moesz zechcie j zmieni. Zmiany, ktrych dokonasz, bd wynikay z potrzeby zwikszenia efektywnoci, lepszego zrozumienia problemu lub dlatego, e dostpne bd jakie nowe klasy, ktrych zechcesz uy w swojej implementacji. Wszystko, co zagraa prywatnoci wewntrznej implementacji klasy, zmniejsza zarazem elastyczno jzyka. Dlatego te funkcje inline odgrywaj bardzo wan rol, poniewa praktycznie eliminuj one potrzeb uywania makroinstrukcji preprocesora i towarzyszcych im problemw. W przypadku uycia funkcji inline funkcje skadowe mog by rwnie efektywne, co makroinstrukcje preprocesora. Oczywicie, funkcje inline, wystpujce w definicjach klas, mog by rwnie naduywane. Programista jest wrcz do tego zachcany, poniewa atwiej napisa kod w taki sposb. Nie stanowi tojednak powanego problemu, poniewa pniej, poszukujc moliwoci zmniejszenia kodu, mona zawsze zamieni funkcje inline w zwyke funkcje, bez wpywu na ich dziaanie. Dewiz projektanta powinno by powiedzenie: najpierw spraw, aby dziaao, a pniej optymalizuj".

:0

Thinking in C++. Edycja polska

;wiczenia
Rozwizania wybranych wicze znajduj si w dokumencie elektronicznym: The Thinking in C++ Annotated Solution Guide, ktry mona pobra za niewielkopat z witryny http://www.BruceEckel.com. 1. Napisz program, ktry uywa przedstawionej na pocztku rozdziau makroinstrukcji F() i pokazuje, e niejest ona rozwijana poprawnie, jak to opisano w tekcie. Popraw makroinstrukcj i udowodnij, e teraz ju dziaa poprawnie. 2. Napisz program, wykorzystujcy makroinstrukcj FLOOR(), przedstawion na pocztku rozdziau. Wska warunki, w ktrych nie dziaa ona poprawnie. 3. Zmodyfikuj plik MacroSideEffects.cpp w taki sposb, aby makroinstrukcja BAND( ) dziaaa prawidowo. 4. Utwrz dwie identyczne funkcje fl() i f2(). Okrel funkcj fI() jako funkcj inline, pozostawiajc f2() zwykfunkcj. Wykorzystaj do oznaczenia punktw pocztkowych i kocowych standardowfunkcj biblioteczn clock(), dostpn w pliku nagwkowym <ctime>, a nastpnie porwnaj czasy wykonania obu funkcji, aby przekona si, ktra z nich jest szybsza. Moe trzeba bdzie dokona wielokrotnego wywoania funkcji w ptli, w ktrej mierzony jest czas, aby uzyska moliwe do wykorzystania rezultaty. 5. Przeprowad eksperymenty z wielkoci i zoonoci kodu, zawartego w funkcjach, wystpujcych w poprzednim wiczeniu. Sprawd, czy da si okreli graniczny punkt, w ktrym wykonanie funkcji inline i zwyczajnej funkcji zajmuje tyle samo czasu. Jeeli masz takmoliwo, to wyprbuj rne kompilatory i zanotuj uzyskane wyniki. 6. Udowodnij, e funkcje inline podlegajdomylnie wewntrznemu czeniu. 7. Utwrz klas zawierajctablic znakw. Dodaj konstruktor typu inline, wykorzystujcy funkcj memset(), pochodzc ze standardowej biblioteki jzyka C, do inicjalizacji tablicy wartociargumentu konstruktora (domylnie powinna wynosi ona " "), oraz funkcj inline o nazwie print(), drukujc wszystkie znaki znajdujce si w tablicy. 8. W przykadowym programie NestFriend.cpp, opisanym w rozdziale 5., wymie wszystkie funkcje na funkcje inline w taki sposb, aby nie byy funkcjami in situ. Zamie rwnie funkcje initiaIize() na konstruktory. 9. Zmodyfikuj program StringStack.cpp, opisany w rozdziale 8., w taki sposb, aby wykorzystywa funkcje inline. 10. Utwrz wyliczenie o nazwie Hue, zawierajce elementy red, blue i yellow. Nastpnie utwrz klas o nazwie Color, zawierajc skadow typu Hue, oraz konstruktor, przypisujcy tej skadowej warto, zgodnie z podanym argumentem. Dodaj funkcje udostpniajce, umoliwiajce pobieranie" i ustawianie" wartoci skadowej typu Hue. Uczy wszystkie te funkcje funkcjami inline.

Rozdzia 9. Funkcje inline

321

l. Zmodyfikuj poprzednie wiczenie w taki sposb, by zostao wykorzystane podejcie zwizane z obserwatorem" i modyfikatorem". 2. Zmodyfikuj program Cpptime.cpp w taki sposb, by mierzy czas od rozpoczcia dziaania programu do chwili, gdy uytkownik nacinie klawisz Enter" albo Return". t3. Utwrz klas, posiadajcdwie funkcje skadowe, bdce funkcjami inline. Pierwsza z funkcji, zdefiniowana wewntrz klasy, powinna wywoywa drug funkcj, bez potrzeby tworzeniajej wczeniejszej deklaracji. Napisz funkcj main() tworzcobiekt tej klasy i wywoaj pierwszz funkcji. 14. Utwrz klas A, posiadajc domylny konstruktor typu inline, ktry ogasza swoje wywoanie. Nastpnie utwrz klas B, umie w niej, w charakterze skadowej, obiekt klasy A i utwrz konstruktor klasy B, bdcy funkcjinline. Utwrz tablic obiektw kIasy B i zobacz, co si stanie. 15. Utwrz du liczb obiektw z poprzedniego wiczenia, uywajc klasy Time do okrelenia rnicy w czasie pomidzy wykorzystaniem zwyczajnych konstruktorw i konstruktorw inline jeeli dysponujesz programem profilujcym, sprbuj go rwnie uy). 16. Napisz program, ktry przyjmuje acuchjako argument wiersza polece. Utwrz ptl for, usuwajcz tego acucha w kadym przebiegu pojednym znaku, i wykorzystaj opisan w rozdziale makroinstrukcj DEBUG() do drukowania za kadym razem aktualnej postaci acucha. 17. Popraw makroinstrukcj TRACE( ) w taki sposob,jak opisano w rozdziale, i udowodnij, e dziaa ona poprawnie. 18. Zmodyfikuj makroinstrukcj FIELD() w taki sposb, aby zawieraa dodatkowo numer indeksowy. Utwrz klas, ktrej skadowe zostan utworzone za pomoc wywoa makroinstrukcji FffiLD(). Dodaj funkcj skadow, pozwalajc na dostp do pola, utworzonego za pomoc makroinstrukcji na podstawie jego numeru indeksowego. Napisz funkcj main(), umoliwiajc przetestowanie tej klasy. 19. Zmodyfikuj makroinstrukcj FIELD() w taki sposb, aby dla kadego utworzonego przez siebie pola automatycznie tworzya funkcje udostpniajce (dane powinny jednak nadal pozosta danymi prywatnymi). Utwrz klas, ktrej skadowe utworzono za pomoc wywoa makroinstrukcji FIELD(). Napisz funkcj main(), umoliwiajcprzetestowanie klasy. 20. Napisz program przyjmujcy w wierszu polece dwa argumenty liczb cakowit i nazw pliku. Uyj funkcji znajdujcych si w pliku nagwkowym require.h, by upewni si, e: podano odpowiedni liczb argumentw, warto liczby cakowitej zawiera si w przedziale od 5 do 10, a plik moe by pomylnie otwarty. 2 Napisz program wykorzystujcy makroinstrukcj ffOPEN() do otwarcia pliku w charakterze strumienia wejciowego. Zwr uwag na tworzenie obiektu ifstream oraz jego zasig.

Thinking in C++. Edycja polska 22. (Tradne) Dowiedz si, w jaki sposb wygenerowa na wyjciu kompilatora kod w asemblerze. Utwrz plik zawierajcy bardzo ma funkcj oraz funkcj main(), w ktrej ta funkcjajest wywoywana. Wygeneruj kodwjzyku asemblera, gdy funkcjatajest rozwijanajako funkcja inline i gdy nie jest ona rozwijana, a nastpnie poka, e w przypadku wersji inline nie wystpuje narzut zwizany z wywoaniem funkcji.

Zarzdzanie nazwami
Tworzenie nazw jest jedn z podstawowych czynnoci wykonywanych podczas programowania w miar rozrastania si projektu liczba uywanych nazw moe wyda si przytaczajca. C++ pozwala na zachowanie doskonaej kontroli nad tworzeniem nazw, ich widocznoci, przydzielan nazwom pamici oraz czeniem. Sowo kluczowe static zostao w jzyku C przecione, kiedy nikt jeszcze nie wiedzia, co oznacza termin przecienie", a jzyk C++ nada jeszcze sowu static dodatkowe znaczenia. Podstawowym pojciem, wsplnym dla wszystkich zastosowa sowa static, wydaje si: co, co pozostaje na swoim miejscu" Qak statyczne adunki elektryczne), niezalenie od tego, czy oznacza to fizyczne miejsce w pamici, czy te widoczno w obrbie pliku. W tym rozdziale dowiesz si, w jaki sposb sowo kluczowe static steruje pamici i widocznoci, a take poznasz osigalny w jzyku C++, lepszy sposb kontroli dostpu do nazw, w ktrym jest wykorzystana przestrze nazw (ang. namespace). Dowiesz si rwnie, jak uywa funkcji, ktre zostay napisane i skompilowane w jzyku C.

Rozdzia 10.

Statyczne elementy jzyka C


Zarwno w jzyku C, jak i w C++ sowo kluczowe static ma dwa podstawowe znaczenia, ktre, niestety, czsto ze sob koliduj: 1. Umieszczony pod raz ustalonym adresem oznacza to, e obiekt zosta utworzony w specjalnym statycznym obszarze danych zamiast by tworzony na stosie podczas kadego wywoania funkcji. Odpowiada to pojciupamici statycznej. 2. Lokalny w stosunku do okrelonej jednostki translacji (a take w stosunku do zasigu klasy w jzyku C++, jak zobaczymy pniej). W tym przypadku sowo kluczowe static decyduje o widocznoci nazwy, w taki sposb, e nie jest ona widoczna pozajednostktranslacji lub klas. Opisuje to rwnie pojcie czenia, okrelajce, ktre nazwy s widziane przez program czcy.

Thinking in C++. Edycja polska W tej czci przyjrzymy si powyszym znaczeniom sowa kluczowego static, w takiej postaci, wjakiej zostay one odziedziczone pojzyku C.

mienne statyczne znajdujce si wewntrz funkcji


Kiedy wewntrz funkcji tworzona jest zmienna lokalna, kompilator przydziela jej pami, za kadym razem, gdy wywoywana jest funkcja przesuwajc wskanik stosu o odpowiedni wielko w d. Jeeli istnieje inicjator tej zmiennej, to inicjalizacja nastpuje, ilekrojest przekraczanyjej punkt sekwencyjny. Jednak czasami istnieje potrzeba zachowania wartoci pomidzy wywoaniami funkcji. Mona to osign, definiujc zmienn globaln, ale w takim przypadku zmienna nie bdzie znajdowaa si pod wyczn kontrol funkcji. Jzyki C i C++ pozwalaj na utworzenie w obrbie funkcji obiektu statycznego pami nie jest przydzielana temu obiektowi na stosie, tylko w obszarze danych statycznych programu. Obiekt taki jest inicjalizowany tylko jednokrotnie podczas pierwszego wywoania funkcji; zachowuje on ponadto swoj warto pomidzy wywoaniami funkcji. Na przykad ponisza funkcja przy kadym wywoaniu zwraca nastpny znak znajdujcy si w tablicy: //: C10:StaticVariablesInfunctions.cpp #include " . ./require.h" #include <iostream> using namespace std; char oneChar(const char* charArray = 0) { static const char* s; if(charArray) { s = charArray; return *s; } else require(s, "niezainicjowana zmienna s"); if(*s == '\0') return 0; return *s++; char* a = "abcdefghijklmnopqrstuvwxyz"; int main() { // oneChar(); // require() koczy si niepowodzeniem oneChar(a); // Inicjalizacja s wartoci a while((c = oneCharO) !- 0) cout << c << endl;
char c;

} ///:-

Statyczna zmienna, zdefiniowana jako static char* s, przechowuje swoj warto pomidzy wywoaniami funkcji oneCbar(), poniewa przydzielona jej pami tue stanowi czci ramki stosu funkcji, ale znajduje si w obszarze danych statycznych programu. Kiedy funkcja oneChar() zostanie wywoana z argumentem typu char , to warto tego argumentu przypisywana jest zmiennej s i zwracany jest pierwszy

Rozdzia 10. Zarzdzanie nazwami

325

znak, znajdujcy si w tablicy. Kade kolejne wywoanie funkcji oneChar( ) bez argumentu powoduje przypisanie argumentowi charArray domylnej, zerowej wartoci, co informuje funkcj, e nadal pobierane s znaki, wskazywane przez uprzednio zainicjowan warto zmiennej s. Funkcja ta bdzie nadal zwraca znaki, a do momentu, gdy zostanie napotkana warto zerowa, koczca tablic znakow. W tym miejscu funkcja przestanie inkrementowa wskanik, dziki czemu nie wykroczy on poza koniec tablicy. Co si jednak stanie w przypadku, gdy funkcja oneChar() zostanie wywoana bez argumentw, a warto zmiennej s nie zostanie uprzednio zainicjowana? W definicji zmiennej s mona by dostarczy warto inicjujc:
static char* s = 0;

Lecz jeeli w przypadku statycznej zmiennej wbudowanego typu nie zostanie okrelona inicjujca j warto, to kompilator gwarantuje, e zmienna ta zostanie zainicjowana wartoci zerow (przeksztacon w odpowiedni typ) w czasie rozpoczcia pracy programu. Tak wic gdy funkcja oneChar() jest wywoywana po raz pierwszy, zmienna s ma warto zerow. W tym przypadku zostanie to wykryte przez instrukcj warunkowaif(!s)1. Przedstawiona powyej inicjalizacja zmiennej s jest bardzo prosta. Jednake inicjalizacja obiektw statycznych (podobnie jak wszystkich innych obiektw) moe by dowolnym wyraeniem, zawierajcym stae oraz uprzednio zadeklarowane zmienne i funkcje. Naley by wiadomym tego, e przedstawiona powyej funkcja jest bardzo podatna na problemy zwizane z wielowtkowoci ilekro projektowane s funkcje, zawierajce zmienne statyczne, naley pamita o kwestiach dotyczcych wielowtkowoci.

Statyczne obiekty klas znajdujce si wewntrz funkcji


Zasady s identyczne jak w przypadku statycznych obiektw, wcznie z tym, e w przypadku obiektw wymagany jest jaki rodzaj inicjalizacji. Jednak przypisanie wartoci zerowej ma sens jedynie w przypadku typw wbudowanych typy zdefiniowane przez uytkownika musz by inicjalizowane za pomoc wywoania konstruktora. Jeeli zatem podczas definicji statycznego obiektu nie zostan okrelone argumenty konstruktora, to klasa bdzie musiaa posiada konstruktor domylny, jak w poniszym przykadzie: //: C10:StaticObjectsInFunctions.cpp #include <iostream> using namespace std; class X { int i ; public: X(int ii - 0) : i(ii) {} // Domylny -X() { cout << "X::~X()" << endl; }

Wywoywan w funkcji require() przyp. tum.

26
void f() { static X xl(47); static X x2; // Wymagany jest konstruktor domylny
int main() { f(); } III-

Thinklng ln C++. Edycja polsk

Statyczny obiekt typu X, znajdujcy si wewntrz funkcji f(), moe by zainicjowany zarwno za pomoc listy argumentw konstruktora, jak i konstruktora domylnego. Inicjalizacja ta zachodzi wwczas, gdy sterowanie po raz pierwszy przechodzi przez definicj obiektu i tylko po raz pierwszy.

Destruktory obiektw statycznych


Destruktory obiektw statycznych (czyli wszystkich obiektw umieszczonych w statycznej pamici, a nie tylko lokalnych obiektw statycznych, jak w powyszym przykadzie) s wywoywane wtedy, gdy koczy prac funkcja main() lub wywoana jest jawnie funkcja exit(), zawarta w standardowej bibliotece C++. W przypadku wikszoci implementacji funkcja main(), koczc prac, wywouje po prostu funkcj erit(). Oznacza to, e wywoanie funkcji exit() wewntrz destruktora jest potencjalnie niebezpieczne, moe bowiem prowadzi do nieskoczonej rekurencji. Destruktory obiektw statycznych nie s wywoywane, jeeli opuci si program, uywajc funkcji abort(), nalecej do standardowej biblioteki jzyka C. Mona okreli dziaania, ktre nastpi po opuszczeniu funkcji main() (albo wywoania funkcji exit()), uywajc do tego celu funkcji atexit(), zawartej w standardowej bibliotece jzyka C. W takim przypadku funkcja zarejestrowana przez funkcj atexit() moe zosta wywoana przed destruktorami wgzelkich obiektw, utworzonych przed opuszczeniem funkcji main() (lub wywoaniem funkcji exit()). Podobnie jak w przypadku zwykego niszczenia obiektw, niszczenie obiektw statycznych nastpuje w kolejnoci odwrotnej do kolejnoci ich inicjalizacji. Jednak niszczone s tylko te obiekty, ktre zostay utworzone. Na szczcie, narzdzia programistyczne jzyka C++ umoliwiaj zapamitanie kolejnoci tworzenia obiektw. Obiekty globalne s zawsze tworzone przed wejciem do funkcji main(), a usuwane po jej zakoczeniu. Jednake w przypadku gdy funkcja zawierajca lokalne obiekty statyczne nie jest nigdy wywoywana, nie jest te nigdy wykonywany jej konstruktor, a wic take i jej destruktor. Ilustruje to poniszy przykad:

//: C10:StaticDestructors.cpp // Destruktory obiektw statycznych #include <fstream> using namespace std; ofstreamout("statdest.out"); // Plik ledzenia

class Obj { char c; // Identyfikator public: Obj(char cc) : c(cc) { out << "Obj::Obj() dla " << c << endl:

iat 10. Zarzdzanie nazwami ~Obj() { out << "Obj: :~ObjO dla " << c << endl;

327

Obj a ( ' a ' ) ; // Obiekt globalny (pami statyczna) // Konstruktor i destruktor s zawsze wywoywane void f() { static Obj b ( ' b ' ) ;

void g() { static Obj c('c'); int main() { out << "wewntrz funkcji main()" << endl; f(); // Wywoanie statycznego konstruktora obiektu b // Funkcja g() nie zostaa wywoana out << "opuszczanie funkcji main()" << endl;
W klasie Obj skadowa znakowa c dziaa jako identyfikator, dziki czemu zarwno konstruktor, jak i destruktor mog drukowa informacje o obiekcie, na ktrym dziaaj. Obiekt ajest obiektem globalnym, wic jego konstruktorjest wywoywany zawsze przed wejciem do funkcji main( ). Jednake konstruktory obiektw statycznych b i c, zdefiniowanych odpowiednio wewntrz funkcji f( ) i g( ), s uruchamiane tylko wwczas, gdy wywoywane s te funkcje. Aby zademonstrowa, ktre konstruktory i destruktory s uruchamiane, wywoywana jest tylko funkcja f( ). Wyniki dziaania programu snastpujce: Obj::Obj() dla a wewntrz funkcji main() Obj::Obj() dla b opuszczanie funkcji main() Obj::~Obj() dla b Obj::~Obj() dla a Konstruktor obiektu a jest wywoywany przed wejciem do funkcji main( ), a konstruktor obiektu b tylko dlatego, e wywoywana jest funkcja f( ). Kiedy koczy si funkcja main( ), wywoywane s destruktory tych obiektw, ktre zostay utworzone w kolejnoci odwrotnej do kolejnoci ich tworzenia. Oznacza to, e gdyby wywoana zostaa funkcja g( ), to kolejno wykonania destruktorw obiektw b i c zaleaaby od tego, czyjako pierwsza wywoana zostaa funkcja f( ), czy te g( ). Zwr uwag na to, e obiekt out, reprezentujcy plik ledzenia typu ofstream, jest rwnie obiektem statycznym poniewa zosta on zdefiniowany poza wszystkimi funkcjami, wic znajduje si w obszarze danych statycznych. Wane jest, e jego definicja (w odrnieniu od deklaracji extern) znajduje si na pocztku pliku, przed jakimkolwiek moliwym uyciem obiektu out. W przeciwnym razie obiekt byby bowiem uywany, zanim zostaby prawidowo zainicjowany.

!8

Thinklng in C++. Edycja polska

W jzyku C++ konstruktor globalnego obiektu statycznego jest uruchamiany jeszcze przed wywoaniem funkcji main(), co udostpnia prosty i przenony sposb wykonywania kodu przed wejciem do funkcji main(), a take wykonanie kodu zawartego w destruktorze po opuszczeniu funkcji main(). Uzyskanie tego w jzyku C stanowio zawsze eksperyment, wymagajcy przeszukiwanie asemblerowego kodu, inicjujcego prac programu dostarczanego przez producenta kompilatora.

Sterowanie ^czeniem
W normalnym przypadku kada nazwa znajdujca si w zasigu pliku (to znaczy niezagniedona w adnej klasie ani funkcji), jest widoczna we wszystkich jednostkach translacji programu. Jest to czsto okrelane mianem czenia zewntrznego, poniewa w trakcie czenia nazwa jest widoczna dla programu czcego w kadym miejscu, znajdujcym si na zewntrz jednostki translacji, w ktrej si ona znajduje. Zmienne globalne oraz zwyke funkcje podlegajczeniu zewntrznemu. Zdarzaj si jednak przypadki, w ktrych chcielibymy ograniczy widoczno nazwy. Moglibymy potrzebowa zmiennej, widocznej w zasigu pliku, dziki czemu istniaaby moliwo wykorzystania jej przez wszystkie funkcje, zawarte w tym pliku, ale jednoczenie nie chcie, by funkcje, znajdujce si w innych plikach, widziay j lub miay do niej dostp. Nie chcielibymy rwnie niechccy wywoa konfliktu nazw w stosunku do identyfikatorw, znajdujcych si w innych plikach. Obiekt lub nazwa funkcji, znajdujcy si w zasigu pliku, ktry zosta jawnie zadeklarowany jako statyczny, jest lokalny w stosunku do swojej jednostki translacji (zgodnie z terminologi stosowan w ksice w stosunku do pliku cpp, w ktrym wstpuje jego deklaracja). Jego nazwa podlega czeniu wewntrznemu. Oznacza to, e ta sama nazwa moe by uywana w innych jednostkach translacji, nie wywoujc konfliktu nazw. Jedn z zalet czenia wewntrznego jest ta, e nazwa moe zosta umieszczona w pliku nagwkowym i nie ogrywa adnej roli ewentualny konflikt nazw. Nazwy, ktre s typowo umieszczane w plikach nagwkowych, takie jak definicje staych lub funkcje inline, s domylnie czone wewntrznie ^ednak stae podlegaj domylnie wewntrznemu czeniu tylko w jzyku C++ w jzyku C s one domylnie czone zewntrznie). Zwr uwag na to, e czenie odnosi si jedynie do elementw posiadajcych w chwili czenia (adowania) swj adres. Deklaracje klas oraz zmienne lokalne nie podlegajzatem czeniu.

Zamieszanie
Poniej zamieszczono przykad, w ktrym oba znaczenia sowa statyczny" mog si ze sob pokrywa. Wszystkie obiekty globalne nale w sposb niejawny do klasy pamici statycznej, wic jeeli napiszemy (w zasigu pliku):
int a = 0;

:< i

to zmiennej a zostanie przydzielona pami w obszarze danych statycznych, a inicjalizacja tej zmiennej nastpi przed wejciem do funkcji main(). W dodatku zmienna ta bdzie widoczna globalnie, we wszystkich jednostkach translacji. W terminologii

^Rozdzia 10. << Zarzdzanie nazwami

329

dotyczcej widocznoci przeciwiestwem sowa kluczowego static (widoczny tylko w biecejjednostce translacji)jest slowo kluczowe extern, okrelajce wjawny sposb, e widoczno nazwy obejmuje wszystkie jednostki translacji. Tak wic powysza definicjajest rwnowana zapisowi:
extern int a = 0;

Jeeli jednak zamiast tego napiszemy: static int a - 0; tojedynym rezultatem bdzie zmiana widocznoci zmiennej, dziki czemu zmienna a podlega od tej pory czeniu wewntrznemu. Klasa pamici pozostaje jednak niezmieniona obiekt znajduje si w obszarze danych statycznych, niezalenie od tego, czyjego widoczno ma charakter statyczny" (static) czy te zewntrzny" (extern). Kiedy przechodzimy do zmiennych lokalnych, sowo kluczowe static przestaje zmienia widoczno zmiennych, wywierajc natom|ast wpyw na ich klas pamici. Jeeli zmienn, ktra wyglda na zmienn lokaln, zadeklaruje si przy uyciu sowa kluczowego extern, oznacza to, e jest ona przechowywana w jakim innym miejscu pamici ^est wic ona w rzeczywistoci globalna w stosunku do funkcji). Na przykad: //: C10:LocalExtern.cpp //{L} LocalExtern2 #include <iostream> int main() { extern int i ; std : : cout << i :
/ / : C10:LocalExtern2.cpp {0} int i = 5;

W przypadku nazw funkcji (dla funkcji niebdcych skadowymi klas) sowa kluczowe static oraz extern zmieniaj tylko ich widoczno. Dlatego te zapis: extern void f(); jest rwnowany zwykej deklaracji: void f(); Natomiast nastpujcy zapis: static void f(); oznacza, e funkcja f( ) jest widoczna tylko w obrbie biecej jednostki translacji , czasami okrela si, e jest ona statyczna w obrbie pliku.

130

Thinking in C++. Edycja polsk

Inne specyfikatory klas pamici


Najczciej uywane s specyfikatory static i extern; sjeszcze dwa inne, rzadziej stosowane. Specyfikator auto nie jest niemal nigdy wykorzystywany, poniewa informuje on kompilator, e zmienna jest zmienn lokaln. Sowo kluczowe auto jest skrtem sowa automatyczny" i odnosi si do sposobu, wjaki kompilator przydziela zmiennym pami. Poniewa kompilator potrafi zawsze okreli to na podstawie kontekstu, w ktrym zdefiniowane s zmienne, wic sowo kluczowe autojest sowem nadmiarowym. Zmienna zdefiniowana za pomoc sowa kluczowego register jest zmienn lokaln (auto), zawierajc wskazwk dla kompilatora, e bdzie ona intensywnie uywana, w zwizku z czym kompilator powinien o ile to moliwe przechowywa j w rejestrze. Specyfikator register wspomaga wic proces optymalizacji. Rne kompilatory w rozmaity sposb reagujna t wskazwk zazwyczaj posiadajrwnie opcj umoliwiajcjej zignorowanie. W przypadku pobrania adresu zmiennej specyfikator register zostanie niemal na pewno zignorowany. Naley unika stosowania sowa kluczowego register, poniewa kompilator potrafi na og przeprowadzi optymalizacj lepiej ni programista.

Przestrzenie nazw
Mimo e nazwy mog by zagniedane w klasach, to nazwy globalnych funkcji oraz klas znajdujsi nadal wjednej, globalnej przestrzeni nazw. Sowo kluczowe static zapewnia nad tym pewn kontrol, umoliwiajc okrelenie, e zmienne i funkcje podlegaj czeniu wewntrznemu (czyli s one statyczne w obrbie pliku). Jednak w przypadku duego projektu brak kontroli nad globaln przestrzeni nazw moe powodowa problemy. Aby poradzi sobie z nimi w stosunku do klas, producenci tworz czsto dugie, skomplikowane nazwy. Zapobiegaj one co prawda wystpieniu konfliktu, ale ich wpisywanie jest niewygodne (w celu ich uproszczenia czsto uywana jest deklaracja typedef). Niejest to eleganckie rozwizanie wspierane przezjzyk. Wykorzystujc dostpne w jzyku C++ przestrzenie nazw (ang. namespaces), mona podzieli globaln przestrze nazw na atwiejsze w zarzdzaniu czci. Podobnie jak sowa kluczowe class, struct, enum i union, sowo namespace umieszcza nazwy swoich skadowych w odrbnej przestrzeni. Podczas gdy inne wymienione sowa kluczowe majjeszcze inne zastosowania, to w przypadku sowa kluczowego namespace sprowadza si ono jedynie do utworzenia nowej przestrzeni nazw.

Tworzenie przestrzeni nazw


Tworzenie przestrzeni nazw jest do podobne do tworzenia klas: //: C10:MyLib.Cpp namespace MyLib { // Deklaracje } int ma1n() {} ///:-

10. Zarzdzanie nazwami

331

Powoduje ono utworzenie nowej przestrzeni nazw, obejmujcej zawarte w niej deklaracje. W porwnaniu ze sposobem uycia sw kluczowych class, struct, union i enum wystpujtujednak do istotne rnice: 4 Definicja przestrzeni nazw moe wystpowa tylko w zasigu globalnym lub by zagniedona w innej przestrzeni nazw. 4 Po nawiasie klamrowym, zamykajcym definicj przestrzeni nazw, niejest wymagany rednik. f Definicja przestrzeni nazw moe by kontynuowana" w wielu plikach nagwkowych za pomoc skadni, ktra w przypadku klas wygldaaby na powtrzondefinicj: //: C10:Headerl.h #1 fndef HEADERl_H #define HEADERl_H namespace MyLib { extern int x; void f();

#endif // HEADERl_H lll:~

II: C10:Header2.h

#ifndef HEADER2_H

#define HEADER2_H #include "Headerl.h" // Dodanie do MyLib kolejnych nazw namespace MyLib { // To nie jest powtrna definicja! extern int y; void g();

#endif // HEADER2_H ///://: C10:Continuation.cpp #include "Header2.h" int main() {} ///:Mona utworzy synonim nazwy przestrzeni nazw, dziki czemu unika si wpisywania niewygodnych nazw, utworzonych przez producentw bibliotek:
/ / : C10:BobsSuperDuperLibrary.cpp namespace BobsSuperDuperLibrary { class Widget { /* . . . */ }; class Poppit { /* .. . */ };

// Zbyt duo wpisywania! Utworzymy synonim: namespace Bob = BobsSuperDuperLibrary; int main() {} III

W odrnieniu od klasy, nie mona utworzy egzemplarza przestrzeni nazw.

32

Thinking in C++. Edycja polska

tezimienne przestrzenie nazw


Kada jednostka translacji zawiera bezimienn przestrze nazw, ktr mona powiksza, uywajc specyfikatora namespace bez identyfikatora:

//: C10:UnnamedNamespaces.cpp namespace { class Arm { /* ... */ }; class Leg { /* ... */ }; class Head { /* ... */ }; class Robot { Arm arm[4]; Leg leg[16]; Head head[3]; // ... } xanthan; int i, j. k; } int main() {} ///:Nazwy zawarte w tej przestrzeni s automatycznie dostpne w biecej jednostce translacji, bez potrzeby jej nazywania. Bezimienna przestrze nazw jest unikatowa w kadej jednostce translacji. Jeeli w bezimiennej przestrzeni nazw zostan umieszczone nazwy lokalne, to nie maju potrzeby uywania w stosunku do nich sowa kluczowego static w celu okrelenia, e maj podlega czeniu wewntrznemu. Aby ograniczy zasig nazwy do pliku w jzyku C++, zaleca si uywanie bezimiennych przestrzeni nazw zamiast sowa kluczowego static.

>rzyjaciele
Do przestrzeni nazw mona wstrzykn deklaracj friend, umieszczajc j wewntrz kasy, zawartej w tej przestrzeni: / / : C10:FriendInjection.cpp namespace Me { class Us { //... friend void you(); }: } int main() {} II/:Obecnie funkcja you() naley do przestrzeni nazw Me. Jeeli przyjaciel" zostanie umieszczony w obrbie klasy, zawartej w globalnej przestrzeni nazw, bdzie on wstrzyknity" globalnie.

Uywanie przestrzeni nazw


Do nazwy zawartej w przestrzeni nazw mona odwoa si na trzy sposoby: okrelajc j za pomoc operatora zasigu, za pomoc dyrektywy using, umoliwiajcej dostp do wszystkich nazw zawartych w przestrzeni nazw, oraz uywajc deklaracji using, udostpniajcejjednorazowo pojedyncznazw.

Rozdzia 10. Zarzdzanie nazwami

333

Zasig
Kada nazwa znajdujca si w przestrzeni nazw moe by wskazana jawnie, za pomoc operatora zasigu w taki sam sposb, w jaki odwoujemy si do nazw w obrbie kIas: //: C10:ScopeResolution.cpp namespace X { class Y { static int i ; public: void f(); }:
class Z; void funcO;

int u, v, w; public: Z(int i); int gO; }: X::Z::Z(int i) { u - v - w - i; } int X : : Z : :g() { return u = v = w = 0; }

int X : : Y : : i = 9; class X : : 2 {

a.g(); } int main(){} lll:~ Zwr uwag na to, e definicja X::Y::i mogaby si rwnie dobrze odnosi do skadowej klasy Y, zagniedonej w klasie X, a nie do przestrzeni nazw X. Na podstawie przedstawionych wyej rozwaa mona stwierdzi, e przestrzenie nazw w duym stopniu przypominajklasy.

void X::func() { X : : Z a(l);

Dyrektywa using
Poniewa wpisywanie penych kwalifikatorw nazw, zawartych w przestrzeni nazw, szybko moe sta si mczce, sowo kluczowe using umoliwia zaimportowanie od razu caej przestrzeni nazw. Uycie tego sowa wraz ze sowem kluczowym namespace nosi nazw dyrektywy using (ang. using directive). Dyrektywa using powoduje, e nazwy wygldaj tak, jakby naleay do najbliszego zasigu, obejmujcego t deklaracj, co pozwala na wygodne uywanie nazw niekwalifikowanych. Rozwamy prost przestrze nazw: //: C10:NamespaceInt.h fifndef NAMESPACEINT_H define NAMESPACEINT_H namespace Int { enum sign { positive. negative }; class Integer { int i; sign s;

Thinking in C++. Edycja polska public: Integer(int ii : i(ii).

0)

s(i >- 0 ? positive : negative)

sign getSign() const { return s; } void setSign(sign sgn) { s - sgn; }

#endif // NAMESPACEINT_H lll:~ Jednym z zastosowa dyrektywy using jest przeniesienie wszystkich nazw zawartych w przestrzeni Int do innej przestrzeni nazw i jednoczenie pozostawienie je zagniedonymi w tej przestrzeni nazw:

//: C10:NamespaceMath.h #ifndef NAMESPACEMATH_H #define NAMESPACEMATH_H #include "NamespaceInt.h" namespace Math { using namespace Int; Integer a. b; Integerdivide(Integer. Integer);
#endif // NAMESPACEMATH_H lll:~

Mona rwnie zadeklarowa wszystkie nazwy przestrzeni nazw Int wewntrz funkcji, pozostawiajcjejednak zagniedonymi wewntrz tej funkcji:

//: C10:Arithmetic.cpp #include "NamespaceInt.h" void arithmetic() { using namespace Int; Integer x;
x.setSign(positive); } int main(){} lll:~

Gdyby nie zostaa uyta dyrektywa using, wszystkie nazwy, znajdujce si w przestrzeni nazw, musiayby posiada pene kwalifikatory. Jeden z aspektw dyrektywy using moe wydawa si, na pierwszy rzut oka, nieco nielogiczny. Widoczno nazw, wprowadzona za pomoc dyrektywy using, jest ograniczona do zasigu, w ktrym dyrektywa ta zostaa uyta. Mona jednak zasoni nazwy pochodzce z dyrektywy using w taki sposb, jakby byy one nazwami globalnymi w stosunku do tego zasigu!

//: C10:NamespaceOverridingl.cpp #include "NamespaceMath.h" int main() { using namespace Math;

Integer a; // Zasania Math::a; a.setSign(negative); // Teraz uycie operatora zasigu do

Rozdzia 10. << Zarzdzanie nazwami // wybrania Math::a jest niezbdne: Math::a.setSign(positive); } /II-

335

Zamy, e istnieje druga przestrze nazw, zawierajca niektre nazwy znajdujce si w przestrzeni nazw Math: //: C10:NamespaceOverriding2.h #ifndef NAMESPACEOVERRIDING2_H #define NAMESPACEOVERRIDING2_H #include "NamespaceInt.h" namespace Calculation { using namespace Int; Integerdivide(Integer, Integer); // ... }

#endif // NAMESPACEOVERRIDING2JH / / / : -

Poniewa ta przestrze nazw zostaa rwnie wprowadzona za pomoc dyrektywy using, istnieje moliwo wystpienia kolizji. Dwuznaczno wystpuje jednak w miejscu uycia nazwy, a nie w miejscu wystpowania dyrektywy using:
/ / : C10:OverridingAmbiguity.cpp #include "NamespaceMath.h" #i nc1ude "NamespaceOve r r i d i ng2.h" void s() { using namespace Math; using namespace Calculation; // Wszystko jest w porzdku, a do instrukcji: //! divide(l. 2); // Dwuznaczno } int main() {} ///:-

A zatem moliwe jest uycie dyrektyw using w taki sposb, by wprowadzi pewn liczb przestrzeni nazw, zawierajcych kolidujce ze sob nazwy, nawet nie wywoujc dwuznacznoci.

Deklaracja using
Mona wprowadza do biecego zasigu po jednej nazwie, wykorzystujc deklaracj using. W odrnieniu od dyrektywy using, traktujcej nazwy w taki sposb, jakby byy one zadeklarowane globalnie w stosunku do zasigu, deklaracja using jest deklaracj w obrbie biecego zasigu. Oznacza to, e moe ona zasoni nazwy pochodzce z dyrektywy using: //: C10:UsingDeclaration.h #ifndef USINGDECLARATION_H #define USINGDECLARATION_H namespace U { inline void f() {} inline void g() {} } namespace V { inline void f() {} inline void g{) {}

36

Thinking in C++. Edycja polska

#endif // USINGDECLARATION_H IIIII: C10:UsingDeclarationl.cpp |include "Us1ngOeclaration.h" void h() { using namespace U; // Dyrektywa using using V::f; // Deklaracja using f(); // Wywoanie V::f(); U::f(); // Nazwa funkcji musi by w peni okrelona } int main() {} ///:Deklaracja using podaje w peni okrelon nazw identyfikatora, nie zawiera jednak adnych informacji o typie. Oznacza to, e w przypadku gdy przestrze nazw zawiera zbir przecionych funkcji o tych samych nazwach, deklaracja using obejmuje wszystkie funkcje nalece do przecionego zbioru. Mona umieci deklaracj using w kadym miejscu, w ktrym moe wystpi zwyka deklaracja. Deklaracja using zachowuje si w taki sam sposb, jak kada inna deklaracja, z jednym wyjtkiem poniewa nie jest w niej podawana lista argumentw, za pomoc deklaracji using moliwe jest przecienie funkcji o identycznych typach argumentw (przeciwnie ni w przypadku zwykego przecienia). Jednak ta dwuznaczno nie ujawnia si a do miejsca uycia takiej funkcji (a nie miejsca jej deklaracji). Deklaracja using moe wystpi rwnie w przestrzeni nazw, gdzie jej znaczenie jest takie samo, jak w kadym innym miejscu definiuje ona w tej przestrzeni nazw: //: C10:UsingDeclaration2.cpp #include "UsingDeclaration.h" namespace Q { using U: :f; using V: :g; void m() { using namespace Q; f(); // Wywoanie U::f(); g(); // Wywoanie V: :g(); } int main() {} lll:~ Deklaracja using stanowi synonim pozwala ona na deklaracj tej samej funkcji w rnych przestrzeniach nazw. Jeeli doprowadzi to do wielokrotnej deklaracji tej samej funkcji podczas importowania rnych przestrzeni nazw, nie wystpi problem nie pojawi si adne dwuznacznoci ani zwielokrotnienia.

Wykorzystywanie przestrzeni nazw


Niektre z przedstawionych powyej regu mog wyda si pocztkowo nieco znie1 chcajce zwaszcza gdy odniose wraenie, e bdziesz uywa ich wszystkiC" jednoczenie. Jednakna og mona sobie poradzi, wykorzystujc przestrzenie na/* w bardzo prosty sposb pod warunkiem, e wiesz, jak one dziaaj. Naley

Rozdzia 10. Zarzdzanie nazwami

337

wszystkim pamita, e wprowadzajc globaln dyrektyw using (umieszczajc, poza jakimkolwiek zasigiem, using namespace") udostpnia si temu plikowi przestrze nazw. Dziaa to zwykle bez zarzutu w przypadku plikw zawierajcych implementacj (plikw cpp"), poniewa dyrektywa using obowizuje tylko do zakoczenia ich kompilacji. Oznacza to, e nie ma ona wpywu na inne pliki, dziki czemu mona dostosowa zarzdzanie przestrzeniami nazw dla kadego pliku z osobna. Jeli na przykad odkryjesz kolizj nazw, spowodowan uyciem zbyt wielu dyrektyw using wjakim konkretnym pliku implementacyjnym, to atwo mona wprowadzi w tym pliku zmiany w taki sposb, aby wykorzystywa on pene, jawne nazwy lub deklaracje using bez potrzeby modyfikacji innych plikw implementacyjnych. Inn kwesti s pliki nagwkowe. Waciwie nigdy nie umiecisz globalnych dyrektyw using w plikach nagwkowych, poniewa oznaczaoby to, e we wszystkich plikach, do ktrych zostayby doczone te pliki, udostpnione byyby rwnie okrelone za pomoc tych dyrektyw przestrzenie nazw (a pliki nagwkowe mog by doczane rwnie do innych takich plikw). Tak wic w plikach nagwkowych naley uywa albo jawnych, penych kwalifikatorw dyrektyw using, umieszczonych wewntrz zasigw, albo te deklaracji using. Jest to praktyka, ktra wystpuje w tej ksice. Postpujc zgodnie z ni, nie zamiecisz" globalnej przestrzeni nazw, co spowodowaoby powrt do wiata C++ sprzed powstania przestrzeni nazw.

Statyczne skadowe w C++


Zdarzaj si sytuacje, w ktrych potrzebny jest pojedynczy obszar pamici, wykorzystywany przez wszystkie obiekty klasy. W jzyku C mona by uy w takim przypadku zmiennej globalnej, ale nie jest to zbyt bezpieczne. Dane globalne mog by uywane przez kadego, a ich nazwy niekiedy kolidujz innymi nazwami, wystpujcymi w duych projektach. W idealnej sytuacji dane mogy by przechowywane w taki sposb, jakby byy globalne, pozostajc rwnoczenie ukryte wewntrz klasy i przejrzycie z t klas powizane. Uzyskuje si to za pomoc statycznych danych skadowych, zawartych w kasach. Niezalenie od tego, ile obiektw danej klasy zostanie utworzonych, kada jej skadowa statyczna zajmuje pojedynczy obszar pamici. Wszystkie obiekty dziel midzy siebie ten sam obszar pamici, przechowujcy statyczne dane skadowe; umoliwiaj wic one komunikacj" pomidzy obiektami. Dane statyczne nalejednak do klasy ich nazwy znajduj si w zasigu klasy i mog by publiczne, prywatne lub chronione.

iefiniowanie pamici Ja statycznych danych sMadowych


Poniewa dane statyczne zajmuj pojedynczy obszar pamici, niezalenie od tego, ile utworzono obiektw klasy, pami ta musi zosta zdefiniowana w jednym miejscu.

138

Thinking in C++. Edycja polska

Kompilator nie przydzieli ci tej pamici, a program czcy zgosi bd, w przypadku gdy statyczne dane skadowe zostan zadeklarowane, ale nie bd zdefiniowane. Definicja musi wystpi na zewntrz klasy (wstawianie jej do klasy nie jest moliwe) i dopuszczalne jest tylko jedno jej wystpienie. Tak wic typowe jest umieszczenie definicji w pliku, zawierajcym implementacj klasy. Skadnia definicji sprawia niektrym kopoty, ale jest w rzeczywistoci do logiczna. Jako przykad utworzymy statycznskadow, wewntrz klasy takjak to przedstawiono poniej:
class A {

static int i; public:

Nastpnie trzeba zdefiniowa pami dla statycznej skadowej w pliku zawierajcym definicj klasy:
int A::i = 1;

Gdybymy definiowali zwyk zmiennglobaln, to wygldaoby to nastpujco:


int i = 1;

Ale w naszym przypadku do okrelenia zmiennej A::i zosta uyty operator zasigu oraz nazwa klasy. Niektrzy majkopot ze zrozumieniem, e cho zmienna A::ijest prywatna, w zapisie tym znajduje si co, co wyglda na wykonywanie na niej operacji w otwarty sposb. Czy nie oznacza to zamania mechanizmw zabezpieczajcych? Jest to zupenie bezpieczne dziaanie z dwch powodw. Po pierwsze: jedynym miejscem, w ktrym wolno dokona takiej inicjalizacji, jest definicja. Istotnie, gdyby skadow statycznby obiekt, posiadajcy konstruktor, to zamiast uywania operatora = naleaoby wywoa ten konstruktor. Po drugie: po dokonaniu definicji skadowej statycznej uytkownik nie moe dokona jej powtrnej definicji spowodowaoby to zgoszenie bdu przez program czcy. Ponadto twrca klasy jest zmuszony do utworzenia tej definicji, gdy w przeciwnym wypadku kodu nie daoby si poczy podczas testowania. Wszystko to stanowi gwarancj, e definicja wystpuje tylko jeden raz i e znajduje si cakowicie w rkach twrcy klasy. Cae wyraenie inicjalizujce skadow statyczn jest zawarte w zasigu klasy. wiadczy o tym poniszy przykad:

//: C10:Statinit.cpp // Zasig inicjatora skadowej statycznej finclude <iostream> using namespace std;

int x = 100;
class WithStatic { static int x; static int y; public:

Rozdzia 10. * Zarzdzanie nazwami void print() const { cout << "WithStatic::x = " << x << endl; cout << "WithStatic: :y = " << y << endl ;

339

int WithStatiC::x = 1; int WithStatic: :y = x + 1; // WithStatiC::x NIE ::x

int main() { WithStatic ws; ws.print();


W tym przypadku kwalifikator WithStatic:: rozciga zasig kIasy WithStatic na ca definicj.

lnicjalizacja tablicy statycznej


W rozdziale 8. zostay wprowadzone stae statyczne (static const), pozwalajce na zdefiniowanie wewntrz ciaa klasy staych wartoci. Moliwe jest rwnie utworzenie tablic obiektw statycznych zarwno staych, jak i niebdcych staymi. Skadniajest w tym przypadku do spjna: / / : C10:StaticArray.cpp // lnicjalizacja statycznych tablic w klasach class Values { // Stale statyczne s inicjalizowane na miejscu: static const int scSize = 100; static const long scLong = 100; // W przypadku tablic statycznych dziaa automatyczne // zliczanie. Tablice i skadowe statyczne niecakowite // oraz niebdce staymi musza by // inicjalizowane zewntrznie. static const int scInts[]; static const long scLongs[]; static const float scTable[]; static const char scLetters[]; static int size; static const float scFloat; static float table[]; static char letters[]; int Values::size 100;

const float Values::scFloat = 1.1;


const int Values::scInts[] = {

99. 47. 33. 11. 7

const long Values::scLongs[]

99. 47, 33. 11. 7

10

Thinking in C++. Edycja polska const float Values::scTable[] 1.1. 2.2. 3.3, 4.4

const char Values::scLetters[] 'a'. 'b', 'c', 'd', 'e'. 'f'. 'g', 'h'. '1'. 'j'
float Values::table[4] 1.1, 2.2. 3.3, 4.4

char Values::letters[10] = 'a', 'b', 'c', 'd', 'e'. 'f', 'g'. 'h'. 'i'. 'j' int main() { Values v; } ///:W przypadku statycznych staych, cakowitych typw, definicji mona dokona wewntrz klas. Jednake w kadym innym przypadku (wliczajc w to tablice typw cakowitych, nawet gdy s one statycznymi staymi) naley dostarczy pojedyncz, zewntrzn definicj skadowej. Definicje takie podlegaj czeniu wewntrznemu, dlatego te mog one zosta umieszczone w plikach nagwkowych. Skadnia inicjalizacji statycznych tablic jest taka sama, jak w przypadku kadego innego agregatu, w tym automatycznego zliczania. Mona rwnie utworzy statyczne stae obiekty klas, a take tablice takich obiektw. Nie mona ich jednak inicjalizowa wewntrz klasy", co jest dopuszczalne tylko w przypadku statycznych staych, wbudowanych typw cakowitych: / / : C10:StaticObjectArrays.cpp // Statyczne tablice obiektw klas class X { int i; public: X(int ii) : i(ii) {} class Stat { // To nie dziaa, ale by moe // chciaby tak wanie zrobi: / / ! static const X x(100): // Zarwno stae statyczne obiekty klas. jak // takie, ktre nie s staymi, musza by // zainicjalizowane zewntrznie: static X x2;

static X xTable2[]; static const X x3; static const X xTable3[];

X Stat::x2(100);

X Stat::xTable2[] - {

Rozdzia 10. Zarzdzanie nazwami


X ( 1 ) .X ( 2 ) ,X ( 3 ) .X ( 4 )
const X Stat::x3(100);

341

const X Stat::xTable3[] = X(1), X(2). X(3). X(4) int main() { Stat v; } ///:Inicjalizacje zarwno staych tablic obiektw klas, jak i tablic niebdcych staymi musz by przeprowadzone w ten sam sposb, zgodnie z typow skadni definicji static.

Klasy zagniedone i klasy lokalne


Mona w atwy sposb umieci statyczne dane skadowe w klasach, ktre s zagniedone w innych klasach. Sposb ich definiowania stanowi do intuicyjne i oczywiste rozszerzenie okrela si po prostu dodatkowy poziom zasigu. Jednak statyczne dane skadowe nie mog wystpowa w klasach lokalnych (takich, ktre zostay zdefiniowane wewntrz funkcji). Ilustruje to poniszy przykad: //: C10:Local .cpp // Skadowe statyczne i klasy lokalne #include <iostream> using namespace std; // Zagniedona klasa MOE posiada statyczne // dane skadowe: class Outer { class Inner { static int i; // W porzdku

int Outer: :Inner: :i - 47; // Klasa lokalna nie moe posiada statycznych // danych skadowych: void f() { public: / / ! static int i: // Bd // (Jak zostaoby zdefiniowane i?)

class Local {

int main() { Outer x; f(); } ///:Nie ulega wtpliwoci, na czym polega problem ze statyczn skadow klasy lokalnej: wjaki sposb odwoa si do skadowej, zawartej w zasigu pliku, byjzdefiniowa? Klasy lokalne s w praktyce uywane bardzo rzadko.

42

Thinking in C++. Edycja polska

>tatyczne funkcje sktadowe


Mona rwnie utworzy statyczne funkcje skadowe, ktre podobnie jak statyczne dane skadowe dziaaj w stosunku do klasy jako caoci, a nie dla poszczeglnych jej obiektw. Zamiast definiowa funkcj globaln, znajdujc si w globalnej lub lokalnej przestrzeni nazw (i ,^asmiecajaca" t przestrze), mona umieci tak funkcj wewntrz klasy. Tworzc statycznskkdow, okrela sijej powizanie z okrelonklas. Statyczna funkcja skadowa moe by wywoana w zwyky sposb za pomoc kropki lub strzaki w celu powizania jej z jakim obiektem. Czciej uywa si jednak wywoania samej funkcji skadowej, bez okrelenia adnego elementu. Wykorzystuje si w tym celu operator zasigu, jak w poniszym przykadzie: / / : C10:SimpleStaticMemberFunction.cpp class X { public: static void f(){}:

int main() X::f();


Widzc wewntrz klasy statyczn funkcj skadow, naley pamita o tym, e projektant dy do tego, aby bya ona, w oglnym sensie, poczona pojciowo z tklas. Statyczne funkcje skadowe nie maj dostpu do zwykych danych skadowych klasy, a wycznie do jej danych statycznych. Mog one rwnie wywoywa tylko statyczne funkcje skadowe. Zwykle adres biecego obiektu (this) jest niejawnie przekazywany podczas wywoywania kadej funkcji skadowej klasy. Jednake adres ten nie jest przekazywany statycznym funkcjom skadowym i dlatego nie maj one dostpu do zwykych skadowych klasy. W ten sposb uzyskuje si niewielkie zwikszenie szybkoci do poziomu funkcji globalnej poniewa funkcja, bdca skadow statyczn, nie jest obarczona dodatkowym narzutem zwizanym z przekazywaniem parametru this. Rwnoczenie osiga si korzy, polegajc na zamkniciu funkcji w klasie. W przypadku danych skadowych sowo kluczowe static oznacza, e dla wszystkich obiektw klasy istnieje tylko jeden obszar pamici, zawierajcy statyczne dane skadowe. Zachodzi tu analogia do uycia sowa kluczowego static w celu zdefiniowania obiektu znajdujcego si wewntrz funkcji. Oznacza on, e dla wszystkich wywoa tej funkcji uywanajest tylkojedna kopia zmiennej lokalnej. Poniszy przykad prezentuje uycie zarwno statycznych danych skadowych, jak i statycznych funkcji skadowych: / / : C10:StaticMemberFunctions.cpp class X {

static int j: public:

int i ;

X(int ii = 0) : i(ii) { // Funkcje skadowe, nie bdce funkcjami

Rozdzia 10. Zarzdzanie nazwami // statycznymi , maja dostp do statycznych // funkcji skadowych oraz statycznych danych:

343

int val() const { return i; } static int incrO { //! i++; // Bd - statyczna funkcja skadowa // nie ma dostpu do danych skadowych, ktre // nie s statyczne return ++j; } static int f() { //! val(): // Bd - statyczna funkcja skadowa // nie ma dostpu do funkcji skadowej, ktra // nie jest funkcj statyczn return incr(); // W porzdku - wywoanie funkcji statycznej

j - i:

int X : : j = 0;

int main() { X x; X* xp = &x; x.f(); xp->f(); X : : f C ) ; // Dziaa tylko ze skadowymi statycznymi } /IIPoniewa statyczne funkcje skadowe nie posiadaj wskanika this, nie maj one dostpu ani do danych skadowych niebdcych danymi statycznymi, ani do funkcji skadowych niebdcych funkcjami statycznymi. Jak wida w funkcji main( ), skadowa statyczna moe by wybrana zarwno przy uyciu zapisu zawierajcego kropk lub strzak, zwizujc t funkcj z obiektem, jak i bez obiektu (poniewa skadowa statyczna jest zwizana z klas, a nie z konkretnym obiektem) za pomoc nazwy klasy i operatora zasigu. Jeszcze jedno interesujce spostrzeenie. Z uwagi na sposb inicjaIizacji, ktry nastpuje w przypadku skadowych statycznych, mona umieci wewntrz k!asy statyczn skadow o tym samym typie co klasa. Poniej zamieszczono przykad, w ktrym zosta utworzony tylko jeden obiekt typu Egg dziki uczynieniu konstruktora prywatnym. Obiekt tenjest dostpny, ale nie mona utworzy adnego nowego obiektu klasy Egg: //: C10:Singleton.cpp // Statyczna skadowa tego samego typu gwarantuje // e istnieje tylko jeden obiekt tego typu. // Jest to rwnie nazywane wzorcem "singla". #include <iostream> using namespace std; class Egg { static Egg e; int i ; Egg(int ii) : i(ii) {}

Thinking in C++. Edycja polska

Egg(const Egg&); // Blokada konstruktora kopiujcego public: static Egg* instance() { return &e; } int val() const { return i; } Egg Egg: :e(47); int main() { //! Egg x(l); // Bd - nie mona utworzy obiektu klasy Egg // Mona odwoywa si tylko do jej pojedynczego egzemplarza: cout << Egg::instance()->valO << endl;
Inicjalizacja skadowej e odbywa si po zakoczeniu deklaracji, dziki czemu kompilator posiada wszelkie informacje, niezbdne do przydzielenia obiektowi pamici i wywoania jego konstruktora. Aby skutecznie zapobiec utworzeniu jakiegokolwiek innego obiektu, zostao tu dodane co jeszcze drugi prywatny konstruktor, nazywany konstruktorem kopiujcym (ang. copy-constructor). Zostanie on wprowadzony dopiero w nastpnym rozdziale. Jednak, tytuem zapowiedzi, warto nadmieni, e w razie usunicia konstruktora kopiujcego, zdefiniowanego w powyszym przykadzie, mona by utworzy obiekty klasy Egg, uywajc nastpujcych sposobw:
Egg e = *Egg::instance(); Egg e2(*Egg::instanceO);

W obu sposobach zosta wykorzystany konstruktor kopiujcy. Aby zatem zablokowa tak moliwo, konstruktor kopiujcy zadeklarowano jako prywatny (nie jest potrzebna adna jego definicja, poniewa nigdy nie zostanie on wywoany). Dyskusja dotyczca konstruktora kopiujcego zajmuje znaczn-cz nastpnego rozdziau, wic po przeczytaniu go wszystko powinno sta si zupeniejasne.

Zalenoci przy inicjalizacji obiektw statycznych


Gwarantuje si, e w obrbie konkretnej jednostki translacji kolejno inicjalizacji obiektw statycznych jest zgodna z kolejnoci wystpowania definicji tych obiektw w jednostce translacji. Jest rwnie pewne, e kolejno niszczenia obiektw jest odwrotna do kolejnoci ich inicjalizacji. Nie istniejjednak adne gwarancje dotyczce kolejnoci inicjalizacji obiektw statycznych, znajdujcych si w rnych jednostkach translacji, a jzyk nie dostarcza adnych rodkw, ktre umoliwiayby okrelenie tej kolejnoci. Moe to stwarza powane problemy. Poniej przedstawiono przykad sytuacji, prowadzcej do natychmiastowej katastrofy (zatrzymanie prostych systemw operacyjnych oraz przerwanie dziaania procesu w przypadku bardziej wyrafinowanych). Jeeli jeden z phkw bdzie mia nastpujczawarto:

Rozdzia 10. Zarzdzanie nazwami // Pierwszy plik #include <fstream> std::ofstream out("out.txt"); a w drugim pliku obiekt out wykorzystywany bdzie wjednym z inicjatorw: // Drugi plik #include <fstream> extern std::ofstream out; class Oof { public: CtofC) { std::out << "ojej"; }
} oOf;

345

to program taki moe dziaa albo nie. Jeeli rodowisko programistyczne zbuduje program w taki sposb, e najpierw zostanie zainicjowany pierwszy plik, to nie wystpi problemy. Jeeli jednak kolejno inicjalizacji bdzie odwrotna, to konstruktor klasy Oof, zakadajcy istnienie obiektu out, ktry nie zosta jeszcze utworzony, spowoduje chaos. Problem ten wystpuje tylko w przypadku inicjalizacji obiektw statycznych, ktre s wzajemnie od siebie uzalenione. Obiekty statyczne umieszczone w jednostce translacji s inicjalizowane przed pierwszym wywoaniem funkcji znajdujcej si w tej jednostce ale moe to nastpi nawet po zakoczeniu funkcji main(). Nie mona mie pewnoci co do kolejnoci inicjalizacji obiektw statycznych, jeeli znajduj si one w rnych plikach. Bardziej wyrafinowany przykad takiej sytuacji mona znale w ksice The Annotated C++ Reference Manual"2. W jednym z plikw, w zasigu globalnym, znajduj si nastpujce wiersze: extern int y;
int x = y + 1;

a w drugim, rwnie w zasigu globalnym takie: extern int x;


int y = x + 1;

W przypadku wszystkich obiektw statycznych mechanizm odpowiedzialny za czenie i adowanie programu zapewnia, e zanim zostanie wykonana dynamiczna inicjalizacja, okrelona przez programist, przeprowadzana jest inicjalizacja statyczna, polegajca na wyzerowania danych. W przedstawionym poprzednio przykadzie wyzerowanie pamici zajmowanej przez obiekt out typu fstream nie ma adnego szczeglnego znaczenia, wic w rzeczywistoci obiekt ten pozostaje niezdefiniowany do momentu wywoania konstruktora. Jednake w przypadku typw wbudowanych inicjalizacja polegajca na wyzerowaniu danych ma sens. Jeeli pIiki s inicjalizowane w takiej kolejnoci, wjakiej przedstawionoje powyej, to zmienna yjest najpierw statycznie inicjalizowana wartoci zerow. W zwizku z tym zmienna x przyjmuje warto jeden, a nastpnie zmienna y jest dynamicznie inicjalizowana wartoci dwa. Jeeli jednak pliki s inicjalizowane w odwrotnej kolejnoci, to najpierw zmienna x jest statycznie inicjalizowana wartoci zerow, nastpnie zmienna y jest dynamicznie inicjalizowana wartocijeden, a na koniec x uzyskuje warto rwndwa.
sSbDustrup, Margaret ElUs, TheAnnotaIed C++ Reference Manual (Addison-Wesley, 1990, s. 20-21).

.6

Thinking in C++. Edycja polska Programista musi zdawa sobie spraw z takich sytuacji, poniewa moliwe jest napisanie programu zawierajcego zalenoci zwizane z inicjalizacj zmiennych statycznych, ktry bdzie dziaa na jednej platformie, a przeniesiony do innego rodowiska kompilacji niespodziewanie przestanie dziaa.

ak mona temu zaradzi?


Istniej trzy sposoby, ktre umoliwi ci poradzenie sobie z tym problemem: 1. Nie rb tego. Unikanie zalenoci, zwizanych ze statyczninicjalizacj obiektw, stanowi najlepsze rozwizanie. 2. Jeeli to konieczne, umie krytyczne definicje obiektw statycznych w pojedynczym pliku, co pozwoli na przenony sposb kontroli nad ich inicjalizacj, polegajcy na uporzdkowaniu ich we waciwej kolejnoci. 3. Jeeli jeste przewiadczony, e rozproszenie obiektw statycznych pomidzy jednostki translacji jest nieuniknione jak w przypadku biblioteki, gdzie nie ma moliwoci nadzorowania uywajcego jej programisty to masz do dyspozycji dwie techniki programistyczne rozwizujce ten problem.

Pierwsza technika
Technika ta zostaa opracowana przez Jerry'ego Schwartza podczas tworzenia przez niego biblioteki iostream (poniewa obiekty cin, cout oraz cerr s zdefiniowane jako statyczne i znajduj si w odrbnych plikach). Jest to w rzeczywistoci technika gorsza ni druga z zaprezentowanych, ale istnieje ona ju dugo, dziki czemu mona napotka wykorzystujcyjkod. Warto zatem zrozumie, wjaki sposb dziaa. Technika ta wymaga utworzenia dodatkowej klasy w pliku nagwkowym biblioteki. Klasa ta jest odpowiedzialna za dynamiczn inicjalizacj obiektw statycznych wchodzcych w skad biblioteki. Poniej znajduje si prosty przykad:
/ / : C10:Initializer.h // Technika inicjalizacji obiektw statycznych #ifndef INITIALIZER_H #define INITIALIZER_H

#include <iostream> extern int x; // Deklaracje, a nie definicje extern int y;

class Initializer { static int initCount; public: Initializer() { std::cout << "Initializer()" << std::endl; // Inicjalizacj tylko za pierwszym razem if(initCount++ 0) { std::cout << "inicjalizacj" << std::endl; x = 100: y = 200:

Rozdzia 10. << Zarzdzanie nazwami

347

-Initializer() { std::cout << "~Initializer()" << std::endl; // Sprztanie tylko za ostatnim razem if(--initCount == 0) { std::cout << "sprztanie" << std::endl; // Wszelkie niezbdne czynnoci zwizane ze sprztaniem

// Ponisza definicja tworzy obiekt, // w kadym pliku, do ktrego jest doczony // plik Initializer.h. ale obiekt ten jest widoczny // tylko w obrbie biecego pliku: static Initializer init; |endif // INITIALIZER_H lll:~
Deklaracje zmiennych x oraz y zapowiadjjedynie, e obiekty te istniej, ale nie powoduj przydzielenia im pamici. Jednak definicja obiektu inlt typu Initializer przydziela temu obiektowi pami w kadym pliku, do ktrego zosta doczony plik nagwkowy. Jednake z uwagi na to, e nazwa ta jest statyczna (sowo to decyduje tym razem o widocznoci, a nie o sposobie przydzielenia pamici jest ona przydzielana domylnie w obrbie pliku), jest ona widoczna tylko w obrbie biecej jednostki translacji, dziki czemu program czcy nie zgasza bdu wielokrotnej definicji. Poniej przedstawiono zawarto pliku zawierajcego definicje zmiennych x, y oraz initCount:

//: C10:InitializerDefs.cpp {0}

#include "Initializer.h" // Statyczna inicjalizacja spowoduje // wyzerowanie wartoci poniszych zmiennych: int x; int y; int Initializer::initCount: lll:~
(oczywicie, doczenie pliku nagwkowego spowoduje rwnie utworzenie w tym pliku, statycznego wzgldem niego, egzemplarza obiektu init). Zamy, e dwa pozostae pliki zostay utworzone przez uytkownika biblioteki: / / : C10:Initializer.cpp {0} // Inicjalizacja obiektw statycznych #include "Initializer.h"

// Definicje dla pliku Initializer.h

//: C10:Initializer2.cpp //{L} InitializerDefs Initializer // Inicjalizacja obiektw statycznych #include "Initializer.h" using namespace std;

Thinking in C++. Edycja polska

int main() { cout << "wewntrz funkcji main()" << endl; cout<< "opuszczanie funkcji main()" << endl; Nie ma znaczenia, ktra jednostka translacji zostanie zainicjowana jako pierwsza. Gdy po raz pierwszy zostanie zainicjowana jednostka translacji zawierajca plik Initializer.h, warto skadowej initCount bdzie wynosia zero, wic zostanie wykonana inicjalizacja (zaley to w istotny sposb od faktu, e obszar danych statycznych jest zerowany przed wykonaniem jakiejkolwiek dynamicznej inicjalizacji). We wszystkich pozostaych jednostkach translacji warto skadowej initCount bdzie niezerowa, w zwizku z czym inicjalizacja zostanie pominita. Sprztanie odbywa si w odwrotnej kolejnoci, a destruktor ~Initializer( ) gwarantuje, e zostanie ono wykonane tylko jeden raz. W powyszym przykadzie wykorzystano globalne statyczne obiekty wbudowanych typw. Technika ta dziaa rwnie w stosunku do klas, lecz w takim przypadku ich obiekty musz zosta zainicjowane dynamicznie przez klas Initializer. Jednym ze sposobw, umoliwiajcych wykonanie tego zadania, jest utworzenie klas pozbawionych konstruktorw i destruktorw, posiadajcych zamiast nich funkcje inicjalizujce i sprztajce o innych nazwach. Czciej spotykanym rozwizaniem jest jednak zdefiniowanie wskanikw do obiektw i utworzenie tych obiektw wewntrz konstruktora Initializer( ) za pomocoperatora new.

>ruga technika
Kiedy pierwsza z wymienionych technik znajdowaa si ju od duszego czasu w uyciu, kto (nie wiem jednak, kto to uczyni) przedstawi technik opisan w tym podrozdziale, znacznie prostszi bardziej przejrzystod poprzedniej. Fakt, ejej odkrycie zajo tyle czasu, stanowi rodzaj hodu oddanego zoonoci jzyka C++. W technice tej wykorzystano fakt, e obiekty statyczne, znajdujce si wewntrz funkcji, s inicjalizowane pierwszy (i jedyny) raz podczas wywoania tej funkcji. Naley pamita, e problem, ktry staramy si rozwiza, nie polega na tym, kiedy inicjalizowane s obiekty statyczne (mona tym sterowa oddzielnie), tylko na zapewnieniu waciwej kolejnoci inicjalizacji. Druga technika jest bardzo przemylna i elegancka. W przypadku kadej zalenoci dotyczcej inicjalizacji, obiekt statyczny jest umieszczany wewntrz funkcji, zwracajcej referencj do tego obiektu. Dziki temu jedynym sposobem uzyskania dostpu do statycznego obiektu jest wywoanie funkcji, a w przypadku gdy obiekt ten musi odwoa si do innego obiektu, od ktrego zaley, musi wywoa funkcj tego obiektu. Kiedy funkcja jest wywoywana po raz pierwszy, nastpuje inicjalizacja obiektu. Kolejno inicjalizacji obiektw statycznych jest na pewno waciwa, poniewa zaley ona od projektu kodu, a nie od kolejnoci, wyznaczonej w dowolny sposbprzez program czcy. Jako przykad posu dwie klasy, wzajemnie od siebie zalene. Pierwsza z nich zawiera skadow typu bool, inicjalizowan wycznie przez konstruktor, dziki czemu, w przypadku statycznego obiektu tej klasy, mona okreli, czy konstruktor by ju

Rozdzia 10. Zarzdzanie nazwami

349

wywoywany (obszar danych statycznych jest inicjalizowany wartoci zerow w trakcie uruchamiania programu;jesli zatem konstruktor nie byjeszcze wywoywany, skadowa typu bool zawiera warto false): / / : C10:Oependencyl.h #ifndef DEPENDENCYl_H #define DEPENDENCYl_H #include <iostream> class Dependencyl { bool init; public: Dependencyl() : init(true) { std::cout << "konstrukcja Dependencyl" << std::endl: } void print() const { std::cout << "Dependencyl init: " << init << std::endl;
#endif // DEPENDENCYl_H / / / : -

Konstruktor informuje, e zosta wywoany, a ponadto stanu obiektu mona wydrukowa za pomoc funkcji print(), co umoliwia sprawdzenie, czy obiekt zosta zainicjowany. Druga z klas jest inicjalizowana za pomoc obiektu pierwszej klasy, co powoduje powstanie zalenoci: / / : C10:Dependency2.h #ifndef DEPENDENCY2_H #define DEPENDENCY2_H #include "Dependencyl.h" class Dependency2 { Dependencyl dl; public: Dependency2(const Dependencyl& depl): dl(depl){ std::cout << "konstrukcja Dependency2 "; print(); } void print() const { dl.print(): } }:

#endif // DEPENDENCY2_H lll:~

Konstruktor tej klasy informuje, e zosta wywoany, oraz drukuje stan obiektu dl. Dziki temu mona sprawdzi, czy obiekt ten zosta ju zainicjowany, zanim zosta wywoany konstruktor. Aby zademonstrowa, na czym mog polega problemy, statyczne definicje obiektw zostay najpierw umieszczone w poniszym pliku w nieprawidowej kolejnoci, co odpowiada sytuacji, w ktrej program czcy zainicjowaby obiekt Dependency2 przed inicjaHzacjobiektu Dependencyl. Nastpnie kolejno tajest odwrotna, dziki czemu mona zobaczy, jak to dziaa w przypadku prawidowej kolejnoci inicjaIizacji. Na koniec zostaa zaprezentowana druga technika.

Thinking in C++. Edycja polska


Dla zwikszenia czytelnoci wywietlanych wynikw zostaa utworzona funkcja separator( ). Sztuczka polega na tym, e nie mona globalnie wywoa funkcji, chyba e jest ona uywana do inicjalizacji zmiennej. Funkcja separator( ) zwraca wic fikcyjnwarto, uywando inicjalizacji pary zmiennych globalnych.

//: C10:Techn1que2.cpp #include "Dependency2.h" using namespace std; // Funkcja zwraca warto, dziki czemu // moe zosta wywoana jako globalny inicjator: int separator() { cout << "- .................... " << endl; return 1; // Symulacja problemu zalenoci: extern Dependencyl depl; Dependency2 dep2(depl); Dependencyl depl: int xl = separator(); // Ale w tej kolejnoci wszystko jest w porzdku: Dependencyl deplb; Dependency2 dep2b(deplb): int x2 = separator(); // Umieszczenie statycznych obiektw w funkcjach // zapewnia prawidowe dziaanie Dependencyl& dl() { static Dependencyl depl;
return depl;

Dependency2& d2O { static Dependency2 dep2(dlO); return dep2; int main() { Dependency2& dep2 - d2(); } II/:Funkcje dl() i d2() zawieraj statyczne egzemplarze obiektw klas Dependencyl i Dependencyl. Obecnie jedynym sposobem odwoania si do tych obiektw jest wywoanie funkcji, ktre podczas pierwszego wywoania inicjalizuj zawarte w sobie obiekty statyczne. Dziki temu gwarantowana jest poprawno inicjalizacji, o czym mona si przekona po uruchomieniu programu i obejrzeniu generowanych przez niego wynikw. A otojak powinno si przygotowa kod w celu zastosowania tej techniki. Zwykle obiekty statyczne zostayby zdefiniowane w oddzielnych plikach (poniewa jestemy z jakich powodw do tego zmuszeni naley pamita, e to wanie definiowanie statycznych obiektw w rnych plikach powoduje problemy), wic zamiast nich zdefiniujemy w oddzielnych plikach funkcje zawierajce te obiekty. Musz one by jednak zadeklarowane w plikach nagwkowych:

Rozdzia 10. Zarzdzanie nazwami


/ / : C10:DependencylStatFun.h #ifndef DEPENOENCYlSTATFUN_H
#define DEPENDENCYlSTATFUN_H

351

#include "Dependencyl.h" extern Dependencyl& dl(); #endif // DEPENDENCYlSTATFUN_H lll:~

W rzeczywistoci specyfikator extern jest niepotrzebny w przypadku deklaracji funkcji. A oto drugi plik nagwkowy:
/ / : C10:Dependency2StatFun.h #ifndef DEPENDENCY2STATFUN_H #define DEPENDENCY2STATFUN_H #include "Dependency2.h" extern Dependency2S d2(); #endif // DEPENDENCY2STATFUN_H / / / : -

Nastpnie w plikach implementacyjnych, w ktrych poprzednio zostayby umieszczone definicje obiektw statycznych, umieszczamy definicje funkcji, zawierajcych te obiekty:
/ / : C10:DependencylStatFun.cpp {0} #include "DependencylStatFun.h" Dependencyl& dl() { static Dependencyl depl; return depl;

} ///:-

Przypuszczalnie w tym pliku mgby znajdowa si jeszcze jaki inny kod. Tak natomiast przedstawia si drugi z plikw:
/ / : C10:Dependency2StatFun.cpp {0} #include "DependencylStatFun.h" #include "Dependency2StatFun.h" Dependency2& d2() { static Dependency2 dep2(dlO);

return dep2; } ///:-

Tak wic mamy obecnie dwa pliki, ktre mogyby zosta poczone w dowolnej kolejnoci. W przypadku gdyby zawieray one zwyke obiekty statyczne, spowodowaoby to dowoln kolejno inicjalizacji znajdujcych si w nich obiektw. Poniewa jednak znajduj si w nich funkcje zawierajce te obiekty, nie ma ju niebezpieczestwa zwizanego z ich niewaciw inicjalizacj: //: C10:Technique2b.cpp //{L} DependencylStatFun Dependency2StatFun #inc1ude "Dependency2StatFun.h" int main() { d2(): } ///:Po uruchomieniu tego programu wida, e inicjalizacj statycznego obiektu klasy Dependencyl odbywa si zawsze przed inicjalizacj statycznego obiektu klasy Dependencyl. Jest to znacznie prostsze podejcie ni prezentowane w przypadku pierwszej techniki.

Thinking in C++. Edycja polska By moe zechcesz umieci funkcje dl() i d2() jako funkcje inline wewntrz swoich plikw nagwkowych. Nie naley jednak tego robi. Funkcja inline moe by powielona w kadym pliku, w ktrym wystpuje, a powielenie to obejmuje rwnie definicj znajdujcego si w niej statycznego obiektu. Poniewa funkcje inline podlegajautomatycznie wewntrznemu czeniu, spowodowaoby to utworzenie wielu obiektw statycznych, znajdujcych si w rnychjednostkach translacji, co z pewnocibyloby przyczynkopotw. Poniewa trzeba zapewni, by istniaa tylko definicja kadej funkcji zawierajcej obiekt statyczny, oznacza to, e nie mona uywa w tym celu funkcji inline.

>pecyfikacja zmiany sposobu czenia


Co si stanie, gdy w programie pisanym w jzyku C++ zechcemy uy biblioteki jzyka C? Jeeli zadeklarujemy funkcj jzyka C: float f(1nt a. char b); to kompilator uzupeni jej nazw, tworzc co w rodzaju _f_int_char i umoliwiajc przecianie nazwy tej funkcji (oraz czenie bezpieczne da typw). Jednak kompilator jzyka C, kompilujcy t funkcj, z capewnocirae dokona uzupenieniajej nazwy, wic jej wewntrzna nazwa ma posta _f. Tak wic program czcy nie bdzie w stanie okreli odwoa do funkcji f(), wystpujcych w programie napisanym w jzyku C++. Mechanizmem umoliwiajcym rozwizanie tego problemu jest w jzyku C++ specyfikacja zmiany sposobu lczenia (ang. alternate linkage specification), ktra zostaa wprowadzona do jzyka za pomocprzecienia sowa kluczowego extern. Po sowie kluczowym extern umieszczono acuch, ktry okrela sposb czenia, dotyczcy znajdujcej si po nim deklaracji: extern "C" float f(int a. char b); Powysza deklaracja informuje kompilator, by w stosunku do funkcji f() uywa czenia waciwego dlajzyka C, dziki czemu nie bdzie on stosowa uzupenianiajej nazwy. Jedynymi okrelonymi w standardzie specyfikacjami czenia s "C" oraz "C++", ale producenci kompilatorw umoliwiaj obsug innych jzykw rwnie w taki sam sposb. Jeeli zmiana sposobu czenia dotyczy grupy deklaracji, mona umieci je w nawiasie klamrowym, jak pokazano poniej: extern "C" { float f(int a. char b); double d(int a. char b); } W przypadku caych plikw nagwkowych mona rwnie stosowa zapis: extern "C" { Include "Myheader.h" } Wikszo producentw kompilatorw jzyka C++ uywa specyfikacji zmiany sposobu fa' czenia w swoich plikach nagwkowych, ktre dziaaj zarwno w jzyku C, jak i w C++-

Rozdzia 10. Zarzdzanie nazwami

353

Podsumowanie
Sowo kluczowe static moe by nieco mylce, poniewa w pewnych sytuacjach decyduje ono o lokalizacji przydzielonej pamici, a w innych okrela widoczno i sposb czenia nazw. Od momentu wprowadzenia przestrzeni nazw jzyka C++ dostpne jest lepsze i znacznie bardziej elastyczne narzdzie, umoliwiajce nadzr nad tym, w jaki sposb nazwy s rozpowszechniane w duych projektach. Uycie sowa kluczowego static wewntrz klasy jest kolejnym sposobem umoliwiajcym zarzdzanie nazwami w ramach programu. Nazwy takie nie koliduj z nazwami globalnymi; rwnoczenie sone widoczne i dostpne w obrbie programu, co zapewnia wikszkontrol nad pielgnacjcaego kodu.

wiczenia
Rozwizania wybranych wicze znajduj si w dokumencie elektronicznym: The Thinking in C++ Annotated Solution Guide, ktry mona pobra za niewielk opat z witryny http://www.BruceEckel.com. 1. Utwrz funkcj zawierajc statyczn zmienn bdc wskanikiem i pobierajc argument o zerowej domylnej wartoci. Jeeli w wywoaniu funkcji zostanie podana warto argumentu, to bdzie on wskazywa pocztek tablicy liczb cakowitych. Jeeli funkcja zostanie wywoana bez argumentu (uyty zostanie argument domylny), to powinna ona zwrci za kadym razem nastpn warto, znajdujcsi w tablicy, a do napotkania wartoci -1 (oznaczajcej koniec tablicy). Sprawd dziaanie tej funkcji w funkcji main(). 2. Utwrz funkcj zwracajca przy kadymjej wywoaniu nastpn warto cigu Fibonacciego. Dodaj funkcji argument typu bool, o domylnej wartoci false, ktry w przypadku podania wartoci true spowoduje wyzerowanie" funkcji do pocztku cigu Fibonacciego. Sprawd dziaanie tej funkcji w funkcji main(). 3. Utwrz klas zawierajc tablic liczb cakowitych. Ustal wielko tej tablicy, uywajc do tego celu statycznej staej cakowitej, znajdujcej si wewntrz klasy. Dodaj sta cakowit i zainicjalizuj j, wykorzystujc listy inicjatorw konstruktora. Okrel konstruktorjako funkcj inline. Dodaj statyczn zmienncakowiti zainicjujjza pomocokrelonej wartoci. Utwrz statycznfunkcj skadow, drukujcstatyczne dane skadowe. Utwrz skadow funkcj inline, ktra wywouje funkcj print(), drukujc wszystkie wartoci zawarte w tablicy oraz wywoujc utworzonpoprzednio statyczn funkcj skadow. Sprawd dziaanie klasy w funkcji main(). 4. Utwrz klas o nazwie Monitor, zapamitujc, ile razy wywoana bya jej funkcja skadowa incident(). Dodaj funkcj skadowprint(), drukujc

54

Thinking in C++. Edycja polska liczb wywoa funkcji incident(). Nastpnie utwrz funkcj globaln (niebdc funkcj skadow), zawierajc statyczny obiekt klasy Monitor. Podczas kadego wywoania funkcja ta powinna wywoywa funkcj incident(), a nastpnie funkcj print(), wywietlajcliczb wywoa. Sprawd dziaanie tej funkcji w funkcji main(). 5. Za pomoc funkcji skadowej decrement() zmodyfikuj opisan w poprzednim wiczeniu klas Monitor w taki sposb, aby moga dekrementowa warto licznika. Utwrz klas Monitor2 tak, abyjej konstruktor przyjmowa jako argument wskanik do obiektu klasy Monitorl, a nastpnie zapamitywa ten wskanik i wywoywa funkcje incident() oraz print(). W destruktorze klasy Monitor2 wywoaj natomiast funkcje decrement() i print(). Teraz utwrz, wewntrz funkcji, statyczny obiekt klasy Monitor2. Wykonaj eksperymenty w funkcji main() wywoaj t funkcj, po czymjej nie wywouj, obserwujc, jaki ma to wpyw na destruktor klasy Monitor2. 6. Utwrz globalny obiekt klasy Monitor2 i zobacz, co si stanie. 7. Utwrz klas, ktra posiada destruktor drukujcy komunikat, a nastpnie wywoujcy funkcj exit(). Utwrz globalny obiekt tej klasy i zobacz, co si stanie. 8. Przeprowad eksperymenty z kolejnoci wywoa konstruktora i destruktora w programie StaticDestructors.cpp wywoujc funkcje f() i g(), zawarte w funkcji main(), w odwrotnej kolejnoci. Czy uywany przez ciebie kompilator obsuguje to poprawnie? 9. W programie StaticDestructors.cpp przetestuj domylnobsug bdw swojej implementacji, zamieniajc oryginalndefinicj obiektu out w deklaracj extern i umieszczajc rzeczywistdefinicj po definicji obiektu a (ktrego konstruktor Obj() przesya informacje do obiektu obj). Upewnij si, e w czasie uruchamiania tego programu komputer nie robi niczego wanego albo e jest on odporny na tego rodzaju bdy. 10. Udowodnij, e zmienne statyczne wzgldem pliku, znajdujce si w plikach nagwkowych, nie kolidujze sob, gdy zostandoczone do wicej nijednego pliku cpp. 11. Utwrz prost klas, zawierajc skadow cakowit, konstruktor, ktry inicjalizuje t skadowana podstawie swojego argumentu, funkcj skadow, przypisujcjej warto swojego argumentu, oraz funkcj print(), drukujc warto tej skadowej. Umie swojklas w pliku nagwkowym i docz go do dwch plikw cpp. W jednym z tych plikw utwrz egzemplarz klasy, a w drugim zadeklaruj jego identyfikator za pomoc specyfikatora extern i przetestuj jego dziaanie w funkcji main(). 12. Uczy egzemplarz obiektu, utworzonego w poprzednim wiczeniu, obiektem statycznym i sprawd, czy uniemoliwia to znalezienie tego obiektu przez program czcy. 3. Zadeklaruj funkcj w pliku nagwkowym. Zdefiniuj t funkcj w jednym pliku cpp i wywoaj j wewntrz funkcji main(), w drugim pliku cpp. Skompiluj program i upewnij si, e dziaa. Nastpnie zmie definicj funkcji na statyczn i przekonaj si, e program czcy nie moe jej odnale.

Rozdzia 10. Zarzdzanie nazwami

355

14. Zmodyfikuj program Volatile.cpp, opisany w rozdziale 8., w taki sposb, aby mg rzeczywicie dziaajako procedura obsugi przerwa. Wskazwka: procedura obsugi przerwa nie pobiera adnych argumentw. 15. Napisz i skompiluj prosty program, wykorzystujcy sowa kluczowe auto i register. 16. Utwrz plik nagwkowy, zawierajcy przestrze nazw. Wewntrz tej przestrzeni umie kilka deklaracji funkcji. Nastpnie utwrz drugi plik nagwkowy, doczajcy poprzedni plik, ktry kontynuuje t przestrze nazw i dodaje do niej jeszcze kilka deklaracji funkcji. Potem utwrz plik cpp, w ktrym doczony jest drugi plik nagwkowy. Utwrz (krtszy) synonim swojej przestrzeni nazw. W definicji jednej z funkcji wywoaj jedn ze swoich funkcji, uywajc specyfikacji zasigu. W definicji innej funkcji wpisz dyrektyw using, wprowadzajc swojprzestrze nazw do zasigu funkcji, a nastpnie poka, e do wywoania funkcji znajdujcych si w tej przestrzeni nie jest potrzebna specyfikacja zasigu. 17. Utwrz plik nagwkowy zawierajcy bezimiennprzestrze nazw. Docz ten plik nagwkowy w dwch oddzielnych plikach cpp i poka, e ta przestrze nazw jest unikatowa w kadej jednostce translacji. W. Wykorzystujc plik nagwkowy z poprzedniego wiczenia, poka, e nazwy znajdujce si w bezimiennej przestrzeni nazw s automatycznie dostpne w obrbie jednostki translacji, bez koniecznoci uywania kwalifikatorw. 19. Zmodyfikuj program FriendInjection.cpp, dodajc definicj zaprzyjanionej funkcji i wywoujc j wewntrz funkcji main(). 20. Uywajc pliku Arithmetic.cpp zademonstruj, e dyrektywa using nie wykracza poza funkcj, w ktrej zostaa uyta. 21. Rozwi problem wystpujcy w pliku OverridingAmbiguity.cpp, uywajc najpierw specyfikacji zasigu, a nastpnie za pomocdeklaracji using, wymuszajcej na kompilatorze wybrjednej z funkcji posiadajcych identyczne nazwy. 22. W dwch plikach nagwkowych utwrz dwie przestrzenie nazw, z ktrych kada bdzie zawieraa klas (z wszystkimi definicjami inline) o takiej samej nazwie, jak znajdujca si w drugiej przestrzeni nazw. Utwrz plik cpp, w ktrym doczone s oba pliki nagwkowe. Utwrz funkcj i uyj wewntrz niej dyrektyw using, wprowadzajc obie przestrzenie nazw. Sprbuj utworzy obiektjednej z klas i zobacz, co si stanie. Uczy dyrektywy using globalnymi (umieszczajcje na zewntrz funkcji) i zobacz, czy co to zmieni. Rozwi problem, uywajc specyfikacji zasigu, a nastpnie utwrz obiekty obu klas. 23. Rozwi problem przedstawiony w poprzednim zadaniu, uywajc deklaracji using, wymuszajcej na kompilatorze wybrjednej z klas posiadajcych identyczne nazwy. 24. Pobierz deklaracje przestrzeni nazw z plikw BobsSuperDuperLibrary.cpp oraz UnnamedNamespaces.cpp i umie je w oddzielnych plikach

Thinking in C++. Edycja polska

nagwkowych, nadajc w trakcie tego procesu nazw bezimiennej przestrzeni nazw. W trzecim pliku nagwkowym utwrz za pomoc deklaracji using now przestrze nazw, czc w sobie elementy zawarte w dwch pozostaych przestrzeniach nazw. Wprowad swojnow przestrze nazw do funkcji main( ), uywajc do tego dyrektywy using, i odwoaj si do wszystkich elementw zawartych w tej przestrzeni nazw. 25. Utwrz plik nagwkowy, w ktrym doczane s pliki nagwkowe <string> i <iostream>, ale nie s stosowane adne deklaracje ani definicje using. Uyj stranikw doczania" w taki sposb, jak w plikach nagwkowych zawartych w ksice. Utwrz klas, posiadajctylko funkcje inline, zawierajc skadow typu string, konstruktor, inicjalizujcy j swoim argumentem, i funkcj print( ), wywietlajcjej zawarto. Utwrz plik cpp i sprawd dziaanie tej klasy w funkcji main( ). 26. Utwrz klas zawierajcskadowe statyczne typw double i long. Utwrz statyczn funkcj skadow, drukujc ich wartoci. 27. Utwrz klas zawierajcskadowcakowit, konstruktor, inicjalizujcyj na podstawie swojego argumentu, oraz funkcj print( ), wywietlajcjej warto. Nastpnie utwrz drugklas, zawierajcstatyczny obiekt pierwszej klasy. Dodaj statycznfunkcj skadow, wywoujcfunkcj print( ) statycznego obiektu. Sprawd dziaanie swojej klasy w funkcji main( ). 28. Utwrz klas zawierajcstastatyczntablic liczb cakowitych oraz statyczn tablic liczb cakowitych, niebdc sta. Utwrz statyczn funkcj skadow, drukujczawarto tych tablic. Sprawddziaanie swojej klasy w funkcji main( ). 29. Utwrz klas zawierajcobiekt typu string, konstruktor, inicjalizujcy go wartociswojego argumentu, oraz funkcj print( ),^vyswietlajacajego warto. Utwrz drugklas, zawierajcstatyczne tablice obiektw pierwszej klasy zarwno bdce, jak i niebdce staymi, a take statyczne funkcje skadowe, drukujce zawarto tych tablic. Sprawd dziaanie drugiej z klas w funkcji main( ). 30. Utwrz struktur, zawierajcskadowcakowitaj oraz domylny konstruktor, inicjalizujcy t skadowwartocizerow. Uczy t struktur lokaln w stosunku do funkcji. Wewntrz tej funkcji utwrz tablic obiektw swojej struktury i poka, e kada skadowa zostaa automatycznie zainicjowana wartoci zerow. 31. Utwrz klas reprezentujcpoczenie z drukark, pozwalajcna posiadanie tylkojednej drukarki. 32. W pliku nagwkowym utwrz klas Mirror, zawierajcdwie dane skadowe wskanik do obiektu klasy Mirror oraz skadow typu bool. Utwrz dwa konstruktory. Konstruktor domylny powinien inicjalizowa skadow typu bool wartoci true, a wskanik do obiektu kasy Mirror wartoci zerow. Drugi konstruktor powinien pobierajako argument wskanik do obiektu klasy Mirror, a nastpnie przypisywa go wskanikowi zawartemu w klasie, natomiast skadowtypu bool inicjalizowa wartocifake. Dodaj funkcj

Rozdzia 10. Zarzdzanie nazwami

357

skadow test() jeeli warto wskanika zawartego w obiekciejest niezerowa, to funkcja ta powinna zwrci warto zwracanprzez funkcj test(), wywoan za porednictwem tego wskanika. Jeeli natomiast warto wskanikajest zerowa, to funkcja powinna zwrci warto skadowej typu bool. Nastpnie utwrz pi plikw cpp w taki sposb, by do kadego z nich zosta doczony plik nagwkowy klasy Mirror. W pierwszym pliku cpp, za pomoc domylnego konstruktora, utwrz globalny obiekt klasy Mirror. W drugim pliku, za pomoc specyfikatora extern, zadeklaruj obiekt znajdujcy si w pierwszym pliku, a take, przy uyciu konstruktora pobierajcego wskanik do pierwszego obiektu, zdefiniuj globalny obiekt klasy Mirror. Kontynuuj taki sposb tworzenia kolejnych obiektw a do ostatniego pliku, zawierajcego rwnie globalndefinicj obiektu. W znajdujcej si w tym pliku funkcji main( ) wywoaj funkcj test() i sprawd wywietlony przez t funkcj wynik. Jeeli wynikiem tym jest true, dowiedz si, wjaki sposb mona zmieni kolejno czenia plikw przez program czcy, i zmieniaj j dopty, dopki wynikiem zwracanym przez funkcj bdzie false. 33. Rozwi problem, ktry wystpi w poprzednim wiczeniu, wykorzystujc pierwsz technik przedstawion w ksice. 34. Rozwi problem, ktry wystpi w wiczeniu 32., wykorzystujc drug technik przedstawion w ksice. 35. Nie doczajc pIiku nagwkowego, zadeklaruj funkcj puts(), znjdujcsi w standardowej bibliotecejzyka C, iwywoaj t funkcj w funkcji main().

Thinking in C+. Edycja polska

Referencje i konstruktor kopiujcy


Referencje przypominajstae wskaniki, automatycznie wyuskiwane przez kompilator. Mimo e referencje istniej rwnie w jzyku Pascal, ich wersja uywana w jzyku C++ zostaa zaczerpnita z Algolu. Wjzyku C++ sone niezbdne do obsugi skadni przeciania operatorw (opisanej w rozdziale 12.); stanowi one jednak rwnie oglne uatwienie, pozwalajce na nadzr nad sposobem, wjaki parametry sprzekazywane do i z funkcji. W tym rozdziale dokonamy najpierw pobienego przegldu rnic pomidzy wskanikami w jzykach C i C++, a nastpnie wprowadzimy pojcie referencji. Znaczna cz rozdziau powicona jednak bdzie do trudnej, przynajmniej dla pocztkujcych programistwjzyka C++, kwestii konstruktorowi kopiujcemu, czyli specjalnemu konstruktorowi (wymagajcemu referencji), tworzcemu nowy obiekt na podstawie istniejcegoju obiektu tego samego typu. Konstruktor kopiujcy jest uywany przez kompilator do przekazywania obiektw przez warto do i z funkcji. Na koniec zostanie wyjaniona do tajemnicza cecha jzyka C++ wskanik do skladowej.

Rozdzia

1 1 .

Wskaniki w C++
Najistotniejsza rnica pomidzy wskanikami wjzykach C i C++ polega na tym, e C++ jest jzykiem o silniejszej kontroli typw. Szczeglnie wyranie ujawnia si to w przypadku wskanika typu void*. Jzyk C nie pozwala na swobodne przypisywanie wskanikwjednych typw wskanikom innych typw, ale umoliwia dokonanie tego za pomoc wskanika typu void*. Na przykad w nastpujcy sposb:

BO ptak* p; skala* s; void* v; v = s; p - v;

Thinking in C++. Edycja polska

Ta cecha jzyka C, umoliwiajca niejawne traktowanie dowolnego typu w taki sposb, jakby by innym typem, stanowi spory wyom w systemie typw. Jzyk C++ nie pozwala na takie operacje; kompilator zgasza w takich przypadkach komunikat o bdzie i jeeli naprawd istnieje potrzeba traktowania jakiego typu tak, jakby by innym typem, to trzeba uczyni to jawnie zarwno dla kompilatora, jak i dla osoby czytajcej kod uywajc rzutowania (w rozdziale 3. wprowadzono udoskonalon, ,jawna" skadni rzutowania, dostpnwjzyku C++).

Referencje w C++
Referencje (&) przypominaj stae wskaniki, ktre s automatycznie wyuskiwane. Zazwyczaj s uywane w listach argumentw funkcji i jako wartoci zwracane przez funkcje. Mona jednak rwnie utworzy samodzielne referencje. Ilustruje to poniszy przykad:
/ / : Cll : FreeStandingReferences.cpp #include <iostream> using namespace std; // Zwyke, samodzielne referencje: int y; int& r - y;

// Podczas tworzenia, referencja musi // zosta zainicjowana istniejcym obiektem. // Mona jednak rwnie napisa: const int& q - 12; // (1) // Referencje s zwizane z obszarem pamici // jakiego innego obiektu: int x = 0; // (2) int& a <<= x; // (3) int main() { cout << "x = " << x << ", a = " << a << endl ; a++; cout << "x - " << x << " . a - " << a << endl ;
W wierszu (1) kompilator przydziela obszar pamici, inicjalizuje go wartoci 12, a nastpnie zwizuje z tym obszarem referencj. Sedno problemu tkwi w tym, e referencja musi by zwizana z obszarem pamici, nalecym do jakiego innego obiektu. Podczas odwoywania si do referencji odnosimy si wanie do tego obszaru. Tak wic po wpisaniu takich wierszy, jak (2) i (3), a nastpnie inkrementacji referencji a w rzeczywistoci jest inkrementowana warto zmiennej x, jak pokazano w funkcji main( ). Najprostszym sposobem ujcia referencji jest traktowanie jej jako szczeglnego wskanika. Jedn z zalet tego wskanika" jest to, e nigdy nie trzeba zastanawia si nad tym, czy zosta on zainicjowany (zapewnia to kompilator) ani w jaki sposb go wyuska (to rwnie wykonuje kompilator).

Rozdziat 1 1 . << Referencje i konstruktor kopiujcy Z uywaniem referencji zwizane spewne reguly: 1. Referencja musi zosta zainicjowana podczas tworzenia (wskaniki mog zosta zainicjowane w dowolnej chwili). 2. Po zainicjalizowaniu referencji za pomocjakiego obiektu nie monajej zmieni w taki sposb, by wskazywaa inny obiekt (wskanikom zawsze mona przypisa adres innego obiektu). 3. Nie istniejpuste referencje. Zawsze mona zaoy, e referencjajest powizana z prawidowym obszarem pamici.

361

Wykorzystanie referencji w funkcjach


Miejscem, w ktrym najczciej mona spotka referencje, s argumenty funkcji oraz wartoci przez nie zwracane. W przypadku gdy referencja jest wykorzystywana w charakterze argumentu funkcji, wszelkie jej modyfikacje, zachodzce wewntrz funkcji, maj wpyw na warto argumentu znajdujcego si na zewntrz funkcji. Oczywicie, mona osign to samo, przekazujc wskanik, ale referencje maj znacznie bardziej przejrzyst skadni (moesz rwnie traktowa referencje wyczniejako uatwienie skadniowe). W przypadku zwracania przez funkcj referencji naley zachowa tak sam ostrono, jak w razie zwracania przez funkcj wskanika. To, z czym zwizana jest referencja, nie powinno przesta istnie po powrocie z funkcji, poniewa w przeciwnym wypadku zakoczyoby si to odwoaniem do nieokrelonego obszaru pamici. Oto przykad: //: Cll:Reference.cpp // Proste referencje jzyka C++ int* f(int* x) { (*x)++; return x; // Bezpieczne, zmienna x znajduje si poza zasigiem

}
int& g(int& x) { x++; // Taki sam rezultat, jak w funkcji f() return x; // Bezpieczne, zmienna x znajduje si poza zasigiem
}

int& h() { //! return q; // Bd static int x; return x; // Bezpieczne, zmienna x znajduje si poza zasigiem

int q;

,.

)
int main() { f(&a): // Brzydko (cho jawnie) g(a); // Przejrzycie (lecz w ukryty sposb)

int a = 0; -

Thinking in C++. Edycja polska

Wywoanie funkcji f() nie jest tak wygodne i przejrzyste, jak w przypadku uycia referencji, ale nie ulega wtpliwoci, e jest przekazywany adres. Podczas wywoania funkcji g() rwnie przekazywany jest adres (przez referencj), ale nie jest to widoczne.

eferencje do stafych
Argument funkcji, bdcy referencj, widoczny w pliku Reference.cpp, jest poprawny tylko w przypadku, gdy jest on obiektem niebdcym sta. Jeeli za jest on sta, to funkcja g() nie przyjmie go w charakterze argumentu, co jest w istocie suszne, poniewa funkcja ta modyfikuje swj argument. Jeeli wiadomo, e funkcja respektuje fakt, e jej argument jest sta, to uczynienie argumentu referencj do staej pozwoli na uycie tej funkcji we wszystkich sytuacjach. Oznacza to, e w przypadku wbudowanych typw funkcja nie zmodyfikuje swojego argumentu, a co do typw zdefiniowanych przez uytkownika bdzie ona wywoywa wycznie stae funkcje skadowe: niemodyfikujce publicznych danych skadowych klasy. Uycie referencji do staych w charakterze argumentw funkcji jest szczeglnie wane z uwagi na to, e funkcja moe pobra obiekt tymczasowy. Mgby on zosta utworzony jako warto, zwracana przez inn funkcj lub jawnie przez uytkownika funkcji. Obiekty tymczasowe s zawsze staymi, wic jeeli jako argument nie zostanie okrelona referencja do staej, to argument taki nie zostanie zaakceptowany przez kompilator. Poniej przedstawiono ilustrujcy to zjawisko bardzo prosty przykad: / / : CU:ConstReferenceArguments.cpp // Przekazywanie referencji jako staych void f(int&) {} void g(const int&) {} int main() { / / ! f(l); // Bd g(l): } lllWywoanie f(l) powoduje zgoszenie bdu w czasie kompilacji, poniewa kompilator musi najpierw utworzy referencj. Dokonuje tego, przydzielajc pami dla liczby cakowitej; inicjalizuje j wartocijeden i zwraca adres, z ktrym zwizana bdzie referencja. Przydzielony obszar pamici musi by sta, poniewa zmiana jego wartoci nie miaaby sensu w aden sposb nie mona by si bowiem do niego ponownie odwoa. W przypadku wszystkich obiektw tymczasowych naley przyj to samo zaoenie s one niedostpne. Informowanie przez kompilator o modyfikacji takich danych jest korzystne, poniewa rezultat takiej modyfikacji zostaby utracony.

Referencje do wskanikw
W przypadku zamiaru modyfikacji wartoci wskanika (a nie wartoci, ktr on wskazuje), podanego jako parametr, naleaoby uy w jzyku C nastpujcej deklaracji funkcji: void f(int**); a funkcji naleaoby przekaza parametr bdcy adresem wskanika:

Rozdzia 1 1 . Referencje i konstruktor kopiujcy


int i = 47; int* ip - &i; f(&ip);

363

W przypadku referencji, dostpnych w jzyku C++, skadnia jest prostsza. Argument funkcji staje si referencj do wskanika, w zwizku z czym nie maju potrzeby pobieraniajego adresu. Przybiera to nastpujcposta:
/ / : Cll:ReferenceToPointer.cpp #include <iostream> using namespace std; void increment(int*& 1) { i++; } int main() { int* i = 0; cout << "i - " << i << endl; increment(i); cout << "i = " << i << endl: } ///:-

Po uruchomieniu powyszego programu mona si przekona, e inkrementowany jest wskanik, a nie warto, ktr wskazuje.

Wskazwki dotyczce przekazywania argumentw


Normaln praktyk, zwizan z przekazywaniem argumentw do funkcji, powinno by przekazywanie ich do staych w postaci referencji. Mimo e na pierwszy rzut oka moe to wyglda na podyktowane jedynie wzgldami efektywnoci (a kwestii poprawy efektywnoci podczas projektowania i budowy programu nie naley zazwyczaj bra pod uwag), to w tym przypadku wchodzi w rachub co wicej jak przekonamy si w dalszej czci rozdziau, do przekazania obiektu przez warto niezbdny jest konstruktor kopiujcy, a nie zawszejest on dostpny. Uzyskana poprawa efektywnoci moe by znaczna. Wynika to z prostego powodu przekazanie argumentu przez warto wymaga wywoania konstruktora i destruktora, ale w przypadku gdy nie zamierza si modyfikowa argumentu, to do przekazania go za pomoc referencji do staej potrzebnejest tylko umieszczenie na stosiejego adresu. , : Waciwie przekazywanie adresu niejest zalecan metod przekazywania argumentu jedynie wwczas, gdy zamierza si dokona takiego zniszczenia obiektu, e przekazanie go przez warto stanowi jedyne bezpieczne rozwizanie (lepsze ni modyfikacja obiektu zewntrznego, czego na og nie spodziewa si wywoujcy funkcj). Kwestia ta jest tematem nastpnego podrozdziau.

instruktor kopiujcy
Zapoznawszy si z podstawowymi wiadomociami na temat referencji wjzyku C++, moesz zmierzy si z jednym z trudniejszych poj tego jzyka konstruktorem kopiujcym, okrelanym czsto jako X(X&) (,Ji od referencji do X"). Konstruktor

64

Thinking in C++. Edycja polska ten ma kluczowe znaczenie dla sterowania przekazywaniem i zwracaniem przez warto typw zdefiniowanych przez uytkownika podczas wywoywania funkcji. Jak si wkrtce przekonamy, jest on tak wany, e kompilator zawsze automatycznie tworzy konstruktor kopiujcy, jeeli nie zosta on zdefiniowany przez programist,

>rzekazywanie i zwracanie przez warto


Aby zrozumie potrzeb istnienia konstruktora kopiujcego, rozwamy sposb, w jaki jzyk C obsuguje przekazywanie i zwracanie zmiennych przez warto w trakcie wywoania funkcji. Jeeli zostanie zadeklarowana, a nastpnie wywoana funkcja: int f(int x, char c); int g = f(a, b); to skd kompilator moe wiedzie, w jaki sposb przekaza i zwrci wartoci tych zmiennych? Po prostu wie! Zakres typw, z ktrymi musi sobie poradzi, jest do niewielki char, int, float, double, a take ich odmiany informacje te s wbudowane w kompilator. Jeeli dowiesz si, w jaki sposb wygenerowa program w asemblerze za pomoc uywanego przez ciebie kompilatora, i odnajdziesz instrukcje, wygenerowane dla wywoania funkcji f(), to bdone rwnowane poniszym instrukcjom:
push b push a call f() add sp,4 mov g, register a

Kod ten zosta znacznie uproszczony, by mg stanowi oglny przykad symbole zmiennych b i a mog mie rn posta, w zalenoci o'd tego, czy zmienne te bd globalne (w tym przypadku nosz nazwy _b i _a), czy te lokalne (kompilator bdzie uywa indeksw wzgldem wskanika stosu). Podobnie rzecz si ma z identyfikatorem zmiennej g. Posta wywoania funkcji f() bdzie zaleaa od uywanego przez kompilator sposobu uzupeniania nazw, a posta tekstu register a" (,jejestr a") od tego, w jaki sposb nazywane s rejestry procesora w jzyku asemblera. Logika, zawarta w kodzie, pozostanie jednak taka sama. W jzykach C oraz C++ argumenty s najpierw umieszczane na stosie od prawej strony do lewej a nastpnie wywoywana jest funkcja. Kod wywoujcy funkcj jest odpowiedzialny za usunicie argumentw ze stosu (co realizuje instrukcja add sp,4). Naley jednak zwrci uwag na to, e kompilator, przekazujc argumenty przez warto, umieszcza po prostu na stosie ich kopie. Wie bowiem, jaki maj one rozmiar, a take to, e umieszczenie na stosie powoduje utworzenie ich dokadnych kopii. Warto zwracana przez funkcj f() jest umieszczana w rejestrze. I w tym przypadku kompilator wie wszystko, co niezbdne na temat typu zwracanej wartoci poniewa typ ten jest wbudowany w jzyk, kompilator moe zwrci t warto, umieszczajc j w rejestrze. W przypadku prostych typw danych, dostpnych w jzyku C, dziaanie polegajce.na kopiowaniu poszczeglnych bitw, tworzcych warto, jest rwnoznaczne z kopiowaniem obiektu.

Rozdzia U.. * Referencje i konstruktor kopiujcy

365

Przekazywanie i zwracanie duych obiektw


Zastanwmy si teraz nad typami zdefiniowanymi przez uytkownika. Jeeli utworzymy klas, a nastpnie bdziemy chcieli przekaza obiekt tej klasy przez warto, to skd kompilator bdzie wiedzia, co ma w takim przypadku zrobi. Nie jest to typ wbudowany w kompilator, lecz zdefiniowany przez uytkownika. Aby to sprawdzi, zaczniemy od prostej struktury, ktra jest w oczywisty sposb zbyt dua, by monaj byo zwrci, uywajc do tego celu rejestrw:
/ / : Cll:PassingBigStructures.cpp struct Big { char buf[100]; int i; long d; } B. B2; Big bigfun(Big b) { b.i = 100; // Jaka operacja na argumencie return b; int main() { B2 = bigfun(B);

'

Rozszyfrowanie wygenerowanego przez kompilator programu w jzyku asemblera jest w tym przypadku nieco trudniejsze, poniewa wikszo kompilatorw wykorzystuje funkcje pomocnicze, nie umieszczajc caego kodu w miejscu wywoania funkcji. Wywoanie funkcji bigfun( ) w funkcji main( ) rozpoczyna si tak, jak mona si tego spodziewa na stosie jest umieszczana caa zawarto obiektu B (niektre kompilatory zapisuj w rejestrach adres obiektu typu Big oraz jego wielko, a nastpnie wywouj funkcj pomocnicz, umieszczajc ten obiekt na stosie). W rozpatrywanym poprzednio fragmencie kodu przed wywoaniem funkcji niezbdne byo jedynie umieszczenie jej argumentw na stosie. Jednak w przypadku programu PassingBigStructures.cpp zaobserwowa mona jeszcze dodatkow operacj przed wywoaniem funkcji na stosie jest umieszczany adres struktury B2, cho jest oczywiste, e nie jest ona argumentem. Aby zrozumie, dlaczego tak si dzieje, naley zdawa sobie spraw z ogranicze naoonych na kompilator podczas tworzenia przez niego kodu, wywoujcego funkcj.

Himka stosu podczas wywoJania funkcji


Gdy kompilator generuje kod realizujcy wywoanie funkcji, najpierw umieszcza na stosie wszystkie jej argumenty, a nastpnie j wywouje. Kod, generowany wewntrz funkcji, powoduje przesunicie wskanika stosu jeszcze bardziej w d w celu zapewnienia miejsca, w ktrym bd przechowywane jej zmienne lokalne (pojcie w d" ma w tym wypadku charakter wzgldny komputer moe zwiksza Iub zmniejsza wskanik stosu podczas umieszczania na nim wartoci). Jednak w trakcie wykonywania instrukcji CALL asemblera procesor umieszcza na stosie adres kodu

166

Thinking in C++. Edycja polska

programu, spod ktrego nastpio wywoanie funkcji, dziki czemu instrukcja RETURN asemblera moe wykorzysta ten adres w celu powrotu do miejsca wywoania. Adres ten stanowi, oczywicie, wito, gdy bez niego program cakiem by si pogubi. Poniej przedstawiono posta ramki stosu po wykonaniu instrukcji CALL i przydzieleniu pamici zmiennym lokalnym funkcji:

Argumenty funkcji Adres powrotny Zmienne lokalne


Kod, wygenerowany dla pozostaej czci funkcji, zakada, e pami bdzie zorganizowana doktednie w taki sposb, dziki czemu moe on ostronie pobiera argumenty funkcji oraz zmienne lokalne, nie naruszajc adresu powrotnego. Blok pamici zawierajcy wszystko to, co jest uywane przez funkcj w czasie jej wywoania, nazwijmy ramkfunkcji. By moe wydaje ci si rozsdne, by funkcja zwracaa wartoci na stosie. Kompilator mgby umieci je na stosie, a funkcja zwrciaby warto przesunicia; okrela ono, w ktrym miejscu stosu rozpoczyna si zwracana warto.

Wielobieno
Przyczyna problemu tkwi w tym, e jzyki C oraz C++ akceptuj wystpowanie przerwa czyli s one wielobiene (ang. re-entrant). Dopuszczaj one rwnie rekurencyjne wywoania funkcji. Oznacza to, e w dowolnym momencie wykonywania programu moe nastpi przerwanie, niewstrzymujce dziaania programu. Oczywicie, osoba tworzca procedur obsugi przerwa (ang. interrupt service routine lSR) jest odpowiedzialna za zachowanie i pniejsze odtworzenie wszystkich uywanych przez ni rejestrw. Jednake jeli taka procedura wymaga wykorzystania pamici znajdujcej si poniej wskanika stosu, musi mie moliwo dokonania tego w bezpieczny sposb (mona traktowa procedur obsugi przerwa jako zwyk funkcj, niepobierajc argumentw i niezwracajc wartoci zachowujc, a pniej odtwarzajc stan procesora; wywoanie procedury obsugi przerwa nastpuje na skutek wystpienia jakiego zdarzenia sprztowego, a nie jawnego wywoania jej w programie). Wyobramy sobie, co by si stao, gdyby zwyka funkcja prbowaa pozostawi na stosie zwracan przez siebie warto. Nie mona zmienia adnej czci stosu znajdujcej si powyej adresu powrotnego, wic funkcja musiaaby umieci t warto poniej tego adresu. Jednak, w chwili wykonywania instrukcji RETURN asemblera, wskanik stosu musi wskazywa adres powrotny (lub znajdujc si bezporednio pod nim pozycj w zalenoci od uywanego komputera). A zatem tu przed wykonaniem tej instrukcji funkcja musiaaby przesun wskanik stosu do gry, usuwajc w ten sposb wszystkie swoje zmienne lokalne. Gdyby podj prb pozostawienia

Rozdzia L Referencje i konstruktor kopiujcy

367

zwracanej przez funkcj warto poniej wskanika stosu, pojawioby si w tym momencie niebezpieczestwo zwizane z nadejciem przerwania. Procedura obsugi przerwa mogaby przesun wskanik stosu w d, zapamitujc na stosie adres powrotu oraz uywane przez siebie zmienne lokalne, co spowodowaoby zniszczenie wartoci zwrconej przez nasz funkcj. Aby rozwiza ten problem, kod wywoujcy funkcj mglby odpowiada za przydzielenie na stosie przed wywoaniem funkcji dodatkowego obszaru pamici, przechowujcego zwracane przez ni wartoci. Jednake jzyk C nie zosta w taki sposb zaprojektowany, ajzyk C++ musi by z nim zgodny. Jak si wkrtce przekonamy, kompilator jzyka C++ wykorzystuje bardziej efektywny sposb przekazywania wartoci zwracanej przez funkcj. Kolejny pomys mgby polega na zwracaniu wartoci za porednictwem jakiego obszaru danych globalnych, ale to rwnie nie dziaaoby poprawnie. Wielobieno oznacza, e kada funkcja moe by procedur obsugi przerwania dla dowolnej innej funkcji. Procedur tak moe wic by rwniefunkcja, ktrajest aktualnie wykonywana. Tak wic, po umieszczeniu wartoci zwracanej przez funkcj w obszarze danych globalnych, mgby nastpi powrt do tej samej funkcji, ktra nastpnie zamazaaby zwrcon warto1. Ten sam tok rozumowania odnosi si do rekurencji. Jedynym miejscem, w ktrym mona bezpiecznie przekaza zwracane wartoci, s rejestry. Powracamy wic do problemu, sprowadzajcego si do pytania, co uczyni w przypadku, gdy rejestry nie s dostatecznie due, by pomieci zwracan warto.. Rozwizaniem jest umieszczenie na stosie adresu miejsca, w ktrym naley zapisa warto zwracanprzez funkcj jakojednego z j e j argumentw. Jednoczenie naley pozwoli funkcjina skopiowanie bezporednio w tym miejscu zwracanej przez ni wartoci. Rozwizuje to nie tylko wszystkie problemy, ale jest rwnie bardziej efektywne. Z tego take powodu w programie PassingBigStructures.cpp kompilator przed wywoaniem funkcji bigfun( ) w funkcji main( ) umieci na stosie adres struktury B2. Gdyby przyjrze si asemblerowemu kodowi funkcji bigfun( ), to mona by zauway, e oczekuje ona tego ukrytego argumentu. Ponadto wewntrz tej funkcji jest dokonywane kopiowanie zwracanej wartoci pod wskazywany przez ten argument adres.

Kopiowanie bitw kontra inicjalizacja


Posiadamy ju zatem dziaajcy mechanizm, umoliwiajcy przekazywanie i zwracanie duych, prostych struktur. Zwr jednak uwag na to, e dysponujemy jedynie sposobem kopiowania bitw z jednego miejsca na drugie, co z pewnoci dobrze sprawdza si w przypadku prostego sposobu postrzegania zmiennych przez jzyk C. Jednak wjzyku C++ zmienne mogby czym duo bardziej wyszukanym ni tylko zlepkami bitw posiadaj bowiem znaczenie. Znaczenie to moe nie tolerowa kopiowania swoich bitw. Niejest to dobry przyUad, poniewa jak autor napisa wczeniej procedura obsugi przerwa nie zwraca adnej wartoci. Niebezpieczestwo polega natomiast na tym, e procedura obsugi przerwa mogaby wywola funkcj, ktra zostaa przez t procedur przerwana przyp. tum.

Thinking in C++. Edycja polska

Rozwamy prosty przykad klas zawierajc informacj o tym, ile jej obiektw istnieje rwnoczenie. W rozdziale 10. dowiedzielimy si, e mona to osign, wczajc do klasy statyczn skadow: //: Cll:HowMany.cpp // Klasa zliczajca swoje obiekty finclude <fstream> #include <string> using namespace std; ofstream out("HowMany.out"); class HowMany { static int objectCount; public: HowMany() { objectCount++; } static void print(const string& msg = "") { if(msg.size() != 0) out << msg << ": "; out << "objectCount = " << objectCount << endl; } ~HowMany() { objectCount--; print("~HowManyO");

int HowMany:;objectCount 0; // Przekazywanie i zwracanie PRZEZ WARTO; HowMany f(HowMany x) { x.print("argument x wewntrz fO"); return x; int main() { HowMany h; HowMany;:print("po utworzeniu h"); HowMany h2 - f(h);

HowMany::print("powywolaniu funkcji fO"); } ///:-

Klasa HowMany zawiera statyczn skadow cakowit objectCount (odgrywajc rol licznika obiektw") oraz statyczn funkcj skadow print(), drukujc warto objectCount wraz z dodatkowym, opcjonalnym komunikatem, podanym jako argument. Konstruktor zwiksza warto licznika, ilekro jest tworzony obiekt, a destruktor zmniejsza t warto. Wyniki dziaania programu nie sjednak zgodne z oczekiwaniami:
po utworzeniu h: objectCount = 1 argument x wewntrz f(); objectCount = 1 -HowMany(): objectCount = 0 -HowMany(); objectCount = -1 po wywoaniu funkcji f(): objectCount = -1 ^owMany(): objectCount - -2 -HowMany(): objectCount - -3

Rozdzia 1 1 . Referencje i konstruktor kopiujcy

369

Po utworzeniu obiektu h warto licznika wynosi jeden, co jest poprawne. Jednak po wywoaniu funkcji f() mona by si spodziewa zwikszenia licznika do dwch, poniewa w zasigu znajduje si rwnie drugi obiekt tej klasy h2. Jednak warto tego licznika wynosi zero, co oznacza, e doszo do katastrofy. Ponadto dwa nastpne destruktory spowodoway w kocu osignicie przez licznik wartoci ujemnej. Taka sytuacja nie powinna mie nigdy miejsca. Zastanwmy si, co si dzieje wewntrz funkcji f() bezporednio po przekazaniu jej argumentu przez warto. Pocztkowy obiekt h istnieje na zewntrz ramki funkcji, a w obrbie jej ramki znajduje si dodatkowy obiekt bdcy jego kopi, przekazan przez warto. Argument ten zosta jednak przekazany za pomoc prymitywnej metody jzyka C, sprowadzajcej si do kopiowaniu bit po bicie, podczas gdy klasa HowMany jzyka C++ wymaga, dla zachowania swojej integralnoci, rzeczywistej inicjalizacji i dlatego wanie kopiowanie bitw nie przynosi podanego rezultatu. Pod koniec wywoania funkcji f(), gdy obiekt lokalny wychodzi pozajej zasig, wywoywany jest destruktor, dekrementujcy warto skadowej objectCount, co sprawia, e po opuszczeniu funkcji licznik ten ma warto zerow. Tworzenie obiektu h2 odbywa si take za pomoc kopiowania bitw, wic rwnie w tym przypadku konstruktor nie jest wywoywany. Kiedy zatem obiekty h i h2 wykrocz poza swj zasig, ich destruktory spowoduj nadanie zmiennej skadowej objectCount wartoci ujemnej.

Konstrukcja za pomoc konstruktora kopiujcego


Przyczyn problemu jest przyjte przez kompilator zaoenie dotyczce sposobu, wjaki na podstawie istniejcego obiektu tworzonyjest nowy obiekt. Podczas przekazywania obiektu przez warto jest tworzony nowy obiekt. To przekazywany obiekt wewntrz ramki funkcji, utworzony na podstawie istniejcego obiektu, znajdujcego si poza ramk funkcji. Zdarza si to rwnie czsto w przypadku, gdy obiekt jest zwracany przez funkcj. W wyraeniu:
HowMany h2 - f(h);

nieskonstruowany jeszcze obiekt h2 jest tworzony na podstawie wartoci, zwracanej przez funkcj f(), a wic ponownie nowy obiekt jest tworzony na podstawie obiektuju istniejcego. Kompilator zakada, e tworzenie takiego obiektu powinno odbywa si metod kopiowania bitw. Sprawdza si to w wielu przypadkach, ale nie dziaa w stosunku do klasy HowMany, poniewa znaczenie inicjalizacji wykracza poza zakres zwykego kopiowania. Innym typowym przykadem jest sytuacja, w ktrej klasa zawiera wskaniki. Trzeba odpowiedzie sobie na pytania: co wskazuj i czy naleyje kopiowa, czy te powiza zjakim innym obszarem pamici? Na szczcie, mona ingerowa w ten proces i powstrzyma kompilator przed kopiowaniem bitw. Naley w tym ceIu zdefiniowa swoj funkcj, uywan przez kompilator w sytuacjach, gdy konieczne jest utworzenie nowego obiektu na podstawie obiektuju istniejcego. Wydaje si logiczne, e skoro tworzonyjest nowy obiekt, to

Thinking in C++. Edycja polska funkcja ta jest konstruktorem i jego jedynym argumentem jest obiekt, na ktrego podstawie odbywa si tworzenie nowego obiektu klasy. Argument ten nie moe by jednak przekazany przez warto. Prbujemy bowiem zdefiniowa funkcj, ktra bdzie obsugiwaa przekazywanie argumentw przez warto, natomiast przekazywanie wskanika nie ma sensu, poniewa, mimo wszystko, tworzymy nowy obiekt na podstawie istniejcego. W tym przypadku su pomoc referencje, wic argumentem konstruktora bdzie referencja do obiektu rdowego. Funkcja ta jest nazywana konstruktorem kopiujcym (ang. copy-constructor), czsto okrelanym jako X(X&), co stanowi jego posta dla klasy o nazwie X. Jeeli zostanie utworzony konstruktor kopiujcy, to kompilator nie bdzie kopiowa bitw podczas tworzenia nowych obiektw na podstawie ju istniejcych. Zawsze wywoa utworzony konstruktor kopiujcy. Tak wic jeeli nie zostanie utworzony konstruktor kopiujcy, to kompilator postpi rozsdnie, ale zawsze istnieje moliwo przejcia penej kontroli nad tym procesem. A zatem jest ju moliwe usunicie problemu, ktry wystpi w programie HowMany.cpp: / / : Cll:HowMany2.cpp // Konstruktor kopiujcy finclude <fstream> #include <string> using namespace std; ofstream out("HowMany2.out"); class HowMany2 { string name; // Identyfikator obiektu static int objectCount; public: HowMany2(const string& id "") : name(id) { ++objectCount ; print("HowMany2O"); ~HowMany2() { --objectCount; print("~HowMany2O"); // Konstruktor kopiujcy: HowMany2(const HowMany2& h) : name(h.name) { name += " kopia"; ++objectCount; pri nt ( "HowMany2(const HowMany2&) " ) ; } void print(const string& msg - "") const { if(msg.size() !- 0) out << msg << endl ; out << '\t' << name << ": " << "objectCount - " << objectCount << endl ;
} }

int HowHany2::objectCount = 0;

Rozdzia 1 1 . Referencje i konstruktor kopiujcy // Przekazywanie 1 zwracanie PRZEZ WARTO: HowMany2 f(HowMany2 x) { x.print("argument x wewntrz f O " ) ; out << "Powrot z funkcji f()" << endl; return x; } int main() { HowMany2 h("h"); out << "wywoanie funkcji f()" << endl; HowMany2 h2 = f(h);

371

h2.print("h2 po wywoaniu funkcji f()"); out << "Wywoanie funkcji f(). brak zwracanej wartoci" << endl; f(h);
out << "Po wywoaniu funkcji f()" << endl; III-

Wprowadzono tu szereg nowych elementw, dziki ktrym mona si lepiej zorientowa w sytuacji. Po pierwsze, acuch name pelni funkcj identyfikatora obiektu podczas drukowania informacji na jego temat. Wywoujc konstruktor, mona poda acuch identyfikacyjny (zazwyczaj nazw obiektu), ktry zostanie skopiowany do skadowej name przy uyciu konstruktora typu string. Argument domylny "" tworzy pusty acuch. Konstruktor, podobnie jak poprzednio, zwiksza warto skadowej objectCount, a destruktor zmniejszaj. Nastpn nowoci jest konstruktor kopiujcy, HowMany2(const HowMany2&). Pozwala on na utworzenie nowego obiektu wycznie na podstawie obiektu ju istniejcego, wic do skadowej name kopiowana jest nazwa istniejcego obiektu wraz z przyrostkiem kopia". Dziki temu mona si zorientowa, na jakiej podstawie obiekt ten zosta utworzony. Jeli przyjrze si uwaniej, mona zauway, e wywoanie name(h.name), wystpujce na licie inicjatorw konstruktora, jest w rzeczywistoci wywoaniem konstruktora kopiujcego klasy string. Wewntrz konstruktora kopiujcego zwikszanyjest licznik obiektw jak w przypadku zwykego konstruktora. Oznacza to, e obecnie licznik obiektw bdzie dziaa poprawnie w razie przekazywania i zwracania obiektw przez warto. Funkcja print( ) zostaa zmodyfikowana w taki sposb, by drukowa komunikat, identyfikator obiektu oraz licznik obiektw. Poniewa funkcja musi obecnie mie dostp do skadowej name danego obiektu, wic nie moe by ju statyczn funkcj skadow. Wewntrz funkcji main() zostao dodane drugie wywoanie funkcji f(). Wywoanie to prezentuje typowe dla jzyka C ignorowanie wartoci zwracanej przez funkcj. Wiedzc ju, w jaki sposb zwracana jest warto (kod znajdujcy si wewntrz funkcji obsuguje proces zwracania wartoci, umieszczajc wynik w miejscu przeznaczenia, ktrego adresjest przekazywanyjako ukryty argument), mona si dziwi, co dzieje si w przypadku, gdy jest ona ignorowana. Pewne wiato na to zagadnienie rzucaj wyniki dziaania programu.

Thinking in C++. Edycja polska Zanim zostan zaprezentowane wyniki, poniej zosta zamieszczony krtki program, wykorzystujcy strumienie wejcia-wyjcia do ponumerowania wierszy dowolnego pliku:

//: Cll:Linenum.cpp //{T} Linenum.cpp // Numerowanie wierszy #include ". ,/require.h" #include <vector> #include <string> #include <fstream> #include <iostream> #include <cmath> using namespace std;
int main(int argc, char* argv[]) { requireArgs(argc. 1, "Uzycie: linenum plik\n" "Numerowanie wierszy pliku"); ifstream in(argv[l]); assure(in, argv[l]); string line; vector<string> lines; while(getline(in, line)) // Wczytanie calego pliku lines.push_back(line); if(lines.size() == 0) return 0: int num = 0; // Liczba wierszy w pliku okrela szeroko: const int width = int(loglO(lines.sizeO)) + 1; for(int i = 0: i < lines.size(); i++) { cout.setf(ios::right, ios::adjustfield): cout.width(width); cout << ++num << ") " << lines[i] << endl;

Caly plik jest wczytywany do obiektu klasy vector<string> za pomoc tego samego kodu, ktry by ju prezentowany powyej. Chcielibymy, aby podczas drukowania numerw wszystkie wiersze byy wzgldem siebie wyrwnane. Wymaga to uwzgldnienia liczby wierszy w pliku, dziki czemu szeroko pola przeznaczonego na numer bdzie w kadym wierszu taka sama. Liczb wierszy mona atwo okreli, uywajc funkcji vector::size(). Potrzebujemyjednak przede wszystkim informacji, czy liczba wierszy jest wiksza bd rwna 10, 100, 1000 itd. Jeeli wyznaczy si logarytm dziesitny z liczby wierszy, zaokrgli w d do liczby cakowitej, a nastpnie doda do niego jeden, uzyskanym wynikiem bdzie maksymalna szeroko, zajmowana przez licznik wierszy. atwo dostrzec kilka dziwnych funkcji, wywoywanych wewntrz ptli for setf() oraz width(). Sto funkcje klasy ostream, pozwalajce na kontrol, w tym przypadku, odpowiednio wyrwnania oraz szerokoci wyprowadzanego tekstu. Musz one jednak by wywoywane podczas wyprowadzania kadego wiersza i dlatego wanie umieszczono je w ptli for. Drugi tom ksiki zawiera osobny rozdzia powicony strumieniom wejcia-wyjcia. Mona w nim znale wicej informacji na temat tych funkcji oraz innych sposobw sterowania dziaaniem strumieni wejcia-wyjcia.

Rozdzia U.. Referencje i konstruktor kopiujcy

373

Rezultat uycia programu Linenum.cpp w stosunku do pliku HowMany2.out jest nastpujcy: 1) 2) 3) 4) 5) 6) 7) 8) 9) 10) 11) 12) 13) 14) 15) 16) 17) 18) 19) 20) 21) 22) 23) 24) 25) 26) 27) 28) 29) 30) 31) HowMany2() h: objectCount - 1 Wywoanie funkcji f() HowMany2(const HowMany2&) h kopia: objectCount = 2 argument x wewntrz f() h kopia: objectCount = 2 Powrot z funkcji f() HowMany2(const HowMany2&) h kopia kopia: objectCount = 3 ~HowMany2() h kopia: objectCount - 2 h2 po wywoaniu funkcji f() h kopia kopia: objectCount = 2 Wywoanie funkcji f(), brak zwracanej wartoci HowMany2(const HowMany2&) h kopia: objectCount = 3 argument x wewntrz f() h kopia: objectCount = 3 Powrot z funkcji f() HowMany2(const HowMany2&) h kopia kopia: objectCount = 4 ~HowMany2O h kopia: objectCount = 3 ~HowMany2O h kopia kopia: objectCount = 2 Po wywoaniu funkcji f() -HowMany2() h kopia kopia: objectCount = 1 ~HowMany2() h: objectCount = 0

Jak mona si byo spodziewa, najpierw wywoywanyjest normalny konstruktor dla obiektu h, zwikszajcy licznik obiektw do wartoci jeden. Nastpnie jednak, podczas wchodzenia do funkcji f(), kompilator w celu przekazania argumentu przez warto wywouje niejawnie konstruktor kopiujcy. W obrbie ramki funkcji f() jest tworzony nowy obiekt, bdcy kopi obiektu h (std jego nazwa: h kopia"), w zwizku z czym konstruktor kopiujcy zwiksza licznik obiektw do dwch. Wiersz smy sygnalizuje pocztek powrotu z funkcji f(). Zanimjednak bdzie moga by zniszczona zmienna lokalna h kopia" (wykroczy poza zasig pod koniec funkcji), musi ona zosta skopiowana do obiektu zawierajcego warto zwracan przez funkcj, ktrym jest obiekt h2. Nieistniejcy jeszcze obiekt (h2) jest tworzony na podstawie obiektuju istniejcego (zmiennej lokalnej wewntrz funkcji f()), w zwizku z czym w dziewitym wierszu ponownie jest wywoywany konstruktor kopiujcy. Identyfikator obiektu h2 przyjmuje nazw h kopia kopia", poniewa jest on kopi, utworzon na podstawie kopii bdcej obiektem lokalnym funkcji f(). Po utworzeniu zwracanego obiektu, ale jeszcze przed zakoczeniem funkcji, licznik obiektw przyjmuje chwilowo warto rwn trzy, ale w nastpnej kolejnoci niszczony jest obiekt lokalny h kopia". Po zakoczeniu wywoania funkcji f ( ) w wierszu 13. istniejju tylko dwa obiekty, h oraz h2. Mona si rwnie przekona, e identyfikatorem obiektu h2jest rzeczywicie h kopia kopia".

174

Thinking in C++. Edycja polska

Dbiekty tymczasowe
W wierszu 15. rozpoczyna si wywoanie funkcji f(h), ignorujce tym razem zwracan przez funkcj warto. Jak mona zobaczy w wierszu 16., podobniejak poprzednio, do przekazania parametru jest wywoywany konstruktor kopiujcy. I rwnie jak w poprzednim przypadku, w 21. wierszu, w celu utworzenia wartoci zwracanej przez funkcj, wywoywanyjest konstruktor kopiujcy. Jednak konstruktor kopiujcy wymaga adresu, wskazujcego obiekt docelowy (wskanik this). Skd wic bierze si ten adres? Okazuje si, e kompilator moe utworzy obiekt tymczasowy ilekro potrzebuje go do poprawnego wyznaczenia wartoci wyraenia. W tym przypadku tworzy obiekt, ktry nie jest nawet widoczny, lecz suy tylko do przechowania ignorowanej wartoci, zwracanej przez funkcj f(). Czas ycia takiego tymczasowego obiektu jest moliwie jak najkrtszy, dziki czemu program nie jest zamiecony obiektami tymczasowymi, oczekujcymi na zniszczenie i zajmujcymi cenne zasoby. W pewnych przypadkach obiekt tymczasowy moe zosta natychmiast przekazany innej funkcji, ale poniewa po wywoaniu funkcji nie jest on ju potrzebny, wic zaraz po tym, jak funkcja koczy swoje dziaanie, za pomoc wywoania destruktora obiektu lokalnego (wiersze 23. i 24.) niszczonyjest rwnie obiekt tymczasowy (wiersze 25. i 26.). W wierszach od 28. do 31. niszczony jest obiekt h2, a nastpnie obiekt h, a warto licznika obiektw powraca, prawidowo, do zera.

Domylny konstruktor kopiujcy


Poniewa konstruktor kopiujcy realizuje przekazywanie i zwracanie przez warto, istotne jest, aby kompilator utworzy taki konstruktor dla prostych struktur czyli waciwie zrobi to samo, co w jzyku C. Jednake do tej pory zapoznalimy si jedynie z domylnym, prostym dziaaniem takiego konstruktora, polegajcym na kopiowaniu poszczeglnych bitw obiektu. W przypadku bardziej zoonych typw kompilator jzyka C++ bdzie nadal tworzy automatycznie konstruktor kopiujcy, jeeli nie zostanie on zdefiniowany przez programist. Wwczas kopiowanie bitw nie ma jednak sensu, poniewa w niedostateczny sposb odzwierciedla ono waciwe znaczenie obiektw. Poniszy przykad ilustruje bardziej przemylne dziaanie kompilatora. Zamy, e tworzymy klas zoon z obiektw pewnych istniejcych klas. Metoda ta, nazwana do trafnie kompozycj, stanowi jeden ze sposobw tworzenia nowych klas za pomoc klas ju istniejcych. Wyobramy sobie zachowanie naiwnego uytkownika, ktry prbuje szybko rozwiza problem, tworzc w taki wanie sposb klas. Nic nie wie na temat konstruktorw kopiujcych, wic nie tworzy takiego konstruktora. Poniszy przykad demonstruje, jakie dziaania podejmuje kompilator, tworzc domylny konstruktor kopiujcy dla utworzonej w taki sposb klasy:
/ / : Cll:DefaultCopyConstructor.cpp // Automatyczne tworzenie konstruktora kopiujcego #include <iostream> #include <string>' using namespace std: ^

Rozdzia 1 1 . Referencje i konstruktor kopiujcy class WithCC { // Posiada konstruktor kopiujcy public: // Wymagany jawny domylny konstruktor: WithCCO {} WithCC(const WithCC&) { cout << "WithCC(WithCC&)" << endl;

375

class WoCC { // Nie posiada konstruktora kopiujcego string id; public: WoCC(const string& ident = "") : idCident) {} void print(const string& msg = "") const { if(msg.sizeO != 0) cout << msg << ": "; cout << id << endl;

class Composite { WithCC withcc: // Osadzony obiekt WoCC wocc; public: Composite() : wocc("CompositeO") {} void print(const string& msg = "") const wocc.print(msg);

int main() { Composite c; c.print("Zawartosc obiektu c"); cout << "Wywoanie zlozonego konstruktora kopiujcego" << endl ;

Composite c2 = c; // Wywoanie konstruktora kopiujcego c2.print("Zawartosc obiektu c2");

Klasa WithCC zawiera konstruktor kopiujcy, ktry po prostu informuje o tym, e zosta wywoany, co ujawnia interesujc kwesti. Wewntrz klasy Composite obiekt WithCC jest tworzony za pomoc domylnego konstruktora. Gdyby klasa WithCC nie posiadaa w ogle konstruktora, to kompilator automatycznie utworzyby konstruktor domylny, ktry w tym przypadku nic by nie robi. Jednake dodanie konstruktora kopiujcego informuje kompilator, e zamierzamy samodzielnie tworzy konstruktory. Nie generuje onju wic domylnego konstruktora, i gdyby ten konstruktor nie zosta utworzony, jak to zrobilimy w klasie WithCC zgosiby bd. Klasa WoCC nie posiada konstruktora kopiujcego, ale jej konstruktor zapamituje, w wewntrznej zmiennej typu string, komunikat, ktry mona pniej wydrukowa, wywoujc funkcj print( ). Konstruktor ten jest jawnie wywoywany na licie inicjatorw konstruktora klasy Composite (lista inicjatorw konstruktora zostaa krtko omwiona w rozdziale 8. i zostanie wyczerpujco opisana w 12. rozdziale ksiki). Powd, dla ktrego tak zrobiono, stanie si pniej oczywisty.

Thinking in C++. Edycja polska

Klasa Composite zawiera obiekty skadowe, zarwno klasy WithCC, jak i WoCC (zwr uwag na to, e osadzony w klasie obiekt wocc musi by inicjalizowany w obrbie listy inicjatorw konstruktora), nie zawiera natomiast jawnie zdefiniowanego konstruktora kopiujcego. Jednake w funkcji main( ) obiekt tej klasy jest tworzony za pomockonstruktora kopiujcego, w nastpujcej definicji:
Composite c2 = c;

Konstruktor kopiujcy klasy Composite jest automatycznie generowany przez kompilator, a informacje wywietlane przez program ujawniaj, w jaki sposb zostal on utworzony:
Zawartosc obiektu c: Composite() Wywoanie zlozonego konstruktora kopiujcego WithCC(WithCC&) Zawartosc obiektu c2: Composite()

Tworzc konstruktor kopiujcy klasy wykorzystujcej kompozycj (a take dziedziczenie, o czym przekonamy si w rozdziale 14.), kompilator wywouje rekurencyjnie konstruktory kopiujce wszystkich obiektw skadowych oraz klas podstawowych. Oznacza to, e jeli obiekty skadowe bd zawieray rwnie inne obiekty, to ich konstruktory kopiujce zostan wywoane. Dlatego te, w tym przypadku, kompilator wywouje konstruktor kopiujcy klasy WithCC (informacje wywietlane przez program dowodz, e konstruktor ten zosta wywoany). Poniewa klasa WoCC nie posiada konstruktora kopiujcego, kompilator tworzy dla niej konstruktor, wykorzystujcy po prostu kopiowanie bitw, i wywouje go z wntrza konstruktora kopiujcego klasy Composite. Dowodzi tego wywoanie funkcji Composite::print() w funkcji main( ), poniewa zawarto skadowej c2.wocc jest taka sama, jak skadowej c.wocc. Proces realizowany przez kompilator w trakcie tworzenia konstruktora kopiujcego jest nazywany inicjalizacj za porednictwem elementw skladowych (ang. memberwise initialization). Zawsze lepiej jest przygotowa wasny konstruktor kopiujcy ni pozwoli na to, by utworzy go kompilator. Gwarantuje to nadzr nadjego dziaaniem.

Moliwoci zastpienia konstruktora kopiujcego


Masz zapewne powane wtpliwoci jak mona byo w ogle utworzy dziaajc klas, nie wiedzc nic na temat konstruktora kopiujcego? Pamitaj jednak, e konstruktor kopiujcy jest potrzebny wycznie w przypadku, gdy zamierzasz przekazywa obiekt klasy przez warto. Jeeli taka sytuacja nigdy nie zachodzi, to konstruktor kopiujcyjest niepotrzebny.

Zapobieganie przekazywaniu przez warto


Moesz jednak zada pytanie: ,Jezeli nie utworz konstruktora kopiujcego, to kont pilator zrobi to za mnie. Skd mam wic wiedzie, e obiekt nie zostanie nigdy przekazany przez warto?".

Rozdzia 1 1 . Referencje i konstruktor kopiujcy

377

Istnieje prosty sposb, uniemoliwiajcy przekazywanie obiektu przez warto zadeklarowanie prywatnego konstruktora kopiujcego. Nie trzeba nawet tworzy jego definicji, chyba e jaka funkcja skadowa lub funkcja zaprzyjaniona bdzie musiaa przekaza obiekt przez warto. Jeeli uytkownik sprbuje przekaza lub zwrci obiekt tej klasy przez warto, kompilator zgosi komunikat o bdzie, poniewa konstruktor kopiujcyjest funkcj skadow prywatn. Kompilator nie moe ju rwnie utworzy domylnego konstruktora kopiujcego, poniewa wyranie okrelilimy, e wzilimy to zadanie na siebie. Poniej zamieszczono przykad ilustrujcy t metod: //: Cll:NoCopyConstruction.cpp // Zapobieganie uyciu konstruktora kopiujcego class NoCC { NoCC(const NoCC&); // Brak definicji public: NoCC(int ii = 0) : i(ii) {}

int i ;

}:
void f(NoCC); int main() { NoCC n; //! f(n); // Bd: wywoanie konstruktora kopiujcego //! NoCC n2 = n; // Bd: wywoanie konstruktora kopiujcego //! NoCC n3(n): // Bd: wywoanie konstruktora kopiujcego Zwr uwag na bardziej oglnposta konstruktora: NoCC(const NoCC&); wykorzystujcreferencj do staej.

Funkcje modyfikujce obiekty zewntrzne


Zapis z zastosowaniem referencji przedstawia si lepiej ni ten wykorzystujcy wskaniki, ale przesania nieco swoje prawdziwe znaczenie. Na przykad w bibliotece strumieni wejcia-wyjciajedna z przecionych wersji f u n k c j i g e t ( ) pobiera argument typu char&, a gwnym jej celem jest modyfikacja argumentu, polegajca na zapisaniu w nim wyniku dziaania funkcji get( ). Jednak podczas czytania kodu, wykorzystujcego t funkcj, fakt modyfikacji przez ni obiektu zewntrznego nie jest wcale od razu oczywisty: char c; cin.get(c); Wywoanie funkcji wyglda raczej na przekazanie argumentu przez warto, co sugeruje, e obiekt zewntrzny niejest modyfikowany. Z tego wzgldu z punktu widzenia pielgnacji kodu bezpieczniej jest uy wskanikw, jeli przekazuje si adres obiektu w celujego modyfikacji. Jeeli zawsze

Thinking in C++. Edycja polska

przekazujesz adresy w postaci referencji do staych, z wyjtkiem przypadkw, w ktrych zamierzasz zmienia wartoci zewntrznych obiektw i przekazujesz je wwczas w postaci wskanikw (niebdcych wskanikami do staych), to tworzony w taki sposb kod jest znacznie bardziej przejrzysty.

Vskazniki do skadowych
Wskanik jest zmienn, przechowujc adres jakiego obszaru pamici. W czasie pracy programu mona zmienia jego warto, tak by wskazywa on rne obiekty. Obiektami wskazywanymi przez wskaniki mog by zarwno dane, jak i funkcje. Wystpujce w jzyku C++ wskaniki do skladowych realizuj t sam ide, z wyjtkiem tego, e wskazuj one elementy znajdujce si wewntrz klas. Dylemat w tym przypadku jest nastpujcy: wskaniki wymagaj adresw, a wewntrz klas nie istnieje pojcie adresu" wybr skadowej odbywa si na podstawie okrelenia jej przesunicia w obrbie klasy. Nie mona okreli rzeczywistego adresu, dopki nie zoy si tego przesunicia z adresem konkretnego obiektu. Skadnia wskanikw do sWadowych wymaga wic, aby obiekt by okrelony w chwili, w ktrej dokonuje si wyuskania wskanika do skadowej. Aby zrozumie t skadni, rozwamy prost struktur Simple, zmienn sp, bdc wskanikiem do niej, oraz obiekt tej struktury so. Odwoujc si do skadowych struktury, mona uywa przedstawionej poniej skadni:
/ / : Cll:SimpleStructure.cpp struct Simple { int a; }; int main() { Simple so. *sp = &so; sp->a; so.a; } ///:-

Zamy teraz, e posiadamy zmienn ip, bdc zwykym wskanikiem liczby cakowitej. Aby odwoa si do tego, co wskazuje zmienna ip, naley dokona wyuskania wskanika, uywajc do tego celu operatora *":
*ip = 4;

Zastanwmy si wreszcie, co dzieje si w przypadku, gdy dysponujemy wskanikiem wskazujcym co, co znajduje si wewntrz obiektu klasy, nawet jeeli w rzeczywistoci reprezentuje on jedynie przesunicie wzgldem pocztku tego obiektu. Aby uzyska dostp do tego, co wskazuje, trzeba go wyuska za pomoc operatora *. Jednake wskanik okrela tylko przesunicie w obrbie obiektu, wic trzeba rwnie odwoa si dojakiego konkretnego obiektu. Dlatego te operator * zosta poczony zjego wyuskaniem. Tak wic powsta w ten sposb skadnijest ->* (w przypadku wskanikw do obiektw) oraz .* (w przypadku obiektw lub referencji), jak wid^ w poniszych przykadach:
wskaznikObiektu->*wskaznikSkladowej - 47; otnekt.*wskaznikSkladowej = 47;

Rozdzia 1 1 . Referencje i konstruktor kopiujcy

379

Jak jednak zdefiniowa wskaznikSkladowej? Podobnie jak w przypadku kadego wskanika, trzeba okreli wskazywany przez niego typ i uy w definicji znaku *. Jedyna rnica polega na tym, e trzeba okreli klas, z ktrej obiektami jest uywany ten wskanik. Oczywicie, uzyskuje si to za pomoc nazwy tej klasy oraz operatora zasigu. Tak wic: int K1asaObiektu::*wskaznikSkladowej; definiuje zmienn o nazwie wskaznikSkladowej, bdc wskanikiem do dowolnej sUadowej typu cakowitego, znajdujcej si wewntrz obiektu klasy KlasaObiektu. Mona rwnie zainicjowa wskanik do skadowej w czasie jego definicji (lub pniej, w dowolnej chwili):
int KlasaObiektu::*wskaznikSkladowej = &KlasaObiektu::a;

W rzeczywistoci nie istnieje adres" skadowej KlasaObiektu::a, poniewa odwoujemy si do klasy, a nie do obiektu tej klasy. Tak wic wyraenie &KlasaObiektu::a moe zosta uyte wycznie w kontekcie wskanika do skadowej. Poniej zamieszczono program przedstawiajcy sposoby tworzenia i uywania wskanikw do skadowych: //: Cll:PointerToMemberData.cpp #include <iostream> using namespace std; class Data { public:

void print() const { cout << "a = " << a << ", b = " << b << ", c - " << c << endl:

int a, b, c;

}:

int main() { Data d, *dp - &d; int Data::*pmInt - &Data::a; dp->*pmInt = 47; pmInt = &Data::b; d.*pmInt = 48: pmInt = &Data::c; dp->*pmInt = 49; dp->print():

} III-

Oczywicie, wskaniki te s zbyt niewygodne, by ich gdziekolwiek uywa, z wyjtkiem szczeglnych przypadkw (wanie z myl o nich zostay one utworzone). Wskaniki do skadowych s rwnie do ograniczone mona im przypisa tylko okrelone miejsce w obrbie klasy. Nie mona ich, na przykad, inkrementowa ani porwnywa, jak w przypadku zwykych wskanikw.

30

Thinking in C++. Edycja polska

unkcje
W podobny sposb mona uzyska wskaniki do skadowych bdcych funkcjami. Wskaniki do funkcji (wprowadzone pod koniec rozdziau 3.) s definiowane w nastpujcy sposb:
int (*fp)(float);

Ujcie wskanika (*fp) w nawias jest konieczne, by wymusi na kompilatorze waciw interpretacj definicji. Gdyby nie mia on nawiasu, wygldaby na funkcj zwracajc warto typu int*. Nawiasy odgrywaj rwnie wan rol podczas definiowania i stosowania wskanikw do funkcji skadowych. Wskanik do funkcji zawartej w klasie jest definiowany poprzez wstawienie nazwy klasy oraz operatora zakresu do zwykej definicji wskanika funkcji:
/ / : Cll:PmemFunDefimtion.cpp class Simple2 { public: int f(float) const { return 1: } }:

int (Simple2::*fp)(float) const; int (Simple2::*fp2)(float) const = &Simple2::f; int main() { fp = &Simple2: :f:

Na przykadzie definicji wskanika fp2 przekonujemy si, e wskanik do funkcji skadowej moe by rwnie zainicjowany podczas tworzenia lub w dowolnym innym momencie. W odrnieniu od funkcji niebdcych funkcjami skadowymi, uycie operatora & podczas pobierania adresu funkcji skadowej nie jest opcjonalne. Monajednak poda identyfikator funkcji skadowej bez listy argumentw, poniewa w przypadku przecienia nazwy funkcji rozstrzygnicia wolno dokona na podstawie typu wskanika do skadowej.

>rzyktad
Korzyci wynikajc ze stosowania wskanikw jest moliwo zmiany tego, co wskazuj, podczas pracy programu. W istotny sposb zapewnia to programowaniu wiksz elastyczno, gdy dziki wskanikom mona w czasie dziaania programu okrela lub zmienia jego zachowanie. Podobnie wyglda sprawa ze wskanikami do skadowych pozwalaj one na wybr skadowej podczas pracy programu. W typowym przypadku w utworzonych przez ciebie klasach publicznie dostpne bd jedynie funkcje skadowe (dane skadowe s zazwyczaj traktowane jako cz wewntrznej implementacji), zatem poniszy program wybiera w trakcie dziaania wywoywan funkcj skadow: //: Cll:PointerToMemberFunction.cpp #include <iostream> using namespace std:.

Rozdzia il. Referencje i konstruktor kopiujcy class Widget { public: void f(int) const { cout << "Widget::f()\n"; void g(int) const { cout << "Widget::g()\n"; void h(int) const { cout << "Widget::h()\n"; void iCint) const { cout << "Widget::i()\n"; }:

381

} } } }

int main() { Widget w; Widget* wp = &w; void (Widget::*pmem)(int) const = &Widget::h; (w.*pmem)(l); (wp->*pmem)(2); } ///:Oczywicie, nie mona oczekiwa, e tak skomplikowane wyraenia utworzy jaki przypadkowy uytkownik. W przypadku gdy uytkownik musi bezporednio operowa wskanikami do skadowych, powinien wykorzysta do tego deklaracj typedef. Aby pozby si takich niedogodnoci, mona wykorzysta wskaniki do skadowych w charakterze mechanizmu nalecego do wewntrznej implementacji klasy. Poniej przedstawiono poprzedni przykad z zastosowaniem wskanikw do skadowych wewntrz klasy, Uytkownik przekazuje jedynie numer, wybierajc za jego pomoc wywoywan funkcj 2 : //: Cll:PointerToMemberFunction2.cpp #include <iostream> using namespace std;

class Widget {

void f(int) const { cout << "Widget::f()\n"; } void g(int) const { cout << "Widget::gC)\n"; } void h(int) const { cout << "Widget::h()\n"; } void i(int) const { cout << "Widget::i()\n"; } enum { cnt = 4 }; void (Widget::*fptr[cnt])(int) const; public: Widget() { fptr[0] = &Widget::f; // Wymagana pena specyfikacja fptr[l] - &Widget::g; fptr[2] = &Widget::h; fptr[3] - &Widget::i; } void select(int i, int j) { if(i < 0 | | i >- cnt) return; (this->*fptr[i])(j); } int count() { return cnt; } int main() { Widget w; for(int i - 0; i < w.count(); i++) w.select(i. 47);

Za ten przyWad dzikuj Owenowi Mortensenowi.

(2

Thinking in C++. Edycja polska

Zarwno w interfejsie klasy, jak i w funkcji main() caa implementacja klasy, wcznie z jej funkcjami, zostaa ukryta. Kod musi nawet prosi o podanie liczby dostpnych funkcji, wywoujc w tym celu funkcj count(). Dziki temu osoba implementujca klas moe zmieni liczb funkcji zawartych w wewntrznej implementacji klasy, nie naruszajc kodu wykorzystujcego t klas. Inicjalizacja wskanikw do skadowych, zawarta w konstruktorze, wydaje si okrelona w zbyt szczegowy sposb. Czy nie mona by napisa: fptr[l] = &g; g jest bowiem nazw funkcji skadowej i, w zwizku z tym, jest automatycznie widoczna w zasigu klasy? Problem polega na tym, e nie odpowiadaoby to skadni, stosowanej w przypadku wskanikw do skadowych.Jest ona wymagana po to, by kady a w szczeglnoci kompilator potrafi okreli, co si w tym miejscu dzieje. Podobnie gdy wyuskiwane s wskaniki do skadowych: (this->*fptr[i])(j); posta instrukcji wydaje si znowu zbyt rozbudowana wyglda tak, jakby wskanik this nie byi w niej potrzebny. Rwnie w tym przypadku skadnia wymaga, by wskanik do skadowej by, w momencie jego wyuskania, zawsze przypisany do konkretnego obiektu.

Podsumowanie
Wskaniki dziaaj w jzyku C++ niemal tak samo, jak w jzyku C, co jest dobr wiadomoci. W przeciwnym razie wiele programw, napisanych w jzyku C, nie kompilowaoby si poprawnie w jzyku C++. Jedyne bdy, ktre wystpi podczas kompilacji, bd spowodowane niebezpiecznymi operacjami przypisania. Jeeli operacje te byy naprawd zamierzone, mona unikn bdw zgaszanych przez kompilator, uywajc prostego (ijawnego!) rzutowania. Jzyk C++ zawiera dodatkowo referencje, wywodzce si z takich jzykw programowania, jak Algol i Pascal. W istocie s one staymi wskanikami, automatycznie wyuskiwanymi przez kompilator. Referencje przechowuj adresy, ale traktuje si je jak obiekty. Referencje maj zasadnicze znaczenie dla uproszczenia skadni za pomoc przeciania operatorw (co stanowi temat nastpnego rozdziau). Upraszczaj one rwnie skadni zwizan z przekazywaniem i zwracaniem obiektw przez zwyke funkcje. Konstruktor kopiujcy pobiera jako argument referencj do istniejcego obiektu takiego samego typu i tworzy na jego podstawie nowy obiekt. Kompilator automatycznie wywouje konstruktor kopiujcy w przypadku przekazywania lub zwracania obiektw przez warto. Kompilator automatycznie tworzy konstruktory kopiujce, leczjeli konstruktor taki jest potrzebny w projektowanej klasie, naley zawsze zdefiniowa go samodzielnie, by mie pewno, e dziaa on prawidowo. Jeeli natomiast nie chcesz, by obiekty byy przekazywane lub zwracane przez warto, to powiniene utworzy prywatny konstruktor kopiujcy.

Rozdzia 1 1 . << Referencje i konstruktor kopiujcy

383

Wskaniki do skadowych maj takie samo dziaanie, jak zwyke wskaniki. Pozwalaj na wskazanie okrelonego obszaru pamici (danych lub funkcji) w czasie pracy programu. Wskaniki do skadowych dziaaj bowiem w stosunku do skadowych klas, a nie do globalnych danych lub funkcji. Wprowadzaj one do programowania wiksz elastyczno, pozwalajc na zmian zachowania programu podczas jego pracy.

wiczenia
Rozwizania wybranych wicze znajduj si w dokumencie elektronicznym: The Thinking in C++ Annotated Solution Guide, ktry mona pobra za niewielk opat z witryny http://www.BruceEckel.com. 1. Przekszta fragment kodu ptak i skaa", znajdujcy si na pocztku rozdziau, w program wjzyku C (uywajc strukturjako typw danych) i poka, e program ten si kompiluje. Nastpnie sprbuj go skompilowa, uywajc kompilatorajzyka C++, i zobacz, co si stanie. 2. Wykorzystaj fragmenty programu znajdujcego si na pocztku podrozdziau Referencje w C++" i umieje w funkcji main(). Dodaj do nich instrukcje, drukujce informacje, dziki ktrym mona bdzie przekona si, e referencje przypominaj automatycznie wyuskiwane wskaniki. 3. Napisz program, w ktrym sprbujesz: 1. Utworzy referencj niezainicjowan podczas tworzenia. 2. Zmieni utworzonju referencj w taki sposb, aby odwoywaa si do innego obiektu. 3. Utworzy pustreferencj. 4. Napisz funkcj pobierajc argument bdcy wskanikiem, ktra modyfikuje wskazywanprzez niego warto, a nastpnie zwracajza pomocreferencji. 5. Utwrz klas posiadajc kilka funkcji skadowych, a nastpnie tak zmodyfikuj funkcj, utworzonwpoprzednim wiczeniu, byjej argument wskazywa obiekt tej klasy. Uczy ten wskanik wskanikiem do staej, zdefiniuj kilka funkcji klasyjako funkcje stae i udowodnij, e wewntrz funkcji wywoywane mogby tylko stae funkcje skadowe. Przekszta wskanik bdcy argumentem funkcji w referencj. 6. Wykorzystaj fragmenty kodu znajdujce si na pocztku podrozdziau Referencje do wskanikw" i przekszta je w program. 7. Utwrz funkcj, pobierajcjako argument referencj do wskanika, wskazujcego wskanik i modyfikujcten argument. Wywoaj t funkcj w funkcji main(). 8. Utwrz funkcj, pobierajcargument typu char& i modyfikujcjego warto. W funkcji main() wydrukuj warto zmiennej znakowej (typu char), wywoaj dla tej zmiennej swojfunkcj, a nastpnie ponownie wydrukuj warto tej zmiennej, by si przekona, e rzeczywicie zostaa ona zmodyfikowana. Jak wpywa to na czytelno twojego programu?

84

Thinking in C++. Edycja polska 9. Utwrz klas zawierajc stae funkcje skadowe i funkcje skadowe niebdce staymi. Napisz trzy funkcje, pobierajcejako argumenty obiekt tej klasy tak by pierwsza pobieraa go przez warto, druga przez referencj, a trzecia przez referencj do staej. Sprbuj wywoa oba rodzaje funkcji skadowych klasy wewntrz tych funkcji i wyjanij uzyskane rezultaty. 10. (Nieco trudne) Napisz prost funkcj, pobierajcjako argument liczb cakowit, inkrementujcjej warto i zwracajcj. Wywoaj t funkcj w funkcji main(). Nastpnie dowiedz si, wjaki sposb wygenerowa na wyjciu kompilatora kod w asemblerze i przeled instrukcje tego kodu, aby zrozumie, w jaki sposb przekazywane i zwracane s argumenty i jak przechowywane s na stosie zmienne lokalne. 11. Napisz funkcj przyjmujcargumenty typu char, int, float i double. Wygeneruj za pomoc kompilatora kod w asemblerze i odszukaj instrukcje umieszczajce te argumenty na stosie przed wywoaniem funkcji. 12. Napisz funkcj zwracajc warto typu double. Wygeneruj jej kod w jzyku asemblera i sprawd, w jaki sposb zwracanajest warto funkcji. 13. Wygeneruj kod asemblerowy programu PassingBigStructures.cpp. Przeled i wyjanij sposb, wjaki uywany przez ciebie kompilator generuje kod, umoliwiajcy przekazywanie i zwracanie duych struktur. 14. Napisz prost, rekurencyjnfunkcj, dekrementujcswj argument i zwracajc warto zero, gdy argument osiga warto zerow, a w przeciwnym przypadku wywoujcsamsiebie. Wygeneruj kod tej funkcji wjzyku asemblera i wyjanij, wjaki sposb kod, wygenerowany przez kompilator, obsuguje rekurencj. 15. Napisz kod, ktry udowodni, e kompilator automatycznie generuje konstruktor kopiujcy, jeeli nie zostanie on utworzony przez programist. Udowodnij, e wygenerowany przez kompilator konstruktor przeprowadza kopiowanie bitw w przypadku prostych typw, a dla typw zdefiniowanych przez uytkownika wywouje ich konstruktory kopiujce. 16. Utwrz klas zawierajckonstruktor kopiujcy, informujcy o swoim wywoaniu, przez wyprowadzenie komunikatu do strumienia cout. Nastpnie napisz funkcj, pobierajcjako argument obiekt tej klasy przez warto, i drug funkcj tworzc lokalny obiekt tej klasy i zwracajc go przez warto. Wywoaj obie te funkcje, by przekona si, e konstruktor kopiujcy jest rzeczywicie niejawnie wywoywany podczas przekazywania i zwracania obiektw przez warto. 17. Utwrz klas zawierajcskadowtypu double*. Konstruktor powinien inicjalizowa t skadow, uywajc wywoania new double i przypisujc przydzielonemu obszarowi pamici warto swojego argumentu. Destruktor powinien: drukowa warto, wskazywanprzez skadowklasy, przypisywa wskazywanemu przez niobszarowi warto -1, uy w stosunku do tego obszaru operatora delete i przypisa wskanikowi warto zerow. Nastpnie utwrz funkcj, pobierajc obiekt utworzonej klasy przez warto, i wywoaj jw funkcji main(). Wyjanij, co si stao. Usu problem, tworzc konstruktor kopiujcy.

Rozdzia H. Referencje i konstruktor kopiujcy M. Utwrz klas zawierajc konstruktor, ktry wyglda tak, jak konstruktor kopiujcy, ale zawiera dodatkowy argument, posiadajcy domyln warto. Wyka, e jest on nadal uywany w charakterze konstruktora kopiujcego. 19. Utwrz klas posiadajc konstruktor kopiujcy, informujcy o swoim wywoaniu. Utwrz drug klas, zawierajc skadow bdc obiektem pierwszej klasy, ale nie twrz dla niej konstruktora kopiujcego. Poka, e konstruktor kopiujcy, wygenerowany dla drugiej klasy, automatycznie wywouje konstruktor kopiujcy pierwszej klasy.

385

20. Utwrz bardzo prost klas oraz funkcj, zwracajc obiekt tej klasy przez warto. Napisz drug funkcj, pobierajc referencj do obiektu utworzonej klasy. Wywoaj pierwszztych funkcjijako argument drugiej funkcji i poka, e druga funkcja musi pobiera argument bdcy referencjdo staej. 21. Utwprz prost klas nieposiadajc konstruktora kopiujcego i prost funkcj, pobierajcjako argument obiekt tej klasy przez warto. Zmie swoj klas, dodajc prywatn deklaracj (tylko deklaracj) konstruktora kopiujcego. Wyjanij to, co dzieje si podczas kompilacji tej funkcji. 22. W tym wiczeniu tworzonejest rozwizanie konkurencyjne w stosunku do uycia konstruktora kopiujcego. Utwrz klas X i zadeklaruj jej prywatny konstruktor kopiujcy (ale nie definiuj go). Utwrz publiczn funkcj done() bdc sta funkcj skadow, ktra zwraca kopi obiektu, utworzon za pomoc operatora new. Nastpnie napisz funkcj, pobierajc argument typu const X& i tworzc, za pomoc funkcji clone(), jego lokaln kopi, ktra moe by modyfikowana. Wadtakiego rozwizaniajest konieczno jawnego zniszczenia utworzonej kopii obiektu (za pomoc operatora delete), gdy nie jest on ju potrzebny. 23. Wyjanij, na czym polegajproblemy wystpujce w plikach Mem.cpp oraz MemTest.cpp, zamieszczonych w rozdziale 7. Popraw pliki w taki sposb, by rozwiza te problemy. 24. Utwrz klas zawierajc skadow typu double i funkcj print() drukujc warto tej skadowej. W funkcji main() utwrz wskaniki zarwno do danej, jak i funkcji skadowej klasy. Utwrz obiekt tej klasy oraz wskanik do tego obiektu, a nastpnie odwouj si do obu elementw skadowych klasy za pomoc wskanikw do skadowych, uywajc zarwno obiektu, jak i wskanika do obiektu. 25. Utwrz klas zawierajc tablic liczb cakowitych. Czy mona indeksowa t tablic, wykorzystujc wskanik do skadowej klasy? 26. Zmodyfikuj program PmemFunDeflnition.cpp, dodajc przecion funkcj skadow f ( ) (moesz dowolnie okreli list argumentw tej funkcji, umoliwiajcjej przecienie). Nastpnie utwrz drugi wskanik do skadowej, przypisujc mu przecion wersj funkcji f(), i wywoaj funkcj za pomoc tego wskanika. Wjaki sposb rozrniane s w tym przypadku przecione funkcje?

86

Thinking !n C++. Edycja polska

27. Rozpocznij od programu FunetionTable.cpp, zamieszczonego w rozdziale 3. Utwrz klas zawierajc wektor (utworzony za pomoc kontenera vector) wskanikw do funkcji, wykorzystujc funkcje skadowe add() i remove() do dodawania i usuwania poszczeglnych wskanikw do funkcji. Dodaj funkcj run(), przechodzc przez wszystkie elementy zawarte w wektorze i wywoujc wskazywane przez nie funkcje. 28. Zmodyfikuj poprzednie wiczenie w taki sposb, aby program dziaa ze wskanikami do funkcji skadowych.

Przecianie operatorw
Przecianie operatorw jest nazywane cukierkiem skadniowym", co oznacza, e jest ono innym sposobem wywoania funkcji. Rnica polega na tym, e argumenty tej funkcji nie s umieszczone w nawiasie, natomiast otaczajznaki albo te ssiadujz nimi. Tymczasem zawsze uwaalimyje za niepodlegajce modyfikacjom operatory. Istniej dwie rnice pomidzy uyciem operatorw i zwykym wywoaniem funkcji. Po pierwsze, rni je skadnia operator jest czsto wywoywany" poprzez umieszczenie go pomidzy argumentami, a czasami rwnie za lub przed nimi. Druga rnica polega na tym, e to kompilator decyduje o tym, ktr funkcj" ma wywoa. Na przykad jeeli operator + uywany jest z argumentami zmiennopozycyjnymi, to kompilator wywouje" funkcj, realizujc operacj dodawania zmiennopozycyjnego (wywoanie" to polega na og na wstawieniu odpowiedniego kodu lub instrukcji procesora zmiennopozycyjnego). Jeeli operator + zostanie uyty wraz z liczb zmiennopozycyjn oraz liczb cakowit, to kompilator wywoa" specjaln funkcje, przeksztacajc liczb typu int w liczb typu float, a nastpnie wywoa" kod wykonujcy dodawanie zmiennopozycyjne. Jednak w jzyku C++ moliwe jest zdefiniowanie nowych operatorw, dziaajcych w stosunku do klas. Definicja ta przypomina zwyk definicj funkcji, z wyjtkiem tego, e nazwa funkcji skada si ze sowa kluczowego operator, po ktrym nastpuje operator. To jedyna rnica; poza tym staje si ona tak funkcj, jak kada inna wywoywana przez kompilator, gdy widzi on odpowiadajcyjej wzorzec.

Rozdzia 12.

Ostrzeenie i wyjanienie
atwo jest wpa w nadmierny entuzjazm zwizany z przecianiem operatorw. Na pierwszy rzut oka wydaje si to wietn zabaw. Trzeba jednak pamita, e to tylko cukierek skadniowy" inny sposb wywoania funkcji. Z tego punktu widzenia nie ma adnego powodu, by przecia operator, z wyjtkiem przypadku, gdy kod

88

Thinking in C++. Edycja polska zawierajcy odwoania do klasy bdzie dziki temu atwiejszy do napisania, a szczeglnie atwiejszy do przeczytania (pamitaj, e kod jest znacznie czciej czytany ni pisany). Jeeli ten przypadek nie zachodzi, nie warto zaprzta sobie tym uwagi. Innym czsto spotykanym podejciem do przeciania operatorw jest panika nagle okazao si, e operatory jzyka C utraciy swe dobrze poznane znaczenie. Wszystko si zmienio i cay mj kod, napisany w jzyku C, bdzie dziaa zupenie inaczej P' Nie jest to prawd. aden z operatorw, wykorzystywanych w wyraeniach zawierajcych jedynie wbudowane typu danych, nie mg ulec zmianie. Nie sposb nigdy przeciy w taki sposb operatorw, by wyraenie:
1 << 4;

zachowywao si odmiennie lub wyraenie:


1.414 << 2;

miao jakikolwiek sens. Jedynie wyraenia obejmujce typy zdefiniowane przez uytkownika mog zawiera przecione operatory.

Skadnia
Definicja przecionego operatora przypomina definicj funkcji, lecz nazwa tej funkcji ma posta operator, gdzie znak @ reprezentuje przeciany operator. Liczba argumentw, znajdujcych si na licie argumentw przecianego operatora, zaley od dwch czynnikw: . Czy jest to operator jednoargumentowy Qeden argument), czy te dwuargumentowy (dwa argumenty). 2. Czy operatorjest zdefmiowanyjako funkcja globalna Qeden argument w przypadku operatorajednoargumentowego, dwa argumenty w przypadku operatora dwuargumentowego), czy tejako funkcja skadowa (brak argumentu w przypadku operatorajednoargumentowego; jeden argument w przypadku operatora dwuargumentowego, obiekt staje si wwczas argumentem lewostronnym). Poniej przedstawiono niewielk klas prezentujc skadni zwizan z przecianiem operatorw:
/ / : C12:OperatorOverloadingSyntax.cpp #include <iostream> using namespace std: class Integer { int i; public:

Integer(int ii) : i(ii) {} const Integer operator+(const Integer& rv) const { cout << "operator+" << end1: return Integer(i + rv.i);

Rozdzia 2. * Przecianie operatorw

389

Integer& operator+=(const Integer& rv) { cout << "operator+=" << endl ; i += rv . i ; return *th1s;

int main() { cout << "typy wbudowane:" << endl; 1nt i = 1. j - 2, k - 3; k += i + j; cout << "typy zdefiniowane przez uzytkownika:" << endl; Integer ii(l). jj(2). kk(3); kk += ii + jj; Oba przecione operatory zdefiniowano jako funkcje skadowe inline, informujce o tym, e zostay wywoane. Pojedynczy argument jest tym, co pojawia si po prawej stronie operatora dwuargumentowego. Operatoryjednoargumentowe, definiowanejako funkcje skadowe, nie maj w ogle argumentw. Funkcja skadowa jest wywoywana dla obiektu znajdujcego si po lewej stronie operatora. W przypadku operatorw niebdcych operatorami warunkowymi (operatory warunkowe zwracaj zazwyczaj wartoci typu bool) niemal zawsze zwracany jest obiekt lub referencja do obiektu typu, na ktrym dokonuje si operacji, o ile typy argumentw s takie same jeeli nie s one identyczne, to interpretacja, co powinien zwraca operator, naley do programisty). Dziki temu mog by tworzone zoone wyraenia;
kk += ii + jj;

W powyszym przykadzie operator+ zwraca nowy (tymczasowy) obiekt typu Integer, ktry jest uywany jako argument rv funkcji operator+=. Obiekt tymczasowy jest niszczony, kiedy niejestju potrzebny.

Operatory, ktre mona przecia


Mimo e mona przecia niemal wszystkie operatory, dostpne wjzyku C, to proces ten podlega do cisym ograniczeniom. W szczeglnoci nie mona czy ze sob operatorw, ktre nie posiadaj znaczenia w jzyku C (np. tworzy operatora ** w celu oznaczenia potgowania), nie wolno zmienia kolejnoci obliczania operatorw, a take liczby wymaganych przez nie argumentw. Jest to logiczne wszelkie takie dziaania prowadziyby do utworzenia operatorw, ktre pogarszayby czytelno kodu, zamiastjpoprawia. Dwa nastpne podrozdziay prezentuj przykady wszystkich normalnych" operatorw, przecionych w taki sposb, wjaki najprawdopodobniej bd uywane.

90

Thinking in C++. Edycja polska

)peratory jednoargumentowe
Zamieszczony poniej przykadowy program przedstawia skadni, umoliwiajc przecienie wszystkich operatorwjednoargumentowych, zarwno w postaci funkcji globalnych (funkcji zaprzyjanionych, niebdcych funkcjami skadowymi), jak i funkcji skadowych. Stanowi one rozwinicie, przedstawionej uprzednio, klasy Integer oraz wprowadzaj now klas Byte. Znaczenie poszczeglnych operatorw bdzie zaleao od sposobu ich uycia; zanim jednak zrobi si co zaskakujcego, naley zawsze mie na uwadze klienta-programist. Poniej znajduje si katalog zawierajcy wszystkie funkcjejednoargumentowe:

//: C12:OverloadingUnaryOperators.cpp #include <iostream> using namespace std; // Funkcje niebdce funkcjami skadowymi: class Integer { long i; Integer* This() { return this; } public: Integer(long 11 - 0) : 1(11) {} // Brak skutkw ubocznych - argumenty s // referencjami do staych: friend const Integer& operator+(const Integer& a); friend const Integer operator-(const Integer& a): friend const Integer operator~(const Integer& a); friend Integer* operator&(Integer&a); friend int operator!(const Integer& a); // Wystpuj skutki uboczne - argumenty nie s // referencjami do staych: // Operator przedrostkowy: friend const Integer& operator++(Integer& a); // Operator przyrostkowy: friend const Integer operator++(Integer&a, int); // Operator przedrostkowy: friend const Integer& operator--(Integer& a); // Operator przyrostkowy: friend const Integer operator--(Integer&a. int); // Operatory globalne: const Integer& operator+(const Integer& a) { cout << "+Integer\n"; return a; // Jednoargumentowy operator + nic nie robi

Rozdzia 12. Przecianie operatorw

391

const Integer operator-(const Integer& a) { cout << "-Integer\n"; return Integer(-a.i); } const Integer operator-(const Integer& a) { cout << "~Integer\n"; return IntegerC~a.i); } Integer* operator&(Integer& a) { cout << "&Integer\n"; return a.This(); // &a jest rekurencyjne! } int operator!(const Integer& a) { cout << "!Integer\n"; return !a. i; } // Operator przedrostkowy - zwraca warto po inkrementacji: const Integer& operator++(Integer& a) { cout << "++Integer\n"; a.i ++; return a; } // Operator przyrostkowy - zwraca warto przed inkrementacja: const Integer operator++(Integer& a, int) { cout << "Integer++\n"; Integer before(a.i); a.i++; return before; } ' // Operator przedrostkowy - zwraca warto po dekrementacji: const Integer& operator--(Integer& a) { cout << "--Integer\n"; a. i - - ; return a; } // Operator przyrostkowy - zwraca warto przed dekrementacja: const Integer operator--(Integer& a, int) { cout << "Integer--\n"; Integer before(a.i); a.i--; return before; // Prezentacja dziaania przecia,zanych operatorw: void f(Integer a) { +a; -a; -a; Integer* ip = &a; !a; ++a; a++;
--a; a--;

192

Thinklng inC++. Edycja polska

// Funkcje skadowe (niejawny argument "this"): class Byte { unsigned char b; public: Byte(unsigned char bb - 0) : b(bb) {} // Brak skutkw ubocznych - stale funkcje skadowe: const Byte& operator+() const { cout << "+Byte\n"; return *this; } const Byte operator-() const { cout << "-Byte\n"; return Byte(-b); } const Byte operator-() const { cout ^< "~Byte\n"; return Byte(~b); } Byte operator!() const { cout << "!Byte\n"; return Byte(!b); } Byte* operator&() { cout << "&Byte\n"; return this; } // Wystpuj skutki uboczne - funkcje skadowe // nie bdce staymi: const Byte& operator++() { // Przedrostek cout << "++Byte\n"; b++; return *this; } const Byte operator++(int) { // Przyrostek cout << "Byte++\n"; Byte before(b); b++; return before: } const Byte& operator--() { // Przedrostek cout << "--Byte\n"; --b: return *this; } const Byte operator--(int) { // Przyrostek cout << "Byte--\n"; Byte before(b); --b;
return before:

void g(Byte b) { +b: -b; -b: Byte* bp = &b; !b;

Rozdzia U. Przecianie operatorw


++b; b++; --b; b--;

393

int main() { Integer a; f(a); Byte b; 9(b);

Funkcje zostay pogrupowane wedug sposobu przekazywania argumentw. Wskazwki dotyczce przekazywania i zwracania argumentw zostan przedstawione pniej. Przykady przedstawione powyej (oraz zawarte w nastpnym podrozdziale) ilustruj typowe sposoby uywania przecionych operatorw, wic mona potraktowa jejako wzorce podczas przeciania wasnych operatorw.

lnkrementacja i dekrementacja
Przecione operatory ++ oraz stanowi pewien problem, poniewa zamierzamy wywoa rne funkcje, w zalenoci od tego, czy wystpuj one przed (przedrostek), czy te za (przyrostek) obiektem, na ktrym dziaaj. Rozwizaniejest proste, cho niektrym wydaje si ono na pocztku nieco zagmatwane. Gdy kompilator widzi, na przykad, wyraenie ++a (preinkrementacja), generuje wywoanie funkcji operator++(a), natomiast widzc wyraenie a++ funkcji operator++(a, int). Oznacza to, e kompilator rozrnia te dwie postacie operatora, wywoujc rne przecione funkcje. W przypadku funkcji skadowych klasy, widocznych w pliku OverloadingUnaryOperators.cpp, kompilator widzc wyraenie ++b generuje wywoanie funkcji B::operator++( ), natomiast widzc wyraenie b++ wywouje funkcj B::operator(int). Uytkownik widzi jedynie, e dla przedrostkowych i przyrostkowych wersji operatorw wywoywane s rne funkcje. Jednak w rzeczywistoci wywoania obu tych funkcji posiadaj odmienne sygnatury, wic s poczone z dwoma zupenie rnymi ciaami funkcji. Kompilator przekazuje sztuczn, sta warto argumentu typu int (ktry nie posiada identyfikatora, poniewa jego warto nie jest nigdy wykorzystywana), by dla przyrostkowej wersji funkcji operatora utworzy inn sygnatur.

Operatory dwuargumentowe
Poniszy program stanowi powtrzenie zawartego w pliku OverloadingUnaryOperators.cpp przykadu przeciania operatorw jednoargumentowych, tym razem dla operatorw dwuargumentowych. Dziki temu jest ilustracj wykorzystania wszystkich operatorw, ktre mog by przeciane. Ponownie zostay one przedstawione zarwno w postaci funkcji globalnych, jak i funkcji skadowych.

Thinking in C++. Edycja polska

#ifndef INTEGER_H #define INTEGER_H

//: C12:Integer.h // Przecione operatory niebdce // funkcjami skadowymi

#include <iostream> // Funkcje niebdce funkcjami skadowymi: 'class Integer { long i; public: Integer(long 11 - 0) : i(ll) {} // Operatory tworzce nowa. zmodyfikowan warto: friend const Integer operator+(const Integer& left, const Integer& right); friend const Integer operator-(const Integer& left. const Integer& right); friend const Integer operator*(const Integer& left, const Integer& nght); friend const Integer operator/(const Integer& left, const Integer& right); friend const Integer operatorl(const Integer& left, const Integer& right); friend const Integer operator^(const Integer& left. const Integer& right); friend const Integer operator&(const Integer& left, const Integer& right); friend const Integer operator|(const Integer& left. const Integer& right); friend const Integer operator<<(const Integer& left. const Integer& right); friend const Integer operator>>(const Integer& left. const Integer& right);

const Integer& right): friend Integer& operator-=(Integer& left. const Integer& right); friend Integer& operator*=(Integer& left. const Integer& right); friend Integer& operator/=(Integer& left, const Integer& right); friend Integer& operator*=(Integer& left. const Integer& right);

// Przypisania modyfikujce i zwracajce l-warto: friend Integer& operator+=(Integer& left.

Rozdzia U. Przecianie operatorw friend Integer& operator^=(Integer& left,

395

#endif // INTEGER_H ///://: C12:Integer.cpp {0} // Implementacja przecionych operatorw #include "Integer.h" #include "../require.h" ' const Integer operator+(const Integer& left, const Integer& right) { return Integer(left.i + right.i); } const Integer operator-(const Integer& left. const Integer& right) {

};

const Integer& right); friend int operator<=(const Integer& left. const Integer& right); friend int operator>=(const Integer& left. const Integer& right); friend int operator&&(const Integer& left, const Integer& right); friend int operator||(const Integer& left, const Integer& right); // Zapis wartoci do strumienia wyjciowego: void print(std::ostream& os) const { os << i; }

// Operatory warunkowe, zwracajce wartoci true lub false: friend int operator==(const Integer& left. const Integer& right); friend int operator!=(const Integer& left. const Integer& right); friend int operator<(const Integer& left. const Integer& right); friend int operator>(const Integer& left.

const Integer& r1ght); friend Integer& operator&=(Integer& left, const Integer& right): friend Integer& operator|=(Integer& left, const Integer& right); friend Integer& operator>>=(Integer& left. const Integer& right): friend Integer& operator<<=(Integer& left, const Integer& right);

Thinking in C++. Edycja polska return Integer(left.i - right.i);

const Integer operator*(const Integer& left, const Integer& r1ght) { return Integer(left.1 * right.i); } const Integer operator/(const Integer& left, const Integer& r1ght) { require(right.i != 0, "dzielenie przez zero"); return Integer(left.i / right.i); const Integer operator^(const Integer& left. const Integer& right) { require(right.i !=0, "modulozero"); return Integer(left.i % right.i); const Integer operator^(const Integer& left. const Integer& right) {
return Integer(left.i ^ right.i);

const Integer operator&(const Integer& left, const Integer& right) {


return Integer(left.i & right.i);

// Przypisania modyfikujce i zwracajce l-warto: Integer& operator+~(Integer& left. const Integer& right) { if(&left == &right) {/* przypisanie do samego siebie */} left.i += right.i; return left; } Integer& operator-=(Integer& left. const Integer& right) { if(&left == &right) {/* przypisanie do samego siebie */} left.i -= right.i; return left;

const Integer operator|(const Integer& left. const Integer& right) { return Integer(left.i | right.i); } const Integer operator<<(const Integer& left, const Integer& right) { return Integer(left.i << right.i); } const Integer operator>>(const Integer& left, const Integer& right) { return Integer(left.i right.i); }

Rozdzia 32. Przecianie operatorw

Integer& operator*=(Integer& left. const Integer& right) { if(&left &rignt) {/* przypisanie do samego siebie */} left.i *= right.i; return left; } Integer& operator/=(Integer& left, const Integer& right) { require(right.i != 0. "dzieleme przez zero"); if(&left -= &right) {/* przypisanie do samego siebie */} left.i /= right.i; return left; } Integer& operatorfc=(Integer& left. const Integer& right) { require(right.i != 0. "modulo zero"); if(&left -- &right) {/* przypisanie do samego siebie */} left.i %- right.i; return left; } Integer& operator^=(Integer& left. const Integer& right) { if(&left == &right) {/* przypisanie do samego siebie */} left.i ^= right.i; return left; } Integer& operator&=(Integer& left. const Integer& right) { if(&left == &right) {/* przypisanie do samego siebie */} left.i &= right.i; return left; } Integer& operator|=(Integer& left. const Integer& right) { if(&left == &right) {/* przypisanie do samego siebie */} left.i |= right.i; return left; } Integer& operator>>=(Integer& left. const Integer& right) { if(&left &right) {/* przypisanie do samego siebie */} left.i - right.i; return left; } Integer& operator<<=(Integer& left. const Integer& right) { if(&left == &right) {/* przypisanie do samego siebie */} left.i <<= right.i; return left; ) // Operatory warunkowe, zwracajce wartoci true lub false: int operator==(const Integer& left. const Integer& right) { return left.i == right.i; } int operator!=(const Integer& left. const Integer& right) {

Thinklng in C++. Edycj

int operator>(const Integer& left.


}

return left.i !- right.i; } int operator<(const Integer& left. const Integer& r1ght) { return left.i < right.i; } const Integer& right) { return left.i > right.i;

int operator||(const Integer& left, const Integer& right) { return left.i | | right.i; } ///://: C12:IntegerTest.cpp //{L} Integer #include "Integer.h" #include <fstream> using namespace std; ofstream out("IntegerTest.out"); void h(Integer& cl. Integer& c2) { // Zoone wyraenie: cl += cl * c2 + c2 % cl:

intoperator>=(const Integer& left. const Integer& right) { return left.i > right.i; } int operator&&(const Integer& left, const Integer& right) { return left.i && right.i; }

int operator<=(const Integer& left, const Integer& right) { return left.i <- right.i; }

#define TRY(OP) \ out << "cl = "; cl.print(out); \ out << ". c2 = "; c2.print(out); \ out << "; cl " #OP " c2 daje "; \ (cl OP c2).print(out): \ out << endl;

TRYC(<) TRYC(>) TRYC(=-) TRYC(!-) TRYC(<-) TRYC(>-) TRYC(&&) TRYC(||)

// Operatory warunkowe: #define TRYC(OP) \ out << "cl "; cl.print(out); \ out << ". c2 = "; c2.print(out); \ out << "; cl " #OP " c2 daje "; \ out << (cl OP c2); \ out << endl:

TRY(+) T R Y ( - ) TRY(*) T R Y ( / ) TRY(SO TRY(^) TRY(&) TRY(|) TRY(<<) TRY() TRY(+-) TRY(-=) TRY(*-) TRY(/=0 TRYU-) TRY(^-) TRY(&=) TRY(|=) TRY(>>-) TRY(<<=)

Rozdzia 12. * Przecianie operatorw

399

int main() { out << "funkcje zaprzyjanione:" << endl; Integer cl(47). c2(9); h(cl. c2); } lll:~ li: C12:Byte.h // Przecione operatory bdce funkcjami skadowymi #ifndef BYTE_H #define BYTE_H #include "../require.h" #include <iostream> // Funkcje skadowe (niejawny argument "this"): class Byte { unsigned char b; public: Byte(unsigned char bb = 0) : b(bb) {} // Brak skutkw ubocznych - stale funkcje skadowe: const Byte operator+(const Byte& right) const { return Byte(b + right.b); }

const Byte

operator-(const Byte& right) const { return Byte(b - right.b);

} const Byte operator*(const Byte& right) const {

return Byte(b * right.b); } const Byte

operator/(const ByteS right) const { require(right.b != 0, "dzielenie przez zero"); return Byte(b / right.b);

} const Byte operator&(const Byte& right) const { require(right.b !- 0, "modulo zero"); return Byte(b % right.b); } const Byte operator^(const Byte& right) const { return Byte(b ^ right.b); } const Byte

operator&(const Byte& right) const { return Byte{b & right.b);

} const Byte operator|(const Byte& right) const { return Byte(b | right.b); } const Byte operator<<(const Byte& right) const { return Byte(b << right.b); } const Byte operator>>(const Byte& right) const {

Thinking in C++. Edycja polska return Byte(b right.b);

Byte& operator+=(const Byte& right) { if(this &right) {/* przypisanie do siebie samego */} b += right.b; return *this; } Byte& operator-=(const Byte& right) { if(this =- &right) {/* przypisanie do siebie samego */} b -= right.b; return *this; } Byte& operator*=(const Byte& right) { if(this &right) {/* przypisanie do siebie samego */} b *= right.b; return *this; } Byte& operator/-(const Byte& right) { require(right.b != 0. "dzielenie przez zero"); if(this = &right) {/* przypisanie do siebie samego */} b /= right.b; return *this; } Byte& operator*=(const Byte& right) { require(right.b != 0. "modulo zero"); if(this == &right) {/* przypisanie do siebie samego */} b *- right.b; return *this; } Byte& operator^=(const Byte& right) { if(this -" &right) {/* przypisanie do siebie samego */} b ^= right.b; return *this; } Byte& operator&=(const Byte& right) { if(this &right) {/* przypisanie do siebie samego */} b & right.b; return *this; } Byte& operator|=(const Byte& right) { if(this &right) {/* przypisanie do siebie samego */} b |= right.b; return *this; } Byte& operator>>-(const Byte& right) { if(this ~ &right) {/* przypisanie do siebie samego */} b = right.b; return *this;

// Przypisania modyfikujce i zwracajce l-warto: // operator= moe by tylko funkcj skadow: Byte& operator=(const Byte& right) { // Obsuga przypisania do siebie samego: if(this &right) return *this; b = right.b; return *this; }

Rozdzia 2. Przecianie operatorw Byte& operator<<=(const Byte& right) { if(th1s == &right) {/* przypisanie do siebie samego */} b <<- right.b; return *this; } // Operatory warunkowe, zwracajce wartoci true lub false: int operator==(const Byte& right) const { return b == right.b; }

401

int operator!=(const Byte& right) const { return b !- right.b; }


int operator<(const Byte& right) const { return b < right.b; } int operator>(const Byte& right) const { return b > right.b; } int operator<=(const Byte& right) const { return b <- right.b; }

int operator||(const Byte& right) const { return b | | right.b; } // Zapis wartoci do strumienia wyjciowego: void print(std::ostream& os) const { os << "Ox" << std::hex << int(b) << std::dec; #endif // BYTE_H ///:/ / : C12:ByteTest.cpp #include "Byte.h" #include <fstream> using namespace std; ofstream out("ByteTest.out");

int operator>=(const Byte& right) const { return b >= right.b; } int operator&&(const Byte& right) const { return b && right.b; }

void k(Byte& bl. Byte& b2) { bl = bl * b2 + b2 * bl; #define TRY2(OP) \ out << "bl "; bl.print(out); \ out << ". b2 = "; b2.print(out); \ out << "; bl " #OP " b2 daje "; \ (bl OP b2).print(out); \ out << endl ;
bl = 9; b2 = 47; TRY2(+) TRY2(-) TRY2(*) TRY2(/) TRY2U) TRY2(^) TRY2(&) TRY2(|) TRY2(<<) TRY2(>>) TRY2(+-) TRY2(-) TRY2(*=) TRY2(/-) TRY2(*=) TRY2(^=)

[Q2 TRY2(S=) TRY2(|=) TRY2(>>=) TRY2(<<-) TRY2(=) // Operator przypisania // Operatory warunkowe: #define TRYC2(OP) \ out << "bl - "; bl.print(out); \ out << ". b2 = "; b2.print(out); \

Thinking in C++. Edycja polska

out << "; bl " ifOP " b2 daje "; \ out << (bl OP b2); \ out << endl;

TRYC2(<) TRYC2(>) TRYC2(==) TRYC2(!=) TRYC2(<=) TRYC2(>=) TRYC2(&&) TRYC2(||)

bl - 9; b2 - 47;

// Przypisanie acuchowe: Byte b3 = 92; bl = b2 - b3; int main() { out << "funkcje skladowe:" << endl; Byte bl(47). b2(9); k(bl. b2); } IIIFunkcja operator= moe zatem wystpowa wycznie w postaci funkcji skadowej. Zostanie to wyjanione poniej. Zwr uwag na to, e wszystkie operatory przypisania zawieraj kod, ktry sprawdza, czy nie nastpuje przypisanie do samego siebie jest to wskazwka o charakterze oglnym. W pewnych sytuacjach sprawdzanie takie nie jest konieczne na przykad w przypadku funkcji operator+= czsto moemy celowo napisa po prostu A += A, dodajc zmienn A do samej siebie. Najwaniejszym miejscem, w ktrym naley sprawdzi, czy nie nastpuje przypisanie do samego siebie, jest operator=. Przypisanie takie mogoby bowiem powodowa dla obiektu katastrofalne skutki (w niektrych przypadkach to dopuszczalne, ale zawsze naley o tym pamita, tworzc operator=). Wszystkie operatory, przedstawione w poprzednich dwch przykadach, zostay przecione do obsugi pojedynczego typu danych. Moliwe jest rwnie takie przecienie operatorw, by obsugiway rne typy, co pozwala, na przykad, na dodawanie jabek do pomaracz. Zanim jednak rozpoczniesz przecianie operatorw, obejmujce wszystkie moliwe przypadki, powiniene przegldn podrozdzia dotyczcy automatycznej konwersji typw, znajdujcy si w dalszej czci rozdziau. Czsto waciwie umiejscowiona konwersja typw pozwala na unikniecie przeciania mnstwa operatorw.

Argumenty i zwracane wartoci


Rozmaite sposoby przekazywania i zwracania wartoci, widoczne podczas przegldania plikw OverloadingUnaryOperators,cpp, Integer.h oraz Byte.h, mog wydawa si? na pierwszy rzut oka niezrozumiae. Mimo e mona dowolnie przekazywa i zwraca

Rozdzia 12. Przecianie operatorw

403

wartoci, to sposoby, prezentowane w tych przykadach, nie zostay wybrane przypadkowo. Odpowiadaj one pewnemu logicznemu wzorowi, ktry zapewne zechcesz wykorzysta w wikszoci przypadkw: 1. Podobniejak wprzypadku kadego innego argumentu funkcji,jezeli bdziesz chcia tyIko odczytajego warto, nie zmieniajcjej, to domylnie powinien on zosta przekazanyjako referencja do staej. Zwyczajne operacje arytmetyczne ^ak +, - itd.) oraz logiczne nie zmieniaj wartoci swoich argumentw, wic zazwyczaj bdziesz przekazywaje w postaci referencji do staych. Gdy funkcja jest skadowklasy, odpowiada to uczynieniujej funkcjsta. Jedynie w przypadku operatorw poczonych z przypisaniem ^ak np. +=) oraz funkcji operator=, zmieniajcych argument znajdujcy si po lewej stronie, argument po lewej stronie niejest sta. Jednak poniewajest on modyfikowany, nadal przekazywanyjest w postaci adresu. 2. Typ wartoci zwracanej przez operator powinien zalee od spodziewanego znaczenia operatora (podobniejak w przypadku typw argumentw, rwnie typ zwracanej wartoci moe by dowolny). Jeeli na skutek dziaania operatora powstaje nowa warto, to abyjzwrci, trzeba utworzy nowy obiekt. Na przykad funkcja Integer::operator+ musi zwraca obiekt klasy Integer, bdcy sumswoich argumentw. Obiekt tenjest zwracany przez wartojako staa, wic rezultat operacji nie moe by modyfikowanyjako l-warto. 3. Wszystkie operatory przypisania modyfikuj l-warto. Aby rezultat operacji mg by uyty w wyraeniach acuchowych, takichjak a=b=c, mona si spodziewa, e zwrcona zostanie referencja do tej samej l-wartoci, wanie zmodyfikowana. Ale czy referencja ta powinna by referencjdo staej, czy te nie? Mimo e wyraenie a=b=cjest czytane od Iewej do prawej, to kompilator analizujeje od prawej do lewej. W celu zapewnienia poprawnego dziaania przypisania acuchowego nie istnieje wic konieczno zwracania wartoci, ktra nie byaby sta. Niektrzy uytkownicy oczekujjednak czasami moliwoci wykonania operacji na obiekcie, do ktrego wanie nastpio przypisanie, jak na przykad w przypadku instrukcji (a=b).func(), wywoujcej funkcj dla obiektu a, po przypisaniu mu wartoci b. Tak wic wartoci zwracane przez wszystkie operatory przypisania powinny by referencjami do l-wartoci, niebdcymi referencjami do staych. 4. Wszyscy spodziewaj si, e wynikiem dziaania operatorw logicznych bd przynajmniej liczby cakowite, a w idealnym przypadku wartoci typu bool (biblioteki, ktre zostay opracowane, zanim wikszo kompilatorwjzyka C++ zacza obsugiwa wbudowany typ bool, uyway wartoci cakowitych lub rwnowanych im wartoci, utworzonych za pomoc typedef). Operatory inkrementacji i dekrementacji stanowi problem z uwagi na to, e wystpuj one zarwno w wersji przedrostkowej, jak i przyrostkowej. Obie wersje zmieniaj warto obiektu, nie mona wic traktowa gojako staej. Wersja przedrostkowa zwraca warto obiektu po jego modyfikacji; mona si wic spodziewa, e zwraca ona z powrotem zmieniony obiekt. Dlatego te, w przypadku przedrostkowej wersji operatora, mona zwrci warto *this jako referencj. Natomiast jeli chodzi o przyrostkow wersj operatora, spodziewane jest zwrcenie wartoci obiektu, zanim jeszcze zostanie ona zmodyfikowana. Konieczne jest wic utworzenie oddzielnego obiektu,

Thinking in C++. Edycja polska

reprezentujcego t warto, i zwrcenie go. Tak wic w przypadku operatora przyrostkowego, dla zachowania spodziewanego znaczenia operacji, konieczne jest zwracanie obiektu przez warto (warto te wiedzie, e czasami wystpuj operatory inkrementacji i dekrementacji, zwracajce wartoci cakowite lub logiczne informuj one, na przykad, e obiekt poruszajcy si wzdu listy znajduje si na jej kocu). Pytanie brzmi wic czy powinny one by zwracane jako stae, czy te nie? Jeeli pozwolimy na modyfikacj obiektu i kto uyje instrukcji w postaci (++a).func( ), to funkcja func( ) bdzie dziaaa na obiekcie a, ale w razie zastosowania instrukcji (a++).func( ) funkcja func( ) dziaaa bdzie na tymczasowym obiekcie, zwrconym przez funkcj operatora przyrostkowego operator++. Obiekty tymczasowe s automatycznie staymi, co powinno by zasygnalizowane przez kompilator. Jednake w celu zapewnienia spjnoci bardziej logiczne wydaje si uczynienie ich w obu przypadkach staymi, jak w zamieszczonych powyej programach. Mona rwnie zdefiniowa wersj przedrostkow operatora w taki sposb, e nie bdzie on zwraca staej; zwrci natomiast sta w jego wersji przyrostkowej. Z uwagi na du liczb znacze, ktre mog zosta przypisane operatorom inkrementacji i dekrementacji, kady taki przypadek naley potraktowa indywidualnie.

Zwracanie przez warto statych


Zwracanie staej przez warto moe si pocztkowo wydawa troch dziwne, wymaga wic nieco obszerniejszego wyjanienia. Wemy pod uwag dwuargumentowy operator+. Jeeli zostanie on zastosowany w takich wyraeniach, jak f(a+b), to wynik dziaania a+b stanie si obiektem tymczasowym, uytym do wywoania funkcji f( ). Poniewa to obiekt tymczasowy, wic automatycznie jest on obiektem staym. A zatem nie ma adnego znaczenia, czy zwracana przez operator warto zostanie jawnie okrelonajako staa, czy te nie. Moliwe jest jednak rwnie wysanie komunikatu do wartoci zwracanej przez wyraenie a+b, a nie tylko przekazanie jej funkcji. Mona na przykad napisa (a+b).g( ), gdzie g( ) oznacza pewn funkcj skadow klasy Integer. Dziki uczynieniu wartoci zwracanej przez operator wartoci sta okrela si, e dla tej wartoci mog zosta wywoane jedynie stae funkcje skadowe klasy. Jest to poprawne z punktu widzenia staych, poniewa zapobiega umieszczeniu w obiekcie potencjalnie wartociowej informacji, ktra najprawdopodobniej zostaaby utracona.

Optymalizacja zwracania wartoci


Warto zwrci uwag na sposb, w jaki tworzone s obiekty zwracane przez warto. Na przykad w przypadku funkcji operator+:

return Integer(left.i + right.i):


Na pierwszy rzut oka moe to przypomina wywoanie funkcji konstruktora", ale w rzeczywistoci nim nie jest. Jest to skadnia obiektu tymczasowego powysza instrukcja gosi: utwrz tymczasowy obiekt klasy Integer i zwr go". Z uwagi na to mona odnie wraenie, e rezultatjest taki sam,jak w przypadku utworzenia lokalnego obiektu, o okrelonej nazwie, i zwrcenia go. Jestjednak zupenie inaczej. Gdyby, zamiast powyszej instrukcji, dokona zapisu:

Rozdzia 3L2. # Przecianie operatorw

405

Integer tmp(left.i + right.i); return tmp; to zostayby przeprowadzone trzy operacje. Po pierwsze, zostaby utworzony obiekt tmp, wcznie z wywoaniem jego konstruktora. Po drugie, konstruktor kopiujcy skopiowaby warto obiektu tmp do miejsca znajdujcego si na zewntrz funkcji, przeznaczonego na zwracan przez ni warto. Po trzecie, na kocu zasigu zostaby wywoany destruktor obiektu tmp. W odrnieniu od opisanego powyej schematu zwracanie wartoci tymczasowej" przebiega w zupenie inny sposb. Gdy kompilator widzi tak konstrukcj, to wie, e obiekt jest tworzony wycznie w celu jego zwrcenia. Kompilator wykorzystuje to, tworzc obiekt bezporednio w miejscu przeznaczonym na warto zwracan przez funkcj. Wymaga to tylko jednego, normalnego wywoania konstruktora (konstruktor kopiujcy nie jest w ogle potrzebny). Poza tym nie zachodzi konieczno wywoywania destruktora, poniewa w rzeczywistoci nigdy niejest w takim przypadku tworzony obiekt lokalny. Tak wic zastosowanie takiej metody zwracania wartoci nie wie si z adnymi kosztami (konieczne jest tylko zrozumienie tego mechanizmu przez programist), a jest znacznie bardziej efektywne. Dziaanie takie nosi nazw optymalizacji zwracania wartoci.

Nietypowe operatory
W przypadku kilku dodatkowych operatorw skadnia zwizana z przecianiem jest nieco odmienna. Funkcja operatora indeksowego, operator[ ], musi by funkcj skadow i wymaga jednego argumentu. Poniewa operator[ ] sugeruje, e obiekt, dla ktrego zosta wywoany, zachowuje sijak tablica, to operator ten zwraca zazwyczaj referencj, dziki czemu mona w wygodny sposb uy go po lewej stronie znaku przypisania. Przecianie tego operatorajest do powszechne odpowiednie przykady znajduj si w dalszej czci ksiki. Operatory new i delete zarzdzaj dynamicznym przydziaem pamici i mog by przeciane na szereg rnych sposobw. Temat tenjest omawiany w rozdziale 13.

Operator przecinkowy
Operator przecinkowy jest wywoywany wtedy, gdy znajduje si obok obiektu o typie, dla ktrego operator ten zosta zdefiniowany. Jednak operator," niejest wywoywany w przypadku listy argumentw funkcji, a tylko w stosunku do obiektw, ktre s zwykle widoczne i oddzielone od siebie przecinkiem. Nie wydaje si, by operator ten mg miejakie szersze zastosowanie jest on dostpny dla zachowania spjnoci jzyka. Poniszy przykad prezentuje, w jaki sposb wywoywana jest funkcja operatora, zarwno wtedy, gdy przecinek znajduje si przed obiektem, jak i za nim:
//: C12:OverloadingOperatorComma.cpp #include <iostream> using namespace std;

06 class After { publ i c : const After& operator.(const After&) const { cout << "After::operator,O" << endl; return *th1s;

Thinking in C++. Edycja polska

class Before {}; Before& operator,(int, BeforeS b) { cout << "Before::operator,O" << endl; return b;
int main() { After a. b; a, b; // Wywoywany operator przecinkowy

Before c; 1, c; // Wywoywany operator przecinkowy Funkcja globalna pozwala na umieszczenie przecinka przed obiektem, ktrego dotyczy operator. Zaprezentowany tu sposb zastosowania operatora jest do niejasny i kontrowersyjny. Mimo e w charakterze skadnikw bardziej zoonych wyrae uywa si oddzielonych przecinkami list, to w wikszoci przypadkw operator ten ma zbyt mgliste znaczenie, aby go stosowa.

Dperator ->
Zwykle operator-> jest uywany wwczas, gdy chcemy, by obiekt wyglda tak, jakby by wskanikiem. Z uwagi na to, e obiekty takie odznaczaj si wiksz inteligencj" ni zwyke wskaniki, czsto nazywa si je inteligentnymi wskanikami. S one szczeglnie przydatne, gdy zamierzamy opakowa" wskanik klas, by uczyni go bezpieczniejszym. Powszechnie stosuje si je w charakterze iteratora, czyli obiektu poruszajcego si w obrbie kolekcji lub kontenera innych obiektw, udostpniajcego za kadym razem pojedynczy obiekt, ale niezapewniajcego bezporedniego dostpu do implementacji kontenera (kontenery i iteratory mona czsto spotka w bibliotekach klas, takich jak standardowa biblioteka jzyka C++, opisana w drugim tomie ksiki). Operator wyuskania wskanika musi by funkcj skadow klasy. Obowizuje w tym przypadku dodatkowe, nietypowe ograniczenie musi on zwraca obiekt (albo referencj do obiektu), rwnie posiadajcy operator wyuskania wskanika, albo zwraca wskanik, ktry moe zosta uyty do wybrania tego, co wskazuje strzaka operatora. Poniej znajduje si prosty przykad uycia operatora:
/ / : C12:SmartPointer.cpp #include <iostream> #include <vector> #include ". ./require.h" using namespace std;

Rozdzia 2. Przecianie operatorw

407

class Obj { static 1nt 1, j; public: void f() const { cout << i++ << endl; } void g() const { cout << j++ << endl; } // Definicje skadowych statycznych: int Obj::i - 47; int Obj::j - 11; // Kontener: class ObjContainer { vector<Obj*> a: public:

friend class SmartPointer;

void add(Obj* obj) { a.push_back(obj);

class SmartPointer { ObjContainer& oc; int index; public: SmartPointer(ObjContainer& objc) : oc(objc) { index = 0; }
// Zwracana warto sygnalizuje koniec listy: bool operator++() { // Przedrostek if(index >= oc.a.sizeO) return false; if(oc.a[++index] == 0) return false; return true; }

bool operator++(int) { // Przyrostek return operator++(); // Uycie wersji przedrostkowej } Obj* operator->() const { return oc.a[index];

require(oc.a[index] ! = 0 , "SmartPointer::operator->0 "zwrocil wartosc zerowa");

int main() { const int sz - 10; Obj o[sz]; ObjContainer oc; for(int i = 0; i < sz; i++) oc.add(&o[i]); // Wypenianie kontenera SmartPointer sp(oc); // Utworzenie iteratora do { sp->f(); // Wywoanie operatora wyuskania wskanika sp->g(); } while(sp++);

Thinking in C++. Edycja polska

Klasa Obj definiuje obiekt, na ktrym dokonywane s w tym programie operacje. Funkcje f() oraz g() wyprowadzaj interesujce wartoci, wykorzystujc statyczne dane skadowe. Wskaniki tych obiektw s zapamitywane w kontenerze typu ObjContainer za pomoc funkcji add(). Kontener ObjContainer wyglda jak tablica wskanikw, ale, jak wida, nie ma sposobu na usunicie z niej wskanikw. Klasa SmartPointer zostaa jednak zadeklarowana jako klasa zaprzyjaniona, dziki czemu ma ona dostp do wntrza kontenera. Klasa SmartPointer przypomina inteligentny wskanik mona przesuwa go do przodu, uywajc operatora ++ (nic nie stoi na przeszkodzie, aby zdefiniowa rwnie operator--), nie wychodzi on poza zakres wskazywanego kontenera i zwraca (za pomoc operacji wyuskania wskanika) wskazywan warto. Zwr uwag na to, e SmartPointer jest specjalnie przystosowany do obsugi kontenera, dla ktrego zosta utworzony w odrnieniu od zwykych wskanikw, nie istniej inteligentne wskaniki oglnego przeznaczenia". Wicej informacji dotyczcych inteligentnych wskanikw mona znale w ostatnim rozdziale ksiki oraz w jej drugim tomie (ktry mona pobra z witryny www.BruceEckel.com). Po wypenieniu kontenera oc obiektami klasy Obj w funkcji main( ) jest tworzony inteligentny wskanik sp klasy SmartPointer. Wywoania inteligentnego wskanika wystpuj w wyraeniach:
sp->f(); // Wywoania inteligentnegowskanika sp->g();

Mimo e obiekt sp nie posiada w rzeczywistoci funkcji skadowych f() oraz g(), operator wyuskania wskanika automatycznie wywouje te funkcje dla wskanika do obiektu klasy Obj, zwracanego przez funkcj SmartPointer::operator->. Kompilator dokonuje wszelkich niezbdnych sprawdze, by upewni si, e wywoania funkcji dziaajpoprawnie. Chocia wewntrzny mechanizm dziaania operatora wyuskania wskanika jest bardziej zoony ni innych operatorw, to jego cel jest identyczny udostpnienie uytkownikom klasy wygodniejszej skadni.

Zagniedony iterator
Czciej spotyka si klas inteligentnego wskanika" lub iteratora" w postaci zagniedonej wewntrz obsugiwanej przez ni klasy. Mona napisa ponownie poprzedni program, zagniedajc klas SmartPointer wewntrz klasy ObjContainer, jak w poniszym przykadzie:
/ / : C12:NestedSmartPointer.cpp #include <iostream> #include <vector> #include ".,/require.h" using namespace std:

class Obj { static int i. j: public: void f() { cout << i++ << endl; } void g() { cout << j++ << endl; }

Rozdzia 2. Przecianie operatorw // Definicje skadowych statycznych: int Obj::i - 47; int Obj::j = 11; // Kontener:

409

class ObjContainer { vector<Obj*> a; public: void add(Obj* obj) { a.push_back(obj): } class SmartPointer; friend SmartPointer; class SmartPointer { ObjContainer& oc; unsigned int index; public: SmartPointer(ObjContainer& objc) : oc(objc) { index = 0; } // Zwracana warto sygnalizuje koniec listy: bool operator++() { // Przedrostek
if(index >= oc.a.sizeO) return false; if(oc.a[++index] == 0) return false; return true;

bool operator++(int) { // Przyrostek return operator++(); // Uycie wersji przedrostkowej } Obj* operator->() const {

require(oc.a[index] ! = 0 , "SmartPointer::operator->O " "zwrocil wartosc zerowa"); return oc.a[index];

// Funkcja tworzca inteligentny wskanik // wskazujcy pocztek obiektu klasy ObjContainer: SmartPointer begin() { return SmartPointer(*this);

int main() { const int sz = 10; Obj o[sz]; ObjContainer oc; for(int i = 0; i < sz; i++) oc.add(&o[i]); // Wypenianie kontenera ObjContainer::SmartPointer sp = oc.begin(); do { sp->f(); // Wywoanie operatora wyuskania wskanika sp->g(); } while(++sp);
Oprcz tego, e klasa iteratora jest obecnie zagniedona w stosunku do poprzedniej wersji programu, wystpuj tutaj tylko dwie rnice. Pierwsz z nich jest deklaracja klasy iteratora, dziki ktrej moe ona zosta wskazanajako zaprzyjaniona:

HO
class SmartPointer; friend SmartPointer;

Thinking in C++. Edycja polska

Zanim klasa zostanie okrelonajako zaprzyjaniona, kompilator musi wiedzie o tym, e w ogle istnieje. Drug rnic stanowi funkcja begin(), bdca skadow klasy ObjContainer. Zwraca ona obiekt klasy SmartPointer, wskazujcy pocztek zawartej w kontenerze sekwencji. Mimo e jest to naprawd tylko kwesti wygody, korzy wynikajca ze zdefiniowania takiej funkcji polega na tym, e naladuje ona w pewnym stopniu konwencj stosowan w standardowej bibliotece C++.

Operator->*
Operator ->* jest operatorem dwuargumentowym, dziaajcym podobnie, jak inne operatory dwuargumentowe. Zosta on udostpniony z myl o sytuacjach, w ktrych wymagane jest naladowanie zachowania wbudowanej skadni wskanika do skadowej, opisanego w poprzednim rozdziale. Podobnie jak operator->, operator wyuskania wskanika do skadowej jest na og uywany w stosunku do obiektw bdcych inteligentnymi wskanikami". Przedstawiony niej przykad bdzie jednak prostszy, aby atwiej go zrozumie. Trik zwizany z definicj funkcji operator->* polega na tym, e musi ona zwraca obiekt, dla ktrego mona wywoa funkcj operator() z argumentami przeznaczonymi dla wywoywanej funkcji skadowej. Operator wywolaniafunkcji operator() musi by funkcj skadow i jest on jedyny w swoim rodzaju, poniewa dopuszcza dowoln liczb argumentw. Z tego powodu obiekt wyglda tak, jakby by w rzeczywistoci funkcj. Mimo e mona zdefiniowa szereg przecionych funkcji operator(), posiadajcych rne argumenty, s one czsto uywane do typw realizujcych tylko jedn operacj albo przynajmniej posiadajcych operacj o charakterze dominujcym. W drugim tomie ksiki mona pozna sposb, w jaki standardowa biblioteka jzyka C++ wykorzystuje operator wywoania funkcji do tworzenia obiektw funkcyjnych". Aby utworzy operator->*, trzeba najpierw utworzy klas zawierajc operator( ) o typie obiektu zwracanego przez operator->*, Klasa ta musi w jaki sposb zdoby niezbdne informacje, dziki ktrym w czasie wywoania funkcji operator() (co odbywa si automatycznie) wskanik do skadowej zostanie wyuskany dla tego obiektu. W zamieszczonym poniej przykadzie konstruktor FunctionObject pobiera i zapamituje zarwno wskanik obiektu, jak i wskanik do funkcji skadowej, ktre s wykorzystywane przez operator() do wywoania waciwego wskanika do funkcji skadowej:
/ / : C12:PointerToMemberOperator.cpp #include <iostream> using namespace std;
class Dog { public: int run(int i) const {

Rozdzia 2. * Przecianie operatorw

411

cout << "biega\n"; return i;

} int eat(int i) const { cout << "je\n"; return i; } int sleep(int i) const { cout << "spi\n"; return i; } typedef int (Dog::*PMF)(int) const; // operator->* musi zwrci obiekt // posiadajcy operator(): class FunctionObject { Dog* ptr; PMF pmem; public: // Zapamitanie wskanika obiektu i wskanika skadowej FunctionObject(Dog* wp, PMF pmf) : ptr(wp), pmem(pmf) { cout << "Konstruktor FunctionObject\n"; } // Wywoanie, wykorzystuja.ee wskanik obiektu // i wskanik skadowej int operator()(int i) const { cout << "FunctionObject::operator()\n"; return (ptr->*pmem)(i); // Wywoanie
FunctionObject operator->*(PMF pmf) cout << "operator->*" << endl; return FunctionObject(this. pmf);

int main() { Dog w; Dog: :PMF pmf = &Dog: :run; cout << (w->*pmf)(l) << endl; pmf = &Dog: :sleep; cout << (w->*pmf)(2) << endl; pmf = &Dog: :eat: cout << (w->*pmf)(3) << endl;
Klasa Dog zawiera trzy funkcje skadowe, z ktrych kada pobiera jako argument i zwraca liczb cakowit. PMF jest typem zdefiniowanym za pomoc deklaracji typedef w celu uproszczenia definiowania wskanikw do funkcji skadowych klasy Dog. Obiekt klasy FunctionObject jest tworzony i zwracany przez operator->*. Zwr uwag na to, e operator->* wie, dla jakiego obiektu zosta wywoany wskanik do funkcji skadowej (this), a take jaki to jest wskanik. Przekazuje wic te wartoci konstruktorowi klasy FunctionObject, ktry je zapamituje. Gdy wywoywany jest operator->*, kompilator reaguje nagym zwrotem, wywoujc operator( ) dIa wartoci

Thinking in C++. Edycja polska

zwracanej przez operator->* i przekazujc mu wszystkie argumenty, ktre zostay podane operatorowi ->*. Funkcja FunctionObject::operator( ) pobiera te argumenty, a nastpnie wyuskuje prawdziwy" wskanik do skadowej, wykorzystujc zapamitany wskanik obiektu oraz wskanik do skadowej. Zwr uwag na to, e podobnie jak w przypadku operatora -> wchodzimy niejako w rodek" wywoania funkcji operator->*. Pozwala to na dokonanie jakich dodatkowych operacji, gdyby zasza taka potrzeba. Zaimplementowany w programie mechanizm operatora ->* dziaa tylko w przypadku funkcji skadowych, ktre pobieraj argument bdcy liczb cakowit i zwracaj liczb cakowit. Stanowi to pewne ograniczenie, ale prba utworzenia przecionych mechanizmw, obejmujcych kad dopuszczaln moliwo, wydaje si niemal niewykonalna. Na szczcie, dostpne w jzyku C++ szablony (opisane w ostatnim rozdziale ksiki oraz w jej drugim tomie) zostay zaprojektowane wanie z myl o takich problemach.

Operatory, ktrych nie mona przecia


Niektre operatory, znajdujce si w zbiorze wszystkich dostpnych operatorw, nie mog by przeciane. Ograniczenia te wynikaj na og ze wzgldw bezpieczestwa. Gdyby przecianie operatorw byo dozwolone, mogoby to spowodowa naraenie na szwank, a nawet naruszenie mechanizmw bezpieczestwa, wprowadzioby utrudnienia albo byoby sprzeczne z przyjt praktyk. 4 Operator wyboru skadowej (.). Obecnie kropka posiada znaczenie zdefiniowane dla wszystkich skadowych klasy. Gdyby pozwoli najej przecianie, to nie mona by byo odwoywa si do skadowych klasy w zwyky sposb naleaoby uywa do tego celu wskanika oraz operatora ->. 4 Operator wyuskania wskanika do skadowej (,*) z tych samych powodw, co w przypadku operatora wyboru skadowej. 4 Nie istnieje operator potgowania. Najbardziej popularnpropozycjby dla niego pochodzcy z Fortranu operator**, ale powodowa on problemy zwizane z analiz skadniow. Poniewa w jzyku C nie ma operatora potgowania, wic rwnie jego obecno w jzyku C++ nie wydaje si konieczna tym bardziej, e zawsze mona zastpi go wywoaniem funkcji. Operator potgowania umoliwiaby wygodny zapis, ale nie dodawaby dojzyka takich nowych cech, dla ktrych warto by wprowadza dodatkowe utrudnienia do procesu kompilacji. 4 Nie ma operatorw definiowanych przez uytkownika. Oznacza to, e nie mona samodzielnie tworzy nowych operatorw takich, ktrych nie ma jeszcze wjzyku. Wynika to czciowo z problemw dotyczcych sposobu, wjaki miayby zosta zdefiniowane priorytety, a czciowo z braku uzasadnienia dla nieuchronnie z tym zwizanych kopotw. 4 Nie mona zmienia regu dotyczcych priorytetw operatorw; sju i tak wystarczajco trudne do zapamitania.

Rozdzia 2. Przecianie operatorw

413

Operatory niebdce skadowymi


W niektrych z poprzednich przykadw operatory mogy by zarwno funkcjami skadowymi klas,jak i zwykymi funkcjami; wydawao si, e nie sprawia to wikszej rnicy. Mona zatem zada pytanie: Ktry ze sposobw wybra?". Na og, jeeli nie ma to znaczenia, powinny by one funkcjami skadowymi dla podkrelenia zwizku pomidzy operatorem i jego klas. Sprawdza si to bardzo dobrze, dopki argument znajdujcy si po lewej stronie operatorajest obiektem danej klasy. Czasami zachodzi jednak potrzeba, by argument znajdujcy si po lewej stronie operatora by obiektem jakiej innej klasy. Typowym tego przykadem jest przecianie operatorw << i dla operacji zwizanych ze strumieniami wejcia-wyjcia. Poniewa strumienie wejcia-wyjcia s jedn z podstawowych bibliotek jzyka C++, to prawdopodobnie zamierzasz przeciy te operatory w wikszoci z tworzonych przez siebie klas, a wic proces ten wart jest zapamitania: / / : C12 : IostreamOperatorOverloadi ng . cpp // Przykady przecionych operatorw, niebdcych // funkcjami skadowymi #include ". ./require.h" #include <iostream> #include <sstream> // "Strumienie acuchowe" #include <cstring> using namespace std; class IntArray { enum { sz - 5 }; int i[sz]; public: IntArray() { memset(i. 0. sz* sizeof(*i)); } int& operator[](int x) { require(x >= 0 && x < sz. "IntArray::operator[] poza zakresem"); return i[x]; }

, :

friend ostream& operator<<(ostream& os. const IntArray& ia); friend istream& operator>>(istream& is. IntArray& ia);

ostream& operator<<(ostream& os. const IntArray& ia) for(int j ^ 0; j < ia.sz; j++) { os << ia.i[j]; if(j != ia.sz -1) OS << ". "; } os << endl ; return os;

istream& operator>>(istream& is. IntArray& ia){ for(int j = 0; j < ia.sz; j++)

414

Thinking in C++. Edycja polska

1s 1a.1[j]; return is; 1nt main() { stringstream input("47 34 56 92 103"); IntArray I; input I; I[4] = -1; // Wykorzystanie przecionego operatora [] cout << I; } IIIKlasa ta zawiera rwnie przeciony operator[ ], zwracajcy referencj do odpowiedniej wartoci zawartej w tablicy. Poniewa zwracanajest referencja, wyraenie:
I[4] = -1;

nie tylko sprawia wraenie bardziej cywilizowanego" ni w przypadku uycia wskanikw, ale wywouje rwnie podany skutek. Wane jest, by przecione operatory przesuni pobieray i zwracay wartoci przez referencje, dziki czemu operacje wywr wpyw na obiekty zewntrzne. W definicjach funkcji wyraenia takie, jak:
os << ia.i[j];

powodujwywoanie istniejcych funkcji przecionych operatorw (tych, ktre zdefiniowano w pliku nagwkowym <iostream>). W powyszym przypadku zostanie wywoana funkcja ostream& operator<<(ostream&, int), poniewa ia.iFj] zwraca warto bdc liczb cakowit. Za kadym razem, gdy dokonywane s operacje na klasach istream lub ostream, ich wartoci s zwracane. Dziki temu monaje wykorzysta w bardziej zoonych operacjach. W funkcji main() zosta wykorzystany nowy typ strumienia wejcia-wyjcia klasa stringstream (zadeklarowana w pliku nagwkowym <sstream>). Pobiera ona acuch (ktory,jak wida powyej, moe zosta utworzony na podstawie tablicy znakowej) i przeksztaca go w strumie wejcia-wyjcia. W powyszym przykadzie oznacza to, e operatory przesuni mog by testowane bez otwierania plikw ani wpisywania danych w wierszu polece. Przedstawiona w powyszym przykadzie posta operatorw << oraz jest standardowa. Jeeli zamierzasz utworzy takie operatory dla swojej klasy, skopiuj sygnatury powyszych funkcji i zwracane przez nie typy wartoci, a nastpnie przyjmij za wzr ciaa tych funkcji.

Podstawowe wskazwki
Murray' zaproponowa, podane poniej, wskazwki, umoliwiajce podjcie decyzji o wyborze pomidzy funkcjami skadowymi klasy i funkcjami niebdcymi funkcjami skadowymi:
1

Rob Murray, C++ Strategies & Tactics, Addison-Wesley, 1993 r., s. 47.

Rozdzia V2. Przecianie operatorw Operator Wszystkie operatoryjednoargumentowe = ( ) [ ] -> ->* += -= |= *= ^= &= |= %= = <<= Wszystkie pozostae operatory dwuargumentowe Zalecany sposb uycia funkcje skadowe musz by funkcjami skadowymi funkcje skadowe

415

funkcje niebdce funkcjami skadowymi

Przecianie operacji przypisania


Dla pocztkujcych programistwjzyka C++ staym rdem utrapiejest instrukcja przypisania. Nic dziwnego, poniewa znak = jest fundamentaln operacj w programowaniu, sigajc kopiowania rejestrw na poziomie maszynowym. Ponadto gdy uyty zostanie znak =, to czasami wywoywany jest rwnie konstruktor kopiujcy (opisany w rozdziale 1 1.):
MojTyp b; MojTyp a - b; a = b;

W drugim wierszu definiowany jest obiekt a. Tworzony jest obiekt, ktrego wczeniej nie byo. Poniewa wiemyjuz,jak skrupulatnyjest kompilatorjzyka C++ w sprawie inicjalizacji obiektw, nie ulega wtpliwoci, e zawsze w miejscu, w ktrym tworzony jest obiekt, musi by wywoywany konstruktor. Ale ktry? Obiekt a jest tworzony na podstawie istniejcego ju obiektu klasy MojTyp (obiektu b, znajdujcego si po prawej stronie znaku rwnoci), wic nie ma w tym przypadku wyboru konstruktorem tym musi by konstruktor kopiujcy. Mimo uycia znaku rwnoci wywoywanyjest zatem konstruktor kopiujcy. Sprawa wyglda inaczej w przypadku trzeciego wiersza. Po lewej stronie znaku rwnoci znajduje si wczeniej zainicjowany obiekt. Oczywicie, w stosunku do ju utworzonego obiektu nie jest wywoywany konstruktor kopiujcy. W tym przypadku w stosunku do obiektu ajest wywoywana funkcja MojTyp::operator=, pobierajca w charakterze argumentu to, co znajduje si po prawej stronie znaku rwnoci (mona zdefiniowa kilka funkcji operator=, przyjmujcych rne typy argumentw znajdujcych si po prawej stronie przypisania). Opisane powyej zachowanie nie ogranicza si do konstruktora kopiujcego. Za kadym razem, gdy obiekt jest inicjalizowany za pomoc znaku =, zamiast konstruktora, wywoanego normalnie w postaci funkcji, kompilator poszukuje konstruktora, ktry przyjmujejako argument to, co znajduje si po prawej stronie znaku rwnoci: / / : C12:CopyingVsInitialization.cpp class F1 { public: {} class Fee public:

,16

Thinking in C++. Edycja polska

Fee(int) {} Fee(const Fi&) {} int main() { Fee fee = 1; // Fee(int) Fi fi; Fee fum = fi; // Fee(Fi)
Naley pamita o tym rozrnieniu, dotyczcym znaku =: jeeli obiekt nie zosta jeszcze utworzony, to koniecznajestjego inicjalizacja; w przeciwnym przypadku wykorzystywany jest operator=, stanowicy przypisanie. Jeszcze lepiej jest unika pisania kodu, wykorzystujcego znak = do inicjalizacji i zawsze uywa zamiast niego jawnego wywoania konstruktora. W ten sposb dwa konstruktory, wywoywane w powyszym programie za pomoc znaku rwnoci, uzyskaj posta: Fee fee(l); Fee fum(fi): Dziki temu uniknie si wprowadzania w bd osb czytajcych kod programu.

Zachowanie si operatora =
W plikach Integer.h oraz Byte.h funkcja operator= moe by wycznie funkcj skadow. Jest ona bezporednio zwizana z obiektem znajdujcym si po lewej stronie znaku =". Gdyby istniaa moliwo zdefiniowania globalnej funkcji operator=, mona by zdefiniowa ponownie wbudowane w jzyk znaczenie tego operatora: int operator-(int. MojTyp): // Globalna definicja - nie jest dozwolona! Kompilator omija t kwesti, wymuszajc definicj funkcji operator= jako funkcji skadowej. Podczas tworzenia operatora = konieczne jest skopiowanie wszystkich niezbdnych informacji z obiektu znajdujcego si po prawej stronie do biecego obiektu (czyli tego, dla ktrego zostaa wywoana funkcja operator=) w celu wykonania tego, co uwaamy w naszej klasie za przypisanie". W przypadku prostych obiektw to oczywiste: //: C12:SimpleAssignment.cpp // Prosty operator=() finclude <iostream> using namespace std: class Value { int a. b; float c; public: Value(int aa - 0. int bb = 0. float cc = 0 . 0 ) : a(aa). b(bb). c(cc) {} Value& operator=(const Value& rv) {

Rozdzia 2. Przecianie operatorw


a - rv.a; b = rv.b; c - rv.c; }

417

return *this;

friend ostream& operator<<(ostream& os. const Value& rv) { return os << "a - " << rv.a << ", b - " << rv.b << ", c = " << rv.c;

int main() { Value a, b(l. 2, 3.3); cout << "a: " << a << endl; cout << "b: " << b << endl; cout << "a po przypisaniu: " << a << endl; W powyszym przykadzie obiekt znajdujcy si po lewej stronie znaku = kopiuje wszystkie elementy obiektu znajdujcego si po prawej stronie znaku, a nastpnie zwraca referencj do samego siebie, co pozwala na tworzenie bardziej zoonych wyrae. Przykad ten zawiera czsto spotykany bd. Gdy dokonuje si operacji przypisania w stosunku do dwch obiektw tego samego typu, naley zawsze najpierw sprawdzi, czy nie przypisuje si obiektu do samego siebie. W pewnych przypadkach, takich jak przedstawiony powyej, wykonanie takiej operacji jest nieszkodliwe, ale w razie dokonania zmian w implementacji klasy moe sta si istotne. Jeeli nie bdzie si rutynowo sprawdza, czy nie nastpuje przypisanie obiektu do samego siebie, to mona w kocu o tym zapomnie, wprowadzajc do programu trudne do wykrycia bdy.
a = b;

riVskazniki zawarte w klasach


Co si dzieje, gdy obiekty nie s tak proste? Na przykad gdy zawieraj one wskaniki do innych obiektw? Zwyke kopiowanie doprowadzi do tego, e dwa obiekty bd wskazyway ten sam obszar pamici. W takich sytuacjach trzeba radzi sobie samemu. Stosowane s dwa podejcia do tego problemu. Prostsza technika polega na kopiowaniu wszystkiego, co jest wskazywane przez wskaniki podczas przypisania Iub wywoania konstruktora kopiujcego. To do proste: //: C12:CopyingWithPointers.cpp // Rozwizanie problemu utosamiania wskanikw // za pomoca. kopiowania wskazywanych przez nie // obszarw podczas przypisania i wywoania // konstruktora kopiujcego #include "../require.h"

#include <string> #include <iostream> using namespace std; class Dog { string nm;

Thinking in C++. Edycja polska

public: Dog(const string& name) : ntn(name) { cout << "Tworzenie psa: " << *this << endl; } // Utworzony przez kompilator konstruktor kopiujcy // i operator= s prawidowe Dog(const Dog* dp, const string& msg) : nm(dp->nm + msg) { cout << "Pies " << *this << " skopiowany z " << *dp << endl;

// Utworzenie obiektu klasy Dog na podstawie wskanika:

~Dog() { cout << "Usunity pies: " << *this << endl; } void rename(const string& newName) { nm = newName; cout << "Pies ztnienil imie na: " << *this << endl; } friend ostream& operator<<(ostream& os. const Dog& d) { return os << " [ " << d.nm << "]";

class DogHouse { Dog* p; string houseName; public: DogHouse(Dog* dog, const string& house) : p(dog), houseName(house) {} DogHouse(const DogHouse& dh) : p(new Oog(dh.p, " skopiowany konstruktorem")). houseName(dh.houseName + " skopiowany konstruktorem") {} DogHouse& operator=(const DogHouse& dh) { // Sprawdzanie przypisania do samego siebie: if(&dh != this) ( p = new Dog(dh.p, " przypisany"); houseName - dh.houseName + " przypisany"; } return *this; } void renameHouse(const string& newName) { houseName - newName; } Dog* getDog() const { return p; } ~DogHouse() { delete p; } friend ostream& operator<<(ostream& os, const DogHouse& dh) { return os << "[" << dh.houseName << "] zawiera " << *dh.p;

int main() { DogHouse fafika(new Dog("Fafik"), "DomekFafika");

Rozdzia 12. Przecianie operatorw cout << fafika << endl; DogHouse fafika2 = fafika; // Uycie konstruktora kopiujcego cout << fafika2 << endl; fafika2.getDog()->rename("Burek"): fafika2.renameHouse("DomekBurka"): cout << fafika2 << endl; fafika - fafika2; // Przypisanie cout << fafika << endl ; fafika.getDog()->rename("Reks"); fafika2.renameHouse("DomekReksa"); } lll:~

419

Klasa Dog (pies) jest prost klas, zawierajcjedynie acuch, przechowujcy imi psa. Jednake na ogl wiadomo, gdy co dzieje si z obiektem tej klasy, poniewa zarwno konstruktor, jak i destruktor wywietlaj informacj o tym, e zostay wywoane. Zwr uwag na to, e drugi konstruktor jest nieco podobny do konstruktora kopiujcego, z wyjtkiem tego, e pobiera wskanik do obiektu klasy Dog, a nie referencj. Ponadto posiada drugi argument, bdcy komunikatem doczanym do imienia psa, ktrego wskanik stanowi pierwszy argument. Pomaga to w przeledzeniu dziaania programu. A zatem ilekro funkcja skadowa drukuje informacj, nie wykonuje tego bezporednio, tylko przesya warto *this do strumienia cout. To z kolei powoduje wywoanie funkcji operator<<, przekazujcej tekst do strumienia ostream. Warto robi to w taki sposb, poniewa jeli bdzie potrzebna modyfikacja formatu wywietlania informacji o obiekcie klasy Dog (w przykadzie zostay dodane znaki [" i ]"), to trzeba zmieni go tylko wjednym miejscu. Klasa DogHouse (psia buda2) zawiera skadow typu Dog* oraz prezentuje cztery funkcje, ktre zawsze trzeba zdefiniowa, gdy klasa zawiera wskaniki: wszystkie niezbdne zwyczajne konstruktory, konstruktor kopiujcy, operator= (mona go albo zdefiniowa, albo zabroni jego uycia) oraz destruktor. Operator = rutynowo sprawdza, czy nastpuje przypisanie obiektu do samego siebie, mimo e w tym przypadku nie jest to absolutnie konieczne. Dziki temu praktycznie wyklucza si moliwo zapomnienia o sprawdzeniu przypisania do samego siebie po wprowadzeniu zmian w kodzie, ktre spowodowayby tak konieczno.

nie odwoa
W powyszym przykadzie zarwno konstruktor kopiujcy, jak i operator= tworzyy now kopi tego, co wskazywa wskanik, a destruktor j usuwa. Jeeli jednak obiekty wymagaj duej iloci pamici lub ich inicjalizacja wie si ze sporym narzutem, to by moe warto unikn kopiowania. Typowe podejcie do tego problemu nazywane jest zliczaniem odwoa (ang. reference counting). Dany obiekt obdarza si pewn ,jnteligencja", dziki ktrej wie on, ile obiektw go wskazuje. W takim przypadku wywoanie konstruktora kopiowania oraz operacja przypisania oznaczaj doczenie jeszcze jednego wskanika do istniejcego obiektu i inkrementacj licznika odwoa. Destrukcja oznacza natomiast dekrementacj licznika odwoa i zniszczenie obiektu w przypadku, gdy licznik odwoa osignie warto zerow. w wywietlanych w programie tekstach, wycznie ze wzgldw gramatycznych, tlum.

Thinking in C++. Edycja polska Co si jednak dzieje, gdy chcemy zapisa co w obiekcie (na przykad w obiekcie klasy Dog z poprzedniego przykadu)? Obiekt klasy Dog moe by uywany przez wiksz liczb obiektw, wic jego modyfikacja mogaby spowodowa zarwno zmian wasnego obiektu, jak i obiektu nalecego do kogo innego, co nie byoby podane. Do rozwizania tego problemu utosamiania" wykorzystywana jest dodatkowa technika, nazywana kopiowaniem przy zapisie (ang. copy-on-write). Przed zapisaniem informacji w obszarze pamici naley si upewni, e nikt inny go nie uywa. Jeeli licznik odwoa ma warto wiksznijeden, to przed zapisaniem informacji w tym obszarze naley wykona jego osobist" kopi, dziki czemu dane nalece do innych obiektw pozostan nienaruszone. Poniej znajduje si prosty przykad zliczania odwoa i kopiowania przy zapisie: / / : C12:ReferenceCounting.cpp // Zliczanie odwoa, kopiowanie przy zapisie include "../require.h" #include <string>

#include <iostream> using namespace std;

class Dog { string nm; int refcount; Dog(const string& name) : nm(name). refcount(l) { cout << "Tworzenie psa: " << *this << endl; } // Zablokowanie operacji przypisania: Dog& operator<<(const Dog& rv); public: // Obiekty klasy Dog mog zosta utworzone tylko na stercie: static Dog* make(const string& name) { return new Dog(name); } Dog(const Dog& d) : nm(d.nm + " kopia"), refcount(l) { cout << "Konstruktor kopiujcy psa: " << *this << endl; } ~Dog() { cout << "Usuwanie psa: " << *this <<^endl; } void attach() { ++refcount; cout << "Pies dolaczony: " << *this << endl; } void detach() { require(refcount ! 0); cout << "Odczanie psa: " << *this << endl; // Niszczenie obiektu w przypadku, gdy nikt go nie uywa: if(--refcount 0) delete this; } // Warunkowe kopiowanie obiektu klasy Dog. // Naley je wywoa przed modyfikacj obiektu // klasy Dog'i przypisa uzyskany wskanik do // wasnego wskanika obiektu klasy Dog.

Rozdzia i2. Przecianie operatorw Dog* unalias() { cout << "Usuwanie poczenia psa: " << *this << endl; // Jeeli nie ma odwoa, to kopiowanie nie jest realizowane: if(refcount 1) return this; --refcount; // Uycie konstruktora kopiujcego w celu utworzenia kopii: return new Dog(*this); }

421

void rename(const string& newName) { nm = newName; cout << "Zmiana imienia psa na: " << *this << endl; } friend ostream& operator<<(ostream& os. const Dog& d) { return os << "[" << d.nm << "]. rc - " << d.refcount;

class DogHouse { Dog* p; string houseName; public: DogHouse(Dog* dog. const string& house) : p(dog), houseName(house) { cout << "Utworzono domek: "<< *this << endl; } DogHouse(const DogHouse& dh) : p(dh.p), houseName("skopiowany konstruktorem " + dh.houseName) { p->attach(); cout << "Konstruktor kopiowania DogHouse: " << *thi s << endl; } DogHouse& operator-(const DogHouse& dh) { // Sprawdzanie przypisania do samego siebie: if(&dh !- this) { houseName - dh.houseName + " przypisany"; // Usuniecie poprzedniej wartoci: p->detach(); p = dh.p; // Podobnie jak w konstruktorze kopiujcym p->attach(); } cout << "operator= klasy DogHouse: " << *this << endl; return *this; } // Zmniejszenie licznika odwoa i warunkowe zniszczenie ~DogHouse() { cout << "Destruktor klasy DogHouse: " << *this << endl; p->detach(); } void renameHouse(const string& newName) { houseName = newName; } void unalias() { p - p->unalias(); }

Thinking in C++. Edycja polska // Kopiowanie przy zapisie. Za kadym razem, gdy // modyfikowany jest wskazywany obiekt, trzeba go // najpierw "odczy": void renameDog(const string& newName) { unalias();

p->rename(newName); } // ...take w przypadku udostpniania obiektu: Dog* getOog() {


unalias(); return p;

} friend ostream& operator<<(ostream& os. const DogHouse& dh) { return os << "[" << dh.houseName << "] zawiera " << *dh.p;

int main() { DogHouse fafika(Dog::make("Fafik"), "DomekFafika"), burka(Dog: :make("Burek") . "DomekBurka"): cout << "Przed uruchomieniem konstruktora kopiujcego" << endl: DogHouse reksa(fafika); cout << "Po utworzeniu reksa za pomoca konstruktora kopiujcego" << endl: cout << "fafika:" << fafika << endl; cout << "burka:" << burka << endl; cout << "reksa:" << reksa << endl; cout << "Przed przypisaniem burka fafika" << endl; burka = fafika; cout << "Po przypisaniu burka - fafika" << endl; cout << "burka:" << burka << endl; cout << "Rozpoczcie kopiowania do samego siebie" <<endl; reksa - reksa: cout << "Po zakoczeniu kopiowania do samego siebie" << endl; cout << "reksa:" << reksa << endl; // Zaznacz ponisze wiersze jako komentarze: cout << "Przed wywoaniem funkcji rename(\"Reks\")" << endl; reksa.getDog()->rename("Reks" ) : cout << "Po wywoaniu funkcji rename(\"Reks\")" << endl;
Klasa DogHouse (psia buda) zawiera wskanik do obiektu klasy Dog (pies). Klasa Dog posiada licznik odwoa i funkcje, umoliwiajce zarzdzanie nim oraz odczytujce jego warto. Zawiera rwnie konstruktor kopiujcy, pozwalajcy na utworzenie nowego obiektu klasy Dog na podstawie obiektu, ktry ju istnieje. Funkcja attach( ) inkrementuje warto licznika odwoa do obiektu klasy Dog, informujc, e uywa go jeszcze jeden obiekt. Funkcja detach( ) dekrementuje licznik odwoa. Gdy licznik ten osignie warto zerow, oznacza to, e nikt nie uywa ju obiektu, wic funkcja skadowa niszczy swj obiekt za pomoc instrukcji delete this. Zanim zostan dokonane jakiekolwiek modyfikacje obiektu (takie jak zmiana przechowywanego w nim imienia psa), trzeba si upewni, e nie zmienia si jednoczenie obiektu, uywanego przezjaki inny obiekt. W tym celu naley wywoa funkcj?

Rozdzia d. Przecianie operatorw

423

DogHouse::unalias(), ktra wywouje z kolei funkcj Dog::unalias(). Druga z wymienionych funkcji zwraca istniejcy wskanik obiektu Dog w przypadku, gdy licznik odwoa ma warto jeden (co oznacza, e tego obiektu nie wskazuje aden inny obiekt), ale gdy licznik ma warto wiksz nijeden, tworzy kopi tego obiektu. Konstruktor kopiujcy klasy DogHouse, zamiast tworzy wasny obszar pamici, przypisuje swojemu wskanikowi obiektu Dog wskanik obiektu Dog, ktry pochodzi z obiektu bdcego argumentem konstruktora. Nastpnie, z uwagi na to, e jest ju nastpnym obiektem, wykorzystujcym ten sam blok pamici, inkrementuje licznik odwoa, wywoujc funkcj Dog::attach(). Funkcja operator= ma do czynienia z obiektem znajdujcym si po lewej stronie znaku =, ktry ju zosta wczeniej utworzony. Musi wic najpierw wyczyci go", wywoujc w stosunku do tego obiektu funkcj detach(), ktra spowoduje usunicie poprzedniego obiektu klasy Dog, jeeli aden inny obiekt ju go nie uywa. Nastpnie operator= wykonuje te same operacje, co konstruktor kopiujcy. Zwr uwag na to, e na samym pocztku dokonuje on sprawdzenia, czy obiekt nie jest kopiowany do samego siebie. Destruktor wywouje funkcj detach(), usuwajc warunkowo obiekt klasy Dog. Aby zaimplementowa kopiowanie przy zapisie, trzeba nadzorowa wszystkie operacje zapisujce informacje w przydzielonym bloku pamici. Na przykad funkcja skadowa renameDog() pozwala na zmian informacji zawartych w obszarze pamici obiektu. Jednak najpierw wykorzystuje ona funkcj unalias(), zapobiegajc w ten sposb modyfikacjom obiektu klasy Dog, wskazywanego przez wicej ni jeden obiekt klasy DogHouse. Rwnie w przypadku koniecznoci zwrcenia przez klas DogHouse wskanika do obiektu klasy Dog, w stosunku do tego wskanika wywoywanajest najpierw funkcja unaIias(). W funkcji main( ) testowane s rozmaite funkcje, ktre, by realizowa zliczanie odwoa, musz dziaa prawidowo: konstruktor, konstruktor kopiujcy, operator= oraz destruktor. Dziki wywoaniu funkcji renameDog() testowane jest rwnie kopiowanie przy zapisie. Poniej zamieszczono wyniki dziaania programu (po ich niewielkim przeformatowaniu): Tworzenie psa: [Fafik], rc = 1 Utworzono domek: [DomekFafika] zawiera [Fafik]. rc = 1 Tworzenie psa: [Burek], rc = 1 Utworzono domek: [DomekBurka] zawiera [Burek], rc = 1 Przed uruchomieniem konstruktora kopiujcego Pies dolaczony: [Fafik], rc = 2 Konstruktor kopiowania DogHouse: [skopiowany konstruktorem DomekFafika] zawiera [Fafik], rc = 2 Po utworzeniu reksa za pomoca konstruktora kopiujcego fafika:[DomekFafika] zawiera [Fafik]. rc = 2 burka:[DomekBurka] zawiera [Burek], rc = 1 reksa:[skopiowany konstruktorem DomekFafika] zawiera [Fafik], rc = 2

24 Przed przypisaniem burka - fafika Odczanie psa: [Burek], rc - 1

Thinking in C++. Edycja polska

Usuwanie psa: [Burek]. rc = 0 Pies dolaczony: [Fafik]. rc - 3 operator<< klasy DogHouse: [DomekFafika przypisany] zawiera [Fafik]. rc - 3 Po przypisaniu burka - fafika burka:[DomekFafika przypisany] zawiera [Fafik]. rc 3 Rozpoczcie kopiowania do samego siebie operator= klasy DogHouse: [skopiowany konstruktorem DomekFafika] zawiera [Fafik]. rc - 3 Po zakoczeniu kopiowania do samego siebie reksa:[skopiowany konstruktorem OomekFafika] zawiera [Fafik], rc - 3 Przed wywoaniem funkcji rename("Reks") Usuwanie poczenia psa: [Fafik], rc - 3 Konstruktor kopiujcy psa: [Fafik kopia], rc - 1 Zmiana imienia psa na: [Reks]. rc - 1 Po wywoaniu funkcji rename("Reks") Destruktor klasy DogHouse: [skopiowany konstruktorem DomekFafika] zawiera [Reks], rc - 1 Odczanie psa: [Reks], rc - 1 Usuwanie psa: [Reks], rc - 0 Destruktor klasy DogHouse: [DomekFafika przypisany] zawiera [Fafik], rc = 2 Odczanie psa: [Fafik], rc = 2 Destruktor klasy DogHouse: [DomekFafika] zawiera [Fafik], rc = 1 Odczanie psa: [Fafik], rc - 1 Usuwanie psa: [Fafik]. rc - 0
Studiujc uwanie wyniki dziaania programu, ledzc kod rdowy i eksperymentujc z programem, pogbisz swoj wiedz na temat stosowanych w programie technik.

Automatyczne tworzenie operatora =


Z uwagi na to, e wikszo programistw spodziewa si, e operacja przypisywania jednego obiektu drugiemu obiektowi tego samego typu jest moliwa do wykonania, kompilator automatycznie tworzy operator typ::operator=(typ), jeeli nie zostanie on zdefiniowany. Dziaanie tego operatora naladuje dziaanie automatycznie utworzonego konstruktora kopiujcego jeeli klasa zawiera obiekty (albojest klaspochodn innej klasy), to dla kadego z tych obiektw jest wywoywany rekurencyjnie operator= . Operacja ta nosi nazw przypisania za porednictwem elementw skadowych (ang. memberwise assignment). Ilustruje to poniszy przykad: / / : C12:AutomaticOperatorEquals.cpp #include <iostream> using namespace std: class Cargo { public: Cargo& operator-(const Cargo&) { cout << "wewntrz Cargo::operator-O" << endl; return *this:

Rozdzia 32. << Przecianie operatorw

425

class Truck Cargo b;

int main() { Truck a. b; a - b: // Drukuje: "wewntrz Cargo::operator=O"

Automatycznie wygenerowany operator= klasy Truck wywouje funkcj Cargo:: operator=. Zwykle nie naley pozwala na to kompilatorowi. W przypadku niebanalnych klas (zwaszcza gdy zawieraj one wskaniki !) naley zawsze utworzy operator= w jawny sposb. Jeeli naprawd nie chcemy, aby ktokolwiek dokonywa przypisywania do obiektw klasy, naley zadeklarowa operator= jako funkcj prywatn (nie ma potrzeby jej definiowania, pod warunkiem, e nie bdzie ona uywana w obrbie klasy).

Automatyczna konwersja typw


Gdy w jzykach C i C++ kompilator widzi wyraenie lub wywoanie funkcji wykorzystujce typ niebdcy dokadnie typem, ktry jest w danej chwili potrzebny, to czsto moe przeprowadzi automatyczn konwersj typw z aktualnego do aktualnie wymaganego. W jzyku C++ ten sam efekt mona uzyska w przypadku typw definiowanych przez uytkownika, definiujc funkcje odpowiedzialne za automatyczn konwersj typw. Funkcje te istniej w dwch postaciach jako szczeglny rodzaj konstruktora oraz przeciony operator.

Konwersja za pomoc konstruktora


Jeeli zdefiniujemy konstruktor, pobierajcy jako jedyny argument obiekt (lub referencj) innego typu, to taki konstruktor umoliwi kompilatorowi dokonanie automatycznej konwersji typw. wiadczy o tym poniszy program:

//: C12:AutomaticTypeConversion.cpp // Konstruktor umoliwiajcy konwersje typw class One { public: One() {} }:

class Two { public: Two(const One&) {} }:


void f(Two) {}

>6 int main() {


One one;

Thinking in C++. Edycja polska

} ///:-

f(one); // Wymagany obiekt klasy Two, a uyty klasy One

Gdy kompilator widzi funkcj f(), wywoywan z obiektem klasy One, zaglda do deklaracji funkcji f() i wykrywa, e wymaga ona argumentu bdcego obiektem klasy Two. Nastpnie poszukuje sposobu przeksztacenia obiektu klasy One w obiekt klasy Two i zauwaa konstruktor Two::Two(One), ktry zostaje niejawnie wywoywany. Utworzony w ten sposb obiekt klasy Two jest przekazywany funkcji f(). W powyszym przypadku automatyczna konwersja typw pozwala na unikniciu kopotu definiowania dwch przecionych wersji funkcji f(). Odbywa si to jednak kosztem niejawnego wywoania funkcji, ktre moe mie znaczenie w przypadku, gdy istotna jest efektywno wywoania funkcji f().

Zapobieganie konwersji za pomoc konstruktora


Zdarzaj si przypadki, w ktrych automatyczna konwersja typw, realizowana za pomoc konstruktora, moe stwarza problemy. Aby j wykluczy, naley zmodyfikowa konstruktor, poprzedzajc go sowem kluczowym explicit Qawny), ktre dziaa tylko w przypadku konstruktorw. Zamieszczony poniej przykad ilustruje wykorzystanie sowa kluczowego explicit do zmodyfikowania konstruktora klasy Two, pochodzcej z poprzedniego programu: / / : C12:Exp1icitKeyword.cpp // Wykorzystanie sowa kluczowego "explicit" class One { public:
One() {}
}:

class Two { public: explicit Two(const One&) {}


}:

void f(Two) {} int main() { / / ! f(one); // Automatyczna konwersja nie jest dozwolona f(Two(one)); // W porzdku - konwersja dokonana przez uytkownika
}
IIIOne one;

Okrelajc konstruktor klasy Two jako ,jawny" (explicit), da si od kompilatora, by nie przeprowadza za pomoc tego konstruktora adnej automatycznej konwersji (inne konstruktory tej klasy, niezdefiniowane jako ,jawne", mog nadal dokonywa automatycznej konwersji typw). Jeeli uytkownik chce wykona konwersj, okrelon przez ten konstruktor, musi zapisa to w programie. W powyszym kodzie podczas wywoania f(Two(one)), na podstawie obiektu one, tworzony jest tymczasowy obiekt klasy Two to samo zrobi kompilator w poprzedniej wersji programu.

Rozdzia 2. * Przecianie operatorw

427

Operator konwersji
Drugim sposobem, umoliwiajcym automatyczn konwersj typw, jest przecienie operatora. Mona utworzy funkcj skadow, pobierajc aktualny typ i przeksztacajc go na typ docelowy, wykorzystujc sowo kluczowe operator, poprzedzajce nazw typy, do ktrego ma zosta dokonana konwersja. Jest to wyjtkowa posta przecienia operatora, poniewa sprawia ona wraenie, jakby nie okrelono typu zwracanego przez operator jest nim bowiem nazwa przecianego operatora. Pokazuje to nastpujcy przykad:
/ / : C12:OperatorOverloadingConversion.cpp class Three { int i; public:

Three(int ii - 0. int - 0) : i(ii) {}

class Four { int x; public: Four(int xx) : x(xx) {} operator ThreeC) const { return Three(x); }

}:
void g(Three) {} int main() { Four four(l); g(four); g(l); // Wywoanie Three(l,0) W przypadku konwersji dokonywanej za pomoc konstruktora bya za ni odpowiedzialna klasa docelowa. Jednak co do konwersji dokonywanej przy uyciu operatora, to jest ona realizowana przez klas rdow. Korzyci wynikajc z zastosowania techniki, wykorzystujcej konstruktory, jest moliwo dodawania do istniejcego systemu nowych sposobw konwersji podczas tworzenia nowych klas. Jednake utworzenie jednoargumentowego konstruktora zawsze definiuje automatyczn konwersj typw (nawet gdy posiada on wicej argumentw pod warunkiem, e pozostae argumenty bd posiaday wartoci domylne), ktra nie zawsze moe by podana (i w tym przypadku mona j wykluczy, uywajc sowa kluczowego explicit). Nie ma take moliwoci zdefiniowania za pomoc konstruktorw konwersji z typw zdefiniowanych przez uytkownika na typy wbudowane to moliwe wycznie za pomoc przeciania operatorw.

Jednym z najwaniejszych powodw stosowania globalnych przecionych operatorw zamiast operatorw, bdcych funkcjami skadowymi klas, jest fakt, e w przypadku operatorw globalnych automatyczna konwersja typw moe zosta zastosowana w odniesieniu do kadego argumentu. Jeli za chodzi o funkcj skadow, to argument znajdujcy si po lewej stronie operatora musiju by odpowiedniego typu.

Thinking in C++. Edycja polska Aby konwersji podlegay oba argumenty, naley zastosowa globaln wersj operatodf, pozwalajc na uniknicie mudnego kodowania. Ilustruje to przedstawiony poniej niewielki przykad: / / : C12:ReflexivityInOverloading.cpp class Number { int i; public: Number(int ii - 0) : i(ii) {} const Number operator+(const Number& n) const { return Number(i + n.i); friend const Number operator-(const Number&. const Number&); const Number operator-(const Number& nl. const Number& n2) { return Number(nl.i - n2.i); int main() { Number a(47). b(ll); a + b; // W porzdku a + 1; // Drugi argument przeksztacony do typu Number //! 1 + a; // le! Pierwszy argument nie jest typu Number a - b; // W porzdku

a - 1; // Drugi argument przeksztacony do typu Number 1 - a; // Pierwszy argument przeksztacony do typu Number } ///:-

Klasa Number posiada zarwno operator+, bdcy skadow klasy, jak i zaprzyjaniony operator-. Poniewa istnieje konstruktor, pobierajcy pojedynczy argument typu int, liczby cakowite mog by automatycznie przeksztacane na obiekty klasy Number, ale musi si to odbywa w odpowiednich warunkach. Jak wida w funkcji main( ), dodanie do siebie dwch obiektw klasy Number dziaa poprawnie, poniewa odpowiada dokadnie przecionemu operatorowi. Rwnie w przypadku, gdy kompilator spotyka wyraenie, w ktrym po obiekcie klasy Number wystpuje znak +, a nastpnie liczba cakowita, moe dopasowa to wyraenie do funkcji skadowej Number::operator+, przeksztacajc za pomockonstruktora argument cakowity na typ Number. Gdy jednak napotyka liczb cakowit, znak +, a nastpnie obiekt klasy Number, to nie wie, co ma robi, poniewa dysponuje jedynie funkcj Number:: operator+, wymagajc, by argument znajdujcy si po lewej stronie by obiektem klasy Number. Dlatego te kompilator zgasza wwczas bd. Zupenie inaczej wyglda sprawa z zaprzyjanionym operatorem odejmowania. Kompilator musi co prawda uzupeni oba argumenty, ale moe to zrobi nie jest bowiem ograniczony wymaganiem, by argument znajdujcy si po lewej stronie by argumentem typu Number. Tak wic widzc:

1 - a
moe przeksztaci pierwszy argument na typ Number, uywajc w tym celu konstruktora.

Rozdzia 2. Przecianie operatorw

429

Czasami wystpuje potrzeba ograniczenia uycia operatorw przez uczynienie ich skadowymi. Na przykad w przypadku mnoenia macierzy przez wektor musi on znajdowa si po prawej stronie operatora. Jeeli chcesz jednak, by operatory mogy dokona konwersji dowolnego argumentu, zdefiniuj gojako funkcj zaprzyjanionklasy. Na szczcie, w przypadku wyraenia 1 -1, kompilator nie dokonuje przeksztacenia obu argumentw na obiekty klasy Number, aby nastpnie wywoa operator-. Oznaczaoby to, e istniejcy kod jzyka C zaczby nagle dziaa inaczej. Kompilator rozpoczyna wic od najprostszej moliwoci, ktrjest w tym przypadku wbudowany operator, obsugujcy wyraenie 1 -1.

Przykted konwersji typw


Przykadem, w ktrym automatyczna konwersja typw jest wyjtkowo przydatna, s wszelkie klasy zawierajce acuchy znakowe (w tym przypadku implementujemy po prostu klas, uywajc standardowej klasy C++ string poniewa mona to atwo wykona). Aby uywa wszystkich istniejcych funkcji operujcych na acuchach, dostpnych w standardowej bibliotece jzyka C, bez korzystania z automatycznej konwersji typw, naleaoby utworzy funkcj skadow dla kadej z nich, jak przedstawiono poniej:

#include <cstring> #include <cstdlib> .#include <string> using namespace std;

// Brak automatycznej konwersji typw #include "../require.h"

//: C12:Stringsl.cpp

class Stringc { string s; public: Stringc(const string& str = "") : s(str) {} int strcmp(const Stringc& S) const { return ::strcmp(s.c_str(). S.s.c_str()); // ... itd., dla kadej funkcji zawartej w string.h int main() { Stringc sl("witam"). s2("wszystkich"); sl.strcmp(s2); W powyszym przykadzie utworzona zostaa jedynie funkcja strcmp( ). Jednak dla kadej funkcji zawartej w pliku nagwkowym <cstring>, ktra mogaby by potrzebna, naleaoby utworzy odpowiadajc jej funkcj. Na szczcie, mona udostpni automatyczn konwersj typw, umoliwiajc tym samym dostp do wszystkich funkcji zawartych w pIiku nagwkowym <cstring>: / / : C12:Strings2.cpp

// Z automatyczna, konwersja typw

130 #include ". ./require.h" #include <cstring> #include <cstdlib> #include <string> using namespace std; class Stringc { string s; public: Stringc(const string& str = "") : s(str) {} operator const char*O const { return s.c str();

Thinking in C++. Edycja polska

int main() { Stringc sl("hello"). s2("there"); strcmp(sl. s2); // Standardowa funkcja jzyka C strspn(sl, s2); // Dowolna funkcja operujca na acuchach! Obecnie kada funkcja pobierajca argument typu char* moe pobiera rwnie argument typu Stringc, poniewa kompilator wie, w jaki sposb przeksztaci obiekt klasy Stringc w obiekt typu char*.

Putapki automatycznej konwersji typw


Poniewa to kompilator decyduje o sposobie niejawnego przeksztacania typw, niepoprawne zaprojektowanie konwersji moe by przyczyn kopotw. Prostym i oczywistym przykadem jest klasa X, posiadajca moliwo przeksztacenia obiektu w obiekt klasy Y za pomoc operatora Y( ). Jeeli klasa Y posiada konstruktor, pobierajcy pojedynczy argument typu X, to reprezentuje on tak sam konwersj typw. W razie wystpienia takiej konwersji kompilator moe dokona konwersji z typu X na Y na dwa sposoby, zgasza wic bd dwuznacznoci: / / : C12:TypeCorwersionAmbiguity.cpp class Orange; // Deklaracja klasy class Apple { public: operator Orange() const; // Konwersja z Apple do Orange class Orange { public: Orange(Apple); // Konwersja z Apple do Orange void f(Orange) {} int main() { Apple a; / / ! f(a); // Bd ' dwuznaczna konwersja

Rozdzia dL2. Przecianie operatorw

431

Oczywistym sposobem rozwizania problemu jest unikanie takich sytuacji. Naley po prostu udostpni tylko jedn moliwo automatycznej konwersji z jednego typu na drugi. Bardziej zoony problem, ktremu naley si przyjrze uwaniej, wystpuje wtedy, gdy udostpnia si automatycznkonwersj na wicej ni tylkojeden typ. Jest to czasami nazywaneprzecieniem wyjcia (ang.fan-out): II: C12:TypeConversionFanout.cpp class Orange {}; class Pear {}; class Apple { public: operator Orange() const; operator PearC) const; // Overloaded eat(): void eat(Orange); void eat(Pear); int main() { Apple c; //! eat(c); // Bd - Apple -> Orange lub Apple -> Pear ??? Klasa Apple zapewnia automatyczn konwersj zarwno na typ Orange, jak i na Pear. Puapka polega na tym, e nie stanowi to problemu, dopki kto nie utworzy dwch niewinnie wygldajcych, przecionych wersji funkcji eat( ) (w przypadku tylkojednej wersji funkcji eat( ) kod zawarty w funkcji main( ) dziaa bez problemu). Podobniejak w poprzednim przypadku, rozwizaniemjest umoliwienie tylkojednego sposobu automatycznej konwersji typu i powinno to stanowi haso przewodnie, zwizane z automatyczn konwersj typw. Mona rwnie zdefiniowa konwersje na inne typy, ale nie powinny one odbywa si automatycznie. Wolno uy w tym celujawnych wywoa funkcji o nazwach w rodzaju: utworzA( ) i utworzB( ). Ukryte dziatonia Automatyczna konwersja typw powoduje niekiedy znacznie wiksz liczb niejawnych dziaa ni przewidywane. Przedstawion poniej modyfikacj programu CopyingVsInitialization.cpp mona potraktowajako amigwk:
/ / : C12:CopyingVsInitialization2.cpp class Fi {};
class Fee { public: Fee(int) {} Fee(const Fii) {}

32
class Fo { int i; public: Fo(int x - 0) : i(x) {} operator Fee() const { return Fee(i);

Thinking in C++. Edycja polska

int main() { Fo fo; Fee fee = fo;

Nie ma konstruktora umoliwiajcego utworzenie obiektu fee klasy Fee na podstawie obiektu klasy Fo. Jednake klasa Fo umoliwia automatyczn konwersj obiektu swojego typu na obiekt klasy Fee. Nie zdefiniowano co prawda konstruktora kopiujcego, zapewniajcego moliwo utworzenia obiektu klasy Fee na podstawie istniejcego ju obiektu tej klasy, ale jest to jedna z funkcji tworzonych automatycznie przez kompilator (domylny konstruktor, konstruktor kopiujcy, operator= oraz destruktor s automatycznie generowane przez kompilator). Wic dla niewinnie wygldajcej instrukcji:
Fee fee - fo;

jest wywoywany operator automatycznej konwersji typw oraz tworzony konstruktor kopiujcy. Uywaj automatycznej konwersji typw z rozwag. Podobnie jak w przypadku wszystkich przecie operatorw, okazuje si ona wspaniaym narzdziem w sytuacjach, w ktrych umoliwia uniknicie mudnego kodowania, ale zazwyczaj nie warto jej stosowa bez uzasadnionego powodu.

Podsumowanie
Mechanizm przeciania operatorw wprowadzono w istocie wycznie po to, by uatwi ycie programistom. Nie ma w nim nic szczeglnie tajemniczego przecione operatory s jedynie funkcjami o dziwnych nazwach, wywoywanymi przez kompilator po napotkaniu odpowiedniego wzorca. Jeeli jednak przecione operatory nie przynosz tobie (projektantowi klasy) ani uytkownikowi klasy istotnych korzyci, to nie zaprztaj sobie uwagi ich definiowaniem.

wiczenia
Rozwizania wybranych wicze znajduj si w dokumencie elektronicznym: The Thinking in C++ Annotated Solution Guide, ktry mona pobra za niewielk opat z witryny http:/Avww.BruceEckel.com.

Rozdzia 12. Przecianie operatorw

433

1. Utwrz prostklas zawierajcprzeciony operator++. Sprbuj wywoa ten operator zarwno w przedrostkowej,jak i w przyrostkowej postaci. Zobacz, jakie ostrzeenia zostan zgoszone przez kompilator. 2. Utwrz prost klas zawierajc liczb cakowit i przeciony operator+, bdcy funkcj skadow. Utwrz rwnie funkcj skadow print(), pobierajcargument typu ostream& i wyprowadzajcdo tego strumienia informacje. Przetestuj dziaanie swojej klasy, by upewni si, e wszystko funkcjonuje prawidowo. 3. Do klasy, utworzonej w poprzednim wiczeniu, dodaj dwuargumentowy operator-, bdcyjej funkcjskadow. Poka, e moesz uywa obiektw tej klasy w zoonych wyraeniach w rodzaju a + b - c. 4. Do klasy, utworzonej w 2. wiczeniu, dodaj operatory ++ oraz --, zarwno w wersjach przedrostkowych, jak i przyrostkowych, w taki sposb, aby zwracay one odpowiednio obiekt o powikszonej lub pomniejszonej wartoci skadowej. Upewnij si, e przedrostkowe wersje operatorw zwracaj poprawne wartoci. 5. Zmodyfikuj operatory inkrementacji i dekrementacji, utworzone w poprzednim wiczeniu, tak by ich wersje przedrostkowe nie zwracay obiektw bdcych staymi, a wersje przyrostkowe zwracay obiekty bdce staymi. Poka, e dziaaj one prawidowo i wyjanij, dlaczego operatory te powinno si definiowa w taki wanie sposb. 6. Zmie funkcj print(), utworzon si w 2. wiczeniu w taki sposb, aby bya ona przecionym operatorem <<, podobnie jak w programie IostreamOperatorOverloading.cpp. 7. Zmodyfikuj wiczenie 3. w taki sposb, aby operator+ i operator- nie byy funkcjami skadowymi klasy. Poka, e nadal dziaajone prawidowo. 8. Do klasy, utworzonej w 2. wiczeniu, dodaj jednoargumentowy operatori poka, e dziaa on poprawnie. 9. Utwrz klas zawierajcpojedyncz, prywatnskadowtypu char. Dokonaj przecienia operatorw << oraz (w sposb zaprezentowany w programie IostreamOperatorOverloading.cpp) i przetestuj je. Moesz sprawdzi ich dziaanie ze strumieniami fstreams, stringstreams, cin oraz cout. 10. Sprawd, jak sztuczn sta wartoci przekazuje uywany przez ciebie kompilator w przypadku przyrostkowych wersji przecionych operatorw ++ oraz --. U.. Utwrz klas Number, przechowujc warto typu double, i dodaj do niej przecione operatory +, -, *, / oraz przypisania. Wybierz wartoci zwracane przez te funkcje w taki sposb, by wyraenia mogy by czone w acuchy, a take by byy one efektywne. Napisz operator double (), umoliwiajcy automatycznkonwersj typu. 12. Zmodyfikuj poprzednie wiczenie w taki sposb, aby zastosowa optymalizacj zwracania wartoci, o ilejeszcze tego nie zrobie.

34

Thinking in C++. Edycja polska

13. Utwrz klas zawierajc wskanik i poka, e gdy pozwoli si kompilatorowi na wygenerowanie operatora =, to w rezultacie jego uycia powstan wskaniki wskazujce ten sam obszar pamici. Nastpnie rozwi ten problem, definiujc wasny operator=, i poka, e jego dziaanie nie wywouje kopotw, polegajcych na utosamianiu wskanikw. Upewnij si, e sprawdzasz, czy nie nastpuje przypisanie do samego siebie, i obsugujesz ten przypadek waciwie. 14. Utwrz klas o nazwie Bird (ptak), zawierajcskadowtypu string oraz statyczn zmienn typu int. W domylnym konstruktorze uyj zmiennej cakowitej do automatycznego wygenerowania identyfikatora, ktry zostanie umieszczony w zmiennej typu string wraz z nazw klasy (Bird #1, Bird #2 itd.). Dodaj operator<<, zdefiniowany dla strumieni wyjciowych ostream, umoliwiajcy drukowanie obiektw klasy Bird. Utwrz operator przypisania = oraz konstruktor kopiujcy. Sprawd w funkcji main(), czy wszystko dziaa prawidowo. 15. Utwrz klas o nazwie BirdHouse (domek dla ptakw), zawierajc obiekt, wskanik oraz referencj do klasy Bird, utworzonej w poprzednim wiczeniu. Konstruktor klasy powinien pobiera jako argumenty trzy obiekty klasy Bird. W klasie BirdHouse utwrz operator<<, zdefiniowany dla strumieni wyjciowych ostream. Utwrz rwnie operator przypisania = oraz konstruktor kopiujcy. Upewnij si w funkcji main(), e wszystko dziaa prawidowo. Upewnij si, e moesz czy acuchowo przypisania do obiektw klasy BirdHouse, a take budowa wyraenia zawierajce wiele operatorw. 16. Do klas Bird oraz BirdHouse, opisanych w poprzednim wiczeniu, dodaj skadowtypu int. Utwrz operatory +, -, *, /, bdce funkcjami skadowymi, wykonujcymi operacje na odpowiednich danych skadowych kadej z tych klas. Upewnij si, e dziaaj one poprawnie. 17. Powtrz poprzednie wiczenie, uywajc operatorw niebdcych funkcjami skadowymi. %&. Do programw SmartPointer.cpp oraz NestedSmartPointer.cpp dodaj operator--. 19. Zmodyfikuj program CopyingVsInitialization.cpp w taki sposb, by wszystkie wystpujce w nim konstruktory drukoway komunikaty, informujce o tym, co si aktualnie dzieje. Nastpnie sprawd, czy obie formy wywoanie konstruktora kopiujcego (w postaci znaku przypisania i z uyciem nawiasu) s sobie rwnowane. 20. Sprbuj utworzy dlajakiej klasy operator= niebdcy funkcj skadow i zobacz, jakie bdy zgosi w takim przypadku kompilator. 21. Utwrz klas, zawierajckonstruktor kopiujcy, posiadajcy drugi argument, ktrym bdzie acuch o domylnej wartoci Wywoanie KK". Utwrz funkcj pobierajc obiekt tej klasy przez warto i poka, e konstruktor kopiujcy jest wywoywany poprawnie.

Rozdzia 2. Przecianie operatorw

435

22. W programie CopyingWithPointers.cpp usu operator= klasy DogHouse i poka, e wygenerowany przez kompilator operator= poprawnie kopiuje acuchy, ale w przypadku wskanika do obiektu klasy Dog tworzyjedynie jego kolejn kopi. 23. Do programu ReferenceCounting.cpp, zarwno do klasy Dog,jak i DogHouse, dodaj statyczn skadow cakowit oraz zwyk skadow cakowit. We wszystkich konstruktorach obu klas inkrementuj statyczn cakowit i jednoczenie przypisz jej warto zwykej skadowej cakowitej obiektu zapamitujc w ten sposb liczb utworzonych obiektw. Dokonaj niezbdnych modyfikacji, dziki ktrym wszystkie instrukcje drukujce informacje bd podaway identyfikatory cakowite, przypisane obiektom. 24. Utwrz klas zawierajcskadowtypu string. Zainicjalizujjwkonstruktorze, ale nie twrz konstruktora kopiujcego ani operatora =. Utwrz drug klas, zawierajcskadow, bdcobiektem pierwszej klasy, i rwnie nie twrz dla niej konstruktora kopiujcego ani operatora =. Poka, e jej konstruktor kopiujcy oraz operator= zostay poprawnie wygenerowane przez kompilator. 25. Pocz ze sobklasy zawarte w plikach OverloadingUnaryOperators.cpp oraz Integer.cpp. 26. Zmodyfikuj program PointerToMemberOperator.cpp, dodajc do klasy Dog dwie nowe funkcje skadowe, niepobierajce argumentw i zwracajce warto void. Utwrz przeciony operator->*, wsppracujcy z tymi funkcjami, i przetestuj jego dziaanie. 27. Do programu NestedSmartPointer.cpp dodaj operator->*. 28. Utwrz dwie klasy AppIe i Orange. W klasie Apple utwrz konstruktor, pobierajcyjako argument obiekt klasy Orange. Utwrz funkcj pobierajc obiekt klasy Apple i wywoaj j z obiektem klasy Orange, pokazujc e wywoanie takie jest poprawne. Nastpnie zmodyfikuj konstruktor klasy Apple, uywajc sowa kluczowego explicite, by zademonstrowa, e zapobiega to automatycznej konwersji typw. Zmodyfikuj wywoanie funkcji w taki sposb, aby konwersja bya dokonywana wjawny sposb, a program ponownie dziaa poprawnie. 29. Do programu ReflexivityInOverloading.cpp dodaj globalny operator* i poka, e dziaa on w sposb symetryczny. 30. Utwrz dwie klasy, a nastpnie zdefiniuj operator+ oraz funkcje odpowiedzialne za konwersje w taki sposb, aby dodawanie byo symetryczne w stosunku do obu tych klas. 31. Popraw program TypeConversionFanout.cpp, tworzcjawn funkcj, wywoywan w ceIu konwersji typw i zastpujcjeden z operatorw automatycznej konwersji typw. 32. Napisz prosty program, uywajcy operatorw +, -, * oraz / w stosunku do liczb typu double. Dowiedz si, wjaki sposb wygenerowa na wyjciu kompilatora C++ program w asemblerze, i przyjrzyj si wygenerowanemu kodowi, by zrozumiec,jak dziaa, a nastpnie to wyjanij.

Thinking in C++. Edycja polska

16

Dynamiczne tworzenie obiektw


Czasami znanajest dokadna liczba, typy i czas ycia obiektw. Ale nie zawsze. Iloma samolotami bdzie zarzdza system kontroli lotw? Ilu figur geometrycznych wymaga system CAD? Z ilu wzw bdzie skadaa si sie? Do rozwizywania oglnych problemw programistycznych konieczna jest moliwo tworzenia i niszczenia obiektw w czasie pracy programu. Oczywicie, jzyk C zawsze udostpnia funkcje dynamicznej alokacji pamici, malIoc() i free( ) (a take inne warianty funkcji malIoc()), przydzielajce w trakcie pracy programu pami ze sterty (zwanej czasami rwniepamici woln). Jednak wjzyku C++ to po prostu nie zadziaa. Konstruktor nie pozwoli na przekazanie sobie adresu pamici, ktr ma zainicjalizowa i s ku temu powody. Gdyby byo to moliwe, naleaoby: 1. Zapomnie o tym. W ten sposb gwarantowana inicjalizacja obiektw wjzyku C++ nie byabyju taka. 2. Przypadkowo zrobi co z obiektem, zanim zostaby zainicjalizowany, spodziewajc si, e bdzie to dziaao poprawnie. 3. Dostarczy obiekt niewaciwej wielkoci. Oczywicie, nawet gdyby wszystko zostao wykonane prawidowo, to kady, kto modyfikowaby pniej program, mgby popeni te bdy. Niewaciwa inicjalizacja jest odpowiedzialna za znaczn cz problemw programistycznych, wic gwarancja wywoania konstruktorw w stosunku do obiektw utworzonych na stercie jest szczeglnie istotna. W jaki jednak sposb jzyk C++ zapewnia poprawn inicjalizacj i sprztanie, pozwalajc rwnoczenie uytkownikowi na dynamiczne tworzenie obiektw na stercie?

Rozdzia 13.

Rozdzia 13. Dynamiczne tworzenie obiektw

439

Wszystkie te trzy regiony pamici s czsto umieszczane w pojedynczym, cigym obszarze fizycznej pamici znajduj s w nim kolejno: obszar danych statycznych, stos oraz sterta (w porzdku okrelonym przez twrc kompilatora). Jednak nie obowizuj w tej kwestii adne reguy. Stos moe znajdowa si w jakim szczeglnym miejscu, a sterta moe by zaimplementowana w postaci da przydzielenia blokw pamici, kierowanych do systemu operacyjnego. Programista nie zajmuje si zazwyczaj tymi zagadnieniami, wic powinien myle jedynie o tym, e gdy potrzebna mu jest pami, to znajduje si ona wanie w tym miejscu.

Obshiga sterty w jzyku C


Jzyk C zawiera w swojej standardowej bibliotece nastpujce funkcje, umoliwiajce dynamiczne przydzielanie pamici w trakcie pracy programu: malloc() oraz jej warianty, calloc() i realloc(), dostarczajce pami ze sterty, oraz free( ), zwracajc z powrotem przydzielonpami. Funkcje te s praktyczne w uyciu, lecz prymitywne wymagaj one zarwno zrozumienia, jak i uwagi ze strony programisty. Aby utworzy na stercie egzemplarz klasy, wykorzystujc w tym celu dostpne w jzyku C funkcje obsugujce pami dynamiczn, naleaoby wykona przykadowo nastpujce dziaania: //: C13:MallocClass.cpp // Funkcja mallocO z obiektami klas // Co trzeba by zrobi, gdyby nie byo operatora "new" #include "../require.h" include <cstdlib> // mallocO i free() #include <cstring> // memset() #include <iostream> using namespace std; class Obj {

enum { sz = 100 }; char buf[sz]; public: void initialize() { // Nie mona uy konstruktora cout << "inicjalizacja Obj" << endl;

int i. j. k;

void destroy() const { // Nie mona uy destruktora cout << "niszczenie Obj" << endl;

memset(buf, 0. sz);

i = j = k - 0;

int main() { Obj* obj = (Obj*)malloc(sizeof(Obj)); require(obj != 0); obj->initialize(); // . . . nieco pniej: obj->destroy(): free(obj);

440

Thinking in C++. Edycja polska Sposb wykorzystania funkcji malloc() do przydzielenia obiektowi pamici ilustruje poniszy wiersz: Obj* obj = (Obj*)malloc(sizeof(Obj)): W przedstawionej instrukcji uytkownik musi okreli wielko obiektu (to pierwsze miejsce, w ktrym mona popeni bd). Funkcja malloc() zwraca warto typu void*, poniewa tworzy ona po prostu obszar pamici, a nie obiekt. Jzyk C++ nie pozwala na przypisanie wartoci typu void* jakiemukolwiek innemu wskanikowi, trzeba wic uy rzutowania. Z uwagi na to, e funkcja malloc() moe nie przydzieli wymaganej pamici (i zwrci w takim przypadku warto zerow), trzeba sprawdzi warto zwrconego przez ni wskanika, by upewni si, e jej wywoanie zakoczyo si powodzeniem. Jednak znacznie powaniejszy problem stanowi wiersz: Obj->init1alize(): Jeeli uytkownik postpowa do tej pory waciwie, musi pamita o inicjalizacji obiektu, zanim przystpi do korzystania z niego. Naley zwrci uwag na to, e nie mona w tym przypadku uy konstruktora, poniewa nie ma sposobu wywoania go w jawny sposb1 konstruktor jest bowiem wywoywany przez kompilator w trakcie tworzenia obiektu. Problem polega na tym, e uytkownik moe zapomnie o dokonaniu inicjalizacji przed rozpoczciem uywania obiektu, umieszczajc w ten sposb w programie rdo potencjalnych powanych bdw. Okazuje si rwnie, e dla wielu programistw funkcje dynamicznego przydziau pamici, dostpne w jzyku C, wydaj si niezrozumiae i skomplikowane nie naley do rzadkoci widok programisty jzyka C, wykorzystujcego mechanizmy pamici wirtualnej do przydzielenia ogromnych tablic zmiennych w obszarze danych statycznych tylko po to, by nie zaprzta sobie gowy dynamicznym przydziaem pamici. Z uwagi na to, e jzyk C++ prbuje uczyni wykorzystywanie bibliotek bezpiecznym i atwym dla zwykego programisty, nie sposb zaakceptowa podejcia do pamici dynamicznej przyjtego wjzyku C.

Operator new
Przyjtym w jzyku C++ rozwizaniem jest poczenie w jeden operator o nazwie new wszystkich dziaa koniecznych do utworzenia obiektu. Podczas generowania obiektu za pomoc operatora new (poprzez wyraenie new) przydziela si na stercie ilo pamici, niezbdn do zapamitania obiektu, i wywouje si w stosunku do niej konstruktor. Tak wic uycie instrukcji:
MojTyp *fp = new MojTyp(1.2);

podczas pracy programu jest rwnowane wywoaniu funkcji maUoc(sizeof(MojTyp)) (czsto jest to rzeczywicie wywoanie funkcji malloc()) oraz konstruktora klasy Istnieje specjalna konstrukcja, nazywana umieszczaniem new, pozwalajca na wywoanie konstruktora dla przydzielonego uprzednio obszaru pamici. Zostaa ona opisana w dalszej czci rozdziau.

Rozdzia 13. # Dynamiczne tworzenie obiektw

441

MojTyp, z adresem przydzielonej pamici w charakterze wskanika this oraz list argumentw (1,2). W chwili przypisania adresu wskanikowi fp wskazywany obszar pamici jest ju istniejcym, zainicjowanym obiektem (wczeniej nie byo nawet do niego dostpu). Ponadto jest on od razu typu MojTyp, dziki czemu nie ma potrzeby rzutowania wskanika. Domylnie operator new upewnia si, e przydzielenie pamici zakoczyo si powodzeniem, zanim jeszcze przekae adres konstruktorowi. Nie ma wic potrzeby jawnego sprawdzania, czy jego wywoanie zakoczyo si pomylnie. W dalszej czci rozdziau dowiesz si, co dzieje si w przypadku, gdy nie bdzie moliwoci przydzielenia obiektowi pamici. W wyraeniu new mona wykorzysta dowolny dostpny konstruktor kIasy. Jeeli konstruktor nie posiada argumentw, to zapisuje si wyraenie new, nie podajc Iisty argumentw konstruktora:
MojTyp *fp = new MojTyp;

Zwr uwag na to, jak prosty sta si proces tworzenia obiektw na stercie skada si on obecnie tylko z jednego wyraenia, zawierajcego wszelkie informacje o wielkoci, konwersje i testy zwizane z bezpieczestwem. Obiekt na stercie moe by teraz utworzony rwnie atwoJak na stosie.

Operator delete
Wyraeniem komplementarnym w stosunku do wyraenia new jest wyraenie delete, ktre najpierw wywouje destruktor, a nastpnie zwalnia przydzielon uprzednio pami (czsto wykonuje to, wywoujc funkcj free()). Podobnie jak wyraenie new zwraca wskanik do obiektu, wyraenie delete wymaga podaniajego adresu: delete fp: Powysze wyraenie powoduje destrukcj, a nastpnie zwolnienie pamici zajmowanej przez utworzony wczeniej dynamicznie obiekt klasy MojTyp. Operator delete moe by wywoany wycznie w stosunku do obiektu utworzonego wczeniej za pomoc operatora new. Jeeli obszar pamici obiektu zosta przydzielony za pomoc funkcji maIloc() (albo calloc() czy realloc()), a nastpnie uyje si w stosunku do niego operatora delete, to dziaanie tego operatora nie bdzie zdefiniowane. Z uwagi na to, e wikszo domylnych implementacji operatorw new i delete wykorzystuje funkcje maUoc() oraz free(), prawdopodobnie skoczy si to zwolnieniem pamici, bez wywoania destruktora. Jeeli wskanik usuwany za pomoc operatora delete jest zerowy, to nic si nie stanie. Z tego powodu niekiedy zaleca si przypisanie wskanikowi wartoci zerowej bezporednio po jego usuniciu, co zapobiega ponownemu usuniciu wskazywanego obiektu. Usuwanie obiektu czciej ni jednokrotnie jest zym pomysem i z pewnoci spowoduje problemy.

42

Thinking in C++. Edycja polska

>rosty przyktad
Poniszy przykad pokazuje, e w trakcie dynamicznego tworzenia obiektu jest dokonywana inicjalizacja: / / : C13:Tree.h #ifndef TREE_H #define TREE_H #include <iostream> class Tree { int height; public: Tree(int treeHeight) : height(treeHeight) {} -Tree() { std::cout << "*"; } friend std: :ostream& operator<<(std::ostream& os, const Tree* t) { return os << "Wysokosc drzewa wynosi : " << t->height << std::endl; #endif // TREE_H lll:~

II: C13:NewAndDelete.cpp // Prosta demonstracja operatorw new i delete #include "Tree.h"


using namespace std; int main() { Tree* t = new Tree(40); cout << t; delete t; Drukujc warto obiektu klasy Tree, mona udowodni, e wywoywany jest jej konstruktor. Wyprowadzenie wartoci jest w tym przypadku realizowane za pomoc przecienia operatora << w taki sposb, by mona go uy w stosunku do strumienia ostream i wskanika do obiektu klasy Tree. Jednak mimo e funkcja ta jest zadeklarowana jako zaprzyjaniona, jest ona zdefiniowana jako funkcja inline! Zrobiono to wycznie dla wygody zdefiniowanie zaprzyjanionej funkcji jako inline nie zmieniajej statusu, okrelonego sowem kluczowym friend, ani faktu, e jest ona funkcj globaln, a nie funkcj skadow klasy. Warto rwnie zauway, e warto zwracana przez t funkcj jest wynikiem caego wyraenia i jest ona typu ostream& (co jest konieczne dla zapewnienia zgodnoci z typem wartoci zwracanej przez funkcj).

Narzut menedera pamici


Podczas automatycznego tworzenia obiektw na stosie informacja o ich wielkoci i czasie ycia jest umieszczana bezporednio w generowanym kodzie, poniewa kompilatorowi znane s dokadnie: typ, wielko i zasig obiektu. Tworzenie obiektw na stercie wie si z dodatkowym narzutem, zarwno pod wzgldem czasowym, jak i pamiciowym. Poniej przedstawiono typowy scenariusz takiej operacji (funkcj malloc( ) mona zastpi przez calloc( ) lub realloc( )).

Rozdzia 13. Dynamiczne tworzenie obiektw

443

Wywoywana jest funkcja malloc(), zgaszajca danie przydziau pamici z dostpnej puli (kod ten moe w rzeczywistoci stanowi cz funkcji malloc()). W celu znalezienia dostatecznie duego bloku pamici, by zrealizowa zgoszone danie, przeszukiwanajest dostpna pula pamici. Realizuje si to zapomocprzegldujakiego rodzaju mapy lub katalogu, informujcych o tym, ktre bloki pamici s aktualnie uywane, a ktre dostpne. Proces ten jest stosunkowo szybki, ale moe wymaga szeregu prb, w zwizku z czym moe nie by deterministyczny. Nie mona wic liczy na to, e wywoanie funkcji maUoc() zajmie za kadym razem tyle samo czasu. Zanim zostanie zwrcony wskanik przydzielonego bloku pamici, jego wielko i pooenie musz zosta zapamitane, dziki czemu nastpne wywoania funkcji malloc() nie wykorzystaj go ponownie, a po wywoaniu funkcji free() system bdzie wiedzia, ile pamici naley zwolni. Wszystko to mona zaimplementowa na rne sposoby. Na przykad nic nie stoi na przeszkodzie, aby podstawowe operacje systemu przydziau pamici byy realizowane przez procesor. Jeeli ci to interesuje, to moesz napisa programy testowe, ktre pomog w okreleniu, w jaki sposb w uywanym przez ciebie systemie zostaa zaimplementowana funkcja malloc(). Moesz rwnie przeczyta kod rdowy biblioteki (zawsze dostpne srda bibliotek GNU C).

Zmiany w prezentowanych wczeniej przykadach


Wykorzystujc operatory new i delete oraz wszystkie omawiane powyej cechyjzyka, mona zmodyfikowa przykadow klas Stash, zaprezentowan w pierwszej czci ksiki. Przyjrzenie si powstaemu na nowo kodowi bdzie stanowio poyteczny przegld opisywanych zagadnie. W przedstawionych powyej rozwaaniach ani klasa Stash, ani Stack nie posiaday" wskazywanych przez siebie obiektw gdy obiekty klas Stash i Stack wykraczay poza zasig, kIasy te nie usuway wskazywanych przez siebie obiektw. Powodem, dla ktrego nie byo to moliwe, by fakt, e w ceIu zachowania oglnego charakteru klas Stash i Stack przechowyway one wskaniki typu void*. Jeeli usunie si taki wskanik, mona spodziewa sijedynie zwolnienia zajmowanej pamici z uwagi na to, e nie zawiera on adnej informacji o typie, kompilator nie wie, jaki powinien wywoa destruktor.

Usuwanie wskanika void* prawdopodobnie Wdem


Warto w tym miejscu podkreli, e wywoanie operatora delete w stosunku do wskanika typu void* stanowi niemal na pewno bd, chyba e obiekt, wskazywany przez ten wskanik, jest bardzo prosty w szczeglnoci nie powinien on posiada destruktora. Poniszy przykad pokazuje, co si w takim przypadku dzieje:

Thinking in C++. Edycja polska //: C13:BadVoi dPointerDelet i on.cpp // Usuwanie wskanikw typu void* moe powodowa // wyciekanie pamici #include <iostream> using namespace std; class Object { void* data; // Jaki obszar pamici const int size; const char id; public: Object(int sz, char c) : size(sz). id(c) { data = new char[size]; cout << "Konstrukcja obiektu " << id << ". size - " << size << endl; ~Object() { cout << "Destrukcja obiektu " << id << endl; delete []data; // W porzdku, tylko zwalniana jest // pami - nie ma potrzeby wywoywania destruktora

int main() { Object* a - new Object(40. 'a'); delete a; void* b = new Object(40. 'b'); delete b; Klasa Object zawiera wskanik typu void*, inicjalizowany za pomoc zwykych" danych (nie wskazuje obiektw posiadajcych destruktory). W destruktorze klasy Object w stosunku do tego wskanika jest wywoywany operator delete nie pociga to za sob adnych niepodanych skutkw, poniewa chcemy w ten sposb jedynie zwolni przydzielony uprzednio obszar pamici. Jak jednak wida w funkcji main(), jest bardzo wane, aby operator delete zna typ obiektu, w stosunku do ktrego zosta uyty. Wyniki dziaania programu s nastpujce: Konstrukcja obiektu a, size = 40 Destrukcja obiektu a Konstrukcja obiektu b. size - 40 Poniewa wyraenie delete a wie", e zmienna a jest wskanikiem do obiektu klasy Object, wywoywany jest destruktor i tym samym nastpuje zwalnianie pamici wskazywanej przez skadow data. Jednake w przypadku gdy operuje si obiektem, wykorzystujc wskanik typu void*, jak w wyraeniu delete b, zwalniana jest tylko pami, przydzielona obiektowi wskazywanemu przez ten wskanik. Nie jest natomiast wywoywany destruktor obiektu, a w zwizku z czym nie jest rwnie zwalniana pami, wskazywana przez skadow data. Podczas kompilacji tego programu nie pojawi si prawdopodobnie adne ostrzeenia kompilator zaoy, e programista wie, co robi. Efektem bdzie natomiast bardzo powolne wyciekanie pamici.

Rozdzia 13. Dynamiczne tworzenie obiektw

445

Jeeli w twoim programie wycieka pami, przyjrzyj si wszystkim zawartym w nim instrukcjom delete i sprawd, jakiego typu s usuwane przez nie wskaniki. Jeeli ktry ze wskanikw ma typ void*, oznacza to, e prawdopodobnie zostaa odkryta jedna z przyczyn wyciekania pamici Qezyk C++ stwarza wiele okazji umoliwiajcych wyciekanie pamici).

Odpowiedzialno za sprztanie wskanikw


W celu zapewnienia elastycznoci kontenerom Stash oraz Stack (aby mogy one przechowywa dowolne typy obiektw) bd one zawiera wskaniki typu void*. Oznacza to, e przed uyciem wskanika, zwrconego przez obiekt klasy Stash czy Stack, trzeba dokona jego rzutowania na odpowiedni typ tak jak w powyszym przykadzie. Rzutowanie takie naley rwnie wykona przed usuniciem obiektu, gdy w przeciwnym razie spowoduje to wyciekanie pamici. Drug kwesti zwizan z wyciekaniem pamici jest konieczno zapewnienia, by operator delete zosta rzeczywicie wywoany w stosunku do wszystkich obiektw, przechowywanych w kontenerze. Kontener nie moe by wacicielem" wskanika, poniewa przechowuje go jako wskanik typu void* i w zwizku z tym nie jest w stanie poprawnie przeprowadzi sprztania. Za sprztnicie obiektu musi by odpowiedzialny uytkownik. Stanowi to powany problem w przypadku, gdy do jednego kontenera doda si wskaniki, wskazujce zarwno obiekty utworzone na stosie, jak i na stercie. W stosunku do wskanikw, ktrym nie zostaa przydzielona pami na stercie, nie mona bowiem w bezpieczny sposb uy wyraenia delete (a skd, po pobraniu wskanika z kontenera, mona si dowiedzie, gdzie by on przydzielony?). Tak wic trzeba mie pewno, e obiekty przechowywane w nowej wersji kontenerw Stash i Stack zostay utworzone wycznie na stercie albo zachowujc ostrono podczas programowania, albo tworzc klasy, ktrych obiekty mog by utworzone tylko na stercie. Naley rwnie upewni si, e kIient-programista wemie na siebie odpowiedzialno za posprztanie wszystkich wskanikw zawartych w kontenerze. W zamieszczonych poprzednio przykadach mona byo zaobserwowa, w jaki sposb destruktor klasy Stack sprawdza, czy wszystkie obiekty klasy Link zostay pobrane ze stosu". W przypadku klasy Stash trzeba jednak zastosowa odmienne rozwizanie.

Stash przechowujca wskaniki


Nowa wersja klasy Stash, noszca nazw PStash, przechowuje wskaniki do obiektw utworzonych na stercie, podczas gdy wersja tej klasy, przedstawiona w poprzednich rozdziaach, kopiowaa obiekty przez warto bezporednio do kontenera. Gdy uywa si operatorw new i delete, przechowywanie wskanikw do obiektw utworzonych na stercie jest atwe i bezpieczne. Poniej zamieszczono plik nagwkowy klasy Stash w wersji przechowujcej wskaniki:

446 //: C13:PStash.h // Przechowuje wskaniki zamiast obiektw #ifndef PSTASH_H #define PSTASH_H

Thinking in C++. Edycja polska

class PStash { int quantity; // Liczba elementw pamici int next; // Nastpny pusty element // Pami wskanikw: void** storage; void inflate(int increase); public: PStash() : quantity(0), storage(0). next(0) {} ~PStash(): int add(void* element); void* operator[](int index) const; // Pobranie elementu // Usuniecie odwoania do elementu: void* remove(int index); // Liczba zapamitanych elementw: int count() const { return next; } #endif // PSTASH_H lll:~ Podstawowe elementy danych nie ulegy zasadniczym zmianom, ale skadowa storage jest obecnie tablic wskanikw typu void*. Przydzielenie pamici tej tablicy odbywa si za pomoc operatora new, a nie funkcji malloc(). W wyraeniu: void** st - new void*[quantity + increase]; typem przydzielanego obiektu jest void*, wic wyraenie to przydziela po prostu tablic wskanikw typu void*. Destruktor usuwa pami przechowujc wskaniki typu void*, natomiast nie prbuje usuwa tego, co one wskazuj ^ak ju wczeniej wspomniano, zwolnioby to jedynie pami, nie wywoujc destruktorw wskaniki typu void* nie zawieraj bowiem informacji o typie). Kolejn zmianjest zastpienie funkcji fetch( ) operatorem [ ], ktry z punktu widzenia skadni wydaje si bardziej logiczny. Poniewajednak operator ten zwraca wartoci typu void*, wic uytkownik musi ponownie pamita o typach obiektw przechowywanych w kontenerze i dokonywa rzutowania pobieranych wskanikw (problem ten zostanie rozwizany w nastpnych rozdziaach). Poniej znajdujsi definicje funkcji skadowych klasy:

}:

#include "../require.h" #include <iostream> #include <cstring> // funkcje 'mem' using namespace std: int PStash::add(void*element) { const int inflateSize - 10; if(next >= quantity) inflate(inflateSize);

#include "PStash.h"

//: C13:PStash.cpp {0} // Definicje klasy Stash, przechowujcej wskaniki

Rozdzia 13. Dynamiczne tworzenie obiektw storage[next++] - element; return(next - 1); // Numer indeksu // Obiekty nie s wasnoci kontenera: PStash::~PStash() { for(int i - 0; i < next; i++) require(storage[i] == 0. "PStash nie zostal uprzatniety"); delete []storage; // Zamiast funkcji fetch uyto przecionego operatora void* PStash::operator[](int index) const { require(index >= 0, "PStash::operator[] indeks ma wartosc ujemna"); if(index >= next) return 0; // Oznaczenie koca // Tworzenie wskanika do zadanego elementu: return storage[index]; void* PStash::remove(int index) j void* v = operator[](index); // "Usuwa" wskanik: if(v !- 0) storage[index] = 0; return v; void PStash::inflate(int increase) { const int psz - sizeof(void*); void** st = new void*[quantity + increase]; memset(st, 0, (quantity + increase) * psz); memcpy(st. storage. quantity * psz); quantity += increase: delete []storage; // Stary obszar pamici storage = st; // Wskanik do nowego obszaru pamici

447

Funkcja add( ) dziaa waciwie tak samo, jak poprzednio, jednak obecnie, zamiast kopiowania caych obiektw, w kontenerze przechowywane sjedynie wskaniki. Funkcja inflate( ), ktra w poprzedniej wersji operowaa tylko na zwykych cigach bajtw, zostaa zmodyfikowana w taki sposb, by moga obsugiwa tablic wskanikw typu void*. Przydzielony obszar pamici, zamiast stosowanego poprzednio kopiowania kolejnych elementw tablicy, jest najpierw zerowany za pomoc standardowej funkcja jzyka C memset( ) (nie jest to konieczne, poniewa klasa PStash prawdopodobnie obsuguje pami prawidowo, ale odrobina ostronoci nigdy nie zaszkodzi). Nastpnie funkcja memcpy( ) przenosi istniejce dane z dawnego obszaru pamici do nowego. Takie funkcje, jak memset( ) oraz memcpy( ), s nierzadko optymalizowane latami, dziki czemu mog dziaa znacznie szybciej ni ptle, wykorzystywane w poprzedniej wersji programu. Z drugiej strony w przypadku takich funkcji, jak inflate( ), ktre prawdopodobnie nie bd uywane zbyt czsto, mona nie zauway adnej rnicy w wydajnoci. Jednakeju tylko to, e wywoania funkcji s bardziej zwize ni Pftle, moe pomc w ustrzeeniu si bdw zwizanych z kodowaniem.

Thinking in C++. Edycja polska

W celu obarczenia klienta-programisty odpowiedzialnoci za sprztanie obiektw udostpniono dwie metody, umoliwiajce dostp do wskanikw przechowywanych w obiekcie klasy PStash operator[ ], zwracajcy wskanik, ale pozostawiajcy go w kontenerze, oraz funkcj skadow remove(), rwnie zwracajc wskanik, ale zarazem usuwajcgo z kontenera i przypisujcodpowiadajcej mu pozycji warto zerow. Gdy wywoywany jest destruktor klasy PStash, sprawdza on, czy wskaniki wszystkich obiektw zostay usunite. Jeeli tak nie jest, wywietla komunikat, co zapobiega wyciekaniu pamici (bardziej eleganckie rozwizanie tego problemu zostanie przedstawione w nastpnych rozdziaach).

Test
Poniej znajduje si program testowy klasy Stash, dostosowany do wsppracy z klasPStash: //: C13:PStashTest.cpp //{L} PStash // Test klasy Stash, przechowujcej wskaniki #include "PStash.h" #include ". ./require.h" #include <iostream> #include <fstream> #include <string> using namespace std; int main() { PStash intStash; // Operator "new" dziala rwnie z wbudowanymi typami // Zwr uwag na skadnie "pseudokonstruktora": for(int i - 0; i < 25; i++) intStash.add(newint(i)); for(int j - 0: j < intStash.count(); j++) cout << "intStash[" << j << "] = " << *(int*)intStash[j] << endl; // Sprztanie: for(int k - 0; k < intStash.count(); k++) delete intStash.remove(k); ifstream in ("PStashTest.cpp"); assure(in. "PStashTest.cpp"); PStash sthngStash; string line; while(getline(in. line)) stringStash.add(new string(line)); // Drukowanie acuchw: for(int u - 0; stringStash[u]; u++) cout << "stringStash[" << u << "] - "
<< *Cstring*)stringStash[u] << endl; // Sprztanie: for(int v - 0; v < stringStash.count(); v++) delete (string*)stringStash.remove(v);

Rozdzia 13. Dynamiczne tworzenie obiektdw

449

Podobnie jak poprzednio, obiekty klasy Stasb s tworzone, a nastpnie wypeniane informacjami jednak tym razem informacjami s wskaniki, powstae w wyniku uycia wyrae new. Zwr uwag na wiersz znajdujcy si w pierwszej czci programu:

intStash.add(new int(i));
Wyraenie new int(i) wykorzystuje skadni pseudokonstruktora, w zwizku z czym pami dla nowego obiektu typu int jest przydzielana na stercie, a nastpnie obiekt ten jest inicjalizowany wartoci zmiennej i. Podczas wydruku warto zwracana przez funkcj PStash::operator[ ] musi by rzutowana na odpowiedni typ odbywa si to rwnie w stosunku do pozostaych obiektw klasy PStash, zawartych w programie. Jest to niepodany efekt, zwizany z uyciem wskanikw typu void* w wewntrznej reprezentacji klasy. Problem ten zostanie rozwizany w nastpnych rozdziaach ksiki. Drugi z testw otwiera plik rdowy i wczytuje jego kolejne wiersze do innego obiektu klasy PStash. Kady wiersz tekstu jest wczytywany do zmiennej Iine typu string za pomoc funkcji getline(), a nastpnie, na podstawie zawartoci tej zmiennej, na stercie jest tworzony acuch zawierajcy jego niezalen kopi. Gdybymy za kadym razem przekazywali funkcji PStash::add() adres zmiennej line, to w rezultacie otrzymalibymy cagrup wskanikw, wskazujcych zmiennline, a zmienna ta zawieraabyjedynie wiersz ostatnio odczytany z pliku. Podczas pobierania wskanikw widoczne jest wyraenie: *(string*)stringStash[v] Wskanik, zwracany przez operator[ ] w celu nadania mu odpowiedniego typu, musi by rzutowany na typ string*. Nastpnie jest on wyuskiwany, dziki czemu wynikiem wyraeniajest obiekt typu string, nastpnie przekazywany przez kompilator do strumienia cout. Utworzone na stercie obiekty musz zosta zniszczone za pomoc funkcji reniove(), gdy w przeciwnym razie w czasie pracy programu pojawi si komunikat informujcy, e nie wszystkie obiekty przechowywane w obiekcie klasy PStash zostay prawidowo usunite. Zwr uwag na to, e w przypadku liczb cakowitych nie jest potrzebne rzutowanie, poniewa nie posiadaj one destruktora, a zaley nam tylko na zwolnieniu zajmowanej przez nie pamici:
delete intStash.remove(k);

Jeeli jednak w przypadku wskanikw do acuchw zapomni si o rzutowaniu, to rezultatem bdzie kolejne (powolne) wyciekanie pamici. Dlatego te rzutowanie jest niezbdne:
delete (string*)stringStash.remove(k);

Niektre z przedstawionych problemw (ale nie wszystkie) mona rozwiza za pomocszablonw (zob. rozdzia 16.).

150

Thinking in C++. Edycja polska

Operatory new i delete dla tablic


W jzyku C++ tablice mog by tworzone z rwn atwoci, zarwno na stosie, jak i na stercie, a dla kadego obiektu znajdujcego si w tablicy wywoywany jest, oczywicie, konstruktor. Obowizuje tu jednak jedno ograniczenie z wyjtkiem przypadku inicjalizacji agregatowej, dokonywanej na stosie (opisanej w rozdziale 6.), klasa musi posiada konstruktor domylny, poniewa dla kadego obiektu musi zosta wywoany konstruktor nieposiadajcy argumentw. Podczas tworzenia tablic obiektw na stercie za pomoc operatora new naley jednak uczyni co jeszcze. Przykadem takiej tablicy jest:
MojTyp* fp = new MojTyp[100];

Powysza instrukcja przydziela na stercie pami, niezbdn dla 100 obiektw klasy MojTyp, i dla kadego z tych obiektw wywouje konstruktor. Jednake obecnie dysponujemy wskanikiem typu MojTyp*, takim samym, jak uzyskany w wyniku wykonania instrukcji:
MojTyp* fp2 - new MojTyp;

tworzcej pojedynczy obiekt. Poniewa to my napisalimy kod, wiemy, e wskanik fp jest w rzeczywistoci adresem pocztku tablicy, dlatego te moliwy jest wybr elementw tej tablicy za pomoc wyrae w rodzaju fp[3]. Co jednak dzieje si w chwili niszczenia tej tablicy? Wyraenia:
delete fp2; // W porzdku delete fp: // Nie przyniesie spodziewanego rezultatu

wygldaj tak samo i ich rezultat bdzie rwnie identyczny. Dla obiektu klasy MojTyp, wskazywanego przez podany adres, zostanie wywoany destruktor, a nastpnie zwolniona pami. W przypadku wskanika fp2 dziaa to doskonale, ale dla wskanika fp oznacza, e nie zostanie wywoanych pozostaych 99 destruktorw. Zostanie jednak nadal zwolniona waciwa ilo pamici, poniewa zostaa ona przydzielona w postaci jednego, duego obszaru. Wielko tego obszarujest przechowywana w jakim miejscu przez procedur zajmujcsi przydziaem pamici. Rozwizanie tego problemu wymaga poinformowania kompilatora, e zwalniany obszar jest w rzeczywistoci adresem pocztku tablicy. Uzyskuje si to za pomoc nastpujcej skadni: delete []fp: Pusty nawias kwadratowy przekazuje kompilatorowi informacj, by wygenerowa kod. Pobiera on liczb elementw zawartych w tablicy, zapisan w jakim miejscu podczas jej tworzenia, a nastpnie wywouje destruktor dla takiej wanie liczby obiektw. W rzeczywistoci jest to udoskonalona skadnia, wywodzca si z wczeniejszej postaci instrukcji, ktrmonajeszcze czasami spotka w starszych programach: delete [100]fp: a ktra zmuszaa programist do wpisania liczby obiektw zawartych w tablicy, wprowadzajc zarazem moliwo popenienia pomyki. Dodatkowy narzut, zwizany z obsug tej liczby przez kompilator, jest bardzo niewielki, a ponadto uznano, e lepiej okreli liczb obiektw tylko wjednym miejscu programu zamiast we dwch.

Rozdzia 13. * Dynamiczne tworzenie obiektw

451

Upodabnianie wskanika do tablicy


Nawiasem mwic, warto zdefiniowanego powyej wskanika fp mona zmieni w taki sposb, by wskazywa cokolwiek, co nie ma sensu w przypadku adresu pocztku tablicy. Bardziej logiczne jest zdefiniowanie go jako staej, dziki czemu kada prba jego modyfikacji zakoczy si zgoszeniem bdu. Aby uzyska ten efekt, mona uy nastpujcej konstrukcji: int const* q - new int[10];
lub

const int* q = new int[10]; lecz w obu przypadkach sowo kluczowe const zostanie zwizane z typem int, to znaczy ze wskazywanym obiektem, a nie z samym wskanikiem. Zamiast tego naley napisa:

int* const q = new int[10];


Obecnie elementy tablicy wskazywanej przez q mog by modyfikowane, ale niedozwolona jest jakakolwiek zmiana wartoci wskanika q (np. q++) podobnie jak w przypadku zwykego identyfikatora tablicy.

Brak pamici
Co si dzieje, gdy operator new nie potrafi znale cigego obszaru pamici, wystarczajco duego, by pomieci tworzony obiekt? W takim przypadku wywoywana jest specjalna funkcja, nazywanafunkcj obslugi operatora new (ang. new handler). cilej, sprawdzany jest wskanik do tej funkcji, a jeli ma on warto niezerow, wywoywana jest wskazywana przez niego funkcja. Domylnym zachowaniem funkcji obsugi operatora new jest zgoszenie wyjtku temat ten zosta opisany w drugim tomie ksiki. Gdyjednak uywa si w programie przydziau pamici na stercie, warto przynajmniej zastpi funkcj obsugi operatora new wasn funkcj, ktra wywietli komunikat o braku pamici, a nastpnie spowoduje zakoczenie dziaania programu. W ten sposb podczas uruchamiania programu bdzie dostpna wskazwka, informujca o tym, co si stao. W ostatecznej wersji programu trzeba bdzie uy rzetelniejszej metody naprawczej. Aby wymieni funkcj obsugi operatora new, naley doczy do programu plik nagwkowy new.h, a nastpnie wywoa funkcj set_new_handler(), podajc jej adres funkcji, ktra ma zosta zainstalowana:

//: C13:NewHandler.cpp // Zmiana funkcji obsugi operatora new #include <iostream> #include <cstdlib> #include <new> using namespace std; int count - 0;

452 void out_of_memory() { cerr << "pamiec wyczerpaa sie po " << count << " przydziaach!" << endl; exit(l); int main() { set_new_handler(out_of_memory); while(l) { count++; new int[1000]; // Powoduje wyczerpanie pamici

Thinking In C++. Edycja polska

Funkcja obsugi operatora new nie moe pobiera argumentw i musi zwraca warto void. Ptla while bdzie przydziela na stercie obiekty cakowite (wyrzucajc ich adresy) dopty, dopki cakowicie wyczerpie si dostpna pami. Podczas nastpnego uycia operatora new pami nie moe ju zosta przydzielona, w zwizku z czym jest wywoywana funkcja obsugi operatora new. Zachowanie funkcji obsugi operatora new jest zwizane z operatorem new, wic jeeli przeciy si operator new (co zostanie opisane w nastpnym podrozdziale), to domylnie nie zostanie wywoana jego funkcja obsugi. Jeeli chcesz, aby funkcja ta bya nadal wywoywana, naley umieci kod wywoujcy t funkcj wewntrz przecionego operatora new. Oczywicie, mona napisa bardziej wyrafinowane funkcje obsugi operatora new, nawet takie, ktre podejmprb odzyskania pamici (sone powszechnie znane pod nazwzb/eraczy mieci). Niejest tojednak zadanie dla pocztkujcego programisty.

Przecianie operatorw new i delete


Podczas tworzenia wyraenia new najpierw, za pomoc operatora new, przydzielana jest pami, a nastpnie wywoywany jest konstruktor. Natomiast w przypadku wyraenia delete najpierw wywoywany jest destruktor, a pniej, za pomoc operatora delete, zwalniana jest pami. Wywoania konstruktora oraz destruktora nie s nigdy dostpne (w przeciwnym przypadku mona by je bowiem przypadkowo uszkodzi), mona jednak zmieni funkcje odpowiedzialne za przydzia pamici operator new oraz operator delete. System przydziau pamici, wykorzystywany przez operatory new i delete, jest przeznaczony do oglnego uytku. W szczeglnych sytuacjach moe on jednak nie spenia oczekiwa. Najczciej spotykanprzyczynzmiany sposobu przydziau pami?cl jest efektywno moe zdarzy si sytuacja, w ktrej przydzielanych i zwalnia^ nych bdzie tak wiele obiektw jakiej klasy, e obsuga pamici stanie si wskim gardem systemu. Jzyk C++ pozwala na przecienie operatorw new i delete w celu implementacji wasnego systemu alokacji pamici, dziki czemu mona upo^ ra si z takimi problemami.

Rozdzia 13. Dynamiczne tworzenie obiektw

453

Inn kwesti jest fragmentacja sterty. Podczas przydzielania pamici obiektom rnych rozmiarw moliwe jest rozdrobnienie sterty, ktre w praktyce doprowadzi do wyczerpania si dostpnej pamici. Pami mogaby byjeszcze dostpna, ale z uwagi na fragmentacj aden jej obszar nie jest dostatecznie duy, by zaspokoi aktualne potrzeby. Tworzc wasny system przydziau pamici, przeznaczony dla konkretnej klasy, mona zagwarantowa, e nigdy si to nie zdarzy. W systemach wbudowanych oraz w systemach czasu rzeczywistego moe wystpi konieczno bardzo dugiej pracy programu, dysponujcego nader ograniczonymi zasobami. System taki moe ponadto wymaga, by przydzia pamici zabiera zawsze tyle samo czasu, nie dopuszczajc moliwoci wyczerpania si pamici na stercie lub wystpienia jej fragmentacji. W takim przypadku rozwizaniem jest wasny system przydziau pamici w przeciwnym razie programici musieliby cakowicie unika uywania operatorw oew i delete, nie mogc wykorzysta tych cennych waciwoci jzyka C++. Podczas przeciania operatorw new i delete naley pamita, e zmienia si tylko sposb, wjakiprzydzielanajestzwykfapami. Kompilator wykorzysta przygotowan przez nas funkcj new, zamiast uywanej domylnie do przydzielania pamici, a nastpnie wywoa konstruktor dla przydzielonego obszaru pamici. Tak wic mimo e kompilator, widzc operator new, przydziela pami oraz wywouje konstruktor, to przeciajc operator new mona zmieni jedynie jego cz, odpowiedzialn za przydzia pamici (podobne ograniczenie dotyczy operatora delete). Przeciajc operator new, zmienia si rwniejego zachowanie zwizane z brakiem pamici. O podejmowanym wwczas dziaaniu trzeba zatem zdecydowa wewntrz tego operatora zwrci warto zerow, napisa ptl, wywoujc funkcj obsugi operatora new i ponawiajc prby przydziau pamici, lub (w typowym przypadku) zgosi wyjtek bad_alloc (omawiany w drugim tomie ksiki, dostpnym w witrynie http:/flielion.pUonline/thinking/index..html). Przecianie operatorw new i delete nie rni si od przeciania innych operatorw. Istniejejednak moliwo wyboru, czy ma zosta przeciony globalny system przydziau pamici, czy te system uywany do przydzielania pamici konkretnej kasie.

frzecianie globalnych operatorw new i delete


Jest to drastyczne posunicie, stosowane gdy globalne wersje operatorw new i delete dziaaj w sposb niezadowalajcy w caym systemie. Przecienie globalnych wersji operatorw spowoduje, e domylne operatory stan si cakowicie niedostpne nie bdzie mona ich wywoa nawet z wntrza nowych, przygotowanych przez siebie definicji. Przeciony operator new musi pobiera argument typu size_t (standardowy typ rozmiarw, okrelony w standardzie jzyka C). Argument ten jest generowany i przekazywany funkcji przez kompilator okrela on wielko obiektu, ktremu naley przydzieli pami. Funkcja musi zwrci albo wskanik do obiektu danej wielkoci (albo wikszej, jeeli s do tego jakie powody), albo warto zerow, w przypadku gdy nie mona znale wolnej pamici (wwczas konstruktor nie zostanie wywoany!).

454

Thinking in C++. Edycja polska Jednake gdy nie ma moliwoci przydzielenia danej pamici, naleaoby prawdopodobnie zrobi co bardziej konstruktywnego ni zwrcenie po prostu wartoci zerowej na przykad wywoa funkcj obsugi operatora new lub zgosi wyjtek, sygnalizujc wystpienie problemu. Wartoci zwracan przez operator new jest void*, a nie wskanik do jakiego okrelonego typu. Dostarczana jest jedynie pami, a nie tworzony obiekt. Tworzenie obiektu dokonuje si dopiero podczas wywoania konstruktora, czyli dziaania gwarantowanego przez kompilator, znajdujcego si poza naszkontrol. Operator delete pobiera natomiast wskanik typu void* do pamici, ktra zostaa uprzednio przydzielona za pomocoperatora new. Jest on typu void*, poniewa operator delete otrzymuje ten wskanik dopiero po wywoaniu destruktora, ktry pozbawia zajmowan przez obiekt pami cech waciwych obiektom. Wartoci zwracan przez operator delete jest void. Poniej znajduje si prosty przykad, ilustrujcy sposb przeciania globalnych operatorw new i delete: / / : C13:GlobalOperatorNew.cpp // Przecianie globalnych operatorw new i delete #include <cstdio> #include <cstdlib> using namespace std; void* operator new(size_t sz) { printf("operator new: %d bajtow\n", sz); void* m = malloc(sz); if(!m) puts("brak pamieci"); return m; void operator delete(void* m) puts("operator delete"): free(m); class S { int i[100]; public: SO { puts("S::SO"): } ~SO { puts("S::~SO"); } int main() { puts("tworzenie i niszczenie zmiennej calkowitej"); int* p - new int(47); delete p; puts("tworzenie i niszczenie obiektu s"); delete s; puts("tworzenie i niszczenie tablicy S[3]"); delete []sa;
S* sa - new S[3]; S* s new S;

Rozdzia 13. Dynamiczne tworzenie obiektw

455

W powyszym programie przedstawiono ogln posta przecionych operatorw new i delete. Wykorzystuj one do przydziau pamici funkcje standardowej biblioteki jzyka C malloc() oraz free() (s one prawdopodobnie uywane rwnie przez standardowe operatory new i delete!). Ponadto drukuj one komunikaty, informujce o tym, co aktualnie robi. Zwr uwag na to, e zamiast strumieni wejcia-wyjcia s tu uywane funkcje printf( ) oraz puts(). Podczas tworzenia obiektu klasy iostream (na przykad globalne obiekty cin, cout oraz cerr) jest bowiem wywoywany operator new, przydzielajcy mu pami. Uycie funkcji printf() nie grozi blokad systemu, gdy funkcja ta nie wywouje operatora new podczas swojej inicjalizacji. W funkcji main() tworzone s obiekty wbudowanych typw; maj one udowodni, e rwnie w ich przypadku wywoywane s przecione wersje operatorw new oraz delete. Nastpnie tworzony jest pojedynczy obiekt klasy S, a pniej rwnie tablica obiektw klasy S. W przypadku tablicy na podstawie liczby danych bajtw mona wywnioskowa, e przydzielana jest dodatkowa pami, suca do zapisania informacji (w obrbie tablicy) o liczbie obiektw zawartych w tablicy. We wszystkich przypadkach uywane s przecione wersje operatorw new i delete.

Przecianie operatorw new i delete w obrbie klasy


Mimo e nie ma koniecznoci jawnego deklarowania jako funkcji statycznych przecionych operatorw new i delete, znajdujcych si w obrbie klasy, utworzymy je jako statyczne funkcje skadowe. Podobnie jak poprzednio, skadnia jest taka sama, jak w przypadku przeciania kadego innego operatora. Gdy kompilator napotyka operator new, uyty do utworzenia obiektu naszej klasy, to zamiast globalnej wersji operatora wybiera funkcj skadow operator new. Jednak globalne wersje operatorw new i delete s nadal uywane w stosunku do wszystkich pozostaych typw obiektw (o ile nie majone wasnych wersji operatorw new i delete). W poniszym przykadzie dla klasy Framis utworzono prosty system przydziau pamici. Fragment pamici zostaje przydzielony na boku" w statycznym obszarze pamici, na pocztku pracy programu; pami ta jest wykorzystywana do przydzielania pamici obiektom klasy Framis. W celu okrelenia, ktre bloki zostay ju przydzielone, jest stosowana prosta tablica bajtw, zawierajca pojednym bajcie dIa kadego bloku:
/ / : C13:Framis.cpp // Lokalne przecienie operatorw new i delete |include <cstddef> // size_t |include <fstream>

#include <iostream> #include <new> using namespace std; ofstream out("Framis.out");

class Framis { enum { sz = 10 }; char c[sz]; // Nie uywana - zajmuje tylko pami static unsigned char pool[]: static bool alloc_map[]; public: enum { psize - 100 }; // Liczba dopuszczalnych obiektw

Thinking in C++. Edycja polska Framis() { out << "Framis()\n"; } -Framis() { out << "~Framis() . . . "; } void* operator new(size_t) throw(bad_alloc); void operator delete(void*); unsigned char Framis::pool[psize * sizeof(Framis)]; bool Framis::alloc_map[psize] - {false}; // Wielko jest ignorowana - zakada si. // ze jest to obiekt klasy Framis void* Framis::operator new(size_t) throw(bad_alloc) { for(int i 0; i < psize; i++) if(!alloc_map[i]) { out << "uywany blok " << i << " ... "; allocjnap[i] - true; // Oznaczenie bloku jako uywanego return pool + (i * sizeof(Framis)); out << "brak pamici" << endl; throw bad alloc(): void Framis::operator delete(void* m) { if(!m) return; // Sprawdzenie czy wskanik nie jest pusty // Zakadamy, ze obiekt zosta utworzony w dostpnej puli // Wyznaczanie numeru przydzielonego bloku: unsigned long block = (unsigned long)m - (unsigned long)pool; block /= sizeof(Framis); out << "zwalnianie bloku " << block << endl; // Oznaczenie bloku jako wolnego: alloc_map[block] = false; int main() { Framis* f[Framis::psize]; try { for(int i - 0; i < Framis::psize; i++) f[i] = new Framis; new Framis; // Brak pamici } catch(bad_alloc) { cerr << "Brak pamici!" << endl; } // Uycie zwolnionej pamici: Framis* x - new Framis; delete x; for(int j - 0; j < Framis::psize; j++) delete f[j]; // Usuwanie f[10] - w porzdku } ///:Pula pamici, uywana jako sterta dla obiektw klasy Framis, zostaa utworzona jako tablica bajtw dostatecznie dua, by pomieci w sobie psize obiektw klasy Framis. Mapa przydziau zawiera psize elementw, wic dla kadego bloku przeznaczono jedn zmienn typu bool. Wszystkie wartoci mapy przydziau zostay zainicjowane wartociami false, za pomoc sztuczki z inicjalizacj agregatow pierwszego

delete f[10]; f[10] - 0;

Rozdzia 13. Dynamiczne tworzenie obiektw

457

elementu tablicy. Polega ona na tym, e kompilator automatycznie inicjalizuje jej wszystkie pozostae elementy wartoci domyln (ktr, w przypadku typu bool, jest warto false). Lokalny operator new ma tak sam skadni, jak operator globalny. Sprawdza on jedynie map przydziau, poszukujc w niej wartoci false. Nastpnie zmienia warto znalezionego elementu na true, zaznaczajc w ten sposb, e zosta on ju przydzielony, a na koniec zwraca adres odpowiadajcego mu bloku pamici. Jeeli funkcja operatora new nie zdoa znale adnego wolnego bloku pamici, to wyprowadza do pliku ledzenia" komunikat, a nastpnie zgasza wyjtek bad_aUoc. To pierwszy przykad wyjtkw zamieszczony w niniejszej ksice. Z uwagi na to, e wyjtki zostay dokadnie omwione dopiero w drugim tomie ksiki, zaprezentowano w tym miejscu jedynie bardzo prosty sposb ich uycia. W funkcji operatora new mona zauway dwa elementy zwizane z obsug wyjtkw. Po pierwsze, bezporednio po licie argumentw funkcji znajduje si specyfikacja throw(bad_alloc), informujca kompilator (oraz czytelnika), e funkcja moe zgosi wyjtek typu bad_alloc. Po drugie, jeli nie mona przydzieli pamici, to funkcja zgasza wyjtek za pomoc instrukcji throw bad_alloc. Gdy zostanie zgoszony wyjtek, funkcja przerywa swoj prac, a sterowanie zostaje przekazane do procedury obsugi wyjtku (ang. exception handler), zrealizowanej w postaci klauzuli catch. W funkcji main() mona dostrzec inny element tego obrazu, ktrym jest klauzula try-catch. Blok try jest zawarty w nawiasie klamrowym i znajduje si w nim cay kod, ktry moe zgosi wyjtki w tym przypadku s to wywoania operatora new, zawierajce obiekty klasy Framis. Bezporednio po bloku try nastpuje jedna lub wicej klauzul catch, z ktrych kada okrela typ wykrywanego przez siebie wyjtku. W tym przypadku klauzula catch(bad_alloc) informuje, e wyapane zostan wyjtki bad_aUoc. Klauzula ta zostanie wykonana tylko wtedy, gdy nastpi zgoszenie wyjtku typu bad_alloc, a pniej wykonanie programu bdzie kontynuowane od miejsca znajdujcego si za ostatni spord klauzul catch, tworzcych grup (w tym przypadku istnieje tylkojedna klauzula catch, ale moe by ich wicej). W powyszym przykadzie mona uywa strumieni wejcia-wyjcia, poniewa globalne operatory new i delete pozostay nienaruszone. Operator delete zakada, e adres obiektu klasy Framis zosta utworzony w obrbie dostpnej puli. Zaoenie to jest usprawiedliwione, poniewa lokalny operator new jest wywoywany za kadym razem, gdy na stercie tworzony jest pojedynczy obiekt klasy Framis. Jednak nie dzieje si tak w przypadku tablicy obiektw w stosunku do tablic wywoywany jest globalny operator new. Tak wic uytkownik moe przypadkowo wywoa operator delete, nie uywajc pustego nawiasu kwadratowego, sygnalizujcego destrukcj tablicy. Mogoby to spowodowa problemy. Uytkownik moe rwnie poda wskanik do obiektu utworzonego na stosie. Jeeli takie sytuacje wydajsi moliwe, to naleaoby doda wiersz, sprawdzajcy, czy adres zawartyjest w obrbie puli, z ktrej przydzielana jest pami, i czy wskazuje on pocztek bloku (by moe dostrzegasz ju moliwo przecienia operatorw new i delete w celu wykrycia wyciekw pamici).

458

Thinking in C++. Edycja polska Operator delete wyznacza numer bloku reprezentowanego przez wskanik, a nastpnie przypisuje znacznikowi mapy przydziau tego bloku warto false, oznaczajc, e blok ten zosta zwolniony. W funkcji main() zostaje dynamicznie przydzielonych tyle obiektw klasy Framis, by zabrako dla nich pamici umoliwia to sprawdzenie zachowania operatora new w razie braku pamici. Nastpniejeden z obiektwjest usuwany, a inny tworzony, by pokaza, e zwolniona pami moe by powtrnie wykorzystana. Z uwagi na to, e przyjty sposb przydziau pamici zosta zaprojektowany specjalnie dla obiektw klasy Framis, jest prawdopodobnie znacznie szybszy ni system przydziau pamici oglnego zastosowania, uywany w przypadku domylnych operatorw new i delete. Naley jednak wiedzie, e nie bdzie on automatycznie dziaa w razie wykorzystania dziedziczenia (opisanego w rozdziale 14.).

Przecianie operatorw new i delete w stosunku do tablic


Jeeli operatory new i delete zostan przecione w obrbie kasy, to zostan one wywoane za kadym razem, gdy bdzie dynamicznie tworzony obiekt tej klasy. Jednak jeeli utworzy si tablic tych obiektw, to do przydzielenia obszaru pamici, ktry pomieci od razu ca t tablic, zostanie wywoany globalny operator new[ ], a do zwolnienia tej pamici globalny operator delete[ ]. Przydzia pamici tablicom obiektw mona kontrolowa, przeciajc w obrbie klasy specjalne wersje operatora new[ ] oraz operatora delete[ ]. Poniszy przykad pokazuje, kiedy wywoywane s te wersje operatorw: / / : C13:ArrayOperatorNew.cpp // Operator new dla tablic #include <new> // Definicja size_t

#include <fstream> using namespace std; ofst ream t race("ArrayOperatorNew.out"); class Widget { enum { sz = 10 }; int i[sz]; public: Widget() { trace << "*"; } -Widget() { trace << "-"; } void* operator new(size_t sz) { trace << "Widget::new: " << sz << " bajtow" << endl; return ::new char[sz]; }

void operator delete(void* p) { trace << "Widget::delete" << endl; ::delete []p; } void* operator new[](size_t sz) { trace << "Widget::new[]: " << sz << " bajtow" << endl; return ::newchar[sz];

Rozdzia 13. Dynamiczne tworzenie obiektw

459

void operator delete[](void* p) { trace << "Widget::delete[]" << endl; ::delete []p;

int main() { trace << "new Widget" << endl; Widget* w = new Widget; trace << "\ndelete Widget" << endl; delete w; trace << "\nnew Widget[25]" << endl; Widget* wa = new Widget[25]; trace << "\ndelete []Widget" << endl; delete []wa;
W powyszym przykadzie wywoywane s globalne wersje operatorw new i delete. Rezultatjest wic taki sam, jakby w ogle nie byo przecionych wersji tych operatorw, z wyjtkiem tego, e zostay dodane informacje, umoliwiajce ledzenie wykonywania kodu. Oczywicie, w przecionych wersjach operatorw new i delete mona wykorzysta dowolny system przydzielania pamici. A zatem skadnia operatorw new i delete przeznaczonych dla tablic jest taka sama, jak w przypadku ich wersji dla indywidualnych obiektw, z wyjtkiem uycia nawiasw kwadratowych. W obu przypadkach dostarczana jest informacja o wielkoci pamici, ktra musi zosta przydzielona. Wielko przekazana wersji dziaajcej w przypadku tablic bdzie wielkoci caej tablicy. Warto zapamita, e jedynym dziaaniem wymaganym od przecionego operatora new jest zwrcenie przez niego wskanika do odpowiednio duego bloku pamici. Mimo e mona by rwnie w tym miejscu dokona inicjalizacji przydzielonej pamici, to zwykle jest to zadanie konstruktora, ktry zostanie automatycznie wywoany dla tego obszaru pamici przez kompilator. Konstruktor i destruktor drukujpo prostu znaki, dziki ktrym mona zobaczy, kiedy zostay one wywoane. Oto jak wyglda zawarto pliku ledzenia dla przykadowego kompilatora: new Widget Widget: :new: 40 bajtow * delete Widget ~Widget: :delete new Widget[25] Widget::new[]: 1004bajtow delete []Widget ------Widget::delete[]

Jak si tego mona byo spodziewa, utworzenie indywidualnego obiektu wymagao 40 bajtw (na tym komputerze liczby cakowite zajmuj cztery bajty). Wywoywany jest operator new, a nastpnie konstruktor (oznaczony znakiem *). Analogicznie, wywoanie delete powoduje, e najpierw wywoywanyjest destruktor, a nastpnie operator delete.

460

Thinking in C++. Edycja polska

Jak ju wspomniano, podczas tworzenia tablicy obiektw klasy Widget jest wywoywana tablicowa" wersja operatora new. Zwr jednak uwag na to, e dany rozmiar pamici jest wikszy o cztery bajty od oczekiwanego. Te cztery bajty su systemowi do zapamitania informacji dotyczcych tablicy, a szczeglnie liczby znajdujcych si w niej obiektw. Tak wic w zapisie: delete []widget; nawias klamrowy informuje kompilator, e usuwana jest tablica obiektw. Dziki temu kompilator generuje kod, ktry odczytuje liczb elementw zawartych w tablicy, a nastpnie tylokrotnie wywouje destruktor. Mona zauway, e chocia tablicowe wersje operatorw new i delete s wywoywane tylko jednokrotnie dla caego obszaru zajmowanego przez tablic to domylny konstruktor oraz destruktor s wywoywane w stosunku do kadego obiektu znajdujcego si w tablicy.

Wywotania konstruktora
Majc na uwadze, e instrukcja: MojTyp* f * new MojTyp; najpierw wywouje operator new, przydzielajc obszar pamici mieszczcy obiekt typu MojTyp, a nastpnie wywouje dla tego obszaru konstruktor klasy MojTyp, mona postawi pytanie, co si stanie, gdy nie powiedzie si przydzielenie pamici za pomoc operatora new? W takim przypadku konstruktor nie jest wywoywany, wic mimo e nadal dysponujemy jedynie niepomylnie skonstruowanym obiektem, to przynajmniej nie zostaje dla niego wywoany konstruktor, ktremu musiaaby zosta przekazana zerowa warto wskanika this. Oto przykad, ktry tego dowodzi: //: C13:NoMemory.cpp // Konstruktor nie jest wywoywany, gdy wywoanie // operatora new skoczy si niepowodzeniem #include <iostream>
#include <new> // Definicja bad_alloc

using namespace std;

class NoMemory { public: NoMemory() { cout << "NoMemory::NoMemory()" << endl: void* operator new(size_t sz) throw(bad_alloc)( cout << "NoMemory::operator new" << endl; throw bad_alloc(); // "Brak pamici"

int main() { NoMemory* nm = 0:

try {

nm = new NoMemory; } catch(bad_alloc) { cerr << "Wyjtek braku pamici" << endl;

} l//:~

cout << "nm = " << nm << endl ;

Rozdzia 13. Dynamiczne tworzenie obiektw

461

Podczas pracy programu nie jest wywietlany komunikat konstruktora, a jedynie komunikat operatora new oraz wywietlany przez procedur obsugi wyjtku. Poniewa nigdy nie nastpuje powrt z funkcji new, nigdy nie jest te wywoywany konstruktor i w zwizku z tym ani razu nie jest te drukowany jego komunikat. Wane jest, aby wskanik nm zosta zainicjowany wartoci zerow, poniewa wyraenie new nigdy nie zostaje wykonane do koca. Warto wskanika powinna natomiast wynosi zero, by mie pewno, e nigdy nie zostanie on niewaciwie uyty. Jednake w procedurze obsugi wyjtku naleaoby w rzeczywistoci zrobi co wicej ni tylko wydrukowanie komunikatu i kontynuowanie programu w taki sposb, jakby obiekt zosta prawidowo utworzony. W idealnym przypadku trzeba by uczyni co, co umoliwioby programowi rozwizanie problemu. Przynajmniej naley zakoczyjego prac, odnotowujc wczeniej wystpienie bdu w dzienniku. We wczeniejszych wersjach jzyka C++ standardow praktyk w przypadku braku pamici byo zwracanie przez operator new wartoci zerowej. Zapobiegao to wywoaniu konstruktora. Jeeli jednak podejmie si prb zwrcenia wartoci zerowej operatora new, to zgodny ze standardem jzyka kompilator powinien poinformowa, e naley zamiast tego zgosi wyjtek bad_alloc.

Operatory umieszczania new i delete


Istniejjeszcze dwa inne, rzadziej spotykane zastosowania przecionego operatora new: 1. Zamierzamy umieci obiekt w okrelonym miejscu pamici. To szczeglnie wane w przypadku sprztowych systemw wbudowanych, w ktrych obiekt moe by synonimemjakiego elementu sprztowego. 2. Podczas wywoania funkcji new chcemy mie moliwo wyboru spord rnych sposobw przydziau pamici. W obu tych sytuacjach wykorzystamy ten sam mechanizm: przeciony operator new moe pobiera wicej ni jeden argument. Jak ju wiadomo, pierwszym jego argumentem jest zawsze wielko obiektu, niejawnie wyznaczana i przekazywana przez kompilator. Pozostae argumenty mog by jednak dowolne adres, pod ktrym chcemy umieci obiekt, referencja do funkcji przydzielajcej mu pami, obiekt lub cokolwiek innego, co tylko moe okaza si przydatne. Sposb, w jaki przekazywane s operatorowi new dodatkowe argumenty, moe na pierwszy rzut oka wydawa si nieco osobliwy. Naley umieci list argumentw (bez argumentu size_t, obsugiwanego przez kompilator) po sowie kluczowym new, a przed nazw klasy, ktrej obiektjest tworzony. Na przykad w instrukcji: X* xp = new(a) X; zmienna a zostanie przekazana operatorowi new jako drugi argument. Oczywicie, bdzie to dziaa tylko w przypadku, gdy taki operator new zostanie w ogle zdefiniowany. Poniszy przykad ilustruje, w jaki sposb mona umieci obiekt pod konkretnym adresem:

462 _ Thinking

in

C++.

Edycja

polska

/ / : C13:PlacementOperatorNew.cpp // Umieszczanie za pomoc operatora new #include <cstddef> // S1ze_t #include <iostream> using namespace std; class X { public: X(int ii = 0) : i(ii) { cout << "this = " << this << endl;
} ~XO {
int i ;

cout << "X::-X(): " << this << endl;

void* operator new(size_t. void* loc) { return loc;

int main() { int 1[10]; cout << "1 = " << 1 << endl ; X* xp = new(l) X(47); // Obiekt X jest umieszczany w tablicy 1 xp->X::-XC): // Jawne wywoanie destruktora // Uywane TYLKO w przypadku umieszczania! Zwr uwag na to, e operator new zwraca jedynie wskanik, ktry jest mu przekazywany. Tak wic posta wywoujcego kodu decyduje o tym, gdzie w pamici zostanie umieszczony obiekt, a nastpnie jako cz wyraenia new dla tego obszaru pamici zostanie wywoany konstruktor. Mimo e powyszy przykad prezentuje tylko jeden dodatkowy argument, nic nie stoi na przeszkodzie, by doda kolejne, jeeli sone potrzebne do innych celw. Problemem jest natomiast usunicie obiektu. Istnieje tylko jedna wersja operatora delete, nie ma wic moliwoci, by nakaza: do tego obiektu uyj mojego specjalnego mechanizmu, zwalniajcego pami". Zamierzamy wywoa destruktor; nie chcemy jednak, by pami zostaa zwolniona za pomoc mechanizmu obsugujcego pami dynamiczn, poniewa nie zostaa ona przydzielona na stercie. Rozwizaniem tego problemu jest bardzo nietypowa skadnia. Uywajc zapisu: xp->X::~X(); // Jawne wywoanie destruktora mona jawnie wywoa destruktor. Jednak konieczne jest w tym miejscu powane ostrzeenie. Niektrzy traktuj to jako sposb niszczenia obiektw w dowolnej chwili, zanim jeszcze skoczy si ich zasig. Naley natomiast skorygowa ten zasig lub (bardziej poprawnie) uy dynamicznego tworzenia obiektw (w przypadku gdy czas ycia obiektw ma by okrelany w trakcie wykonania programu). Wywoanie destruktora w stosunku do obiektu utworzonego na stosie spowoduje powane problemy. poniewa zostanie on wywoany ponownie, na kocu zasigu obiektu. Jeeli natomiast destruktor zostanie wywoany w taki sposb w stosunku do obiektu utworzoneg0

Rozdzia 13. << Dynamiczne tworzenie obiektw

463

na stercie, to zostanie on co prawda wykonany, ale nie nastpi zwolnienie pamici zajmowanej przez obiekt. A zatem cel nie zostanie prawdopodobnie osignity. Jedynym powodem, dla ktrego destruktor moe zosta wywoany jawnie w taki sposb, jest obsuga skadni umieszczania operatora new. Istnieje rwnie operator umieszczania delete, wywoywany jedynie wwczas, gdy konstruktor wyraenia umieszczania new zgosi wyjtek (dziki czemu pami zostanie automatycznie wyczyszczona w przypadku wystpienia wyjtku). Operator umieszczania delete posiada list argumentw, odpowiadajc operatorowi umieszczania new, ktry zosta wywoany przed zgoszeniem wyjtku. Temat ten opisano w rozdziale powiconym obsudze wyjtkw w drugim tomie ksiki.

Podsumowanie
Tworzenie automatycznych zmiennych na stosie jest wygodne i najbardziej efektywne. Jednake do rozwizywania oglnych problemw programistycznych konieczna jest moliwo tworzenia i niszczenia obiektw w trakcie wykonywania programu szczeglnie gdy jest to wynikiem reakcji na informacje pochodzce spoza programu. Mimo e mechanizm dynamicznego przydziau pamici jzyka C przydziela pami na stercie, to nie jest on atwy w uyciu, a ponadto nie gwarantuje dokonania konstrukcji, koniecznej w jzyku C++. Dziki przeniesieniu mechanizmu dynamicznego tworzenia obiektw do wntrzajzyka tworzenie obiektw na stercie za pomoc operatorw new i delete jest rwnie proste, jak w przypadku tworzenia ich na stosie. Dodatkow korzyci takiego rozwizania jest jego dua elastyczno. Jeli dziaanie operatorw new i delete nie odpowiada naszym potrzebom zwaszcza gdy nie jest ono dostatecznie efektywne mona je zmieni. Istnieje rwnie moliwo modyfikacji zachowania programu w przypadku, gdy skoczy si dostpna na stercie pami.

wiczenia
Rozwizania wybranych wicze znajduj si w dokumencie elektronicznym The Thinking in C++ Annotated Solution Guide, ktry mona pobra za niewielk opat z witryny http://www.BruceEckel.com. 1. Utwrz kas Counted, zawierajca skadow cakowit id, oraz statyczn skadow cakowit count. Domylny konstruktor tej klasy powinien rozpoczyna si od: ,,Counted(): id(count++) {". Powinien on rwnie drukowa warto skadowej id oraz informowa, e obiekt zosta utworzony. Destruktor powinien informowa o tym, e zosta wywoany i rwnie drukowa warto skadowej id. Przetestuj dziaanie kasy. 2. Tworzc obiekt klasy Counted (z poprzedniego wiczenia) za pomoc operatora new i usuwajc go za pomocoperatora delete, upewnij si, e podczas uycia operatorw new i delete zawsze wywoywane s konstruktory i destruktory. Utwrz rwnie na stercie tablic obiektw tej klasy, a nastpniejzniszcz.

164

Thinking in C++. Edycja polska 3. Utwrz obiekt klasy PStash i wypenij go obiektami klasy, opisanej w wiczeniu 1., utworzonymi za pomocoperatora new. Zobacz, co si stanie, gdy skoczy si zasig obiektu klasy PStash i wywoany zostanie jego destruktor. 4. Utwrz wektor vector< Counted*> i wypenij go wskanikami do obiektw klasy Counted, opisanej w wiczeniu 1. Obiekty te utwrz za pomoc operatora new. Przejd przez wszystkie elementy wektora, drukujc zawarte w nim obiekty, a nastpnie przejd przez nie ponownie, usuwajc (za pomoc operatora delete) kady z nich. 5. Powtrz poprzednie wiczenie, dodajc do klasy Counted funkcj skadow f(), drukujc komunikat. Przejd przez wszystkie elementy wektora, wywoujc dla kadego obiektu funkcj f(). 6. Powtrz wiczenie 5., wykorzystujc klas PStash. 7. Powtrz wiczenie 5., wykorzystujc plik Stack4.h, zamieszczony w rozdziale 9. 8. Utwrz dynamicznie tablic obiektw klasy Counted (z wiczenia 1.). W stosunku do otrzymanego wskanika zastosuj operator delete nie uywajc nawiasu kwadratowego. Wyjanij uzyskane rezultaty. 9. Utwrz obiekt klasy Counted (z wiczenia 1.) za pomoc operatora new, dokonaj rzutowania otrzymanego wskanika na typ void*, a nastpnie go usu. Wyjanij uzyskane rezultaty. 10. Za pomoc swojego komputera uruchom program NewHandler.cpp i zobacz, jakwywietli on warto licznika. Oblicz wielko pamici dostpnej dla programu. 3il. Utwrz klas posiadajcprzecione operatory nw i delete, zarwno w wersjach dla pojedynczych obiektw, jak i dla tablic. Poka, e obie wersje tych operatorw dziaaj poprawnie. 3L2. Wymyl test dla programu Framis.cpp, ktry pozwoli na oszacowanie, o ile szybciej dziaaj wasne wersje operatorw new i delete w porwnaniu z operatorami globalnymi. Z3. Zmodyfikuj klas NoMemory w taki sposb, by zawieraa tablic liczb cakowitych i, zamiast zgasza wyjtek bad_alloc, przydzielaa pami. W funkcji main() utwrz podobndo zawartej w programie NewHandler.cpp ptl while, powodujacprzekroczenie dostpnej pamici, i zobacz, co si stanie w przypadku, gdy funkcja operator new nie bdzie sprawdzaa, czy pami zostaa pomylnie przydzielona. Nastpnie dodaj do funkcji operator new test, zgaszajcy wyjtek bad_alloc. 14. Utwrz klas zawierajcoperator umieszczania new, posiadajcy drugi argument typu string. Klasa powinna zawiera statyczny wektor vector<string>, w ktrym przechowywany bdzie drugi argument operatora new. Operator umieszczania new powinien przydziela pami w normalny sposb. W funkcji main() wywoaj przygotowany przez siebie operator umieszczania new, z argumentem' bdcym acuchem, opisujcym wywoania (moesz uy do tego zadania makroinstrukcji preprocesora FE oraz Lft4E__).

Rozdzia %3. Dynamiczne tworzenie obiektw

465

15. Zmodyfikuj program ArrayOperatorNew.cpp, tworzc statyczny wektor vector<Widget*> i dopisujc do niego adres kadego obiektu klasy Widget, przydzielonego za pomocfunkcji operator new i usuwajc go, gdyjest on zwalniany za pomoc funkcji operator delete (aby dowiedzie si, jak to zrobi, moesz zajrze do informacji dotyczcych klasy vector, zawartych w dokumentacji standardowej biblioteki jzyka C++ lub w drugim tomie ksiki, dostpnym w Internecie). Utwrz drug klas 0 nazwie MemoryChecker, posiadajcdestruktor, ktry drukuje liczb wskanikw obiektw klasy Widget, zawartych w wektorze. Napisz program, zawierajcy pojedynczy, globalny egzemplarz obiektu klasy MemoryChecker 1 przydzielajcy dynamicznie oraz niszczcy w funkcji main() szereg obiektw i tablic obiektw klasy Widget. Poka, e klasa MemoryChecker ujawnia wystpujce w programie wycieki pamici.

Thinking in C++. Edycja polska

Dziedziczenie i kompozycja
Do najbardziej przekonywajcych cech jzyka C++ naley moliwo wielokrotnego wykorzystywania kodu. Jednak aby miaa ona przeomowe znaczenie, musi pozwala na wicej ni tylko na kopiowanie kodu i jego modyfikacj. To ujcie, stosowane w jzyku C, nie sprawdza si zbyt dobrze. Podobnie jak wszystko w jzyku C++, rozwizanie tego problemu jest zwizane z klasami. Kod jest wykorzystywany wielokrotnie dziki tworzeniu nowych klas. Zamiast jednak generowa je od pocztku, uywa si w tym celu ju istniejcych, ktre zostay przez kogo utworzone i uruchomione. Sztuczka polega na tym, by wykorzystywa te klasy, nie naruszajc istniejcego kodu. W tym rozdziale poznamy dwa sposoby osignicia tego celu. Pierwszy jest do oczywisty wewntrz klas istniejcych tworzy si obiekty nowych. Nosi to nazw kompozycji (ang. composition), poniewa nowe klasy s zestawiane z obiektw tych klas, ktreju istniej. Drugie podejciejest bardziej wyrafinowane. Nowa klas tworzy sijako rodzaj)uz istniejcej klasy. Pobiera si dosownie posta istniejcej klasy, dodajc do niej kod i nie modyfikujc jednoczenie oryginalnej klasy. Ta magiczna czynno nazywana jest dziedziczeniem (ang. inheritance), a wikszo zwizanej z nim pracy wykonuje kompilator. Dziedziczenie jest jednym z kamieni wgielnych programowania obiektowego i pociga za sobdodatkowe konsekwencje, omwione w rozdziale 15. Okazuje si, e zarwno skadnia, jak i dziaanie kompozycji i dziedziczenia s w duej mierze do siebie podobne (co jest logiczne obie metody umoliwiaj bowiem tworzenie nowych typw na podstawie typw ju istniejcych). W tym rozdziale przedstawimy mechanizmy, umoliwiajce wielokrotne wykorzystywanie kodu.

Rozdzia 14.

Skadnia kompozycji
W rzeczywistoci uywalimy kompozycji od samego pocztku do tworzenia klas Klasy komponowalimy z obiektw typw wbudowanych (a czasami z acuchw) Okazuje si, e kompozycj mona rwnie atwo stosowa w przypadku typw zdefiniowanych przez uytkownika. Wemy pod uwag klas, ktra mogaby zjakiego powodu okaza si przydatna: / / : C14:Useful.h // Klasa do ponownego wykorzystania #ifndef USEFUL_H #define USEFUL_H class X { public:

int i;

X() { i = 0: }

void set(int ii) { i - i i : } int read() const { return i: } int permute() { return i - i * 47; } #endif // USEFUL_H II/:Dane skadowe zawarte w tej klasie s prywatne, wic mona zupenie bezpiecznie osadzi obiekt klasy X w innej klasie, w charakterze obiektu publicznego, co czyni interfejs tej klasy do prostym: //: C14:Composition.cpp // Powtrne wykorzystanie kodu za pomoca. kompozycji #include "Useful.h" class Y { public: X x; // Osadzony obiekt

}:

int i:

Y() { i - 0: }

void f(int ii) { i = ii: } int g() const { return i; } int main() { y.f(47): y.x.set(37): // Dostp do osadzonego obiektu Dostp do funkcji skadowych osadzonego obiektu (okrelanego rwnie mianem obiektupodrzdnego] wymaga wyboru dodatkowej skadowej. Znacznie czciej mona spotka prywatne obiekty osadzone, dziki czemu staj si one elementem wewntrznej implementacji kiasy (co oznacza, e implementacja ta moe by dowolnie zmieniana). Funkcje tworzce publiczny interfejs nowo utworzonej klasy mog zawiera odwoania do osadzonego obiektu, ale niekoniecznie naladujjego interfejs:

V y:

//: C14:Composition2.cpp // Prywatny obiekt osadzony #include "Useful.h" class Y { int i; X x; // Osadzony obiekt public: Y() { i = 0; } void f(int ii) { i = i i ; x.set(ii); int g() const { return i * x.read(): void permute() { x.permute(); } int main() {

Yy; y.f(47);

} III-

y.permute();

W powyszym przykadzie funkcja permutate( ) zostaa przeniesiona do interfejsu nowej klasy, lecz pozostae funkcje skadowe klasy X s uywane wycznie wewntrz funkcji skadowych klasy Y.

Skadnia dziedziczenia
Skadnia zwizana z kompozycjjest do oczywista, ale dziedziczenie wymaga zastosowania zupenie nowej, odmiennej notacji. Stosujc dziedziczenie, oznajmia si: nowa klasajest podobna do tamtej, istniejcej ju klasy". Oznacza si to w kodzie programu, podajac,jak zwykle, nazw nowej klasy. Przed otwierajcym nawiasem klamrowym, rozpoczynajcym ciao klasy, umieszcza si dwukropek oraz nazw klasy podstawowej (lub klas podstawowych, oddzielonych przecinkami - - w przypadku wielokrotnego dziedziczenia). W rezultacie wszystkie dane oraz funkcje skadowe klasy podstawowej znajd si automatycznie w nowo utworzonej klasie. Ilustruje to poniszy przykad: //: C14:Inheritance.cpp // Proste dziedziczenie #include "Useful.h" #include <iostream> using namespace std; class Y : public X { int i; // Inne ni i klasy X public: Y() { i = 0; } int change() { i = permute(); // Wywoanie funkcji o innej nazwie return i; } void set(int ii) (

Thinking in C++. Edycja polska

X::set(ii): // Wywoanie funkcji o tej samej nazwie

1 = ii ;

int main() { cout << "sizeof(X) - " << sizeof(X) << endl; cout << "sizeof(Y) = " << sizeof(Y) << endl; Y D; O.change(); // Wykorzystanie funkcji interfejsu klasy X: D.read(); D.permute(); // Zdefiniowane ponownie funkcje zasaniaj // funkcje klasy podstawowej: D.set(12); } IIIA zatem klasa Y dziedziczy elementy klasy X, co oznacza, e klasa Y bdzie posiadaa wszystkie dane i funkcje skadowe, zawarte w klasie X. W rzeczywistoci klasa Y zawiera obiekt podrzdny klasy X zupenie taki sam, jaki powstaby w rezultacie utworzenia w klasie Y skadowej, bdcej obiektem klasy X, a nie w w y n i k u dziedziczenia. Zarwno obiekty skadowe, jak i pami zajmowana przez klas podstawow sokrelane mianem obiektw podrzdnych. Wszystkie prywatne skadowe klasy X pozostaj skadowymi prywatnymi w klasie Y czyli fakt dziedziczenia przez klas Y po klasie X nie oznacza wcale, e klasa Y moe naruszy mechanizmy ochrony klasy X. Prywatne elementy skadowe klasy X znajduj si nadal na swoim miejscu, zajmujc pami nie mona si tylko bezporednio do nich odwoa. Jak wida w funkcji main( ), dane skadowe klasy Y zostay poczone z danymi klasy X, co mona wywnioskowa z faktu, e warto zwracana przez wyraenie sizeof(Y) jest dwukrotnie wiksza ni warto zwracana przez sizeof(X). Nazw klasy podstawowej poprzedzono sowem kluczowym public. Podczas dziedziczenia wszystko jest domylnie prywatne. Gdyby nazwa klasy podstawowej nie zostaa poprzedzona sowem kluczowym public, oznaczaoby to, e wszystkie skadowe publiczne klasy podstawowej stayby si w klasie pochodnej skadowymi prywatnymi. Prawie nigdy niejest to naszym zamiarem1 zazwyczaj podanejest pozostawienie wszystkich skadowych publicznych klasy podstawowej skadowymi publicznymi w klasie pochodnej. Uzyskuje si to, uywajc podczas dziedziczenia sowa kluczowego public. ^ W funkcji change() wywoywanajest funkcja klasy podstawowej permute(). Klasa pochodna ma bezporedni dostp do wszystkich publicznych funkcji klasy podstawowej. Funkcja set(), znajdujca si w klasie pochodnej, zmienia definicj funkcji set(), za" wartej w klasie podstawowej. Oznacza to, ejeeli w stosunku do obiektw klasy V Kompilatorjzyka Java nie pozwala na obnienie poziomu dostpu do skadowej podczas dziedziczenia.

Rozdzia 14. Dziedziczenie i kompozycja

471

zostan wywoane funkcje read( ) oraz permute( ), to wywoane zostan wersje tych funkcji, zawarte w klasie podstawowej. Jeeli jednak w stosunku do obiektu klasy Y uyje si funkcji set(), to zostanie wywoana jej przedefiniowana wersja. Wynika z tego, e jeeli nie odpowiada nam wersja jakiej funkcji, pozyskanej za pomoc dziedziczenia, to moemy zmieni jej dziaanie (mona rwnie doda do klasy zupenie nowe funkcje,jak w przypadku funkcji change()). Jednake po zmianie definicji moe zaistnie potrzeba wywoaniajej wersji, zawartej w klasie podstawowej. Jeeli wewntrz funkcji set() wywoa si po prostu funkcj set(), to wywoana zostanie rekurencyjnie lokalna wersja funkcji. W celu wywoania wersji funkcji, zawartej w klasie podstawowej, naleyjawnie wskaza klas podstawow, uywajc do tego celu operatora zasigu.

Lista inicjatorw konstruktora


Wiadomojuz,jak wanejest wjzyku C++ zagwarantowanie prawidowej inicjalizacji w takim samym stopniu dotyczy to rwnie kompozycji oraz dziedziczenia. Podczas tworzenia obiektu kompilator zapewnia wywoanie konstruktorw dla wszystkich jego obiektw podrzdnych. W przedstawionych do tej pory przykadach wszystkie obiekty podrzdne posiaday domylne konstruktory, wywoywane przez kompilator. Co si jednak dzieje w przypadku, gdy obiekty skadowe nie maj domylnych konstruktorw, albo gdy chcemy zmieni domylny argument ktrego z konstruktorw? Stanowi to problem, poniewa konstruktor nowej klasy nie ma dostpu do prywatnych danych skadowych obiektu podrzdnego, nie moe wic ich bezporednio zainicjowa. Rozwizanie jest proste naley wywoa konstruktor obiektu podrzdnego. Jzyk C++ udostpnia w tym celu specjaln konstrukcj, nazywan list inicjatorw konstruktora (ang. constructor initializer list). Posta tej listy stanowi odzwierciedlenie skadni dziedziczenia. W przypadku dziedziczenia nazwa kIasy podstawowej umieszczana jest po dwukropku, a przed otwierajcym nawiasem klamrowym, rozpoczynajcym ciao klasy. Na licie inicjatorw konstruktora wywoania konstruktorw obiektw podrzdnych umieszczane s natomiast za list argumentw konstruktora i dwukropkiem, a przed otwierajcym nawiasem klamrowym, rozpoczynajcym ciao funkcji. W przypadku klasy MojTyp, dziedziczcej z klasy Bar, moe to przybra nastpujcposta:
MojTyp::MojTyp(int i) : Bar(i) { // ...

pod warunkiem, e klasa Bar posiada konstruktor, pobierajcy pojedynczy argument typu cakowitego.

lnicjalizacja obiektw sktedowych


Okazuje si, e takiej samej skadni uywa si do inicjalizacji obiektw skadowych w przypadku kompozycji. Zamiast nazw klas podaje si jednak wwczas nazwy

472

Thinking in C++. Edycja polska

obiektw. Jeli na licie inicjatorw zostanie umieszczona wiksza liczba wywoa konstruktorw, to oddziela sije przecinkami:
MojTyp2::MojTyp2(1nt i) : Bar(i). m(i+l) { // ...

Jest to pocztek konstruktora klasy MojTyp2, dziedziczcej po klasie Bar i zawierajcej obiekt skadowy o nazwie m. Zwr uwag na to, e o ile na licie inicjatorw konstruktora widoczny jest typ klasy podstawowej, o tyle w przypadku obiektu skadowego widocznyjestju tylkojego identyfikator.

Typy wbudowane znajdujce si na licie inicjatorw


Lista inicjatorw konstruktora pozwala najawne wywoanie konstruktorw obiektw skadowych. W rzeczywistoci nie ma adnego innego sposobu wywoania tych konstruktorw, Idea polega na tym, e wszystkie konstruktory musz zosta wywoane przed wejciem do ciaa konstruktora nowej klasy, Dziki temu wszelkie odwoania do funkcji skadowych obiektw podrzdnych zawsze bddotyczyy zainicjowanych obiektw. Niemoliwe jest zaistnienie sytuacji, w ktrej dotarlibymy do nawiasu klamrowego, otwierajcego konstruktor, a nie zostayby jeszcze wywoane jakiekolwiek konstruktory wszystkich obiektw skadowych i wszystkich klas podstawowych choby miay to by niejawne wywoania ich domylnych konstruktorw. To kolejne potwierdzenie gwarancji, udzielanej przez jzyk C++, e aden obiekt (ani jego cz) nie przekroczy bramki startowej", zanim nie zostanie wywoanyjego konstruktor. Idea polegajca na tym, e obiekty skadowe s inicjalizowane w momencie osignicia otwierajcego nawiasu klamrowego, stanowi rwnie przydatn wskazwk programistyczn. Po natrafieniu na klamr otwierajc mona zaoy, e wszystkie obiekty podrzdne zostay ju prawidowo zainicjowane, i skupi si na specyficznych zadaniach, ktre ma do zrealizowania konstruktor. Jestjednak pewien kopot: co z obiektami skadowymi wbudowanych typw, ktre nie posiadaj konstruktorw? Dla zachowania spjnoci skadni dozwolone jest traktowanie typw wbudowanych w taki sposb, jakby posiaday pojedynczy konstruktor, pobierajcy jeden argument - zmienn identycznego typu jak ta, ktra jest inicjalizowana. Dziki temu mona napisa:
/ / : C14:PseudoConstructor.cpp class X { int i ; float f: char c; char* s: public:
X ( ) : i(7). f(1.4), c ( ' x ' ) , s("czesc") {}

int main() { X x; int i(100): // Zastosowany w zwykej definicji int* ip = new int(47);

Rozdzia 14. Dziedziczenie i kompozycja

473

Dziaanie tych wywoa pseudokonstruktorw" polega na dokonaniu zwykych przypisa. To wygodna technika, stanowica zarazem przykad dobrego stylu programowania, wic czsto si jej uywa. Skadni pseudokonstruktora mona wykorzysta nawet podczas tworzenia zmiennej wbudowanego typu, nie nalecej do klasy: int i(100); int* ip = new int(47); Powoduje to, e zmienne wbudowanych typw funkcjonuj w sposb nieco bardziej przypominajcy obiekty. Naleyjednak pamita, e nie sto prawdziwe konstruktory. W szczegolnosci,jezeli nie dokona sijawnego wywoania pseudokonstruktora, to nie zostanie dokonana adna inicjalizacja.

czenie kompozycji i dziedziczenia


Oczywicie, kompozycji mona uywa cznie z dziedziczeniem. Poniszy przykad prezentuje tworzenie bardziej skomplikowanych klas, podczas ktrego wykorzystano obie te techniki: //: C14:Combined.Cpp // Dziedziczenie i kompozycja class A { int i; public: A(int ii) : i(ii) {} -A() {} void f() const {} }:

class B {

int i; public: 8(int ii) : i(ii) {} ~B() {} void f() const {} }:

class C : public B { A a; public: C(int ii) : B(ii), a(ii) {} -CO {} // Wywo>uje -A() i ~B() void f() const { // Zmiana definicji a.f(): B::f();

int main() C c(47);

474

Thinking in C++. Edycja polska

Klasa C dziedziczy po klasie B i posiada obiekt skadowy (,,jest skomponowana z") klasy A. Jak wida, lista inicjatorw konstruktora zawiera zarwno wywoanie konstruktora klasy podstawowej,jak i konstruktora obiektu skadowego. Funkcja C::f() zmienia definicj funkcji B::f(), ktr dziedziczy, a take wywouje wersj tej funkcji, zdefiniowan w klasie podstawowej. Ponadto wywouje funkcj a.f(). Zwr uwag na to, e jedyny przypadek dotyczcy zmiany definicji ma miejsce podczas dziedziczenia w przypadku obiektu skadowego mona manipulowa jedyniejego interfejsem publicznym, a nie wolno zmieni definicje. Oprcz tego wywoanie funkcji f ( ) dla obiektu klasy C nie spowoduje wywoania funkcji a.f(), gdyby nie bya zdefiniowana funkcja C::f(). Mogoby natomiast spowodowa wywoanie funkcjiB::f().

Automatyczne wywotenia destruktorw


Mimo e czsto zachodzi konieczno jawnego wywoywania konstruktorw poprzez umieszczenie ich na licie inicjatorw, nigdy nie ma potrzebyjawnego wywoywania destruktorw. Kada klasa posiada bowiem tylko jeden destruktor i nie pobiera on adnych argumentw. Jednake kompilator nadal zapewnia, e wywoane zostan wszystkie destruktory znajdujce si w caej hierarchii klas poczynajc od destruktora ostatniej wyprowadzonej klasy i posuwajc si wstecz, a do klasy najbardziej podstawowej (gwnej). Warto podkreli, e konstruktory i destruktory maj nieco niezwyke cechy, poniewa zawsze wywoywany jest konstruktor i destruktor kadej klasy znajdujcej si w hierarchii. Tymczasem w przypadku zwykych funkcji skadowych wywoywana jest tylko jedna jej wersja, a nie wszystkie, zdefiniowane w klasach podstawowych. Ponadto jeeli zamierza si wywoa zdefiniowan w klasie podstawowej wersj normalnej funkcji skadowej, ktra zostaa zasonita, to trzeba zrobi to w j a w n y sposb.

Kolejno wywotywania konstruktorw i destruktorw


Interesujcejest, wjakiej kolejnoci wywoywane skonstruktory i destruktory,jesli obiekt posiada wiele obiektw podrzdnych. Poniszy przykad pokazuje, jak si to odbywa:
/ / : C14:Ordercpp // Kolejno wywoywania konstruktorw i destruktorw #include <fstream> using namespace std; ofstream out("order.out");

#define CLASS(ID) class ID { \ public: \ ID(int) { out << #ID " konstruktor\n"; } \

-ID() { out << #ID " destruktor\n"; } \

Rozdzia 14. Dziedziczenie i kompozycja CLASS(Basel); CLASS(Memberl); CLASS(Member2); CLASS(Member3); CLASS(Member4); class Derivedl : public Basel { Memberl ml; Member2 m2; public: Derivedl(int) : m2(l). ml(2). Basel(3) { out << "Derivedl konstruktor\n"; } ~Derivedl() { out << "Derivedl destruktor\n";

475

class Derived2 : public Derivedl { Member3 m3; Member4 m4; public: Derived2() : m3(l). Denvedl(2), m4(3) out << "Dehved2 konstruktor\n"; } ~Derived2() { out << "Derived2 destruktor\n";

int main() { Derived2 d2;


}
III-

Najpierw tworzony jest obiekt klasy ofstream, umoliwiajcy zapisywanie danych wyjciowych w pliku. Nastpnie, aby unikn mudnego wpisywania kodu i zadermonstrowa wykorzystanie techniki makroinstrukcji (zostanie ona zastpiona w rozdziale 16. znacznie doskonalsz technik), tworzona jest makroinstrukcja suca do utworzenia klas, ktre nastpnie zostay uyte w dziedziczeniu i kompozycji. Wszystkie konstruktory i destruktory zapisuj w pliku ledzenia informacje o tym, e zostay wywoane. Zwr uwag na to, e konstruktory nie s konstruktorami domylnymi kady z nich pobiera jako argument liczb cakowit. Argumenty te nie posiadaj identyfikatorw jedynym powodem ich stosowaniajest wymuszeniejawnego wywoania konstruktorw na licie inicjatorw (brak identyfikatorw zapobiega zgaszaniu ostrzee przez kompilator). Wyniki pracy programu s nastpujce: Basel konstruktor Memberl konstruktor Member2 konstruktor Derivedl konstruktor Member3 konstruktor Member4 konstruktor Derived2 konstruktor Derived2 destruktor

476

Thinking in C++. Edycja polska

Member4 destruktor Member3 destruktor Derivedl destruktor Member2 destruktor Memberl destruktor Basel destruktor
Konstrukcja rozpoczyna si zatem na najbardziej podstawowym poziomie hierarchii klas. Na kadym poziomie najpierw wywoywany jest konstruktor klasy podstawowej, a pniej konstruktory obiektw skadowych. Destruktory s wywoywane w odwrotnej kolejnoci ni konstruktory jest to istotne z uwagi na potencjalne zalenoci (w konstruktorach i destruktorach klas pochodnych musimy mie prawo zaoy, e obiekty podrzdne klas podstawowych s dostpne i zostay ju skonstruowane lub nie zostay jeszcze zniszczone). Interesujce jest rwnie, e w przypadku obiektw skadowych kolejno wywoa konstruktorw nie jest wcale zwizana z kolejnoci ich wywoa na licie inicjatorw konstruktora. Kolejno ta jest wyznaczona przez porzdek, w jakim obiekty skadowe zostay zadeklarowane w klasie. Gdyby istniaa moliwo zmiany kolejnoci wywoa konstruktorw za pomoc listy inicjatorw konstruktora, to mona by w dwch rnych konstruktorach utworzy dwie odmienne sekwencje wywoa konstruktorw obiektw skadowych. Nieszczsny destruktor nie wiedziaby wwczas, wjaki sposb prawidowo wyznaczy odwrotn kolejno wywoa destruktorw, co mogoby doprowadzi do problemw zwizanych z zalenociami.

Ukrywanie nazw
Jeeli utworzymy za pomoc dziedziczenia klas i dostarczymy now definicj dla jednej z zawartych w niej funkcji skadowych, to zaistnieje jedna z dwu sytuacji. Pierwsza polega na tym, e definicja zawarta w klasie pochodnej bdzie posiadaa taksamsygnatur i typ zwracanej wartosci,jak definicja znajdujca si w klasie podstawowej. Jest to nazywaneprzedefiniowaniem (ang. redefining), w przypadku zwykych funkcji skadowych, lub zasanianiem (ang. overriding), w przypadku gdy funkcja skadowa klasy podstawowej jest funkcj wirtualn (funkcje wirtualne stanowi normalny przypadek i zostan opisane w rozdziale 15.). Co jednak dzieje si w sytuacji, gdy w klasie pochodnej zmienimy list argumentw funkcji albo zwracan przez ni warto? Poniej znajduje si stosowny przykad:

#include <string>

//: C14:NameHiding.cpp // Ukrywanie przecianych nazw podczas dziedziczenia #include <iostream> using namespace std;

class Base { public: int f() const { cout << "Base::f()\n"; return 1;

Rozdzia 14. Dziedziczenie i kompozycja int f(string) const { return 1; } void g() {}
1

477

class Derivedl : public Base { public: void g() const {}

};

class Derived2 : public Base { public: // Przedefiniowanie: int f() const { cout << "Derived2::f()\n"; return 2;

class Derived3 : public Base { public: // Zmiana zwracanego typu: void f() const { cout << "Derived3::f()\n" class Derived4 : public Base { public: // Zmiana listy argumentw: int f(int) const { cout << "Derived4::f()\n"; return 4;

int main() { string s("witam") ; Derivedl dl; int x = dl.f(); dl.f(s); Derived2 d2; x - d2.f(); / / ! d2.f(s); // Wersja z acuchem jest ukryta Denved3 d3; / / ! x = d3.f(); // Wersja zwracajca warto int jest ukryta Derived4 d4; / / ! x = d4.f(); // Wersja f() jest ukryta x = d4.f(l): W klasie Base widocznajest przeciona funkcja f( ). Klasa Derivedl nie wprowadza adnych modyfikacji do funkcji f( ), zmienia natomiast definicj funkcji g( ). Jak wida w funkcji main( ), w klasie Derivedl dostpne s obie przecione funkcje f( ). Jednake w klasie Derived2 zmieniono definicj tylkojednej z przecionych funkcji f( ), a w rezultacie rwnie druga wersja tej funkcji staje si niewidoczna. W klasie Derived3 zmiana typu zwracanej wartoci powoduje ukrycie obu wersji funkcji, zawartych w klasie podstawowej, natomiast klasa Derived4 pokazuje, e zmiana listy argumentw funkcji rwnie powoduje ukrycie obujej wersji, znajdujcych si w klasie

478

Thinking in C++. Edycja polska podstawowej. Mona oglnie stwierdzi, e ilekro przedefiniowuje si funkcj, ktrej nazwajest przeciona w klasie podstawowej, to wszystkie pozostae wersje tej funkcji s automatycznie ukrywane w nowej klasie. Jak przekonamy si w rozdziale 15., dodanie sowa kluczowego virtual pociga za sob dalsze konsekwencje, dotyczce przeciania funkcji. W przypadku zmiany interfejsu klasy podstawowej, polegajcej na zmodyfikowaniu sygnatury lub wartoci zwracanej przez jej funkcj skadow, klasa jest wykorzystywana w nieco odmienny sposb ni ten, z myl o ktrym utworzono dziedziczenie. Nie oznacza to wcale, e poda si w zym kierunku; gwnym zastosowaniem dziedziczeniajest bowiem wspieranie polimorfizmu, natomiast zmiana sygnatury funkcji lub typu zwracanej przez ni wartoci jest w rzeczywistoci modyfikacj interfejsu klasy podstawowej. Jeeli jest to zgodne z naszymi zamierzeniami, uywamy dziedziczenia przede wszystkim w celu powtrnego wykorzystania kodu, nie za utrzymania jednolitego interfejsu klasy podstawowej (bdcego zasadniczym przejawem polimorfizmu). Zazwyczaj posugiwanie si dziedziczeniem w taki sposb oznacza uycie klasy o oglnym przeznaczeniu i dostosowanie jej do konkretnych potrzeb co jest zwykle, chocia nie zawsze, uwaane za domen kompozycji. Wemy na przykad pod uwag klas Stack, opisan w 9. rozdziale ksiki. Jeden z problemw z ni zwizanych polega na tym, za kadym razem, gdy pobiera si wskanik z kontenera, trzeba dokona jego rzutowania. Jest to nie tylko mczce, ale rwnie niebezpieczne wskanik taki mona bowiem rzutowa na dowolnie wybrany typ. Podejcie, ktre na pierwszy rzut oka moe wydawa si lepsze, polega na wykorzystaniu dziedziczenia do utworzenia specjalizowanej wersji oglnej klasy Stack. Poniej zamieszczono przykad, wykorzystujcy posta tej klasy, przedstawion w rozdziale 9.: / / : C14:InheritStack.cpp // Specjalizacja klasy Stack #include "../C09/Stack4.h" finclude "../require.h" #include <iostream> #include <fstream> #include <string> using namespace std; class StringStack : public Stack { public: void push(string* str) { Stack::push(str); string* peek() const { return (string*)Stack::peek(); string* pop() { return (string*)Stack::pop(); ~StringStack() { string* top = pop(); while(top) { delete top; top - pop();
} } }

Rozdzia 14. Dziedziczenie i kompozycja

479

int main() { ifstream in("InheritStack.cpp"); assure(in. "InheritStack.cpp"); string line; StringStack textlines; while(getline(1n, line)) textlines.push(new string(line)); string* s; while((s = textlines.popO) !- 0) { // Brak rzutowania! cout << *s << endl; delete s;

Poniewa wszystkie funkcje skadowe, zawarte w pliku Stack4.h, s funkcjami inline, nie ma potrzeby czenia programu z adnym dodatkowym moduem. Klasa StringStack stanowi wersj klasy Stack, wyspecjalizowan w taki sposb, by funkcja push( ) moga pobiera wycznie wskaniki do acuchw (string). Poprzednio klasa Stack pobieraa wskaniki typu void*, co powodowao, e uytkownik nie dysponowa adnym mechanizmem kontroli typw, ktry sprawdzaby, czy do kontenera zostay dodane odpowiednie wskaniki. Ponadto funkcje peek() i pop( ), zamiast wskanikw typu void*, zwracaj obecnie wskaniki do acuchw, dziki czemu mona ich uywa bez koniecznoci rzutowania. Do zdumiewajce jest natomiast, e to dodatkowe zabezpieczenie, polegajce na kontroli typw w funkcjach push(), peek() i pop(), zupenie nic nie kosztuje! Kompilator otrzymuje dodatkow informacj, wykorzystywan podczas kompilacji, ale funkcje s funkcjami inline, i niejest w ich przypadku generowany aden dodatkowy kod. Odgrywa tu rol ukrywanie nazw, poniewa w szczeglnoci funkcja push( ), zdefiniowana w klasie pochodnej, posiada inn sygnatur n i j e j definicja, znajdujca si w klasie podstawowej funkcje te rni si list argumentw. Gdyby w obrbie klasy istniay dwie wersje funkcji push( ), to mielibymy do czynienia z przecieniem, ale w tym przypadku przecienie niejest naszym celem, poniewa nadal pozwalaoby na przekazywanie funkcji push( ) dowolnego wskanika jako wskanika typu void*. Na szczescie,jezyk C++ ukrywa wersj, zdefiniowanjako push(void*) w klasie podstawowej, na rzecz nowej wersji funkcji, zdefiniowanej w klasie pochodnej, pozwalajc w ten sposb na umieszczanie w kontenerze (bdcym obiektem klasy StringStack) jedynie wskanikw do acuchw. Poniewa moemy ju zagwarantowa dokadn znajomo typu obiektw znajdujcych si w kontenerze, destruktor klasy dziaa prawidowo, co rozwizuje problem prawa wasnoci a przynajmniej jeden zjego aspektw. Nastpnie umieszczajc za pomoc funkcji push( ) wskanik do acucha w kontenerze, bdcym obiektem klasy StringStack, przekazujemy jej (zgodnie ze znaczeniem klasy StringStack) rwnie prawo wasnoci tego wskanika. Gdy pobieramy wskanik za pomoc funkcji pop( ), otrzymujemy nie tylko wskanik, ale rwnie prawo do jego posiadania. Wszelkie wskaniki, pozostawione w kontenerze StringStack do c h w i l i wywoania

480

Thinking in C++. Edycja polska

destruktora, acuchw, chw, a nie konywana i

s usuwane przez ten destruktor. A poniewa s to zawsze wskaniki do instrukcja delete jest stosowana w odniesieniu do wskanikw acudo wskanikw typu void*, dziki czemu destrukcja jest poprawnie wywszystko dziaa prawidowo.

Rozwizanie to ma jednak pewn wad dziaa wycznie ze wskanikami do acuchw. Gdybymy potrzebowali klasy Stack, dziaajcej z jakim innym rodzajem obiektw, musielibymy napisa jej now wersj, funkcjonujc tylko z tym wanie rodzajem. Szybko staoby si to uciliwe problem ten mona ostatecznie rozwiza za pomocszablonw (zob. rozdzia 16). Moemy uczyni jeszcze jedno spostrzeenie, dotyczcego powyszego przykadu. W procesie dziedziczenia uleg zmianie interfejs klasy Stack. Jeeli zmieni si interfejs, oznacza to, e klasa StringStack nie jest w rzeczywistoci rodzajem klasy Stack. Z tej przyczyny nigdy nie bdzie mona jej poprawnie uy zamiast klasy Stack. Powoduje to, e zastosowanie dziedziczenia jest w tym przypadku dyskusyjne -jeeli nie utworzylimy klasy StringStack, ktrajesr rodzajem klasy Stack, to po co uywalimy dziedziczenia? Bardziej poprawna wersja klasy StringStack zostanie przedstawiona w dalszej czci rozdziau.

Funkcje, ktre nie s automatycznie dziedziczone


Nie wszystkie funkcje znajdujce si w klasie podstawowej s automatycznie dziedziczone przez klas pochodn. Konstruktory i destruktory s odpowiedzialne za tworzenie i niszczenie obiektw, ale wiedz" one jedynie, jakie dziaania naley wykona z obiektami nalecym do ich wasnej klasy. Zachodzi zatem konieczno wywoania wszystkich konstruktorw i destruktorw klas, znajdujcych si niej od nich w hierarchii. Tak wic konstruktory i destruktory nie podlegaj dziedziczeniu i muszby tworzone oddzielnie dla kadej klasy pochodnej. Ponadto dziedziczeniu nie podlega operator=, poniewa wykonuje on dziaania zblione do konstruktora. Innymi sowy to, e wiemy, w jaki sposb wszystkim skadowym obiektu, znajdujcego si po lewej stronie znaku =, przypisa wartoci pochodzce z obiektu po jego prawej stronie, nie oznacza wcale, e operacja przypisania bdzie miaa w klasie pochodnej nadal to samo znaczenie. Funkcje te, zamiast podlega dziedziczeniu, s generowane przez kompilator o ile nie przygotuje si ich wasnych wersji (w przypadku konstruktorw nie mona utworzy adnego konstruktora, jeeli kompilator ma utworzy domylny konstruktor i konstruktor kopiujcy). Kwestia ta zostaa krtko opisana w rozdziale 6. Wygenerowane konstruktory wykorzystuj inicjalizacj za porednictwem elementw skadowych, natomiast utworzony przez kompilator operator= stosuje przypisanie za porednictwem elementw skadowych. Poniszy przykad zawiera funkcje wygenerowane przez kompilator.

Rozdzia 14. Dziedziczenie i kompozycja


/ / : C14:SynthesizedFunct1ons.cpp // Funkcje generowane przez kompilator #include <iostream> using namespace std; c l a s s GameBoard { public: GameBoardO { cout << "GameBoard()\n"; } GameBoard(const GameBoard&) { cout << "GameBoard(const GameBoard&)\n";

481

GameBoard& operator=(const GameBoard&) { cout << "GameBoard::operator=()\n"; return *this; }

~GameBoard() { cout << "~GameBoard()\n"; }

class Game { GameBoard gb; // Kompozycja public: // Wywoywany domylny konstruktor klasy GameBoard: Game() { cout << "Game()\n"; } // Trzeba jawnie wywoa konstruktor kopiujcy // klasy GameBoard, gdy w przeciwnym przypadku // zamiast niego zostanie wywoany // automatycznie domylny konstruktor: Game(const Game& g) : gb(g.gb) { cout << "Game(const Game&)\n"; Game(int) { cout << "Game(int)\n"; } Game& operator=(const Game& g) { // Trzeba jawnie wywoa operator przypisania // klasy GameBoard. gdy w przeciwnym przypadku // przypisywanie go nie bdzie w ogle // realizowane! cout << "Game::operator=()\n"; return *this;

gb = g.gb;

} class Other {}; // Zagniedona klasa // Automatyczna konwersja typu: operator Other() const { cout << "Game::operator Other()\n"; return Other(); } ~ameO { cout << "-Game()\n"; }
class Chess : public Game {}: void f(Game::Other) {} class Checkers : public Game { public: // Wywoywany domylny konstruktor klasy podstawowej: Checkers() { cout << "Checkers()\n"; }

482

Thinking in C++. Edycja polska // Trzeba jawnie wywoa konstruktor kopiujcy // klasy podstawowej, gdy w przeciwnym przypadku // zostanie zamiast niego wywoany konstruktor domylny: Checkers(const Checkers& c) : Game(c) { cout << "Checkers(const Checkers& c)\n"; Checkers& operator=(const Checkers& c) { // Trzeba jawnie wywoa wersje operatora=(), // znajdujc si w klasie podstawowej, gdy // w przeciwnym przypadku w klasie podstawowej // nie zostanie zrealizowana operacja przypisania: Game::operator=(c); cout << "Checkers::operator=()\n"; return *this;

int main() { Chess dl; // Domylny konstruktor Chess d2(dl); // Konstruktor kopiujcy //! Chess d3(l); // Bd - nie ma konstruktora z argumentem int dl = d2: // Wygenerowany operator= f(dl); // Konwersja typw JEST dziedziczona Game::Other go; //! dl = go; // operator= nie jest generowany // dla ronych typw Checkers cl, c2(cl):

cl = c2; } III-

Konstruktory oraz operatory przypisania klas GameBoard oraz Game przedstawiaj si, dziki czemu mona zobaczy, kiedy zostay one uyte przez kompilator. Oprcz tego operator Other( ) dokonuje automatycznej konwersji typu obiektu klasy Game na obiekt zagniedonej w niej klasy Other. Klasa Chess zostaa utworzona z kIasy Game za pomocdziedziczenia i nie tworzy adnych nowych funkcji (aby zobaczy, w jaki sposb reaguje w takim przypadku kompilator). Funkcja f ( ) pobiera obiekt klasy Other, co umoliwia przetestowanie funkcji automatycznej konwersji typu. W funkcji main( ) wywoywane s: domylny konstruktor i konstruktor kopiujcy klasy Chess. Wersje tych konstruktorw, zawarte w klasie Game, s wywoywane w ramach hierarchii wywoa konstruktorw. Mimo e przypomina to dziedziczenie, nowe wersje konstruktorw s w rzeczywistoci generowane przez kompilator. Jak si mona byo spodziewa, adne konstruktory zawierajce argumenty nie zostay wygenerowane automatycznie, poniewa wymagaoby to od kompilatora zbyt duej domylnoci. W klasie Chess zosta rwnie wygenerowany, w postaci nowej funkcji, operator=. Odbyo si to z wykorzystaniem przypisania za porednictwem elementw skadowych (tak wic wywoywanajestjego wersja zawarta w klasie podstawowej), poniewa funkcja tego operatora ta nie zostaajawnie utworzona w nowej klasie. Oczywicie, kompilator wygenerowa rwnie automatycznie destruktor klasy Chess.

Rozdzia 14. Dziedziczenie i kompozycja

483

Z uwagi na wszystkie reguy, dotyczce przepisywania funkcji obsugujcych tworzenie obiektw, moe na pocztku wydawa si nieco dziwne, e dziedziczonyjest operator automatycznej konwersji typw. Nie jest to jednak pozbawione podstaw -jeeli klasa Game posiada wystarczajc liczb elementw, niezbdnych do utworzenia obiektu klasy Other, to elementy te s rwnie zawarte w kadej klasie wyprowadzonej z klasy Game i konwersja taka jest nadal poprawna (mimo e w rzeczywistoci naleaobyj przedefiniowa). Funkcja operator= jest generowana wylcznie w celu przypisywania obiektw tego samego typu. Jeeli zamierzamy przypisywa obiektom jednego typu obiekty innego typu, to zawsze musimy utworzy funkcj operator= samodzielnie. Przygldajc si bliej klasie Game, mona zauway, e konstruktor kopiujcy oraz operator przypisania zawieraj jawne wywoania odpowiednio konstruktora kopiujcego oraz operatora przypisania obiektu skadowego. Jest to zazwyczaj konieczne, poniewa w przeciwnym razie zamiast konstruktora kopiujcego zostaby uyty domylny konstruktor obiektu skadowego, a w przypadku operatora przypisania nie nastpioby w ogle przypisanie wartoci obiektowi skadowemu! Przyjrzyjmy si wreszcie klasie Checkers, zawierajcej jawne definicje konstruktora domylnego, konstruktora kopiujcego oraz operatora przypisania. W przypadku konstruktora domylnego wywoywany jest automatycznie konstruktor domylny klasy podstawowej, i zwykle o to nam chodzi. Wanejestjednak, ejeeli zdecydujemy si na napisanie wasnego konstruktora kopiujcego oraz operatora przypisania, to kompilator zaoy, e wiemy, co robimy, i nie wywola automatycznie ich wersji zawartych w klasie podstawowej, co uczyniby w przypadku wygenerowanych przez siebie wersji tych funkcji. Jeeli chcemy, aby wywoane zostay ich wersje zawarte w klasie podstawowej (a zazwyczaj tak wanie jest), to musimy wywoa je sami. W konstruktorze kopiujcym klasy Checkers wywoanie to jest widoczne na licie inicjatorw konstruktora:

Checkers(const Checkers& c) : Game(c) {


W operatorze przypisania klasy Checkers wywoanie funkcji klasy podstawowej znajduje si w pierwszym wierszu ciaa funkcji:
Game::operator=(c);

Wywoania te powinny stanowi dla ciebie wzorzec, wykorzystywany za kadym razem, gdy tworzyszjak klas, uywajc do tego dziedziczenia.

Dziedziczenie a statyczne funkcje sktedowe


Statyczne funkcje skadowe dziaaj tak samo, jak funkcje skadowe niebdce funkcjami statycznymi: 1. S one dziedziczone w klasach pochodnych. 2. W przypadku przedefiniowania statycznej funkcji skadowej wszystkie pozostae przecione funkcje, zawarte w klasie podstawowej, s ukrywane.

484

Thinking in C++. Edycja polska

3. W przypadku zmiany sygnatury funkcji, zawartej w klasie podstawowej, ukrywane s wszystkie funkcje o tej nazwie, znajdujce si w klasie podstawowej ^est to waciwie wariant poprzedniego punktu). Statyczne funkcje skadowe nie mog by jednak funkcjami wirtualnymi (temat ten zostanie szczegowo opisany w rozdziale 15.)-

Wybr midzy kompozycj a dziedziczeniem


Zarwno kompozycja, jak i dziedziczenie powoduj utworzenie w nowej klasie obiektw podrzdnych. W obu przypadkach do skonstruowania obiektw podrzdnych wykorzystywanajest lista inicjatorw konstruktora. By moe zastanawiasz si, jakajest pomidzy nimi rnica i kiedy wybra dan metod. Na og kompozycja jest wykorzystywana wwczas, gdy zamierzamy zawrze w nowej klasie waciwoci klasy, ktra ju istnieje, ale nie jej interfejs. Oznacza to, e obiektjest osadzany w nowej klasie po to, by wykorzysta jego wasnoci. Jednake uytkownik nowo utworzonej klasy widzi interfejs tej klasy, a nie klasy oryginalnej. Robi si to zazwyczaj, osadzajc w nowej klasie obiekty istniejcych ju klas jako skadowe prywatne. Czasami jednak jest uzasadnione, by uytkownik klasy mia bezporedni dostp do komponentw nowo utworzonej klasy, to znaczy, aby skadowe te byy publiczne. Obiekty skadowe posiadaj wasn kontrol dostpu, wic mona to bezpiecznie wykona. Ponadto gdy uytkownik widzi, e klasa zostaa utworzona przez poczenie ze sob grupy elementw, atwiej mu zrozumie jej interfejs. Dobrym tego przykademjest klasa Car (samochd): //: C14:Car.cpp // Publiczna kompozycja class Engine { public: void start() const {} void rev() const {} void stop() const {}
class Wheel { public: void inflate(int psi) const {}

class Window { public:

void rollup() const {} void rolldown() const {}

Rozdzia 14. Dziedziczenie i kompozycja class Door { public: Window window; void open() const {} void close() const {}
}:

485

class Car { public: Engine engine; Wheel wheel[4]; Door left, right; // Dwoje drzwi int main() {
Car car;

ca r . 1 eft . wi ndow . rol 1 up( ) ; car.wheel[0].inflate(72); Z uwagi na to, e kompozycja klasy Car stanowi element analizy problemu (a nie tylko cz jego wewntrznego projektu), uczynienie skadowych publicznymi pomaga klientowi-programicie zrozumie, w jaki sposb naley uywa tej klasy, a zarazem wymaga od twrcy klasy tworzenia mniej zoonego kodu. Po chwili zastanowienia przyznasz zapewne, e nie miaoby sensu uycie do budowy klasy Car obiektu pojazd" samochd nie zawiera bowiem pojazdu, onjest pojazdem. Relacja typujestjesl wyraana za pomoc dziedziczenia, natomiast relacja posiada za pomoc kompozycji.

Tworzenie podtypw
Zamy, e zamierzamy utworzy rodzaj obiekt klasy ifstream, ktry nie tylko bdzie otwiera plik, ale rwnie pamita jego nazw. Mona w tym celu wykorzysta kompozycj, zamykajc obiekty klas ifstream oraz string wewntrz nowej klasy: //: C14:FNamel.cpp // Klasa fstream z nazwa pliku #include ". ./require.h" #include <iostream> #include <fstream> #include <string> using namespace std; class FNamel { ifstream file; string fileName; bool named; public: FNamel() : named(false) {} FNamel(const string& fname) : fileName(fname), file(fname.c_str{)) { assure(file, fileName): named - true;

Thinking in C++. Edycja polska string name() const { return f11eName; } void name(const string& newName) { if(named) return; // Nie zapisuj poprzedniej nazwy fileName = newName; named - true; } operator ifstream&O { return file; } int main() { FNamel file("FNamel.cpp"); cout << file.name() << endl; // Bd - close() nie jest skadow: / / ! file.close(); } lll:~

Wystpuje tu jednak problem. Za pomoc operatora automatycznej konwersji typu z FNamel do ifstream& prbuje si umoliwi uycie obiektu klasy FNamel w kadym miejscu, w ktrym mgby by uywany obiekt klasy ifstream. Jednak wiersz znajdujcy si w funkcji main():
file.close();

nie skompiluje si, poniewa automatyczna konwersja typw zachodzi wycznie w przypadku wywoywania funkcji, a nie wyboru skadowych. Sposb ten nie bdzie wic dziaa. Drug metodjest dodanie do klasy FNamel definicji funkcji close():
void close() { file.close(); }

Rozwizanie to sprawdzi si wwczas, gdy bdziemy chcieli udostpni tylko niektre funkcje klasy ifstream. W takim przypadku wykorzystamy tylko cz klasy i zastosowanie kompozycji bdzie usprawiedliwione. Co jednak zrobi, jeli zamierzamy udostpni ca zawarto klasy? Jest to nazywane tworzeniem podtypw (ang. subtyping), poniewa tworzymy w takim przypadku nowy typ na podstawie typuju istniejcego. Chcemy bowiem, aby mia on taki sam interfejs, jak typ istniejcy (powikszony o jakie dodatkowe funkcje, ktre zamierzamy do niego doda), dziki czemu bdzie mona go uywa w kadym miejscu, w ktrym mgby zosta uyty istniejcy typ. Jest to wanie przypadek, w ktrym niezbdne jest dziedziczenie. Analizujc zamieszczony poniej program mona si przekona, w jaki sposb tworzenie podtypw w idealny sposb rozwizuje problem wystpujcy w poprzednim przykadzie: //: C14:FName2.cpp // Utworzenie podtypu rozwizuje problem #include "../require.h" #include <iostream> #include <fstream> #include <string> using namespace std; class FName2 ; public ifstream { string fileName; bool named; public:

Rozdzia 14. * Dziedziczenie i kompozycja FName2() : named(false) {} FName2(const string& fname) : ifstream(fname.c_str{)). fileName(fname) {

487

assure(*this, fileName); named = true;

} string name() const { return fileName; } void name(const string& newName) { if(named) return; // Nie zapisuj poprzedniej nazwy fi1eName = newName; named = true;

int main() { FName2 file("FName2.cpp"); assure(file. "FName2.cpp"); cout << "name: " << file.name() << endl; string s; getline(file, s); // Te funkcje rwnie dzia1ajaJ file.seekg(-200, ios::end); file.close(); A zatem kada funkcja skadowa, dostpna w przypadku obiektu klasy ifstream, jest rwnie dostpna w obiekcie kiasy FName2. Rwnie funkcje niebdce funkcjami skadowymi dziaaj z obiektami klasy FName2 jak na przykad funkcja getline( ), wymagajca obiektu klasy ifstream. Dzieje si tak, poniewa klasa FName2jest rodzajem klasy ifstream, a nie dlatego, e zawiera jedynie obiekt tej klasy. Ta bardzo wana kwestia bdzie rozwaana do koca biecego rozdziau oraz w nastpnym rozdziale ksiki.

Dziedziczenie prywatne
Mona dziedziczy po klasie podstawowej w sposb prywatny, pomijajc sowo public na licie klas podstawowych, albo jawnie wpisujc na niej sowo kluczowe private (co jest prawdopodobnie lepsz praktyk, poniewa dziki temu uytkownik nie ma wtpliwoci, e o to wanie chodzio). Uywajc dziedziczenia prywatnego dokonuje si implementacji na motywach czego" - tworzy si now klas, ktra zawiera wszystkie dane i funkcje klasy podstawowej, lecz pozostaj one ukryte, wic stanowijedynie elementjej wewntrznej implementacji. Uytkownik klasy nie ma dostpu dojej wewntrznych funkcji, a obiekt takiej klasy nie moe by traktowany jako egzemplarz klasy podstawowej ^ak w przypadku klasy FName2.cpp). Mona si zastanawia, jakiejest zastosowanie dziedziczenia prywatnego, skoro konkurencyjne rozwizanie, polegajce na uyciu kompozycji w celu utworzenia wewntrz klasy obiektu prywatnego, wydaje si bardziej odpowiednie. Dziedziczenie prywatne doczono do jzyka po to, by by on kompletny, ale skoro jedynym powodem jego istnienia jest niewprowadzanie baaganu, to zapewne czciej posuysz si kompozycj ni dziedziczeniem prywatnym. Jednak mog si czasem zdarzy sytuacje, w ktrych chcemy utworzy interfejs czciowo zgodny z tym, ktry wystpuje w kJasie podstawowej, nie pozwalajc na uywanie obiektw tej klasy w taki sposb, jakby byy one obiektami kJasy podstawowej. Dziedziczenie prywatne zapewnia tak moliwo.

488

Thinking in C++. Edycja polska

Upublicznianie sktedowych dziedziczonych prywatnie


Podczas dziedziczenia prywatnego wszystkie publiczne skadowe klasy podstawowej staj si w klasie pochodnej skadowymi prywatnymi. Jeeli chcemy, by ktre z nich byy widoczne publicznie, wystarczy wymieni ich nazwy (bez argumentw funkcji ani zwracanych przez nie wartoci) w publicznej czci klasy pochodnej: / / : C14:PrivateInheritance.cpp class Pet { public: char eat() const { return ' a ' ; } int speak() const { return 2; } float sleep() const { return 3 . 0 ; } float sleep(int) const { return 4 . 0 ; } }; class Goldfish : Pet { // Dziedziczenie prywatne public: Pet::eat; // Nazwa upublicznionej skadowej Pet::sleep; // Widoczne s obie przecione funkcje skadowe int main() { Goldfish bob; bob.eat(); bob.sleep(); bob.sleep(l); / / ! bob.speak();// Bd - prywatna funkcja skadowa } ///:Tak wic dziedziczenie prywatnejest przydatne, gdy chcemy ukry cz funkcji zawartych w klasie podstawowej. Warto zwrci uwag na to, e podanie nazwy przecionej funkcji powoduje upublicznienie wszystkichjej wersji, zawartych w klasie podstawowej. Naley gruntownie przemyle decyzj o zastosowaniu dziedziczenia prywatnego zamiast kompozycji dziedziczenie prywatne powoduje pewne komplikacje, gdy uywa si go cznie z identyfikacj typw w trakcie pracy programu (stanowi ona temat jednego z rozdziaw drugiego tomu ksiki, ktry mona pobra z witryny http:Melion.pl/online/thinking/index.html).

Specyfikator protected
Po wprowadzeniu do dziedziczenia sowo kluczowe protected (chroniony) nareszcie uzyskao znaczenie. W idealnym wiecie skadowe prywatne klasy powinny by zawsze prywatne, ale w rzeczywistych projektach zdarzaj si sytuacje, w ktrych chcemy ukry co przed ogem, umoliwiajc jednak dostp skadowym klas pochodnych. Zastosowanie sowa kluczowego protected jest ukonem w stron pragmatyzmu

Rozdzia 14. Dziedziczenie i kompozycja

489

oznacza ono: ,jest to prywatne, dopki chodzi o uytkownika klasy, ale dostpne dla kadego, kto dziedziczy z tej klasy". Najlepiej dane skadowe powinny pozosta prywatne zawsze naley zostawi sobie moliwo zmiany wewntrznej implementacji. Nastpnie mona pozwoli klasom pochodnym na kontrolowany dostp do nich, wykorzystujc do tego celu chronione funkcje skadowe: //: C14:Protected.cpp // S1owo kluczowe protected |1nclude <fstream> using namespace std; class Base { int i ; protected: int read() const void set(int ii) public: Base(int ii = 0) int value(int m)
}:

{ return i; } { i = ii ; } : i(ii) {} const { return m*i; }

class Derived : public Base { public: Derived(int jj - 0) : j(jj) {} void change(int x) { set(x); }
int main() { Derived d: d.change(10):

int j:

Przykady ilustrujce konieczno zastosowania sowa kluczowego protected mona znale w dalszej czci ksiki, a take wjej drugim tomie.

Dziedziczenie chronione
Podczas dziedziczenia klasa podstawowa jest domylnie prywatna, co oznacza, e wszystkie jej publiczne funkcje skadowe s prywatne dla uytkownika nowo utworzonej klasy. Zazwyczaj stosuje si dziedziczenie publiczne, dziki czemu interfejs klasy pochodnej jest taki sam, jak interfejs klasy podstawowej. Jednake w czasie dziedziczenia mona rwnie uy sowa kluczowego protected. Dziedziczenie chronione oznacza implementacj na motywach" w stosunku do innych klas, lecz to relacja typu ,jest" dla klas pochodnych oraz zaprzyjanionych. Jest czym, czego nie uywa si zbyt czsto, ale co jest dostpne w jzyku po to, aby by on kompletny.

490

Thinking in C++. Edycja polska

Przecianie operatorw a dziedziczenie


Operatory, z wyjtkiem operatora przypisania, s automatycznie dziedziczone w klasach pochodnych. Mona to pokaza, tworzc klas pochodn klasy zawartej w p l i k u nagwkowym C12:Byte.h:
/ / : C14:OperatorInheritance.cpp // Dziedziczenie przecionych operatorw #include "../C12/Byte.h" #include <fstream>

using namespace std; ofstream out("ByteTest.out"); class Byte2 : public Byte { public: // Konstruktory nie s dziedziczone: Byte2(unsigned char bb = 0) : Byte(bb) {} // operator= nie podlega dziedziczeniu, ale jest // generowany dla przypisania za porednictwem // elementw skadowych. Jednake, generowany // jest tylko operator= umoliwiajcy przypisanie // w stosunku obiektw tego samego typu, wiec inne // operatory przypisania musza by utworzone jawnie: Byte2& operator=(const Byte& right) { Byte: :operator=(right); return *this; Byte2& operator=(int i) { Byte: :operator=(i) ; return *this;

// Funkcje testowe podobne do zawartych w pliku C12:ByteTest.cpp: void k(Byte2& bl. Byte2& b2) { bl = bl * b2 + b2 % bl; #define TRY2(OP) \ out << "bl = "; bl.print(out); \ out << ", b2 = ": b2.print(out): \ out << "; bl " #OP " b2 daje "; \ (bl OP b2).print(out): \ out << endl ;

bl = 9: b2 = 47; TRY2(+) TRY2(-) TRY2(*) TRY2(/) TRY2U) TRY2(^) TRY2(&) TRY2( ) TRY2(<<) TRY2(>>) TRY2(+-) TRY2(-=) TRY2(*=) TRY2(/=) TRY2U=) TRY2(^=) TRY2(S=) TRY2(|=) TRY2(>>=) TRY2(<<=) TRY2(=) // Operator przypisania

Rozdzia 14. Dziedziczenie i kompozycja

491

// Operatory warunkowe: #define TRYC2(OP) \ out << "t>l = ": bl.print(out): \ out << ", b2 = "; b2.print(out); \ out << "; bl " <<P " b2 daje "; \ out << (bl OP b2); \ out << endl ;

bl = 9; b2 = 47; TRYC2(<) TRYC2(>) TRYC2(==) TRYC2C!=) TRYC2(<=) TRYC2(>=J TRYC2(&&) TRYC2( 1 1 )


// Przypisanie acuchowe: Byte2 b3 = 92; bl = b2 = b3;
int main() { out << "funkcje skladowe:" << endl; Byte2 bl(47). b2(9); k(bl, b2);

Kod testowy jest identyczny, jak w programie C12:ByteTest.cpp, z wyjtkiem tego, e zamiast klasy Byte zostaa uyta klasa Byte2. W ten sposb sprawdzono, e dziki dziedziczeniu wszystkie operatory dziaaj w poprawnie w klasie Byte2. Analizujc dokadnie klas Byte2, zauwaysz, e konstruktor tej klasy musi by jawnie zdefiniowany i e generowany automatyczniejestjedynie operator=, przypisujcy obiekt klasy Byte2 obiektowi klasy Byte2. Wszystkie pozostae operatory przypisania, ktre bd ci potrzebne, musisz wygenerowa samodzielnie.

Wielokrotne dziedziczenie
Skoro mona dziedziczy po jednej klasie, to moliwo rwnoczesnego dziedziczenia po wikszej liczbie klas wydaje si logiczna. W istocie, wielokrotne dziedziczenie jest moliwe, ale zagadnienie, czy stanowi ono racjonalny element projektowania, jest przedmiotem nieustajcej debaty. Z ca pewnoci nie naley go prbowa, nie majc duszej praktyki w programowaniu i nie znajc gruntownie jzyka. Zanim dojdziesz do wniosku, e musisz wykorzysta wielokrotne dziedziczenie, z pewnoci niemal zawsze dasz sobie rad, uywajc wyczniejednokrotnego dziedziczenia. Pocztkowo wielokrotne dziedziczenie wydaje si do proste podczas dziedziczenia do listy klas podstawowych dodaje si wiksz liczb klas, oddzielajc je przecinkami. Jednake wielokrotne dziedziczenie tworzy wiele moliwoci powstania niejednoznacznoci; z tego powodu w drugim tomie ksiki powicono mu cay rozdzia.

192

Thinking in C++. Edycja polska

Programowanie przyrostowe
Jedn z zalet dziedziczenia i kompozycji jest to, e umoliwiaj one programowanie przyrostowe, pozwalajc na dodawanie nowego kodu bez wprowadzania bdw do kodu ju istniejcego. Jeeli pojawi si bdy, to bd one izolowane w obrbie nowego kodu. Dziki dziedziczeniu (albo kompozycji), wykorzystujcemu istniejc, dziaajc klas, dodajc do niej dane i funkcje skadowe (a take zmieniajc definicje istniejcych funkcji skadowych za pomoc dziedziczenia), pozostawiamy istniejcy kod ktrego kto jeszcze moe uywa w stanie nienaruszonym; nie wprowadzamy do niego bdw. Jeeli wystpi jaki bd, to wiemy, e znajduje si w nowym kodzie, ktryjest znacznie krtszy i atwiejszy do przeczytania ni w przypadku modyfikacji treci ju istniejcego kodu. To do zdumiewajce, jak wyranie klasy s od siebie odgraniczone. Nie potrzebujemy nawet kodu rdowego funkcji skadowych, by mc powtrnie wykorzysta kod wystarczy do tego plik nagwkowy, opisujcy klas, oraz plik wynikowy lub biblioteka, zawierajca skompilowane funkcje skadowe Qest to prawd zarwno w przypadku dziedziczenia,jak i kompozycji). Warto pamita, e projektowanie oprogramowaniajest procesem przyrostowym, podobnie jak proces uczenia si ludzi. Moesz wykona dowoln liczb analiz, ale przystpujc do realizacji projektu nadal nie znasz odpowiedzi na wszystkie pytania. Osigniesz znacznie wicej -- i znacznie szybciej ujrzysz rezultaty -- jeeli zaczniesz hodowa" swj projekt, jakby by yw, rozwijajc si istot, zamiast tworzy go od razu w caoci, jak wielki szklany drapacz chmur 2 . Mimo e dziedziczenie jest uyteczn technik eksperymentowania, to w pewnym momencie, gdy projekt nieco okrzepnie, musisz ponownie przyjrze si swojej hierarchii klas pod ktem przeksztaceniajej w logiczn struktur3. Pamitaj, e dziedziczenie suy rwnie do odzwierciedlenia relacji, ktr mona wyrazi sowami: ta nowa klasa jest typu tamtej, istniejcej ju klasy". Program nie powinien koncentrowa si na sposobie upychania bitw, ale na tworzeniu i operowaniu obiektami rnych typw, przedstawiajcych model w kategoriach okrelonych przez przestrze problemu.

lzutowanie w gr
We wczeniejszej czci rozdziau przedstawilimy, jak wszystkie obiekty klasy dziedziczcej po klasie ifstream posiadaj wszystkie cechy i zachowania waciwe obiektom klasy ifstream. W pliku FName2.cpp mona byo wywoa kad funkcj klasy ifstream w stosunku do obiektw klasy FName2.

Wicej na temat tej idei mona przeczyta w ksice Kenta Becka: Extreme Programming Explained Addison-Wesley, 2000).

>atrz Martin Fowler: Refactoring: Improving lhe Design ofExisting Code (Addison-Wesley, 1999).

Rozdzia 14. Dziedziczenie i kompozycja

493

Najwaniejszym aspektem dziedziczenia nie jest jednak to, e dostarcza ona nowej klasie funkcje skadowe. Stanowi ona relacj, ustanowion pomidzy nowo utworzon klas i klas podstawow. Relacj t mona streci nastpujco: ,,ta nowa klasa jest typu tamtej, istniejcej ju klasy". Slowa te nie stanowijedynie abstrakcyjnego sposobu opisu dziedziczenia s one wspierane bezporednio przez kompilator. Jako przykad rozwamy klas podstawow o nazwie Instrument, reprezentujc instrumenty muzyczne, oraz klas pochodn o nazwie Wind (instrumenty dte). Poniewa dziedziczenie oznacza, e funkcje klasy podstawowej s dostpne rwnie w klasie pochodnej, kady komunikat, ktry moe by wysany do klasy podstawowej, moe by rwnie wysany dojej klasy pochodnej. Tak wic jeeli klasa Instrument posiada funkcj skadow play( ), to ma j rwnie klasa Wind. Wyraajc si cile, oznacza to, e obiekt klasy Wind jest rwnie obiektem typu Instrument. Poniszy przykad pokazuje, w j a k i sposb kompilator wspiera to stwierdzenie:

// Dziedziczenie i rzutowanie w gr enum note { middleC, Csharp. Cflat }; // Itd. class Instrument { public: void play(note) const {} }: // Obiekty klasy Wind s obiektami typu Instrument // poniewa maj one taki sam interfejs: class Wind : public Instrument {}: void tune(Instrument& i) { // ... i.play(middleC):
int main() { Wind f1ute; tune(flute): // Rzutowanie w gre Interesujca jest w tym przykiadzie funkcja tune( ), pobierajca jako argument referencj do obiektu typu Instrument. Jednake funkcja tune( ) zostaa wywoywana w funkcji main( ) z argumentem bdcym referencj do obiektu klasy Wind. Gdy przypomnimy sobie, e w jzyku C++ obowizuj bardzo skrupulatne reguy w sprawie kontroli typw, to moe wyda si dziwne, e funkcja pobierajca argumentjednego typu tak atwo akceptuje argument innego typu. Naley jednak zda sobie spraw z tego, e obiekt klasy Wind jest rwnie obiektem typu Instrument, i nie ma takiej funkcji, ktr mogaby wywoa funkcja tune( ) dla obiektu klasy Instrument i ktrej nie byoby rwnie w klasie Wind ^est to gwarantowane przez dziedziczenie). Kod znajdujcy si w funkcji tune( ) funkcjonuje w stosunku do klasy Instrument i kadej jej klasy pochodnej, a dziaanie, polegajce na konwersji referencji lub wskanika do klasy Wind na referencj lub wskanik do klasy Instrument, nazywanejest rzutowaniem w gr (ang. upcasting).

//: C14:Instrument.cpp

494

Thinking in C++. Edycja polska

Dlaczego rzutowanie w gr"?


Geneza tego terminu ma charakter historyczny i odwouje si do sposobu, w jaki tradycyjnie byy rysowane diagramy dziedziczenia: z klas gwn (najbardziej podstawow), znajdujc si na grze strony, rozrastajce si w d (oczywicie, diagramy te mona rysowa w dowolny sposb, ktry okae si pomocny). A zatem w przypadku programu Instrument.cpp diagram dziedziczenia wyglda nastpujco:

Rzutowanie z klasy pochodnej do podstawowej powoduje przesunicie si w gr diagramu dziedziczenia, dlatego te jest ono powszechnie nazywane rzutowaniem w gr. Rzutowanie w gr jest zawsze bezpieczne, poniewa od typu bardziej wyspecjalizowanego przechodzimy do bardziej oglnego jedyna zmiana, ktra moe zaj w interfejsie klasy, polega na tym, e moe on utraci cz funkcji, ale nie moe w ten sposb uzyska nowych funkcji. Dlatego te kompilator pozwala na rzutowanie w gr, bez koniecznoci uywania jawnych rzutowa ani stosowania jakiej szczeglnego notacji.

Rzutowanie w gr a konstruktor kopiujcy


Jeeli pozwoli si kompilatorowi na wygenerowanie konstruktora kopiujcego klasy pochodnej, to automatycznie wywoa on konstruktor kopiujcy klasy podstawowej, a nastpnie konstruktory kopiujce wszystkich obiektw skadowych (albo przeprowadzi kopiowanie ich bitw w przypadku typw wbudowanych), co zapewni jego prawidowe dziaanie: //: C14:CopyConstructor.cpp // Poprawne tworzenie konstruktora kopiujcego #include <iostream> using namespace std; class Parent { int i: public: Parent(int ii) : i(ii) { cout << "Parent(int ii)\n"; } Parent(const Parent& b) : i(b.i) { cout << "Parent(const Parent&)\n"; } Parent() : i(0) { cout << "Parent()\n": } friend ostream& operator<<(ostream& os. const Parent& b) { return os << "Parent: " << b.i << endl:

Rozdzia 14. Dziedziczenie i kompozycja class Member { int 1; public:

495

Member(int ii) : i(ii) { cout << "Member(int ii)\n"; } Member(const Member& m) : i(m.i) { cout << "Member(const Member&)\n"; } friend ostrearr operator<<(ostream& os, const Member& m) { return os << "Member: " << m.i << endl;

class Child : public Parent { int 1; Member m; public: Child(int ii) : Parent(ii), i(ii), m(ii) { cout << "Child(int ii)\n"; friend ostream& operator<<(ostream& os, const Child& c){ return os << (Parent&)c << c.m << "Child: " << c.1 << endl;

int main() { Child c(2); cout << "wywoanie konstruktora kopiujcego: " << endl; Child c2 = c; // Wywoanie konstruktora kopiujcego cout << "wartoci skadowych obiektu c2:\n" << c2; Funkcja operator<<, zawarta w klasie Child,jest interesujca ze wzgldu na sposb, w jaki wywouje ona funkcj operator<< w stosunku do zawartej w niej czci klasy Parent rzutujc obiekt klasy Child na referencj Parent& (w przypadku rzutowania na obiekt klasy podstawowej, zamiast na referencj, na og uzyskalibymy wynik niezgodny z oczekiwanym): return os << (Parent&)c << c . m Poniewa dziki temu kompilator ujmuje obiekt jako obiekt klasy Parent, to wywouje zdefiniowany w tej wanie klasie operator<<. Klasa Child nie posiada zatem jawnie zdefiniowanego konstruktora kopiujcego. Kompilator generuje wic konstruktor kopiujcy (poniewa jest to jedna z czterech funkcji, ktre generuje, obok domylnego konstruktora jeeli nie zosta utworzony aden konstruktor operatora przypisania oraz destruktora), wywoujc konstruktory kopiujce kIas Parent oraz Member. wiadcz o tym wyniki pracy programu: Parent(int ii) Member(int ii ) Child(int ii)

96

Thinking in C++. Edycja polska wywoanie konstruktora kopiujcego: Parent(const Parent&) Member(const Member&) wartoci skadowych obiektu c2: Parent: 2 Member: 2 Child: 2 Jeeli jednak sprbujemy napisa wasny konstruktor kopiujcy klasy Child, ale popenimy niewinn pomyk i napiszemy go nieprawidowo: Child(const Child& c) : i(c.i). m(c.m) {} to wwczas dla zawartej w klasie Child czci, stanowicej klas podstawow, zostanie wywoany domylny konstruktor. Jest to bowiem przypadek, do ktrego powraca kompilator, gdy nie ma moliwoci wyboru adnego innego konstruktora (trzeba pamita, e dla kadego obiektu, rwnie dla obiektu podrzdnego zawartego w innej klasie, zawsze musi zosta wywoanyjaf konstruktor). Wyniki dziaania programu byyby w tym przypadku nastpujce: Parent(int ii) Member(int ii) Child(int ii) wywoanie konstruktora kopiujcego: Parent() Member(const Member&) wartoci skadowych obiektu c2: Parent: 0 Member: 2 Child: 2 Prawdopodobnie odbiega to od naszych oczekiwa, poniewa na og yczymy sobie, by cz obiektu naleca do klasy podstawowej zostaa skopiowana z istniejcego obiektu do nowego obiektu w ramach dziaania konstruktora kopiujcego. Aby ustrzec si tego bdu, ilekro tworzymy konstruktor kopiujcy, powinnimy pamita o poprawnym wywoaniu konstruktora klasy podstawowej (tak jak to robi kompilator). Na pierwszy rzut oka wyglda to nieco dziwnie, ale stanowi jeszcze jeden przykad rzutowania w gr: Child(const Child& c) : Parent(c), i(c.i), m(c.m) { cout << "Child(Child&)\n"; Dziwnym elementem jest tutaj wywoanie konstruktora kopiujcego klasy Parent, zapisanym w postaci Parent(c). Co jednak oznacza przekazanie obiektu klasy Child konstruktorowi klasy Parent? Ale klasa Child jest klas pochodn klasy Parent, wic referencja do obiektu klasy Child jest rwnie referencj do obiektu klasy Parent. Konstruktor kopiujcy klasy podstawowej dokonuje rzutowania w gr referencji do obiektu klasy Child na referencj do obiektu klasy Parent i uywa go do przeprowadzenia konstrukcji przez kopiowanie. Piszc wasne konstruktory kopiujce, niemal zawsze postpisz tak samo.

Rozdzia 14. << Dziedziczenie i kompozycja

497

Kompozycja czy dziedziczenie (po raz drugi)


Jednym z najprostszych kryteriw umoliwiajcych okrelenie, czego naley uywa: kompozycji czy te dziedziczenia, jest odpowied na pytanie, czy w przypadku tworzonej klasy kiedykolwiek bdzie potrzebne rzutowanie w gr. W poprzedniej czci rozdziau specjalizowana wersja klasy Stack zostaa utworzona za pomoc dziedziczenia. Jest jednak cakiem prawdopodobne, e obiekty klasy StringStack bd uywane wyczniejako kontenery przechowujce acuchy i nigdy nie bdzie stosowane w stosunku do nich rzutowanie w gr. Bardziej odpowiednim rozwizaniem jest wic w tym przypadku kompozycja: / / : C14:InheritStack2.cpp // Kompozycja czy dziedziczenie #include "../C09/Stack4.h" #include ". ./require.h" #include <iostream> #include <fstream> #include <string> using namespace std;

class StringStack ( Stack stack; // Osadzenie zamiast dziedziczenia public: void push(string* str) { stack.push(str): } string* peek() const { return (string*)stack.peek(); }
string* pop() { return (string*)stack.pop():

int main() { ifstream in("InheritStack2.cpp"): assure(in, "InheritStack2.cpp"): string line; StringStack textlines; while(getline(in, line)) textlines.push(new string(1ine)); string* s: while((s - textlines.popO) !- 0) // Brak rzutowania! cout << *s << endl ; Programjest identyczny z programem InheritStack.cpp, z wyjtkiem tego, e w tym wypadku obiekt klasy Stack jest osadzony w klasie StringStack, a funkcje skadowe odwouj si do tego wanie zagniedonego obiektu. Nie wie si to z adnym narzutem czasowym ani pamiciowym, poniewa obiekt podrzdny zajmuje tyle samo miejsca, a caa dodatkowa kontrola typwjest dokonywana w trakcie kompilacji. Mimo ejest to zazwyczaj bardziej skomplikowane, mona by uy w tym przypadku rwnie dziedziczenia prywatnego, wyraajcego implementacj na motywach". Powinno ono rwnie prowadzi do poprawnego rozwizania problemu. Jednz sytuacji,

498

Thinking in C++. Edycja polska w ktrych mogoby mie to znaczenie, bylaby konieczno uycia wielokrotnego dziedziczenia. Znalezienie rozwizania, w ktrym zamiast dziedziczenia mogaby zosta wykorzystana kompozycja, oznaczaoby wyeliminowanie potrzeby stosowania wielokrotnego dziedziczenia.

Rzutowanie w gr wskanikw i referencji


W programie istrument.cpp rzutowanie w gr odbywa si w trakcie wywoania funkcji na zewntrz funkcji pobierana jest referencja do obiektu klasy Wind, ktra wewntrz funkcji staje si referencj do klasy Instrument. Rzutowanie w gr moe mie rwnie miejsce podczas zwykego przypisania wartoci wskanikowi lub referencji:
Wind w; Instrument* ip = &w; // Rzutowanie w gr Instrument& ir = w; // Rzutowanie w gr

Podobniejak w przypadku funkcji, adna z tych sytuacji nie wymagajawnego rzutowania.

Kryzys
Oczywicie, kade rzutowanie w gr powoduje utrat informacji o typie obiektu. Jeeli napiszemy:
Wind w; Instrument* ip = &w;

to kompilator bdzie traktowa zmienn ip wycznie jako wskanik do obiektu klasy Instrument i nic ponadto. Oznacza to, e nie moe on wiedzie, e zmienna ip w rzeczywistoci wskazuje akurat obiekt klasy Wind. Tak wic gdy wywoamy funkcj skadow play( ), piszc: ip->play(middleC); to kompilator wie jedynie, e wywouje funkcj play( ) dla wskanika obiektu klasy Instrument. Wywoa zatem zawart w klasie podstawowej wersj funkcji Instrument::play( ) zamiast funkcj Wind::play( ). Tak wic zachowanie programu bdzie niepoprawne. Jest to powany problem zosta on rozwizany w rozdziale 15., w ktrym opisano kolejny filar programowania obiektowego polimorfizm (zaimplementowany w jzyku C++ w postaci funkcji wirtualnych).

Podsumowanie
Zarwno dziedziczenie, jak i kompozycja pozwalaj na utworzenie nowego typu danych na podstawie typwju istniejcych. Obie te metody osadzaj w nowo utworzonym typie obiekty podrzdne istniejcych typw. Zazwyczaj kompozycji uywarny

Rozdzia 14. Dziedziczenie i kompozycja

499

jednak w przypadku, gdy chcemy wykorzysta istniejce typy w charakterze elementu wewntrznej implementacji nowej klasy. Dziedziczenie stosujemy za wtedy, gdy chcemy wymusi, by nowy typ danych by tego samego typu, co klasa podstawowa (rwnowano typw gwarantuje rwnowano interfejsw). Poniewa klasa pochodna posiada interfejs klasy podstawowej, to moe ona by rzutowana w gr - na klas podstawow, co ma najistotniejsze znaczenie dla polimorfizmu, o czym przekonamy si w rozdziale 15. Mimo e wielokrotne wykorzystywanie kodu, uzyskiwane za pomoc kompozycji oraz dziedziczenia, znacznie pomaga w szybkim rozwoju projektu, to na og przed udostpnieniem hierarchii uywanych klas innym programistom trzeba je jeszcze przeprojektowa. Naszym gwnym celem jest utworzenie hierarchii klas, w ktrej kada klasa bdzie miaa okrelone zastosowanie i nie bdzie ani zbyt dua (gdy wwczas zawieraaby tak wiele funkcji, e jej uywanie byoby niewygodne), ani irytujco maa (bo wwczas nie mona by uywajej osobno albo trzeba by doda do niej nowe funkcje).

wiczenia
Rozwizania wybranych wicze mona znale w dokumencie elektronicznym The Thinking in C+ + Annotated Solution Guide, ktry mona pobra za niewielk opat z witryny http://www.BruceEckel.com. 1. Zmodyfikuj klas Car, zawart w programie Car.cpp, w taki sposb, by dziedziczya rwnie z klasy o nazwie Vehicle (pojazd), umieszczajc w klasie Vehicle odpowiednie funkcje skadowe (tworzc w niej jakie funkcje skadowe). Dodj do klasy Vehicle konstruktor niebdcy konstruktorem domylnym, ktry musisz wywoa w konstruktorze klasy Car. 2. Utwrz dwie klasy, A i B, posiadajce domylne konstruktory, sygnalizujce, e zostay wywoane. Z klasy A wyprowad now klas o nazwie C i utwrz w niej obiekt skadowy klasy B, ale nie twrz konstruktora klasy C. Utwrz obiekt klasy C i przyjrzyj si wynikom dziaania programu. 3. Utwrz trzypoziomow hierarchi klas, posiadajcych domylne konstruktory oraz destruktory, ktre informujo swoim uruchomieniu, wyprowadzajc komunikaty do strumienia cout. Upewnij si, e w przypadku obiektu ostatniej z klas pochodnych wszystkie trzy konstruktory i destruktory s automatycznie wywoywane. Wytumacz kolejno, wjakiej odbywajsi wywoania. 4. Zmodyfikuj program Combined.cpp, by dodajeszczejeden poziom dziedziczenia i nowy obiekt skadowy. Dodaj kod, informujcy o tym, kiedy wywoywane s konstruktory i destruktory. 5. W programie Combined.cpp utwrz klas D, dziedziczc z klasy B oraz posiadajc obiekt skadowy klasy C. Dodaj kod informujcy o tym, kiedy wywoywane s konstruktory i destruktory.

500

Thiriking in C++. Edycja polska

6. Zmodyfikuj program Order.cpp, dodajc na kolejnym poziomie dziedziczenia klas Derived3, posiadajcobiekty skadowe klas Member4 i Member5. Przyjrzyj si wynikom dziaania programu. 7. W programie NameHiding.cpp sprawd, e w klasach Derived2, Derived3 i Derived4 niejest dostpna adna ze znajdujcych si w klasie podstawowej wersji funkcji f(). 8. Zmodyfikuj program NameHiding.cpp, dodajc do klasy Base trzy przecione funkcje h(), i poka, e przedefiniowaniejednej z nich w klasie pochodnej powoduje ukrycie wszystkich pozostaych. 9. Z klasy vector<void*> wyprowad klas pochodn StringVector, a nastpnie przedefiniuj funkcje skadowe pushJback( ) oraz operator[ ] w taki sposb, aby pobieray i zwracay wartoci typu string*. Co si stanie, jeli sprbujesz uy funkcji push_back() z argumentem typu void*? 10. Utwrz klas zawierajc liczb typu long i uyj w konstruktorze skadni wywoania pseudokonstruktora, by zainicjowa t liczb. L Utwrz klas o nazwie Asteroid. Wykorzystaj dziedzicznie, by utworzy specjalizowan wersj klasy PStash, opisanej w rozdziale 13. (PStash.h i PStash.cpp). Wersja ta pobiera i zwraca wskaniki do obiektw klasy Asteroid. Zmodyfikuj rwnie program PStashTest.cpp, by przetestowa swoje klasy. Zmodyfikuj utworzon klas w taki sposb, aby zamiast dziedziczenia zawieraa obiekt skadowy klasy PStash. 12. Powtrz poprzednie wiczenie, uywajc klasy vector zamiast klasy PStash. 13. W programie SynthesizedFunctions.cpp zmodyfikuj klas Chess, dodajc do niej domylny konstruktor, konstruktor kopiujcy oraz operator przypisania. Wyka, e napisaeje prawidowo. 14. Utwrz dwie klasy o nazwach Traveler i Pager, nieposiadajce domylnych konstruktorw, a tylko konstruktory pobierajce argument typu string, ktry klasy te bd kopiowa do wewntrznej zmiennej typu string. Dla kadej z klas utwrz prawidowy konstruktor kopiujcy oraz operator przypisania. Nastpnie utwrz z klasy Traveler, poprzez dziedziczenie, klas BusinessTraveler i dodaj do niej obiekt skadowy typu Pager. Napisz poprawny konstruktor domylny, konstruktor pobierajcy argument typu string, konstruktor kopiujcy i operator przypisania. 15. Utwrz klas zawierajcdwie statyczne funkcje skadowe. Utwrzjej klas pochodn i przedefiniuj jednz tych funkcji skadowych. Poka, e druga funkcja zostaa ukryta w klasie pochodnej. 16. Poszukaj informacji o innych funkcjach skadowych klasy ifstream. Wyprbuj je w pliku FName2.cpp, uywajc do tego celu obiektu File. 17. Uyj dziedziczenia prywatnego (private) i chronionego (protected) do utworzenia dwch klas, bdcych klasami pochodnymi jednej klasy podstawowej. Nastpnie sprbuj dokona rzutowania w gr klas pochodnych na klas podstawow. Wyjanij uzyskany rezultat.

Rozdzia 14. << Dziedziczenie i kompozycja

501

18. W programie Protected.cpp dodaj do klasy Derived funkcj skadow, ktra wywouje chronion funkcj skadow read( ) klasy Base. 19. Zmodyfikuj program Protected.cpp w taki sposb, aby klasa Derived wykorzystywaa dziedziczenie chronione. Zobacz, czy dla obiektu klasy Derived moesz wywoa funkcj vaIue(). 20. Utwrz kias o nazwie SpaceShip, zawierajc funkcj fly(). Z klasy SpaceShip wyprowad klas Shuttle, dodajc do niej funkcj land( ). Utwrz obiekt klasy Shuttle, za pomoc wskanika lub referencji dokonaj jego rzutowania w gr, na klas SpaceShip, a nastpnie sprbuj wywoa funkcj land( ). Wyjanij uzyskane rezultaty. 21. Zmodyfikuj program Instrument.cpp, dodajc do klasy Instrument funkcj prepare(). Wywoaj funkcj prepare() wewntrz funkcji tune(). 22. Zmodyfikuj program Instrument.cpp w taki sposb, aby funkcja play( ) drukowaa komunikat do strumienia cout, a klasa Wind zawieraa przedefmiowan funkcj play( ), drukujcdo strumienia cout inny komunikat. Uruchom program i wyjanij, dlaczego funkcjonuje on odmiennie od oczekiwanych rezultatw. Nastpnie umie sowo kluczowe virtual (zostanie ono opisane w rozdziale 15.) przed deklaracjfunkcji play( ), znajdujcsi w klasie Instrument, i zobacz, wjaki sposb zmienio si dziaanie programu. 23. W pliku CopyConstructor.cpp wyprowad z klasy Child now klas i utwrz w niej obiekt skadowy m klasy Member. Napisz prawidowy konstruktor, konstruktor kopiujcy, operator= oraz operator<< dla strumieni wejcia-wyjcia i przetestuj dziaanie tej klasy w funkcji main(). 24. Zmodyfikuj program CopyConstructor.cpp, dodajc do kIasy Child wasny konstruktor kopiujcy (nie wywoluj konstruktora kopiujcego klasy podstawowej). Rozwi problem, dokonujc na licie inicjatorw konstruktora kopiujcego klasy Child prawidowego, jawnego wywoania konstruktora kopiujcego klasy podstawowej. 25. Zmodyfikuj program InheritStack2.cpp, by zamiast klasy Stack uywa wektora vector<string>. 26. Utwrz klas Rock, posiadajcdomylny konstruktor, konstruktor kopiujcy, operator przypisania oraz destruktor (z ktrych wszystkie sygnalizuj, e zostay wywoane), wyprowadzajc komunikat do strumienia cout. W funkcji main( ) utwrz wektor vector<Rock> (przechowujcy wartoci obiektw klasy Rock) i dodaj do niego kiIka obiektw. Uruchom program i wyjanij uzyskane wyniki. Sprawd, czy s wywoywane destruktory obiektw klasy Rock, znajdujcych si w wektorze. Nastpnie powtrz wiczenie, uywajc wektora vector<Rock*>. Czyjest moliwe utworzenie wektora vector<Rock&>? 27. wiczenie to tworzy wzorzec projektowy o nazwieporednik (ang. proxy). Rozpocznij od klasy podstawowej Subject i utwrz w niej trzy funkcje: f(), g() i h(). Nastpnie wyprowad z niej klas Proxy oraz dwie inne, zawierajce implementacje Implementationl i Implemetation2. Klasa Proxy powinna zawiera wskanik do obiektu klasy Subject, a wszystkie

502

Thinking in C++. Edycja polska

funkcje skadowe klasy Proxy powinny przekazywa swoje wywoania do siebie samych za porednictwem zawartego w klasie wskanika klasy Subject. Konstruktor klasy Proxy pobiera wskanik do obiektu klasy Subject, ktry zostaje zainstalowany w klasie Proxy (zazwyczaj za pomoc konstruktora). W funkcji main( ) utwrz dwa rne obiekty klasy Proxy, wykorzystujce dwie rozmaite implementacje. Nastpnie zmodyfikuj klas Proxy tak, aby mona byo dynamicznie zmienia implementacje. 28. Zmodyfikuj programArrayOperatorNew.cpp, znajdujcy si w rozdziale 13., by pokaza, ejeeli dziedziczy si z klasy Widget, to przydzia pamici nadal dziaa prawidowo. Wyjanij, dlaczego dziedziczenie nie dzialaloby prawidowo w przypadku programu Framis.cpp, zawartego w rozdziale 13. 29. Zmodyfikuj programFramis.cpp, znajdujcy si w rozdziale 13., dziedziczc po klasie Framis i tworzc dla klasy pochodnej nowe wersje operatorw new i delete. Poka, e dziaajone prawidowo.

Polimorfizm i funkcje wirtualne


Polimorfizm (zaimplementowany w jzyku C++ w postaci funkcji wirtualnych) stanowi trzeci fundamentaln waciwo obiektowego jzyka programowania po abstrakcji danych i dziedziczeniu. Tworzy on nowy wymiar oddzielenia interfejsu od implementacji, odgraniczajc pojcie co od pojecia;'a&. Polimorfizm zapewnia lepsz organizacj i czytelno kodu, a take tworzenie moliwych do rozszerzania programw, ktre mog rosn" nie tylko podczas tworzenia pierwotnego projektu, ale rwnie wtedy, gdy trzeba wyposay go w nowe cechy. Kapsukowanie tworzy nowe typy danych, czc ze sob cechy z zachowaniami. Kontrola dostpu oddziela interfejs od implementacji, czynic szczegy prywatnymi. Taki sposb mechanicznej organizacji kodu jest od razu zrozumiay dla kadego, kto posiada dowiadczenie w programowaniu wjzykach proceduralnych. Jednak funkcje wirtualne dokonujpodziau w kategoriach typw. Dziki lekturze rozdziau 14. dowiedzielimy si, e dziedziczenie pozwala na traktowanie obiektu w taki sposb, jakby by obiektem swojego typu albo swojego typu podstawowego. Ta zdolno ma podstawowe znaczenie, poniewa umoliwia traktowanie wielu typw danych (wyprowadzonych z tego samego typu podstawowego) w taki sposb, jakby byy jednym typem. Dziki temu pojedynczy fragment kodu moe dziaa identycznie z rnymi typami danych. Funkcje wirtualne pozwalaj typowi danych na wyraenie swojej odmiennoci w stosunku od innego, podobnego typu, pod warunkiem, e oba typy danych s wyprowadzone z tego samego typu podstawowego. Odmienno ta wyraa si poprzez rnice w zachowaniu funkcji, ktre mona wywoa za porednictwem kIasy podstawowej. W niniejszym rozdziale poznamy funkcje wirtualne. Zaczniemy od podstaw, obejmujcych proste przykady programw, ktrych pozbawiono wszystkiego, z wyjtkiem wirtualnoci".

Rozdzia 15.

504

Thinking in C++. Edycja polska

Ewolucja programistw jzyka C++


Wydaje si, e programici jzyka C przyswajajsobiejzyk C++ w trzech etapach. Najpierw jako ,,lepsze C", poniewa jzyk C++ wymusza deklarowanie wszystkich funkcji przed ich uyciem ijest znacznie bardziej wymagajcy w kwestii uywania zmiennych. Czsto mona znale bdy, zawarte w programie napisanym w jzyku C, ju podczas kompilowania go za pomoc kompilatorajzyka C++. Drugim etapemjest obiektowe" C++. Oznacza to dostrzeenie korzyci: zwizanych z organizacj kodu, ze zgrupowaniem struktur danych wraz z dziaajcymi na tych danych funkcjami, wynikajcych ze znaczenia konstruktorw i destruktorw, a by moe rwnie z prostego dziedziczenia. Wikszo programistw majcych dowiadczenie w pracy z jzykiem C szybko dostrzega wynikajce z tego poytki, poniewa to wanie prbuj osign, tworzc biblioteki. W przypadkujzyka C++ mogliczy w tej kwestii na pomoc kompilatora. atwo jest utkn na poziomie obiektw, poniewa szybko mona go osign i zapewnia on wiele korzyci, nie wymagajc zarazem wikszego wysiku umysowego. Mona zasmakowa w generowaniu nowych typw danych -- tworzy si klasy i obiekty, wysya do nich komunikaty, a wszystko jest atwe i przyjemne. Nie id jednak na atwizn; jeeli zatrzymasz si w tym miejscu, nie poznasz najwspanialszej czci jzyka, ktra pozwala na przeskok do prawdziwego programowania obiektowego. Mona to zrobi tylko za pomoc funkcji wirtualnych. Funkcje wirtualne wzmacniaj pojcie typu zamiast zamyka kod w strukturach i odgradza go; dla pocztkujcego programisty jzyka C++ stanowi wic bez wtpienia najtrudniejsze do zrozumienia pojcie. Jednake wyznaczaj one rwnie punkt zwrotny w rozumieniu programowania obiektowego. Jeeli nie uywasz funkcji wirtualnych, oznacza to, ejeszcze go nie poje. Z uwagi na to, e funkcje wirtualne s cile zwizane z pojciem typw, a typy stanowi istot programowania obiektowego, funkcje wirtualne nie posiadaj adnego odpowiednika w tradycyjnych jzykach proceduralnych. Bdc programist rozumujcym w kategoriach procedur, nie znajdziesz punktu odniesienia, na ktrego podstawie mgby uj funkcje wirtualne, jak to jest moliwe w przypadku niemal kadej innej waciwoci tego jzyka. Cechy zawarte w jzyku proceduralnym mog by zrozumiae na poziomie algorytmicznym, lecz funkcje wirtualne mona poj wycznie z punktu widzenia projektu.

Rzutowanie w gr
W rozdziale 14. przedstawiono, w j a k i sposb obiekty mogy by uywane, zarwno jako obiekty swoich typw, jak i jako obiekty swoich typw podstawowych. Ponadto mona nimi operowa za pomoc adresu typu podstawowego. Pobranie adresu

Rozdzia 15. Polimorfizm i funkcje wirtualne

505

obiektu (zarwno w postaci wskanika, jak i referencji) i traktowanie go jako adresu typu podstawowego nosi nazw rzutowania w gr, z uwagi na sposb, w jaki rysowane s diagramy dziedziczenia w postaci drzew, ze znajdujc si na grze klas podstawow. Zetknlimy si rwnie z problemem, zawartym w poniszym kodzie: //: C15:lnstrument2.cpp // Dziedziczenie i rzutowanie w gr #include <iostream> using namespace std; enum note { middleC. Csharp, Eflat }; // Itd. class Instrument { public: void play(note) const { cout << "Instrument::play" << endl;

}
}:

// Obiekty klasy Wind s obiektami typu Instrument // poniewa maj one taki sam interfejs: class Wind : public Instrument { public: // Redefine interface function: void play(note) const { cout << "Wind::play" << endl;

void tune(Instrument& i) { // . .. i .play(middleC): } int main() { Wind flute: tune(flute); // Rzutowanie w gr } IIIFunkcja tune( ) pobiera (przez referencj) obiekt klasy Instrument, lecz bez protestu przyjmuje rwnie obiekty wszelkich typw wyprowadzonych klasy Instrument. W funkcji main( ) mona zauway, e dzieje si tak w przypadku, gdy obiekt klasy Wind przekazywanyjest do funkcji tune( ), bez potrzeby rzutowania. Jest to dopuszczalne interfejs klasy Instrument musi by zawarty w klasie Wind, poniewa klasa Wind dziedziczy publicznie po klasie Instrument. Rzutowanie w gr z klasy Wind do Instrument moe zawzi" interfejs, ale nigdy nie ograniczy go w wikszym stopniu ni do penego interfejsu klasy Instrument. Te same zasady dotycz wskanikw jedyna rnica polega na tym, e uytkownik musi jawnie pobra adres obiektw przekazywanych funkcji.

506

Thinking in C++. Edycja polska

Problem
O tym, na czym polega problem zwizany z programem Instrument2.cpp, mona si przekona uruchamiajc w program. Wynikiem jego dziaania jest wyprowadzenie tekstu Instrument::play. Nie o to nam, oczywicie, chodzio, poniewa przypadkowo wiemy, e obiektemjest naprawd obiekt klasy Wind, a niejaki Instrument. Wynikiem dziaania programu powinno by wywoanie funkcji Wind::play. Z tego wanie powodu kady obiekt klasy wyprowadzonej z klasy Instrument powinien posiada wasnwersj funkcji play( ), uywanbez wzgldu na sytuacj. Zachowanie programu Instrument2.cpp nie jest niespodziank, jeeli uwzgldni si podejcie jzyka C w stosunku do funkcji. Aby zrozumie te kwestie, trzeba pozna pojcie wizania.

Wizanie wywotenia funkcji


Poczenie wywoania funkcji z jej ciaem jest nazywane wizaniem (ang. binding). W przypadku gdy wizanie jest dokonywane przed uruchomieniem programu (przez kompilator lub program czcy), mwimy o wczesnym wizaniu (ang. early binding). By moe nigdy wczeniej nie zetkne si z tym pojciem, poniewa w przypadku jzykw proceduralnych nigdy nie stanowio ono moliwej do wybrania opcji kompilatory jzyka C znaj tylko jeden sposb wywoania funkcji i rodzajem tym jest wanie wczesne wizanie. Problem, ktry pojawi si w powyszym programie, zosta spowodowany wczesnym wizaniem, poniewa kompilator, dysponujc jedynie adresem obiektu klasy Instrument, niejest w stanie okreslic,jakapowinien wywoa funkcj. Rozwizanie nosi nazw pnego wizania (ang. late binding), co oznacza, e wizanie dokonywane jest w trakcie wykonywania programu na podstawie informacji o typie obiektu. Pne wizanie jest niekiedy rwnie okrelane mianem wizania dynamicznego (ang. dynamie binding) albo wizania podczas wykonywania programu (ang. runtime binding). Jeli jzyk obsuguje pne wizanie, musi istnie jaki mechanizm, umoliwiajcy okrelenie typu obiektu w trakcie wykonywania programu i wywoanie odpowiedniej funkcji skadowej. W przypadku kompilowanego jzyka kompilator co prawda nadal nie wie, jaki jest rzeczywisty typ obiektu, ale wstawia kod, umoliwiajcy odnalezienie i wywoanie odpowiedniego ciaa funkcji. Realizacja mechanizmu, odpowiedzialnego za pne wizanie, zaley od jzyka, ale mona sobie wyobrazi, jakiego rodzaju informacje dotyczce typu musz zosta umieszczone w obiekcie. W dalszej czci rozdziau zobaczymy, jak to funkcjonuje.

Funkcje wirtualne
Aby spowodowa pne wizaniejakiej okrelonej funkcji w j z y k u C++, wymagane jest uycie sowa kluczowego virtual w deklaracji tej funkcji, znajdujcej si w klasie podstawowej. Pne wizanie ma miejsce wycznie w przypadku funkcji

Rozdzia 15. Polimorfizm i funkcje wirtualne

507

wirtualnych i tylko wwczas, gdy uywany jest adres klasy podstawowej, w ktrej zawarte s te funkcje wirtualne, mimo e mog one by rwnie zdefiniowane w jakiej wczeniejszej klasie podstawowej. Aby zadeklarowajak funkcjjako funkcj wirtualn, wystarczy poprzedzi deklaracj tej funkcji sowem kluczowym virtual. Sowo to jest wymagane wycznie w deklaracji funkcji, a nie w jej definicji. Jeeli funkcja zostaa zadeklarowana jako wirtualna w klasie podstawowej, to jest ona rwnie funkcj wirtualn we wszystkich jej klasach pochodnych. Przedefiniowanie funkcji wirtualnej w klasie pochodnej jest zazwyczaj okrelane mianemzaslaniania (ang. overriding}. Zwr uwag na to, e wystarczy zadeklarowa funkcj jako wirtualn w klasie podstawowej. Wszystkie funkcje, znajdujce si w klasach pochodnych, ktrych sygnatury bdodpowiaday deklaracji zawartej w klasie podstawowej, bd wywoywane za pomoc mechanizmu obsugujcego funkcje wirtualne. W deklaracjach zawartych w klasie pochodnej mona uy sowa kluczowego virtual (nie wywouje to adnych niepodanych skutkw), alejest to nadmiarowe i niekiedy mylce. Aby uzyska podane zachowanie programu Instrument2.cpp, wystarczy przed deklaracj funkcji play(), znajdujc si w klasie podstawowej, umieci sowo kluczowe virtual: //: C15:Instrument3.cpp // Pne wizanie dziki uyciu sowa kluczowego virtual |include <iostream> using namespace std; enum note { middleC, Csharp, Cflat }; // Itd. class Instrument { public: virtual void play(note) const { cout << "Instrument::play" << endl:

// Obiekty klasy Wind s obiektami typu Instrument // poniewa maj one taki sam interfejs: class Wind : public Instrument { public: // Zasonicie funkcji interfejsu: void play(note) const { cout << "Wind::play" << endl;

void tune(Instrument& i) // ... i.play(middleC); int main() { Wind flute: tune(flute): // Rzutowanie w gore

508

Thinking in C++. Edycja polska

Program ten jest identyczny z programem Instrument2.cpp, z wyjtkiem dodanego sowa kluczowego virtual, ajednak dziaa od niego zupenie inaczej. Obecnie wprowadza on tekst Wind::play.

Rozszerzalno
W przypadku gdy funkcja pIay( ) jest zadeklarowana jako wirtualna w klasie podstawowej, mona utworzy dowolnlczb nowych typw, nie zmieniajc w ogle funkcji tune(). W dobrze zaprojektowanym programie obiektowym wikszo funkcji bdzie wygldaa tak, jak funkcja tune( ) komunikujc si wycznie z interfejsem klasy podstawowej. Program taki jest rozszerzalny, poniewa nowe funkcje s dodawane poprzez wyprowadzanie nowych typw danych ze wsplnej klasy podstawowej. Nie ma potrzeby wprowadzania jakichkolwiek zmian w funkcjach, odwoujcych si do interfejsu klasy podstawowej, by mogy one obsugiwa nowo utworzone klasy. Poniej zamieszczony jest przykad dotyczcy instrumentw. Zawiera on wiksz liczb funkcji wirtualnych i szereg nowych klas, dziaajcych prawidowo z poprzedni, niezmienionpostacifunkcji tune(): //: C15:Instrument4,cpp // Moliwo rozszerzania programw w programowaniu obiektowym #include <iostream> using namespace std; enum note { middleC. Csharp. Cflat }; // Itd. class Instrument { public: virtual void play(note) const { cout << "Instrument::play" << endl; } virtual char* what() const { return "Instrument"; } // Zakadamy, ze funkcja modyfikuje obiekt: virtual void adjust(int) {} }: class Wind : public Instrument { public: void play(note) const { cout << "Wind::play" << endl; char* what() const { return "Wind": } void adjust(int) {}

}.
class Percussion : public Instrument { public: void play(note) const { cout << "Percussion::play" << endl; 1 char* what() const { return "Percussion"; } void adjust(int) {}

Rozdzia 15. << Polimorfizm i funkcje wirtualne class Stringed : public Instrument { public: void play(note) const { cout << "Stringed::play" << endl; char* what() const { return "Stringed"; } void adjust(int) {}

509

class Brass : public Wind { public: void play(note) const { cout << "Brass::play" << endl; }

char* what() const { return "Brass"; }

class Woodwind : public Wind { public: void play(note) const { cout << "Woodwind::play" << endl; } char* what() const { return "Woodwind";
// Funkcja taka sama. jak poprzednio: void tune(Instrument& i) { // ... i.play(middleC);

// Nowa funkcja: void f(Instrument& i) { i.adjust(l); } // Rzutowanie w gr podczas inicjalizacji tablicy: Instrument* A[] - { new Wind, new Percussion. new Stringed. new Brass.
int main() { Wind flute; Percussion drum; Stringed violin; Brass flugelhorn; Woodwind recorder; tune(flute); tune(drum); tune(violin); tune(flugelhorn); tune(recorder); f(flugelhorn);

510

Thinking in C++. Edycja polska

Poniej klasy Wind zosta utworzony dodatkowy poziom dziedziczenia, ale mechanizm funkcji wirtualnych dziaa prawidowo, niezalenie od istniejcej liczby poziomw. Funkcja adjust( ) nie zostaa zasonita w klasach Brass (blaszane instrumenty dte) i Woodwind (drewniane instrumenty dte). W takich przypadkach automatycznie uywana jest definicja, znajdujca si najbliej" w hierarchii klas. Kompilator gwarantuje, e zawsze islnitjejaka definicja funkcji wirtualnej, dziki czemu nigdy nie moe zdarzy si wywoanie, ktre nie zostaoby powizane z ciaem jakiej funkcji (byoby to katastrofalne). Tablica A[ ] zawiera wskaniki do klasy podstawowej Instrument, dziki czemu rzutowanie w gr odbywa si podczas inicjalizacji tablicy. Tablica ta oraz funkcja f ( ) zostan wykorzystane w dalszej czci dyskusji. W przypadku wywoa funkcji tune( ) rzutowanie przebiega oddzielnie w stosunku do kadego rodzaju obiektu, jednak zawsze dziaa zgodnie z oczekiwaniami. Mona to opisa jako wysanie komunikatu obiektowi, ktry zadecyduje o tym, co z nim zrobi". Funkcja wirtualna jest lup, ktrej mona uy, prbujc dokona analizy projektu gdzie powinny znajdowa si klasy podstawowe i w jaki sposb zamierzamy rozszerza program. Nawet gdy, rozpoczwszy prac nad programem, nie odkryjemy waciwych interfejsw klas podstawowych oraz funkcji wirtualnych, czsto zauwaymy je pniej, czasami nawet znacznie pniej planujc jego rozszerzenie lub przeprowadzajc jego pielgnacj w jaki inny sposb. Nie wynika to z bdw popenionych w trakcie analizy czy projektowania oznacza jedynie, e nie znalimy lub nie moglimy znae od razu wszystkich informacji. Dziki cisej modularyzacji klas wjzyku C++ wystpienie takiej sytuacji nie stanowi powanego problemu, poniewa zmiany dokonywane w jednej czci systemu nie przenosz si do jego innych czesci,jak wjzyku C.

W jaki sposb jzyk C++ realizuje pne wizanie?


W jaki sposb dokonuje si pne wizanie? Caa praca jest wykonywana w niewidoczny sposb przez kompilator, instalujcy wszelkie niezbdne mechanizmy zwizane z pnym wizaniem gdy tego zadamy, tworzc funkcje wirtualne. Z uwagi na to, e rozumienie mechanizmu dziaania funkcji wirtualnych wjzyku C++ czsto przydaje si podczas programowania, w podrozdziale zostanie szczegowo opisany sposb, wjaki kompilator realizuje ten mechanizm. Sowo kluczowe virtual informuje kompilator, e nie powinien dokonywa wczesnego wizania. Powinien natomiast automatycznie zainstalowa wszystkie mechanizmy, niezbdne do przeprowadzenia pnego wizania. Oznacza to, e w przypadku gdy funkcja play( ) zostanie wywoywana dla obiektu Brass, podanego w postaci adresu obiektu klasypodstawowej Instrument, wywoana zostanie odpowiednia funkcja.

Rozdzia 15. Polimorfizm i funkcje wirtualne

511

Aby to zrealizowa, typowy kompilator 1 dla kadej klasy zawierajcej funkcje wirtualne tworzy pojedyncz tablic (nazywan VTABLE). W tablicy VTABLE kompilator umieszcza adresy wirtualnych funkcji zawartych w klasie. W kadej klasie posiadajcej funkcje wirtualne niejawnie umieszczany jest wskanik wirtualny (oznaczany skrtem VPTR od ang. vpointer, virtualpointer), wskazujcy tablic VTABLE tej klasy. Gdy za porednictwem wskanika obiektu klasy podstawowej wywouje si funkcj wirtualn (czyli dokonuje si wywoania polimorficznego), kompilator niejawnie wstawia kod, pobierajcy wskanik VPTR i odnajdujcy adres funkcji w tablicy VTABLE. Wywouje on w ten sposb waciw funkcj i umoliwia realizacj pnego wizania. Wszystkie te dziaania tworzenie dla kadej klasy tablicy VTABLE i wstawianie kodu, wywoania funkcji wirtualnej -- odbywaj si automatycznie, w zwizku z czym nie trzeba sobie nimi zaprzta uwagi. Dziki wirtualnym funkcjom- dla obiektu zostaje wywoana waciwa funkcja, nawet w przypadku gdy kompilator nie zna dokadniejego typu. W kolejnych podrozdziaach opisano ten proces bardziej szczegowo.

Przechowywanie informacji o typie


W adnej z klas nie jest w jawny sposb przechowywana informacja o typie. Tymczasem poprzednie przykady i zwyczajna logika podpowiadaj, e w obiektach musi by przechowywanyjaki rodzaj informacji dotyczcej ich typu w przeciwnym razie typ obiektw nie mgby bowiem zosta okrelony w trakcie dziaania programu. To prawda, ale informacja dotyczca typu jest ukryta. Aby si o tym przekona, przeledmy przykadowy program, porwnujcy wielkoci obiektw klas, zawierajcych funkcje wirtualne, z obiektami tych klas, ktre ich nie uywaj: / / : C15:Sizes.cpp // Wielko obiektw klas z funkcjami // wirtualnymi i bez nich #include <iostream> using namespace std; class NoVirtual { int a; public: void x() const {} int i() const { return 1: }
}:

class OneVirtual { int a: public: virtual void x() const {} int i() const { return 1; } Kompilatory mogrealizowa dziaanie funkcji wirtualnych w dowolny sposb, ale przedstawiona tutaj metoda stanowi niemal uniwersalne rozwizanie problemu.

Thinking in C++. Edycja polska

class TwoVirtuals { public: virtual void x() const {} virtual int i() const { return 1; int main() { cout << "int: " << sizeof(int) << endl; cout << "NoVirtual : " << sizeof(NoVirtual) << endl; cout << "void* : " << sizeof(void*) << endl; cout << "OneVirtual : " << sizeof(OneVirtual) << endl; cout << "TwoVirtuals: " << sizeof(TwoVirtuals) << endl; W przypadku braku funkcji wirtualnych (klasa NoVirtual) wielko obiektu jest do2 kadnie zgodna z oczekiwaniami jest ona wielkoci pojedynczej zmiennej typu int. Jeli chodzi o klas OneVirtual, zawierajcjednfunkcj wirtualn, wielkoci obiektu jest wielko obiektu klasy NoVirtual, powikszona o rozmiar wskanika typu void*. Okazuje si, e w przypadkujednej lub wikszej liczby funkcji wirtualnych kompilator umieszcza w strukturze klasy pojedynczy wskanik (VPTR). Nie ma adnej rnicy wielkoci pomidzy obiektami klas OneVirtual i TwoVirtuals. Wynika to z tego, e wskanik VPTR wskazuje tablic zawierajc adresy funkcji. Wystarczy tylkojedna tablica, poniewa adresy wszystkich funkcji wirtualnych klasy zawarte s w pojedynczej tablicy. Przykad ten wymaga zastosowania przynajmniej jednej skadowej. Gdyby klasy nie zawieray w ogle danych skadowych, kompilatorjzyka C++ narzuciby niezerow wielko obiektw, z uwagi na to, e kady obiekt musi posiada unikalny adres. atwo to zrozumie, prbujc wyobrazi sobie odwoywanie si do obiektw o zerowej wielkoci, znajdujcych si w tablicy. W zwizku z tym do obiektw, ktre miayby zerow wielko, wstawiane s lepe" skadowe. W przypadku umieszczenia w klasie informacji o typie, bdcego wynikiem uycia funkcji wirtualnych, informacja ta zastpuje lep" skadow. Aby si o tym przekona, sprbuj zaznaczy jako komentarze deklaracje int a, znajdujce si we wszystkich klasach powyszego programu.

int a;

Obraz funkcji wirtualnych


Do zrozumienia, co si waciwie dzieje podczas uywania funkcji wirtualnych, przydatne jest graficzne przedstawienie podejmowanych niejawnie dziaa. Poniej zamieszczono rysunek przedstawiajcy tablic wskanikw A[ ], zawart w programie Instrument4.cpp.

W przypadku niektrych kompilatorw mog wystpi niezgodnoci, dotyczce rozmiarw, ale zdarza si? to rzadko.

Rozdzia 15. << Polimorfizm i funkcje wirtualne

513

Tablica wskanikw do obiektw klasy Instrument nie posiada adnej konkretnej informacji o typach kady z jej elementw wskazuje jedynie obiekt typu Instrument. Obiekty klas Wind, Percussion, Stringed i Brass nale do tej kategorii, poniewa wszystkie te klasy s klasami pochodnymi klasy Instrument (a zatem maj taki sam interfejs, jak klasa Instrument, i mog reagowa na te same komunikaty), wic ich adresy mog by umieszczone w tablicy. Kompilator nie wie jednak, e s one czym wicej ni tylko obiektami klasy Instrument, wic gdyby decyzja naleaa do niego, wywoaby wersje wszystkich funkcji, zawarte w klasie podstawowej. Lecz w tym przypadku funkcje te zostay zadeklarowane ze sowem kluczowym virtual, wic dzieje si co innego. Ilekro tworzona jest klasa zawierajca funkcje wirtualne lub wyprowadzamy klas z innej klasy, zawierajcej funkcje wirtualne, kompilator tworzy dIa tej klasy unikatow tablic VTABLE, widoczn po prawej stronie diagramu. W tablicy VTABLE umieszcza adresy wszystkich funkcji, ktre zostay zadeklarowane w tej klasie albo wjej klasach podstawowych. Jeeli funkcja, ktra zostaa zadeklarowana w klasie podstawowej, nie zostanie zasonita, to kompilator wpisuje do tablicy adres funkcji zawartej w klasie podstawowej (wida to w przypadku funkcji adjust tablicy VTABLE klasy Brass). Nastpnie kompilator umieszcza w klasie wskanik VPTR (co mona byo zobaczy w programie Sizes.cpp). W przypadku prostego dziedziczenia, takiego jak przedstawione powyej, kady obiekt posiada tylko jeden wskanik VPTR. Wskanik ten musi zosta zainicjowany po to, by wskazywa adres pocztku waciwej tablicy VTABLE (odbywa si to w konstruktorze, co zostanie pniej przedstawione bardziej szczegowo). Kiedy wskanik VPTR zostanie zainicjowany za pomoc adresu odpowiedniej tablicy VTABLE, obiekt w istocie wie", jakiego jest typu. Ale ta samowiadomo" jest bezuyteczna, dopki nie zostanie on uyty w miejscu wywoania funkcji wirtualnej. Kiedy funkcja wirtualna jest wywoywana za porednictwem adresu klasy podstawowej (w tej sytuacji kompilator nie posiada wszystkich informacji, niezbdnych do przeprowadzenia wczesnego wizania), dzieje si co szczeglnego. Zamiast typowego

514

Thinking in C++. Edycja polska wywoania funkcji, ktre jest instrukcj CALL asemblera, kompilator generuje inny kod, realizujcy wywoanie funkcji. Poniej przedstawiono, w jaki sposb przebiega wywoanie funkcji adjust( ) dla obiektu klasy Brass, jeeli jest dokonywane za porednictwem wskanika do obiektu klasy Instrument (taki sam rezultat daje referencja do obiektu klasy Instrument):

Kompilator zaczyna od wskanika do obiektu klasy Instrument, ktry wskazuje adres pocztku obiektu. Wszystkie obiekty klasy Instrument, a take klas pochodnych tej klasy, posiadaj wskaniki VPTR w tym samym miejscu (czsto na pocztku obiektu), dziki czemu kompilator moe pobra wskanik VPTR obiektu. Wskazuje on pocztek tablicy VTABLE. Wszystkie adresy funkcji zawartych w tablicy VTABLE s uoone w tym samym porzdku, niezalenie od konkretnego typu obiektu. Adres funkcji play( ) jest pierwszy, what( ) drugi, a funkcji adjust( ) trzeci. Kompilator wie, e niezalenie od konkretnego typu obiektu, funkcja adjust( ) znajduje si pod adresem VPTR+2. Tak wic zamiast nakaza: wywoaj funkcj, znajdujc si pod adresem bezwzgldnym Instrument::adjust" (wczesne wizanie - dziaanie nieprawidowe), generuje on kod, zawierajcy polecenie: wywoaj funkcj znajdujc si pod adresem VPTR+2". Z uwagi na to, e pobieranie wartoci wskanika VPTR oraz wyznaczanie aktualnego adresu funkcji odbywaj si w trakcie pracy programu, wynikiem jest oczekiwane pne wizanie. Wysyamy do obiektu komunikat, a obiekt domyla si", co powinien z nim zrobi.

Rzut oka pod mask


Pomocne moe by przeanalizowanie kodu w j z y k u asemblera, wygenerowanego dla wywoania funkcji wirtualnej. Dziki temu mona sprawdzi, czy rzeczywicie dokonuje si w tym przypadku pne wizanie. Poniej przedstawiono kod wygenerowany przez kompilator dla wywoania: 1 .adjust(l); wewntrz funkcji f(Instrument& i): push 1 push si

mov bx. word ptr [si] call word ptr [bx+4] add sp, 4
Argumenty wywoania funkcji jzyka C++, podobnie jak w jzyku C, s umieszczane na stosie od prawej strony do lewej (ta kolejno jest niezbdna do obsugi zmiennej listy argumentw jzyka C), wic pierwszy na stosie jest umieszczany argument rwny 1. W tym miejscu funkcji rejestr si (element architektury procesorw X86 fir^ my Intel) zawiera adres zmiennej i. Jest on rwnie umieszczany na stosie jako ad^ res pocztku interesujcego nas obiektu. Pamitaj, e adres pocztku obiektu odpowiada

Rozdzia 15. * Polimorfizm i funkcje wirtualne

515

wartoci wskanika this, ktry przed wywoaniem kadej funkcji skadowej jest niejawnie umieszczany na stosie w charakterze argumentu. Dziki temu funkcja ta wie, dlajakiego obiektu zostaa wywoana. Dlatego te przed wywoaniem kadej funkcji skadowej (z wyjtkiem statycznych funkcji skadowych, nieposiadajcych wskanika this) na stosie jest zawsze umieszczany o jeden argument wicej ni wynosi liczba argumentw funkcji. Nastpnie naley zrealizowa wywoanie funkcji wirtualnej. Najpierw musi zosta pobrany wskanik VPTR, dziki ktremu bdzie mona znale tablic VTABLE. W przypadku tego kompilatora wskanik VPTR jest wstawiany na pocztku obiektu, wic zawarto wskanika this odpowiada wskanikowi VPTR. Instrukcja znajdujca si w wierszu: mov bx. word ptr [si] pobiera sowo, wskazywane przez rejestr si (czyli wskanik this), ktrym jest wskanik VPTR. Umieszcza ona warto wskanika VPTR w rejestrze bx. Wskanik VPTR, znajdujcy si w rejestrze bx, wskazuje adres pocztku tablicy VTABLE. Jednake wskanik funkcji, ktr naley wywoa, znajduje si w tej tablicy nie pod adresem zerowym, tylko pod drugim (poniewa jest to trzecia funkcja na licie). Wprzypadku tego modelu pamici kady wskanik funkcji zajmuje dwa bajty, wic w ceIu wyliczenia adresu funkcji w tablicy kompilator dodaje do VPTR warto cztery. Warto zauway, e jest to staa warto, okrelona podczas kompilacji, wic jedyn istotn kwestijest to, e wskanik funkcji, znajdujcy si pod adresem o numerze dwa, jest adresem funkcji adjust(). Na szczcie, kompilator zajmuje si wszystkimi szczegami za nas, gwarantujc, e wszystkie wskaniki funkcji we wszystkich tablicach VTABLE danej hierarchii klas wystpuj w tym samym porzdku, niezalenie od kolejnoci, wjakiej mog one by zasaniane w klasach pochodnych. Po wyznaczeniu adresu waciwej funkcji w tablicy VTABLE funkcja ta jest wywoywana. Za pomoc instrukcji: call word ptr [bx+4] rwnoczenie pobieranyjest adres i wywoywana funkcja, znajdujca si pod nim. Na kocu cofanyjest wskanik stosu w celu usunicia wszystkich argumentw, ktre zostay na nim umieszczone przed wywoaniem funkcji. W kodzie asemblera, wygenerowanym dla jzykw C oraz C++, czsto mona zobaczy, e kod wywoujcy funkcj usuwajej argumenty ze stosu, ale moe to przebiega rnie, w zalenoci od architektury procesora i implementacji kompilatora.

Instalacja wskanika wirtualnego


Z uwagi na to, e wskanik VPTR wyznacza funkcjonowanie wirtualnej funkcji obiektu, wanejest, aby wskanik ten zawsze wskazywa waciw tablic VTABLE. Nie chcielibymy nawet mie moliwoci wywoania wirtualnej funkcji, zanim wskanik VPTR nie zostaby prawidowo zainicjowany. Oczywicie, miejscem, w ktrym inicjalizacja moe by zagwarantowana, jest konstruktor, ale aden z przykadw klasy Instrument nie posiada konstruktorw.

316

Thinking in C++. Edycja polska Jest to przypadek, w ktrym niezbdne jest utworzenie domylnego konstruktora. W przykadach klasy Instrument kompilator tworzy domylny konstruktor, ktry nie wykonywa adnych dziaa oprcz inicjalizacji wskanika VPTR. Konstruktor ten jest, oczywicie, automatycznie wywoywany dla wszystkich obiektw klasy Instrument, zanim mona cokolwiek z nimi zrobi. Dziki temu wiadomo, e wywoywanie wirtualnych funkcji jest zawsze bezpieczne. Konsekwencje, wynikajce z automatycznej inicjalizacji wskanika VPTR w obrbie konstruktora, zostan omwione w jednym z nastpnych podrozdziaw.

Obiekty s inne
Warto zrozumie, e rzutowanie w gr odbywa si wycznie w stosunku do adresw. Jeeli kompilator widzi obiekt, zna jego dokadny typ i dlatego (w jzyku C++) nie uywa pnego wizania w adnym wywoaniu funkcji a przynajmniej nie musi go stosowa. Ze wzgldu na efektywno, w razie wywoania wirtualnej funkcji w stosunku do obiektw wikszo kompilatorw przeprowadza wczesne czenie, poniewa zna ich dokadny typ. Ilustruje to poniszy przykad:
. '-V

//: C15:Early.cpp // Wczesne czenie 1 funkcje wirtualne #include <iostream> #include <string> using namespace std; class Pet { public: virtual string speak() const { return ""; } class Dog : public Pet { public: string speak() const { return "Hau!"; int main() { Dog ralph; Pet* pl = &ralph; Pet& p2 - ralph; // Pne czenie w obu przypadkach: cout << "pl->speak() = " << pl->speak() <<endl; cout << "p2.speak() - " << p2.speak() << endl; // Wczesne czenie (prawdopodobnie):

Pet p3;

cout << "p3.speak() - " << p3.speak() << endl; } III-

Podczas wywoa pl->speak( ) oraz p2.speak( ) uywany jest adres, co oznacza, e informacja nie jest pena zmienne pl i p2 mog reprezentowa adres obiektu klasy Pet lub innej klasy, wyprowadzonej z klasy Pet, musi by wic wykorzystany mechanizm funkcji wirtualnych. W czasie wywoania p3.speak( ) nie wystpuje ta niejednoznaczno. Kompilator wie, e ma do czynienia z obiektem i znajego dokadny

Rozdzia 15. Polimorfizm i funkcje wirtualne

517

typ, nie istnieje wic moliwo, by by to obiekt klasy pochodnej klasy Pet tojest doktadnie obiekt klasy Pet. Tak wic zostanie w tym przypadku zastosowane wczesne wizanie. Jeeli jednak kompilator nie chce si zbytnio trudzi, to moe w tym przypadku uy rwnie pnego wizania i rezultat bdzie taki sam.

Dlaczego funkcje wirtualne?


Moesz w tym miejscu zapyta: Skoro ta technikajest tak wana i umoliwia zawsze wywoywanie tych waciwych<< funkcji, to dlaczego stanowi ona opcj? Dlaczego musz w ogle o niej wiedzie?". To dobre pytanie, a odpowied na nie stanowi element podstawowej filozofii jzyka C++: Poniewa nie jest tak efektywna". W przedstawionym wczeniej fragmencie programu w jzyku asemblera zamiast jednego, prostego wywoania funkcji, znajdujcej si pod bezwzgldnym adresem, widniej dwie -- bardziej skomplikowane - instrukcjejzyka asemblera, niezbdne do wywoania funkcji wirtualnej. Wymaga to zarwno wikszej iloci pamici, jak i duszego czasu wykonania. W przypadku niektrych jzykw obiektowych przyjto zaoenie, e pne wizanie ma dla programowania obiektowego tak fundamentalne znaczenie, e naley je realizowa zawsze. W zwizku z tym nie powinno by ono opcjonalne, a uytkownik nie musi w ogle o nim wiedzie. Jest to decyzja projektowa, podjta podczas tworzenia jzyka i sprawdza si ona w przypadku wielu jzykw programowania 3 . Jednake jzyk C++ jest spadkobiercjzyka C, w ktrym efektywno ma podstawowe znaczenie. Jzyk C powsta bowiem po to, by zastpi jzyk asemblera podczas implementacji systemu operacyjnego (w ten sposb czynic ten system operacyjny Unix znacznie bardziej przenonym od jego poprzednikw). Jednym z gwnych powodw powstania jzyka C++ by zamiar uczynienia programowania wjzyku C bardziej efektywnym 4 . A pierwszym pytaniem programistyjzyka C po zetkniciu z jzykiem C++ jest: Jaki to bdzie miao wpyw na wielko i szybko programu?". Gdyby odpowied brzmiaa: Wszystko po staremu, tylko podczas wywoania funkcji zawsze wystpuje niewielki narzut", wielu pozostaoby przyjzyku C, nie zamieniajc go na C++. Ponadto nie byoby moliwe stosowanie funkcji inline, poniewa funkcje wirtualne musz mie adres, ktry trzeba umieci w tablicy VTABLE. Tak wic funkcje wirtualne stanowi opcj; domyln opcj jzyka s funkcje, ktre nie s wirtualne, co oznacza rozwizanie gwarantujce wiksz szybko. Stroustrup stwierdzi, e przywiecaa mu nastpujca myl: Jeeli czego nie uywasz, nie pa za to". Tak wic wystpowanie sowa kluczowego virtual umoliwia regulacj efektywnoci. Jednake podczas projektowania klas nie naley przejmowa si wzgldami efektywnoci. Jeeli zamierzasz wykorzystywa polimorfizm, to uywaj wszdzie Na przykad Smalltalk, Java i Python wykorzystujto podejcie z powodzeniem. W Bell Labs, gdzie powstajzyk C++, pracuje bardzo wielu programistwjzyka C. Zwikszenie ich efektywnoci, choby w najmniejszym stopniu, pozwala firmie na zaoszczdzenie milionw.

518

Thinking in C++. Edycja polska

funkcji wirtualnych. Tylko w przypadku poszukiwania sposobw przyspieszenia kodu trzeba znale funkcje, ktre mona przeksztaci w funkcje nie bdce funkcjami wirtualnymi (a w takich przypadkach zazwyczaj znacznie wicej mona uzyska w innych obszarach dobry program profilujcy lepiej wskae wskie gardla ni programista, opierajcy si na przypuszczeniach). Praktyka dowodzi, e wpyw przejcia na jzyk C++ na wielko i szybko programw siga, w porwnaniu zjzykiem C, okoo 10 procent, a czstojest ona znacznie mniejsza. Powodem sprawiajcym, e moliwejest uzyskanie nawet lepszej efektywnoci czasowej i pamiciowej, jest to, e w jzyku C++ mona zaprojektowa program w taki sposb, by by mniejszy i szybszy.

Abstrakcyjne klasy podstawowe i funkcje czysto wirtualne


W trakcie projektowania nierzadko wystpuje potrzeba, by klasa podstawowa stanowia wycznie interfejs dla swoich klas pochodnych. Oznacza to, e nie chcemy, by ktokolwiek tworzy obiekty klasy podstawowej, ajedynie dopuszczamy wykorzystanie rzutowania w gr, umoliwiajce uycie jej interfejsu. Uzyskujemy to, czynic klas abstrakcyjn, co zachodzi w przypadku, gdy utworzy si w niej przynajmniej jedn/un^c/ czysto wirtualn (ang. pure virtualfunction). Funkcje czysto wirtualne mona rozpozna po tym, e uywaj sowa kluczowego virtual, a na ich kocu wystpuje = 0. Kompilator nie pozwoli nikomu na utworzenie obiektu takiej klasy. Klasy abstrakcyjne snarzdziem, umoliwiajcym narzucenie pewnego modelu. Gdy dziedziczonajest klasa abstrakcyjna, to w klasie pochodnej musz zosta zaimplementowane wszystkie jej czysto wirtualne funkcje, gdy w przeciwnym wypadku klasa pochodna rwnie stanie si klas abstrakcyjn. Utworzenie funkcji czysto wirtualnej pozwala na umieszczeniejejjako funkcji skadowej w interfejsie klasy, bez koniecznoci tworzenia (by moe pozbawionego sensu) kodu, stanowicego ciao tej funkcji. Rwnoczenie uycie funkcji czysto wirtualnych wymusza na klasach pochodnych dostarczenie ich definicji. We wszystkich przykadach dotyczcych instrumentw funkcje zawarte w klasie podstawowej Instrument byy jedynie wypeniaczami". Gdyby zostay one kiedykolwiek wywoane, oznaczaoby to, e co zadziaao nieprawidowo. Zadaniem klasy Instrument jest bowiem utworzenie wsplnego interfejsu dla wszystkich wyprowadzonych z niej klas pochodnych. Okrelenie wsplnego interfejsu nastpuje jedynie z tej przyczyny, e w ten sposb moe on zosta odmiennie wyraony dla kadej klasy pochodnej. Tworzy on podstawowy szablon, wyznaczajcy to, co jest wsplne dla wszystkich klas pochodnych - nic wicej. Tak wic klasa Instrument jest odpowiednim kandydatem do klasy abstrakcyjnej. Klas abstrakcyjn mona utworzy, gdy zamierza si jedynie operowa zbiorem klas za porednictwem wsplnego interfejsu, ale interfejs ten nie musi posiada implementacji (a przynajmniej nie musi ona by pena).

Rozdzia 15. Polimorfizm i funkcje wirtualne

519

Obiekty klas, wyraajcych takie pojcia, jak klasa Instrument, zrealizowana w postaci klasy abstrakcyjnej, prawie nigdy nie posiadaj znaczenia. Oznacza to, e klasa Instrument suy jedynie do okrelenia interfejsu, a nie konkretnej implementacji. Tworzenie obiektu, ktry byby tylko instrumentem, nie ma zatem sensu i zapewne chcielibymy uniemoliwi uytkownikowi tworzenie takich obiektw. Mona to uzyska, definiujc wszystkie wirtualne funkcje klasy Instrument w taki sposb, by drukoway one komunikat o bdzie to jednak spowodowaoby opnienie pojawienia si informacji o bdzie a do momentu uruchomienia programu, a ponadto wymagaoby gruntownego przetestowania przez uytkownika. Znacznie lepiej jest wykry ten problemju na etapie kompilacji. Poniej zostaa przedstawiona skadnia deklaracji czysto wirtualnej funkcji: virtual void f() = 0; W ten sposb naley zwrci si do kompilatora, by zarezerwowa funkcji miejsce w tablicy VTABLE, ale nie powinien umieszcza w nim adnego konkretnego adresu. Jeeli chobyjedna funkcja w klasie zostaa zadeklarowanajako funkcja czysto wirtualna, to tablica VTABLE niejest kompletna. Jak wic reaguje kompilator w przypadku, gdy kto prbuje utworzy obiekt klasy, ktrej tablica VTABLE jest niekompletna? Nie moe bezpiecznie utworzy obiektu klasy abstrakcyjnej, zgasza wic komunikat o bdzie. Dziki temu kompilator gwarantuje czysto" abstrakcyjnej klasy. Tworzc klas abstrakcyjn, mamy pewno, e klient-programista nie uyjejej w niepoprawny sposb.

Thinking in C++. Edycja polska Przedstawiony poniej program Instrument4.cpp zosta zmodyfikowany w taki sposb, by wykorzystywa funkcje czysto wirtualne. Poniewa klasa Instrument zawiera obecnie wycznie funkcje czysto wirtualne, nazywamy j klas czysto abstrakcyjn .;pure abstract class): / I : C15:Instrument5.cpp // Czysto abstrakcyjne klasy podstawowe #include <iostream> using namespace std; enum note { middleC. Csharp, Cflat }; // Itd. class Instrument { public: // Funkcje czysto wirtualne: virtual void play(note) const = 0; virtual char* what() const = 0; // Zakadamy, e funkcja modyfikuje obiekt: virtual void adjust(int) = 0; }:

// Pozostaa cz programu pozostaa taka sama...

class Wind : public Instrument { public: void play(note) const { cout << "Wind::play" << endl; } char* what() const { return "Wind": } void adjust(int) {} class Percussion : public Instrument { public: void play(note) const { cout << "Percussion::play" << endl: } char* what() const { return "Percussion" void adjust(int) {} class Stringed : public Instrument { public: void play(note) const { cout << "Stringed::play" << endl: } char* what() const { return "Stringed" void adjust(int) {} class Brass : public Wind { public: void play(note) const { cout << "Brass::play" << endl: } char* what() const { return "Brass": }

Rozdzia 15. Polimorfizm i funkcje wirtualne class Woodwind : public Wind { public: void play(note) const { cout << "Woodwind::play" << endl; }

521

char* what() const { return "Woodwind"; }

// Funkcja taka sama. jak poprzednio: void tune(Instrument& i) {


// . . . i .play(middleC):

// Nowa funkcja:

void f(Instrument& i) { i.adjust(l); } int main() { Wind flute; Percussion drum; Stringed violin; Brass flugelhorn; Woodwind recorder; tune(flute); tune(drum); tune(violin); tune(flugelhorn); tune(recorder); f(flugelhorn); } II/:Funkcje czysto wirtualne sprzydatne, poniewa deklarujonejawnie abstrakcyjno klasy, informujc zarwno uytkownika, jak i kompilator o tym, w jakim celu zostay utworzone. Warto zauway, e czysto wirtualne funkcje uniemoliwiaj przekazywanie przez warto argumentw typw abstrakcyjnych klas. Jesl to rwnie sposb na uniknicie okrajania obiektw (krtko opisanego w dalszej czci rozdziau). Dziki uczynieniu klasy abstrakcyjn moesz mie pewno, e w trakcie rzutowania w gr na t klas jest zawsze uywany wskanik lub referencja. Wprawdzie jedna czysto wirtualna funkcja powoduje, e tablica VTABLE jest niekompletna, lecz wcale to nie oznacza, e nie moemy zdefiniowa cia niektrych pozostaych funkcji. Czsto bdziemy chcieli wywoa wersj funkcji, zawart w klasie podstawowej, nawetjeli jest ona funkcj wirtualn. Dobrym pomysem jest zawsze umieszczanie wsplnego kodu moliwie jak najbliej klasy gwnej w hierarchii klas. Nie tylko zapewnia to oszczdno miejsca, ale rwnie uatwia propagacj wprowadzanych zmian.

122

Thinking in C++. Edycja polska

2zysto wirtualne definicje


Moliwe jest utworzenie w klasie podstawowej definicji czysto wirtualnej funkcji. Kompilator nadal nie pozwoli na powstanie obiektw takiej abstrakcyjnej klasy podstawowej, a czysto wirtualne funkcje musz zosta zdefiniowane w klasach pochodnych, aby mona byo tworzy ich obiekty. Jednak funkcja taka moe zawiera wsplny fragment kodu, wywoywany przez niektre lub wszystkie definicje klas pochodnych, dziki czemu nie trzeba bdzie powiela go w kadej funkcji. Poniej zamieszczono przykad zawierajcy definicje czysto wirtualnych funkcji: / / : C15 : PureVi rtualDefinitions . cpp // Definicje czysto wirtualnych funkcji // klasy podstawowej #include <iostream> using namespace std; class Pet { public: virtual void speak() const = 0; virtual void eat() const = 0; // Czysto wirtualna funkcja nie moe by funkcj inline: / / ! virtual void sleep() const = 0 {} // W porzdku, funkcja nie jest zdefiniowana jako funkcja inline void Pet: :eat() const { cout << "Pet::eat()" << endl; void Pet: :speak() const { cout << "Pet::speak()" << endl:

class Oog : public Pet { public:

// Wykorzystanie wsplnego kodu klasy Pet: void speak() const { Pet::speak(); } void eat() const { Pet::eat(); }

int main() { Dog simba; // Pies Richarda simba.speak(); simba.eat();


Pozycje speak i eat tablicy VTABLE klasy Pet pozostaj nadal niezapenione, ale istniej funkcje o takich nazwach, ktre mona wywoa w klasach pochodnych. Inna korzy, wynikajca z moliwoci definicji funkcji czysto wirtualnych, polega na tym, e pozwala ona na przeksztacenie funkcji wirtualnej w funkcj czysto wirtualn, bez koniecznoci zmiany istniejcego kodu ^jest to rwnie sposb na znalezienie klas, ktre nie zasaniajtej funkcji).

Rozdzia 15. Polimorfizm i funkcje wirtualne

523

Dziedziczenie i tablica VTABLE


Mona sobie wyobrazi, co nastpi w przypadku, gdy tworzy si klas pochodn i zasania niektre z wirtualnych funkcji. Dla nowo powstaej klasy kompilator tworzy tablic VTABLE i umieszcza w niej adresy nowych funkcji, przepisujc do niej rwnie adresy tych wszystkich wirtualnych funkcji klasy podstawowej, ktre nie zostay zasonite. Tak czy inaczej, kady obiekt, ktry mona utworzy (czyli obiekt klasy nie zawierajcej funkcji czysto wirtualnych), ma do dyspozycji peny zestaw adresw funkcji, zawarty w tablicy VTABLE. Dziki temu nigdy nie moe wystpi sytuacja, polegajca na wywoaniu funkcji, ktrej adresu nie ma w tablicy (co byoby katastrofalne). Co jednak dzieje si w przypadku, gdy uywamy dziedziczenia, a nastpnie dodajemy do klasypochodnej nowe funkcje wirtualne? Oto prosty przykad: / / : C15:AddingVirtuals.cpp // Dodawanie funkcji wirtualnych podczas dziedziczenia #include <iostream> #include <string> using namespace std; class Pet { string pname; public: Pet(const string& petName) : pname(petName) {} virtual string name() const { return pname; } virtual string speak() const { return ""; } }: class Dog : public Pet { string name; public: Dog(const string& petName) : Pet(petName) {} // Nowa wirtualna funkcja w klasie Dog: virtual string sit() const { return Pet::name() + " siedzi": } string speak() const { // Zasonicie return Pet::name() + " mowi ' H a u ! ' " ;

int main() { Pet* p[] = {new Pet("zwyczajny"),new Dog("bob")}: cout << "p[0]->speak() - " << p[0]->speak() << endl: cout << "p[l]->speak() = " << p[l]->speak() << endl; / / ! cout << "p[l]->sit() = " ll\ << p[l]->sit() << endl; // Niedozwolone } II/-.Klasa Pet zawiera dwie funkcje wirtualne speak() i naine(). Klasa Dog dodaje trzeci wirtualn funkcj sit(), a take zasania znaczenie funkcji speak(). W zrozumieniu tego, co si w tym przypadku dzieje, pomocny bdzie diagram. Poniej przedstawiono tablice VTABLE, utworzone przez kompilator dla klas Pet i Dog:

524

Thinking in C++. Edycja polska

Warto podkreli, e kompilator umieszcza adres funkcji speak() zarwno w tablicy VTABLE klasy Dog, jak i w tablicy VTABLE klasy Pet na tej samej pozycji. Podobnie by si stao w przypadku, gdyby z klasy Dog zostaa wyprowadzona klasa Pug jej wersja funkcji sit() zostaaby umieszczona w tablicy VTABLE tej klasy na identycznej pozycji, na ktrej znajduje si ona w tablicy VTABLE klasy Dog. Dzieje si tak dlatego, e kompilator generuje kod, ktry do wyboru wirtualnej funkcji uywa prostego, liczbowego przesunicia wzgldem pocztku tablicy VTABLE (co mona byo zobaczy w przykadzie, prezentujcym kod w jzyku asemblera). Niezalenie od konkretnego podtypu, do ktrego naley Obiekt, jego tablica VTABLE jest wypeniona zawsze w ten sam sposb, dziki czemu wywoywanie wirtualnych funkcji odbywa si zawsze identycznie. Jednak w tym przypadku kompilator ma do czynienia wycznie ze wskanikiem do obiektu klasy podstawowej. Klasa ta posiada tylko funkcje speak() i name( ), wic sonejedynymi funkcjami, na ktrych wywoanie pozwoli kompilator. Skd zreszt miaby wiedzie, ejest to obiekt klasy Dog, skoro posiada tylko wskanik do obiektu klasy podstawowej? Rwnie dobrze wskanik ten moe wskazywa obiekt jakiego innego typu, nie posiadajcego funkcji sit(). Obiekt ten moe, ale nie musi, posiada na tej pozycji swojej tablicy VTABLE adres jakiej innej funkcji ale w adnym z tych przypadkw nie chodzio nam o wywoanie wirtualnej funkcji, ktrej adres znajduje si w tej tablicy. Kompilator nie pozwala wic na wywoywanie wirtualnych funkcji, ktre istniej wycznie w klasach pochodnych. Zdarzaj si jeszcze mniej typowe przypadki, w ktrych wiemy, e wskanik w rzeczywistoci wskazuje obiekt jakiej konkretnej podklasy. Jeeli chcemy wywoa funkcj, ktra istnieje wycznie w danej klasie pochodnej, musimy zastosowa rzutowanie wskanika. Moemy unikn komunikatu o bdzie, generowanego w przypadku poprzedniego programu, zapisujc to w nastpujcy sposb:

((Dog*)p[l])->sit()
W tym przypadku mona stwierdzi, e wskanik p[l] wskazuje obiekt klasy Dog, ale na og nie posiadamy takiej wiedzy. Jeeli problem jest okrelony w taki sposb, e musimy zna dokadnie typy wszystkich obiektw, powinnimy go przemyle, poniewa oznacza to, e prawdopodobnie nie wykorzystujemy poprawnie funkcji wirtualnych. Istniejjednak takie sytuacje, w ktrych projekt dziaa najlepiej (albo nie ma innej moliwoci), gdy znane s typy wszystkich obiektw, przechowywanych w kontenerze oglnego przeznaczenia. Jest to problem identyfikacji typw podczas pracy programu (ang. run-time type identification RTTI). Identyfikacja typw podczas pracy programu dotyczy rzutowania wskanikw klasy podstawowej w d na wskaniki klasy pochodnej (pojcia ,,w gr" i w d" odnosz si do typowego diagramu klas, w ktrym klasa podstawowa znajduje si na

Rozdzia 15. Polimorfizm i funkcje wirtualne

525

grze). Rzutowanie w gr odbywa si automatycznie, bez koniecznoci wymuszania, poniewa jest ono zupenie bezpieczne. Rzutowanie w dl nie jest bezpieczne, gdy podczas kompilacji nie jest dostpna informacja, dotyczca rzeczywistych typw rzutowanych obiektw. Rzutowanie to wymaga wic precyzyjnej wiedzy, dotyczcej typu obiektu. Jeeli dokona si rzutowania na niewaciwy typ, spowoduje to problemy. Identyfikacja typw podczas pracy programu zostaa opisana w dalszej czci rozdziau. W drugim tomie ksiki znajduje si rwnie rozdzia, powicony temu zagadnieniu.

Okrajanie obiektw
Gdy wykorzystywany jest polimorfizm, wystpuje wyrana rnica pomidzy przekazywaniem adresu obiektw a przekazywaniem ich przez warto. We wszystkich przedstawionych do tej pory przykadach (a take w tych, ktre zostan jeszcze przedstawione) przekazywane s adresy, a nie obiekty. Zrobiono tak z uwagi na to, e 5 wszystkie adresy maj identyczn wielko , wic przekazywanie adresu obiektu klasy pochodnej (ktry jest zazwyczaj wikszym obiektem) odbywa si tak samo, jak przekazywanie adresu obiektu klasy podstawowej (ktry jest na og mniejszym obiektem). Jak ju wspomniano, jest to wanie celem uywania polimorfizmu kod, ktry operuje na obiektach typu podstawowego, moe w przezroczysty sposb operowa rwnie na obiektach typw pochodnych. Jeeli dokona si rzutowania w gr na obiekt, a nie na wskanik czy referencj, zdarzy si co zdumiewajcego obiekt zostanie okrojony" w taki sposb, aby to, co z niego zostanie, stanowio podobiekt, odpowiadajcy typowi, do ktrego dokonywane byo rzutowanie. Poniszy przykad pokazuje, co si dzieje podczas okrajania obiektu:

//: C15:ObjectSlicing.cpp #include <iostream> #include <string> using namespace std; class Pet { string pname; public: Pet(const string& name) : pname(name) {} virtual string name() const { return pname; } virtual string description() const { return "To jest "+ pname;

class Oog : public Pet {

string favoriteActivity; public: Oog(const string& name. const string& activity) : Pet(name), favoriteActivity(activity) {} W rzeczywistoci, nie dla wszystkich komputerw, poszczeglne wskaniki maj takie same wielkoci. W kontekcie przedstawionej dyskusji, monajejednak traktowa w taki sposob,jakby byiy takie same.

Thinking in C++. Edycja polska string description() const { return Pet::name() + " lubi favoriteActivity;

void describe(Pet p) { // Funkcja okraja obiekt cout << p.description() << endl; int main() { Pet p("Alfred"); Dog d("Puszek". "spac"); describeCp); describe(d) ; Obiekt typu Pet jest przekazywany przez warto funkcji describe(). Funkcja ta wywouje nastpnie dla obiektu klasy Pet wirtualn funkcjdescription(). Mona si byo spodziewa, e pierwsze wywoanie tej funkcji, w funkcji main(), spowoduje wyprowadzenie tekstu: To jest Alfred", a drugie ,Puszek lubi spa". W rzeczywistoci, oba wywoania uyjwersji funkcji description(), znajdujcej si w klasie podstawowej. W trakcie dziaania programu zachodz dwa procesy. Po pierwsze, poniewa funkcja describe() pobiera obiekt (a nie wskanik czy referencj), wszelkie wywoania tej funkcji powoduj umieszczenie na stosie obiektu wielkoci obiektu klasy Pet, a nastpnie, po powrocie z wywoanej funkcji, usunicie go ze stosu. Oznacza to, ejeeli funkcji describe() zostanie przekazany obiekt klasy pochodnej klasy Pet, kompilator pozwoli na to, ale skopiuje jedynie t cz obiektu, ktr stanowi obiekt klasy Pet. Powoduje to okrojenie czci obiektu klasy pochodnej,jak przedstawia poniszy rysunek:

Zdziwienie moe wywoa rwnie wywoanie funkcji wirtualnej. Funkcja Dog:: description( ) wykorzystuje zarwno cz obiektu, dziedziczon po klasie Pet (cz ta nadal istnieje), jak i dodan w klasie Dog, ktraju nie istnieje, gdy zostaa odkrojona". Co w takim razie dzieje si podczas wywoania funkcji wirtualnej? Obyo si bez katastrofy, poniewa obiekt zosta przekazany przez warto. Kompilator zna dokadnie typ obiektu, poniewa wymuszone zostao przeksztacenie obiektu klasy pochodnej w obiekt klasy podstawowej. Podczas przekazywania przez warto zosta uyty konstruktor kopiujcy klasy Pet. Zainicjowa on wskanik VPTR adresem tablicy VTABLE klasy Pet, a nastpnie skopiowa jedynie t cz obiektu, ktra zostaa odziedziczona z klasy Pet. Klasa Pet nie posiada konstruktora kopiujcego, wic zosta on wygenerowany przez kompilator. Niezalenie od pierwotnego typu, na skutek okrojenia obiekt naprawd staje si obiektem typu Pet.

!"

Rozdzia 15. Polimorfizm i funkcje wirtualne

527

Okrajanie obiektu, ktre odbywa si podczas jego kopiowania do nowo utworzonego obiektu, w rzeczywistoci powoduje utrat jego czci, zamiast zmienia po prostu znaczenie adresu, jak w przypadku wskanikw lub referencji. Z tego powodu nie uywa si zbyt czsto rzutowania w gr na obiekt w rzeczywistoci, jest to jedna z tych sytuacji, na ktre trzeba zwraca uwag i zapobiega ich wystpieniu. Warto podkreli, e gdyby w powyszym przykadzie funkcja description() bya w klasie podstawowej funkcj czysto wirtualn (co nie jest pozbawione logiki, gdy tak naprawd nie wykonuje ona w klasie podstawowej adnych dziaa), to kompilator mgby zapobiec okrajaniu obiektw, poniewa nie pozwoliby na utworzenie" obiektu typu podstawowego (co ma miejsce w trakcie rzutowania w gr przez warto). Jest to by moe najwaniejsza zaleta funkcji czysto wirtualnych uniemoliwienie okrajania obiektw dziki generowaniu bdw w trakcie kompilacji.

Przecianie i zasanianie
Jak wiadomo na podstawie lektury rozdziau 14., przedefiniowanie funkcji przecionej w klasie podstawowej powoduje ukrycie wszystkich pozostaych wersji tej funkcji, znajdujcych si w klasie podstawowej. Gdy dotyczy to funkcji wirtualnych, sytuacja wyglda nieco inaczej. Przyjrzyjmy si zmodyfikowanej wersji programu NameHiding.cpp, pochodzcego z rozdziau 14.: / / : C15:NameHiding2.cpp // Funkcje wirtualne ograniczaj przeciganie #include <iostream> #include <string> using namespace std; class Base { public: virtual int f() const { cout << "Base::f()\n"; return 1; virtual void f(string) const {} virtual void g() const {} class Derivedl : public Base public: void g() const {} class Derived2 : public Base { public: // Przecienie funkcji wirtualnej: int f() const { cout << "Derived2::f()\n"; return 2;

Thinking in C++. Edycja polska class Derived3 : public Base {


public:

// N1e mona zmienia typu zwracanej wartoci: //! void f() const{ cout << "Derived3::f()\n";}

class Derived4 : public Base { public: // Zmiana listy argumentw: int f(int) const { cout << "Derived4::f()\n"; return 4;

int main() { string s("czesc"); Derivedl dl; int x = dl.f(): dl.f(s): Derived2 d2; x = d2.f(): / / ! d2.f(s); // Wersja dla acuchw zostaa ukryta Derived4 d4; x = d4.f(l); / / ! x = d4.f(); // Wersja f() zostaa ukryta / / ! d4.f(s); // Wersja dla acuchw zostaa ukryta Base& br = d4; // Rzutowanie w gore //! br.f(l); // Wersja z klasy pochodnej nie jest dostpna br.f(): // Dostpna wersja z klasy podstawowej br.f(s); // Dostpna wersja z klasy podstawowej } IIIPierwsz kwesti godn odnotowania jest to, e w klasie Derived3 kompilator nie dopuszcza zmiany typu wartoci, zwracanej przez zasonit funkcj (pozwoliby na to, gdyby funkcja f( ) nie bya funkcj wirtualn). Jest to istotne ograniczenie, poniewa kompilator musi zagwarantowa moliwo polimorficznego wywoania funkcji za porednictwem klasy podstawowej. Gdyby za klasa podstawowa oczekiwaa zwrcenia przez funkcj f( ) wartoci typu int, wersja funkcji f( ), zawarta w klasie pochodnej, musiaaby utrzyma to w mocy, gdy w przeciwnym razie nastpiaby katastrofa. Zasada przedstawiona w rozdziale 14. nadal obowizuje jeeli zostanie zasonita jedna z przecionych funkcji skadowych klasy podstawowej, to ich pozostae przecione wersje stan si niewidoczne w klasie pochodnej. Kod testujcy klas Derived4, zawarty w funkcji main( ), pokazuje, co dzieje si nawet w przypadku, gdy nowa wersja funkcji f( ) nie zasania interfejsu istniejcej funkcji wirtualnej obie, znajdujce si w klasie podstawowej, wersje funkcji f( ) s zakrywane przez funkcj f(int). Jeeli jednak dokona si rzutowania w gr obiektu d4 na klas Base, to dostpne s wwczas jedynie wersje funkcji zawarte w klasie podstawowej (poniewa ich obecnojest gwarantowana przez interfejs klasy podstawowej). Z kolei jej wersje zawarte w klasie pochodnej nie bd dostpne (poniewa nie zostay one wyspecyfikowane w interfejsie klasy podstawowej).

Rozdzia 15. Polimorfizm i funkcje wirtualne

529

Zmiana typu zwracanej wartoci


Przedstawiona powyej klasa Derived3 sugeruje, e podczas zasaniania nie mona zmieni typu wartoci zwracanej przez funkcj. Na og jest to zgodne z prawd, lecz istnieje szczeglny przypadek, w ktrym mona w pewnym stopniu zmodyfikowa typ zwracanej wartoci. Jeeli funkcja zwraca wskanik lub referencj do obiektu klasy podstawowej, to przedefiniowana wersja funkcji moe zwraca wskanik lub referencjjej kIasy pochodnej. Ilustruje to poniszy przykad: / / : C15:VariantRetjrn.cpp // Zwracanie wskanika lub referencji // do typu pochodnego w czasie zasaniania #include <iostream> #include <string> using namespace std; class PetFood { public: virtual string foodType() const = 0: class Pet { public: virtual string type() const = 0; virtual PetFood* eats() = 0: class Bird : public Pet { public: string type() const { return "Ptak"; class BirdFood : public'PetFood { public: string foodType() const { return "nasiona": // Rzutowanie w gr do typu podstawowego: PetFood* eats() { return &bf; } private: BirdFood bf; class Cat : public Pet { public: string type() const { return "Kot": } class CatFood : public PetFood { public: string foodType() const { return "ptaki' // Zwracany jest dokadny typ: CatFood* eats() { return &cf; } private: CatFood cf;

}:

530 int main() { Bird b; Cat c; Pet* p[] = { &b. &c. }; for(int i = 0; i < sizeof p / sizeof *p; i++) cout << p[i]->type() << " je " << p[i]->eats(>->foodType() << endl;

Thinking in C++. Edycja polska

// Funkcja moe zwrci dokadny typ: Cat::CatFood* cf - c.eats(): Bird: :BirdFood* bf; // Funkcja nie moe zwrci dokadnego typu: //' bf = b.eats(); // Trzeba rzutowa w d: bf = dynamic_cast<Bird::BirdFood*>(b.eatsO); } ///:Funkcja skadowa Pet::eats() zwraca wskanik do obiektu klasy PetFood. W klasie Bird ta funkcja skadowajest przeciona dokadnie tak, jak w klasie podstawowej, wcznie z typem zwracanej wartoci. Oznacza to, e funkcja Bird::eats() dokonuje rzutowania w gr klasy BirdFoot na klas PetFood. Jednake w klasie Cat typem wartoci zwracanej przez funkcj eats() jest wskanik do obiektu klasy CatFood, bdcej klas wyprowadzon z klasy PetFood. Fakt, e zwracany typ jest dziedziczony po typie zwracanym przez funkcj klasy podstawowej,jestjedynym powodem, dla ktrego program kompiluje si poprawnie. Warunek jest zatem wci speniony funkcja eats() zawsze zwraca wskanik do obiektu klasy PetFood. Z punktu widzenia polimorfizmu nie wydaje si to konieczne. Dlaczego nie rzutowa po prostu w gr wszystkich zwracanych typw na typ PetFood*, tak jak funkcja Bird::eats()? Jest to na og dobre rozwizanie, ale rnicajest widoczna pod koniec funkcji main( ) funkcja Cat::eats( ) potrafi zwrci dokadny typ, podczas gdy warto zwracana przez funkcj Bird::eats( ) musi by rzutowana w d, na odpowiedni typ. Tak wic moliwo zwrcenia dokadnego typu ma troch bardziej oglny charakter i nie wie si z utrat informacji o konkretnym typie, powodowan przez automatyczne rzutowanie w gr. Jednak zwracanie typu podstawowego zwykle rozwizuje problemy, wic jest to nieco bardziej wyspecjalizowana waciwo.

Funkcje wirtualne a konstruktory


Kiedy tworzonyjest obiekt zawierajcy funkcje wirtualne, jego wskanik VPTR musi zosta zainicjowany za pomoc adresu waciwej tablicy VTABLE. Musi to zosta wykonane, zanim zaistnieje jakakolwiek moliwo wywoania wirtualnej funkcji. Jak mona si tego spodziewa, poniewa to konstruktor wykonuje prac polegajc na powoaniu obiektu do ycia, jego zadaniem jest rwnie ustawienie wartoci wskanika VPTR. Kompilator potajemnie umieszcza na pocztku konstruktora kod, inicjalizujcy wskanik VPTR. Poza tym, zgodnie z tym, co napisano w rozdziale l4.,

Rozdzia 15. Polimorfizm i funkcje wirtualne

531

jeeli dlajakiej klasy nie zostaniejawnie utworzony konstruktor, to wygeneruje go kompilator. Jeeli klasa zawiera wirtualne funkcje, to wygenerowany konstruktor bdzie zawiera odpowiedni kod, inicjujcy wskanik VPTR. Wynika z tego szereg konsekwencji. Pierwsza dotyczy efektywnoci. Powodem udostpnienia funkcji inline byo umoliwienie zmniejszenia narzutu, zwizanego z wywoaniem maych funkcji. Gdyby jzyk C++ nie zawiera funkcji inline, to do tworzenia tych makr" byby wykorzystywany preprocesor. Jednake preprocesor nie wie nic na temat dostpu do klas i w zwizku z tym nie mona by go uy do utworzenia makroinstrukcji, bdcych funkcjami skadowymi. Ponadto w przypadku konstruktorw, ktre musiayby posiada ukryty kod wstawiony przez kompilator, makroinstrukcje w ogle by nie dziaay. Kiedy poluje" si na zawarte w programie luki, zwizane z efektywnoci, trzeba wiedzie, e kompilator wstawia do funkcji konstruktora niewidoczny kod. Musi on nie tylko zainicjowa wskanik VPTR, ale rwnie sprawdzi, jaka jest warto wskanika this (w przypadku gdyby operator new zwrci warto zerow), oraz wywoa konstruktory klas podstawowych. Wszystko to wydaje si niezgodne z wyobraeniem, e wywoanie konstruktora polega tylko na wywoaniu niewielkiej funkcji, do ktrej mona wstawi w kod. W szczeglnym przypadku wielko konstruktora moe zniweczy oszczdnoci, zwizane z unikniciem wywoania funkcji. Jeeli uywa si wielu wywoa konstruktorw wstawionych do kodu, moe on znacznie si rozrosn; nie przyniesie to korzyci, zwizanych z poprawjego szybkoci. Oczywicie, nie oznacza to, e naley od razu uczyni wszystkie niewielkie konstruktory funkcjami, ktre nie s wstawiane do kodu poniewa znacznie atwiej jest napisajejako funkcje inline. Lecz poszukujc sposobw przyspieszenia programu, warto rozway rezygnacj z wstawiania konstruktorw do kodu.

Kolejno wywotywania konstruktorw


Kolejn ciekaw kwesti, zwizan z konstruktorami i funkcjami wirtualnymi, jest kolejno wywoywania konstruktorw oraz sposb, w jaki wirtualne wywoania realizowane s w obrbie konstruktorw. Wszystkie konstruktory klas podstawowych s zawsze wywoywane wewntrz konstruktorw klas pochodnych. Jesl to logiczne, poniewa konstruktory maj specjalne zadanie nadzoruj poprawne tworzenie obiektw. Konstruktor klasy pochodnej ma dostp jedynie do wasnych skadowych, a nie do skadowych zawartych w klasie podstawowej. Jedynie konstruktor klasy podstawowej moe poprawnie zainicjowa swoje elementy. Dlatego wanie tak wanejest wywoanie wszystkich konstruktorw - w przeciwnym razie cay obiekt nie byby bowiem utworzony poprawnie. Z tego wanie powodu kompilator wymusza wywoanie konstruktora dla kadego elementu zawartego w klasie pochodnej. Jeeli konstruktor nie zostaniejawnie wymieniony na licie inicjatorw konstruktora, to kompilator wywoa konstruktor domylny. Jeeli domylnego konstruktora nie bdzie, kompilator zgosi bd.

532

Thinking in C++. Edycja polska

Kolejno wywoania konstruktorw jest istotna. Podczas dziedziczenia wiadomo wszystko na temat klasy podstawowej; jest rwnie dostp do jej publicznych oraz chronionych skadowych. Oznacza to, e bdc w klasie pochodnej mamy prawo zaoy, e wszystkie skadowe klasy podstawowej s prawidowe. W chwili wywoania normalnej funkcji skadowej konstrukcja jest ju wykonana wczeniej, wic zostay ju utworzone wszystkie skadowe wszystkich czci obiektu. Jednake wewntrz konstruktora musimy mie moliwo zaoenia, e zostay ju utworzone wszystkie wykorzystywane skadowe. Jedynym sposobem zagwarantowania tego jest uprzednie wywoanie konstruktora klasy podstawowej. Dziki temu, gdy znajdujemy si w konstruktorze klasy pochodnej, wszystkie skadowe klasy podstawowej, do ktrych moemy mie dostp, sju zainicjowane. Po to, by wewntrz konstruktora wiedzie, e wszystkie skadowe s poprawne, naley zawsze, gdy jest to moliwe inicjalizowa wszystkie obiekty skadowe (to znaczy obiekty umieszczone wewntrz klasy za pomoc metody kompozycji) na licie inicjatorw konstruktora. Stosujc t praktyk, mona zaoy, e zostay zainicjowane wszystkie skadowe klasy podstawowej oraz wszystkie obiekty skadowe biecego obiektu.

Wywoywanie funkcji wirtualnych wewntrz konstruktorw


Hierarchia wywoa konstruktorw powoduje powstanie interesujcego pytania. Co si stanie, gdy wewntrz konstruktora zostanie wywoana funkcja wirtualna? Mona sobie wyobrazi, co dzieje si w przypadku zwyczajnej funkcji skadowej wywoanie wirtualnejest rozstrzygane w trakcie pracy programu, poniewa obiekt nie moe wiedzie, czy naley do klasy, wewntrz ktrej znajduje si wykonywana wanie funkcja skadowa, czy te do jakiej jej klasy pochodnej. Z uwagi na spjno moe si wydawa, e tak samo dzieje si w przypadku konstruktorw. Jestjednak inaczej. W przypadku wywoania funkcji wirtualnej wewntrz konstruktora wykorzystywana jest jedynie jej lokalna wersja. Oznacza to, e mechanizm wywoywania funkcji wirtualnych nie dziaa w przypadku konstruktorw. Zachowanie takie jest logiczne z dwch powodw. Pod wzgldem pojciowym, zadaniem konstruktora jest powoanie obiektu do istnienia (co wcale nie jest proste). Wewntrz kadego konstruktora obiekt moe by utworzony jedynie czciowo wiadomo tylko, e zainicjowane zostay obiekty klasy podstawowej, aIe nieznane s utworzone z niej klasy pochodne. Wywoanie wirtualnej funkcji sigajednak ,,w gb" lub na zewntrz" hierarchii klas. Powoduje wywoanie funkcji w klasie pochodnej. Gdyby mona byo zrobi to wewntrz konstruktora, spowodowaoby to wywoanie funkcji, ktra miaaby dostp do niezainicjowanych skadowych, co niezawodnie doprowadzioby do katastrofy. Drugi powd ma charakter techniczny. Gdy wywoywany jest konstruktor, to jedn z pierwszych jego czynnoci jest inicjalizacja wskanika VPTR. Jednake moe on jedynie wiedzie, e jest biecego" typu tego, dla ktrego napisano konstruktor. Kod zawarty w konstruktorze nie ma zupenie pojcia o tym, czy obiekt jest klasy podstawowej, czy tejakiej innej. Gdy kompilator generuje kod tego konstruktora.

Rozdzia 15. Polimorfizm i funkcje wirtualne

533

tworzy kod dla konstruktora wanie tej klasy, a nie klasy podstawowej lub klasy pochodnej (poniewa klasa nie moe wiedzie o tym, jaka klasa po niej dziedziczy). Tak wic wskanik VPTR, ktrego uywa, musi wskazywa tablic VTABLE tej klasy. Wskanik VPTR pozostaje zainicjalizowany adresem tej tablicy do koca ycia obiektu, chyba e nie jest to ostatnie wywotanie konstruktora. Jeeli pniej nastpi wywoanie konstruktora klasy, usytuowanej niej w hierarchii dziedziczenia, konstruktor ten przypisze wskanikowi VPTR adres swojej tablicy VTABLE i tak dalej, a ostatni z konstruktorw skoczy swoje dziaanie. Jest to dodatkowy powd, dla ktrego konstruktory s wywoywane w kolejnoci od klasy znajdujcej si na kocu hierarchii dziedziczenia. Lecz podczas tej serii wywoa konstruktorw kady z nich przypisuje wskanikowi VPTR adres swojej tablicy VTABLE. Jeeli konstruktor wywoywania funkcji wykorzystuje mechanizmy funkcji wirtualnych, to wywoania te zostan zrealizowane za porednictwem jego tablicy VTABLE, a nie tablicy VTABLE, znajdujcej si na kocu hierarchii ^ak po wywoaniu wszystkich konstruktorw). Ponadto wiele kompilatorw rozpoznaje, e wewntrz konstruktora wywoywana jest funkcja wirtualna, dokonujc wczesnego wizania. Pne wizanie spowodowaoby bowiem jedynie wywoanie funkcji lokalnej. W adnym z wymienionych przypadkw uzyskane rezultaty nie bd zgodne z tym, czego mona by si pierwotnie spodziewa po wywoaniu wirtualnej funkcji wewntrz konstruktora.

Destruktory i wirtualne destruktory


W stosunku do konstruktorw nie wolno uywa sowa kluczowego virtual, ale destruktory mog, a czsto nawet musz, by wirtualne. Konstruktor ma za zadanie zbudowanie obiektu, fragment po fragmencie, wywoujc najpierw konstruktory klasy podstawowej, a nastpnie konstruktory klas pochodnych, w kolejnoci dziedziczenia (przy okazji musi rwnie wywoa konstruktory obiektw skadowych). Szczeglne zadanie ma przed sob rwnie destruktor musi rozmontowa" obiekt, ktry by moe naley do hierarchii klas. Aby to wykona, kompilator generuje kod, wywoujcy wszystkie destruktory, ale w kolejnoci odwrotnej ni podczas konstrukcji. Oznacza to, e destruktor zaczyna od klasy znajdujcej si na kocu hierarchii i przechodzi przez kolejne poziomy, a do osignicia poziomu klasy podstawowej. Dziaanie takie jest zarwno bezpieczne, jak i podane, poniewa dziki temu biecy destruktor zawsze wie, e skadowe klasy nadrzdnej istniej i dziaaj. Jeeli wewntrz destruktora zachodzi potrzeba wywoania funkcji klasy podstawowej, rwnie mona tego bezpiecznie dokona. Tak wic destruktor moe przeprowadzi swoje porzdki, a nastpnie wywoa destruktor wyszego poziomu, ktry z kolei przeprowadzi swoje porzdki itd. Kady destruktor zna klas, zktorejzosta\a wyprowadzonajego klasa, aIe nie wie,jakie sjej klasy pochodne. Naley pamita, e konstruktory i destruktory sjedynymi miejscami, w ktrych musi wystpi taka hierarchia wywoia (i dlatego jest ona automatycznie generowana przez kompilator). W przypadku wszystkich pozostaych funkcji tylko one zostan wywoane

Thinking in C++. Edycja polska (a nie ich wersje, zawarte w klasie podstawowej), niezalenie od tego, czy s wirtualne, czy te nie. Wersja tej samej funkcji, zawarta w klasie podstawowej (niezalenie od tego, czyjest ona wirtualna, czy te nie), moe by wywoanajedynieJcwnie. Zazwyczaj dziaanie destruktorwjest zupenie prawidowe. Cojednak dzieje si, gdy zamierzamy operowa obiektem za porednictwem wskanika do jego klasy podstawowej (czyli za porednictwem jego standardowego interfejsu)? Dziaanie takie jest gwnym celem programowania obiektowego. Problem pojawia si, gdy zamierzamy usun obiekt wskazywany przez wskanik tego typu, utworzony uprzednio na stercie za pomoc polecenia new. Jeeli wskanik jest typu wskanika do klasy podstawowej, to kompilator wie tylko, w jaki sposb podczas realizacji instrukcji delete wywoa wersj destruktora, zawart w klasie podstawowej. Czy nie wyglda to znajomo? To ten sam problem, do ktrego rozwizania zostay utworzone funkcje wirtualne. Na szczcie, funkcje wirtualne dziaaj w przypadku destruktorw tak samo,jak w dla wszystkich innych funkcji z wyjtkiem konstruktorw. / / : C15:VirtualDestructors.cpp // Zachowanie wirtualnych i niewirtualnych destruktorw #include <iostream> using namespace std; class Basel { public: -Basel() { cout << "-Basel()\n"; } class Derivedl : public Basel { public: -Derivedl() { cout << "~DerivedH)\n"; } class Base2 { public: virtual ~Base2() { cout << "~Base2()\n" class Derived2 : public Base2 { public: ~Derived2() { cout << "~Derived2()\n";

int main() { Basel* bp = new Derivedl; // Rzutowanie w gr delete bp; Base2* b2p - new Derived2; // Rzutowanie w gr
} ///:delete b2p;

Po uruchomieniu programu zauwaymy, e instrukcja delete bp wywoa jedynie destruktor klasy podstawowej, natomiast instrukcja delete b2p destruktor klasy pochodnej, a nastpnie destruktor klasy podstawowej, cojest zachowaniem podanym. Gdyby zapomnie o uczynieniu destruktora wirtualnym, stanowioby to niebezpieczny bd. Co prawda na og nie wpywaoby to bezporednio na dziaanie programu, ale powodowaoby niezauwaalne wycieki pamici. Ponadto fakt, ejaka destrukcja jestjednak wykonywana, mgby wjeszcze wikszym stopniu ukry ten problem.

Rozdzia 15. Polimorfizm i funkcje wirtualne

535

Mimo e destruktor, podobnie jak konstruktor, jest wyjtkow" funkcj, to moe on by funkcj wirtualn, poniewa obiekt wie ju, jakiego jest typu (co nie jest prawd podczas konstrukcji). Gdy obiekt zostanie ju skonstruowany, jego wskanik VPTR jest zainicjowany, wic mogby wywoywane funkcje wirtualne.

Czysto wirtualne destruktory


Mimo e czysto wirtualne destruktory s dozwolone w standardzie C++, to zwizane jest z nimi ograniczenie funkcje czysto wirtualnych destruktorw musz zawiera kod. Brzmi to nielogicznie jak funkcja moe by czysto" wirtualna, skoro wymaga kodu? Jeeli jednak uwiadomimy sobie, e konstruktory i destruktory s szczeglnymi operacjami, nabiera to sensu zwaszcza gdy sobie przypomnimy, e zawsze wywoywane s wszystkie destruktory, w caej hierarchii klas. Gdyby mona bylo pomin definicj czysto wirtualnego destruktora, to jaka funkcja zostaaby wywoana podczas destrukcji? Istnienie kodu czysto wirtualnego destruktorajest absolutnie niezbdne kompilatorowi i programowi czcemu. Jeeli destruktorjest funkcjczysto wirtualn, ale musi zawiera kod, tojaki z niego wynika poytek? Jedyna zauwaalna rnica pomidzy czysto wirtualnymi destruktorami i zwykymi wirtualnymi destruktorami polega na tym, e czysto wirtualne destruktory powoduj, e klasa podstawowa staje si klas abstrakcyjn. Z tego powodu nie mona utworzy jej obiektu (ale miaoby to miejsce rwnie wwczas, gdy jakakolwiek inna funkcja skadowa klasy podstawowej bya funkcjczysto wirtualn). Wszystko staje si jednak nieco niezrozumiae, gdy utworzy si klas pochodn klasy posiadajcej czysto wirtualny destruktor. W odrnieniu od wszystkich innych czysto wirtualnych funkcji, w klasie pochodnej nie trzeba tworzy definicji czysto wirtualnych destruktorw. Dowodem na tojest poprawne kompilowanie si poniszego pliku: / / : C15:UnAbstract.cpp // Wydaje si, ze czysto wirtualne // destruktory zachowuj si dziwnie class AbstractBase { public: virtual ~AbstractBase() = 0; AbstractBase::-AbstractBase() {} class Derived : public AbstractBase {}; // Nie jest potrzebne zasonicie destruktora? int main() { Derived d; } ///:Zwykle czysto wirtualne funkcje, zawarte w klasie podstawowej, powinny spowodowa, e klasa pochodna bdzie klas abstrakcyjn, o ile nie zostanie w tej klasie utworzonajej definicja (a take definicje innych czysto wirtualnych funkcji). W tym przypadku wydaje si to jednak nie obowizywa. Naley jednak pamita, e kompilator automatycznie tworzy definicj destruktora dla kadej klasy, w ktrej nie zosta on

36

Thinking in C++. Edycja polska zdefiniowany. Konstruktor klasy podstawowej zostaje niejawnie zasonity, poniewa jego definicja zostaje utworzona przez kompilator, wic klasa Derived nie jest w rzeczywistoci klasabstrakcyjn. Mona zatem zada interesujce pytanie jaki jest poytek z czysto wirtualnego destruktora? W odrnieniu od zwykych funkcji czysto wirtualnych, trzeba utworzy jego kod. W klasie pochodnej nie jestemy zmuszeni do utworzenia jego definicji, poniewa kompilator generuje destruktor automatycznie. Jaka jest zatem rnica pomidzy zwykym wirtualnym destruktorem i czysto wirtualnym destruktorem? Jedyna rnica wystpuje wtedy, gdy posiadamy klas, zawierajc tylko jedn czysto wirtualna funkcj destruktor. Wwczas jedynym skutkiem czystoci destruktora jest uniemoliwienie tworzenia egzemplarzy klasy podstawowej. Gdyby istniay jakiekolwiek inne czysto wirtualne funkcje, to wanie one mogyby uniemoliwi tworzenie egzemplarzy klasy podstawowej, ale w przypadku gdy nie ma takich funkcji, rol tak przejmie czysto wirtualny destruktor. Tak wic, o ile dodanie wirtualnego destruktora jest niezbdne, to czy jest on wirtualny, czy te nie, nie ma ju wielkiego znaczenia. Po uruchomieniu poniszego programu mona zauway, e kod czysto wirtualnego destruktorajest wywoywany po wersji destruktora, zawartej w klasie pochodnej (tak jak w przypadku kadego innego destruktora):

/ / : C15 : PureVi rtualDestructors . cpp // Czysto wirtualny destruktor // wymaga zdefiniowania kodu |include <iostream> using namespace std; class Pet { public: virtual ~Pet() = 0;
Pet::~Pet() { cout << "-Pet()" << endl ;

class Oog : public Pet { public: ~Dog() { cout << "~Oog()" << endl ;

int main() { Pet* p - new Dog; // Rzutowanie w gr delete p; // Wywoanie wirtualnego destruktora } III-.-

Wskazwka: ilekro w klasie zawarta jest funkcja wirtualna, naley natychmiast doda do niej wirtualny destruktor (nawet jeeli nie wykonuje on adnych dziaa). Dziki temu ustrzeesz si niespodzianek w przyszoci.

Rozdzia 15. Polimorfizm i funkcje wirtualne

537

Wirtualne wywotenia w destruktorach


Podczas destrukcji dzieje si co, czego nie od razu mona by si spodziewa. Jeeli wewntrz zwyczajnej funkcji skadowej wywoywana jest funkcja wirtualna, to wywoanie jest realizowane za pomoc mechanizmu pnego wizania. Nie jest to jednak prawd w przypadku destruktorw, niezalenie od tego, czy s one wirtualne, czy te nie. Wewntrz destruktora wywoywana jest wycznie lokalna" wersja funkcji skadowej mechanizm funkcji wirtualnychjest ignorowany. / / : C15:VirtualsInDestructors.cpp // Wirtualne wywoania w destruktorach #include <iostream> using namespace std; class Base { public: virtual ~Base() { cout << "Basel()\n";
f();

virtual void f() { cout << "Base::f()\n"; }


}:

class Derived : public Base { public: -Derived() { cout << "~Derived()\n"; } void f() { cout << "Derived::f()\n"; }
int main() {
Base* bp = new Derived; // Rzutowanie w gr delete bp;

Podczas wywoania destruktora niejest wywoywana funkcja Derived::f( ), mimo i funkcja f( )jest funkcj wirtualn. Dlaczego tak si dzieje? Zamy, e mechanizm funkcji wirtualnych dziaaby wewntrz destruktora. W takim przypadku wywoanie wirtualnej funkcji mogoby prowadzi do funkcji znajdujcej si bardziej na zewntrz" hierarchii kas (dalej od klasy podstawowej) ni biecy destruktor. Jednak destruktory s wywoywane z zewntrz do wewntrz" (od destruktora klasy znajdujcej si najdalej klasy podstawowej do destruktora klasy podstawowej), wic wywoana aktualnie funkcja mogaby bazowa na elementach obiektu, ktry zosta ju zniszczony\ Kompilator rozstrzyga natomiast wywoania w trakcie kompilacji, wywoujc wycznie lokalne" wersje funkcji. Zwr uwag na to, e tak samo dzieje si w przypadku konstruktorw ^ak to opisano wczeniej), lecz wwczas informacja o typie nie bya dostpna. Tymczasem jeli chodzi o destruktory, informacja (czyli warto wskanika VPTR) jest dostpna, aIe niejest ona wiarygodna.

18

Thinking in C++. Edycja polska

worzenie hierarchii bazujcej na obiekcie


Kwesti, ktra przewija si w ksice od czasu prezentacji klas kontenerowych Stash oraz Stack, jest problem wasnoci". Pojcie waciciela" odnosi si do kogo lub czego, odpowiedzialnego za wywoanie instrukcji delete w stosunku do obiektu utworzonego dynamicznie (za pomoc operatora new). Problem zwizany z uywaniem kontenerw polega na tym, e musz by one dostatecznie elastyczne, by mogy przechowywa rne typy obiektw. Aby to osign, kontenery przechowuj wskaniki typu void*; nie znaj wic typw przechowywanych obiektw. Usunicie obiektu wskazywanego przez wskanik typu void* nie powoduje wywoania destruktora, a zatem kontener nie moe by odpowiedzialny za sprztanie zawartych w nim obiektw. Jedno z rozwiza zostao przedstawione w programie C14:InheritStack.cpp, w ktrym z klasy Stack wyprowadzono now klas, pobierajc i zwracajc wycznie wskaniki do acuchw. Poniewa kontener wiedzia, e moe przechowywa wycznie wskaniki do obiektw, bdcych acuchami, mg je prawidowo usun. Byo to eleganckie rozwizanie, wymagao jednak utworzenia nowej kontenerowej klasy pochodnej dla kadego typu, ktry zamierzalibymy przechowywa w kontenerze (mimo e wydaje si to teraz mao interesujce, warto wiedzie, e kontenery zaczndziaa cakiem dobrze w rozdziale 16., gdy zostan wprowadzone szablony). Problem polega na tym, e pragniemy, by kontener przechowywa obiekty wicej ni jednego typu, lecz nie chcemy uywa wskanikw typu void*. Innym rozwizaniem jest wykorzystanie polimorfizmu, przez wymuszenie, by wszystkie zawarte w kontenerze obiekty dziedziczyy po tej samej klasie podstawowej. Oznacza to, e kontener przechowuje obiekty klasy podstawowej, dziki czemu mona wywoywa w stosunku do nich funkcje wirtualne w szczeglnoci wirtualne destruktory, rozwizujce problem wasnoci. W rozwizaniu tym jest wykorzystana hierarchia o jednym korzeniu (ang singly-rooted hierarchy} lub hierarchia bazujca na obiekcie (ang. object-based hierarchy} poniewa gwna klasa hierarchii nosi na og nazw Object" (obiekt). Okazuje si, e istnieje wiele dodatkowych korzyci, wynikajcych z uywania hierarchii ojednym korzeniu w rzeczywistoci kady jzyk obiektowy, z wyjtkiem C++, wymusza wykorzystywanie takiej hierarchii gdy tworzona jest klasa, to dziedziczy ona, bezporednio lub porednio, ze wsplnej klasy podstawowej, utworzonej przez twrcw jzyka. W przypadku jzyka C++ sdzono, e wymuszenie uycia takiej wsplnej klasy podstawowej mogoby wiza si ze zbyt duym narzutem, wic z tego zrezygnowano. Jednake moemy zdecydowa si na stosowanie wsplnej klasy podstawowej w swoich projektach temat ten zosta omwiony dokadniej w drugim tomie ksiki. W celu rozwizania problemu wasnoci moemy utworzy skrajnie prost klas podstawowObject, zawierajcjedynie wirtualny destruktor. Kontener klasy Stack bdzie przechowywa obiekty klas pochodnych klasy Object:
/ / : C15:OStack.h // Wykorzystanie hierarchii o jednym korzeniu #ifndef OSTACK_H

#define OSTACK H

Rozdziat 15. Polimorfizm i funkcje wirtualne class Object { public: virtual -Object() = 0; // Wymagana definicja: inline Object::-Object() {} class Stack { struct Link { Object* data; Link* next; Link(Object* dat, Link* nxt)

539

data(dat), next(nxt) {} }* head; public:

Stack() : head(0) {} -Stack(){ while(head) delete pop(); } void push(Object* dat) { head = new Link(dat. head); Object* peek() const { return head ? head->data : 0;

Object* pop() { if(head == 0) return 0; Object* result = head->data; Link* oldHead = head; head = head->next; delete oldHead; return result; #endif // OSTACK_H / / / : Dla uproszczenia postanowiono umieci wszystko w pliku nagwkowym, w zwizku z czym (wymagana) definicja czysto wirtualnego destruktora zostaa ulokowana w pliku nagwkowymjako funkcja inline podobniejak funkcja pop( ) (co do ktrej mona si zastanawia, czy niejest zbyt dua, jak na funkcj i n l i n e ) . Obiekty klasy Link przechowuj obecnie wskaniki do obiektw klasy Object, zamiast wskanikw typu void*, a klasa Stack pobiera i zwraca wycznie wskaniki do obiektw typu Object. Obecnie klasa Stack jest bardziej elastyczna, poniewa nie tylko bdzie przechowywaa wskaniki do obiektw wielu rnych typw, ale rwnie zniszczy obiekty pozostawione w kontenerze. Nowe ograniczenie (w rozdziale 16. zniesione kiedy do rozwizania problemu zostan wykorzystane szablony) polega na tym, e typ kadego z obiektw umieszczanych w kontenerze musi by dziedziczony po klasie Object. Jest to do zaakceptowania, gdy tworzy si klas od podstaw; co jednak zrobi w sytuacji, gdy chcemy umieci w kontenerze obiekty istniejcej klasy, na przykad takiej jak string? W takim przypadku nowa klasa musi by klas pochodn zarwno klasy string, jak i Object, co oznacza, e dziedziczy

540

Thinking in C++. Edycja polska

ona po dwu klasach. Nosi to nazw wielokrotnego dziedziczenia (ang. multiple inheritance) i stanowi temat caego rozdziau drugiego tomu ksiki (ktry mona pobra z witryny: http:/faelion.pUonUne/thinking/index.html). Po przeczytaniu tego rozdziau przekonasz si, e wielokrotne dziedziczenie moe by nieraz bardzo skomplikowane i stanowi rodek, ktry naley stosowa z umiarem. Jednak w tym przykadzie wszystkojest na tyle proste, e unikniemy puapek wielokrotnego dziedziczenia: //: C15:OStackTest.cpp //{T} OStackTest.cpp #include "OStack.h" #include "../require.h" #include <fstream> #include <iostream> #include <string> using namespace std; // Wykorzystanie wielokrotnego dziedziczenia. // Chcemy, by obiekt byl zarwno klasy string. jak i Object: class MyString: public string, public Object { public: -MyString() { cout << "usuwanie lancucha: " << *this << endl: MySthng(string s) : string(s) {}

}:
int main(int argc, char* argv[]) { requireArgs(argc. 1); // Argumentem jest nazwa funkcji ifstream in(argv[l]); assure(in, argv[l]); Stack textlines; string line; // Odczytanie pliku i zapamitanie wierszy na stosie: while(getline(in, line)) textlines.push(new MyString(line)) : // Pobranie kilku wierszy ze stosu: MyString* s: for(int i = 0: i < 10: i++) { if((s=(MyString*)textlines.pop())=-0) break:

cout << *s << endl : delete s:

} cout << "Niech reszte zrobi destruktor:" << endl :


Mimo e powyszy program jest podobny do poprzedniej wersji programu testujcego klas Stack, atwo zauway, e ze stosu jest pobieranych jedynie 10 elementw, co oznacza, e prawdopodobnie cz obiektw na nim pozostaa. Poniewa kontener Stack wie, e przechowuje obiekty klasy Object, destruktor potrafi poprawnie wszystko posprzta. wiadcz o tym wyniki wyprowadzane przez program dziki temu, e obiekty klasy MyString drukuj komunikaty w chwili, gdy s one niszczone.

Rozdzia 15. Polimorfizm i funkcje wirtualne

541

Utworzenie kontenera przechowujcego obiekty klasy Object nie jest wcaIe nierozsdnym podejciem -- pod warunkiem posiadania hierarchii o jednym korzeniu (wymuszonej albo przez jzyk, albo przez wymaganie, by kada klasa bya klas pochodn klasy Object). W tym przypadku istnieje gwarancja, e wszystkie obiekty s obiektami klasy Object, dziki czemu uywanie kontenerw nie jest trudne. Jednak wjzyku C++ nie mona oczekiwa tego po kadej klasie, wic wybierajc to rozwizanie jestemy zmuszeni do przejcia przez wielokrotne dziedziczenie. Jak zobaczymy w rozdziale 16., szablony umoliwiajrozwizanie tego problemu w znacznie prostszy i bardziej elegancki sposb.

Przecianie operatorw
Operatory, podobniejak inne funkcje skadowe, mogby rwnie zdefiniowanejako wirtualne. Implementacja wirtualnych operatorw jest jednak czsto nieprzejrzysta, poniewa operacje mog by wykonywane na dwch obiektach, ktrych typy nie s znane. Zdarza si to zazwyczaj w przypadku obiektw matematycznych (dIa ktrych czsto definiowane soperatory). Przyjrzyjmy si na przykad systemowi, ktry operuje na macierzach, wektorach i wartociach skalarnych, bdcych obiektami klas wyprowadzonych z klasy Math: / / : C15:OperatorPolymorphism.cpp // Polimorfizm z przecionymi operatorami #include <iostream> using namespace std; class Matrix; class Scalar; class Vector; class Math { public: virtual Math& operator*(Math& rv) virtual Math& multiply(Matrix*) virtual Math& multiply(Scalar*) = virtual Math& multiply(Vector*) = virtual -Math() {}
}:

= 0; 0; 0; 0;

class Matrix : public Math { // Macierz public: Math& operator*(Math& rv) { return rv.multiply(this); // Orugi przydzia }
Math& multiply(Matrix*) { cout << "Macierz * Macierz" << endl; return *this.
}

Math& multiply(Scalar*) { cout << "Skalar * Macierz" << endl; return *this;

Thinking in C++. Edycja polska

Math& multiply(Vector*) { cout << "Wektor * Macierz" << endl; return *this;

class Scalar : public Math { // Skalar public: Math& operator*(Math& rv) { return rv.muHiply(this); // Drugi przydzia } Math& multiply(Matnx*) { << cout "Macierz * Skalar" << endl; return *this; Math& multiply(Scalar*) { cout << "Skalar * Skalar" << endl; return *this; }

Math& multiply(Vector*) { cout << "Wektor * Skalar" << endl; return *this;

class Vector : public Math { // Wektor public: Math& operator*(Math& rv) { return rv.multiply(this); // Drugi przydzia Math& multiply(Matrix*) { cout << "Macierz * Wektor" << endl; return *this; Math& multiply(Scalar*) { cout << "Skalar * Wektor" << endl; return *this; Math& multiply(Vector*) { cout << "Wektor * Wektor" << endl; return *this;

int main() { Matrix m; Vector v; Scalar s; Math* math[] = { &m. &v, &s }: for(int i = 0; i < 3; i++) for(1nt j = 0; j < 3; j++) { Math& ml = *math[i]; Math& m2 - *math[j]; ml * m2;

Dla uproszczenia zosta przeciony tylko operator*. Jego zadaniemjest umoliwienie mnoenia przez siebie dwch dowolnych obiektw klasy Math i zwracanie podanego wyniku naley zauway, e mnoenie macierzy przez wektor jest zupenie

Rozdzia 15. Polimorflzm i funkcje wirtualne

543

inn operacj ni mnoenie wektora przez macierz. Problem polega na tym, e wyraenie ml * m2, znajdujce si w funkcji main( ), zawiera dwa rzutowania w gr referencji do obiektw klasy Math, a zatem dwa obiekty o nieznanych typach. Funkcja wirtualnajest w stanie dokonajedynie pojedynczego przydziau to znaczy okrelenia typu nieznanego obiektu. W celu okrelenia obu typw w przykadzie zostaa wykorzystana technika, nazywana przydzielaniem wielokrotnym (ang. mulliple dispatching), dziki ktrej to, co wyglda na pojedyncze wywoanie wirtualnej funkcji, skutkujejeszcze drugim wirtualnym wywoaniem. W momencie wykonania tego drugiego wywoania okrelone sju typy obu obiektw i w zwizku z tym moliwejest podjcie odpowiedniego dziaania. Nie jest to widoczne na pierwszy rzut oka, ale jeeli przyjrze si przez chwil programowi, wszystko powinno sta si zrozumiae. Przykad ten zosta dokadniej omwiony w rozdziale powiconym wzorcom projektowym, zamieszczonym w drugim tomie ksiki, ktry mona pobra z witryny http:Melion.pl/online/thinking/index.html.

Rzutowanie w d
Zgodnie z oczekiwaniami, skoro istnieje rzutowanie w gr czyli przemieszczanie si w gr hierarchii dziedziczenia powinno te istnie rzutowanie w dl, umoliwiajce przejcie w d tej hierarchii. Jednak rzutowanie w gr jest proste, poniewa w miar poruszania si w gr hierarchii klasy zawsze cz si w coraz bardziej oglne klasy. Oznacza to, e podczas rzutowania w gr kada klasa pochodna ma zawsze wyranie okrelon klas podstawow (zazwyczaj jedn, z wyjtkiem przypadku wielokrotnego dziedziczenia), ale podczas rzutowania w d istnieje na og wybr spord wielu moliwoci. Na przykad Okrg jest typu Figura (rzutowanie w gr), aIe rzutujc w d Figura, mona otrzyma Okrg, Kwadrat, Trjkt itd6. Tak wic problem polega na okreleniu sposobu bezpiecznego rzutowania w d (by moe jeszcze waniejsz kwestijest odpowied na pytanie, po co dokonuje si rzutowania w d zamiast wykorzysta polimorfizm, umoliwiajcy automatyczne okrelenie typu; sposoby unikania rzutowania w d zostay opisane w drugim tomie ksiki). Jzyk C++ umoliwia specjalne jawne rzutowanie (przedstawione w rozdziale 3.) o nazwie dynamic_cast, bdce operacj rzutowania w dl bezpiecznego dla typw (ang. type-safe downcast). Gdy wykonuje si tak operacj, prbujc zrealizowa rzutowanie w d na okrelony typ, to zwrcona warto bdzie wskanikiem do danego typu tylko w przypadku, gdy rzutowaniejest poprawne i zakoczy si powodzeniem. W przeciwnym razie zostanie zwrcona warto zerowa, oznaczajca, e podany docelowy typ jest nieprawidowy. Poniej zamieszczono, ograniczony do minimum, przykad rzutowania w d:
//: C15:DynamicCast.cpp include <iostream> using namespace std; class Pet { public: virtual -PetO{}}:
Przykad odwouje si hierarchii typw przedstawionej na str. 33 niniejszej ksiki przyp. red.

544 class Dog : public Pet {}; class Cat : public Pet {}; int main() { Pet* b = new Cat; // Rzutowanie w gr // Prba rzutowania na wskanik typu Dog*: Dog* dl - dynamic_cast<Dog*>(b); // Prba rzutowania na wskanik typu Cat*: Cat* d2 - dynamic_cast<Cat*>(b); cout << "dl = " << (long)dl << endl; cout << "d2 = " << (long)d2 << endl; } ill:~

Thinking in C++. Edycja polska

Rzutowania dynamic_cast trzeba uywa w odniesieniu do prawdziwie polimorficznej hierarchii zawierajcej funkcje wirtualne poniewa do okrelenia rzeczywistego typu wykorzystuje ono informacje, zawarte w tablicy VTABLE. W powyszym przykadzie wystarcza w zupenoci, e klasa podstawowa posiada wirtualny destruktor. W funkcji main() wskanik do obiektu klasy Cat jest rzutowany w gr na wskanik obiektu klasy Pet, a nastpnie prbuje si go rzutowa w d zarwno na wskanik do obiektu klasy Dog, jak i na wskanik do obiektu klasy Cat. Wartoci obu wskanikw s drukowane, dziki czemu po uruchomieniu programu mona si przekona, e w wyniku nieprawidowego rzutowania w d uzyskuje si warto zerow. Oczywicie, podczas rzutowania w d trzeba si zawsze upewni, e zwrcona warto, bdcajego wynikiem, jest niezerowa. Nie naley si rwnie spodziewa, e zwracany bdzie zawsze taki sam wskanik, poniewa w trakcie rzutowania w gr i w d dokonywane jest czasami wyrwnywanie wskanikw (szczeglnie w przypadku wielokrotnego dziedziczenia). Rzutowanie dynamic_cast wymaga do dziaania pewnego narzutu nie jest on wielki, ale jeeli dokonuje si licznych rzutowa (w takim przypadku naleaoby si uwanie przyjrze projektowi programu), to kwestie zwizane z wydajnoci mog sta si problemem. W pewnych przypadkach podczas rzutowania w d moemy jednak wiedzie co szczeglnego, co sprawi, e zyskamy pewno, z jakim typem mamy do czynienia. Dziki temu rzutowanie dynamic_cast stanie si niepotrzebne i bdziemy mogli uy zamiast niego rzutowania static_cast. Poniej przedstawiono tak sytuacj: / / : C15:StaticHierarchyNavigation.cpp // Poruszanie si w hierarchii klas // za pomoc static_cast #include <iostream> #include <typeinfo> using namespace std; class class class class Shape { public: virtual -Shape() {}; }; Circle : public Shape {}; Square : public Shape {}; Other {};

int main() { Circle c; Shape* s = &c; // Rzutowanie w gore - normalne i prawidowe ' // Bardziej jawne, ale niepotrzebne: s = static_cast<Shape*>(&c):

Rozdzia 15. * Polimorf1zm i funkcje wirtualne // (poniewa rzutowanie w gr jest tak bezpieczn // i typow operacj, ze rzutowanie wprowadza baagan) Circle* cp = 0; Square* sp = 0; // Statyczne poruszanie si w hierarchii klas // wymaga dodatkowej informacji o typie: if(typeid(s) == typeid(cp)) // RTTI C++ cp = static_cast<Circle*>(s); if(typeidCs) == typeid(sp)) sp = static_cast<Square*>(s); if(cp != 0) cout << "To jest okrag!" << end1; if(sp != 0) cout << "To jest kwadrat!" << endl; // Statyczna nawigacja jest TYLKO obejciem stosowanym // z powodu efektywnoci - rzutowanie dynamic_cast // jest zawsze bezpieczniejsze. Jednak instrukcja: // Other* op = static_cast<Other*>(s); // powoduje zgoszenie bdu, podczas gdy instrukcja: Other* op2 = (Other*)s; // tego nie robi

545

W programie wykorzystano nowcechjzyka, ktra zostaa w peni opisana dopiero w drugim tomie ksiki, w ktrym powicono jej cay rozdzia mechanizm identyfikacji typw podczas pracy programu (ang. run-time type identification RTTI). Mechanizm ten pozwala na uzyskanie informacji o typie, ktra zostaa utracona podczas rzutowania w gr. Rzutowanie dynamic_cast jest w rzeczywistoci jedn z postaci identyfikacji typw podczas pracy programu. W powyszym programie do okrelenia typu wskanika uywane jest sowo kluczowe typeid (zadeklarowane w pliku nagwkowym <typeinfo>). Jak wida, typ rzutowanego w gr wskanika obiektu typu Shape jest kolejno porwnywany ze wskanikami do obiektu typu Circle i Square w celu sprawdzenia, czy ktry z nich odpowiada. Mechanizm identyfikacji typw podczas pracy programu to wicej ni tylko sowo kluczowe typeid - atwo mona sobie rwnie wyobrazi, w jaki sposb mona by bez trudu zaimplementowa wasny system informacji o typie, wykorzystujcy funkcj wirtualn. Tworzony jest obiekt klasy Circle, a jego adres jest rzutowany w gr na wskanik klasy Shape druga wersja tego wyraenia pokazuje, w jaki sposb mona uy rzutowania static_cast, aby rzutowanie w gr byo nieco bardziej jawne. Poniewa jednak rzutowanie w gr jest zawsze bezpieczne, i jest to czsto wykonywana operacja, uwaam, e w przypadku rzutowania w gr jawne rzutowanie wprowadza baagan i jest niepotrzebne. Do okrelenia typu wykorzystano mechanizm identyfikacji typw podczas pracy programu, a nastpnie, do rzutowania w d, zastosowano rzutowanie static_cast. Warto jednak podkreli, e w tym programie odbywa si to waciwie tak samo, jakby uyte zostao rzutowanie dynamic_cast, natomiast klient-programista musi dokona pewnych sprawdze, by dowiedzie si, ktre z rzutowa zakoczyo si powodzeniem. Zanim zamiast rzutowania dynamic_cast uyje si rzutowania static_cast, na og dobrze jest mie do czynienia z sytuacj nieco bardziej deterministyczn ni przedstawiona powyej (i jeszcze raz trzeba zaznaczy, e przed wykorzystaniem rzutowania dynamic_cast naley ponownie przyjrze si swojemu projektowi).

546

Thinking in C++. Edycja polska

Jeeli hierarchia klas nie zawiera funkcji wirtualnych (co wiadczy o wtpliwej jakoci projektu) albojeeli posiadamyjakie inne informacje, pozwalajce na bezpieczne rzutowanie w d, rzutowanie statyczne jest nieco szybsze ni rzutowanie dynamic_cast. Ponadto rzutowanie static_cast nie pozwala na przekroczenie granic hierarchii klas, jak ma to miejsce w przypadku tradycyjnego rzutowania, dziki czemu jest od niego bezpieczniejsze. Jednake statyczne poruszanie si po hierarchii klasjest zawsze ryzykowne i dopki nie mamy do czynienia z jakim szczeglnym przypadkiem, powinnimy uywa rzutowania dynamic_cast.

Podsumowanie
Polimorfizm, zaimplementowany w jzyku C++ za pomoc funkcji wirtualnych, oznacza wielopostaciowo". W programowaniu obiektowym mamy do czynienia z tym samym obliczem (wsplnym interfejsem, zawartym w klasie podstawowej) oraz rnymi postaciami o tym obliczu rozmaitymi wersjami funkcji wirtualnych. Jak przekonalimy si w tym rozdziale, bez uycia abstrakcji danych i dziedziczenia nie jest moliwe zrozumienie ani nawet utworzenie przykadu polimorfizmu. Polimorfizm jest wasnoci, ktrej nie sposb zaprezentowa w izolacji ^ak na przykad deklaracji const czy instrukcji switch); istnieje wycznie we wspdziaaniu, jako element wikszego obrazu zwizkw pomidzy klasami. Niektrzy s zdezorientowani innymi, nieobiektowymi cechami jzyka C++, takimi jak przecianie oraz argumenty domylne, przedstawianymi czasamijakojego cechy obiektowe. Nie dajmy si na to nabra jeeli nie nastpuje pne wizanie, to nie ma rwnie mowy o polimorfizmie. Aby w swoich programach efektywnie uywa polimorfizmu a zatem rwnie technik obiektowych musimy rozszerzy swoje spojrzenie na programowanie, tak aby nie obejmowao ono jedynie skadowych oraz komunikatw zwizanych z pojedyncz klas, ale rwnie cechy wsplne wielu klas oraz wzajemne relacje pomidzy nimi. Mimo e wymaga to znacznego wysiku, to rzeczjest warta gry ze wzgldu na istotne korzyci: szybsze tworzenie programw, lepsz organizacj kodu, moliwo rozbudowy programw oraz atwiejszpielgnacja ich kodu. Polimorfizm zamyka obiektowe waciwoci jzyka. Jednak jzyk C++ zawiera jeszcze dwa wane elementy szablony (ktre zostan przedstawione w rozdziale 16., a bardziej szczegowy ich opis znajduje si w drugim tomie ksiki) oraz obsug wyjtkw (opisan w drugim tomie ksiki). Elementy te umoliwiaj zwikszenie potencjau programistycznego w takim samym stopniu, jak elementy zwizane z programowaniem obiektowym takiejak abstrakcyjne typy danych, dziedziczenie oraz polimorfizm.

wiczenia
Rozwizania wybranych wicze znajduj si w dokumencie elektronicznym The Thinking in C++ Annotated Solution Guide, ktry mona pobra za niewielk opat z witryny www.BruceEckel.com.

Rozdzia 15. Polimorfizm i funkcje wirtualne

54'

1. Utwrz prost hierarchi figur" - klas podstawow o nazwie Shape (figura) oraz klasy pochodne o nazwach Circle (okrg), Square (kwadrat) i Triangle (trjkt). W klasie podstawowej utwrz funkcj wirtualn o nazwie draw( ) (rysuj) i zaso j w klasach pochodnych. Na stercie utwrz tablic wskanikw do obiektw klasy Shape (dokonujc w ten sposb rzutowania w gr wskanikw) i wywoaj funkcj draw( ) za porednictwem wskanikw do klasy podstawowej, sprawdzajc dziaanie funkcji wirtualnych. Jeeli uywany przez ciebie program uruchomieniowy na to pozwala, wykonaj program krokowo. 2. Zmodyfikuj poprzednie wiczenie w taki sposb, by funkcja draw( ) bya funkcj czysto wirtualn. Sprbuj utworzy obiekt klasy Shape. Wykonaj prb wywoania tej czysto wirtualnej funkcji w obrbie konstruktora i zobacz, co si stanie. Pozostawiajc funkcj draw( ) funkcjczysto wirtualn, utwrzjej definicj. 3. Rozszerzajc poprzednie wiczenie, utwrz funkcj, pobierajcprzez warto argument, bdcy obiektem klasy Shape, a nastpnie sprbuj rzutowa w gr obiekt kiasy pochodnej, uywajc gojako argumentu tej funkcji. Zobacz, co si wwczas stanie. Popraw funkcj w taki sposb, by pobieraa referencj do obiektu klasy Shape. 4. Zmodyfikuj program C14:Combined.cpp w taki sposb, by funkcja f ( ) bya w klasie podstawowej funkcj wirtualn. Zmie zawarto funkcji main( ), by dokona rzutowania w gr i wirtualnego wywoania funkcji. 5. Zmodyfikuj program Instrument3^.cpp. dodajc wirtualn funkcje prepare(). Wywoaj t funkcje wewntrz funkcji tune(). 6. Utwrz hierarchi dziedziczenia klasy Rodent (gryzo): Mouse (mysz), Gerbil (myszoskoczek), Hamster (chomik) itd. W klasie podstawowej utwrz funkcje, ktre s wsplne dla wszystkich gryzoni i przedefiniuj je w klasach pochodnych, umoliwiajc rne zachowania, w zalenoci od konkretnego typu pochodnego klasy Rodent. Utwrz tablic wskanikw do obiektw klasy Rodent, wypenij j wskanikami do obiektw poszczeglnych klas pochodnych, a nastpnie wywoaj funkcje, zawarte w klasie podstawowej, obserwujc, co si stanie. 7. Zmodyfikuj poprzedni przykad, uywajc, zamiast tablicy wskanikw, wektora vector<Rodent*>. Upewnij si, e pami jest zwalniana w odpowiedni sposb. 8. Rozpoczynajc od utworzonej poprzednio hierarchii Rodent, wyprowad z klasy Hamster klas BlueHamster (bkitny chomik jest takie stworzenie; miaemje kiedy w dziecistwie), zaso funkcje klasy podstawowej i poka, e nie trzeba zmienia kodu wywoujcego funkcje klasy podstawowej, by dostosowa go do nowego typu. 9. Rozpoczynajc od utworzonej poprzednio hierarchii Rodent, dodaj niewirtualny destruktor, utwrz obiekt klasy Hamster, uywajc do tego operatora new. Nastpnie dokonaj rzutowania uzyskanego wskanika na typ Rodent*, a potem usu wskazywany obiekt za pomoc delete, by pokaza, e nie spowoduje to wywoania wszystkich destruktorw znajdujcych si w hierarchii. Zamie destruktor na wirtualny i poka, e wszystko dziaa teraz prawidowo.

548

Thinking in C++. Edycja polska

10. Rozpoczynajc od utworzonej poprzednio hierarchii Rodent, zmodyfikuj klas Rodent, by bya czysto abstrakcyjn klas podstawow. 11. Utwrz system kontroli ruchu powietrznego, posiadajcy klas podstawow Aircraft (samolot) i rne typy pochodne. Utwrz klas Tower (wiea), zawierajc wektor vector<Aircraft*>, ktra wysya komunikaty do rnych samolotw, znajdujcych si podjej kontrol. 12. Utwrz model szklarni, wyprowadzajc z klasy Plant (rolina) rne gatunki rolin i wbudowujc w swojszklarni mechanizmy utrzymujce roliny. Z3. W programie Early.cpp uczy klas Pet czysto abstrakcyjnklaspodstawow. 14. W programie AddingVirtuaIs.cpp uczy wszystkie funkcje skadowe klasy Pet funkcjami czysto wirtualnymi, ale utwrz definicj funkcji name( ). Popraw odpowiednio klas Dog, wykorzystujc definicj funkcji name( ), zawart w klasie podstawowej. 15. Napisz niewielki program, prezentujcy rnic pomidzy wywoaniem funkcji wirtualnej wewntrz normalnej funkcji skadowej i wewntrz konstruktora. Program powinien udowodni, e te dwa wywoania daj rne wyniki. 16. Zmodyfikuj program VirtualsInDestructors.cpp, tworzc klas pochodn klasy Derived i zasaniajc w niej funkcj f ( ) oraz destruktor. W funkcji main( ) utwrz obiekt nowego typu, dokonaj jego rzutowania w gr, a nastpnie usu go za pomoc operatora delete. 17. W programie powstaym w ramach poprzedniego wiczenia dodaj w kadym destruktorze wywoanie funkcji f(). Wyjanij, co si stao. 18. Utwrz klas, zawierajcskadow, oraz klas pochodn, zawierajc dodatkowskadow. Wykorzystujc operator sizeof, napisz funkcj niebdc funkcj skadow, pobierajcprzez warto obiekt klasy podstawowej i drukujc wielko tego obiektu. W funkcji main( ) utwrz obiekt klasy pochodnej, wydrukuj jego wielko, a nastpnie wywoaj napisanuprzednio funkcj. Wyjanij, co si stao. 19. Utwrz prosty przykad wywoania funkcji wirtualnej i wygeneruj dla niego program wjzyku asemblera. Odszukaj kod wjzyku asemblera, realizujcego wirtualne wywoanie, przeled ten kod i wyjanij jego dziaanie. 20. Utwrz klas, zawierajcjedn funkcj wirtualn, i jedn funkcj, niebdc funkcj wirtualn. Utwrz now klas pochodn, a nastpnie obiekt tej klasy i dokonaj rzutowania w gr na wskanik do klasy podstawowej. Uyj funkcji clock(), zawartej w pliku nagwkowym <ctime> (naley poszuka jej w dokumentacji biblioteki jzyka C), by zmierzy rnic pomidzy wywoaniem wirtualnym i wywoaniem niewirtualnym. Aby zauway rnic, trzeba wykona wiele wywoa kadej funkcji wewntrz ptli, w ktrej dokonywanyjest pomiar czasu. 21. Zmodyfikuj program C14:Order.cpp, dodajc w klasie podstawowej funkcj wirtualn za pomoc makroinstrukcji CLASS (tak aby co drukowaa) oraz czynic destruktor wirtualnym. Utwrz obiekty rnych klas pochodnych

Rozdzia 15. Polimorfizm i funkcje wirtualne

549

i dokonaj ich rzutowania w gr na klas podstawow. Upewnij si, e dziaajwirtualne mechanizmy, a take, e dokonywanajest poprawna konstrukcja i destrukcja. 22. Utwrz klas, zawierajctrzy przecione funkcje wirtualne. Utwrzjej kias pochodn i zaso w niej jedn z funkcji. Utwrz obiekt klasy pochodnej. Czy za porednictwem obiektu klasy pochodnej mona wywoa wszystkie funkcje klasy podstawowej? Dokonaj rzutowania adresu obiektu na wskanik do klasy podstawowej. Czy wszystkie trzy funkcje mog by wywoane za porednictwem klasy podstawowej? Usu definicj funkcji zasaniajcej, znajdujcsi w klasie pochodnej. Czy teraz mona wywoa wszystkie trzy funkcje za porednictwem obiektu klasy pochodnej? 23. Zmodyfikuj program VariantReturn.cpp, by pokaza, e dziaa tak samo z wykorzystaniem referencji, jak z wykorzystaniem wskanikw. 24. Wjaki sposb mona okreslic,jak w programie Early.cpp kompilator realizuje wywoanie za pomoc wczesnego czy te pnego wizania? Ustal, wjaki sposb odbywa si to w przypadku uywanego przez ciebie kompilatora. 25. Utwrz klas podstawow, zawierajc funkcj clone(), zwracajc wskanik do kopii biecego obiektu. Utwrz dwie kIasy pochodne, ktre zawieraj funkcje zasaniajce funkcj clone( ), zwracajce wskaniki do kopii obiektw swoich typw. Utwrz w funkcji main( ) obiekty obu klas pochodnych i dokonaj ich rzutowania w gr, a nastpnie dla kadego z nich wywoaj funkcj clone() i sprawd, czy utworzone kopie sodpowiednich podtypw. Poeksperymentuj z funkcjacIone() w taki sposb, aby zwracaa wskanik do typu podstawowego, a nastpnie sprbuj zwraca okrelony dokadnie typ pochodny. Czy moesz wyobrazi sobie sytuacje, w ktrych koniecznejest zastosowanie drugiego z opisywanych sposobw? 26. Zmodyfikuj program OStackTest.cpp, tworzc wasn klas. Nastpnie, wykorzystujc wielokrotne dziedziczenie, utwrz klas pochodn wygenerowanej uprzednio kIasy i klasy Object. Utworzysz w ten sposb co, co mona umieci w kontenerze Stack. Przetestuj dziaanie swojej klasy w funkcji main(). 27. Do programu OperatorPolymorphism.cpp dodaj typ Tensor. 28. (rednio trudne) Utwrz klas podstawowaX, nieposiadajcdanych skadowych ani konstruktora, a tylko pojedyncz funkcj wirtualn. Utwrz klas Y, bdcklaspochodnkiasy X, nieposiadajcjawnego konstruktora. Wygeneruj kod programu wjzyku asemblera i przeanalizuj go, aby okreli, czy dla klasy X tworzonyjest konstruktor, ajeeli tak, tojakijestjego kod. Wyjanij uzyskane rezultaty. Skoro kIasa X nie posiada domylnego konstruktora, to dlaczego kompilator nie zgasza bdu? 29. (rednio trudne) Zmodyfikuj poprzednie wiczenie, tworzc w obu klasach konstruktory w taki sposb, by kady z nich wywoywa funkcj wirtualn. Wygeneruj kod programu wjzyku asemblera. Zobacz, gdzie przypisywana jest warto wskanikowi VPTR wewntrz kadego z konstruktorw.

550

Thinking in C++. Edycja polska

Czy uywany przez ciebie kompilator stosuje wewntrz konstruktora mechanizm wywoa funkcji wirtualnych? Ustal, dlaczego wewntrz konstruktora wywoywanajest lokalna wersja funkcji. 30. (Trudne) Gdyby wywoania funkcji w stosunku do obiektu przekazywanego przez warto nie podlegafy wczesnemu wizaniu, to wywoanie wirtualne mogyby odwoywa si do nieistniejcych czci obiektu. Czy to moliwe? Napisz kod wymuszajcy wywoanie wirtualne i zobacz, czy wywoa to zaamanie wykonywania programu. Aby wyjanijego zachowanie, sprawd, co si stanie w przypadku przekazania obiektu przez warto. 31. (Trudne) Sprawd dokadnie, ile razy wicej czasu wymaga wywoanie funkcji wirtualnej sigajc do informacji, dotyczcychjzyka asemblera uywanego przez ciebie procesora albo innej dokumentacji technicznej, i okrelajc liczb cykli zegarowych wymaganych w przypadku prostego wywoania funkcji w porwnaniu z liczbcykli wymagandla wywoania funkcji wirtualnej. 32. Okrel wielko wskanika VPTR w uywanej przez ciebie implementacji jzyka. Nastpnie, wykorzystujc wielokrotne dziedziczenie, utwrz klas pochodndwch klas, zawierajcych funkcje wirtualne. Czy powstaa w ten sposb klasa posiadajeden czy dwa wskaniki VPTR? 33. Utwrz klas, posiadajc dane skadowe oraz funkcje wirtualne. Napisz funkcj, ktra przeglda pami zajmowanprzez obiekt i drukuje rnejego czci. Aby to zrobi, naley wykona szereg eksperymentw i metodprb i bdw odkry, gdzie w obiekcie przechowywanyjest wskanik VPTR. 34. Udajc, e funkcje wirtualne nie istniej, zmodyfikuj program Instrument4.cpp w taki sposb, by zastpi w nim wywoania funkcji wirtualnych rzutowaniem dynamic_cast. Wyjanij, dlaczegojest to zy pomys. 35. Zmodyfikuj program StaticHierarchyNavigation.cpp w taki sposb, by zamiast dostpnej w C++ identyfikacji typw podczas pracy programu (RTTI) utworzy swj wasny mechanizm identyfikacji typw, uywajc do tego zawartej w klasie podstawowej funkcji whatAmI( ) (,,czymjestem") oraz wyliczenia enum type { Circles, Squares };. 36. Uywajcjako punktu wyjcia programu PointerToMemberOperator.cpp, zamieszczonego w rozdziale 12., poka, e polimorfizm dziaa rwnie w przypadku wskanikw do skadowych, nawet gdy operator->* jest przeciony.

Wprowadzenie do szablonw
Dziedziczenie i kompozycja umoliwiaj powtrne wykorzystanie kodu obiektw. Dostpne w jzyku C++ szablony pozwalaj na ponowne zastosowanie kodu rdowego. Mimo e szablony dostpne w jzyku C++ s narzdziem programistycznym oglnego przeznaczenia, to gdy zostay one wprowadzone dojzyka, wydaway si zniechca do uywania hierarchii klas kontenerw, bazujcych na obiektach (pokazanych pod koniec rozdziau 15.). Na przykad kontenery i algorytmy standardu jzyka C++ (opisane w dwch rozdziaach drugiego tomu ksiki, ktry mona pobra z witryny http:/flielion.pUonline/thinking/index.html) zostay zbudowane wycznie z wykorzystaniem szablonw i z punktu widzenia programisty s one do atwe w uytku. W rozdziale tym przedstawiono nie tylko podstawy szablonw, ale zawarto w nim rwnie wprowadzenie do kontenerw, bdcych podstawowymi elementami programowania obiektowego. Zostay one zrealizowane niemal w peni w postaci kontenerw, zawartych w standardowej bibliotece jzyka C++. W ksice pojawiy si ju przykady kontenerw klasy Stash i Stack wic czytelnik nabra ju pewnej wprawy w ich uywaniu. W niniejszym rozdziale do kontenerw zostanie dodane pojcie iteratora. Mimo e kontenery s idealnymi przykadami wykorzystania szablonw, w drugim tomie ksiki (zawierajcym rozdzia, opisujcy bardziej zaawansowane zagadnienia dotyczce szablonw) dowiesz si, e istnieje rwnie wiele innych sposobw ich stosowania.

Rozdzia 16.

Kontenery
Zamy, e zamierzamy utworzy stos, tak jak ju zostao przedstawione w ksice. Stos ten bdzie przechowywa liczby cakowite i zostanie zrealizowany w prosty sposb:

i52

Thinking in C++. Edycja polska //: C16:IntStack.cpp // Prosty stos liczb cakowitych //{L} fibonacci #include "fibonacci.h" #include "../require.h" #include <iostream> using namespace std; class IntStack { enum { ssize - 100 }; int stack[ssize]; public: IntStack() : top(0) {} void push(int i) { require(top < ssize, "Zbyt wiele wywolan funkcji pushO"); stack[top++] = i;
} int pop() {

int top;

require(top > 0, "Zbyt wiele wywolan funkcji popO"); return stack[--top];

int main() { IntStack is; // Umieszczenie na stosie kilku liczb Fibonacciego, // w celu uczynienia go bardziej interesujcym: for(int i - 0; i < 20; i++) is.push(fibonacci(i)); // Pobranie ze stosu i wydrukowanie liczb: for(int k - 0; k < 20; k++) cout << is.pop() << endl; Klasa IntStack jest prostym przykadem rozwijanego w d stosu. Dla uproszczenia zosta on utworzony jako stos staej wielkoci, ale nic nie stoi na przeszkodzie, by zmodyfikowa go w taki sposb, aby powiksza si automatycznie, przydzielajc pami na stercie, jak w przypadku klasy Stack, opisywanej w tej ksice. W funkcji main( ) na stosiejest umieszczanych kilka Iiczb cakowitych, ktre pniej s z niego z powrotem pobierane. Aby uczyni ten przykad bardziej interesujcym, umieszczane na stosie liczby tworzone s za pomoc funkcji fibonacci( ), generujcej znany cig liczb, opisujcy rozmnaanie si krlikw. Poniej zamieszczono plik nagwkowy, deklarujcy t funkcj: / / : C16:fibonacci.h // Generator liczb Fibonacciego int fibonacci(int n); ///:A oto jego implementacja: / / : C16:fibonacci.cpp {0} #include "../require.h"

Rozdzia 16. << Wprowadzenie do szablonw int fibonacci(int n) { const int sz = 100: require(n < sz); static int f[sz]; // Inicjalizowana wartociami zerowymi f[0] - f[l] - 1: // Poszukiwanie niewypenionych elementw tablicy: int i; for(i - 0; i < sz; i++) if(f[i] == 0) break; while(i <= n) { f[i] - f[i-l] + f[i-2];
i++;

553

} ///:-

return f[n];

Jest to do efektywna implementacja, poniewa nigdy nie generuje ona adnej liczby wicej ni jednokrotnie. Zastosowano w niej statyczn tablic liczb cakowitych, a ponadto wykorzystano fakt, e kompilator inicjalizuje tablice statyczne wartociami zerowymi1. Pierwsza ptla for powoduje przesunicie indeksu i do pierwszego elementu tablicy, ktrego warto jest zerowa, a nastpnie ptla while dodaje do tablicy kolejne liczby cigu Fibonacciego, a do osignicia danego elementu. Zwr uwag na to, e jeeli pozycja n tablicy zostaa ju zainicjowana liczbami cigu Fibonacciego, to ptla whilejest w caoci pomijana2.

Potrzeba istnienia kontenerw


Oczywicie, stos liczb cakowitych nie jest narzdziem o decydujcym znaczeniu. Prawdziwa potrzeba uywania kontenerw pojawia si w przypadku tworzenia obiektw na stercie za pomoc operatora new i w razie usuwania ich instrukcj delete. Na og w trakcie pisania programu nie wiadomo, ile obiektw bdzie potrzebnych. Na przykad w systemie obsugujcym kontrol ruchu lotniczego nie zamierzamy ogranicza liczby samolotw, obsugiwanych przez system. Nie chcielibymy, aby program przerwa prac tyIko dlatego, e przekroczylimy jak ich liczb. W komputerowym systemie wspomagajcym projektowanie mamy do czynienia z du liczb symboli, ale to uytkownik okrela (w trakcie pracy programu) ile ich dokadnie potrzeba. Kiedy uwiadomisz sobie t tendencj, zauwaysz wiele podobnych przykadw, dotyczcych sytuacji wystpujcych w twoich programach. Programici jzyka C, ktrzy w swoim systemie obsugi pamici" wykorzystywali pami wirtualn, s czsto zaniepokojeni pomysem stosowania operatorw new i delete oraz klas kontenerowych. Najwidoczniej jedynym sposobem stosowanym ' W tym przypadku lepiej byoby wykorzysta inicjalizacj agregatow, definiujc tablic f [ ] w postaci: static int f[sz] = {1,1};, co zapobiegoby wielokrotnemu przypisywaniu wartocijej dwm pierwszym elementom, podczas kadego wywoania funkcji przyp. tlum. Znacznie efektywniej byoby od razu, na pocztku funkcji, zwraca warto elementu f[n], w przypadku, gdy niejest on zerowy, co pozwolioby unikn niepotrzebnego poszukiwania niewypenionych elementw tablicy w sytuacji, gdy warto danego elementu cigu zostaaju uprzednio wyliczona przyp- tum.

,54

Thinking in C++. Edycja polska wjzyku C bylo utworzenie ogromnej, globalnej tablicy, wikszej ni to, czego mgby kiedykolwiek potrzebowa program. Nie wymaga to co prawda duo mylenia (ani znajomoci funkcji malloc() i free()), lecz powoduje tworzenie programw, ktre nie s atwo przenone i mog zawiera trudne do znalezienia bdy. Ponadto jeeli w jzyku C++ zostanie utworzona ogromna tablica obiektw, narzut wprowadzony przez konstruktor i destruktor moe w znacznym stopniu spowolni jego dziaanie. Podejcie stosowane w jzyku C++ sprawdza si znacznie lepiej gdy potrzebny jest obiekt, tworzymy go za pomoc operatora new i umieszczamy jego wskanik w kontenerze. Nastpnie pobieramy go i wykonujemy na nim jakie operacje. Tworzymy zatem tylko te obiekty, ktre s niezbdne. Zazwyczaj rwnie kiedy uruchamiany jest program, nie s dostpne wszystkie informacje, potrzebne do inicjalizacji obiektw. Operator new pozwala na wstrzymanie si z utworzeniem obiektu do chwili, gdy wydarzy si co wjego otoczeniu. Tak wic w najbardziej typowej sytuacji bdziemy tworzy kontenery, przechowujce wskaniki do interesujcych nas obiektw. Obiekty te bd tworzone za pomoc operatora new, a uzyskany w ten sposb wskanik zostanie umieszczony w kontenerze (potencjalnie dokonujc jednoczenie rzutowania w gr), a nastpnie pobierany, gdy bdziemy chcieli co z tym obiektem zrobi. Technika ta pozwala na tworzenie bardziej elastycznych i oglnych programw.

Podstawy szablonw
Teraz jednak pojawiaj si problemy. Posiadamy klas IntStack, przechowujc liczby cakowite. Chcemy jednak, by stos przechowywa symbole, samoloty, roliny itd. Kadorazowa modyfikacja kodu rdowego nie wydaje si, w przypadku jzyka promujcego jego wielokrotne wykorzystywanie, najbardziej inteligentnym rozwizaniem. Naley wskazajaki lepszy sposb. Istniej trzy techniki wielokrotnego wykorzystywania kodu, ktrych mona uy w tym przypadku: stosowana w jzyku C (zaprezentowana tutaj dla kontrastu), uywana w Smalltalku (ktra istotnie wpyna na jzyk C++) oraz wykorzystywana w jzyku C++ czyli szablony. Rozwizanie stosowane w jzyku C. Oczywicie, bdziemy starali si unika rozwizania stosowanego w jzyku C, poniewa jest ono chaotyczne, podatne na bdy i nieeleganckie. W podejciu tym kopiujemy kod rdowy klasy Stack i rcznie wprowadzamyjego modyfikacje (i zarazem nowe bdy). Z pewnociniejest to zbyt wydajna technika. Rozwizanie jzyka SmaUtalk W jzyku Smalltalk (a w lad za nim rwnie w Javie) przyjto proste i oczywiste rozwizanie jeeli chcemy wykorzysta ponownie kod, naley uy dziedziczenia. Aby to zrealizowa, klasa kadego kontenera zawiera elementy standardowej klasy podstawowej Object (podobnej do przedstawionej w przykadzie, zamieszczonym pod koniec rozdziau 15.). Jednak z uwagi na to, e w jzyku

Rozdzia 16. Wprowadzenie do szablonw

555

Smalltalk podstawowe znaczenie ma biblioteka, klasy nie s nigdy tworzone od podstaw. Muszone zosta wyprowadzone zjakiej istniejcej ju klasy. Naley zawsze znale klas najbardziej odpowiadajc potrzebom, utworzy jej klas pochodn i wprowadzi w niej niezbdne zmiany. Oczywicie, jest to korzystne, poniewa minimalizuje nakad pracy (dlatego, zanim zostanie si produktywnym programistjzyka Smalltalk, trzeba spdzi wiele czasu na uczeniu si biblioteki klas). Oznacza to jednak rwnie, e wszystkie klasy jzyk Smalltalk s w kocu elementami pojedynczego drzewa dziedziczenia. Tworzc now klas, zawsze musimy wyprowadzi j z jakiej gazi tego drzewa. Wiksza cz drzewa ju istnieje (tworzy j biblioteka klas jzyka Smalltalk), a jego korzeniem jest klasa o nazwie Object ta sama, ktr zawiera kady kontener, dostpny w Smalltalku. To sprytna sztuczka, poniewa oznacza ona, e kada klasa znajdujca si w hierarchii klasjzyka Smalltalk (i w Javie 3 )jestfclaspochodnklasy Object, a zatem kada klasa moe by przechowywana w dowolnym kontenerze (wczajc w to rwnie sam kontener). Jest to rodzaj hierarchii o jednym korzeniu, opartej na podstawowym, oglnym typie (noszcym czsto nazw Object, podobnie jak w jzyku Java) i okrelanej mianem hierarchii bazujcej na obiekcie". By moe znaszju ten termin i sdzisz, e jest to jakie podstawowe pojcie dotyczce programowania obiektowego, jak na przykad polimorfizm. W rzeczywistoci odnosi si ono do hierarchii klas, w ktrej korzeniu znajduje si klasa Object (aIbo jaka inna, noszca podobn nazw). Posiada ona klasy kontenerw, przechowujce obiekty klasy Object. Poniewa biblioteka klas jzyka Smalltalk ma znacznie dusz histori i zwizanych jest z ni znacznie wicej dowiadcze ni w przypadku jzyka C++, a take z uwagi na to, e pierwotnie kompilatory jzyka C++ nie zawieraty bibliotek klas kontenerowych, dobrym pomysem wydaje si skopiowanie w jzyku C++ biblioteki jzyka Smalltalk. Zostao to wykonane w charakterze eksperymentu za pomoc wczesnej 4 implementacji jzyka C++ , a poniewa implementacja ta zawiera pokan ilo kodu, wielu programistw zaczo jej uywa. Prbujc uywa klas kontenerowych, napotkali oni jednak problem. Problem polega na tym, e w jzyku Smalltalk (a take w wikszoci innych jzykw obiektowych, jakie znam) wszystkie klasy automatycznie dziedzicz po pojedynczej hierarchii, co nie jest prawd w jzyku C++. Moemy posiada eleganck hierarchi bazujc na obiekcie, wraz z jej klasami kontenerowymi, ale pewnego dnia zakupimy zestaw klas, zawierajcych figury lub samoloty, u innego producenta, niewykorzystujcego tej hierarchii (wycznie dlatego, e stosowanie takiej hierarchii wie si z pewnym narzutem, ktrego wystrzegaj si programici). W jaki sposb umieci oddzielne drzewo klas w naszej hierarchii bazujcej na obiekcie? Poniej zamieszczono rysunek, pokazujcy, na czym polega problem:

' Z wyjtkiem prymitywnych typw danych, dostpnych w Javie. Z uwagi na efektywno, nie zostay one wyprowadzone z klasy Object. Biblioteka OOPS, zaimplementowana przez Keitha Gorlena, gdy pracowa w National Institutes ofHealth.

Thinking in C++. Edycja polska

Z uwagi na to, e jzyk C++ umoliwia istnienie wielu niezalenych hierarchii klas, zapoyczona z jzyka Smalltalk hierarchia bazujca na obiekcie, nie sprawdza si w nim zbyt dobrze. Rozwizanie wydaje si oczywiste. Jeeli moemy posiada wiele hierarchii dziedziczenia, to powinnimy mie moliwo dziedziczenia po wicej ni jednej klasie problem zostanie rozwizany przez wielokrotne dziedziczenie. Postpujemy wic nastpujco (podobny przykad znajduje si na kocu rozdziau 15.):

Obecnie klasa OFigura posiada cechy i zachowanie klasy Figura, ale poniewa dziedziczy ona rwnie po klasie Object, moe by umieszczona w Kontenerze. Dodatkowe dziedziczenie klas OOkrg, OKwadrat itd. jest niezbdne, bo umoliwia ono rzutowanie tych klas w gr na klas OFigura, dziki czemu bd one poprawnie funkcjonowa. A zatem sprawy zaczynajsi nieco komplikowa. Producenci kompilatorw tworzyli i udostpniali swoje hierarchie klas kontenerw, bazujce na obiektach, z ktrych wikszo zostaa ju zastpiona wersjami opracowanymi z wykorzystaniem szablonw. Mona twierdzi, e wielokrotne dziedziczenie jest niezbdne do rozwizywania oglnych problemw programistycznych, ale, jak przekonamy si w drugim tomie ksiki, zwizanej z nim zoonoci lepiej unika, wykorzystujcje wycznie w szczeglnych przypadkach.

ozwizanie z wykorzystaniem szablonw


Mimo e hierarchia klas bazujca na obiekcie, wykorzystujca wielokrotne dziedziczenie, jest pojciowo do prosta, to jej stosowanie okazuje si w praktyce niewygodne. W swojej pierwszej ksice5 Stroustrup zaprezentowa to, co uwaa za preferowane rozwizanie konkurencyjne w stosunku do hierarchii bazujcej na obiekcie. Klasy kontenerowe zostay utworzone jako ogromne makroinstrukcje preprocesora,
!jarne Stroustrup: The C++ Programming Language, Addison-Wesley, 1986 (pierwsze wydanie).

Rozdzia 16. Wprowadzenie do szablonw

557

posiadajce argumenty, za pomoc ktrych mona byo okreli podany typ. Gdy chcielimy utworzy kontener, przechowujcy okrelony typ, naleao w tym celu wykona kilka wywoa makroinstrukcji. Niestety, metoda ta bya pomieszana z rozwizaniami prezentowanymi w istniejcej wwczas literaturze, dotyczcej Smalltalka oraz dowiadczeniami progamistycznymi, a poza tym by Niestety, rozwizanie to byo mieszank metod prezentowanych w istniejcej wwczas literaturze dotyczcej Smalltalka i dowiadcze programistycznych, a poza tym byo niezbyt porczne. W zasadzie nikt go nie uywa. W tym czasie Stroustrup i zesp zajmujcy si jzykiem C++ w Bell Labs zmodyfikowali oryginalne rozwizanie z wykorzystaniem makroinstrukcji, upraszczajc je i przenoszc z domeny preprocesora do kompilatora. Ten nowy mechanizm zastpowania kodu zosta nazwany szablonem6 (ang. template) reprezentowa on zupenie inne ujcie kwestii wielokrotnego uycia kodu. W szablonach, zamiast ponownego stosowania kodu obiektu, jak w przypadku dziedziczenia i kompozycji, wykorzystano kod rdowy. Zamiast obiektw oglnej klasy podstawowej o nazwie Object kontener przechowuje nieokrelony parametr. Gdy wykorzystujemy szablony, zastpieniem tego parametru zajmuje si kompilator wykonuje to podobnie do dziaania dawnych makroinstrukcji, ale w sposb znacznie bardziej przejrzysty i atwiejszy w uyciu. Obecnie, zamiast zajmowa si dziedziczeniem czy kompozycj, zwizanymi ze stosowaniem klas kontenerowych, uywamy szablonowej wersji kontenera, nadajc mu posta odpowiadajc naszemu problemowi, jak na rysunku poniej:

Kompilator wykona ca prac za nas, czego ostatecznym efektem bdzie utworzenie kontenera dokadnie speniajcego nasze wymagania. Nastpi ono bez potrzeby uywania niewygodnej hierarchii dziedziczenia. Szablony dostpne w jzyku C++ realizuj koncepcj sparametryzowanego typu. Inna korzy, wynikajca z dziaania wykonywanego przez kontenery, polega na tym, e pocztkujcy programici, ktrzy nie znaj dziedziczenia (albo moe ono wydawa si im niewygodne), mog od razu przystpi do uywania klas kontenerowych ^ak to uczynilimy z wektorami, wykorzystywanymi w caej ksice).

' Wydaje si, e inspiracj dla szablonw byy generaliajzyka ADA.

58

Thinking in C++. Edycja polska

Skadnia szablonw
Sowo kluczowe template (szablon) informuje kompilator, e nastpujca po nim definicja klasy bdzie operowaajednym lub wiksz liczb niesprecyzowanych typw. Do momentu wygenerowania kodu rzeczywistej klasy na podstawie szablonu typy te musz zosta okrelone, dziki czemu kompilator bdzie w stanie je zastpi. Poniej znajduje si niewielki przykad, ilustrujcy skadni szablonw. Zostaje utworzona tablica sprawdzajca zakres indeksw:

//: C16:Array.cpp #include ". ./require.h" #include <iostream> using namespace std; template<class T> class Array { enum { size - 100 }; T A[size]; public: T& operator[](int index) { require(index >= 0 && index < size. "Indeks poza zakresem"); return A[index];

int main() { Array<int> ia; Array<float> fa; for(int i = 0; i < 20; i++) { ia[i] = i * i; fa[i] = float(i) * 1.414; } for(int j = 0; j < 20; j++) cout << j << ": " << ia[j] << ". " << fa[j] << endl;

A zatem przypomina to cakiem zwyczajn klas, z wyjtkiem wiersza:

template<class T>
ktry oznacza, e symbol T stanowi parametr podstawienia oraz e reprezentuje on nazw typu. Parametr T jest uywany wewntrz klasy wszdzie tam, gdzie zwykle wystpowaby normalny typ, przechowywany przez kontener. W klasie Array elementy s wstawiane oraz pobierane za pomoc tej samej funkcji przecionego operatora [ ]. Zwraca on referencj, wic moe by uywany po obu stronach znaku rwnoci (zarwno jako l-warto, jak i jako p-warto). Zwr uwag na to, e w przypadku gdy indeks znajduje si poza zakresem, do wydrukowania komunikatu wykorzystywana jest funkcja require( ). Poniewa operator[ ] jest funkcj inUne, mona uywa takiego sposobu w celu upewnienia si, e nie nastpuje naruszanie granic tablicy, a z gotowego kodu usun funkcj require( ).

Rozdzia 16. Wprowadzenie do szablonw

559

W funkcji main() atwo utworzy tablice przechowujce rne typy obiektw. Gdy napiszemy: Array<int> ia; Array<float> fa; kompilator rozwinie szablon Array ^est to nazywane tworzeniem wystpienia lub konkretyzacja^ dwukrotnie, tworzc dwie wygenerowane klasy, ktrych mona traktowa jako klasy Array_int oraz Array_float (rne kompilatory mog uzupenia nazwy na rozmaite sposoby). S to klasy podobne do tych, ktre mona by utworzy dokonujc zastpienia wasnorcznie, z jednym wyjtkiem kompilator tworzy je automatycznie w trakcie definicji obiektw ia oraz fa. Zwr rwnie uwag na to, e powtarzaniu si nazw klas albo zapobiega kompilator, albo s one scalane przez program czcy.

Definicje funkcji niebedacych funkcjami inline


Oczywicie, niekiedy chcemy, aby funkcje skadowe klasy nie byy definicjami funkcji inline. W takim przypadku kompilator musi widzie" deklaracj template przed definicj funkcji skadowej. Poniej przedstawiono poprzedni przykad, prezentujcy definicj funkcji skadowej, niebdcej funkcjinline: / / : C16:Array2.cpp // Definicja szablonu funkcji, // niebdcej funkcj inline #include ". ./require.h" template<class T> class Array { enum { size = 100 };

T A[size]; public: T& operator[](int index);

template<class T> T&Array<T>::operator[](int index) { require(index >- 0 && index < size, "Indeks poza zakresem"); return A[index];
int main() { Array<float> fa; fa[0] - 1.414; Kademu odwoaniu do nazwy klasy szablonu musi towarzyszy lista argumentw szablonu, jak w nazwie funkcji Array<T>::operator[]. Mona sobie wyobrazi, e wewntrznie nazwa kasy jest uzupeniana argumentami znajdujcymi si na licie argumentw szablonu, dajc w wyniku unikatowy identyfikator nazwy klasy dla kadej konkretyzacji szablonu.

BO

Thinking in C++. Edycja polska

1iki nagtwkowe
Nawet w przypadku tworzenia definicji funkcji niebdcych funkcjami inline wszystkie deklaracje oraz definicje, dotyczce szablonu, bdziemy zazwyczaj umieszcza w pliku nagwkowym. Moe to wyglda na pogwacenie zasady dotyczcej plikw nagwkowych, polegajcej na tym, by nie umieszcza w nich nic takiego, co powoduje przydzielenie pamici" (co zapobiega bdom wielokrotnych definicji w trakcie czenia), ale definicje zawarte w szablonach maj charakter szczeglny. Poprzedzenie jakiegokolwiek elementu tekstem template<...> oznacza, e kompilator nie przydziela mu w tym miejscu pamici, czekajc, a programista kae to zrobi (za pomoc konkretyzacji szablonu). Gdzie w kompilatorze programie czcym istnieje mechanizm usuwajcy wielokrotne definicje identycznych szablonw. Tak wic ze wzgldu na atwo uycia niemal zawsze bdziemy umieszcza w plikach nagwkowych (w caoci) zarwno deklaracje, jak i definicje szablonw. Zdarzaj si sytuacje, w ktrych trzeba umieci definicj szablonu w oddzielnym pliku cpp z uwagi na konieczno spenienia jakich szczeglnych wymaga (na przykad wymuszajc istnienie konkretyzacji szablonu w tylko jednym pliku dIl systemu Windows). Wikszo kompilatorw jest wyposaonych w mechanizmy, ktre na to pozwalaj by je wykorzysta, musisz przejrze dokumentacj uywanego przez siebie kompilatora. Niektrzy przeczuwaj, e umieszczenie caego kodu rdowego implementacji w pliku nagwkowym umoliwia nabywcom biblioteki kradzie i modyfikacj zawartego w niej kodu. Moe to stanowi problem, w zalenoci jednak od sposobu, wjaki postrzegasz t kwesti czy sprzedajesz produkt, czy te usug. Jeeli jest to produkt, musisz zrobi wszystko, co w twojej mocy, by go zabezpieczy i prawdopodobnie nie bdziesz chcia udostpnia nabywcy kodu rdowego, a jedynie skompilowany kod. Lecz wiele osb postrzega oprogramowanie jako usug, a nawet jako co wicej subskrypcj usug. Klient oczekuje twojej porady i chce, by nadal zajmowa si pielgnacj fragmentu kodu przeznaczonego do wielokrotnego uytku. Dziki temu sam nie bdzie musia tego robi i skupi si na wykonywaniu swojej pracy. Uwaam, e wikszo klientw bdzie ci traktowa jako wartociowe rdo pomocy, w zwizku z czym nie nara na szwank waszych stosunkw. A jeeli chodzi o tych nielicznych, ktrzy wol co ukra ni kupi albo samodzielnie zrobi co oryginalnego, to prawdopodobnie i tak nie bd w stanie dotrzyma ci kroku.

Klasa lntStack jako szablon


Poniej zamieszczono kontener oraz iterator, pochodzce z pliku IntStack.cpp, ktre zostay zrealizowane jako oglny kontener za pomoc szablonw:
/ / : C16:StackTemplate.h // Prosty szablon stosu #ifndef STACKTEMPLATE_H define STACKTEMPLATE_H #include "../require.h" template<class T> class StackTemplate'{

Rozdzia 16. Wprowadzenie do szablonw enum { ssize - 100 }; T stack[ssize]; public: StackTemplate() : top(0) {} void push(const T& i) { require(top < ssize, "Zbyt wiele wywolan funkcji pushO"); stack[top++] - i;

561

int top;

T pop() { require(top > 0, "Zbyt wiele wywolan funkcji popO"); return stack[--top];

int size() { return top; } #endif // STACKTEMPLATE_H ///:Zwr uwag na to, e szablon opiera si na pewnych zaoeniach, dotyczcych przechowywanych w nim obiektw. Na przykad klasa StackTemplate zakada, wewntrz funkcji push(), e klasa T posiada jaki rodzaj operacji przypisania. Mona powiedzie, e szablon sugeruje interfejs" typw, ktre jest w stanie przechowywa. Inaczej mwic, szablony tworz rodzaj mechanizmu sabej kontroli typw (ang weak typing) w jzyku C++, ktry jest normalnie jzykiem o cisej kontroli typw. Zamiast da, by dopuszczalny obiekt by jakiego konkretnego typu, saba kontrola typw wymagajedynie, by dla danego obiektu byy dostpne funkcje skadowe, ktre zamierza wywoa szablon. Tak wic kod zawierajcy sabkontrol typw moe by zastosowany w odniesieniu do dowolnego obiektu, ktry akceptuje wywoania tych funkcji, co sprawia, ejest on znacznie bardziej elastyczny7. Poniej zamieszczono poprawiony kod, testujcy dziaanie szablonu: //: C16:StackTemplateTest.cpp // Test prostego szablonu stosu //{L} fibonacci #include "fibonacci.h" #include "StackTemplate.h" #include <iostream> #include <fstream> #include <string> using namespace std; int main() { StackTemplate<int> is; for(int i = 0; i < 20; i++) is.push(fibonacci(i)); for(int k - 0; k < 20; k++) cout << is.pop() << endl; ifstream in("StackTemplateTest.cpp"); assure(in. "StackTemplateTest.cpp"); Wszystkie metody wjzykach Smalltalk i Python s metodami o sabej kontroli typw, w zwizku z czymjzyki te ne potrzebujmechanizmu szablonw. W rezultacie wjzykach tych uzyskuje si szablony bez stosowania szablonw.

}:

562
string line; StackTemplate<str1ng> strings; while(getline(in, line)) strings.push(line); while(strings.size() > 0) cout << strings.pop() << endl;

Thinklng in C++. Edycja polska

Jedyna rnica w stosunku do poprzedniej wersji programu polega na sposobie utworzenia obiektu is. Wewntrz listy argumentw szablonu okrela si typ obiektu obsugiwanego przez stos i przez iterator. Aby zademonstrowa oglny charakter szablonu StackTemplate, zosta rwnie utworzony obiekt przechowujcy acuchy. Zosta on przetestowany przez odczytywanie wierszy tekstu, zawartych w pliku rdowym programu.

State w szablonach
Argumenty szablonw nie ograniczaj si do typw klas mona uywa w nich rwnie typw wbudowanych. Wartoci tych typw mog sta si nastpnie staymi czasu kompilacji dla okrelonych konkretyzacji szablonu. W stosunku do takich argumentw mona uywa nawet wartoci domylnych. Poniszy przykad pozwala na ustalenie podczas konkretyzacji wielkoci klasy Array, lecz okrela rwniejej domylnwielko: / / : C16:Array3.cpp // Wbudowane typy jako argumenty szablonw #include ". ./require.h" #include <iostream> using namespace std; template<class T. int size - 100> class Array { T array[size]; public: T& operator[](int index) { require(index > 0 && index < size, "Indeks poza zakresem"); return array[index]; } int length() const { return size; } class Number { float f; public: Number(float ff - O.Of) : f(ff) {} Number& operator-(const Nuwber& n) { f - n.f; return *this; }

operator float() const { return f; } friend ostream& operator<<(ostream& os, const Number& x) return os << x.f;

Rozdzia 16. << Wprowadzenie do szablonw

563

template<class T. int size = 20> class Holder { Array<T, size>* np; public: Holder() : np(0) {} T& operator[](int i) { require(0 <= i && i < size); if(!np) np - new Array<T. size>; return np->operator[](i); }

int length() const { return size; ~Holder() { delete np; }

int main() { Holder<Number> h; for(int i = 0; i < 20; i++) h[i] = i; for(int j = 0; j < 20; j++) cout << h[j] << endl ; Podobnie jak poprzednio, klasa Array jest kontrolowan tablic obiektw, uniemoliwiajc przekroczenie zakresu indeksw. Klasa Holder przypomina klas Array, z wyjtkiem tego, e zawiera ona wskanik do obiektu klasy Array, a nie osadzony wewntrz obiekt tej klasy. Wskanik ten niejest inicjalizowany wewntrz konstruktora inicjalizacja jest odoona do momentu pierwszego dostpu do kontenera. Technika ta nosi nazw leniwej inicjalizacji (ang. lazy initialization) moemy j stosowa w przypadku, gdy tworzymy wiele obiektw, ale nie odwoujemy si do nich wszystkich i zaley nam na oszczdzaniu pamici. Mona zauway, e w przypadku adnego z szablonw warto parametru size nie jest zapisywana wewntrz klasy. Wykorzystuje si j w taki sposb, jakby bya skadow, uywan wewntrz funkcji skadowych.

Klasy Stack i Stash jako szablony


Powracajce problemy prawa wasnoci", dotyczce klas kontenerw Stash i Stack, ktre byy rozwaane w ksice, wynikaj z faktu, e kontenery te nie mogy dokadnie wiedzie", jakiego dokadnie typu s przechowywane w nich obiekty. Najbliszym rozwizania problemu by przykad klasy Stack jako kontenera obiektw klasy Object", zaprezentowany w programie OStackTestcpp, zamieszczonym w rozdziale 15. W przypadku gdy klient-programista nie usunie jawnie wszystkich wskanikw do obiektw, przechowywanych w kontenerze, kontener powinien poprawnie usun te wskaniki. Innymi sowy, kontener jest wacicielem" wszystkich obiektw, ktre nie zostay z niego usunite, odpowiada zatem za ich usuniecie. Problem polega na tym, e usuwanie wymaga znajomoci typu obiektu, natomiast nie jest ona potrzebna do utworzenia oglnej klasy kontenerowej. Jednake w przypadku szablonw moemy napisa

564

Thlnking in C++. Edycja polska kod nieznajcy typw obiektw, bez trudu konkretyzujc nowe wersje kontenera dla kadego typu, ktry bdziemy chcieli w nim przechowywa. Indywidualnie skonkretyzowane kontenery znaj typ przechowywanych przez siebie obiektw, dlatego te mog wywoa dla nich waciwe destruktory (zakadajc w typowym przypadku, polegajcym na zastosowaniu polimorfizmu, e dostpne s wirtualne destruktory). W przypadku klasy Stack okazuje si to do proste, poniewa wszystkie jej funkcje skadowe mog by bez trudu zdefiniowane jako funkcje inline:

//: C16:TStack.h // Klasa Stack jako szablon #ifndef TSTACK_H fdefine TSTACK_H template<class T> class Stack { struct Link { T* data; Link* next; Link(T* dat. Link* nxt): data(dat), next(nxt) {} }* head; public: Stack() : head(0) {} ~Stack(){ while(head) delete pop(); } void push(T* dat) { head = new Link(dat. head); } T* peek() const { return head ? head->data : 0; } T* pop(){ if(head 0) return 0; T* result = head->data; Link* oldHead = head; head - head->next; delete oldHead;
return result;

#endif // TSTACK_H ///:Jeeli porwnamy powyszy plik z przykadem zamieszczonym w pliku nagwkowym OStack,h, znajdujcym si na kocu rozdziau 15., zobaczymy, e klasa Stack waciwie wcale si nie zmienia, z wyjtkiem tego, e klasa Object zostaa zastpiona przez T. Program testowy jest rwnie prawie taki sam, oprcz tego, e wyeliminowano w nim konieczno wielokrotnego dziedziczenia po klasach string oraz Object klasy okrelajcej typ przechowywanych obiektw (a nawet konieczno istnienia samej klasy Object). Poniewa nie ma obecnie klasy MyString, sygnalizujcej wywoanie swojego destruktora, dodano niewielk klas, dziki ktrej mona si przekona o tym, e kontener Stack usuwa znajdujce si w nim obiekty:

Rozdzia 16. << Wprowadzenie do szablonw //: C16:TStackTest.cpp //{T} TStackTest.cpp #include "TStack.h" #include "../require.h" #include <fstream> #include <iostream> #include <string> using namespace std; class X { public: virtual -X()

565

cout << "-X " << endl;

int main(int argc. char* argv[]) { requireArgs(argc. 1); // Argumentem jest nazwa pliku ifstream in(argv[l]); assure(in. argv[l]); Stack<string> textlines; string line; // Odczytywanie pliku i zapamitywanie wierszy na stosie: while(getline(in. line)) textlines.push(new string(line)); // Pobranie kilku wierszy ze stosu: string* s; for(int i = 0; i < 10: i++) { if((s - (string*)textlines.pop())==0) break: cout << *s << endl ; delete s;
} // Pozostae acuchy usunie destruktor. // Prezentacja poprawnej destrukcji : Stack<X> xx; for(int j = 0; j < 10; j++) xx.push(new X);

Destruktor klasy X jest destruktorem wirtualnym nie dlatego, e jest w tym przypadku niezbdny, ale poniewa obiekty tej klasy mogyby w przyszoci zosta wykorzystane do przechowywania obiektw klas dziedziczcych po klasie X. Zwrmy uwag na to, jak atwo mona, uywajc klasy Stack, tworzy rozmaite wersje stosw zarwno dla acuchw, jak i obiektw klasy X. Dziki szablonom poczylimy ze sob to, co najlepsze atwo uywania klasy Stack z zapewnieniem poprawnego usuwania obiektw.

Kontener wskanikw Stash, wykorzystujcy szablony


Przeksztacenie kodu klasy PStash w wersj wykorzystujc szablony niejestju tak proste, poniewa klasa ta zawiera kilka funkcji, ktre nie powinny by funkcjami inline. Jednake definicje tych funkcji w postaci szablonw nadal mog by umieszczone w pliku nagwkowym (kompilator oraz program czcy zapewni rozwizanie wszelkich problemw zwizanych z wielokrotnymi definicjami). Kod wyglda

Thinking in C++. Edycja polska bardzo podobnie do kodu zwykej klasy PStash, z wyjtkiem tego, e wielko, o ktr powikszany jest przydzielony obszar pamici (uywana przez funkcj inflate()), zostaa wczona do szablonu jako niebdcy klas parametr o okrelonej wartoci domylnej. Dziki temu jego warto moe by modyfikowana w miejscu konkretyzacji (oznacza to jednak, e warto ta jest staa mona rwnie twierdzi, e powinna istnie moliwo jej zmiany w czasie ycia obiektu): //: C16:TPStash.h #ifndef TPSTASH_H ldefine TPSTASH_H

temp1ate<c1ass T, int incr = 10> class PStash { int quantity; // Liczba elementw pamici int next; // Nastpny pusty element T** storage;

void inflate(int increase = incr); public: PStash() : quantity(0). next(0). storage(0) {} -PStash(); int add(T* element); T* operator[](int index) const; // Pobranie elementu // Usunicie odwoania do elementu: T* remove(int index);

// Liczba zapamitanych elementw: int count() const { return next; }

template<class T, int incr> int PStash<T. incr>::add(T* element) if(next >= quantity) inflate(incr); storage[next++] = element; return(next - 1); // Numer indeksu // Pozostawione obiekty s wasnoci kontenera: template<class T. int incr> PStash<T. incr>::~PStash() { for(int i - 0; i < next: i++) { delete storage[i]; // Zerowe wskaniki nie s problemem storage[i] = 0; // Dla pewnoci } delete []storage; template<class T. int incr> T* PStash<T. incr>::operator[](int index) const { require(index >= 0, "PStash::operator[] indeks ma wartosc ujemna"); if(index >- next) return 0; // Oznaczenie koca require(storage[index] != 0, "PStash::operator[] zwrcony pusty wskaznik"); // Tworzenie wskanika do danego elementu: return storage[index];

Rozdzia 16. Wprowadzenie do szablonw template<class T, 1nt incr> T* PStash<T. incr>::remove(int index) { // operator[] dokonuje sprawdzenia poprawnoci indeksu: T* v = operator[](index); // "Usunicie" wskanika: if(v != 0) storage[index] * 0; return v; template<class T, int incr> void PStash<T, incr>::inflate(int increase) { const int psz = sizeof(T*);

557

} endif // TPSTASH_H II/:-

T** st - new T*[quantity + increase]; memset(st. 0, (quantity + increase) * psz); memcpy(st. storage, quantity * psz); quantity += increase; delete []storage; // Stary obszar pamici storage - st; // Wskanik do nowego obszaru pamici

Domylna warto, o jak zwikszany jest przydzielony obszar pamici, jest niewielka, co gwarantuje wywoywanie funkcji inflate( ). Dziki temu moemy si upewni, e dziaa ona poprawnie. Aby sprawdzi kontrol prawa wasnoci w wersji klasy PStash, wykorzystujcej szablony, zostaa utworzona, widoczna poniej, klasa AutoCounter. Informuje ona o wywoaniu swojego konstruktora i destruktora, co pozwala na upewnienie si, e wszystkie utworzone obiekty zostay rwnie zniszczone. Klasa AutoCounter umoliwia wycznie tworzenie obiektw swojego typu na stercie:
#ifndef AUTOCOUNTER_H #define AUTOCOUNTER_H #include ". ./require.h"

//: C16:AutoCounter.h

#include <iostream> #include <set> // Kontener standardowej biblioteki C++ #include <string> class AutoCounter { static int count; int id; class CleanupCheck { std::set<AutoCounter*> trace; public: void add(AutoCounter* ap) { trace.insert(ap); 1 void remove(AutoCounter* ap) { require(trace.erase(ap) == 1. "Proba dwukrotnego usunicia obiektu klasy AutoCounter"); } ~CleanupCheck() { std::cout << "~CleanupCheck()"<< std::endl;

require(trace.size() == 0, "Nie wszystkie obiekty klasy AutoCounter zostay usuniete");

Thinking in C++. Edycja polska

// Zapobieganie przypisaniu i wywoaniu konstruktora kopiujcego: AutoCounter(const AutoCounter&); void operator=(const AutoCounter&); public: // Obiekty mona tworzy tylko za pomoc tej funkcji: static AutoCounter* create() { return new AutoCounter(); -AutoCounter() { std::cout << "usuwanie[" << id << "]" << std::endl; verifier.remove(this): } // Wydruk zarwno obiektw, jak i wskanikw: friend std::ostream& operator<<( std::ostream& os, const AutoCounter& ac){ return os << "AutoCounter " << ac.id;
friend std::ostream& operator<<( std::ostream& os, const AutoCounter* ac){ return os << "AutoCounter " << ac->id: #endif // AUTOCOUNTER_H lll:~

static CleanupCheck verifier; AutoCounter() : id(count++) { verifier.add(this); // Rejestracja samego siebie std::cout << "utworzony[" << id << "]" << std::endl; }

Klasa AutoCounter realizuje dwa zadania. Po pierwsze, kolejno numeruje kady utworzony obiekt klasy AutoCounter numer kadego z,nich jest przechowywany w skadowej id i jest on generowany za pomoc statycznej skadowej count. Drugie zadanie jest bardziej zoone. Statyczny egzemplarz zagniedonej khsy CleanupCheck (noszcy nazw verifier) zapamituje wszystkie tworzone i niszczone obiekty klasy AutoCounter i zgasza komunikat, w przypadku gdy nie wszystkie obiekty zostay przez nas usunite (czyli nastpi wyciek pamici). Dziaanie to jest realizowane za pomoc klasy set (zbir), pochodzcej ze standardowej biblioteki jzyka C++. Stanowi ona wspaniay przykad tego, jak przydatne s dobrze zaprojektowane szablony (o wszystkich kontenerach, zawartych w standardowej bibliotece klas jzyka C++, mona przeczyta w drugim tomie ksiki, dostpnym w Intemecie). Parametrem szablonu klasy set jest przechowywany typ w tym przypadku zosta on skonkretyzowany w celu przechowywania wskanikw w obiektach klasy AutoCounter. Klasa set pozwala na dodanie do zbioru tylko jednego egzemplarza kadego unikatowego obiektu w funkcji add( ) odbywa si to za pomoc wywoania funkcji set::insert(). Funkcja insert() w rzeczywistoci zwraca warto, ktra zawiera informacj o ewentualnej prbie dodania do zbioru czego, coju wczeniej zostao do niego doczone. Jednake w tym przypadku, poniewa dodajemy jedynie adresy obiektw, moemy polega na udzielanej przez jzyk C++ gwarancji, e wszystkie obiekty posiadajunikatowe adresy.

Rozdzia 16. Wprowadzenie do szablonw

569

W funkcji remove( ) do usunicia ze zbioru wskanika do obiektu klasy AutoCounter jest wykorzystywana funkcja set::erase( ). Zwracana warto informuje, ile egzemplarzy danego elementu zostao usunitych w naszym przypadku spodziewamy si tylko wartoci wynoszcej zero lubjeden. Jeelijednak warto wynosia zero, oznacza to, e obiekt zosta ju usunity ze zbioru i prbujemy wyeliminowa go z niego po raz drugi, co stanowi bd w programie, ktry zostanie zgoszony za porednictwem funkcji require( ). Destruktor klasy CIeanupCheck dokonuje ostatecznego sprawdzenia, upewniajc si, e wielko zbioru jest zerowa, czyli wszystkie obiekty zostay poprawnie usunite. Jeeli nie jest ona zerowa, oznacza to wyciek pamici, co jest sygnalizowana za pomoc funkcji require( ). Konstruktor oraz destruktor klasy AutoCounter odpowiednio rejestruje i wyrejestrowuje si, uywajc do tego celu obiektu verifier. Zwr uwag na to, e konstruktor, konstruktor kopiujcy oraz operator przypisania s prywatne, dziki czemu jedynym sposobem utworzenia obiektu jest wywoanie statycznej funkcji skadowej create( ). Stanowi to prosty przykad fabryki (ang. factory) i gwarantuje tworzenie wszystkich obiektw na stercie, dziki czemu obiekt verifier nie bdzie niepokojony przypisaniami i konstrukcjami realizowanymi za pomoc konstruktora kopiujcego. Poniewa wszystkie funkcje skadowe s funkcjami inline, jedynym powodem utworzenia pliku, zawierajcego implementacj, jest umieszczenie w nim definicji statycznych danych skadowych: //: C16:AutoCounter.cpp {0} // Definicje statycznych danych skadowych klasy #include "AutoCounter.h" AutoCounter: :CleanupCheck AutoCounter: :verifier; int AutoCounter::count = 0: Dysponujc klas AutoCounter, moemy obecnie przetestowa usugi, udostpniane przez klas PStash. Poniszy przykad nie tylko dowodzi tego, e destruktor klasy PStash usuwa wszystkie obiekty, ktre znajduj si aktualnie w jego posiadaniu, ale pokazuje te, w jaki sposb klasa AutoCounter wykrywa wszystkie nieusunite obiekty. //: C16:TPStashTest.cpp //{L} AutoCounter #include "AutoCounter.h" #include "TPStash.h" #include <iostream> #include <fstream> using namespace std; int main() { PStash<AutoCounter> acStash; for(int i - 0; i < 10; i++) acStash . add(AutoCounter : : create() ) ; cout << "Reczne usuniecie 5 elementow:" << endl; for(int j - 0; j < 5; j++) delete acStash.remove(j);

70

Thinking

in

C++.

Edycja

polska

// ... wymuszenie generacji komunikatw o bdach. cout << acStash.remove(5) << endl; cout << acStash.remove(6) << endl; cout << "Reszte usuwa destruktor:" << endl ; // Powtrzenie testw z wczeniejszych rozdziaw: ifstream in("TPStashTest.cpp"); assure(in, "TPStashTest.cpp"); PStash<string> stringStash; string line; while(getline(in. line)) stringStash.add(new string(line)); // Drukowanie acuchw: for(int u - 0; stringStash[u]; u++) cout << "stringStash[" << u << "] - " << *stringStash[u] << endl:

cout << "Usuniecie dwoch elementw bez ich niszczenia:" << endl ;

Gdy z kontenera PStash usuwany jest pity i szsty element klasy AutoCounter, odpowiedzialno za nie przejmuje uytkownik. Poniewajednak nigdy ich nie niszczy, powoduj one wyciekanie pamici, ktre jest wykrywane przez klas AutoCounter w trakcie pracy programu. Po uruchomieniu programu zauwaysz, e wywietlany komunikat o bdzie nie jest dostatecznie precyzyjny. Jeeli w swoich programach uywasz mechanizmu zaprezentowanego na przykadzie klasy AutoCounter, zapewne bdziesz chcia wywietli bardziej szczegowe informacje, dotyczce obiektw, ktre nie zostay usunite. W drugim tomie ksiki zaprezentowano bardziej wyrafinowane sposoby, ktre to umoliwiaj.

Przydzielanie i odbieranie prawa wasnoci


Powrmy do problemu prawa wasnoci. Kontenery przechowujce obiekty w postaci wartoci nie musz na og uwzgldnra prawa wasnoci obiektw, poniewa w oczywisty sposb s wacicielami przechowywanych w nich obiektw. Jeeli jednak kontener przechowuje wskaniki (co zdarza si w jzyku C++ czciej, gwnie z uwagi na polimorfizm), to jest bardzo prawdopodobne, e wskaniki te s jeszcze uywane w jakim innym miejscu programu. Usuwanie obiektu nie jest zatem konieczne, poniewa do usunitego obiektu mogyby si odwoywa inne wskaniki, znajdujce si w programie. Aby temu zapobiec, naley zastanowi si nad prawem wasnoci podczas projektowania i uywania kontenera. Wiele programw jest znacznie prostszych i nie wystpuje w nich problem zwizany z prawem wasnoci wskaniki do obiektw przechowuje pojedynczy kontener i s one wykorzystywane wycznie przez niego. W takich przypadkach prawo wasnoci jest okrelone bardz prosto wacicielem obiektw jest kontener.

Rozdzia 16. Wprowadzenie do szablonw

571

Najlepszym sposobem rozwizania problemu prawa wasnoci jest pozostawienie wyboru klientowi-programicie. Czsto jest to realizowane za pomoc argumentu konstruktora, ktry domylnie okrela prawo wasnoci (to najprostszy przypadek). Mog rwnie istnie funkcje typu pobierz" i ustaw", umoliwiajce sprawdzenie i zmian prawa wasnoci kontenera. Jeeli kontener posiada funkcj, usuwajc obiekty, stan prawa wasnoci na og wpywa na ten proces. A zatem opcje sterujce niszczeniem obiektw mona rwnie znale w funkcjach usuwajcych elementy z kontenera. Mona wyobrazi sobie dodanie danych, okrelajcych prawo wasnoci kadego obiektu znajdujcego si w kontenerze, dziki czemu byoby wiadomo, czy musi on zosta zniszczony. Stanowi to wariant zliczania odwoa, z wyjtkiem tego, e to kontener, a nie obiekt, zna liczb odwoa, wskazujcych kady obiekt:

#ifndef OWNERSTACK_H

//: C16:OwnerStack.h // Stos z prawem wasnoci okrelanym // podczas pracy programu

#define OWNERSTACK_H

template<c1ass T> class Stack { struct Link { T* data; Link* next; Link(T* dat, Link* nxt) : data(dat). next(nxt) {} }* head; bool own; public: Stack(bool own = true) : head(0). own(own) {} ~Stack(); void push(T* dat) { head = new Link(dat,head); } T* peek() const { return head ? head->data : 0; } T* pop(): bool ownsC) const { return own; } void owns(bool newownership) { own = newownership; } // Automatyczna konwersja typu - zwarca // wartosc true. jezeli stos nie jest pusty: operator bool() const { return head != 0; } template<class T> T* Stack<T>::pop() ifChead == 0) return 0; T* result = head->data; Link* oldHead = head; head = head->next; delete oldHead; return result:

Thlnking in C++. Edycja polska teraplate<class T> Stack<T>::~StackO { 1f(!own) return; while(head) delete pop(); #endif // OWNERSTACK_H ///:Domylnym zachowaniem kontenera jest niszczenie obiektw, ale mona je zmieni albo modyfikujc argument konstruktora, albo uywajc funkcji skadowych owns(), umoliwiajcych przydzielanie i odbieranie prawa wasnoci. Jak w przypadku wikszoci szablonw, caa implementacja jest zawarta w pliku nagwkowym. Poniej zamieszczono niewielki test, sprawdzajcy moliwoci kontenera, zwizane z prawem wasnoci: / / : C16:OwnerStackTest.cpp / / { L } AutoCounter #include "AutoCounter.h" #include "OwnerStack.h" #include"../require.h" #include <iostream> #include <fstream> #include <string> using namespace std; int main() { Stack<AutoCounter> ac; // Przydzielenie prawa wasnoci Stack<AutoCounter> ac2(false); // Odebranie prawa wasnoci AutoCounter* ap; for(int i = 0; i < 10; i++) { ap - AutoCounter:::create(); ac.push(ap);
if(i % 2 == 0)

ac2.push(ap);

while(ac2) cout << ac2.pop() << endl; // Nie jest konieczne niszczenie obiektw, // poniewa kontener ac jest "wacicielem" // wszystkich obiektw Kontener ac2 nie jest wacicielem umieszczanych w nim obiektw, w zwizku z czym nadrzdnym" kontenerem jest obiekt ac, ktry ponosi odpowiedzialno", zwizan z prawem wasnoci umieszczonych w nim obiektw. Jeeli w trakcie istnienia kontenera zachodzi potrzeba zmiany jego prawa wasnoci, to mona wykorzysta do tego celu funkcj owns(). Moliwa byaby rwnie indywidualna zmiana prawa wasnoci, tak by dotyczya ona kadego obiektu z osobna. Prawdopodobnie jednak uczyniaby ona rozwizanie problemu prawa wasnoci bardziej skomplikowanym ni sam problem.

Rozdzia 16. Wprowadzenie do szabtonw

573

Przechowywanie obiektw jako wartoci


W przypadku gdy nie dysponuje si szablonami, tworzenie kopii obiektw wewntrz oglnych kontenerw stanowi powany problem. Gdy uywa si szablonw, wszystko jest wzgldnie proste wystarczy okreli, e zamiast wskanikw przechowuje si obiekty:

//: C16:ValueStack.h // Stos przechowujcy wartoci obiektw #ifndef VALUESTACK_H #define VALUESTACK_H #include "../require.h" template<class T. int ssize = 100> class Stack { // Domylny konstruktor dokonuje inicjalizacji // obiektu dla kadego elementu zawartego w tablicy:

int top: public: Stack() : top(0) {} // Konstruktor kopiujcy kopiuje obiekty do tablicy: void push(const T& x) { require(top < ssize. "Zbyt wiele wywolan funkcji push()"); stack[top++] = x; } T peek() const { return stack[top]; } // Po pobraniu ze stosu, obiekty nadal istniej // - nie s ju tylko dostpne: T pop() { require(top > 0. "Zbyt wiele wywolan funkcji popO"); return stack[--top]: } }: #endif // VALUESTACK_H ///:Wikszo pracy wykonuje konstruktor kopiujcy obiektw, przechowywanych w kontenerze pobierajc i zwracajc obiekty przez warto. Wewntrz funkcji push() umieszczenie obiektu w tablicy obsugujcej stos jest dokonywane za pomoc funkcji T::operator=. Aby sprawdzi, czy kontener dziaa poprawnie, utworzono kias SetfCounter, zapamitujctworzenie obiektw oraz wywoania konstruktora kopiujcego: / / : C16:SelfCounter.h #ifndef SELFCOUNTER_H #define SELFCOUNTER_H #include "ValueStack.h" #include <iostream> class SelfCounter { static int counter;

T stack[ssize];

int id; public:

Thinking in C++. Edycja polska SelfCounter() : id(counter++) { std::cout << "Utworzony: " << id << std::endl; }

SelfCounter(const SelfCounter& rv) : id(rv.nd){ std::cout << "Skopiowany: " << id << std::endl;
SelfCounter operator=(const SelfCounter& rv) { std::cout << "Przypisany " << rv.id << " do " << id << std::endl; return *this; }

~SelfCounter() { std::cout << "Zniszczony: "<< id << std::endl;


friend std::ostream& operator<<( std::ostream& os, const SelfCounter& sc){ return os << "SelfCounter: " << sc.id;

#endif // SELFCOUNTER_H ///:/ / : C16:SelfCounter.cpp {0} #include "SelfCounter.h" int SelfCounter::counter - 0; / / / : / / : C16:ValueStackTest.cpp //{L} SelfCounter #include "ValueStack.h" #include "SelfCounter.h" #include <iostream> using namespace std; int main() { Stack<SelfCounter> sc; for(int i = 0: i < 10: i++) sc.push(SelfCounter());

for(int k = 0; k < 10: k++) cout << sc.pop() << endl; } III-

// Wywoanie funkcji peek() jest prawidowe, // wynik jest obiektem tymczasowym: cout << sc.peek() << endl;

Podczas tworzenia konteneru Stack dla kadego obiektu zawartego w tablicy jest wywoywany domylny konstruktor klasy, ktrej obiekty s przechowywane w kontenerze. Po uruchomieniu programu bez widocznego powodu jest tworzonych 100 obiektw klasy SelfCounter ale jest to jedynie inicjalizacja tablicy. Moe si ona okaza nieco kosztowna, ale nie ma sposobu, by jej unikn w tak prostym przykadzie. Pojawi si jeszcze bardziej zoone problemy, gdy uczyni si kontener Stack bardziej oglnym, pozwalajc mu na dynamiczne powikszanie swojego rozmiaru. W przedstawionej powyej implementacji wymagaoby to bowiem utworzenia nowej (wikszej) tablicy, skopiowania elementw zawartych w starej tablicy do nowej tablicy, a nastpnie usunicia starej tablicy (tak wanie dziaa klasa vector, zawarta w standardowej bibliotecejzyka C++).

Rozdzia 16. Wprowadzenie do szablonw

575

Wprowadzenie do iteratorw
lterator jest obiektem, ktry porusza si w obrbie kontenera innych obiektw. Udostpnia on za kadym razem pojedynczy, zawarty w kontenerze obiekt, nie zapewniajc jednak bezporedniego dostpu do implementacji tego kontenera. Iteratory stanowi standardowy sposb dostpu do elementw, niezalenie od tego, czy kontener umoliwia bezporedni dostp do swoich elementw. Iteratory s najczciej stosowane wraz z klasami kontenerw stanowi one podstawow koncepcj, zawart w projekcie i sposobie wykorzystywania standardowych kontenerwjzyka C++, ktre zostay wyczerpujco opisane w drugim tomie ksiki (mona go pobra z witryny http:/flielion.pUonline/thinking/index.html). Iteratory s rwnie rodzajem wzorcw projektowych, bdcych tematemjednego z rozdziaw drugiego tomu ksiki. Pod wieloma wzgldami iteratory s sprytnymi wskanikami" i, jak si przekonamy, naladuj one na og operacje realizowane przez wskaniki. Jednak, w odrnieniu od wskanikw, iteratory zostay zaprojektowane w bezpieczny sposb, wic znacznie mniej prawdopodobne jest w ich przypadku wykonanie operacji odpowiadajcej przekroczeniu zakresu indeksw tablicy (albo, jeeli ju si to zrobi, atwiej bdzie si mona o tym dowiedzie). Powrmy do pierwszego przykadu w rozdziale. Oto jak wyglda po dodaniu do niej prostego iteratora: //: C16:IterIntStack.cpp // Prosty stos liczb cakowitych z iteratorami //{L} fibonacci #include "fibonacci.h" #include "../require.h" #include <iostream> using namespace std: class IntStack { enum { ssize = 100 }; int stack[ssize]; public: IntStackC) : top(0) {} void push(int i) { require(top < ssize. "Zbyt wiele wywolan funkcji pushO"); stack[top++] - i;

int top;

int pop() {

require(top > 0. "Zbyt wiele wywolan funkcji popO"); return stack[--top];

friend class IntStackIter;

}:
// Iterator przypomina "sprytny" wskanik: class IntStackIter { IntStack& s; int index;

Thinking In C++. Edycja polska

public: IntStackIter(IntStack& is) : s(is). index(0) {} int operator++() { // Przedrostkowy require(index < s.top. "iterator przesunity poza zakres"): return s.stack[++index]; }
int operator++(int) { // Przyrostkowy require(index < s.top, "iterator przesunity poza zakres");

return s.stack[index++];

int main() { IntStack is; for(int i - 0; i < 20; i++) is.push(fibonacci(i)); // Przejcie przez kontener za pomoca iteratora: IntStackIter it(is); for(int j - 0; j < 20; j++) cout << it++ << endl ; Iterator IntStackIter zosta utworzony wycznie w celu wsppracy z kontenerem IntStack. Zwr uwag na to, e klasa IntStackIter jest klas zaprzyjanion (friend) klasy IntStack, co daje jej dostp do prywatnych elementw klasy IntStack. Podobnie jak w przypadku wskanika, zadaniem klasy IntStackIter jest poruszanie si w obrbie obiektu klasy IntStack i zwracanie wartoci. W tym prostym przykadzie iterator IntStackIter potrafi przemieszcza si tylko do przodu (uywajc zarwno przedrostkowej, jak i przyrostkowej postaci operatora ++). Nie.obowizuj jednak adne ograniczenia dotyczce tego, w jaki sposb iterator moe by zdefiniowany, z wyjtkiem tych zwizanych z kontenerem, z ktrym wsppracuje. Jest jak najbardziej moliwe (z uwzgldnieniem ogranicze, narzuconych przez kontener), e iterator porusza si w dowolnym kierunku w obrbie stowarzyszonego ze sob kontenera, a take e umoliwia modyfikacj elementw przechowywanych przez kontener. Tradycyjnie iterator jest tworzony za pomoc konstruktora, ktry przycza go do obiektu pojedynczego kontenera; w czasie swojego ycia iterator nie jest rwnie zazwyczaj przyczany do adnego innego kontenera (iteratory s zazwyczaj mae i niewiele kosztuj" zawsze mona wic w atwy sposb utworzy nowy iterator). Korzystajc z iteratora, mona przej przez elementy umieszczone na stosie, nie pobierajc ich w taki sam sposb, wjaki wskanik przebiega przez kolejne elementy tablicy. Jednake iterator zna wewntrzn struktur stosu i sposb, w jaki mona porusza si w obrbie jego elementw. Mimo e poruszamy si w obrbie stosu, majc zudzenie Jnkrementacji wskanika", w rzeczywistoci podejmowane s bardziej skomplikowane dziaania. Oto istota iteratorw upraszczaj one zoony proces, polegajcy na przemieszczaniu si od jednego elementu kontenera do drugiego, przedstawiajc go w sposb, ktry przypomina uywanie wskanika. Celem jest, by kady iterator zawarty w naszym programie posiada taki sam interfejs, dziki czemu

Rozdzia 16. Wprowadzenie do szablonw

577

kod wykorzystujcy iterator nie musi zwraca uwagi na to, co on wskazuje. Wie" p>o prostu, e moe zmienia pozycje wszystkich iteratorw w taki sam sposb, co sprawia, e kontener, wskazywany przez iterator, staje si niewany. Mona zatem tworzy bardziej oglny kod. Wszystkie kontenery i algorytmy, zawarte w standardowej bibliotece jzyka C++, opieraj si na tej zasadzie dotyczcej iteratorw. Aby wszystko uczyni nieco bardziej oglnym, naleaoby stwierdzi: kady kontener posiada zwizan ze sob klas, noszc nazw iterator". Jednake w typowym przypadku spowodowaoby to problemy, zwizane z nazwami. Rozwizaniem jest dodanie do klasy kadego kontenera zagniedonego iteratora (zwr uwag na to, e w tym przypadku nazwa ,4terator" rozpoczyna si ma liter, dziki czemu jest zgodna z konwencj stosowan w standardowej bibliotece C++). Poniej przedstawiono wersj programu IterIntStack.cpp, w ktrej w klasie kontenera zagniedono iterator:

//: C16:NestedIterator.cpp // Zagniedenie iteratora w kontenerze //{L} fibonacci #include "fibonacci.h" #include "../require.h" #include <iostream> #include <string> using namespace std; class IntStack { enum { ssize = 100 }; int stack[ssize]; int top; public: IntStack() : top(0) {} void push(int i) { require(top < ssize. "Zbyt wiele wywolan funkcji push()"); stack[top++] = i: } int pop() { require(top > 0, "Zbyt wiele wywolan funkcji popO"); return stack[--top]; } class iterator; friend class iterator; class iterator { IntStack& s; int index; public: iterator(IntStack& is) : s(is), index(0) {} // Do utworzenia iteratora-wartownika // bdcego "znacznikiem koca": iterator(IntStack& is. bool) : s(is), index(s.top) {} int current() const { return s.stack[index]; } int operator++() { // Przedrostkowy require(index < s.top. "iterator przesunity poza zakres"); return s.stack[++index];

Thinking in C++. Edycja polska int operator++(int) { // Przyrostkowy require(index < s.top, "iterator przesunity poza zakres"); return s.stack[index++];

friend ostream& operator<<(ostream& os, const iterator& it) { return os << it.current(); iterator begin() { return iterator(*this): } // Tworzenie "znacznika koca": iterator end() { return iterator(*this. true);} int main() { IntStack is; for(int i = 0; i < 20; i++) is.push(fibonacci(i));

// Przeskoczenie iteratorem do przodu: iterator& operator+=(int amount) { require(index + amount < s.top, "IntStack: :iterator: :operator+=O " "proba przekroczenia zakresu"); index += amount; return *this; } // Aby sprawdzi, czy znajdujemy si na kocu: bool operator==(const iterator& rv) const { return index ~ rv.index; } bool operator!=(const iterator& rv) const { return index != rv.index: }

cout << "Przejcie przez caly kontener IntStack\n"; IntStack::iterator it - is.beginO; while(it != is.endO) cout << it++ << endl ; cout << "Przejcie przez czesc kontenera IntStack\n" IntStack: :iterator start is.beginO, end - is.begin(); start += 5, end += 15; cout << "pocztek - " << start << endl ; cout << "koniec = " << end << endl : while(start !- end) cout << start++ << endl ;

Podczas tworzenia zagniedonej, zaprzyjanionej (friend) klasy naley najpierw zadeklarowajej nazw, nastpnie zadeklarowajjako klas zaprzyjanion, a dopiero potem zdefiniowa sam klas. W innym przypadku kompilator tego nie zrozumie. Iterator wzbogacono o nowe elementy. Funkcja skadowa current( ) zwraca element kontenera, ktry jest aktualnie wybrany przez iterator. Mona przeskoczy" iteratorem do przodu, o dowoln liczb elementw, wykorzystujc do tego celu operator+=. Dostpne s rwnie dwa przecione operatory: == i !=, porwnujce ze sob

Rozdzia 16. * Wprowadzenie do szablonw

579

dwa iteratory. Mog one zestawia ze sob dwa dowolne obiekty klasy IntStack:: iterator, ale utworzono je gwnie z myl o sprawdzeniu, czy iterator znajduje si na kocu cigu elementw w taki sam sposb, jak czyni to prawdziwe" iteratory standardowej biblioteki jzyka C++. Pomys polega na tym, e dwa iteratory wyznaczaj zakres, obejmujcy pierwszy element wskazywany przez pierwszy iterator, ale nieobejmujcy ostatniego elementu, wskazywanego przez drugi z iteratorw. Jeeli zatem chcemy przej przez zakres elementw, wyznaczony przez te dwa iteratory, powinnimy zrobi to w nastpujcy sposb: while(start != end) cout << start++ << endl; gdzie start i end s dwoma iteratorami, wyznaczajcymi zakres. Zwr uwag, e iterator end, nazywany czsto wartownikiem albo znacznikiem koca (ang. end sentinel), niejest wyuskiwany; informuje nasjedynie, e znajdujemy siju na kocu sekwencji. Tak wic reprezentuje on element nastpny po ostatnim". Zazwyczaj zamierzamy przechodzi przez wszystkie elementy znajdujce si w kontenerze, dlatego te musi istnie jaki sposb umoliwiajcy kontenerowi utworzenie iteratorw, oznaczajcych pocztek sekwencji oraz znacznik koca. W tym przypadku, podobnie jak w standardowej bibliotece jzyka C++, iteratory te s zwracane przez funkcje skadowe kontenera, noszce nazwy begin() oraz end(). Funkcja begin( ) wykorzystuje pierwszy konstruktor klasy iterator, ktry domylnie wskazuje pocztek kontenera Qest to pierwszy element umieszczony na stosie). Jednake niezbdny jest rwnie drugi konstruktor, wykorzystywany przez funkcj end(), ktry umoliwia utworzenie iteratora, bdcego znacznikiem koca. Znajdowanie si na kocu" oznacza w tym przypadku wskazywanie szczytu stosu, poniewa skadowa top wyznacza zawsze nastpn dostpn (ale nieuywan) pozycj na stosie. Konstruktor ten pobiera drugi argument typu bool, bdcy jedynie fikcyjnym argumentem, umoliwiajcym rozrnienie obu konstruktorw. Do wypenienia kontenera IntStack w funkcji main() uyto ponownie generatora liczb cigu Fibonacciego, natomiast do poruszania si w obrbie caego kontenera (a take wszego zakresu znajdujcych si w nim elementw) wykorzystano iteratory. Nastpnym krokiem jest, oczywicie, uczynienie kodu bardziej oglnym. Uywamy do tego celu szablonw do sparametryzowania typu elementw, przechowywanych w kontenerze. Dziki temu nie musimy przechowywa w nim liczb cakowitych, a zatem moemy umieszcza w nim elementy dowolnego typu:
/ / : C16:IterStackTemplate.h // Prosty szablon stosu z zagniedonym iteratorem #ifndef ITERSTACKTEMPLATE_H #define ITERSTACKTEMPLATE_H #include "../require.h"

#include <iostream>

template<class T, int ssize - 100> class StackTemplate { T stack[ssize]; int top; public:

Thinking in C++. Edycja polska

StackTemplate() : top(0) {} void push(const T& i) {

} T pop() {
}

stack[top++] - i;

require(top < ssize, "Zbyt wiele wywolan funkcji pushO");

require(top > 0, "Zbyt wiele wywolan funkcji popO"); return stack[--top];

class iterator; // Wymagana deklaracja friend class iterator; // Uczy klas przyjacielem class iterator { // A teraz j zdefiniuj StackTemplate& s; int index; public: iterator(StackTemplate& st): s(st),index(0){} // Do utworzenia iteratora-wartownika // bdcego "znacznikiem konca": iterator(StackTemplate& st, bool) : s(st). index(s.top) {} T operator*() const { return s.stack[index];} T operator++() { // Posta przedrostkowa require(index < s.top, "iterator przesunity poza zakres"); return s.stack[++index]; } T operator++(int) { // Posta przyrostkowa require(index < s.top, "iterator przesunity poza zakres"); return s.stack[index++]; } // Przeskoczenie iteratorem do przodu: iterator& operator+=(int amount) { require(index + amount < s.top, " StackTemplate::iterator::operator+-O " "proba przekroczenia zakresu"); index + amount; return *this; } // Aby sprawdzi, czy znajdujemy si na kocu: bool operator~(const iterator& rv) const { return index rv.index; } bool operator!-(const iterator& rv) const { return index !- rv.index; } friend std::ostream& operator<<( std::ostream& os. const iterator& it) { return os << *it;
iterator begin() { return iterator(*this); } // Tworzenie "znacznika koca": iterator end() { return iterator(*this. true);}

}: &ndif // ITERSTACKTEMPLATE H ///:-

Rozdzia 16. << Wprowadzenie do szablonw

581

Przejcie ze zwykej klasy do szablonu odbywa si wic w do przejrzysty sposb. Postpowanie polegajce na utworzeniu i uruchomieniu najpierw zwykej klasy, a nastpnie na przeksztaceniu jej w szablon jest na og uwaane za atwiejsze ni tworzenie szablonu od podstaw. Zwr uwag na to, e zamiast napisa po prostu: friend iterator; // Uczy klas przyjacielem w programie uyto postaci: friend class iterator; // Uczy klas przyjacielem Jest to istotne, poniewa nazwa iterator" znajduje si ju w zasigu wyznaczonym przez doczony plik. Zamiast funkcji skadowej current() klasa iterator posiada operator*, umoliwiajcy wybr biecego elementu. Dziki temu iterator przypomina raczej wskanik to czsto stosowana praktyka. Poniej zamieszczono poprawiony program przykadowy, umoliwiajcy przetestowanie szablonu:

//: C16:IterStackTemplateTest.cpp //{L} fibonacci #i nc1ude "fi bonacci.h" #include "IterStackTemplate.h" #include <iostream> #include <fstream> #include <string> using namespace std; int main() { StackTemplate<int> is; for(int i = 0; i < 20; i++) is.push(fibonacci(i)); // Przejcie za pomoc iteratora: cout << "Przejcie przez caly kontener StackTemplate\n"; StackTemplate<int>::iterator it = is.begin(); while(it != is.endO) cout << it++ << endl; cout << "Przejcie przez czesc kontenera\n"; StackTemplate<int>::iterator start = is.begin(), end = is.begin(); start += 5, end += 15; cout << "pocztek = " << start << endl; cout << "koniec = " << end << endl; while(start !- end) cout << start++ << endl; ifstream in("IterStackTemplateTest.cpp"); assure(in. "IterStackTemplateTest.cpp"); string 1ine; StackTemplate<string> strings; while(getline(in, line)) strings.push(line); StackTemplate<string>::iterator

Thinking in C++. Edycja polska

sb - strings.begin(), se = strings.end(); while(sb != se) cout << sb++ << endl ; Pierwsze uycie iteratora polegajedynie na przejciu od pocztku do koca kontenera (co pokazuje, e znacznik koca dziaa prawidowo). W przypadku jego drugiego uycia widzimy, jak atwo mona, dziki wykorzystaniu iteratorw, okreli zakres elementw kontenera (kontenery i iteratory, zawarte w standardowej bibliotece jzyka C++, niemal wszdzie wykorzystuj rozumiane w ten sposb pojcie zakresu). Przeciony operator+ przenosi iteratory start i end do pozycji znajdujcych si wewntrz zakresu elementw, umieszczonych w kontenerze is, a nastpnie elementy te s drukowane. Zwr uwag na to, e element wskazywany przez znacznik koca nie naley ju do zakresu, dlatego te, by poinformowa nas o przekroczeniu zakresu, moe on wskazywa element znajdujcy si tu za jego kocem. Nie naley jednak wyuskiwa znacznika koca, bo skoczy si to wyuskiwaniem zerowego wskanika (w klasie StackTemplate::iterator umieciem zapobiegajce temu zabezpieczenie, ale w kontenerach i iteratorach, znajdujcych si standardowej bibliotece C++ ze wzgldu na efektywno nie ma takiego kodu, wic trzeba zwrci na to uwag). Aby wreszcie zweryfikowa, e kontener StackTempIate dziaa rwnie z obiektami klas, jest konkretyzowany jego egzemplarz dla klasy string. Nastpnie jest on wypeniany wierszami pochodzcymi z pliku rdowego, ktre s na kocu drukowane.

asa Stack z iteratorami


Zaprezentowany proces moemy powtrzy w przypadku dynamicznie powikszanej klasy Stack, ktra bya przedstawiona jako przykad w caej ksice. Poniej zaprezentowano klas Stack, zawierajcdoczony do niej, zagniedony iterator: //: C16:TStack2.h // Szablon klasy Stack, zawierajcy zagniedony iterator #ifndef TSTACK2_H #define TSTACK2_H template<class T> class Stack { struct Link { T* data; Link* next; Link(T* dat, Link* nxt) : data(dat), next(nxt) {} }* head; public: Stack() : head(0) {} -Stack(); void push(T* dat) { head = new Link(dat. head); } T* peek() const { return head ? head->data : 0; } T* pop(); // Zagniedona klasa iteratora: class iterator; // Wymagana deklaracja

Rozdzia 16. * Wprowadzenie do szablonw

583

friend class iterator; // Uczy klas przyjacielem class iterator { // A teraz j zdefiniuj Stack::Link* p; public:

iterator(const Stack<T>S tl) : p(tl.head) {} // Konstruktor kopiujcy: iterator(const iterator&tl) : p(tl.p) {} // Iterator bdcy znacznikiem koca: iterator() : p(0) {} // operator++ zwraca warto logiczn sygnalizujc koniec: bool operator++() { if(p->next) p = p->next; else p = 0; // Koniec listy return bool(p); } bool operator++(int) { return operator++(): } T* current() const { if(!p) return 0; return p->data; } // Operator wyuskania wskanika: T* operator->() const { require(p !- 0, "PStack::iterator::operator-> zwrocil 0"): return current(); } T* operator*() const { return current(); } // Konwersja do typu bool, do testw warunkw: operator bool() const { return bool(p): } // Porwnanie, umoliwiajce wykrycie koca: bool operator==(const iterator&) const { return p == 0; } bool operator!=(const iterator&) const { return p != 0;

iterator begin() const { return iterator(*this); } iterator end() const { return iteratorO; } template<class T> Stack<T>::~StackO while(head) delete pop(); template<class T> T* Stack<T>::pop() { if(head == 0) return 0; T* result - head->data; Link* oldHead = head; head = head->next; delete oldHead; return result; }

#endif // TSTACK2 H ///:-

Thinking in C++. Edycja polska Jak mona rwnie zauway, klasa zostaa zmodyfikowana w taki sposb, by obsugiwa prawo wasnoci. Dziaa ono obecnie poprawnie, poniewa klasa zna dokadnie typ obiektw, przechowywanych w kontenerze (albo przynajmniej ich typ podstawowy, w przypadku ktrego wszystko bdzie dziaa pod warunkiem wykorzystania wirtualnych destruktorw). Kontener domylnie niszczy pozostawione w nim obiekty, ale jestemy odpowiedzialni za zniszczenie wszystkich wskanikw, ktre pobralimy z kontenera. Iterator jest prosty i fizycznie zajmuje bardzo mao miejsca ma wielko pojedynczego wskanika. Gdy tworzony jest obiekt klasy iterator, jest inicjalizowany gow listy powizanej i mona go jedynie przesuwa do przodu, wzdu listy. Jeeli zamierzamy zacz od nowa, od pocztku listy, tworzymy nowy iterator; jeeli natomiast chcemy zapamita jak pozycj listy, na podstawie istniejcego iteratora, wskazujcego t pozycj, tworzymy nowy iterator (uywajc do tego celu konstruktora kopiujcego iteratora). Do wywoania funkcji obiektu, wskazywanego przez iterator, moemy wykorzysta funkcj current(), operator* lub operator wyuskania wskanika -> (czsto wystpujcy wrd iteratorw). Implementacja ostatniego z wymienionych operatorw wyglda jak funkcja current(), poniewa zwraca wskanik do biecego obiektu. Jest jednak zupenie od niej inna, gdy operator wyuskania wskanika realizuje dodatkowe poziomy wyuskania (zob. rozdzia 12.). Klasa iterator odpowiada wzorcowi, znanemu z poprzedniego przykadu. Zostaa ona osadzona wewntrz klasy kontenera; zawiera konstruktory, tworzce zarwno iterator wskazujcy element w kontenerze, jak i iterator stanowicy ,^nacznik kocowy". Z kolei klasa kontenera posiada funkcje begin() oraz end(), umoliwiajce utworzenie tych iteratorw. (Gdy dowiesz si wicej na temat standardowej biblioteki jzyka C++, zobaczysz, e uywane tutaj nazwy iterator, begin(.) oraz end() wywodz si w oczywisty sposb ze standardowych klas kontenerowych. Pod koniec rozdziau przekonasz si, e dziki nim zamieszczone tutaj klasy kontenerw mog by uywane w taki sposb, jakby byy klasami standardowej biblioteki kontenerw jzyka C++). Caa implementacja kontenera zostaa zawarta w pliku nagwkowym; nie ma wic oddzielnego pliku cpp. Poniej przedstawiono niewielki test, sprawdzajcy dziaanie iteratora: //: C16:TStack2Test.cpp #include "TStack2.h" #include "../require.h" #include <iostream> #include <fstrearn> #include <string> using namespace std; int mainC) { ifstream file("TStack2Test.cpp"); assure(file. "TStack2Test.cpp"); Stack<string> textlines; // Odczytywanie pliku i zapamitywanie wierszy na stosie: string line: while(getline(file. line))

Rozdzia 16. Wprowadzenie do szablonw

585

textl1nes.push(new string(11ne)); int i = 0; // Uycie iteratora do wydrukowania wierszy // znajdujcych si na licie: Stack<string>::iterator it = textlines.begin(); Stack<string>::iterator* it2 = 0; while(it != textlines.endO) { cout << it->c_str() << endl; it++; if(++i == 10) // Zapamitanie 10. wiersza it2 - new Stack<string>::iterator(it); } cout << (*it2)->c_str() << endl; delete it2; Klasa Stack jest konkretyzowana w celu przechowywania obiektw klasy string, a nastpnie wypeniana wierszami odczytanymi z pliku. Nastpnie tworzony jest iterator, wykorzystywany do poruszania si w obrbie utworzonego cigu elementw. Dziesity wiersz pliku jest zapamitywany dziki wygenerowaniu za pomoc konstruktora kopiujcego drugiego iteratora, utworzonego na podstawie pierwszego. Pniej ten wiersz jest drukowany, a utworzony dynamicznie iterator niszczony. W tym przypadku do kontroli czasu ycia obiektu wykorzystano dynamiczne tworzenie obiektw.

Klasa PStash z iteratorami


Posiadanie iteratorw ma sens w przypadku wikszoci klas kontenerw. Poniej pokazano iterator dodany do klasy PStash:

//: C16:TPStash2.h // Szablon klasy PStash, zawierajcy zagniedony iterator #ifndef TPSTASH2_H #define TPSTASH2_H #include ".,/require.h" #include <cstdlib> template<class T. int incr = 20> class PStash { int quantity; int next; T** storage; void inflate(int increase - incr); public: PStashO : quantity(0). storage(0), next(0) {} ~PStash(); int add(T* element); T* operator[](int index) const; T* remove(int index); int count() const { return next: } // Zagniedona klasa iteratora: class iterator; // Wymagana deklaracja friend class iterator; // Uczy klas przyjacielem class iterator { // A teraz j zdefiniuj

Thinking in C++. Edycja polska PStash& ps; int index; public: iterator(PStash& pStash) : ps(pStash). index(0) {} // Do utworzenia iteratora-wartownika // bdcego "znacznikiem koca": iterator(PStash& pStash, bool) : ps(pStash). index(ps.next) {} // Konstruktor kopiujcy: iterator(const iterator& rv) : ps(rv.ps), index(rv.index) {} iterator& operator=(const iterator& rv) { ps - rv.ps; index = rv.index; return *this; } iterator& operator++() { require(++index <- ps.next, "PStash::iterator::operator++ " "przesunal indeks poza zakres"); return *this; } iterator& operator++(int) { return operator++(); }

iterator& operator--() { require(--index >= 0, "PStash::iterator::operator-- " "przesunal indeks poza zakres"); return *this; } iterator& operator--(int) { return operator--(); } // Przeskoczenie iteratorem do przodu lub tyu: iterator& operator+=(int amount) { require(index + amount < ps.next && index + amount >= 0, "PStash::iterator::operator+= " "proba uzyskania indeksu poza zakresem"); index +- amount; return *this; } iterator& operator-=(int amount) { require(index - amount < ps.next && index - amount >= 0. "PStash::iterator::operator-= " "proba uzyskania indeksu poza zakresem"); index -= amount; return *this; } // Utworzenie nowego iteratora. przesunitego do przodu iterator operator+(int amount) const { iterator ret(*this); ret += amount; // operator+= sprawdza zakres return ret;

Rozdzia 16. << Wprowadzenie do szablonw

587

T* current() const { return ps.storage[index]; T* operator*() const { return current(); } T* operator->() const { require(ps.storage[index] != 0, "PStash::iterator::operator-> zwrocil 0"); return current(); } // Usunicie biecego elementu: T* remove(){ return ps.remove(index); } // Testy porwna do wykrycia koca: bool operator==(const iterator& rv) const { return index rv.index; }
bool operator!=(const iterator& rv) const { return index !^ rv.index;

iterator begin() { return iterator(*this): } iterator end() { return iterator(*this, true);} // Usuwanie obiektw zawartych w kontenerze: template<class T, int incr> PStash<T, incr>: :~PStash() { for(int i = 0; i < next; i++) { delete storage[i]; // Zerowe wskaniki nie s problemem storage[i] = 0; // Dla pewnoci } delete []storage;
template<class T, int incr> int PStash<T. incr>::add(T*element) if(next >= quantity) inflate();

storage[next++] - element; return(next - 1); // Numer indeksu

template<class T. int incr> inline T* PStash<T. incr>::operator[](int index) const { require(index >= 0. "PStash::operator[] indeks ma wartosc ujemna"); if(index >= next) return 0; // Oznaczenie koca require(storage[index] != 0, "PStash::operator[] zwrcony pusty wskaznik"); return storage[index]; template<class T. int incr> T* PStash<T, incr>::remove(int index) // operator[] sprawdza zakres:

Thinking in C++. Edycja polska


T* v - operator[](index); // "Usunicie" wskanika: storage[index] = 0; return v; template<class T, int incr> void PStash<T. incr>::inf1ate(int increase) { const int tsz = sizeof(T*);

} #endif // TPSTASH2_H Ill:~

T** st = new T*[quantity + increase]; memset(st. 0, (quantity + increase) * tsz); memcpy(st, storage. quantity * tsz); quantity += increase; delete []storage; // Stary obszar pamici storage = st; // Wskanik do nowego obszaru pamici

Wiksz cz programu stanowi do oczywiste przeniesienie poprzedniej wersji klasy PStash oraz zagniedonego iteratora do postaci szablonu. Jednake tym razem operatory zwracaj referencje do biecego iteratora, co jest sposobem bardziej typowym i elastycznym. Destruktor wywouje operator delete dla wszystkich wskanikw przechowywanych w kontenerze, a poniewa ich typ jest okrelony za pomoc szablonu, destrukcja jest dokonywana w prawidowy sposb. Naley pamita o tym, e jeeli kontener przechowuje wskaniki do obiektw klasy podstawowej, klasa ta powinna posiada wirtualny destruktor, zapewniajcy poprawne usuwanie obiektw klas pochodnych, ktrych adresy s rzutowane w gr podczas umieszczania ich w kontenerze. Klasa PStash::iterator odpowiada modelowi operatora, zwizanego przez cay czas ycia z jednym obiektem kontenera. Ponadto konstruktor kopiujcy pozwala na powstanie nowego iteratora. Wskazuje on ten sam element, co istniejcy iterator, na podstawie ktrego jest tworzony. W efekcie w kontenerze zostaje utworzona zakadka. Funkcje skadowe operator+= oraz operator-= pozwalaj na przeniesienie iteratora o okrelon liczb pozycji, zwracajc uwag na granice kontenera. Przecione operatory inkrementacji i dekrementacji przesuwaj iterator ojednpozycj. Funkcja skadowa operator+ zwraca nowy iterator, przesunity do przodu o wielko skadnika. Podobnie jak w poprzednim przykadzie, do wykonywania operacji na elemencie wskazywanym przez iterator uywane s operatory wyuskania wskanika, natomiast funkcja remove() niszczy biecy obiekt, wywoujc funkcj remove() kontenera. Kod tego samego rodzaju, co poprzednio (w rodzaju kontenerw standardowej biblioteki C++) jest uywany do utworzenia znacznika koca funkcja skadowa end( ) kontenera oraz operatory == i !=, suce do porwna. W zamieszczonym poniej przykadzie s tworzone i testowane dwie rne wersje obiektw klasy Stash jedna dla obiektw klasy o nazwie Int, informujca o wywoaniu swoich konstruktorw i destruktorw, a druga przechowujca obiekty klasy string, zawartej w bibliotece standardowej:

Rozdzia 16. Wprowadzenie do szablonw

589

//: C16:TPStash2Test.cpp include "TPStash2.h" finclude ",./require.h" include <iostream> #include <vector> #include <string> using namespace std; class Int { int i; public: Int(int ii = 0) : i(ii) { cout << ">" << i << ' '; } ~IntO { cout << "~" << i << ' '; } operator int() const { return i; } friend ostream& operator<<{ostream& os. const Int& x) { return os << "Int: " << x.i; }
friend ostream& operator<<(ostream& os, const Int* x) { return os << "Int: " << x->i:

int main() { { // Wymuszenie wywoania destruktora PStash<Int> ints: for(int i = 0; i < 30; i++) ints.add(new Int(i)); cout << endl; PStash<Int>::iterator it = ints.begin(); it +- 5; PStash<Int>::iterator it2 = it + 10; for(; it !- it2: it++) delete it.remove(); // Domylne usuwanie cout << endl; for(it - ints.begin();it !- ints.end();it++) if(*it) // Funkcja remove() powoduje powstawanie "dziur" cout << *it << endl; } // W tym miejscu wywoywany jest destruktor obiektu "ints" cout << "\n \n"; i fstream i n("TPStash2Test.cpp"); assure(in. "TPStash2Test.cpp"); // Konkretyzacja dla klasy string: PStash<string> strings; string line; while(getline(in, line)) strings.add(new string(line)); PStash<string>::iterator sit = strings.begin(); for(; sit !- strings.end(); sit++) cout << **sit << endl; sit - strings.begin(); int n = 26; sit += n; for(; sit != strings.end(): sit++) cout << n++ << ": " << **sit << endl;

)0

Thinking in C++. Edycja polska Dla uatwienia klasa Int posiada zwizany ze sob operator<< klasy ostream zarwno dla argumentw typu Int&, jak i Int*. Pierwszy blok kodu, znajdujcy si w funkcji main(), jest zawarty w nawiasie klamrowym, by wymusi destrukcj obiektu klasy PStash<Int> i zarazem automatyczne usuwanie obiektw przez destruktor tej klasy. Pewien zakres elementw jest pobierany z kontenera i usuwany rcznie, by pokaza, e kontener PStash sam usunie pozostae elementy. Dla obu konkretyzacji klasy PStash s tworzone iteratory, wykorzystywane do poruszania si w obrbie kontenera. Zwr uwag na elegancj uzyskan dziki uyciu tych konstrukcji nie jestemy przytaczani szczegami implementacyjnymi, zwizanymi z uywaniem tablicy. Informujemy jedynie kontener i iterator co maj robi, a nie w jaki sposb. Dziki temu uzyskane rozwizanie jest atwiejsze do zrozumienia, zbudowania i modyfikacji.

>laczego iteratory?
Powyej zostay przedstawione przykady iteratorw, ale zrozumienie, dlaczego s one takie wane, wymaga bardziej zoonego przykadu. W prawdziwym programie obiektowym czsto spotyka si, wykorzystywane razem polimorfizm, dynamiczne tworzenie obiektw oraz kontenery. Kontenery oraz dynamiczne tworzenie obiektw rozwizuj problem, polegajcy na nieznajomoci liczby i typw niezbdnych obiektw. Ponadto jeeli kontener jest skonfigurowany w taki sposb, e przechowuje wskaniki do obiektw klasy podstawowej, to za kadym razem, gdy w kontenerze umieszczanyjest wskanik do ob"iektu klasy pochodnej, odbywa si rzutowanie w gr (z towarzyszc mu organizacj kodu i korzyciami, wynikajcymi z rozszerzalnoci). Zamieszczony poniej przykad, ostatni program w pierwszym tomie ksiki, czy ze sob rozmaite aspekty wszystkiego, co poznalimy do tej pory. Jeeli rozumiesz dziaanie tego programu, jeste gotowy, by przystpi do lektury drugiego tomu ksiki. Zamy, e tworzymy program, pozwalajcy uytkownikowi na edycj i tworzenie rnego rodzaju rysunkw. Kady rysunek jest obiektem, zawierajcym kolekcj obiektw klasy Shape:
/ / : C16:Shape.h #ifndef SHAPE_H #define SHAPE_H #include <iostream> #include <string>
class Shape { public: virtual void draw() = 0; virtual void eraseO = 0; virtual ~Shape() {}

Rozdzia 16. * Wprowadzenie do szablonw

591

class Circle public Shape { public: Circle() {} ~Circle() { std::cout << "Circle::-Circle\n"; void draw() { std::cout << "Circle::draw\n";J void erase() { std::cout << "Circle::erase\n" class Square public Shape public: Square() {} -Square() { std::cout << "Square::~Square\n": void draw() { std::cout << "Square::draw\n";} void erase() { std::cout << "Square::erase\n"; class Line : public Shape { public: Line() {} ~Line() { std::cout << "Line::~Line\n"; } void draw() { std::cout << "Line::draw\n";} void erase() { std::cout << "Line::erase\n";} }:

#endif // SHAPE_H ///:-

W powyszym pliku nagwkowym wykorzystano klasyczn struktur funkcji wirtualnych, znajdujcych si w klasie podstawowej i zasonitych w klasie pochodnej. Zwr uwag na to, e klasa Shape zawiera wirtualny destruktor. Powinien on by dodawany automatycznie do kadej klasy, zawierajcej funkcje wirtualne. Jeeli kontener przechowuje wskaniki lub referencje do obiektw klasy Shape, to w przypadku gdy dla tych obiektw wywoywane s wirtualne destruktory wszystko zostanie usunite poprawnie. Kady z rodzajw rysunkw, znajdujcych si w poniszym programie, wykorzystuje inny rodzaj szablonu klasy kontenera uywane s klasy PStash oraz Stack, zdefiniowane w biecym rozdziale, oraz klasa vector, pochodzc ze standardowej biblioteki jzyka C++. Wykorzystywanie" kontenerw jest tutaj skrajnie proste i dziedziczenie moe okaza si wcale nie najlepszym podejciem (bardziej odpowiednia wydaje si kompozycja). Jednake w tym przypadku dziedziczenie jest prostym rozwizaniem i nie wpywa ujemnie na istot przykadu. //: C16:Drawing.cpp #include <vector> // Uywane s rwnie standardowe wektory! #include "TPStash2.h" #include "TStack2.h" #include "Shape.h" using namespace std; // Klasa Drawing jest podstawowym kontenerem klasy Shape: class Drawing : public PStash<Shape> { public: ~Drawing() { cout << "~Drawing" << endl; }

Thinking in C++. Edycja poteka

// Klasa Plan jest innym kontenerem klasy Shape: class Plan : public Stack<Shape> { public: -Plan() { cout << "~Plan" << endl; } // Klasa Schematic jest innym kontenerem klasy Shape: class Schematic : public vector<Shape*> { public: -Schematic() { cout << "~Schematic" << endl; } // Szablon funkcji: template<class Iter> void drawAll(Iter start. Iter end) { while(start ! end) { (*start)->draw(); start++;

int main() { // Kady rodzaj kontenera // posiada inny interfejs: Drawing d; d.add(new Circle); d.add(new Square); d.add(new Line); Plan p; p.push(new Line); p.push(new Square); p.push(new Circle); Schematic s; s.push_back(new Square); s.push_back(new Circle); s.push_back(new Line); Shape* sarray[] - { new Circle, new Square. new Line }: // Iteratory oraz szablon funkcji pozwalaj // na traktowanie ich w sposb oglny: cout << "Drawing d:" << endl; drawA11(d.begin(). d.endO); cout << "Plan p:" << endl; drawAll(p.begin(). p.end{)); cout << "Schematic s:" << endl; drawAll(s.begin(). s.endO): cout << "Array sarray:" << endl; // Dziaa to nawet w przypadku tablic wskanikw: drawAll(sarray, sarray + sizeof(sarray)/sizeof(*sarray)); cout << "Koniec funkcji main" << endl ;
Wszystkie rnice si od siebie typy kontenerw przechowuj wskaniki do obiektw klasy Shape, a take, podlegajce rzutowaniu w gr, wskaniki do obiektw kIas pochodnych klasy Shape. Jednake z uwagi na polimorfizm wszystko dziaa poprawnie podczas wywoywania wirtualnych funkcji.

Rozdzia 16. Wprowadzenie do szablonw

593

Zwr uwag na to, e tablica wskanikw do obiektw typu Shape sarray moe by rwnie uwaana za kontener.

Szablony funkcji
W funkcji drawAU() pojawia si co nowego. W niniejszym rozdziale uywalimy wycznie szablonw klas, ktre powodoway konkretyzacj nowych klas na podstawie jednego lub wikszej liczby parametrw okrelajcych typy. Jednak rwnie atwo mona tworzy szablonyfunkcji, generujce nowe funkcje na podstawie parametrw okrelajcych typy. Powd, dla ktrego tworzone s szablony funkcji, jest taki sam jak w przypadku szablonw klas prbujemy utworzy oglny kod i robimy to, opniajc specyfikacj jednego lub wikszej liczby typw. Okrelamy jedynie, e typy te s parametrami, obsugujcymi pewne operacje, nie precyzujc dokadnie, ojakietypychodzi. Szablon funkcji drawAll() mona traktowa jako algorytm (i tak wanie nazywana jest wikszo szablonw funkcji, zawartych w standardowej bibliotece jzyka C++). Okrela on po prostu, w jaki sposb zrobi co w stosunku do danych iteratorw, opisujcych zakres elementw, pod warunkiem, e iteratory te mog by wyuskiwane, inkrementowane i porwnywane. S one rodzajami iteratorw, ktre tworzylimy w tym rozdziale, a take nieprzypadkowo rodzajem iteratorw tworzonym przez kontenery, zawarte w standardowej bibliotece jzyka C++, czego dowodem jest wykorzystanie w powyszym przykadzie klasy vector. Chcielibymy rwnie, aby szablon drawAU() by algorytmem oglnym (ang. generic algorithm) w taki sposb, by kontenery mogy by dowolnych typw i abymy nie musieli pisa nowej wersji algorytmu dla kadego nowego typu kontenera. Jest to sytuacja, w ktrej szablony funkcji sniezbdne, poniewa generujone automatycznie okrelony kod dla kadego kolejnego typu kontenera. Lecz bez dodatkowego porednictwa, zapewnionego przez iteratory, uzyskanie takiej oglnoci nie byoby moliwe. To dlatego wanie iteratory s takie wane pozwalaj na napisanie kodu oglnego przeznaczenia, wykorzystujcego kontenery, nieznajcego wewntrznej struktury adnego kontenera (zwr uwag na to, e w jzyku C++ iteratory oraz algorytmy oglne wymagajdo dziaania szablonw funkcji). Dowd na to mona dostrzec w funkcji main(), poniewa szablon drawAll() dziaa bez adnych modyfikacji z wszystkimi, rnymi od siebie rodzajami kontenerw. Co ciekawsze, szablon ten funkcjonuje rwnie ze wskanikami do pocztku i koca tablicy sarray. Moliwo traktowania tablic jako kontenerw stanowi integralny element projektu standardowej biblioteki jzyka C++, ktrej algorytmy bardzo przypominajszablon funkcji drawAll(). Z uwagi na to, e szablony klas kontenerw s rzadko przedmiotem dziedziczenia i rzutowania w gr, widocznych w przypadku ,^wyczajnych" klas, w klasach kontenerw niemal nigdy nie spotyka si funkcji wirtualnych. Klasy kontenerw umoliwiaj ponowne wykorzystywani kodu za pomoc szablonw, a nie dziedziczenia.

Thinking in C++. Edycja polska

>odsumowanie
Klasy kontenerw stanowi zasadniczy element programowania obiektowego. S one innym sposobem uproszczenia oraz ukrycia szczegw programu, a take przyspieszenia procesu jego powstawania. Ponadto zapewniaj one duy stopie bezpieczestwa i elastycznoci, zastpujc proste tablice oraz stosunkowo atwe techniki strukturyzacji danych, dostpne wjzyku C. Z uwagi na to, e klienci-programici potrzebuj kontenerw, powinny by one proste w uyciu. W tym wanie wzgldzie przychodz z pomoc szablony. Dziki nim skadnia wielokrotnego wykorzystania kodu rdowego (w przeciwiestwie do wielokrotnego wykorzystania kodu obiektw, dostpnego dziki dziedziczeniu i kompozycji) staje si oczywista nawet dla pocztkujcego uytkownika. W istocie wielokrotne wykorzystywanie kodu, moliwe dziki zastosowaniu szablonw, jest wyranie atwiejsze ni dziedziczenie i kompozycja. Mimo e dziki tej ksice wiesz, w jaki sposb tworzy klasy kontenerw i iteratorw, w praktyce znacznie bardziej przydatne jest poznanie kontenerw i iteratorw zawartych w standardowej bibliotece jzyka C++, poniewa mona si spodziewa, e s one dostpne dla kadego kompilatora. Dziki lekturze drugiego tomu ksiki (ktry mona pobra z witryny http:/flielion.pUonline/thinking/index.html) przekonasz si, e kontenery i algorytmy zawarte w standardowej bibliotece jzyka C++ zaspokoj wszystkie twoje potrzeby, dziki czemu nie bdziesz musia samodzielnie tworzy nowych. W rozdziale zostay zasygnalizowane kwestie zwizane z klasami kontenerw, ale, jak si mona tego spodziewa, mog one by znacznie bardziej skomplikowane. Zoona biblioteka klas kontenerowych moe rozwizywa wszystkie rodzaje dodatkowych problemw, zwizanych z kontenerami, m.in. wielowtkowo, trwao (ang. persistence) oraz zbieranie nieuytkw (odmiecanie).

2wiczenia
Rozwizania wybranych wicze znajduj si w dokumencie elektronicznym The Thinking in C++ Annotated Solution Guide, ktry mona pobra za niewielk opat z witryny www.BruceEckel.com. 1. Zaimplementuj hierarchi dziedziczenia, widocznna znajdujcym si w rozdziale diagramie klasy OKsztaft. 2. Zmie rozwizanie wiczenia 1., zamieszczonego w rozdziale 15., by zamiast tablicy wskanikw do obiektw klasy Shape wykorzysta klas Stack oraz iterator, zawarte w pliku nagwkowym TStack2.h. Do hierarchii klas dodaj destruktory, dziki ktrym bdzie mona zobaczy, e obiekty klasy Shape s niszczone w momencie wyjcia kontenera Stack poza zasig.

Rozdzia 16. Wprowadzenie do szablonw

595

3. Zmodyfikuj plik nagwkowy TPStash.h w taki sposb, by wielko, o ktr powikszanyjest przydzielony obszar pamici, uywana przez funkcj inflate(), moga by zmieniana w trakcie ycie danego obiektu kontenera. 4. Zmodyfikuj plik nagwkowy TPStash.h w taki sposb, by wielko, o ktr powikszanyjest przydzielony obszar pamici, uywana przez funkcj inflate(), automatycznie zmieniaa si w celu ograniczenia liczby wywoa tej funkcji. Na przykad za kadym razem, gdy wywoywanajest funkcja inflate(), mona by podwaja wielko, uywanprzyjej nastpnym wywoaniu. Zademonstruj dziaanie zmodyfikowanej w taki sposb klasy, wywietlajc komunikat podczas kadego wywoania funkcji inflate() i piszc program testowy, zawarty w funkcji main(). 5. Przekszta funkcj fibonacci() w szablon funkcji o parametrze okrelajcym typ zwracanej przez funkcj wartoci (zamiast wartoci typu int bdzie ona moga zwraca wartoci typw long, float itp.). 6. Wykorzystujcjako wewntrznimplementacj klas vector, zawart w standardowej bibliotecejzyka C++, utwrz szablon klasy Set, umoUwiajcy umieszczanie w kontenerze tylko jednego egzemplarza kadego unikatowego obiektu. Utwrz zagniedon klas iteratora, obsugujc przedstawione w rozdziale pojcie ,/nacznika koca". W funkcji main() napisz kod, testujcy dziaanie klasy Set, a nastpnie zastp j klas set, zawart w standardowej bibliotecejzyka C++, aby sprawdzi, czy dziaa ona poprawnie. 7. Zmodyfikuj plik nagwkowy AutoCounter.h w taki sposb, aby mona byo uy obiektu zawartej w nim klasyjako obiektu skadowego dowolnej klasy, ktrej tworzenie i niszczenie zamierza si ledzi. Dodaj skadow typu string, przechowujc nazw tej klasy. Przetestuj to narzdzie wewntrz dowolnej wasnej klasy. 8. Utwrz wersj klasy zawartej w pliku nagwkowym OwnerStack.h, ktra wykorzystuje w charakterze wewntrznej implementacji klas vector standardowej bibliotekijzyka C++. By moe, aby to zrobi, konieczne bdzie uzyskanie informacji o niektrych funkcjach skadowych klasy vector (albo przynajmniej przejrzenie zawartoci pliku nagwkowego <vector>). 9. Zmodyfikuj klas zawart w pliku nagwkowym ValueStack.h w taki sposb, by w przypadku umieszczania na stosie funkcj push( ) wikszej liczby obiektw, ktre powoduje przekroczenie dostpnego miejsca, automatycznie powikszaa przydzielon pami. Zmie program ValueStackTest.cpp w taki sposb, by przetestowa t now cech klasy. 10. Powtrz poprzednie wiczenie, alejako wewntrznej implementacji klasy ValueStack uyj klasy vector, zawartej w standardowej bibliotecejzyka C++. Zwr uwag na to, o ile prostszejest takie rozwizanie. 3i. Zmodyfikuj program ValueStackTest.cpp w taki sposb, by w funkcji main() zamiast kasy Stack wykorzystywa klas vector, zawart w standardowej bibliotece jzyka C++. Zwr uwag na zachowanie si programu pojego uruchomieniu czy podczas tworzenia obiektu klasy vector rwnie tworzona jest grupa domylnych obiektw?

)6

Thinking in C++. Edycja polska

12. Zmodyfikuj klas zawart w pliku nagwkowym TStack2.h w taki sposb, by wykorzystywaa w charakterze wewntrznej implementacji klas vector, zawart w standardowej bibliotecejzyka C++. Upewnij si, e nie zmieni si interfejs klasy, dziki czemu program TStack2Test.cpp bdzie dziaa bez modyfikacji. Z3. Powtrz poprzednie wiczenie, zamiast klasy vector uywajc klasy stack, zawartej w standardowej bibliotecejzyka C++ (moe to wymaga uzyskania informacji dotyczcych klasy stack lub przeszukania pliku nagwkowego <stack>). 14. Zmodyfikuj klas zawartw pliku nagwkowym TStash2.h w taki sposb, by wykorzystywaa w charakterze wewntrznej implementacji klas vector, zawart w standardowej bibliotece jzyka C++. Upewnij si, e nie zmieni si interfejs klasy, dziki czemu program TStash2Test.cpp bdzie dziaa bez modyfikacji. 15. Zmodyfikuj iterator IntStackIter zawarty w pliku IterIntStack.cpp, tak by doda do niego konstruktor znacznika koca" oraz operatory == i !=. W funkcji main() wykorzystaj ten iterator do przejcia przez wszystkie elementy zawarte w kontenerze, a do napotkania znacznika koca. 16. Wykorzystujc pliki nagwkowe TStack2.h, TPStash2.h oraz Shape.h, utwrz konkretyzacje szablonw kontenerw Stack oraz PStash dla typu Shape*, nastpnie wypenij kady z nich rnymi rzutowanymi w gr wskanikami do obiektw klasy Shape, a potem uyj iteratorw do przejcia przez kady kontener i wywoania dla kadego obiektu funkcji draw(). 17. Przekszta klas Int, zawart w programie TPStash2Test.cpp, w szablon klasy, dziki czemu bdzie ona moga przechowywa dowolny typ obiektw (w dowolny sposb zmie nazw tej klasy na co bardzjej odpowiedniego). 18. Przekszta klas IntArray, zawart w programie IostreamOperatorOverloading.cpp, znajdujcym si w rozdziale 12., w szablon klasy, parametryzujc zarwno typ przechowywanych obiektw, jak i wielko wewntrznej tablicy. 19. Przekszta klas ObjContainer, zawart w programie NestedSmartPointer.cpp, znajdujcym si w rozdziale 12., w szablon klasy. Przetestuj go za pomoc dwu rnych klas. 20. Zmodyfikuj programy C15:OStack.h oraz C15:OStackTest.cpp, przeksztacajc klas Stack w szablon, dziki czemu bdzie on automatycznie wielokrotnie dziedziczy zarwno po zawartej w nim klasie, jak i po klasie Object. Skonkretyzowana klasa Stack powinna pobiera i zwraca wycznie wskaniki zawartego w niej typu. 2JL Powtrz poprzednie wiczenie, wykorzystujc klas vector zamiast klasy Stack. 22. Z klasy vector<void*> wyprowad klas StringVector, a nastpnie przedefiniuj funkcje skadowe push_back() oraz operator[ ] w taki sposb, by pobieray i zwracay wycznie wskaniki typu string* (dokonujc odpowiedniego rzutowania). Nastpnie przygotuj szablon, ktry bdzie

Rozdzid 16. Wprowadzenie do szablonw

597

automatycznie tworzy klas kontenera, wykonujc to samo zadanie w stosunku do wskanikw dowolnego typu. Technika tajest czsto stosowana w celu ograniczenia rozrastania si kodu, spowodowanego zbyt wieloma konkretyzacjami szablonw. 23. Dodaj do iteratora PStash::iterator, zawartego w pliku nagwkowym TPStash2.h, operator-, dziaajcy w sposb podobny do funkcji operator+, i przetestuj jego dziaanie. 24. W programie Drawing.cpp dodaj szablon funkcji, wywoujcej funkcje skadowe erase(), i przetestuj go. 25. (Trudne) Zmodyfikuj klas Stack, zawart w pliku nagwkowym TStack2.h, zapewniajc pen ,^iarnistosc" prawa wasnoci do kadego wskanika dodaj znacznik, okrelajcy, czy ten wskanik jest wacicielem wskazywanego przez siebie obiektu, i obsuguj t informacj w funkcji push() oraz w destruktorze. Dodaj funkcje skadowe, umoliwiajce odczytanie i zmian prawa wasnoci dIa kadego wskanika. 26. (Trudne) Zmodyfikuj program PointerToMemberOperator.cpp, znajdujcy si w rozdziale 12., w taki sposb, by przeksztaci klas FunctionObject oraz operator->* w szablony, dziaajce z dowolnym zwracanym typem (w przypadku operatora->* naley uy szablonw skladowych, opisanych w drugim tomie ksiki). Dodaj i przetestuj moliwo podawania zera, jednego i dwch argumentw w funkcjach skadowych klasy Dog.

Thinking in C++. Edycja polska

Styl kodowania
Tematem tego dodatku nie jest sposb robienia wci w kodzie i umieszczania nawiasw okrgych oraz klamrowych, mimo e o tych kwestiach rwnie si w nim wspomina. Dotyczy on oglnych zasad stosowanych w ksice, zwizanych z organizacj wydrukw zawierajcych kod programw. Wiele spord poruszonych tu zagadnie byo stopniowo wprowadzanych w poszczeglnych rozdziaach ksiki, ale poniewa dodatek ten znajduje si na jej kocu, mona zaoy, e kady temat stanowi otwart kwesti, a w przypadku wtpliwoci mona zajrze do odpowiedniego podrozdziau. Wszystkie decyzje dotyczce stylu kodowania przedstawionego w ksice zostay podjte po dokadnym przemyleniu, co czasami zajo lata. Oczywicie, kady ma jakie powody, dla ktrych formatuje kod w taki a nie inny sposb zamierzam jedynie opowiedzie o tym, w jaki sposb wypracowaem wasny styl kodowania, a take o ograniczeniach oraz o czynnikach zewntrznych, ktre wpyny na podjte przez mnie decyzje.

Dodatek A

Oglne zasady
Zamieszczone w tekcie ksiki identyfikatory (funkcje, zmienne oraz nazwy klas) zostay oznaczone czcionk pogrubion. Tak sam czcionk wyrniono wikszo wystpujcych w tekcie sw kluczowych. W celu prezentacji przykadw zawartych w ksice uywam odrbnego stylu, rozwijanego latami. Jego szczegln inspiracj stanowi styl, ktry Bjarne Stroustrup zastosowa w swojej pierwszej ksice The C++ Programming Language"1. Temat stylu formatowania moe by przedmiotem wielogodzinnej gorcej dyskusji, wic pragn zaznaczy, e nie zamierzam, odwoujc si do moich przykadw, narzuca
Bjame Stroustrup: The C++ Programming Language, Addison-Wesley, 1986 (pierwsze wydanie).

Thinking in C++. Edycja polska

komukolwiek poprawnego stylu formatowania mamjedynie wasne powody, ktre sprawiaj, e zyska on wanie taki ksztat. Poniewa jzyk C++ jest jzykiem programowania o swobodnej strukturze, moesz uywa takiego stylu kodowania, jaki ci najbardziej odpowiada. Uwaam, e stosowanie w obrbie projektujednolitego stylu formatowaniajest istotne. W Internecie mona znale wiele narzdzi, umoliwiajcych przeformatowanie kodu caego projektu w celu osignicia tej cennej jednolitoci. Programy zawarte w tej ksice s plikami, ktre zostay automatycznie wyodrbnione z tekstu ksiki, co pozwolio mi na ich przetestowanie w celu upewnienia si, e dziaaj poprawnie. Tak wic wszystkie wydrukowane w ksice programy powinny dziaa i kompilowa si bez bdw, jeeli uywany kompilator jest zgodny ze standardem jzyka C++ (naley pamita, e nie wszystkie kompilatory jzyka C++ obsuguj wszystkie waciwoci jzyka). Zawarte w programach bdy, ktre powinny spowodowa zgoszenie komunikatw o bdach podczas kompilacji, zostay umieszczone w komentarzach, po znakach //!, dziki czemu mona atwoje odnale, a take przetestowa za pomoc metod automatycznych. Odkryte i zgoszone autorowi bdy pojawi si najpierw w elektronicznej wersji ksiki (w witrynie www.BruceEckel.com), a dopiero pniej wjej poprawionych wydaniach. Zgodnie z jednym ze standardw stosowanych w ksice wszystkie zawarte w niej programy s kompilowane i czone bez bdw (mimo e mog czasami powodowa wywietlanie ostrzee). Aby to uzyska, niektre z programw, zawierajcejedynie przykady kodu i niesamodzielne, kryjpuste funkcje main() jak widoczna poniej: int m a i n ( ) {} Pozwala to programowi czcemu na zakoczenie pracy bez zgaszania bdw. W przypadku funkcji main() standardem jest zwracanie przez ni wartoci cakowitej, lecz standardjzyka C++ okrela, ejeeli w funkcji main() nie ma instrukcji return, to kompilator automatycznie generuje kod, powodujcy zwrcenie przez funkcj wartoci zerowej. Ta wanie moliwo (brak instrukcji return w funkcji main()) jest wykorzystywana w ksice (niektre kompilatory mog zgasza z tego powodu ostrzeenia, ale nie sone zgodne ze standardemjzyka C++).

azwy plikw
W jzyku C pliki nagwkowe (obejmujce deklaracje) tradycyjnie posiadaj nazwy o rozszerzeniach .h, natomiast pliki zawierajce implementacj (powodujce przydzielanie pamici i generacj kodu) zawarte s w plikach o nazwach z rozszerzeniem .c. Jzyk C++ przeszed ewolucj. Pocztkowo by rozwijany w systemie Unix, w ktrym system operacyjny rozrnia wielkie i mae litery w nazwach plikw. Na pocztku rozszerzenia nazw plikw byy zapisanymi wielkimi literami wariantami rozszerze, stosowanych w jzyku C odpowiednio .H i .C. To nie dziaao, oczywicie, w przypadku systemw operacyjnych, ktre nie rozrniaj wielkich i maych

Dodatek A Styl kodowania

601

liter, takich jak DOS. Producenci kompilatorw jzyka C++ dla systemu DOS uywali takich rozszerze, jak hxx i cxx odpowiednio do plikw nagwkowych oraz plikw zawierajcych implementacj albo do rozszerze hpp oraz cpp. Pniej kto doszed do wniosku, e jedynym powodem stosowania rnych rozszerze nazw plikw jest umoliwienie kompilatorowi okrelenia, czy kompiluje plik zawierajcy program w jzyku C, czy te C++. Poniewa kompilator nigdy nie kompiluje bezporednio plikw nagwkowych, naley zmieni tylko rozszerzenie nazwy pliku, zawierajcego implementacj. Obecnie waciwie we wszystkich systemach przyjto zwyczaj, polegajcy na uywaniu rozszerzenia nazwy cpp do plikw zawierajcych implementacj oraz rozszerzenia h do plikw nagwkowych. Zwr uwag na to, e w przypadku doczania standardowych plikw nagwkowych jzyka C++ wykorzystywanajest moliwo stosowania plikw nieposiadajcych rozszerze nazw, np. #include <iostream>.

Znaczniki pocztkowych i kocowych komentarzy


Bardzo istotn kwesti w tej ksicejest to, by wszystkie widoczne w niej programy zostay zweryfikowane pod ktem poprawnoci (przynajmniej przez kompilator). Uzyskano to dziki automatycznemu wyodrbnieniu zamieszczonych w niej plikw. W tym celu wszystkie wydruki programw, ktre przewidziano do kompilacji (w odrnieniu od nielicznych fragmentw kodu), zawieraj znaczniki i komentarze, bdce znacznikami pocztku i koca. Znaczniki te s wykorzystywane przez program narzdziowy wyodrbniajcy kod ExtractCode.cpp, zawarty w drugim tomie ksiki (dostpny w Internecie pod adresem http:/flielion.pVonline/thinking/index.htmfy, tworzcy pliki zawierajce kod na podstawie wydrukw zawartych w tekstowej wersji ksiki. Znacznik koca wydruku informuje program ExtractCode.cpp, e w tym miejscu koczy si program. Jednake po znaczniku pocztku znajduje si informacja dotyczca podkatalogu, do ktrego plik naley (podkatalogi odpowiadaj strukturze rozdziaw, wic plik zawarty w rozdziale 8. powinien mie w tym miejscu znacznik C08). Po tej informacji nastpuje dwukropek i nazwa pliku rdowego. Poniewa program ExtractCode.cpp dla kadego podkatalogu tworzy rwnie pliki makefile, informacja o tym, jak utworzy program oraz wiersz polece wykorzystywany do jego przetestowania, zawarte s rwnie w kodzie rdowym. Jeeli program ma charakter samodzielny (nie musi by czony z adnym innym programem), to nie posiada adnych dodatkowych informacji. Takjest rwnie w przypadku plikw nagwkowych. Jeeli jednak plik nie zawiera funkcji main() i naley go poczy z jakim innym programem, to po nazwie pliku wystpuje znacznik {O}. Jeeli wydruk stanowi natomiast program gwny, ale musi zosta poczony z jakimi innymi komponentami, to znajduje si w nim oddzielny wiersz, rozpoczynajcy si znacznikiem //{L}, poprzedzajcym nazwy wszystkich plikw, ktre musz by z nim poczone (bez rozszerze nazw, poniewa mog by one rne dla rnych platform).

)2

Thinking in C++. Edycja polska

Przykady wykorzystania tych znacznikw mona znale w caej ksice. Jeeli plik powinien zosta wyodrbniony, ale znaczniki pocztku i koca nie powinny znale si w utworzonym pliku jeeli ten plik zawiera na przykad dane testowe), to bezporednio po znacznikach pocztku i koca nastpuje znak !".

Jawiasy okrge, klamrowe i wcicia


Nietrudno zauway, e stosowany w ksice styl formatowania odbiega od wielu tradycyjnie stosowanych stylw jzyka C. Oczywicie kady sdzi, e uywany przez niego styl jest najbardziej praktyczny. Jednake za stylem stosowanym w ksice kryje si prosta logika. Zostanie ona przedstawiona poniej wraz z przemyleniami na temat tego, dlaczego rozwiny si niektre inne style kodowania. Zastosowany styl formatowania jest spowodowany tylko jedn przyczyn wymogami prezentacji zarwno w druku,jak i podczas prowadzonych na ywo seminariw. By moe twoje potrzeby sinne, poniewa nie tworzysz zbyt wielu prezentacji. Jednake kod jest znacznie czciej czytany ni pisany, wic powinien by atwy w odbiorze. Moimi dwoma najwaniejszymi kryteriami sa,,czytelnosc" ^ak atwejest dla czytelnika uchwycenie sensu pojedynczego wiersza) oraz liczba wierszy, ktre mieszcz si na stronie. To drugie moe wydawa si zabawne, ale podczas prowadzenia prezentacji konieczno przesuwania przezroczy do przodu i do tyu jest bardzo irytujca /arwno dla publicznoci, jak i dla osoby prowadzcej. Kady chyba zgodzi si z tym, e kod zawarty w nawiasie klamrowym powinien by wcity. Tym, co wywouje kontrowersje, i jest zarazem sytuacj, w ktrej poszczeglne style formatowania najbardziej si midzy sob rhi, jest problem umieszczenia otwierajcego nawiasu klamrowego. Sdz, e odpowied na to pytanie jest przyczyn powstania tylu odmian stylw kodowania (wyliczenie stosowanych stylw kodowania mona znale w ksice Toma Pluma i Dana Saksa: C++ Programming Guidelines", Plum Hall, 1991). Podejm prb wykazania, e wiele stosowanych dzisiaj stylw kodowania wynika z ogranicze istniejcych przed utworzeniem standardu jzyka C (zanim powstay prototypy funkcji) i w zwizku z tym uywanie ich nie jest uzasadnione. Najpierw przedstawi odpowied na kluczowe pytanie: otwierajcy nawias klamrowy powinien zawsze znajdowa si w tym samym wierszu, co jego poprzednik" (pod t nazw rozumiem: cokolwiek, czego dotyczy zawarte w nawiasie klamrowym ciao klas, funkcj, definicj obiektu, w przypadku instrukcji itp."). Jest to pojedyncza, spjna regua, ktr stosuj w caym pisanym przez siebie kodzie, ktra znacznie upraszcza formatowanie. Zwiksza rwnie czytelno" kodu. Odczytujc wiersz: int func(int a); dziki rednikowi znajdujcemu si na kocu wiersza poznajemy, e jest to deklaracja, ktra niejestju kontynuowana, ale w przypadku tego wiersza: 1nt func(int a) {

Dodatek A Styl kodowania

603

od razu wiemy, e jest to definicja, poniewa wiersz koczy si nie rednikiem, tylko klamrowym nawiasem otwierajcym. Dziki zastosowaniu takiego sposobu miejsce, w ktrym jest umieszczony otwierajcy nawias klamrowy, jest identyczne w przypadku definicji wielowierszowej: int func(int a} { int b = a + 1; return b * 2; } i gdy definicja znajduje si tylko w jednym wierszu, co jest czsto stosowane w przypadku funkcji inline: int func(int a) { return (a + 1) * 2; } Podobnie w przypadku klas wiersz: class T h i n g ; jest deklaracj nazwy klasy, a zapis: class Thing { oznacza definicj klasy. We wszystkich tych przypadkach na podstawie pojedynczego wierszu kodu mona okreli, czy jest on deklaracj, czy te definicj. No i oczywicie, ulokowanie otwierajcego nawiasu klamrowego w tym samym wierszu, zamiast w oddzielnym, umoliwia zmieszczenie na stronie wikszej liczby wierszy kodu. Dlaczego istnieje zatem tyle innych stylw? Mona w szczeglnoci zauway, e wikszo programistw tworzy klasy, uywajc przedstawionego powyej stylu (ktrym posuy si rwnie Stroustrup we wszystkich wydaniach swojej ksiki The C++ Programming Language", opublikowanych przez Addison-Wesley). Tworzc funkcje, umieszczaj oni jednak otwierajcy nawias klamrowy w oddzielnym wierszu (co implikuje rwnie stosowanie rozmaitych sposobw wcinania tekstu). Stroustrup rwnie stosowa taki zapis z wyjtkiem krtkich funkcji inline. Dziki stosowaniu opisanego przeze mnie sposobu, wszystko jest spjne nazwa, czymkolwiek ona jest (klas, funkcj, wyliczeniem itp.), poprzedza usytuowany w tym samym wierszu otwierajcy nawias klamrowy, ktry sygnalizuje, e dalej znajduje si ciao dla tej nazwy. Ponadto klamrowy nawias otwierajcy znajduje si zawsze w tym samym miejscu zarwno w przypadku krtkich funkcji inline,jak i zwykych definicji funkcji. Twierdz, e uywany przez wielu styl, stosowany w przypadku definicji funkcji, ma swoje rdo w wersji jzyka C istniejcej przez utworzeniem w tym jzyku prototypw funkcji. Wwczas nie deklarowao si argumentw funkcji wewntrz nawiasu, tylko pomidzy nawiasem zamykajcym i klamrowym nawiasem otwierajcym (co wiadczy o asemblerowych korzeniach jzyka C): void bar() float y:

int x;

/* ciao funkcji */

Thinking in C++. Edycja polska W tym przypadku umieszczenie otwierajcego nawiasu klamrowego w poprzednim wierszu byoby do niezrczne, wic nikt tego nie robi. Jednake programici musieli zdecydowa si, czy nawiasy klamrowe powinny by wcite, razem z zawartym w nich kodem, czy te powinny znajdowa si na poziomie poprzednika". Std te wynika mnogo stosowanych stylw formatowania. S rwnie argumenty, przemawiajce za umieszczaniem otwierajcego nawiasu klamrowego w nastpnym wierszu, za deklaracj (klasy, struktury, funkcji itp.). Przytoczony poniej argument pochodzi od czytelnika i zosta tu zamieszczony, by mona byo si zorientowa, na czym polega problem: Dowiadczeni uytkownicy edytora vi" (vim) wiedz, e dziki dwukrotnemu przyciniciu klawisza ]" uytkownik przechodzi do nastpnego wystpienia w kolumnie A zerowej znaku {" (albo L). Waciwo tajest wyjtkowo przydatna podczas poruszania si w obrbie kodu (przeskok do nastpnej funkcji lub definicji klasy). [Mj komentarz: gdy rozpoczynaem prac w systemie Unix, pojawi si wanie edytor GNU Emacs, ktry bardzo mnie zaabsorbowa. W rezultacie nigdy nie poznaem edytora vi", i dlatego nie rozumuj w terminach pozycji kolumny zerowej". Jednake istnieje wielu uytkownikw edytora vi", ktrych ta kwestia dotyczy]. Umieszczenie znaku {" w nastpnym wierszu eliminuje nieczytelny kod, zawarty w zoonych wyraeniach warunkowych, zwikszajc ich czytelno. Oto przykad:
if(condl && cond2 && cond3) { statement;

Powyszy fragment [twierdzi czytelnik] jest trudno czytelny. Natomiast nastpujcy:

if (condl && cond2 && cond3) { statement;


oddziela instrukcj ,jf' od jej ciaa, dziki czemu jest ona bardziej czytelna. [Twoje zdanie na ten temat bdzie zaleao od tego, do czegojeste przyzwyczajony]. W kocu atwiej jest ogarn wzrokiem nawiasy klamrowe, gdy s one wyrwnane do tej samej kolumny. W ten sposb znacznie lepiej wyrniaj si one w tekcie. [Koniec komentarza czytelnika]. Kwestia miejsca umieszczenia otwierajcego nawiasu klamrowego wywouje prawdopodobnie najwicej sporw. Nauczyem si czyta obie postacie zapisu i doszedem w kocu do wniosku, e rdem problemu s przyzwyczajenia uytkownikw. Zwracam jednak uwag na to, e oficjalny standard kodowania w jzyku Java (ktry znalazem w internetowej witrynie firmy Sun, powiconej jzykowi Java) jest waciwie taki sam, jak zaprezentowany w ksice poniewa coraz wicej osb rozpoczyna programowanie w obu jzykach, spjno w uywanych przez nie stylach kodowania moe si okaza pomocna.

Dodatek A Styl kodowania

605

Dziki stosowanej przeze mnie metodzie zostay wyeliminowane wszystkie wyjtki i przypadki szczeglne, a take w logiczny sposb powsta jednolity styl stosowania wci. Spjno zostaje utrzymana nawet wewntrz ciaa funkcji, jak w poniszym przykadzie: for(int i = 0; 1 < 100; i++) { cout << i << endl; cout << x * i << endl; Styl ten jest atwy i do nauczenia, i do zapamitania. Uywana jest jedna, spjna regua, dotyczca caego formatowania, a nie jedna dla klas, druga dla funkcji (z wariantami dla funkcji jednowierszowych i wielowierszowych), a jeszcze inne dla ptli for, instrukcji if itd. Spjno jest wartoci, ktra sama w sobie warta jest rozwaenia. Jzyk C++jest mimo wszystko nowszymjzykiem ni C i chocia w wielu przypadkach musimy czyni ustpstwa na rzecz jzyka C, to nie powinnimy czerpa z niego zbyt wielu elementw, ktre mog si sta w przyszoci przyczyn problemw. Drobne problemy, pomnoone przez wiele wierszy kodu, staj si wielkimi problemami. Gruntowne omwienie tego tematu (cho w odniesieniu do jzyka C) znale mona w ksice Davida Strakera: C Style: Standards and Guidelines" (Prentice-Hall, 1992). Innym ograniczeniem, z ktrym musiaem si liczy, bya szeroko wiersza. W ksice moe si bowiem znale jedynie 50 znakw; jak jednak postpi w przypadku, gdy si one nie mieszcz? Ponownie staraem si wypracowa spjn strategi dotyczc tego, w jaki dzielone s wiersze, by byy one wyranie widoczne. Dopki stanowi one czjednej definicji, listy argumentw itp., dopty wiersze stanowice kontynuacj powinny by wcite o jeden poziom w stosunku do pocztku tej definicji, listy argumentw itp.

Nazwy identyfikatorw
Ci, ktrzy znajjzyk Java, zauwayli zapewne, e uywam stylu tego jzyka w odniesieniu do wszystkich nazw identyfikatorw. Nie mog by jednak w tej kwestii konsekwentny, poniewa identyfikatory, zawarte w standardowych bibliotekach jzykw C oraz C++, nie stosuj si do tej konwencji. Styl ten jest do prosty. Pierwsza litera identyfikatora jest zapisywana jako wielka tylko wwczas, gdy identyfikator jest nazw klasy. Jeeli jest nazw funkcji lub zmiennej, tojest pisana ma liter. Pozostaa cz identyfikatora skada si z jednego lub wikszej liczby sw, poczonych razem, wyrnionych jednak przez zapisanie wielk liter pierwszego znaku kadego sowa. Tak wic nazwa klasy moe by nastpujca:
class FrenchVanilla : public IceCream {

z kolei identyfikator obiektu:

Thinking in C++. Edycja polska

FrenchVanilla myIceCreamCone(3); a funkcja w taki sposb: void eatIceCreamCone(); (zarwno funkcja skladowa,jak i zwyczajna funkcja). Jedyny wyjtek dotyczy staych czasu kompilacji (zdefiniowanych za pomoc sowa kluczowego const lub dyrektywy #define), ktrych identyfikatory pisane s w caoci wielkimi literami. Korzyciwynikajcz tego stylujest to, e wielko liter ma swoje znaczenie ju na podstawie pierwszej wiadomo, czy mamy do czynienia z klas, czy te obiektem lub funkcj skadow. Jest to szczeglnie przydatne przy odwoaniach do statycznych skadowych klas.

olejno doczania likw nagwkowych


Pliki nagwkowe s doczane w kolejnoci od najbardziej wyspecjalizowanego do najbardziej oglnego". Oznacza to, e kolejno dodawane s: wszelkie pliki nagwkowe, znajdujce si w lokalnym katalogu, pliki nagwkowe przygotowanych przeze mnie narzdzi", takich jak require.h, pliki nagwkowe niezalenych dostawcw, pliki nagwkowe standardowej biblioteki jzyka C++, a na kocu pliki nagwkowe biblioteki jzyka C. Uzasadnienie doczania plikw nagwkowych w takiej wanie kolejnoci pochodzi z ksiki Johna Lakosa Large-Scale C++ Software Design" (Addison-Wesley, 1996): Mona umkn ukrytych bldw, zapewniajc, epliki .h komponentw bd sprawdzaty si samoczynnie bez udzialujakichkolwiek dostarczonych z zewntrz deklaracji czy definicji... Doczenie plikw .h na samym pocztku plikw .c gwarantuje, e wplikach .h nie zabraknie adnej istotnej informacji, niezbdnej dofizycznego interfejsu komponentw (ajeelijej nie ma, to przekonasz si o tym, gdy tylko zaczniesz kompilowa plik .c). Jeeli pliki nagwkowe s doczane w kolejnoci od najbardziej wyspecjalizowanego do najbardziej oglnego", to jest bardziej prawdopodobne, e w przypadku gdy plik nagwkowy bdzie zawiera bdy, dowiemy si o tym szybciej, zapobiegajc przykrym niespodziankom.

Dodatek A Styl kodowania

607

Straniki doczania w plikach nagwkowych


Straniki doczania (ang. include guards) s uywane zawsze w plikach nagwkowych, zapobiegajc wielokrotnemu doczeniu tego samego pliku nagwkowego w trakcie kompilacji pojedynczego pliku .cpp. S one zaimplementowane za pomoc dyrektywy preprocesora #define i sprawdzenia, czy dany stranik nie zosta ju zdefiniowany. Nazwa stranika jest tworzona na podstawie nazwy pliku nagwkowego, zapisanej wielkimi literami, z kropk zastpion znakiem podkrelenia. Na przykad:
// IncludeGuard.h #ifndef INCLUDEGUARD_H

#define INCLUOE6UARD_H // Zawarto pliku nagwkowego... #endif // INCLUDEGUARD_H Widoczny w ostatnim wierszu identyfikator zosta dodany w celu zwikszenia przejrzystoci. Mimo e niektre preprocesory ignoruj wszelkie znaki znajdujce si po dyrektywie #endif, nie jest to regu, wic identyfikator zosta poprzedzony znakami komentarza.

Wykorzystanie przestrzeni nazw


Naley skrupulatnie wystrzega si zanieczyszczenia" przestrzeni nazw, w ktrej doczany jest plik nagwkowy. A zatem zmieniajc przestrze nazw na zewntrz funkcji lub klasy spowodujemy, e modyfikacja ta bdzie dotyczya kadego pliku, w ktrym doczany jest nasz plik nagwkowy, skutkujc wszelkiego rodzaju problemami. W plikach nagwkowych nie s wic dozwolone adne deklaracje using, znajdujce si poza funkcjami lub klasami, ani adne globalne dyrektywy using. Uycie globalnych dyrektyw using w plikach cpp dotyczy wycznie biecego pliku, wic w ksice s one na og uywane w ceIu zwikszenia czytelnoci kodu, szczeglnie w przypadku maych programw.

Uycie funkcji require() i assure()


Funkcje require() i assure(), zdefiniowane w pIiku require.h, s konsekwentnie uywane w wikszoci przykadw zawartych w ksice, umoliwiajc poprawne informowanie o sytuacjach problemowych. Jeeli znane ci s pojcia warunkw wstpnych (ang. preconditions) i warunkw kocowych (ang. postconditions), wprowadzone przez Bertranda Mayera, zauwaysz, e uycie funkcji require() i assure() w mniejszym lub wikszym stopniu okrela warunki wstpne (zazwyczaj) oraz warunki kocowe (czasami). Tak wic na pocztku funkcji, przed wykonaniem jakichkolwiek

08

Thinking in C++. Edycja polska

instrukcji tworzcych jej ,/dze", sprawdzane s warunki wstpne, by si przekona, e wszystko jest w porzdku i wszystkie niezbdne warunki zostay spenione. Nastpnie wykonywane s instrukcje ,jdzenia" funkcji i czasami weryfikowane s pewne warunki kocowe, by sprawdzi, czy nowy stan danych mieci si w przyjtym zakresie parametrw. Nietrudno zauway, e warunki kocowe sprawdzane s w ksice rzadko, a funkcja assure() suy przede wszystkim do upewnienia si, e otwarcie plikw zakoczyo si powodzeniem.

Wskazwki dla programistw


Dodatek ten jest zbiorem sugestii dotyczcych programowania w jzyku C++. Sformuowaem je na podstawie dowiadcze w dziedzinie pracy dydaktycznej oraz programowania, a take wskazwek grona przyjaci, do ktrych zaliczaj si: Dan Saks (wraz z Tomem Plumem, wspautor C++ Programming Guidelines", Plum Hall, 1991), Scott Meyers (autor ,fTective C++", wydanie drugie, Addison-Wesley, 1998) oraz Rob Murray (autor C++ Strategies & Tactics", Addison-Wesley, 1993). Znajduje si w nim rwnie podsumowanie wielu wskazwek, przedstawionych na kartach ksiki Thinking in C++". 1. Niech przede wszystkim dziaa, a dopiero potem niech dziaa szybko. Jest to prawd, nawet gdy rnasz pewno, ejaki fragment kodujest naprawd wany i bdzie gwnym wskim gardem twojego systemu. Nie rb tego. Niech system bdzie najpierw moliwie jak najprostszym projektem. Dopiero pniej, gdy nie bdzie zbyt szybki, zajmij sijego profilowaniem. Niemal zawsze przekonasz si, e twoje" wskie gardo niejest problemem. Oszczdzaj czas na naprawd wane kwestie. 2. Elegancja zawsze si opaca. To nie tylko sztuka dla sztuki. Powoduje, e uzyskujesz program, ktry nie tylkojest atwiejszy do zbudowania i uruchomienia, ale rwnie do zrozumienia i utrzymania, a toju na wymiar finansowy. Aby w to uwierzy, potrzebne jest pewne dowiadczenie. Mona bowiem odnie wraenie, e prbujc uczyni jaki fragment programu eleganckim, nie pracuje si wydajnie. Na wydajno przyjdzie pora, gdy kod bez problemw zintegruje si z reszt systemu, a w jeszcze wikszym stopniu gdy kod lub system bd modyfikowane. 3. Pamitaj o zasadzie dziel i rzd". Jeeli problem, z ktrym masz do czynienia, jest zbyt zoony, sprbuj wyobrazi sobie podstawowe operacje, ktre realizowaby program, przyjmujc zaoenie istnieniajakiego czarodziejskiego fragmentu", obsugujcegojego najtrudniejsze czci. Ten fragment" jest obiektem napisz najpierw wykorzystujcy go kod, a nastpnie przyjrzyj si samemu obiektowi, zamykajcyego najtrudniejsze elementy w kolejnych obiektach itd.

Dodatek B

Thinklng in C++. Edycja polska

4. Nie przepisuj automatycznie kodu napisanego wjzyku C na kod wjzyku C++, o ile nie zamierzasz znacznie zmienijego funkcjonalnoci (to znaczy nie naprawiaj go, jeeli dziaa poprawnie). Przekompilowanie kodu jzyka C wjzyku C++jest poyteczne, poniewa moe ujawni ukryte w programie bdy. Jednake przepisywanie w jzyku C++ dobrze dziaajcego kodu napisanego wjzyku C moe nie by najlepszym sposobem wykorzystania czasu, chyba e wersja wjzyku C++ nadaje si do wielokrotnego wykorzystania jako kasa. 5. Jeeli posiadasz duy fragment kodu wjzyku C, wymagajcy wprowadzenia zmian, najpierw wyodrbnij tejego czci, ktre nie bdmodyfikowane, by moe zamykajc je w klasach interfejsu programowego" w postaci statycznych funkcji skadowych. Nastpnie skup si na kodzie, ktry bdzie zmieniany, dzielc go na klasy, co uatwi wprowadzanie modyfikacji, wymaganych przez pielgnacj kodu. 6. Oddziel twrc klasy odjej uytkownika (klienta-programisty}. Uytkownik klasy jest klientem" i nie musi (albo nie chce) wiedzie, co dzieje si wewntrz klasy. Twrca klasy powinien by ekspertem w projektowaniu i tworzeniu kasy w taki sposb, aby moga zosta ona wykorzystana nawet przez najbardziej niedowiadczonych programistw, dziaajc nadal niezawodnie w aplikacjach. Korzystanie z biblioteki bdzie atwe tylko wwczas, gdy bdzie ona przejrzysta. 7. Tworzc klas, uywaj w miar moliwoci zrozumiaych nazw. Twoim celem powinno by utworzenie interfejsu klienta-programisty w taki sposb, aby by prosty pojciowo. Sprbuj uy nazw na tyle zrozumiaych, by nie wymagay dodatkowych komentarzy. Aby to uzyska, wykorzystuj przecianie nazw funkcji i argumenty domylne, tworzc intuicyjny, atwy w uyciu interfejs 8. Kontrola dostpu pozwala ci (twrcy klasy) na wprowadzenie w przyszoci zmian, bez potrzeby modyfikacji kodu uytkownika, w ktrym klasa tajest uywana. Zachowaj zatem wszystko prywatne, o ile to moliwe, pozostawiajc publicznymjedynie interfejs klasy. Niech dane bdpubliczne tylko wwczas, gdy jeste do tego zmuszony. Jeeli uytkownik klasy nie potrzebuje dostpu dojakiej funkcji, uczyjprywatn. Jeelijaka cz klasy musi by dostpna klasom pochodnymjako chroniona, to udostpnij w tym celu interfejs w postaci funkcji, nie za dane. Dziki temu zmiany w implementacji tej klasy bd miay minimalny wpyw najej klasy pochodne. 9. Nie popadaj w analityczn niemoc. S kwestie, o ktrych si nie dowiesz, dopki nie rozpoczniesz kodowania i twj system nie stanie sijako tako dziaajcym systemem. Jzyk C++ posiada wbudowane zapory przeciwogniowe pozwl im pracowa dla ciebie. Pomyki, popenione przez ciebie wjakiej klasie lub grupie klas, nie narusz integralnoci caego systemu. 10. Wynikiem analizy i projektowania muszby co najmniej: klasy zawarte w systemie, ich publiczne interfejsy oraz zwizki z innymi klasami szczeglnie z klasami podstawowymi. Jeeli stosowana przez ciebie metodyka programowania generuje co wicej, to odpowiedz sobie na pytanie, czy wszystkie tworzone przez ni elementy maj warto w trakcie ycia programu. Jeeli nie, to wanie ty poniesiesz ich koszt. Czonkowie zespow

Dodatek B * Wskazwki dla programistw tworzcych oprogramowanie zazwyczaj nie zajmujsi sprawami, ktre nie wpywaj na ich wydajno to prawda yciowa, ktrej nie uwzgldniono w wielu metodach projektowych. L. Zanim utworzysz klas, napisz kod, ktryjtestuje, i pozostaw go wewntrz klasy. Automatyzuj uruchamianie testw za pomoc pliku makefile lubjakiego podobnego narzdzia. Wszelkie zmiany bd mogy zosta automatycznie zweryfikowane dziki uruchomieniu kodu testujcego, co pozwoli na natychmiastowe wykrycie bdw. Poniewa wiesz, i posiadasz siatk zabezpieczajc w postaci szkieletu testujcego, bdziesz odwaniejszy we wprowadzaniu gruntownych zmian, gdy odkryjesz konieczno ich dokonania. Pamitaj, e najwiksze udoskonalenia wjzykach programowania wynikajz wbudowanych w nie testw, ktre udostpniajkontrol typw, obsug wyjtkw itp., ale sigaj one tylko do pewnego miejsca. Musisz pj dalej, tworzc niezawodny system za pomoctestw, weryfikujcych cechy specyficzne dla okrelonej klasy lub caego programu. ^2. Zanim utworzysz klas, napisz kod, ktry bdziejtestowa. Upewnisz si dziki temu, e projekt klasy jest kompletny. Jeeli nie potrafisz napisa kodu testujcego klas, oznacza to, e nie wiesz,jak powinna ona wyglda. Ponadto pisanie kodu testujcego nierzadko prowadzi do ujawnienia dodatkowych waciwoci lub ogranicze, ktrych wymaga bdzie klasa czsto nie ujawniaj si one podczas analizy i projektowania.

611

13. Pamitaj o fundamentalnej zasadzie inynierii programowania1: Wszystkie problemy projektowe powstae podczas projektowania oprogramowania mog zosta uproszczone przez wprowadzenie dodatkowego, poredniego poziomu pojciowego. Ta ideajest podstawabstrakcji, podstawowej waciwoci programowania obiektowego. 14. Niech klasy bd tak niepodzielne, jak to tylko moliwe kada klasa powinna mie tyIkojedno, wyranie okrelone zadanie. Jeeli projekt klas lub caego systemu staje si zbyt skomplikowany, rozbij zoone klasy na prostsze. Najbardziej oczywistym wskanikiemjest wielko klasy jeeli klasajest dua, to prawdopodobnie peni zbyt wiele funkcji i powinna zosta podzielona. 15. Przyjrzyj si dugim definicjom funkcji skadowych. Dugie i skomplikowane funkcje srwnie trudne i kosztowne w utrzymaniu i prawdopodobnie prbuj zrealizowa samodzielnie zbyt wiele zada. Funkcj tak naley przynajmniej rozbi na wiele funkcji. Moe to rwnie stanowi sugesti utworzenia dodatkowej klasy. 16. Przyjrzyj si dugim listom argumentw. Powoduj one, e wywoania funkcji strudne do napisania, przeczytania i pielgnacji. Sprbuj przenie funkcj skadowdo klasy, do ktrej pasowaaby bardziej, i (lub) przekazywajej obiekty w charakterze argumentw.

Ktr wyjani mi Andrew Koenig.

L2

Thlnklng In C++. Edycja polska

17. Nie powtarzaj si. Jeeli jaki kod powiela si w wielu funkcjach klas pochodnych, umie go wjednej funkcji klasy podstawowej i wywouj t funkcj w funkcjach klas pochodnych. Nie tylko zmniejszysz w ten sposb wielko kodu, ale rwnie uatwisz propagacj wprowadzonych zmian. Dla zachowania efektywnoci moesz uy w tym celu funkcji inline. Czasami odkrycie takiego wsplnego kodu zwiksza w istotny sposb funkcjonalno interfejsu. 18. Przyjrzyj si instrukcjom switch i acuchowym instrukcjom if-eLse. wiadcz one zazwyczaj o kodowaniu ze sprawdzaniem typw, co oznacza, e na podstawie jakiej informacji o typie wybierany jest kod, ktry bdzie wykonywany (na pierwszy rzut oka dokadny typ moe nie wydawa si oczywisty). Taki kod mona na og zastpi, wykorzystujc do tego celu dziedziczenie i polimorfizm polimorficzne wywoanie funkcji dokona za ciebie sprawdzenia typu, a ponadto bdzie bardziej niezawodne i atwiejsze w rozbudowie. 19. Patrzc z punktu widzenia projektu, znajd i oddziel to, co bdzie ulegao modyfikacjom, od tego, co pozostanie niezmienne. Poszukaj elementw, ktre bdziesz chcia zmienia, nie modyfikujc samego projektu, a nastpnie zamknij je w klasach. Znacznie wicej informacji na temat tej idei mona znale w rozdziale powiconym wzorcom projektowym, zamieszczonym
w drugim tomie ksiki, dostpnym w witrynie http:/flielion.pUonline/ thinking/index. html.

20. Zwr uwag na wariancj. Dwa znaczeniowo rne obiekty mog podejmowa identyczne dziaania albo mie takie same obowizki, w zwizku z czym pokusa uczynieniajednego z nich podklasdrugiego, w celu wykorzystania dziedziczenia, moe wydawa si naturalna. Jest to nazywane wariancj, ale naprawd nie ma adnego usprawiedliwienia dla wymuszania wzajemnych relacji nadklasy i podklasy tam, gdzie ich naprawd nie ma. Lepszym rozwizaniemjest utworzenie oglnej klasy podstawowej, tworzcej interfejs dla obu tych klas, bdcych jej klasami pochodnymi wymaga to nieco wicej miejsca, ale zachowuje si korzyci wynikajce z dziedziczenia. Przy okazji mona take dokonajakiego wanego odkrycia zwizanego z projektem. 21. Zwr uwag na ograniczenia zwizane z dziedziczeniem. Dobre projekty prowadzdo dodawania w klasach pochodnych nowych moliwoci. Podejrzane projekty usuwaj natomiast podczas dziedziczenia istniejce wasnoci, nie dodajc adnych nowych. Reguy sjednak po to, byje ama, i jeli uywasz starej biblioteki klas, to bardziej efektywne moe okaza si ograniczenie istniejcej klasy w jej klasie pochodnej ni zmiana struktury caej hierarchii w taki sposb, by nowa klasa zostaa umieszczona tam, gdzie powinna powyej starej klasy. 22. Nie rozszerzaj podstawowej funkcjonalnoci metodtworzenia klas pochodnych. Jeeli jaki element interfejsu jest dla klasy niezbdny, to powinien on znajdowa si w klasie podstawowej, a nie zosta dodany podczas dziedziczenia. Jeeli dodajesz funkcje skadowe, uywajc do tego celu dziedziczenia, to powiniene jeszcze raz zastanowi si nad swoim projektem.

Dodatek B Wskazwki dla programistw

613

23. Mniej znaczy wicej. Zacznij od minimalnego interfejsu klasy, tak maego i prostego, by rozwizywa on aktualny problem, nie prbujc przewidywa wszystkich sposobw, na jakie mogtaby zosta wykorzystana ta klasa. W trakcie uywania klasy dowiesz si, w jaki sposb musisz rozszerzy jej interfejs. Jednak gdy klasajest uywana, nie mona ograniczyjej interfejsu, nie naruszajc kodu jej uytkownikw. Jeeli trzeba doda wiksz liczb funkcji, nie stanowi to problemu nie wpynie na kod uytkownikw w aden inny sposb ni koniecznojego powtrnej kompilacji. Ale nawet w przypadku gdy nowe funkcje zastpuj funkcjonalnie stare, naley pozostawi bez zmian istniejcy interfejs (funkcjonalno mona poczy na poziomie wewntrznej implementacji). Jeeli zamierzasz rozszerzy interfejs istniejcej funkcji, dodajc do niej wikszliczb argumentw, to pozostaw istniejce argumenty w ich aktualnym porzdku, okrelajc wartoci domylne wszystkich dodatkowych argumentw w taki sposb nie wpynie to na posta istniejcych ju wywoa tej funkcji. 24. Odczytuj nazwy swoich klas na gos, by upewni si, e s one logiczne, okrelajc relacj pomidzy klas podstawow i klas pochodnjako ,jest", a w przypadku obiektw skadowych uywajc pojcia posiada". 25. Gdy wahasz si pomidzy dziedziczeniem i kompozycj, odpowiedz sobie na pytanie, czy bdziesz potrzebowa rzutowania w gr do klasy podstawowej. Jeeli nie, to zamiast dziedziczenia wybierz kompozycj (obiekty skadowe). Pozwoli to na uniknicie koniecznoci wielokrotnego dziedziczenia. Gdy zastosujesz dziedziczenie, uytkownicy bd sdzili, e oczekuje si od nich rzutowania w gr. 26. Czasami wystpuje konieczno dziedziczenia w celu uzyskania dostpu do chronionych skadowych klasy podstawowej. Moe to prowadzi do powstania koniecznoci wielokrotnego dziedziczenia. Jeeli nie jest ci potrzebne rzutowanie w gr, to najpierw utwrz klas pochodn, by uzyska dostp do skadowych chronionych. Nastpnie, zamiast uywa dziedziczenia, utwrz obiekt skadowy powstaej uprzednio klasy we wszystkich klasach, w ktrych jest ci to potrzebne. 27. Klasa podstawowajest na og uywana przede wszystkim dla okrelenia interfejsu wyprowadzonych z niej klas. Tak wic gdy tworzysz klas podstawow, domylnie utwrzjej funkcje skadowejako funkcje czysto wirtualne. Jej destruktor rwnie powinien by czysto wirtualny (by wymusi na klasach pochodnych jego zasonicie); pamitaj jednak, by utworzy tre tego destruktora, poniewa zawsze wywoywane s wszystkie destruktory znajdujce si w hierarchii. 28. Gdy umieszczasz w klasiejak funkcj wirtualn, uczy wszystkie funkcje tej klasy funkcjami wirtualnymi i ulokuj w niej wirtualny destruktor. Pozwoli to na uniknicie niespodzianek w funkcjonowaniu interfejsu. Zacznij usuwa sowa kluczowe virtual dopiero wwczas, gdy bdziesz szuka metod poprawy efektywnoci, a uywany przez ciebie program profilujcy wskae ci taki kierunek.

L4

Thinking in C++. Edycja polska

29. Do wyraenia zmian dotyczcych wartoci uywaj danych skadowych, a w przypadku zmian w zachowaniu funkcji wirtualnych. Innymi sowy, jeeli znajdziesz klas, wykorzystujc zmienne stanu oraz funkcje skadowe, zmieniajce swe dziaanie na podstawie wartoci tych zmiennych, to naleyj prawdopodobnie przeprojektowa, wyraajc rnice w zachowaniu za pomoc klas pochodnych i zasaniania funkcji wirtualnych. 30. Jeeli musisz wykonajak nieprzenonusug, to utwrz dla niej abstrakcj i umie j wewntrz klasy. Ten dodatkowy poziom porednictwa zapobiegnie rozprzestrzenieniu si tego nieprzenonego dziaania na cay program. 31. Unikaj wielokrotnego dziedziczenia. Umoliwia ono znalezienie wyjcia z niekorzystnych sytuacji szczeglnie napraw interfejsu nieprawidowo dziaajcej klasy, nad ktrnie mamy kontroli (zob. drugi tom ksiki). Zanim przystpisz do projektowania wielokrotnego dziedziczenia w swoim systemie, powiniene sta si najpierw dowiadczonym programist. 32. Nie uywaj prywatnego dziedziczenia. Mimo e jest ono w jzyku i czasami wydaje si przydatne, to w poczeniu z identyfikacjtypw podczas pracy programu wprowadza do niego znaczny baagan. Zamiast uywa prywatnego dziedziczenia utwrz wewntrz klasy prywatn skadow. 33. Jeeli dwie klasy sze sobwjaki sposb funkcjonalnie powizane (takjak kontenery i iteratory), to sprbuj uczynijednz nich publiczn zaprzyjanion klas, zagniedon w drugiej klasie, tak jak robi to standardowa bibliotekajzyka C++ w przypadku iteratorw i kontenerw (odpowiednie przykady zaprezentowano w ostatniej czci rozdziau 16.). Nie tylko podkrela to zwizek pomidzy tymi dwoma klasami, ale rwnie pozwala na powtrne uycie nazwy klasy przez zagniedenie jej wewntrz innej klasy. Standardowa bibliotekajzyka C++ wykorzystuje to, definiujc zagniedon klas iteratora wewntrz kadej klasy kontenera, co zapewnia konteneromjednolity interfejs. Innym powodem takiego dziaania moe by zamiar zagniedenia klasy, stanowicej element prywatnej implementacji klasy. W tym przypadku korzyci wynikajc z zagniedania jest raczej ukrycie implementacji ni wymieniony powyej zwizek pomidzy klasami czy ch ochrony przestrzeni nazw przed jej zamieceniem". 34. Przecianie operatorw to tylko cukierek skadniowy" inny sposb wywoania funkcji. Jeeli przecienie operatora nie powoduje, e interfejs klasyjest bardziej przejrzysty i atwiejszy w uyciu, to nie naley tego robi. Utwrz w kadej klasie tyIkojeden operator automatycznej konwersji typu. Na og podczas przeciania operatorw postpuj zgodnie ze wskazwkami i przykadowym formatem, przedstawionymi w rozdziale 12. 35. Nie sta si ofiarprzedwczesnej optymalizacji. W szczeglnoci nie przejmuj si pisaniem (albo unikaniem pisania) funkcji inUne, zamianniektrych funkcji na niewirtualne czy te cyzelowaniem kodu, by by on efektywny, gdyjeste dopiero na etapie tworzenia systemu. Twoim pierwszym celem powinno by dowiedzenie poprawnoci projektu, chyba e sam projekt wymagaju pewnej efektywnoci.

Dodatek B Wskazwki dla programistw

615

36. W normalnym przypadku nie pozwl na to, by kompilator tworzy za ciebie konstruktory, destruktory i operator=. Projektanci klas powinni zawsze dokadnie okreli, co powinny one robi, i przez cay czas zachowywa nad nimi pen kontrol. Jeeli nie chcesz mie konstruktora kopiujcego ani operatora =, to zadeklaruj jejako prywatne. Pamitaj, ejeeli utworzyszjakikolwiek konstruktor, zapobiegnie to wygenerowaniu przez kompilator domylnego konstruktora. 37. Jeeli twoja klasa zawiera wskaniki, to, aby dziaaa ona poprawnie, musisz utworzy dla niej konstruktor kopiujcy, operator= oraz destruktor. 38. Pamitaj, e kiedy dla klasy pochodnej tworzysz konstruktor kopiujcy, musiszjawnie wywoa konstruktor kopiujcy klasy podstawowej (a take obiektw skadowych zob. rozdzia 14.) Jeeli tego nie zrobisz, dla klasy podstawowej (oraz obiektw skadowych) zostanie wywoany domylny konstruktor, a do tego zapewne dysz. Podczas wywoania konstruktora kopiujcego klasy podstawowej naley przekaza mu dziedziczony obiekt, z ktrego odbywa si kopiowanie: 39. Pochodna(const Pochodna& p): Podstawowa(p) {//... 40. Podczas tworzenia dla klasy pochodnej operatora przypisania pamitaj 0 jawnym wywoaniu operatora przypisania klasy podstawowej (zob. rozdzia 14.). Jeeli tego nie zrobisz, kopiowanie nie zostanie zrealizowane (to samo dotyczy obiektw skadowych). Aby wywoa operator przypisania klasy podstawowej, uyj nazwy klasy podstawowej i operatora zasigu: 41. Pochodna& operator=(const Pochodna& p) { 42. Podstawowa::operator=(p); 43. Jeeli chcesz zminimalizowa konieczno powtrnej kompilacji w trakcie tworzenia duego systemu, uyj techniki wykorzystujcej klasy-uchwyty (zwane rwnie kotem z Cheshire"), zademonstrowane w rozdziale 5., a pniej usu je tylko w przypadku, gdyby efektywano wykonania programu stanowia problem. 44. Unikaj preprocesora. Uywaj zawsze sowa kluczowego const do podstawiania wartoci, a funkcji inUne zamiast makroinstrukcji. 45. Zachowaj zasigi moliwiejak najmniejsze, abyjak najbardziej zminimalizowa widoczno i czas twoich obiektw. Zabieg ten zmniejsza prawdopodobiestwo uycia obiektu w niewaciwy sposb, a take ogranicza moliwoci powstania ukrytych, trudnych do wykrycia bdw. Zamy na przykad, e masz kontener 1 fragment kodu, ktry porusza si po zawartym w nim elementach. Jeeli skopiujesz ten kod z zamiarem uycia go z nowym kontenerem, moesz przypadkowo uy wielkoci starego kontenerajako grnej granicy liczby elementw zawartych w nowym kontenerze. Jeeli jednak stary kontener bdzie znajdowa si poza zasigiem, bd zostanie wykryty na etapie kompilacji. 46. Unikaj zmiennych globalnych. Zawsze staraj si umieszcza dane w obrbie klas. Bardziej prawdopodobne jest naturalne wystpienie funkcji globalnych ni zmiennych globalnych, chocia moesz pniej odkry, e jaka funkcja globalna powinna by raczej statyczn funkcj skadow klasy.

16

Thlnking In C++. Edycja polska

47. Jeeli musisz zadeklarowa jak klas lub funkcj, zawart w bibliotece, zawsze rb to doczajc jej plik nagwkowy. Jeeli na przykad zamierzasz napisa funkcj, ktra zapisuje informacje do strumienia typu ostream, to nigdy nie deklaruj tego strumienia, uywajc niepenej specyfikacji typu w rodzaju: 48. class ostream; 49. Taki postpowanie powoduje, e kod staje si podatny na zmiany dotyczce reprezentacji (na przykad identyfikator ostream mgby by w rzeczywistoci zadeklarowany za pomoc sowa kluczowego typedef). Zawsze natomiast uywaj pliku nagwkowego: 50. #include <iostream> 51. Podczas tworzenia wasnych klas, jeli biblioteka jest dua, udostpnij jej uytkownikowi skrcon posta pIiku nagwkowego, zawierajcego niekompletne specyfikacje typw (czyli deklaracje nazw klas), na wypadek, gdyby uywa on wycznie wskanikw (pozwoli mu to na przyspieszenie kompilacji). 52. Wybierajc typ wartoci, zwracanej przez przeciony operator, zastanw si, co si stanie, gdy wyraenia s poczone acuchowo. Zwr kopi lub referencj do l-wartoci (uywajc instrukcji return *this), dziki czemu operator bdzie mg zosta wykorzystany w wyraeniach acuchowych (A = B = C). Gdy definiujesz operator=, pamitaj o przypadku x=x. 53. Gdy piszesz funkcj, preferuj przekazywanie jej argumentw w postaci referencji do staych. Dopki nie musisz modyfikowa przekazywanego obiektu, taki sposb przekazywania argumentwjest najlepszy. Cechuje go bowiem prostota przekazywania przez warto, lecz nie wymaga kosztownych konstrukcji i destrukcji, zwizanych z tworzeniem obiektu lokalnego. Zwykle projektujc i tworzc swj system, nie powiniene przejmowa si zbytnio kwestiami efektywnoci, ale nawyk taki gwarantuje powodzenie. 54. Zwracaj uwag na obiekty tymczasowe. Gdy zaley ci na wydajnoci, led pilnie tworzenie obiektw tymczasowych, szczeglnie w przypadku przeciania operatorw. Jeeli konstruktor i destruktor twojej klasy s skomplikowane, to koszt tworzenia i niszczenie obiektw tymczasowych moe by wysoki. Gdy zwracasz warto z funkcji, zawsze staraj si tworzy obiekt w miejscu", bez wywoywania konstruktora w instrukcji return, dokonujc nastpujcego zapisu: 55. retum MojTyp(i,j); 56. a nie taki: 57.MojTypx(i,j); 58. return x; 59. Pierwsza z widocznych powyej instrukcji return (wykorzystujca tak zwan optymalizacj zwracania wartoci) eliminuje wywoanie konstruktora kopiujcego oraz destruktora.

podatek B Wskaz6wKI dla programistw

617

60. Gdy tworzysz konstruktory, we pod uwag wyjtki. W najlepszym razie konstruktor nie powinien zrobi niczego, co powoduje zgoszenie wyjtku. Dobrym rozwizaniemjest rwnie przypadek, w ktrym klasajest skomponowana i dziedziczy wycznie po solidnych klasach, dziki czemu dokonaj one automatycznie porzdkowania w razie zgoszenia wyjtku. Jeeli musisz uywa zwykych wskanikw, to jeste odpowiedzialny za wykrycie swoich wyjtkw i zwolnienie wszelkich wskazywanych przez nie zasobw, zanim zgosisz wyjtek w swoim konstruktorze. Jeli konstruktor musi zakoczy si niepowodzeniem, to waciwym dziaaniemjest zgoszenie wyjtku. 61. Wykonuj w konstruktorach wycznie dziaania absolutnie konieczne. Nie tylko powoduje to mniejszy narzut, zwizany z wywoaniem konstruktora (spord ktrych wiele moe znajdowa si poza twoj kontrol), ale rwnie jest mniej prawdopodobne, e konstruktory takie zgosz wyjtki lub bd przyczyn problemw. 62. Obowizkiem destruktorajest zwolnienie zasobw przydzielonych w czasie ycia obiektu, a nie tylko podczas konstrukcji. 63. Uywaj hierarchii wyjtkw, najlepiej wyprowadzonych ze standardowej hierarchii wyjtkw jzyka C++ i zagniedonych jako kIasy publiczne wewntrz tych klas, ktre zgaszaj wyjtki. Dziki temu osoba wyapujca wyjtki moe wykrywa szczeglne typy wyjtkw, a nastpnie wyjtki typu podstawowego. Jeeli dodasz nowe, pochodne wyjtki, to istniejcy kod klienta nadal je wyapie, uywajc ich typu podstawowego. 64. Zgaszaj wyjtki przez warto, a wyapuj je przez referencj. Pozwl, by mechanizm obsugi wyjtkw zaj si obsug pamici. Jeeli bdziesz zgasza wskaniki do obiektw wyjtkw, ktre zostay utworzone na stercie, to kod wychwytujcyje musi wiedzie o tym, e powinien zniszczy wyjtek, co niejest najlepszym rozwizaniem. Jeeli wyapujesz wyjtek przez warto, wywoujesz dodatkowe konstrukcje i destrukcje, a co gorsza pochodna cz wykrytego przez ciebie obiektu wyjtku moe zosta okrojona podczas rzutowania w gr. 65. Nie pisz wasnych szablonw klas, dopki nie musisz. Zajrzyj najpierw do standardowej biblioteki jzyka C++, a nastpnie poszukaj ich u producentw, tworzcych specjalizowane narzdzia. Biegle opanuj ich uywanie, co znacznie zwikszy twojproduktywno. 66. Podczas tworzenia szablonw przyjrzyj si kodowi, ktry nie zaley od typu, umieszczajc go w klasie podstawowej, niebdcej szablonem; zapobiegniesz w ten sposb niepotrzebnemu rozrastaniu si kodu. Uywajc dziedziczenia lub kompozycji, moesz utworzy szablony, w ktrych wikszo kodu zaley od typu i jest zatem niezbdna. 67. Nie uywaj funkcji zawartych w pliku nagwkowym <cstdio>, takichjak printf(). Naucz si natomiast uywa strumieni wejcia-wyjcia s one bezpieczne dla typw, mona je rozszerza o obsug nowych, s take znacznie bardziej efektywne. Twj trud bdzie regularnie nagradzany. Zawsze raczej uywaj bibliotek C++ ni bibliotekjzyka C.

618

Thinking in C++. Edycja polska

68. Unikaj typw wbudowanychjzyka C. Sone obsugiwane przezjzyk C++ w celu zapewnienia wstecznej zgodnoci, ale s znacznie mniej solidne ni klasy jzyka C++. Uywanie ich spowoduje wiec wydueniu si czasu, powiconego poszukiwaniu bdw. 69. Gdy uywasz typw wbudowanychjako zmiennych globalnych lub automatycznych, nie definiuj ich, dopki nie bdziesz ich mg rwnie zainicjalizowa. Definiuj kad zmienn w oddzielnym wierszu, od razu j inicjalizujc. Gdy definiujesz wskaniki, umie znak *" zaraz za nazw typu. Moesz to robi w bezpieczny sposb pod warunkiem, e w kadym wierszu definiujesz tylkojednzmienn. Taki styl wydaje si mniej mylcy dla osoby czytajcej kod. 70. Zapewnij, by inicjalizacja dotyczya wszystkich aspektw twojego kodu. Dokonuj inicjalizacji wszystkich skadowych na licie inicjatorw konstruktora, nawet w przypadku typw wbudowanych (uywajc wywoa pseudokonstruktorw). Wykorzystywanie listy inicjatorw konstruktora jest czsto znacznie bardziej efektywne w przypadku inicjalizacji obiektw podrzdnych w przeciwnym razie wywoywany jest bowiem domylny konstruktor, co prowadzi do wywoania innych funkcji skadowych (przypuszczalnie operatora =) w ramach przygotowa do wymaganej przez ciebie inicjalizacji. 71. Nie uywaj definicji obiektu w postaci MojTyp a = b;. Zapis taki jest gwnym rdem nieporozumie, poniewa powoduje wywoanie konstruktora, a nie operatora =. Dlajasnoci uywaj zamiast tego zawsze precyzyjnej postaci definicji: MojTyp a(b);. Rezultatjest taki sam, ale nie wprowadzisz w bd innych programistw. 72. Uywaj jawnego rzutowania, opisanego w rozdziale 3. Rzutowanie ignoruje normalny system typw, bdc miejscem potencjalnych bdw. Poniewa jawne rzutowania dzieljedyne dostpne w jzyku C rzutowanie na kategorie dobrze oznaczonych rzutowa, kady, kto uruchamia i utrzymuje kod, moe w atwy sposb odnale wszystkie miejsca, w ktrych wystpienie bdw logicznych jest najbardziej prawdopodobne. 73. Aby program by niezawodny, pewny musi by kadyjego element. Wykorzystuj wszystkie narzdzia udostpniane przezjzyk C++, m.in. kontrol dostpu, wyjtki, poprawno stosowania staych, sprawdzanie typw, w kadej tworzonej przez siebie klasie. Dziki temu tworzc system, w bezpieczny sposb przeniesiesz si na nastpny poziom abstrakcji. 74. Wykorzystuj poprawno stosowania staych. Umoliwi to kompilatorowi wskazanie bdw, ktre w innym przypadku byyby trudno uchwytne i nieatwe do odnalezienia. Praktyka ta wymaga pewnej dyscypliny i musi by stosowana konsekwentnie we wszystkich klasach, ale jest opacalna. 75. Wykorzystuj kontrol bdw zapewnianprzez kompilator. Dokonuj wszystkich kompilacji z wczonymi wszystkimi moliwymi ostrzeeniami i poprawiaj swj kod, by je wszystkie usun. Pisz kod, ktry wykorzystuje raczej bdy i ostrzeenia czasu kompilacji ni czasu wykonania programu (na przykad nie uywaj zmiennej liczby argumentw wywoa funkcji,

Podatek B Wskazwki dla programistw ktre powoduj zablokowanie wszelkiej kontroli typw). Do uruchamiania programu uywaj makroinstrukcji assert(), ale w stosunku do bdw czasu wykonania wykorzystuj wyjtki.

619

76. Przedkadaj bdy czasu kompilacji nad bdy czasu wykonania. Prbuj obsugiwa bdy moliwiejak najbliej miejsca ich powstania. Staraj si raczej poradzi sobie z nimi na miejscu ni zgasza wyjtek. Wyapuj wszystkie wyjtki w najbliszej procedurze obsugi, ktra posiada dostateczn liczb informacji, by sobie z nimi poradzi. Zrb wszystko, co w twojej mocy, z wyjtkiem zgoszonym na biecym poziomie jeeli nie rozwie to problemu, zgo go ponownie (wicej szczegw na ten temat mona znale w drugim tomie ksiki). 77. Jeeli wykorzystujesz specyfikacj wyjtkw (informacje na ten temat znajduj si w drugim tomie ksiki, ktry mona pobra z witryny http:/flielion.pUonline/thinking/index.html), zainstaluj wasn funkcj unexpected(), wykorzystujc do tego celu funkcj set_unexpected(). Funkcja unexpected() powinna rejestrowa bd, zgaszajc ponownie wyjtek. Dziki temu w przypadku, gdy istniejce funkcje zostan zasonite i zacznzgasza wyjtki, bdziesz dysponowa nagraniem podejrzanego", co pozwoli na modyfikacj wywoujcego kodu w celu obsugi wyjtku. 78. Utwrz zdefiniowansamodzielnie funkcj terminate() (sygnalizujcbd programisty), ktra rejestruje bd, bdcy przyczyn wyjtku, a nastpnie zwalnia zasoby i powoduje zakoczenie pracy programu. 79. Jeeli destruktor wywoujejakiekolwiek funkcje, to mogone zgosi wyjtki. Destruktory nie mogzgasza wyjtkw (moe to skutkowa wywoaniem funkcji terminate(), oznaczajcej bd programowy), w zwizku z czym kady destruktor, ktry wywouje funkcje, musi wyapywa i obsugiwa wasne wyjtki. 80. Nie twrz wasnych uzupenie" prywatnych danych skadowych (poprzedzajcych podkrele, notacji wgierskiej itp.), chyba e masz do czynienia z mnstwem istniejcychju wczeniej wartoci globalnych w przeciwnym razie pozwl wyznaczy zasigi klasom i przestrzeniom nazw. 81. Przyjrzyj si przecieniom. Funkcje nie powinny warunkowo wykonywa kodu na podstawie wartoci swoich argumentw, niezalenie od tego, czy s one domylne, czy te nie. W takim przypadku naley, zamiastjednej funkcji, utworzy dwie lub wiksz liczb przecionych funkcji. 82. Ukryj wskaniki wewntrz klas kontenerowych. Pobieraj je tylko wwczas, gdy zamierzasz wykonywa operacje bezporednio na nich. Wskaniki zawsze stanowiy rdo licznych bdw. Gdy uywasz operatora new, sprbuj umieci zwrcony wskanik w kontenerze. Preferuj rozwizanie, w ktrym kontenerjest wacicielem" wskanikw, wic odpowiada rwnie za ich sprztanie. Jeszcze lepiej bdzie, gdy opakujesz" wskanik w klas jeeli chcesz, aby nadal wygldajak wskanik, przeci operator-> oraz operator*. Jeeli jest ci potrzebny samodzielny wskanik, zawsze go zainicjalizuj najlepiej adresem obiektu, ale wartocizerow, gdyjest to konieczne. Przypisz mu warto zerow, gdy usuwasz wskazywany przez niego obszar pamici, by zapobiec wielokrotnemu powtarzaniu tej czynnoci.

M)

Thlnklng In C++. Edycja polska

83. Nie przeciaj globalnych operatorw new i delete zawsze rb to w stosunku do klasy. Przecienie globalnych wersji operatorw wpynie na cay projekt klienta-programisty co, nad czym powinni mie kontrol wycznie twrcy projektu. Gdy przeciasz operatory new i delete w stosunku do klas, nie zakadaj, e znasz wielko obiektu kto mg bowiem utworzy klas pochodn. Wykorzystaj dostarczony argument. Jeeli robisz co szczeglnego, zastanw si, jaki bdzie to miao wpyw na klasy pochodne. 84. Zapobiegaj okrajaniu obiektw. Rzutowanie w gr obiektw przez warto waciwie nigdy nie ma sensu. Aby zapobiec rzutowaniu w gr przez warto, umie w swojej klasie podstawowej funkcje czysto wirtualne. 85. Czasami wystarczy prosta agregacja. System zapewnienia wygody pasaerowi" linii lotniczej skada si z rozcznych elementw: fotela, klimatyzacji, sprztu wideo itd., a w samolocie musisz utworzy wiele takich systemw. Czy bdziesz tworzy prywatne skadowe i zbudujesz dla nich zupenie nowy interfejs? Nie w tym przypadku poszczeglne elementy s rwnie skadnikami publicznego interfejsu, naley zatem uczynije publicznymi obiektami skadowymi. Obiekty te posiadaj swoje implementacje, ktre nadal pozostajbezpieczne. Pamitaj, e taka prosta agregacja niejest czsto stosowanym rozwizaniem, ale si zdarza.

Zalecana literatura
Dodatkowe rda informacji

Dodatek C

Jzyk C
Thinking in C: Foundations for Java & C++", Chuck Allison (MindView, Inc., 2000, kurs na CD-ROM-ie, dostpny rwnie w witrynie www.BruceEckel.com), Jest to niezbyt wyczerpujcy kurs przygotowujcy do nauki jzykw Java oraz C++; zawiera wykad oraz slajdy dotyczce podstaw jzyka C. Zawiera jedynie informacje niezbdne do rozpoczcia nauki innych jzykw. Dodatkowe podrozdziay, dotyczce konkretnych jzykw, wprowadzaj cechy jzykw C++ i Java i s przeznaczone dla osb zamierzajcych programowa w tych jzykach. Zalecane przygotowanie czytelnika: pewne dowiadczenie w programowaniu w jzykach wysokiego poziomu, takich jak Pascal, BASIC, Fortran albo LISP (moliwe jest przebrnicie przez ten CDROM bez takiego dowiadczenia, ale kurs nie zosta pomylany jako wprowadzenie do podstaw programowania).

Oglnie o jzyku C++


The C++ Programming Language", wydanie trzecie, Bjarne Stroustrup, AddisonWesley, 1997. W pewnym stopniu celem mojej ksiki byo przyblienie czytelnikowi moliwoci posuenia si ksik Bjarna jako informatorem. Poniewa ksika ta stanowi opis jzyka, napisany przez autora tego jzyka, to na og siga si po ni, by wyjani jakie wtpliwoci dotyczce oczekiwa zwizanych z jzykiem C++. Jeeli nabrae ju pewnej wprawy w uywaniu tego jzyka i zamierzasz zaj si nim powanie, to bdzie ci ona potrzebna. C++ Primer", wydanie trzecie, Stanley Lippman i Josee Lajoie, Addison-Wesley, 1998. Nie jest to bynajmniej ksika dotyczca podstaw jzyka liczy wiele stron

22

Thinking in C++. Edycja polska i zawiera mnstwo szczegw. Zawsze po ni sigam (oprcz ksiki Stroustrupa), gdy prbuj rozstrzygn jak kwesti. Thinking in C++" powinna dostarczy podstaw do zrozumienia zarwno C++ Primer", jak i ksiki Stroustrupa. C & C++ Code Capsules", Chuck Allison, Prentice-Hall, 1998. Autor zakada, e znaszjujzyki C i C++; przedstawia on pewne kwestie, ktre moge zaniedba albo ktrych moge nie zrozumie zbyt dobrze za pierwszym razem. Dziki ksice mona wypeni luki w znajomoci zarwno jzyka C, jak i C++. The C++ Standard". Jest to dokument, nad ktrym komitet standaryzacyjny tak ciko pracowa przez te wszystkie lata. Niestety, nie jest on dostpny bezpatnie. Mona go jednak naby w postaci dokumentu elektronicznego w formacie PDF za jedyne 18 USD w witrynie www.cssinfo.com.

(siki, ktre napisaem


Ksiki wymieniono w kolejnoci publikacji. Nie wszystkie spord wymienionych pozycji s obecnie dostpne. Computer Interfacing with Pascal & C", wydana nakadem wasnym pod egid wydawnictwa Eisys, 1988 (ksika jest dostpna wycznie za porednictwem witryny www.BruceEckel.com). Wprowadzenie do elektroniki z czasw, gdy wci krlowa CPM, a DOS by parweniuszem. Do realizacji rnych projektw elektronicznych uywaem jzykw wysokiego poziomu, a nierzadko rwnie portu rwnolegego mojego komputera. Ksika stanowi adaptacj moich artykuw, zamieszczanych w pierwszym i zarazem najlepszym czasopimie, z ktrym wsppracowaem Micro Cornucopia (parafrazujc Larry'ego O'Briena, wieloletniego wydawc Software Development Magazine, byo to najlepsze, wydawane kiedykolwiek czasopismo na temat komputerw snuli nawet plany zbudowania robota w doniczce!). Niestety, Micro C przestao istnie na dugo, zanim pojawi si Internet. Ksika ta bya dla mnie wyjtkowo satysfakcjonujcym dowiadczeniem wydawniczym. Using C++", OsborneMcGraw-Hill, 1989. Jedna z pierwszych ksiek na temat jzyka C++. Nakad wyczerpany ukazao si drugie wydanie, pod zmienionym tytuem C++ Inside & Out". C++ Inside & Out", OsborneMcGraw-Hill, 1993. Jak ju wspomniaem, jest to w rzeczywistoci drugie wydanie ksiki Using C++". Zawarty w tej ksice opis jzyka C++jest w miar dokadny, ale pochodzi on mniej wicej z roku 1992 i celem Thinking in C++" byo zastpienie tej ksiki. Wicej na jej temat mona si dowiedzie w witrynie www.BruceEckel.com (dostpne stam rwnie kody rdowe). Thinking in C++", wydanie pierwsze, Prentice-Hall, 1995. ,JBIack Belt C++, the Master's CoUection", pod redakcj Bruce'a Eckela, M&T Books, 1994. Nakad wyczerpany. Zbir rozdziaw, napisanych przez rne znakomitoci zwizane z jzykiem C++, przygotowany na podstawie ich prezentacji na Software Development Conference, dokonanej w grupie tematycznej C++, ktrej przewodniczyem. Widok okadki tej ksiki skoni mnie do przejcia kontroli nad wszystkimi przyszymi projektami okadek.

Dodatek C * Zalecana literatura

623

Thinking in Java", wydanie drugie, Prentice-Hall, 20001. Pierwsze wydanie tej ksiki otrzymao w 1999 roku nagrody: Productivity Award, przyznan przez Software Development Magazine, oraz Editor's Choice Award, wrczan przez Java Developer's Journal. Ksik mona pobra z witryny www.BruceEckel.com.

Gbia i mroczne zauki


Ksiki te umoliwiaj pogbienie wiedzy o jzyku, pomagajc w unikniciu typowych puapek, towarzyszcych nieodcznie tworzeniu programw wjzyku C++. ,,Effective C++" wydanie drugie, Scott Meyers, Addison-Wesley 1998 oraz More Effective C++", Scott Meyers, Addison-Wesley, 1996. Klasyczne, obowizkowe teksty, dotyczce rozwizywania powanych problemw i projektowania kodu w jzyku C++. W ksice Thinking in C++" prbowaem przej i opisa wiele poj zawart y c h w tych ksikach, ale niejestem tak naiwny, by sdzi, e mi si to udao. Jeeli spdzisz odpowiednio duo czasu z jzykiem C++, to z pewnoci signiesz po te pozycje. Dostpne s one rwnie na pytach CD-ROM. Ruminations on C++", Andrew Koenig i Barbara Moo, Addison-Wesley, 1996. Andrew wsppracowa ze Stroustrupem, zajmujc si wieloma kwestiami zwizanymi z jzykiem C++, i zyska status autorytetu w tej dziedzinie. Odkryem rwnie, e precyzjajego wskazwekjest inspirujca. Przez lata wiele si od niego nauczyem zarwno dziki lekturze ksiki, jak i osobistym kontaktom. Large-Scale C++ Software Design", John Lakos, Addison-Wesley, 1996. Omawia problemy i odpowiada na pytania, ktre napotkasz tworzc due projekty, a czsto rwnie nieco mniejsze. C++ Gems", pod redakcj Stana Lippmana, publikacje SIGS, 1996. Wybr artykuw z The C++ Report. The Design & Evolution ofC++", Bjarne Stroustrup, Addison-Wesley, 1994. Informacje pochodzce od twrcy jzyka C++, dotyczce przyczyn podjcia przez niego rnych decyzji projektowych. Niejest wprawdzie niezbdna, ale wartojprzeczyta.

Analiza i projektowanie
,,Extreme Progranuning Explained", Kent Beck, Addison-Wesley, 2000. Uwielbiam t ksik. Owszem, stosuj na og radykalne metody, ale zawsze przeczuwaem, e mgby istnie jaki zupenie inny, znacznie lepszy proces tworzenia oprogramowania i myl, e programowanie ekstremalne jest bU,skie tej idei. Jedyn ksik, ktra wywara na mnie podobny wpyw, bya ,J*eopleWare" (omwiona po1

Przetumaczona ksika zostaa wydana w 2001 r. nakadem Wydawnictwo Helion pt. Thinking in Java. Edycja polska".

Thinking in C++. Edycja polska

niej). Opisywaa przede wszystkim rodowisko i poruszaa zagadnienia kultury organizacyjnej. ,xtreme Programming Explained" dotyczy programowania, odrzucajc wikszo przyjtych rozwiza, nawet najnowsze wynalazki" w tej dziedzinie. Autor posuwa si nawet do twierdzenia, i diagramy s w porzdku, pod warunkiem, e nie spdza si nad nimi zbyt wiele czasu i zamierza si je wszystkie wyrzuci (ksika nie ma na okadce ,^probujacej pieczci UML"). Mgbym zdecydowa si na prac w firmie wycznie na podstawie tego, czy stosowane jest w niej programowanie ekstremalne. Niewielka ksika, zawierajca krtkie rozdziay, atwa w lekturze i ekscytujca, gdy si o niej rozmyla. Zaczynasz wyobraa sobie prac w takiej atmosferze i pojawia si wizja zupenie innego wiata. UML DistUled", Martin Fowler, wydanie drugie, Addison-Wesley, 2000. Przy pierwszym kontakcie jzyk UML jest zniechcajcy, poniewa jest w nim tyle diagramw i szczegw. Wedug Fowlera, wikszo z tych elementw jest niepotrzebna, wic przechodzi on od razu do istoty. W przypadku wikszoci projektw wystarczy tylko znajomo kilku narzdzi, umoliwiajcych tworzenie diagramw. Fowler chce uzyska dobry projekt, nie przejmujc si wszystkimi elementami, ktre maj to umoliwi. Cienka, przyjemna w lekturze ksika pierwsza, ktr naley kupi, aby zrozumie UML. The Unified Software DeveIopment Process", Ivar Jacobsen, Grady Booch, i James Rumbaugh, Addison-Wesley, 1999. Spodziewaem si, e ksika nie bdzie mi si podoba. Wygldaajak nudne szkolne podrczniki. Byem mile zaskoczony tylko niektre fragmenty zawieray wyjanienia, ktre sprawiay wraenie, jakby nie byy zrozumiae dla autorw. Przewaajca cz ksiki jest nie tylko przejrzysta, ale rwnie przyjemna w lekturze. A co najwaniejsze, opisany w niej proces ma wymiar praktyczny. Nie jest to programowanie ekstremalne (i nie cechuje go jego jasno w sprawie testw), ale s tu silnie zaznaczone elementy UML. Nawet gdy nie moesz zaadaptowa programowania ekstremalnego, to wikszo doczya ju do jego zwolennikw, zjednoczonych pod hasem UML jest dobry'.' (niezalenie od ich rzeczywistego z nim dowiadczenia), wic rwnie ty prawdopodobnie bdziesz w stanie go przyj. Sdz, e ksika ta mogaby by przewodnikiem po jzyku UML, czyli t, ktr naley przeczyta po UML Distilled" Fowlera, aby pozna wicej szczegw. Zanim wybierzesz ktrkolwiek z metod, warto pozna punkt widzenia kogo, kto nie prbuje nam adnej z nich sprzeda. atwo jest zastosowa metod, nie rozumiejc tak naprawd, czego si od niej oczekuje i w jaki sposb bdzie pomocna. Przekonujce wydaje si to, e inni jej uywaj. Jednake ludzie kieruj si osobliwym przesdem jeli chc wierzy, e co rozwie ich problemy, to bd tego prbowa (eksperymentowanie jest godne pochway). Jeeli jednak nie doprowadzi to do rozwizania problemu, zdwoj wysiki, ogaszajc wszystkim swe cudowne odkrycie (zaprzeczenie i to ju nie jest dobre). Czynione tu zaoenie moe polega na tym, e gdy inni pyn wraz z tob t sam odzi, nie czujesz si samotny, nawet jeeli ta d zmierza donikd (albo tonie). Nie zamierzam sugerowa, e wszystkie metodyki wiod donikd; naley jednak uzbroi si po zby w narzdzia psychiczne", ktre pozwol ci pozosta w fazie eksperymentowania (to nie dziaa sprbujmy czego innego"), nie przechodzc w faz zaprzeczenia, (nie, to naprawd nie jest problem wszystko w porzdku, nie musimy niczego zmienia"). Myl, e wymienione poniej ksiki, przeczytane zanim wybierzeszjak metod, dostarczci odpowiednich narzdzi.

Dodatek C * Zalecana lfteratura

625

Software Creativity", Robert Glass, Prentice-Hall, 1995. Najlepsza ze znanych mi ksiek, omawiajca perspektywy metodyk. Jest to zbir krtkich esejw i artykuw, ktre Glass napisa lub otrzyma ^ednym z autorw jest P.J. Plauger). S one rezultatem wielu lat rozmyla i studiw powieconych temu tematowi. Autor nie zbacza z tematu ani nie zanudza czytelnika, lecz w atrakcyjnej formie przedstawiajedynie to, co niezbdne. W ksice nie brak rwnie konkretw zawiera setki odnonikw do innych artykuw i ksiek. Wszyscy programici i menederowie powinni j przeczyta, nim ugrzzn w bagnie metodyk. Software Runaways: Monumental Software Disasters", Robert Glass, PrenticeHall, 1997. Ksika szczliwie wydobywa na wiato dzienne to, o czym si zazwyczaj nie wspomina o projektach, ktre upadaj, i to w sposb spektakularny. Myl, e wikszo z nas wci myli: mnie si to nie moe zdarzy" (albo to si nie moe^owtrzy") i sdz, e nie jest to korzystne. Pamitajc, e wszystko moe si zawali, masz duo lepszy punkt wyjcia do tego, aby sprawi, by wszystko poszo dobrze. Object Lessons", Tom Love, SIGS Books, 1993. Jeszcze jedna dobra perspektywiczna" ksika. Peopleware", Tom Demarco i Timothy Lister, wydanie drugie, Dorset House, 1999. Mimo e autorzy maj przygotowanie w zakresie tworzenia oprogramowania, ksika dotyczy projektw i zespow programistycznych. Skupia si jednak bardziej na ludziach i ich potrzebach ni na technologii i jej wymogach. Opowiada o ksztatowaniu rodowiska, w ktrym ludzie bd szczliwi i produktywni, a nie o tym, jakie powinni spenia warunki, by by sprawnymi trybami w maszynie. Ten ostatni pogld odnosi si, jak sdz, do programistw umiechajcych si i przytakujcych podczas wprowadzania w firmie metody XYS, a potem robicych po cichu to samo, co wczeniej. ,,Complexity", M. Mitchell Waldrop, Simon & Schuster, 1992. Ksikajest kronik spotka grupy naukowcw reprezentujcych rne dyscypliny, ktre odbyy si w Santa Fe, w Nowym Meksyku. Uczestnicy dyskutowali o problemach, ktrych nie potrafi rozwiza poszczeglne dyscypliny nauki (rynek giedowy w ekonomii, powstawanie ycia w biologii, przyczyny zachowania si ludzi w socjologii itd.). W toku spotka wypracowali oni interdyscyplinarne ujcie tych problemw, obejmujce wiedz z dziedziny fizyki, ekonomii, chemii, matematyki, informatyki, socjologii i innych nauk. A co waniejsze, powstaj nowe sposoby mylenia o takich niezwykle zoonych problemach dalekie od matematycznego determinizmu oraz iluzji, e napiszemy rwnanie, dziki ktremu zdoamy przewidzie wszelkie zachowania. S one nastawione na obserwacj i szukanie wzorcw, a nastpnie prby ich naladowania wszelkimi dostpnymi rodkami (ksika opisuje, midzy innymi, genez algorytmw genetycznych). Wierz, e taki sposb mylenia jest uyteczny, w miar jak powstaj metody zarzdzania coraz bardziej skomplikowanymi projektami programistycznymi.

Thinking in C++. Edycja polska

Skorowidz
#define, 127, 160, 200,267, 271 #endif, 200 #ifdef, 160, 200, 258 #include, 74,98 #undef, 160 argumenty, 71 domylne, 249, 258 konstruktora, 325 przekazywanie przez referencj, 117 przez warto, 115 wierszapolece, 155 argumenty-wypeniacze, 259 argv, 154 argv[0], 155 arytmetykawskanikw, 157 ASCn,81,307 asembler, 70, 297 asm, 143 assert(), 162, 183,239,316 assure(), funkcja, 607 atexit(), funkcja, 326 ATM, 47 atof(),funkcja, 155 atoi(), funkcja, 155 atol(), funkcja, 155 auto, 330 automatic counting, 243 automatyczna konwersja typw, 425 automatyczne zliczanie, 243

abort(), funkcja, 326 abstrakcja, 26 danych, 179 abstrakcyjne klasy podstawowe, 518 typydanych,28, 108, 195 access specifiers, 30, 212 ADA, 557 adres, 112 funkcji, 163 obiektu,312 powrotny, 366 wirtualnych funkcji, 511 aggregate, 242 aggregate initialization, 242 agregacja, 31 agregat, 89, 242, 270 Algol, 19, 359 algorytm, 45, 593 oglny, 593 Allison,Chuck,621,622 alternate linkage specification, 352 analiza, 42 skadniowa, 70 wymaga, 45 and, 144 and_eq, 144 anonimowe argumenty, 96 anonymous union, 257 APL, 26 argc, 155, 156

B
bad_alloc, 453, 457 bajt, 109 BASIC, 26, 61 BCD, 109 Beck, Kent, 623 bezpieczestwo, 59, 229 stae, 269 biblioteki, 30, 60, 69, 70, 76, 180 funkcji jzyka C, 98 iostream, 346 kIas kontenerowych, 20

628
biblioteki komercyjne, 181 napisane w czystym C, 78 plik nagwkowy, 76 program zarzdzajcy, 99 SGI STL, 88 standardowe, 77 tworzenie, 99 wtkw POSIX, 78 binary-codeddecimal, 109 binding, 506 bit, 109 bitand, 144 bitor, 144 blob, 287 blok dostpu, 219 pamici, 231 bdy.41, 179,229,600 projektowe, 65 Booch, Grady, 624 bool, 110, 349 brakpamieci,451 break, 103

Thinking in C++. Edycja polska compl, 144 Composite, 376 composition, 467 console input, 84 const, 19, 127, 267, 270, 277, 288, 451,546 const char*, 280 const correctness, 293 const_cast, 139 constant folding, 268 Constantine Larry, 57 constraint-based programming, 26 constructor initializer list, 471 continue, 103 copy-constructor, 344, 370 copy-on-write, 420 cout,78,86,91,346,419,455 cpp, 172 CRC, 49 cstdio, 300 cstdlib, 84 cstring,218,429 cykl projektowy, 53 czyste zastpowanie, 35 czysto wirtualne destruktory, 535 czytelno kodu, 69,602

C,13,25,59,95,517 funkcje, 95 C++, 13,58, 319, 348, 382,438, 517 standardowy format doczania plikw, 75 C99, 234 CAD, 437 CALL,297,366,514 calloc(), funkcja, 183,439,441 case, 104 cassert, 163 cast, 83 catch, 457 cerr, 346, 455 cfloat, 109 cfront, 194 char, 83, 109, 249 char*, 154 chroniony dostp, 212 cig Fibonacciego, 579 znakw, 85 cin, 84, 346, 455 class, 27, 32, 144, 220, 330 Class-Responsibility-Collaboration,49 clib.h, 186 client programmers, 30 climits, 109

dane statyczne programu, 324 data, 204 debugger, 159 default, 104 default constructor, 245 definicja, 71 deklaracja,71, 191 funkcji, 191 using, 335 dekrementacja, 393 delete, 20, 40, 41, 136, 183, 184, 241, 441, 534 przecianie, 452 globalnych operatorw, 453 delete this, 422 Demarco, Tom, 625 dereference, 115 destruktor, 19, 232, 255, 348, 423, 432, 462, 533, 564 automatyczne wywoania, 474 czysto wirtualny, 535 funkcje inline,313 kolejno wywoywania, 474 obiektw statycznych, 326 wirtualne wywoania, 537 wirtualny, 533, 565

Skorowidz diagram UML, 29, 32 diagramy przypadkw, 46 dugo sowa, 112 dobry styl programowania, 233 dokumentacja, 198 doczanie nagwkw, 74 plikw, 199 domylne argumenty, 250, 258 domylnie prywatne, 221 domylny konstruktor, 482 kopiujcy, 374 DOS, 78, 84 double, 109, 156 do-while, 99, 101 drukowanie, 263 drzewo skadniowe, 70 dynamie binding, 506 dynamic_cast, 139, 544, 545 dynamiczna alokacja pamici, 437 kontrola typw, 70 dynamiczne tworzenie obiektw, 437 dynamicznyprzydziapamici, 183 dyrektywy preprocesora, 69 #define, 127, 160, 200 #endif, 200 #ifdef, 160, 200 #include, 74 #undef, 160 DEBUG, 160 NDEBUG, 160 dziedziczenie, 20, 32, 283, 467, 484, 497, 613 chronione, 489 diagram dziedziczenia, 494 czenie kompozycji, 473 obnienie poziomu dostpu, 470 ograniczenia, 612 poziomy, 510 protected, 488 prywatne, 487 przecianie operatorw, 490 rzutowanie w gr, 492 skadnia, 469 statyczne funkcje skadowe, 483 tablica VTABLE, 523 upublicznianie skadowych, 488 wielokrotne,491,540 Ellis, Margaret, 345 else, 100 encapsulation, 195 end sentineI, 579 endl,81,91 enum, 148, 256, 330 escape sequences, 81 etapy projektowania obiektw, 50 konstrukcja systemu, 51 rozbudowa systemu, 51 skadanie obiektw, 51 wielokrotne wykorzystywanie obiektw, 51 znajdowanie obiektw, 51 ewolucja, 53 exception handler,41,457 handling,41 exit(), funkcja, 156, 326 explicit, 426, 427 extern, 73, 123, 126, 268, 271, 329, 352 extreme programming, 55, 492

early binding, 37, 506 efektywno, 60 egzemplarz klasy, 27

fabryka, 569 factory, 569 false,87, 100,110 fasz, 87 fan-out,431 float, 109,156, 187, 249 float.h, 109 for,90,99, 101, 102,121,235 inicjaIizacja, 102 krok, 102 warunek, 102 Fortran,26,412 Fowler, Martin, 624 fragmentacja pamici, 184 sterty, 453 free(), funkcja, 15, 183, 437, 439, 554 friend,214,216,218,332,442 fstream, 86, 345 function prototyping, 96 fundament intelektualny, 58 funkcja abort(), 326 assure(), 607 atexit(), 326 atofO, 155 atoi(), 155 atol(), 155 caIloc(), 183,439,441 definicja, 73 exit(), 156, 326

630
funkcja free(), 15, 183,437,439,554 getline(), 86 longjmp(), 15 main(), 63, 80, 161, 302 malloc(), 15, 183, 437, 439, 440, 441, 446, 554 memcpy(), 261,447 memset(),218,284,447 print(), 154, 302 printf(), 198, 617 push_back(), 89 push_front(), 89 puts(), 455 realloc(), 183,439,441 require(), 204, 325, 607 requireArgs(), 318 requireMinArgs(), 318 setjmp(), 15,232 strcmp(), 429 system(), 84 funkcje,71,187,380 adres, 163 anonimowe argumenty, 96 argument definicji, 96 argumenty, 71 domylne, 249 -wypeniacze, 259 czysto wirtualne, 518, 527 definicja, 71 definicjewskanika, 163 deklaracja,71,72,74,301 dynamicznego przydziau pamici, 440 globalne, 388 inline, 19, 297, 301, 308, 328, 352,531, 564 in situ modyfikujce obiekty zewntrzne, 377 nie dziedziczone automatycznie, 480 obsugi operatora new, 451 pobieranie argumentu przez referencj, 278 prototyp, 96 przecianie, 59 na podstawie zwracanych wartoci, 251 nazw, 249 przekazywanie adresw, 279 argumentw, 363 staej przez warto, 275 pusta lista argumentw, 72 ramka stosu, 365 referencja, 361 skadnia deklaracji, 72 skadowe, 29, 191,256, 305,429,468 standardowy sposb przekazywania argumentw, 281

Thinking in C++. Edycja polska statyczne obiekty klas, 325 szablony, 593 tworzenie, 95 udostpniajce, 303 virtual, 506 warto zwracana, 71, 97 wirtualne, 20, 38, 476, 498, 503, 504, 517 zgodne z POSDC, 78 zmiana typu zwracanej wartoci, 529 zmienna lista argumentw, 97, 198 zmienne statyczne, 324 zwracanie adresw, 279 przez warto, 364 staej przez warto, 276

g++, 82 garbage collector, 41 generator kodu, 70 generic algorithm, 593 getline(), funkcja, 86 getval, 134 Glass, Robert, 625 global optimizer, 70 globalna przestrze nazw, 202, 330 globalnyzasig, 19 GNU C++, 82,167 GNU Emacs, 604 goto, 105, 232 dalekie, 232-

H
handle classes, 224 header file, 74 heap,20,40,183 heurystyka, 42 hierarchia bazujca na obiekcie, 538, 555 dziedziczenia, 543 klas, 532, 546 o jednym korzeniu, 538 typw, 33 wywoa konstruktorw, 532 hybrydowyjzykobiektowy, 17
I

identyfikacja typw podczas pracy programu, 524, 545 identyfikator,71,180,599 ffiEE, 109, 156

Skorowidz

if, 98 if-else, 99, 136 ifstream, 86,485 iloczyn logiczny, 131 implementacja, 29, 31, 197 jzyka, 23 include guards, 607 indeksowaniezerowe, 151 inheritance, 32, 467 inicjalizacja, 229, 367,471 agregatowa, 166, 242 obiektw skadowych, 471 statycznych, 325, 344 wskanikw do skadowych, 382 za porednictwem elementw skadowych, 376 inkrementacja, 90, 393 inline, 297, 301,302, 312, 314, 558, 614 input-output stream, 78 instalacja wskanika wirtualnego, 515 int,97,148,187,249 inteligentny wskanik, 406 interfejs, 27,29,44 kIasy, 505 uytkownika, 45 interpreter, 68 BASIC, 68 interrupt service routine, 366 iostream, 75, 77, 78, 86, 199, 346 iostream.h, 75 ISO, 22 ISR, 366 iteracja, 53 iterator, 406, 551, 575, 579, 582, 590 zagniedony, 408, 582

Java, 15, 64 LISP, 26 o sabej kontroli typw, 38 obiektowy, 58 proceduralny, 58 programowania wieloparadygmatowegc PROLOG, 26 Python, 66 Smalltalk, 27, 555 UML, 29, 50 wysokiego poziomu, 15

K
kapsukowanie, 195, 219, 229, 503 karty CRC, 50 klasa-obowizek-wsppraca, 49 klasy, 27, 28,60, 67,144,219, 282, 504 abstrakcyjne, 518 adresy wirtualnych funkcji, 511 bazowe, 32 const, 282 czysto abstrakcyjne, 520 definicja, 218, 225 deklaracja, 225 destruktor, 232, 237 dziedziczce, 31 dziedziczenie, 32, 34, 467 funkcje czysto wirtualne, 518 inline, 302 skadowe, 469 udostpniajce, 303 wirtualne, 476, 507 ifstream, 485 interfejs, 56,508 iterator, 577 kapsukowanie, 195 konstruktor, 230, 237 kopiujcy, 376 kontenerowe, 556 kontroladostpu, 30, 219 lista inicjatorw konstruktora, 283 lokalne, 341 modularyzacja, 510 modyfikatory, 304 nadrzdne, 32 nazwa, 49 obiektw, 28 obserwatory, 304 ofstream, 475 okrojenie, 526 ostream, 372

Jacobsen, Ivar, 624 Java, 15, 235,470, 517, 555, 604 oficjalny standard kodowania, 604 jawnerzutowanie, 139,543 jdro, 52 jednostka translacji, 187, 323, 328, 344 jzyk APL,26 asemblera, 26, 68 BASIC, 26 C, 13, 26,59 C++, 13 C99, 234 Fortran, 26 imperatywny, 26 interpretowany, 68

632
klasy pochodne, 32, 507 podrzdne, 32 podstawowe, 32, 36, 256, 469, 591 polimorfizm, 478 potomne, 32 przechowywanie informacji o typie, 511 przecianie, 527 delete, 455 new, 455 przedefiniowanie, 476 public, 470 rzutowanie, 494 set, 568 skadniki, 28 skadowe, 30, 34, 220 stae, 282 o wartociach okrelonych podczas kompilacji, 285 statyczne funkcje skadowe, 342, 483 obiekty, 325 string,67,85,161,429 stringstream,414 strumieni wejcia-wyjcia, 78 tworzenie, 188 ukrywanie nazw, 476 vector, 67, 89 wraliwa klasa podstawowa, 224 zagniedone, 341 zaprzyjanione, 408, 576 zasanianie, 476, 527 klasy-uchwyty, 223, 224 klient, 46 klient-programista, 30, 61, 211 kod, 16 asemblera, 143 bdu, 42 rdowy, 68 kodowanie, 13 ze sprawdzaniem typw, 612 Koenig, Andrew, 300, 623 kolejno doczania plikw nagwkowych, 606 wywoywania destruktorw, 474 konstruktorw, 474, 531 kolekcja, 406 kolizjanazw, 188 komentarz, 601 znaczniki, 601 komercyjnabiblioteka, 181 Komitet Standaryzacyjny C++, 22 kompilacja,56,69,219

Th!nking in C++. Edycja polska analiza skadniowa, 70 drzewo skadniowe, 70 generator kodu, 70 kontrola typw, 70 optymalizator, 70 preprocesor, 69 program czcy, 70 przebiegi, 70 rozczna, 69, 71, 167 w pamici, 69 kompilator, 23, 37, 56, 68, 70, 81, 167, 187, 194, 382,467,482,493,514,557 funkcje inline, 311 GNU C++, 82 jednostkatranslacji, 187 odwoania do przodu, 313 ograniczeniafunkcji inline, 312 kompilowanie kodu, 63 komponenty, 229 kompozycja, 20,31, 374, 467, 484, 492, 497, 557,613 czenie dziedziczenia, 473 skadnia, 468 kompresja, 184 komunikacja, 49 komunikaty, 27, 34 konformizm, 57 koniunkcja, 133 konkretyzacja, 559 konstruktor, 19, 230, 255, 261, 327,440, 453 argumenty, 325 definicja, 283 domylne argumenty, 258 domylny, 245, 263 funkcje inline, 313 wirtualne, 530 jawne, 426 kolejno wywoywania, 474, 531 kopiujcy, 19, 344, 359, 363, 369, 374, 495, 526 lista inicjatorw, 283, 471 typy wbudowane, 283 wywoywanie funkcji wirtualnych, 532 zapobieganie konwersji, 426 kontenery, 67, 88,406,551, 614 iterator, 575 prawa wasnoci, 570 wskanikw, 565 kontrola bledow,316 dostpu, 30, 195, 212, 219, 222, 503, 610 nazw, 59 typw, 274

Skorowidz dynamiczna, 70 literay napisowe, 275 przypisania wskanikw, 274 statyczna, 70, 224 konwersja typw, 429 automatyczna, 425 operator, 427 puapki automatycznej konwersji, 430 ukryte dziaania, 431 za pomoc konstruktora, 425 zapobieganie konwersji za pomoc konstruktora, 426 konwersjezawajce, 141 kocowe porzdki, 229 kopiowanie bitw, 367 przy zapisie, 420 koszty pocztkowe, 64 kot z Cheshire

M
main(), funkcja, 63, 80, 161, 302 maintenance, 53 make, 167 domylne pliki wynikowe, 170 reguyprzyrostkowe, 169 Make, 167 makefile, 160, 167 .SUFFIXES, 169 all, 170 CPP, 169 domylne pliki wynikowe, 170 makrodefinicje, 168 OFLAG, 172 przykladowyplik, 171 makroinstrukcje, 19, 131, 159, 162, 198,297, 319,615 dostp, 300 maIloc(), funkcja, 15, 183, 437, 439, 440, 441 446, 554 manipulator strumieni, 83 mechanizm funkcji wirtualnych, 510 mem, 261 member, 222 memberwise assignment, 424 initialization, 376 memcpy(), funkcja, 261, 447 memset(), funkcja, 218, 284, 447 meneder sterty, 185 metody, 42 metodyka, 42, 44 mikroprocesor,41 MindView, Inc., 14 miniaturowabiblioteka, 180 model maszyny, 26 rozwizywanego problemu, 26 modularyzacja klas, 510 modulo, 130 modu startowy, 77 modyfikator c-v, 293 modyfikatory, 304 Moo, Barbara, 623 multiparadigm programming languages, 27 multiple dispatching, 543 inheritance, 540 Murray,414 mutable, 290, 291

Lajoie,Josee, 621 Lakos, John, 623 late binding, 38, 506 lazy initialization, 563 leniwa inicjalizacja, 563 Iibrarians, 70 libraries, 70 liczby dziesitne kodowane dwjkowo, 109 zmiennopozycyjne, 28, 109, 156 limits.h, 109 linker, 69, 70 Linux, 172 Lippman, Stanley, 621 LISP, 26 Iista inicjatorw konstruktora, 283, 471 typy wbudowane, 472 powizana, 180, 240 Lister, Timothy, 625 long, 111 longjmp(), funkcja, 15 Love, Tom, 625 l-warto, 130,133,277,558

acuchowanie, 159, 162 acuchy, 81,85 czenie, 76, 126, 323 tablic znakowych, 83 wewntrzne, 126 zewntrzne, 126, 127, 328

634

Thinking in C++. Edycja polska

N
nadklasa, 32 name decoration, 250 namespace, 79, 323, 333 namespaces, 330 narzdzie profilujce, 65 narzut menedera pamici, 442 nawiasy, 602 nazwy, 323 identyfikatorw, 605 ograniczanie widocznoci, 328 plikw, 600 widoczno, 323 zasig, 333 NDEBUG, 160, 163 negacja, 135 new, 20, 40, 136, 183, 184, 237, 241, 440, 455, 461,531 brakpamici, 451 przecianie, 452 globalnych operatorw, 453 new handler, 451 newmem, 261 niejawna konwersja typw, 128 niepena specyfikacja typu, 216, 225 nieskoczona rekurencja, 326 niezmienno bitowa, 290 fizyczna, 290 logiczna, 290 niszczenie obiektw, 326 statycznych, 326 not, 144 not_eq, 144 notacja UML, 50

obiektowe metody projektowania, 25 obiekty,27,194,504 adres, 312 delete,41,441 destruktor, 232 dynamiczne tworzenie, 40,437 funkcje skadowe, 29 globalne, 328 hierarchia, 538 inicjalizacja, 230 obiektw skadowych, 471 obiektw statycznych, 344 interfejs, 27, 44 kontenery, 406 new, 40,440

niszczenie, 40 okrajanie, 525 podrzdne, 468 poIimorfizm, 36 projektowanie, 51 przekazywanie obiektw przez warto, 359 skadowe, 32 Stack, 240 statyczne, 324, 348 stos, 40 struktura pamici, 219 this, 192 tworzenie, 40, 234, 438 tymczasowe, 278, 374 uzalenione, 345 wysyanie komunikatw, 195 zewntrzne, 116 Object, 555 object oriented programming, 18, 25 object-based hierarchy, 538 obraz funkcji wirtualnych, 512 obserwatory, 304 obsuga bdw, 61 wyjtkw, 14,41,457 obszar pamici, 194 odrzucenie niezmiennoci, 290 odmiecanie, 594 odwoania do przodu, 313 OFLAG, 172 ofstream, 86, 87, 327,475 ograniczanie powtrnych kompilacji, 224 ograniczenia semantyczne, 57 okrajanie obiektu, 521, 525, 527 OOP, 18, 25 OOPS, 555 operacjawyuskania, 115 operator, 387 operatory, 107,129,438 !,135 #, 162 &,113,132,136,298 &&, 131 *,114,130, 136 *this, 403 /,130 :,136 ::, 189 @,388 [],449 ^,132 |,132 ||, 131 ~,132

Skorowidz

+, 86, 107, 130 ++, 108, 136, 157, 408, 576 <<, 79, 82, 134, 419 =, 86, 107, 416 ->, 406 ->, 136, 147 ->*,410 >=, 299 , 84, 134 adresu, 136 alternatywy, 132, 138 argumenty, 402 automatyczne tworzenie operatora =, 424 automatycznej dekrementacji, 108 inkrementacji, 108 konwersji typw, 483 bitowe, 132 dekrementacji, 129, 393 delete,20,405,441 dodawania, 107 dosowne, 144 dwuargumentowe, 388, 393 globalne przecione, 427 iloczynu logicznego, 131 indeksowe, 405 inkrementacji, 129, 393 jednoargumentowe, 135, 388, 390 koniunkcji, 132, 138 konwersji, 427 ktrych nie mona przecia, 412 logiczne, 131 acuchowania, 162 matematyczne, 130 mnoenia, 107 modulo, 130 negacji, 132, 135 negacji logicznej, 135 new, 20,405,440,450 nietypowe, 405 odejmowania, 107 potgowania, 412 priorytety, 107 przecianie, 79, 387,413,452 przecinkowe, 137,405 przedrostkowa wersja, 403 przesuni, 133,414 przypisania, 107, 129 relacji, 131 rnicysymetrycznej, 132 rzutowania, 136, 138 sizeof, 111,143,196 skadnia przeciania, 388

sumy logicznej, 131 trjargumentowe, 136 tworzenie,416 umieszczania delete, 461 new, 461 wirtualne, 541 wyuskania, 136 wyuskania wskanika, 406 wywoania funkcji, 410 zaprzyjanione, 428 zasigu, 189,206,333,471 zwracajcy adres elementu, 113 zwracane wartoci, 402 zwracanie staych przez warto, 404 wartoci przez referencje, 414 optymalizacja, 124, 330, 614 zwracania wartoci, 404, 616 optymalizator globalny, 70 lokalny, 70 or, 144 or_eq, 144 ostream,263,372,616 overloaded, 249 overloading, 79 overriding, 35, 476, 507

pamiec,27,41, 112 dynamiczna alokacja, 437 dynamicznyprzydzia, 183 fragmentacja, 184 narzut menedera, 442 ROM, 291 statyczna, 323 statyczne skadowe, 337 uchwyty, 185 wirtualna, 440 parsing, 70 Pascal, 19 pass by reference, 117 pass byvaIue, 115 peephole optimizer, 70 persistence, 594 ptle do-while, 102 for, 90, 102, 235 while, 87 pielgnacja, 53 planowanie, 55

636
pliki,71,78 .a,99 .cpp, 169
.lib, 99 .o,76 .obj, 76 makefile, 167, 168 nagwkowe, 74, 80, 191, 197, 199, 319, 328, 429, 539 nagwkowe biblioteki, 76 nazwy, 600 odczytywanie, 86 standardowy format doczania plikw, 75 ledzenia, 327 wynikowe, 187 zapisywanie, 86 zasig, 328 rdowe, 449 podklasa, 32 podtypy, 485 polimorficzna hierarchia, 544 polimorfizm, 20, 36,478,498, 503, 525, 530, 546, 555, 564 POSiX, 78 postconditions, 607 potrzeby klienta, 43 pne wizanie, 38, 506, 510 prawa wasnoci, 241, 570 prawda, 87 preconditions, 607 preprocesor, 19, 69, 75, 131, 200, 267, 298, 557 dyrektywy, 69 funkcje inline, 315 inline, 315 makroinstrukcje, 298 skJejanie symboli, 316 znacznikiuruchomieniowe, 160 printf(), funkcja, 198, 617 priorytetyoperatorw, 107 private,30,212,301,487 problem wraliwej kasy podstawowej, 224 procedura inicjalizujca, 77 obsugi przerwa, 293, 366, 367 wyjatku,41,457 procesjednoprzebiegowy, 52 procesor, 364 produktywno, 59 program czcy, 69, 70, 179, 506 sterowanytabel, 166 uruchomieniowy, 69, 159

Thinking in C++. Edycja polska wykonywalny, 69 wynikowy, 72 zarzdzajcy bibliotekami, 70 programowanie, 13 ekstremalne, 55 Extreme Programming (XP), 492 na wielkskal, 61 obiektowe, 25, 26, 498 przyrostowe, 492 w parach, 57 z ograniczeniami, 26 zorientowane obiektowo, 18 projekt, 13,43,52 pliki nagwkowe, 202 projektowanie, 42 obiektw, 51 proceduralne, 45 PROLOG, 26 promocja, 187 protected, 30, 212, 214, 488, 489 prototyp, 54 prototypowanie funkcji, 96 prywatny, 212 przebiegi kompilacji, 70 przechowywanie informacji o typie, 511 przecianie, 260, 527 funkcji, 59 globalnych operatorw, 453 na podstawie zwracanych wartoci, 251 nazw funkcji, 19, 249, 250 operacji przypisania, 415 operatorw, 20, 79, 107, 387, 413, 452, 541,614 wyjcia, 431 przedefiniowanie, 476 przejrzysto interfejsu, 314 przekazywanie, 363 duych obiektw, 365 przez referencj, 117 przez warto, 115, 275, 370 staej przez warto, 275 przekompilowanie kodu, 610 przerwania, 366 procedura obshigi, 366 przestrze nazw, 19, 59, 79, 323, 330, 607 bezimienna, 332 globalna, 330 Int, 334 pliki nagwkowe, 202 przyjaciele, 332 std,80,318 tworzenie, 330 using, 332, 333 uywanie, 332

.h,75

Skorowidz wykorzystywanie, 336 zasig, 333 problemu, 26 rozwizania, 26 przesunicia, 133 przydzielanie pamieci,72, 184,236,438 wielokrotne, 543 przyjaciele, 214 zagniedeni, 216 przypadki uycia, 46, 53 przypisanie,92, 130,274 automatyczne tworzenie operatora, 424 kontrola typw, 274 literay napisowe, 275 przecianie, 415 wskaniki zawarte w klasach, 417 za porednictwem elementw skadowych, 424 zliczanie odwoa, 419 pseudokonstruktor, 473 public,30,212,470 publiczny, 212 punkt sekwencyjny, 231, 324 pure abstract class, 520 pure virtual function, 518 push_back(), funkcja, 89 push_front(), funkcja, 89 putc(), 300 puts(), funkcja, 455 p-wartosc,130,133, 136,558 Python,66,68,517,561 require(), funkcja, 204, 325, 607 require.h, 194,206,317 requireArgs(), funkcja, 318 requireMinArgs(), funkcja, 318 return, 97, 600 RETURN, 297, 366 ROM,291 rozkazy komputera, 68 rozczna kompilacja, 69, 71, 167 rozszerzalno, 508 rozwinicie funkcji, 312 RTTI. 524, 545 Rumbaugh, James, 624 runtime binding, 506 run-time type identification, 524, 545 rzutowanie, 39, 83, 113,119, 136,138 const_cast, 141 dynamic_cast, 544 jawne, 139,543 konstruktor kopiujcy, 494 reinterpret_cast, 142 static_cast, 140, 544 w d, 525, 543 bezpieczne dla typw, 543 wgore,39,492,498,504,516

ramka funkcji, 366 stosu, 324 wywoanie funkcji, 365 realloc(), funkcja, 183,439,441 redefining, 476 reference counting, 419 referencja, 19, 59, 117, 119, 271, 276, 359, 360, 382,414,505,529,591,617 do staej, 281 do staych, 362 do wskanika, 362 funkcje, 361 rzutowanie w gr, 498 typu void, 119 register, 124, 330 reguajednej definicji, 72, 199 reguy przyrostkowe, 169 reinterpret_cast, 139 rejestry procesora, 364 rekurencja, 106

scalanie programw wynikowych, 72 scenariusz, 46 sekwencja znakw specjalnych, 81 selektor, 104 setjmp(), funkcja, 15, 232 SGI STL, 88 short, 111 sideeffect, 129 signed, 111 singly-rooted hierarchy, 538 sizeof, 111, 143, 196 sklejanie symboli, 316 skadanie staych, 268 skadnia, 58, 67 deklaracji, 164 funkcji, 72 zmiennej, 73 destruktora, 232 dziedziczenia, 469,471 kompozycji, 468 konstruktora, 232 referencji, 281 szablonw, 558 skadowe, 222 chronione, 31 prywatne, 31, 34

638

Thinking in C++. Edycja polska

skrypt powoki, 84 saba kontrola typw, 561 sowa kuczowe and, 144 and_eq, 144 asm, 143 auto, 330 bitand, 144 bitor, 144 break, 103 case, 104 catch, 457 char, 83, 109, 249 class, 27, 32, 144, 220, 330 compl, 144 const, 19, 127, 267, 270, 277, 288, 451, 546 continue, 103 default, 104 delete, 20, 40,41, 136, 183, 241, 534 do, 101 double, 109 else, 100 enum, 148, 256, 330 explicit, 426, 427 extern, 73, 123,126, 268, 271, 329, 352 float, 109, 249 for,90,101,102,121,235 friend,214,216,218,332,442 goto, 105, 232 if,98 inline,301,302,312,314 int, 97, 249 long, 111 mutable,291 namespace, 79, 333 new, 20, 40, 136, 183, 241, 461 not, 144 not_eq, 144 operator, 387 or, 144 or_eq, 144 private,30,212,487 protected, 30, 212, 214, 488, 489 public,30,212,470 register, 124, 330 return, 97, 600 short, 111 signed, 111 static, 19, 124, 285, 323, 329, 342, 353 struct, 144, 145, 220, 330 switch,104,121,236,546,612 template, 558 this, 192, 382 throw, 457

try, 457 typedef, 144 typeid, 545 union, 330 unsigned, 111, 134 using, 79, 333 virtual, 20, 38, 478, 506, 517, 533, 613 void,97, 119, 164 volatile, 19, 129, 267, 292 while,87,101,121,122 xor, 144 xor_eq, 144 Smalltalk,27,517,554,561 source-level debuggers, 69 sparametryzowany typ, 557 specyfikacja przydziau pamici, 122 systemu, 45 zmiany sposobu czenia, 352 specyfikator, 111 klas pamici, 330 dostepu,30,212,219 private, 213 protected,214 public,212 spjno skadni, 472 sprztanie, 229 wskanikw, 445 sstream, 414 stacja robocza, 57 Stack, 223, 240 stae, 127, 267argumenty funkcji, 275 bezpieczestwo, 269 const, 276, 278,282 funkcje skadowe, 287 jezykC,271 literay napisowe, 275 czone wewntrznie, 268 obiekty, 287 pliki nagwkowe, 268 podstawianie wartoci, 267 przekazywanie staej przez warto, 275 skadanie, 268 static, 285 statyczne, 339 szablony, 562 w klasach, 282 wartoci, 128 okrelone podczas kompilacji, 285 wskaniki, 272, 273, 359 zasig, 271 zwracanie staej przez warto, 276

Skorowidz standard ISO, 22 plikw nagwkowych, 201 Standard Template Library, 88 standardowa biblioteka, 77 C++,88 szablonw, 88 standardowe C++,.22 funkcje biblioteczne, 78 wejcie, 84 standardowy sposb przekazywania argumentw, 281 standardyjezyka,22 startBytes, 182 Stash, 188 static, 19,124, 285, 323, 329, 342, 353 static const, 339 static_cast, 139, 544, 545 statyczna kontrola typw, 70 statyczne funkcje skadowe, 342,483 skadowe, 337 definiowanie pamici, 337 inicjalizacja tablicy statycznej, 339 klasy, 341 statyczny obszar danych, 323 std,80,85,318 sterowanie czeniem, 328 wykonywaniem programu, 99 sterta,20,40,183,437 fragmentacja, 453 obsuga wjzyku C, 439 STL,88 storage, 182 stos, 40,297, 324, 552,573 kontrola dostpu, 223 liczb cakowitych, 553 stranik doczania, 201,607 strcmp(), funkcja, 429 string,67,81,85,88,161,429 scalenie tablic, 86 Stroustrup, Bjarne, 15, 345, 517, 556, 599, 621, 623 struct, 144, 145, 220, 330 struktura programu, 80 struktury, 144,196,211,255 daneskadowe, 189, 197 deklaracja, 214 friend,214 funkcje skadowe, 198 kontrola dostpu, 212 skadowe prywatne, 218 wskaniki, 147 zagniedone, 202 strumie wejcia-wyjcia, 77; 78, 82, 199, 372,4 manipulator, 83 odczytywanie wejcia, 84 styl kodowania, 599 substitutability, 27 substitution principle, 35 subtyping, 486 suma logiczna, 131 Sun, 604 switch, 104, 121, 236, 546, 612 system dwjkowy, 109 operacyjny, 70 uruchomieniowy, 57 system(), funkcja, 84 szablony, 20, 61, 89, 546, 551, 557 argumenty, 562 funkcji, 593 klas, 593 pliki nagwkowe, 560 podstawy, 554 skadnia, 558 stae, 562 szkolenie, 62 szybkie projektowanie, 68

cieka wyszukiwania, 75 rodowisko kompilacji, 69 programistyczne, 159

table-driven code, 166 tablice,89, 151 automatyczne zliczanie, 243 delete, 450 dynamiczne tworzenie, 151 elementy, 151 indeksowanie zerowe, 151 new, 450 przecianie delete, 458 operatorw new, 458 rozmiar, 151 upodabnianie wskanika do tablicy, 451 usuwanie, 184 wskanikw do funkcji, 166 znakowe,81,83, 154, 184 template, 557, 558 testowanie, 56

640
this, 192, 231, 290, 303, 342, 382, 515 this->, 192; throw, 457; Time, 305 . tumaczeniejzyka, 68 touper(), 300 translator, 75 true,87,100,110 trwao, 594 try, 457 try-catch, 457 tworzenie wystpienia, 559 twrca klas, 30 type definition, 144 typedef, 144, 146, 180, 189, 330, 381 typeid, 545 typeinfo, 545 type-safe downcast, 543 Iinkage, 252 typydanych, 108 abstrakcyjne, 28, 108 char, 83, 109 char*, 154 const, 127 double, 109 dynamiczna kontrola, 70 float, 109 int, 97, 148 klasy, 144 kontrola typw w wyliczeniach, 149 czenie bezpieczne, 252 rzutowanie, 119 specyfikatory, 111 statyczna kontrola, 70 string, 250 struktury, 144, 196 tablice, 151 tworzenie podtypw, 485 typwzoonych, 144 unie, 150 void, 97 wbudowane, 28,109 wskaniki, 114 wyliczeniowe, 148 zdefiniowane przez uytkownika, 195

Thinking in C++. Edycja polska umieszczanie new, 440 UML, 50 unie, 150, 255 anonimowe, 257 Unified Modeling Language, 29 unikatowy adres pamici, 194 identyfikator, 194 union, 330 Unix,84,172,517 unsigned, 111, 134 upakowaniesterty, 184 upcasting, 39,493 upublicznianie skadowych, 488 uruchamianie kompilatora, 82 programw, 159 use cases, 46 using, 79, 85, 202, 333 deklaracja, 335 dyrektywa, 333 using directive, 333 uzalenione obiekty, 345 uzupenienia nazw, 250

varargs, 198 vartype, 258 vector, 67, 88, 90, 372 vi,604 vim, 604 virtual, 20, 38,478, 506,517, 533, 613 virtual pointer, 511 void,97,119, 164 void*, 119,186, 359,440,443,446 volatile, 19, 129, 267, 292 vpointer, 511 VPTR,511,512,513,514,530 VTABLE,511,513,517,519 dziedziczenie, 523

W
Waldrop, M. Mitchell, 625 wariancja, 612 warto domylna, 263 zwracana, 71 wartownik, 579 warunki kocowe, 607 wstpne, 607

U
uchwyty pamici, 185 ukrywanie implementacji, 219, 224 nazw, 476

Skorowidz wcicia, 599, 602 wczesne wizanie, 37, 506 weaktyping,561 wejcie konsoli, 84 wektor zmian, 54 wektory,88,151 while,87,90,99,101,121,122 wizanie, 506 dynamiczne, 506 podczas wykonywania programu, 506 pne, 506 wczesne, 506 wywoania funkcji, 506 widoczno, 19, 323 wielobieno, 366 wielokrotne deklaracje, 199 dziedziczenie, 14, 491, 540, 544, 556, 614 uywanie kodu, 61 wykorzystywanie kodu, 467 wielowtkowo, 594 wild-card, 43 Windows, 78 wirtualne definicje, 522 operatory, 541 wywoania w destruktorach, 537 wirtualny, 38 destruktor, 533 wolnapami, 184 wskanik stosu, 324, 367 wskaniki, 112, 114,116, 118,181,184,359 tablice, 152 arytmetyka, 157 definicja, 114,274 dofunkcji,165,380 do obiektw utworzonych na stercie, 445 doskladowej,359,410 do skadowych, 378 funkcji, 163 inicjaiizowanie, 274 inteligentne, 406 rzutowanie w gr, 498 stae, 272 struktury, 147 tablice, 152 this, 290 upodabnianie do tablicy, 451 void, 119 void*,241,359 VPTR,513,514 wirtualne, 511, 515 wycieki pamici, 184, 445 wydajno, 64, 179

64
wyjtki bad_alloc, 453 obsuga, 41 procedura obsugi, 41 zgoszenie, 42 wyliczenia, 149 wyuskanie, 115, 136 wyraenia, 107,137 delete,441 new, 440 warunkowe, 310 wysyanie funkcji, 117, 324 wirtualnej, 514 wirtualnych wewntrz konstruktorw, 532 generowane przez kompilator, 194 komunikatw, 28, 195 polimorficzne, 511 programw, 84 wzorce projektowe, 14,54, 63

xor, 144 xor_eq, 144 XP,55

zagniedeni przyjaciele, 216 zagniedone struktury, 202 zagniedony iterator, 408 zapisywanie danych wyjciowych, 475 zapobieganie przekazywaniu przez warto, 376 zarzdzanie pamici, 41 zasady zastpowania, 35 zasig, 120, 333 globalny, 206 pliku, 328 zasanianie, 35, 476, 507, 527 zastpowalno, 27 zastpowanie czyste, 35 obiektw, 36 zasady, 35 zbieracz mieci, 41, 452 zbieranie nieuytkw, 594 zbiorniknabity, 134 zbir, 568 zewntrzne deklaracje, 74 odwoanie, 187 zgoszenie wyjtku, 451 zliczanie odwoa, 419

642
zmiana typu zwracanej wartoci, 529 zmienna lista argumentw, 97, 198 zmienne automatyczne, 40, 124 definiowanie w locie", 120 deklaracja, 73 globalne, 122, 615 inicjator, 324 lokalne,40,115,124,310,324 acuchowe, 186 czenie, 126 niezainicjowane, 234 register, 330 rejestrowe, 124 sktadnia deklaracji, 73 stae, 127 stanu, 166 static, 124 statyczne, 324 vartype, 258 volatile, 129 wskaniki, 114 zasig, 120 zewntrzne, 123 znacznik, 263 koca, 579 znacznikiuruchomieniowe, 160 preprocesora, 160 sprawdzanie w czasie pracy programu, 161 znak kodASCn,81 nowego wiersza, 81 specjalny, 81 zunifikowany jzyk modelowania, 29 zwalnianie pamici, 462 zwracanie adresw, 279 przez warto, 364 staych przez warto, 404

Thinking in C++. Edycja polska

rda bibliotek GNU C, 443

danie, 28, 29

ksigarnia internetowa http://helion.pl

wiczenia praktyczne
.stron WWW

Adobe

Premier 6

. AutoCAD2000PL
^Cwicz&niapraktyc

2002/XP

Access

MSExcel

2002/XP

Turbo Pascal

r --*
S
J-,tlcwni

;r

NovellNetWore6
zen<a 3^ktyczne

Domowe sieci komputerowe


wiczenia praktyczne

Java

Linux 7.2

Red Hat

^k wczenia praktyczne

PostgreSQL7.2

JLV<4

i , i M T . - -

wiczenia praktyczne" to seria przeznaczona dla tych czytelnikw, ktrzy pragn od podstaw pozna konkretny temat. Ksiki te skadaj si z wicze, dziki czemu czytelnik stopniowo zgbia dane zagadnienie, majc jednoczenie moliwo systematycznego sprawdzania swojej wiedzy. Napisane przejrzystym i prostym jzykiem, bogato ilustrowane, opatrzone czytelnymi przykadami s atw lektur nawet dla tych, ktrzy stawiajpierwsze kroki w wiecie informatyki. Ksiki z tej serii mogby wykorzystywane na kursach i szkoleniach. Informatyka w najlepszym wydaniu

^K

Wydawnictwo Helion

ksigarnia internetowa http://www.helion.pl

ABC

W przypadku ksiek tej serii szczeglny nacisk pooylimy na prostot przekazu. Nasi autorzy unikaj stosowania specjalistycznego jzyka, ktry zniechca wielu pocztkujcych uytkownikw komputerw do dalszej nauki. Praca z ksikami z serii ABC" to doskonaty sposb na stworzenie solidnych podstaw do dalszej nauki. Jeli natomiast Twoim zamiaremjest opanowanie obshigi programu komputerowego w stopniu rednio zaawansowanym, ksiki ,ABC" to najwaciwszy wybr. Czytelnik znajdzie w nich opis wszystkich najwaniejszych funkcji programu, ze szczeglnym naciskiem na optymalizacjpracy i uniwersalno przekazu.

Wydawnictwo Helion
ul. Chopina 6, 44-100 Gliwice; Sl skr. poczt. 462 8(32) 230-98-63, (32) 231 -22-19; e-mail: helion@helion.pl

Dla kadego
PC 2000.

ksigarnia internetowa http://www.helion.pl

Komputer| FrontPage|Dreamweaver| Access l onnn

4 2000 PL

ZfOg

Sai ?nnnl SOL Hi JavaZDeiphie LUUUI *"C I


fc

C++ Builder 6

SepverQn Pages o.U

Aclive

Sieci
komputerowe

Ksiki z tej serii to poradniki, ktre przydadz si kademu, kto powanie myli o rozwoju komputerowych umiejtnoci. Zazwyczaj przygody z kolejnym programem komputerowym zaczynasz od wstpnego zapoznania si zjego podstawowymi moliwociami. Metoda klikn i sprawdz, co si stanie" to bardzo dobry sposb nauki,jednak w ten sposb nie poznasz caego potencjahi swojego oprogramowania. Ksiki z serii Dla kadego" pozwolCi usystematyzowa zdobyt wiedz, umoliwi poznanie wikszoci opcji i polece, podpowiedzJak optymalnie wykorzysta funkcje programu. Po ich lekturze nie bdzie mowy o przypadkowoci dziaa, a efekty Twojej pracy bdwpemi zgodne z zamierzonymi. Dziki prostym przykadom zawartym w tych ksikach bdziesz mg samodzielnie zapozna si z omawianymi zagadnieniami. Ksiki s kierowane do pocztkujcych i rednio zaawansowanych czytelnikw.

Informatyka w najlepszym wydaniu

Wydawnictwo Helion

ksigarnia internetowa http://helion.pl

Leksykon kieszonkowy

Zwize i wyczerpujce kompendium wiedzy, zawierajce niemal wszystkie najistotniejsze dla informatyka-praktyka informacje. Opracowujc ich struktur, autorzy pooyli nacisk na szybko dostpu do poszukiwanej informacji. W leksykonach kieszonkowych nie znajdziesz wykadw o technologii, ale usystematyzowany opis polece, opcji i zmiennych oraz wielu innych elementw, ktrych objanie nie musiszju szuka w kilkusetstronicowych opracowaniach. Jeli zdobyeju podstawowe informacje na temat opisywanej aplikacji, administrowania systemem czy programowania w danym jzyku, to leksykony kieszonkowe bddoskonafym uzupemieniem Twojej wiedzy. Dziki swojemu maemu rozmiarowi mog zastpi podrczne notatki i przyda Ci si w kadej chwili pracy.

Informatyka w najlepszym wydaniu

Wydawnictwo Helion

ksigarnia internetowa http://www.helion.pl

0'ReiUy
XML

_L
Python
Wyraenia regukrne

Access Ba/n danvch

Perl
.|ava Servlet Programowanie

Innowacyjno i profesjonalizm s nierozerwalnie zwizane z dziedzin informatyki. Jeli speniasz obawarunki,jestes nanajlepszej drodze do osignicia sukcesu. Dostp do fachowej literatury znacznie uatwi Ci poznanie najgbszych tajemnic technik programowania czy obshigi najbardziej zaawansowanych programw komputerowych. Ksiki Wydawnictwa O'Reilly to rdo rzetelnej wiedzy, podanej w prosty i przystpny sposb. Ksiki z charakterystycznymi zwierztkami umieszczonymi na biaej okadce cechuje nie tylko bardzo wysoki poziom merytoryczny, ale rwnie przejrzysto i kompleksowo.

Informatyka w najlepszym wydaniu

Wydawnictwo Helion

Ksiga eksperta

ksigarnia internetowa http://helion.pl

Ju sama nazwa serii objania tre tych ksiek. Ksigi eksperta" to prawdziwa skarbnica wiedzy. Znajdziesz w nich waciwie wszystko, co jest potrzebne uytkownikom poszczeglnych rodzajw oprogramowania. Ju na pierwszy rzut oka mona przekona si, e Ksigi eksperta" to solidne opracowania, ktrych lektura stanowi kolejny krok w wiat profesjonalnych zastosowa oprogramowania komputerowego. Konkretne zagadnienia omwiono w nich w sposb dogbny i nad wyraz wyczerpujcy. Ich szczegowo dodatkowo podkrelaj odpowiedzi na z ycia wzite" pytania. Po nabyciu ksiki z tej serii nie bdziesz ju musia wertowa kartek kilku innych ksiek - znajdziesz w niej wszystko, co potrzebne.

Informatyka w najlepszym wydaniu

Wydawnictwo Helien

You might also like