Professional Documents
Culture Documents
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.
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.
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
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
211 212 214 214 216 218 219 219 222 223 223 224 224 226 227
211
Rozdzia 6.
229
230 232 233 235 236 237 240 242 245 246 246
249
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
297
298 300 301 302 303 308 311 312 313 313 314 315 316 316 319 320
323
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
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
503
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
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
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++".
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.
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
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.
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.
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
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.
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.
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.
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
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.
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.
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.
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.
' _(
Strzaka na powyszym diagramie UML jest skierowana od klasy pochodnej do podstawowej. Jak przekonamy si pniej, moe istnie wicej nijedna klasa pochodna.
I
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.
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".
36
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.
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
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):
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
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.
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.
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
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".
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.
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
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:
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.
49
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.
''
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.
52
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.
53
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.
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
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.
j
ii
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
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.
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.
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
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.
;. .
" Jednake warto przejrze rubryk Dana Saksa w C/C++ User's Journal, w ktrej mona znale istotne rozwaania dotyczce wydajnoci bibliotek C++.
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.
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,;-.
62
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++:
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.
63
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.
64
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.
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.
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
Rozdzia 2.
68
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.
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
'..
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}.
71
Mona wyczy statyczn kontrol typw w jzyku C++. Mona rwnie samodzielnie przeprowadzi dynamiczn kontrol typw wystarczy tylko napisa odpowiedni kod.
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
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();
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).
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
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.
75
#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.
#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:
76
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++.
77
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
#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.
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
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.
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
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.
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".
} /// -
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
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.
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.
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
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.
//: 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.
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>
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
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
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
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
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
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.
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
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.
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
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.
Rozdzia 3. * Jzyk C w
C++
99
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
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
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;
} 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.
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
else { cout "nie wybrae a ani b!" endl; continue; // Powrt do gwnego menu
L04
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 }
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.
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;
98
//: 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;
// // // //
Teraz ju zapewne w peni rozumiesz znaczenie nazwy C++" oznacza ona krok naprzd w stosunku do C".
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).
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).
&& || ! , ~ ~
? :
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.
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
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.
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.
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.
//. .
// .. . // . ..
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.
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
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:
//{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
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.
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
// 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
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
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.
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.
130
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
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
PRINT("u". u); PRINT("v". v); u += v; PRINT("u += v", u); u -= v; PRINT("u -= v", u);
u /= v; PRINT("u /= v". u); Oczywicie, p-wartocl wszystkich przypisa, mog by bardziej skomplikowane.
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:
/ / : C03:printBinary.h
/ / : 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.
Ostatecznie funkcja zostaa uyta w przykadzie, prezentujcym operatory dziaajce na bitach: / / : C03:Bitwise.cpp / / { L } printBinary
#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
okrela, wjakim znaczeniu zostay one uyte, na podstawie tego, jak zostao zapisane wyraenie. Na przykad instrukcja:
x = -a;
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.
.38
int main() { int a - 1, b - 1; while(a = b) {
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.
const_cast reinterpret_cast
dynamic_cast
140 i
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
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.
int i = sizeof x; Operator sizeof moe rwnie podawa wielko typw danych, zdefiniowanych przez uytkownika. Zostanie to wykorzystane w dalszej czci ksiki.
144
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.
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.
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 ;
} ///:-
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).
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.
150
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
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;
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:
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:
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
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 =
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 = "
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.
160
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.
#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
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.
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.
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.
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.
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.
166
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.
if ( c == 'w' )
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
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
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.
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".
Rozdzia 3. Jzyk C w
C++
171
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
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.
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
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
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.
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:
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.
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.
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
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
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.
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
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:
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}
191
}
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.
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
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.
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.
196
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
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.
198
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.
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.
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.
#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
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.
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: } }:
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
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:
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(); };
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;
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( ).
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
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() {
::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.
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
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.
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
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.
void A::func() {}
int i;
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();
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
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
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;
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;
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();
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.
219
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:
221
i nt i, j, k;
int f();
void B::g() {
i = j = k = 0; }
int main() {
A a; B b;
} ///:-
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
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.
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.
223
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.
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
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.
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
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.
Rozdzia 6.
230
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.
231
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
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();
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;
Destruktor jest zatem wywoywany automatycznie w miejscu klamrowego nawiasu, zamykajcego zasig, w ktrym zdefiniowano obiekt.
: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>
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++.
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:
//: 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:
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.
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>
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;
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.
}:
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");
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 <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) {
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.
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:
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:
l__
M4
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
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];
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.
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
Rozdzia 7.
250
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.
void f();
251
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.
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
//: C07:Use.Cpp
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(); }:
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;
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>
56
-U();
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;
257
float f;
public:
ii)
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 {
int i ; float f:
}:
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:
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
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.
Mem():
~Mem();
Mem(int sz);
#endif // MEM_H / / / : -
}:
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.
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);
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);
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
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
(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
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:
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.
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
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:
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
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:
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:
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.
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.
276
void fl(const int i) { i++; // Niedozwolone - bd podczas kompilacji }
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.
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
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.
//: 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 " ;
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.
//: 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.
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.
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 {
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
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.
}
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.
98
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
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.
//: 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:
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.
)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;
// 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
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
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
>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)
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
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).
301
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
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).
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
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).
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++;
public: Time() { mark(); } void mark() { lflag = aflag = 0; std::time(&t); } const char* ascii() { updateAsci i(); return asciiRep; }
// 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();
#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
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:
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.
08
//: 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);
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!
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;
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):
} } }
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.
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.
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).
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 {
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();
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.
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
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.
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
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
;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.
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.
Thinking in C++. Edycja polska W tej czci przyjrzymy si powyszym znaczeniom sowa kluczowego static, w takiej postaci, wjakiej zostay one odziedziczone pojzyku 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
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.
26
void f() { static X xl(47); static X x2; // Wymagany jest konstruktor domylny
int main() { f(); } III-
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.
//: 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
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
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
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.
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();
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
32
//: 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.
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.
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;
0)
#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!
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
#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.
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.
138
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 {
Nastpnie trzeba zdefiniowa pami dla statycznej skadowej w pliku zawierajcym definicj klasy:
int A::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
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;
X Stat::x2(100);
X Stat::xTable2[] - {
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.
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
int i ;
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) {}
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.
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;
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.
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
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:
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:
#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"
//: C10:Initializer2.cpp //{L} InitializerDefs Initializer // Inicjalizacja obiektw statycznych #include "Initializer.h" using namespace std;
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
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(): } }:
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.
//: 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:
351
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);
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.
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.
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
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
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().
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:
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
}
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; -
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:
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.
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,
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.
365
'
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.
166
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:
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
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.
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);
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
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.
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.
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
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.
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 ;
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.
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.
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.
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;
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
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);
(2
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.
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
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;
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);
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.
90
)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
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
// 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:
393
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.
#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);
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);
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);
// 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); }
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) {
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;
// 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(<<=)
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 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
} 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 {
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; } // 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); \
out << "; bl " ifOP " b2 daje "; \ out << (bl OP b2); \ out << endl;
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.
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,
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.
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;
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;
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:
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];
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++);
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 {
// 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;
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 {
411
} 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
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.
413
, :
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;
414
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
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
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) {
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;
#include <string> #include <iostream> using namespace std; class Dog { string nm;
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;
~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;
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>
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();
} 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?
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
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.
425
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).
//: C12:AutomaticTypeConversion.cpp // Konstruktor umoliwiajcy konwersje typw class One { public: 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().
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.
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:
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.
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.
//: 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
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();
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*.
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);
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.
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
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.
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.
16
Rozdzia 13.
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.
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.
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
>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:~
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).
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.
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).
446 //: C13:PStash.h // Przechowuje wskaniki zamiast obiektw #ifndef PSTASH_H #define PSTASH_H
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"
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.
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);
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
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.
451
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:
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
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.
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.
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;
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.
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
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.).
#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];
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
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
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"
try {
nm = new NoMemory; } catch(bad_alloc) { cerr << "Wyjtek braku pamici" << endl;
} l//:~
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.
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 ;
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
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__).
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.
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) (
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.
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.
pod warunkiem, e klasa Bar posiada konstruktor, pobierajcy pojedynczy argument typu cakowitego.
472
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.
int main() { X x; int i(100): // Zastosowany w zwykej definicji int* ip = new int(47);
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.
class B {
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();
474
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().
#define CLASS(ID) class ID { \ public: \ ID(int) { out << #ID " konstruktor\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";
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
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 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();
} } }
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
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.
481
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.
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:
Wywoania te powinny stanowi dla ciebie wzorzec, wykorzystywany za kadym razem, gdy tworzyszjak klas, uywajc do tego dziedziczenia.
484
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.)-
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
} 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
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
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)
}:
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
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
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 ;
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
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).
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
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.
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.
497
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.
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
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
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.
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
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.
Rozdzia 15.
504
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
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
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.
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
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
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; }
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
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.
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.
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.
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;
W przypadku niektrych kompilatorw mog wystpi niezgodnoci, dotyczce rozmiarw, ale zdarza si? to rzadko.
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.
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
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.
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;
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
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.
518
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.
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; }:
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
// 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
// Wykorzystanie wsplnego kodu klasy Pet: void speak() const { Pet::speak(); } void eat() const { Pet::eat(); }
523
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
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
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;
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.
!"
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;
// 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).
529
}:
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;
// 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.
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.
532
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.
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.
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.
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.
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.
537
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
#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
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
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:
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;
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
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:~
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
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.
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
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
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
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;
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.
,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
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.
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.
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).
58
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;
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( ).
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.
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
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.
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;
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;
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 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.
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
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.
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);
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
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;
// 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.
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.
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
#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.
573
//: 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];
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; }
#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++).
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() {
}:
// Iterator przypomina "sprytny" wskanik: class IntStackIter { IntStack& s; int index;
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
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
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:
} T pop() {
}
stack[top++] - i;
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);}
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
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.
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; }
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))
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.
//: 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;
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();
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:
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:
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() {}
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";} }:
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; }
// 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.
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.
>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.
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
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
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.
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).
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
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>.
)2
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 !".
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;
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 {
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.
607
#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.
08
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.
Dodatek B
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.
L2
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.
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
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.
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
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.
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
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)
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).
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.
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.
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".
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.
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.
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
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
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
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
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
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
danie, 28, 29
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
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
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.
4 2000 PL
ZfOg
C++ Builder 6
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.
Wydawnictwo Helion
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.
Wydawnictwo Helion
0'ReiUy
XML
_L
Python
Wyraenia regukrne
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.
Wydawnictwo Helion
Ksiga eksperta
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.
Wydawnictwo Helien