You are on page 1of 243

Przedmowa

C++ naley do grupy obiektowych jzykw programowania. Korzenie tych jzykw sigaj koca lat szedziesitych; rok 1967 uwaa si za dat powstania pierwszego jzyka obiektowego, Simula-67. Jzyk Simula-67 bazowa na wczeniejszej pracy nad specjalizowanym jzykiem Simula-1, przeznaczonym do symulacji zdarze dyskretnych, zachodzcych w reaktorach jdrowych. Twrcami Simuli byli dwaj badacze norwescy, Kristen Nygaard i Ole-Johan Dahl, pracujcy w Norweskim Centrum Obliczeniowym. W jzyku tym wprowadzono szereg poj i koncepcji, ktre z niewielkimi zmianami przejy wspczesne jzyki obiektowe. W szczeglnoci, obok typw wbudowanych (integer, real, Boolean), podobnych do stosowanych w najbardziej wwczas znanym jzyku proceduralnym ALGOL, wprowadzono w Simuli pojcie klasy. Wprowadzono rwnie mechanizm dziedziczenia, ktry przekazuje klasie potomnej cechy klasy rodzicielskiej. Kolejnym znaczcym krokiem w rozwoju jzykw obiektowych byo opracowanie przez Alana Kay z Xerox PARC jzyka Smalltalk. Jzyk ten powsta w latach 1970-1972 i posuy jako pierwowzr dla wspczesnej jego wersji, opracowanej przez Adele Goldberg w roku 1983; wersja ta znana jest obecnie pod nazw Smalltalk-80. Okresem szczeglnej aktywnoci w badaniach nad jzykami programowania byy lata siedemdziesite. Powstao wtedy prawdopodobnie kilka tysicy rnych jzykw i ich dialektw, ale przetrwao w szerszym obiegu zaledwie kilka. Tak wic na rynku utrzymay si jzyki: Smalltalk, Ada (sukcesor jzykw ALGOL 68 i Pascal, z pewnym wkadem od jzykw Simula, Alphard i CLU), C++ (pochodzcy z mariau jzykw C i Simula), Eiffel (oryginalne dzieo Bertranda Meyera, bazujcy po czci na Simuli) oraz obiektowe wersje jzykw Pascal (Object Pascal, Turbo Pascal) i Modula-2 (Modula-3). Powstao rwnie szereg jzykw obiektowych dla konstruowania systemw ekspertowych oraz tzw. sztucznej inteligencji. Wrd nich take nastpia ostra selekcja i powszechnie stosowanych jzykw pozostao niewiele; mona tu wymieni dwa szerzej znane: CLOS (akronim od Common Lisp Object System) oraz CLIPS (akronim od C Language Integrated Production System), ktry po rozszerzeniu o mechanizm dziedziczenia znany jest obecnie pod nazw COOL (akronim od. CLIPS Object-Oriented Language). Sprbujmy teraz okreli, jakie wsplne wasnoci maj wszystkie wymienione jzyki. Obecnie uwaa si, e jzyk programowania mona uzna za obiektowy, jeeli spenia nastpujce wymagania: Pozwala definiowa klasy i ich wystpienia, nazywane obiektami. Definicja klasy w jzyku obiektowym jest podobna do definicji abstrakcyjnego typu danych w jzykach proceduralnych (typu, ktry nie jest wbudowany, lecz moe by zdefiniowany przez programist). W jzykach Eiffel, Smalltalk i Simula klasa jest niepodzieln jednostk syntaktyczn, zawierajc definicje struktur danych i definicje operacji wykonywanych na tych strukturach. Natomiast w jzykach hybrydowych, jak np. Object Pascal, Turbo Pascal, CLOS i C++ klasa jest traktowana jako deklaracja typu umieszczana w jednym pliku, a definicje operacji (nazywane metodami, funkcjami skadowymi, lub funkcjami pierwotnymi) umieszcza si zwykle w innym pliku. Konkretna operacja moe by implementowana za pomoc jednej tylko metody lub wielu metod. W pierwszym przypadku nazywamy j monomorficzn, za w drugim polimorficzn albo wirtualn. Wystpienia obiektw danej klasy s tworzone wedug tego samego wzorca, zawartego w definicji klasy. W rezultacie wszystkie obiekty danej klasy maj takie same struktury danych (atrybuty) i operacje. Tym niemniej kady obiekt ma wasn tosamo, a wic, po utworzeniu, istnieje niezalenie od innych obiektw tej samej klasy. Zapewnia ukrywanie informacji (hermetyzacj, ang. encapsulation), co mona rozumie jako

zamknicie obiektu w swego rodzaju czarnej skrzynce lub kapsuce. W wikszoci jzykw obiektowych hermetyzacja nie zawsze jest pena, poniewa zawieraj one mechanizmy kontroli dostpu do elementw klasy. Jako zasad przyjmuje si ukrywanie przed uytkownikiem definicji struktur danych i definicji operacji, przy czym same operacje (lub tylko cz z nich) s publicznie dostpne. Zbir publicznie dostpnych operacji dla obiektu danej klasy nazywa si czsto publicznym interfejsem obiektu. Taka konstrukcja klas jest logiczna, poniewa dla uytkownika klasy istotne jest tylko to, jak si nazywa dana operacja, do czego suy i jak si j uaktywnia (wywouje). Hermetyzacj realizuje si zwykle w ten sposb, e uytkownik nie ma dostpu do kodu rdowego z definicjami klas i operacji, a jedynie do publicznych interfejsw. Dziki temu zapewnia si ochron klas (przede wszystkim predefiniowanych klas bibliotecznych) przed nieuprawnionym dostpem. Posiada wbudowany mechanizm dziedziczenia, dziki ktremu mona tworzy klasy potomne (podklasy, klasy pochodne) od jednej lub kilku klas rodzicielskich, nazywanych superklasami lub klasami bazowymi. Klasy potomne mog z kolei by klasami rodzicielskimi dla swoich klas pochodnych, co pozwala tworzy drzewa (hierarchie) klas. W praktyce klasa bazowa jest na og prost konstrukcj jzykow, za klasa pochodna jest jej specjalizacj, co zwykle prowadzi do rozszerzenia definicji klasy bazowej o nowe elementy. Elementami tymi mog by nowe struktury danych i nowe metody, bd te operacje o tych samych nazwach co w klasie bazowej, ale o innych definicjach. Mechanizm dziedziczenia jest niezwykle efektywny: nie wymaga kopiowania kodu rdowego klasy bazowej, poniewa klasa pochodna automatycznie dziedziczy wszystkie lub wybrane cechy klasy bazowej. W rezultacie mamy lepsz, bardziej przejrzyst organizacj programu. Dysponuje cechami polimorfizmu. Polimorfizm jest terminem zapoyczonym z biologii i oznacza dosownie wielopostaciowo. W odniesieniu do programw obiektowych polimorfizm mona okreli krtko: jeden interfejs (operacja), wiele metod. Najprostsz postaci (wbudowany polimorfizm ad hoc) polimorfizmu jest wykorzystanie tego samego symbolu dla semantycznie nie zwizanych operacji. Polimorfizm tego rodzaju jest charakterystyczny dla wikszoci wspczesnych jzykw programowania wysokiego poziomu, nie tylko obiektowych. Przykadem moe by uywanie tych samych symboli operacji arytmetycznych, np. symbolu mnoenia *, przy mnoeniu liczb cakowitych i rzeczywistych, chocia w kadym konkretnym przypadku bdzie wywoywana inna metoda mnoenia. Realizacja takiego wywoania wymaga wyznaczenia adresu danej metody; jeeli adresy odpowiednich metod s przekazywane w fazie kompilacji, to proces ten nazywany jest wizaniem wczesnym lub statycznym. W jzykach obiektowych mechanizm wizania wczesnego wykorzystuje si rwnie przy przecianiu operatorw, tj. nadawaniu innego znaczenia operatorom wbudowanym w jzyk oraz (co jest specyfik jzyka C++) przy przecianiu funkcji. Jednak o sile jzyka obiektowego decyduje moliwo wykorzystania wizania pnego (dynamicznego), gdy adres wywoywanej metody staje si znany dopiero w fazie wykonania programu. Wizanie pne wystpuje dla hierarchii klas zaprojektowanej w taki sposb, e zdefiniowane w pierwotnej klasie bazowej (korzeniu drzewa klas) metody s redefiniowane w klasach pochodnych z zachowaniem tej samej nazwy, typu, liczby i typw argumentw. Tego rodzaju metody nazywa si wirtualnymi. Zauwamy, e konstrukcja taka nie pozwala na zwizanie wywoania operacji z jej metod w fazie kompilacji, poniewa posta wywoania jest dokadnie taka sama dla kadej operacji w caej hierarchii klas. Dopiero w fazie wykonania, gdy zostanie utworzony obiekt odpowiedniej klasy, mona wywoa metod wirtualn dla tego obiektu.

Jzyk C++ zawiera wszystkie wymienione cechy, a ponadto takie, ktre czyni go bardzo efektywnym; dziki temu staje si de facto standardem przemysowym. De facto, poniewa dotychczasowe prace komitetw normalizacyjnych ANSI X3J16 oraz ISO WG-21 doprowadziy jedynie do opublikowania w roku 1994 zarysu standardu. Tekstem rdowym dla obu komitetw

bya wykadnia jzyka, opublikowana w ksice Margaret Ellis i twrcy C++, Bjarne Stroustrupa, The Annotated C++ Reference Manual [2]. Pierwotnym zamysem Stroustrupa byo wyposaenie bardzo efektywnego jzyka C w klasy, wzorowane na klasach Simuli. Zrealizowa go w roku 1980, konstruujc preprocesor rozszerzonego w ten sposb jzyka C. Nowy jzyk, nazwany C with classes, by wtedy traktowany raczej jako dialekt jzyka C, chocia zawiera ju wikszo cech jzyka C++ (dziedziczenie, kontrola dostpu, konstruktory i destruktory, klasy zaprzyjanione, silna typizacja). W latach 80-tych Stroustrup wraz z grup wsppracownikw wprowadza dalsze mechanizmy i konstrukcje jzykowe (funkcje rozwijalne, argumenty domylne, przecianie operatorw i funkcji, referencje, stae symboliczne, zarzdzanie pamici, dziedziczenie mnogie, funkcje wirtualne, szablony klas i funkcji, obsuga wyjtkw), co wraz ze cilejsz kontrol typw doprowadzio jzyk C++ do obecnego ksztatu. C++ jest jzykiem wysoce modularnym; kady program mona zdekomponowa na oddzielnie kompilowane moduy z publicznym interfejsem i ukryt implementacj. I odwrotnie: do kadego programu mona docza wczeniej opracowane moduy, przechowywane na dysku w postaci tzw. plikw nagwkowych. Kluczowym pojciem w C++ jest klasa, traktowana jako typ definiowany przez uytkownika. W definicji jzyka nie przewidziano standardowej biblioteki klas jako czci rodowiska programowego, chocia opracowanie [4], sygnowane przez AT&T, zawiera opis i sposb korzystania z bibliotek wejcia/wyjcia. W praktyce mona wic spotka wiele bibliotek klas, opracowanych niezalenie od standardu AT&T, jak np. bibliotek NIH (USA, National Institutes of Health), wzorowan na bibliotece jzyka Smalltalk; bibliotek Interviews, ktra pozwala na dogodne uywanie systemu X Window z poziomu C++; bibliotek GNU C++ (g++), opracowan w ramach projektu GNU; biblioteki dla tworzenia obiektw trwaych (POET, ObjectStore, ONTOS, Versant). biblioteki specjalizowane, jak np. RHALE++ (dla oblicze matematycznych w fizyce), SIMLIB (dla symulacji sieci przeczanych). Z biecych informacji wynika, e komitety ANSI/ISO przyjy ju standardy dla nastpujcych klas bibliotecznych: array (szablon tablic), dynarray (szablon tablic dynamicznych), string (szablon acuchw), wstring (szablon acuchw z rozszerzonym kodem znakw), bits<N> (szablon zbioru bitowego o ustalonej licznoci), bitstring (szablon zbioru o zmiennej licznoci) i complex (liczby zespolone). W najbliszym czasie mona si spodziewa przyjcia standardw dla szablonw klas: vector, list i associative array (map). Ze wzgldu na brak niektrych standardw, biblioteki klas s uywane w niniejszej ksice raczej oszczdnie; prawie wycznie bd to biblioteki wejcia/wyjcia z bardzo nielicznymi odstpstwami. Dziki temu zamieszczone w tekcie przykady (a jest ich ponad 160) byy kompilowane i wykonywane zarwno w rodowisku Windows95 (kompilator Borland C++, wersja 5.01, kompilator Visual C++ v.4.0), jak i w rodowisku Unix (kompilatory CC, GNU gcc, GNU g++, v.2.8.1). Prezentowany tekst naley traktowa raczej jako wstp do programowania w jzyku C++, a nie jako wyczerpujcy podrcznik (zarwno w sensie kompletnoci wykadu, jak i znuenia potencjalnego czytelnika). W stwierdzeniu tym nie naley upatrywa samokrytyki; w ksice o rozsdnej objtoci mona dokadnie opisa skadni i semantyk jzyka C++, ale pragmatyka moe mie potencjalnie (i ma) tak wiele kontekstw, e nie jest praktycznie moliwe opisanie wszystkich moliwych wariantw i niuansw. Chocia jzyk C++ zosta nadbudowany nad jzykiem C, zrozumienie prezentowanego tekstu, przykadw i programw, nie wymaga umiejtnoci programowania w jzyku C. Tym niemniej zaoono, e czytelnik ma pewne dowiadczenia programistyczne w ktrym z jzykw wysokiego poziomu, jak np. Pascal, Modula-2, czy wreszcie wspomniany jzyk C. Bardzo polecam uwane przestudiowanie zamieszczonych przykadw; poniewa zdecydowana wikszo z nich to kompletne programy, warto je skompilowa i wykona w dostpnym rodowisku programowym.

Sdz, e towarzyszce przykadom dyskusje i analizy programw oka si interesujce nie tylko dla pocztkujcych, ale i dla zaawansowanych programistw, ktrych uwadze polecam rozdziay 10 i 11, powicone obsudze wyjtkw i dynamicznej kontroli typw. Zawarte w tych rozdziaach opisy i dyskusje oparto na ostatnich ustaleniach wspomnianych komitetw ANSI/ISO, a kompilacja i wykonanie programw wymagaj dostpu do najnowszych kompilatorw, np. Borland C++ w wersji 5.01 lub CC w wersji 4.0. Gdask, w sierpniu 1998 W. M. Porbski

1. Podstawowe elementy jzyka C++


W jzyku C++ wykorzystuje si zbir znakw ASCII, zdefiniowany norm ANSI3.4-1968. Jest to amerykaski wariant midzynarodowego, 7-bitowego kodu ISO 646-1983. Elementami tego zbioru s: mae i due litery alfabetu aciskiego, cyfry od 0 do 9, znaki przestankowe i inne symbole specjalne. Kady znak zbioru ASCII ma swj numer porzdkowy (kod znaku); np. kodem znaku 'A' jest liczba 65 w dziesitnym systemie liczenia, lub 101 w systemie oktalnym (semkowym). Znaki kodu ASCII zestawiono w Dodatku A. Poniewa C++ jest jzykiem o silnej typizacji, zatem kada nazwa (identyfikator) w programie musi mie zwizany z ni typ, podawany w deklaracji identyfikatora. Typ okrela zbir wartoci, jakie mona przypisa danej nazwie oraz zbir operacji, jakie mona zastosowa do takiej nazwy (tj. do reprezentowanej przez t nazw struktury danych), a take sposb interpretacji poszczeglnych operacji. 1.1. Wbudowane typy danych

Kada implementacja jzyka C++ zawiera dwie kategorie typw danych: typy wbudowane i typy definiowane przez uytkownika. Zarwno pierwsze, jak i drugie s abstrakcyjnymi reprezentacjami rzeczywistych struktur danych. Pord typw wbudowanych wyrnia si typy podstawowe: typ char, typ int, typ float, typ double, typ bool i typ enum. Nazwy typw podstawowych mona poprzedza tzw. specyfikatorami typu, tj. sowami kluczowymi short, long, signed i unsigned, ktre zmieniaj wewntrzn reprezentacj danych tych typw. Kady typ wbudowany ma take szereg skojarzonych z nim typw pochodnych: wskaniki, referencje i tablice. Rysunek 1-1 ilustruje typy wbudowane.
int char double enum Podstawowe typy danych unsigned int i; Deklaracja zmiennej i, ktra jest typu unsigned int bool float short unsigned Specyfikatory signed long

Rysunek 1-1 Podstawowe typy danych C++ Poniej zestawiono sensowne (i najczciej uywane) kombinacje typw i specyfikatorw. Dla wielkoci (staych i zmiennych) cakowitych: char, short int, int, long int. Norma ANSI/ISO stanowi, e zapisw short int oraz long int nie mona w programach skraca do short i long. Dla wielkoci (staych i zmiennych) zmiennoprzecinkowych: float, double, long double. Dla wielkoci cakowitych bez znaku, wartoci logicznych, tablic bitowych: unsigned char,

unsigned short int, unsigned int, unsigned long int. Przy jawnym deklarowaniu wielkoci ze znakiem: signed char, signed short int, signed int. F Uwaga1. Jeeli na wielkociach typw char oraz int maj by przeprowadzane operacje arytmetyczne lub logiczne, to s one najpierw niejawnie przeksztacane do typu int. W analogicznej sytuacji dane typu float s przeksztacane do typu double. W jzyku C++ istnieje ponadto typ void, ktry posiada pusty zbir wartoci. Z punktu widzenia skadni typ void zachowuje si analogicznie do typw podstawowych (int, char, float, double, enum). Jednake mona go uywa jedynie jako fragmentu typu pochodnego, poniewa nie ma obiektw typu void. Na pierwszy rzut oka moe si wydawa dziwnym posiadanie typu, dla ktrego nie ma zdefiniowanych wartoci. Jednak w praktyce typ ten jest bardzo uyteczny, szczeglnie w zastosowaniu do funkcji, ktre nie zwracaj adnej wartoci, a wic odpowiadaj znanym z innych jzykw programowania procedurom. F Uwaga 2. Typy char, int (z ewentualnymi specyfikatorami) oraz enum s cznie nazywane typami cakowitymi. Typy cakowite i zmiennopozycyjne s cznie nazywane typami arytmetycznymi. 1.2. Jednostki leksykalne

W jzyku C++ znajdujemy cztery rodzaje jednostek leksykalnych: identyfikatory, sowa kluczowe, literay oraz rne separatory. Spacje, znaki tabulacji poziomej i pionowej, znaki nowego wiersza i nowej strony oraz komentarze (nazywane cznie biaymi znakami) s w oglnoci ignorowane, chyba e su do separacji jednostek leksykalnych. W procesie kompilacji programu jednostki leksykalne s wyodrbniane (ang. parsing) z wejciowego strumienia znakw w ten sposb, e jako nastpn jednostk bierze si najduszy cig znakw po biaym znaku lub po sekwencji takich znakw. 1.2.1. Identyfikatory Nazwa (identyfikator) jest sekwencj liter i cyfr o dowolnej dugoci. Pierwszy znak musi by liter, przy czym znak podkrelenia '_' jest traktowany jako litera. Rozrniane s litery mae i due. Naley unika stosowania nazw, zaczynajcych si od znaku podkrelenia lub dwch kolejnych znakw podkrelenia, poniewa nazwy takie s zarezerwowane dla predefiniowanych identyfikatorw, m. in. dla bibliotek. 1.2.2. Komentarze Komentarze s, w pewnym sensie, wizytwk programisty, poniewa ich zadaniem jest zwikszenie czytelnoci programu. Krtkie komentarze maj posta dowolnego cigu znakw, zapisywanych w jednym wierszu po symbolu komentarza '//'. Taki komentarz koczy si wraz z kocem danego wiersza. Dusze komentarze zaleca si umieszcza pomidzy parami znakw '/*' i '*/'. Jeeli po symbolu '//' lub pomidzy symbolami '/*' i '*/' wystpi pary znakw '//', bd pary znakw '/*' i '*/', to bd one traktowane jak zwyke znaki (w C++ nie ma komentarzy zagniedonych). Podobnie jak w innych jzykach programowania, komentarze wpywaj tylko na wielko kodu rdowego, poniewa s usuwane z programu przez kompilator przed generacj kodu wynikowego. Poniewa komentarze nie s wczane do wynikowego kodu programu, mog by w nich uywane polskie znaki diakrytyczne, np. , , etc. Jest to pewne uatwienie dla polskiego programisty, ktry jest zmuszony korzysta wycznie ze 127 znakw ASCII we wszystkich pozostaych elementach programu. 1.2.3. Sowa kluczowe i operatory Zestawione niej sowa kluczowe s niepodzielnymi cigami znakw. S to zastrzeone identyfikatory, ktre mona uywa jedynie w cile zdefiniowanym kontekcie.

Asm Auto Bool Break Case Catch Char Class Const const_cast Continue Default Delete

do double dynamic_cast else enum explicit extern false float for friend goto if

inline int long mutable namespace new operator private protected public register reinterpret_cast return

Short Signed Sizeof static static_cast struct switch template this throw true try typedef

typeid typename union unsigned using virtual void volatile wchar_t while

Jeeli reprezentacja wewntrzna kodu rdowego jest zapisywana w kodzie ASCII, to ponisze symbole jednoznakowe s uywane jako znaki przestankowe lub operatory: ! [ % ^ ] \ & ; * ' ( : ) " < + > = ? { , } . | / ~

Operatorami s rwnie nastpujce symbole dwu- i trzyznakowe: -> ++ -|| *= .* /= ->* %= << += >> -= <= <<= >= >>= == &= != ^= && |= ::

Ponadto wiersze tekstu programu rdowego, ktre maj by przetwarzane przez preprocesor, znakuje si symbolem '#' w pierwszej kolumnie nowego wiersza. 1.2.4. Stae cakowite Stae cakowite s przykadami literaw staych; literaw, poniewa mwimy jedynie o ich wartociach; staych, poniewa ich wartoci nie mona zmienia. Kady litera jest pewnego typu; np. 2 jest typu int. Staa cakowita jest traktowana jako cakowita liczba dziesitna, jeeli skada si z cigu cyfr dziesitnych. Cig cyfr dziesitnych z zakresu 0-7 jest traktowany jako oktalna (semkowa) liczba cakowita, jeeli pierwszym znakiem cigu jest cyfra 0. Szesnastkowa (ang. hexadecimal) staa cakowita jest cigiem cyfr szesnastkowych (cyfry 0-9 i/lub litery a-f, bd A-F), poprzedzonych dwuznakowymi symbolami '0x' (zero-x)lub '0X'. Przykady: 89 //liczba dziesitna 037 //liczba semkowa 0x12 //liczba szesnastkowa 0X7F //liczba szesnastkowa Typ staej cakowitej przyjmowany domylnie przez kompilator zaley od jej postaci, wartoci i przyrostka. Jeeli jest to liczba dziesitna i nie ma przyrostka, to typ domylny zaley od jej wartoci i jest typem int, long int, lub unsigned long int. Jeeli jest to liczba oktalna lub szesnastkowa i nie ma przyrostka, to typ domylny zaley od jej wartoci i jest typem int, unsigned int, long int, unsigned

long int. Dodajc specyfikator u bd U (dla unsigned int), i/lub l bd L (dla long int) moemy wymusi inny sposb reprezentacji staej cakowitej, np. 25UL, 127u, 38000L. 1.2.5. Stae zmiennopozycyjne Stae zmiennopozycyjne nale do podzbioru liczb rzeczywistych. Mona je zapisywa w notacji dziesitnej z kropk dziesitn, np. 0.0 .28 lub w notacji wykadniczej, np. 1.18e12 -3.1415E-3 2. 3e8 -84.17

Jeeli po liczbie nie podano specyfikatora typu, to kompilator nadaje jej typ domylny double. Dokadno reprezentacji staej zmiennopozycyjnej mona wymusi, dodajc po zapisie liczby specyfikator f lub F (dla typu floatalbo l lub L (dla typu long double), np. -84.17f 1.2.6. Stae znakowe Staa (litera) znakowa jest to cig, zoony z jednego lub wikszej liczby znakw, ujty w pojedyncze apostrofy, np. 'x'. Stae jednoznakowe s typu char. Wartoci staej jednoznakowej jest warto numeryczna znaku w maszynowym zbiorze znakw (np. dla zbioru znakw ASCII, wartoci 'A' jest 65 dziesitnie lub 101 oktalnie). Typem staej wieloznakowej jest int. Pewne znaki, ktre nie maj reprezentacji graficznej na ekranie monitora, czy te na papierze drukarki, mog by reprezentowane w programie przez tzw. sekwencje ucieczki, zapisywane ze znakiem '\' (ang. escape sequences; sowo ucieczka mwi o tym, e nastpny po \ znak ucieka od przypisanego mu standardowego znaczenia), jak pokazano w tablicy 1.1. Wartoci znakw podane w tablicy 1.1 s zapisane w systemie oktalnym lub szesnastkowym. Tablica 1.1 Sekwencje ucieczki dla znakw kodu ASCII Nazwa sekwencji nowy wiersz (new-line) tabulacja pozioma (horizontal tab) tabulacja pionowa (vertical tab) (backspace) powrt karetki (carriage return) nowa strona (form feed) dzwonek (alert) \ (backslash) znak zapytania (question mark) pojedynczy apostrof (single quote) Podwjny apostrof (double quote) znak zerowy integer() liczba oktalna (octal number) Symbol NL (LF) HT VT BS CR FF BEL \ ? ' " NUL ooo Zapis znakowy \n \t \v \b \r \f \a \\ \? \' \" \0 \ooo Warto liczbowa 12 11 13 10 15 14 7 x5c x3f x27 x22 0 ooo .28F, 1.0L 3.14159e-3L

liczba szesnastkowa (hex number) 1.2.7. Stae acuchowe

hhh

\xhh

xhh

Staa (litera) acuchowy jest to cig o dugoci zero lub wicej znakw, ujty w podwjne apostrofy. Jeeli w cigu wystpuj znaki niedrukowalne (np. BEL), to s one reprezentowane przez ich sekwencje ucieczki. W reprezentacji wewntrznej do kadego acucha jest dodawany terminalny znak zerowy '\0' o wartoci 0; tak wic np. acuch "abcd" ma dugo 5 (a nie 4) znakw, poniewa po znaku 'd' kompilator doda znak zerowy '\0'. Jeeli acuch rozciga si na kilka wierszy, to na kocu kadego wiersza mona doda znak '\', ktry sygnalizuje kompilatorowi, e staa acuchowa jest kontynuowana w nastpnym wierszu.

2. Struktura i elementy programu 2.1.


Deklaracje i definicje

Jak ju wspomniano, wszystkie wielkoci wystpujce w programie musz by przed ich uyciem zadeklarowane. Deklaracje ustalaj nieodzowne odwzorowanie pomidzy strukturami danych i operacjami, a reprezentujcymi je konstrukcjami programowymi. Kada deklaracja wie podany przez uytkownika identyfikator z odpowiednim typem danych. Wikszo deklaracji, znanych jako deklaracje definiujce lub definicje, powoduje take utworzenie definiowanej wielkoci, tj. przydzielenie (alokacj) jej fizycznej pamici i ewentualne zainicjowanie. Pozostae deklaracje, nazywane deklaracjami referencyjnymi lub deklaracjami, maj znaczenie informacyjne, poniewa jedynym ich zadaniem jest podanie deklarowanej nazwy i jej typu do wiadomoci kompilatorowi. Tak zadeklarowany identyfikator musi by w programie zdefiniowany albo pniej w tym samym, albo w oddzielnym pliku z kodem rdowym. Dla danego identyfikatora moe wystpi wiele deklaracji referencyjnych, ale tylko jedna deklaracja definiujca (przypadki takie wystpuj najczciej w programach wieloplikowych). Oczywist jest zasada, e aden identyfikator nie moe by uyty w programie przed jego punktem deklaracyjnym w kodzie rdowym. Deklaracja zakoczona rednikiem nazywa si instrukcj deklaracji. Najczciej deklarowanymi wielkociami s zmienne. Zmienn okrela si jako pewien obszar pamici o zadanej symbolicznej nazwie, w ktrym mona przechowywa wartoci, interpretowane zgodnie z zadeklarowanym typem zmiennej. Jzyk C++ uoglnia to pojcie: wymieniony obszar pamici moe nie posiada nazwy, moe mie nie jedn lecz kilka nazw, za adres pocztku obszaru pamici moe by dostpny dla programisty. Przykad 2.1 ilustruje rnorodno moliwych deklaracji i definicji. Przykad 2.1. char znak; char litera = 'A'; char* nazwa = "Cplusplus"; extern int ii; int j = 10; const double pi = 3.1415926; enum day { Mon, Tue, Wed, Thu, Fri, Sat, Sun }; struct mystruct; struct complex { float re, im;}; complex zespolone; typedef complex punkt; class myclass; template<class T> abs(T x) { return x < 0 ? -x : x; } Dyskusja. Jak wida z powyszego przykadu, w deklaracji mona umieci o wiele wicej informacji dla kompilatora, ni jedynie wskaza, e dana nazwa jest zwizana z okrelonym typem. Tylko 3 spord nich extern int ii; struct mystruct; class myclass;

s deklaracjami referencyjnymi, a zatem musz im towarzyszy odpowiednie definicje. Definicje struktury mystruct i klasy myclass musz si znale w tym samym pliku, w ktrym podano ich deklaracje, za definicja zmiennej ii musi by podana w jednym z plikw programu. Pozostae deklaracje s jednoczenie definicjami:

Wielkoci znak, litera oraz j s zmiennymi typw podstawowych. Stosownie do ich typw kompilator przydzieli im odpowiednie obszary pamici. Zauwamy take, e zmienne litera, nazwa oraz j zostay zainicjowane odpowiednimi dla ich typw wartociami. Wielko pi typu double jest sta symboliczn, o czym informuje sowo kluczowe (deklarator staej) const na pocztku deklaracji. Warto tej staej (3.1415926) jest znana kompilatorowi, zatem nie trzeba dla niej rezerwowa pamici. Wielkoci day i complex s definicjami nowych typw; identyfikator zespolone jest zmienn typu complex, za identyfikator punkt jest zastpcz nazw (synonimem) dla complex.

2.1.1.

Deklaracje staych

Jzyk C++ pozwala definiowa szczeglnego rodzaju zmienne, ktrych wartoci s ustalone i niezmienne w programie. Jeeli definicj zmiennej zainicjowanej, np. int cyfra = 7; poprzedzimy sowem kluczowym const const int cyfra = 7; to przeksztacimy w ten sposb symboliczn zmienn cyfra z pierwszej definicji w sta symboliczn o tej samej nazwie. Warto tak zdefiniowanej staej symbolicznej pozostaje niezmienna w programie, a kada prba zmiany tej wartoci bdzie sygnalizowana jako bd. Sowo kluczowe const, ktre zmienia interpretacj definiowanej wielkoci, jest nazywane modyfikatorem typu. Nazwa uzasadniona jest tym, e const ogranicza moliwoci uycia definiowanej wielkoci tylko do odczytu, ale zachowuje informacj o typie ( w przykadzie jak wyej typ staej cyfra zosta zdefiniowany jako int). Dziki temu stae symboliczne w jzyku C++ mona uywa zamiast literaw tego samego typu. Zauwamy te, e skoro nie mona zmienia wartoci staej symbolicznej po jej zdefiniowaniu, to musi ona by zainicjowana. Np. zapis const double pi; jest bdny, poniewa wielko pi nie zostaa zainicjowana. Podobnie jak wszystkie obiekty programu, staa symboliczna moe mie tylko jedn definicj. Po zdefiniowaniu jest ona (domylnie) widoczna tylko w pliku, w ktrym umieszczono jej definicj. W wikszych programach, skadajcych si z wielu plikw, moemy uczyni j widoczn dla innych plikw, umieszczajc w nich deklaracje, informujce kompilator o tym, e w jednym z plikw programu znajdzie jej definicj. Informacj t przekazujemy kompilatorowi za pomoc sowa kluczowego extern, umieszczonego przed deklaracj. Np. sta cyfra moemy udostpni innym plikom programu, umieszczajc w nich deklaracje o postaci: extern const int cyfra; F Uwaga. Starsze kompilatory jzyka C++ odgaduj typ staej symbolicznej na podstawie jej wartoci numerycznej i formatu definicji. Jednak standard wymaga jawnego podawania typu kadej staej.

2.1.2.

Wyliczenia

Alternatywnym, a czsto bardziej przydatnym sposobem definiowania symbolicznych staych cakowitych jest uycie do tego celu typu wyliczeniowego (ang. enumerated type). Wyliczenie deklaruje si ze sowem kluczowym enum, po ktrym nastpuje wykaz staych cakowitych (ang.

enumerators) oddzielonych przecinkami i zamknitych w nawiasy klamrowe. Wymienionym staym s przypisywane wartoci domylne: pierwszej z nich warto 0, a kadej nastpnej warto o 1 wiksza od poprzedzajcej. Np. wyliczenie: enum {mon, tue, wed, thu, fri, sat, sun}; de niuj siedems ay h kowit c i prz pis j im war o ci od0 do6. a zauway,e fi e t c ca yh y ue t two powysz zapis skr s y sekwen jdekla a ji t c y je t t zni ca r c s ay h: const int mon = 0; const int tue = 1; . . . const int sun = 6; Staym typu wyliczeniowego mona rwnie przypisywa wartoci jawnie, przy czym wartoci te mog si powtarza. Np. deklaracja: enum { false, fail = 0, pass, true = 1 }; przypisuje warto 0 do false i fail (do false domylnie) oraz warto 1 do pass i true (do pass domylnie). W wyliczeniach mona po enum umieci identyfikator, ktry stanie si od tego momentu nazw nowego typu. Np. enum days { mon, tue, wed, thu, fri, sat, sun }; definiuje nowy typ days. Pozwala to od tej chwili deklarowa zmienne typu days, np. days anyday = wed; W taki sam sposb mona wprowadzi zapis, ktry imituje typ bool (jeli nasz kompilator nie zawiera implementacji tego typu) enum bool {false, true}; bool found, success; //lub np. bool success = true; Deklaracje zmiennych typu wyliczeniowego mona rwnie umieszcza pomidzy zamykajcym nawiasem klamrowym a rednikiem, np. enum bool {false,true} found, success; F Uwaga. Typ wyliczeniowy jest podobny do typw char oraz shortint w tym, e nie mona na jego wartociach wykonywa adnych operacji arytmetycznych. Gdy warto typu wyliczeniowego pojawia si w wyraeniach arytmetycznych, to jest niejawnie przeksztacana do typu int przed wykonaniem operacji.

2.2. Dyrektywy preprocesora


Kompilacja programu rdowego, napisanego w jzyku C++, przebiega zwykle w czterech lub piciu kolejnych krokach; produktem kocowym jest kod adowalny. W pierwszym kroku uruchamiany jest program, nazywany preprocesorem. Preprocesor przetwarza te wiersze tekstu programu, ktre zaczynaj si znakiem # w pierwszej kolumnie. W wierszach tych zapisujemy

polecenia, nazywane dyrektywami. W odrnieniu od instrukcji, ktre s wykonywane po zakoczeniu kompilacji i uruchomieniu programu, dyrektywy s poleceniami do natychmiastowego wykonania przed rozpoczciem waciwego procesu kompilacji. Poniewa dyrektywy przywouj predefiniowane wielkoci biblioteczne, z reguy umieszcza si je w pierwszych wierszach tekstu programu.

2.2.1.

Dyrektywa #include

Dyrektywa #include pozwala zebra razem wszystkie fragmenty programu rdowego w jeden modu, nazywany plikiem rdowym lub jednostk translacji. Jeeli dyrektywa ta wystpuje w postaci: #include <nazwa-pliku.h> to nazwa-pliku.h odnosi si do standardowego, predefiniowanego pliku nagwkowego (bibliotecznego), umieszczonego w standardowym katalogu plikw doczanych (ang. standard include directory). Polecenie o takiej postaci zleca preprocesorowi poszukiwanie pliku tylko w standardowych miejscach bez przeszukiwania katalogu zawierajcego plik rdowy. Jeeli dyrektywa #include wystpuje w postaci: #include "nazwa-pliku.h" to preprocesor zakada, e plik o nazwie nazwa-pliku.h zosta zaoony przez uytkownika. W takim przypadku poszukiwanie pliku zaczyna si od katalogu biecego; jeeli wynik poszukiwania jest niepomylny, to jest ono kontynuowane w katalogu standardowym. F Uwaga 1. W implementacjach Unix-owych standardowym katalogiem dla plikw nagwkowych jest czsto /usr/include/CC. Jeeli jest inaczej, to mona wymusi pocztkow ciek poszukiwania, dodajc opcj -I w wierszu rozkazowym, np. $ CC -I incl -I/usr/local/include myprog.cc. W implementacji C++ firmy Borland pod systemem MS-DOS katalogiem standardowym bdzie katalog c:\borlandc\include F Uwaga 2. Zarwno dla standardowych, jak i przygotowanych przez uytkownika plikw nagwkowych mona w programie poda jawnie pen ciek do pliku, np. #include</usr/local/include/CC/iostream.h> czy te < c:\borlandc\include\iostream.h >

2.2.2.

Dyrektywa #define

Najprostsza posta dyrektywy #define ma skadni: #define nazwa co czytamy: zdefiniuj identyfikator nazwa. Dyrektyw #define uywa si najczciej w kontekcie z dyrektyw #include, dyrektywami #if #elif #else #endif oraz #ifdef #else #endif i #ifndef #endif. Konteksty te stosuje si gwnie w celu uniknicia wielokrotnego definiowania tego samego identyfikatora i/lub wielokrotnego wstawiania tego samego pliku bibliotecznego do pliku rdowego. Dyrektyw #define mona rwnie zapisa w postaci: #define nazwa sekwencja-znakw

co czytamy: zastpuj wszystkie wystpienia identyfikatora nazwa podan sekwencj znakw. Przykad 2.2. #if SYSTEM == SYSV #define HDR "sysv.h" #elif SYSTEM == BSD #define HDR "bsd.h" #elif SYSTEM == MSDOS #define HDR == "msdos.h" #else #define HDR "default.h" #endif #include HDR Dyskusja. Pokazana wyej sekwencja dyrektyw testuje nazw SYSTEM dla podjcia decyzji, ktr wersj pliku nagwkowego naley wczy do programu. Dyrektywa #if sprawdza warto wyraenia: jeeli warto ta jest rna od zera (prawda), to przetwarzana jest nastpujca po niej dyrektywa #define i dyrektywa #include; jeeli nie to sprawdzane s kolejne alternatywy (elif odpowiada else if), po czym przetwarzana jest dyrektywa #include. Przy wczaniu do programu standardowych identyfikatorw i standardowych plikw bibliotecznych, uytkownik moe czu si zwolniony od takiego sprawdzania, poniewa obowizek ten spoczywa na twrcach kompilatorw. Niej pokazano tego rodzaju testy, zastosowane w plikach nagwkowych iostream.h dla dwch kompilatorw. Przykad 2.3. //Borland C++, plik iostream.h #ifndef __IOSTREAM_H #define __IOSTREAM_H #if !defined(__DEFS_H) #include <_defs.h> #endif // definicje z plikw _defs.h i iostream.h #endif //Borland C++, plik ver.h ... typedef int BOOL; #define TRUE 1 #define FALSE 0 Dyskusja. Dyrektywa #ifndef daje warto TRUE (1) dla warunku nie zdefiniowany. Zatem #ifndef identyfikator daje taki sam efekt, jak #if 0 jeeli identyfikator zosta ju zdefiniowany i taki sam efekt, jak #if 1 jeeli identyfikator nie jest jeszcze zdefiniowany. Zatem #ifndef __IOSTREAM_H sprawdza, czy nazwa __IOSTREAM_H zostaa ju zdefiniowana. Jeeli nie, to przetwarzana jest dyrektywa #define __IOSTREAM_H (definiujca __IOSTREAM_H) i nastpujca po niej dyrektywa #if, ktra sprawdza, czy jest zdefiniowana nazwa __DEFS_H i w zalenoci od wyniku testu

wcza lub nie plik _defs.h i pozosta zawarto pliku iostream.h. Jeeli nazwa __IOSTREAM_H jest ju zdefiniowana, to preprocesor pominie dyrektyw #define i nastpujce po niej dyrektywy, dziki czemu zawarto pliku iostream.h nie zostanie wstawiona po raz drugi. Przykad 2.4. //plik iostream.h dla kompilatora CC Sun Microsystems #ifndef IOSTREAMH #define IOSTREAMH //definicje z pliku iostream.h ... #ifndef NULL #define NULL 0 #endif ... #ifdef EOF #if EOF ! = -1 #define EOF (-1) #endif #else #define EOF (-1) #endif ... #endif Dyrektywa #define bywa niekiedy stosowana dla definiowania staych. Zapisuje si j wtedy w drugiej z podanych postaci, tj. #define nazwa sekwencja-znakw np. #define WIERSZ 80

Taka posta dyrektywy zleca preprocesorowi zastpowanie dalszych wystpie identyfikatora podanym cigiem znakw. Zapisan wyej dyrektyw mona zastpi definicj staej symbolicznej const int WIERSZ = 80;

i uywa nazwy WIERSZ w programie w taki sam sposb. Definiowanie staych za pomoc #define jest mniej korzystne ni za pomoc modyfikatora const z nastpujcych powodw: Zastpienie identyfikatora cigiem znakw nie wnosi adnej dodatkowej informacji, poniewa tak zdefiniowanej staej nie przypisuje si adnego typu. Staa symboliczna definiowana z const niesie informacj o jej typie i moe by uywana przez program uruchomieniowy (ang. symbolic debugger).

2.3. Struktura prostych programw


Program w jzyku C++, niezalenie od jego rozmiaru, jest zbudowany z jednej lub kilku funkcji, opisujcych dane operacje procesu obliczeniowego. W tym sensie funkcje s podobne do podprogramw, znanych w innych jzykach programowania, a umoliwiajcych strukturalizacj i modularyzacj programw. Kady program musi zawiera funkcj o zastrzeonej nazwie main. Jest to funkcja, od ktrej rozpoczyna si dziaanie programu. Funkcja main zawiera zwykle wywoania rnych funkcji, przy czym niektre z nich s definiowane w tym samym programie, za inne pochodz z bibliotek uprzednio napisanych funkcji. W rezultacie program jest zbiorem odrbnych definicji i deklaracji funkcji. Komunikacja pomidzy funkcjami odbywa si za porednictwem argumentw i wartoci zwracanych przez funkcje, bd (rzadziej) przez zmienne zewntrzne, ktrych zakres obejmuje jeden plik rdowy programu. Pokazany niej przykad wprowadzajcy ilustruje struktur prostego programu. Przykad 2.5. // Struktura programu w jezyku C++ #include <iostream.h> void czytaj(); void sortuj(); void pisz(); int main() { czytaj() ; sortuj() ; pisz() ; return 0; } void czytaj() { cout << "czytaj()\n"; void sortuj() { cout << "sortuj()\n"; void pisz() { cout << "pisz()\n"; } } }

Po wykonaniu programu na ekranie monitora pojawi si trzy wiersze tekstu: czytaj() sortuj() pisz() Dyskusja. Przykad prezentuje kilka charakterystycznych cech jzyka C++. Pierwszy wiersz: // Struktura programu w jezyku C++ jest komentarzem. Drugi wiersz: #include <iostream.h> jest instrukcj sterujc, ktra zleca kompilatorowi wczenie do programu zawartoci pliku nagwkowego o nazwie iostream.h. Plik iostream.h jest standardowym (predefiniowanym) plikiem nagwkowym; jest on wyszukiwany przez kompilator w standardowym katalogu (bibliotece) bez przeszukiwania katalogu zawierajcego plik rdowy. W pliku tym jest zdefiniowany symbol cout, ktry reprezentuje tzw. strumie wyjciowy. Jak wida z przykadu, strumie wyjciowy suy do wyprowadzenia tekstu na ekran monitora. Wstawienie tekstu do

strumienia wyjciowego jest wykonywane przez operator wstawiania (ang. insertion operator), oznaczony symbolem '<<'. Symbol ten jest bardzo trafnie wybrany, poniewa sugeruje on kierunek przepywu danych: z postaci wyraenia cout << "czytaj()\n" moemy atwo si domyli, e chodzi o wstawienie do strumienia wyjciowego cout ujtego w podwjne apostrofy tekstu "czytaj()\n". Kada funkcja programu skada si z czterech czci: typu zwracanego (tutaj void oraz int), nazwy funkcji, wykazu (listy) argumentw i ciaa funkcji. Trzy pierwsze czci s cznie nazywane prototypem funkcji. Zakoczone rednikami zapisy: void czytaj(); void sortuj(); void pisz();

s deklaracjami funkcji (cilej instrukcjami deklarujcymi). Instrukcja deklaracji jest jedyn instrukcj, ktr mona zapisa na zewntrz funkcji (w tym przypadku na zewntrz funkcji main). W omawianym programie deklaracje funkcji zawieraj ich prototypy: typem zwracanym jest wbudowany typ prosty void, nazwami funkcji s identyfikatory czytaj, sortuj i pisz, za wykazy argumentw, ujte w nawiasy okrge, s puste. Deklarowanie funkcji przed ich wywoaniem jest w jzyku C++ obowizkowe z oczywistych wzgldw. Natomiast ich definicje mona umieszcza w dowolnym miejscu programu. W rozwaanym przypadku definicje te umieszczono za funkcj main. Jak wida z przykadu, definicja funkcji skada si z typu zwracanego, nazwy funkcji, listy argumentw (formalnych) oraz ciaa funkcji, objtego par nawiasw klamrowych {}. Ciao funkcji moe zawiera instrukcje, tj. polecenia do wykonania przez dany program. Instrukcja wywoania funkcji, np. czytaj() skada si z nazwy funkcji i ujtej w nawiasy okrge listy argumentw aktualnych (w naszym przypadku lista ta jest pusta). Para nawiasw okrgych () jest w tym kontekcie nazywana operatorem wywoania. Nazwa ta jest uzasadniona tym, e wartociowanie funkcji polega na zastosowaniu operatora () do nazwy funkcji. Jeeli funkcja ma niepust list argumentw, to argumenty aktualne s umieszczane wewntrz operatora wywoania. Operacj t nazywa si przekazywaniem argumentw do funkcji. Funkcja main zawiera cztery instrukcje (zauwamy, e kada z nich koczy si rednikiem). Sekwencj instrukcji, ujt w par nawiasw klamrowych, nazywa si instrukcj zoon. Instrukcja zoona jest traktowana jako pojedyncza jednostka i moe si pojawi wszdzie tam, gdzie ze wzgldw syntaktycznych powinna si znale pojedyncza instrukcja. Zauwamy, e instrukcja zoona nie koczy si rednikiem; jej terminatorem jest zamykajcy nawias klamrowy '}'. Instrukcje zoone mog by zagniedane. Instrukcja zoona moe take zawiera deklaracje; w takim przypadku nazywa si j blokiem. Najprostszy program w jzyku C++ moe wyglda nastpujco: void main() {} Definiuje si w nim funkcj typu void, o nazwie main(), ktra nie przyjmuje argumentw i nic nie robi. Wykonanie funkcji main koczy si instrukcj return 0; jest to wymagany przez jzyk C++ sposb opuszczenia bloku main. W oglnoci instrukcja return moe wystpi w dwch postaciach: return; orazreturn wyraenie; Pierwsz z nich mona stosowa w przypadku funkcji typu void jako opcj.

Zauwamy, e w naszym przykadzie funkcja main() jest typu int. W wikszoci kompilatorw przy deklarowaniu i definiowaniu funkcji, sowo kluczowe int mona pomin. W takim przypadku kompilator przyjmuje int jako domylny typ zwracany. Nastpne dwa przykady ilustruj uycie w programach literaw znakowych i acuchowych. Przykad 2.6. //Komentarze, literaly znakowe i tekstowe #include <iostream.h> //Dyrektywa preprocesora int main() { char c;//deklaracja zmiennej c typu char c = '\101'; //101 jest kodem oktalnym znaku 'A' (ASCII) char c1 = '\11' ; /*11 jest kodem oktalnym znaku tabulacji poziomej (ASCII) */ char c2='\x42'; //42 jest kodem heksalnym znaku 'B' (ASCII) char c3 = '\''; //Nadanie zmiennej c3 wartosci ' (apostrof) cout << c << c1 << c2 << '\n'; //Wydruk wartosci zmiennych c,c1,c2 cout << "abcde12345" << '\n'; //Wydruk lancucha znakow "abcde12345" cout << c3 << endl; //Wydruk wartosci zmiennej c3 cout << int(c2); //Wydruk kodu ASCII znaku 'B' return 0; } Wydruk ma posta: A B abcde12345 ' 66 Dyskusja. Zwrmy uwag na nastpujce elementy programu: Krtkie komentarze po rednikach, ktre kocz zapisy instrukcji. Deklaracje zmiennych (np. char c). Inicjowanie zmiennych podczas ich deklaracji (np. char c1 = '\11'). Kilkakrotne uycie operatora wstawiania << tekstu do strumienia wyjciowego. Oddzielne uycie znaku nowego wiersza '\n' po operatorze <<. Uycie tzw. manipulatora endl zamiast znaku '\n'. Rnica pomidzy nimi jest taka, e '\n' jedynie kieruje wyjcie do nastpnego wiersza, za endl wykonuje t sam czynno oraz oprnia bufor wyjciowy. Zastosowanie jawnej konwersji (ang. cast) typw w wyraeniu int(c2) dla wyprowadzenia kodu ASCII znaku 'B', przypisanego do zmiennej c2 w postaci literau '\x42'.

Przykad 2.7. //Literaly tekstowe, operator sizeof #include <iostream.h> int main() { cout << "abcd\n"; cout << "Liczba znakow w \"abcd\" = " << sizeof "abcd"; cout << "" << endl; //Lancuch pusty cout << "Wyrazy przedzielone\t tabulatorem" << "\n"; cout << "Tekst dwuwierszowy \ pojawi sie w jednym wierszu"; return 0; } Posta wydruku: abcd Liczba znakow w "abcd" = 5 Wyrazy przedzielone tabulatorem Tekst dwuwierszowy pojawi sie w jednym wierszu Dyskusja. Nowe elementy w powyszym przykadzie s nastpujce: Uycie operatora sizeof, ktry zwraca liczb bajtw acucha "abcd", wcznie z terminalnym znakiem zerowym \0. Uycie sekwencji ucieczki dla podwjnego apostrofu i znaku tabulacji. Uycie znakw spacji oraz znaku kontynuacji w staych acuchowych. F Uwaga. Znak nowego wiersza moe by reprezentowany bd jako staa znakowa '\n' bd jako jednoznakowy acuch "\n".

2.4. Zasig i czas ycia obiektw programu


Obiekty C++ (niekoniecznie w sensie uywanym w programowaniu obiektowym, poniewa C++ jest jzykiem hybrydowym) mog by alokowane w pamici statycznej (obiekty globalne), na stosie podprogramu (obiekty lokalne) i w pamici swobodnej (obiekty dynamiczne). Obiekty globalne istniej przez cay czas wykonania programu; obiekty lokalne, powoywane do ycia np. w bloku funkcji, istniej tylko do momentu wyjcia z bloku; obiekty dynamiczne istniej od momentu powoania ich do ycia za pomoc specjalnego operatora (new) do chwili ich zniszczenia za pomoc specjalnego operatora (delete). Identyfikatory obiektw w programie musz by unikatowe. Nie znaczy to jednak, e dana nazwa moe by uyta w programie tylko jeden raz: mona j uy ponownie, pod warunkiem istnienia pewnego kontekstu, ktry pozwala rozrni poszczeglne wystpienia. Jednym z takich kontekstw jest sygnatura funkcji, tj. wykaz jej argumentw (oraz ich typw), oddzielonych przecinkami. Pozwala to rozrnia funkcje przecione, tj. dwie funkcje o takiej samej nazwie, ale o rnych sygnaturach. Drugim, bardziej oglnym kontekstem jest zasig (ang. scope). Jzyk C++ wspiera trzy rodzaje zasigu: zasig pliku, zasig lokalny i zasig klasy. Kada zmienna ma skojarzony z ni zasig, ktry wraz z jej nazw jednoznacznie j identyfikuje. Zmienna jest widzialna dla pozostaej czci programu jedynie w obrbie swojego zasigu. Identyfikatory o zasigu pliku, nazywane take globalnymi, sa deklarowane na zewntrz

wszystkich blokw i klas i automatycznie inicjowane wartoci zero. Ich zasig rozciga si od punktu deklaracji do koca pliku rdowego. Tak wic identyfikator zadeklarowany np. przed funkcj main moe by wykorzystany w jej bloku, za deklaracja po bloku funkcji main czyni go niewidocznym w tym bloku. Mona go jednak uczyni widocznym przez umieszczenie w bloku funkcji dodatkowej deklaracji, poprzedzonej sowem kluczowym extern, np. int i; // definicja zmiennej globalnej int main() { double d; // definicja zmiennej lokalnej ... extern int z; // deklaracja referencyjna z = 17; ... } int z; // definicja zmiennej globalnej Jeeli definicja identyfikatora globalnego, np. int ii; jest zawarta w jednym pliku, a chcemy go uywa rwnie w innych plikach, to w plikach tych umieszczamy jego deklaracj, rwnie poprzedzon sowem kluczowym extern: extern int ii; Deklaracja poprzedzona sowem kluczowym extern nie jest definicj nie powoduje alokacji pamici. Jest to jedynie informacja dla kompilatora, e gdzie w programie istnieje definicja int ii. Tak wic identyfikator globalny moe by widoczny dla caego programu, na ktry skada si wicej ni jeden plik. Z powyszego wynika, e obiekty globalne istniej take przez cay czas wykonania programu, skadajcego si z wielu plikw. Jeeli identyfikator globalny poprzedzimy sowem kluczowym static, wtedy staje si on niewidzialny na zewntrz pliku, w ktrym zosta zdefiniowany; np. static int z; pozwala uywa zmiennej z w innym pliku tego samego programu w innym znaczeniu bez obawy wystpienia kolizji nazw. Identyfikatorom statycznym przydzielana jest pami od momentu rozpoczcia wykonania programu do chwili jego zakoczenia. S one inicjowane wartoci zero (lub NULL dla wskanikw) przy braku jawnego inicjatora. Z punktu widzenia czasu ycia wszystkie zmienne (obiekty) o zasigu pliku mona uwaa za statyczne. Podobnie wszystkie funkcje zachowuj si jak obiekty statyczne. Identyfikatory o zasigu lokalnym (np. o zasigu bloku lub funkcji) staj si widzialne od punktu ich deklaracji, a przestaj by widzialne wraz z kocem bloku lub funkcji zawierajcego ich deklaracje (tj. po zamykajcym nawiasie klamrowym }) . Np. zmienna lokalna zdefiniowana w bloku funkcji jest dostpna jedynie w obrbie tej funkcji; dziki temu jej nazwa moe by ponownie uyta w innych fragmentach programu o innym zasigu bez obawy konfliktu. Poniewa bloki mog by zagniedone, zatem kady blok, ktry zawiera instrukcj deklaracji, utrzymuje swj wasny zasig, np. deklaracje { ... int ii = 1; ... { ... int ii = 2; ...} } definiuj dwie zmienne lokalne ii o rozdzielnych zasigach, a wic dwie rne zmienne. Obiekty lokalne powinny by jawnie inicjowane w przeciwnym razie ich zawarto jest nieokrelona. Obiekty takie, z punktu widzenia czasu ich trwania, moemy uczyni statycznymi. Jeeli np. w bloku funkcji zapiszemy deklaracj: static int x = 1; to tym samym zdefiniujemy statyczn zmienn lokaln x. Czas ycia takiej zmiennej bdzie taki sam jak dla zmiennych globalnych, ale zasig pozostanie lokalny.

2.4.1.

Operator zasigu

Kada deklaracja identyfikatora wprowadza go w pewien zasig. Oznacza to, e dana nazwa jest widoczna i mona j uywa jedynie w okrelonej czci programu. Zamy np., e w programie jednoplikowym chcemy uywa tej samej nazwy (identyfikatora) dla zmiennej globalnej i zmiennej lokalnej, ktra ukrywa zmienn globaln. Dla uzyskania dostpu do ukrytej zmiennej globalnej wykorzystuje si jednoargumentowy operator zasigu o symbolu ::. Poprzedzenie nazwy zmiennej symbolem :: informuje kompilator, e operacja bdzie wykonywana na zmiennej globalnej. Podany niej prosty przykad ilustruje wykorzystanie operatora zasigu. Przykad 2.8. // Operator zasiegu :: #include <iostream.h> int z; //zmienna globalna typu int inicjowana zerem void main() { char znak; int x,y ; // zmienne lokalne typu int double z; // zmienna lokalna typu double z = 1.25816; // przypisanie do zmiennej lokalnej ::z = 12; // przypisanie do zmiennej globalnej /* Teraz przypisanie do zmiennej lokalnej wartosci zmiennej globalnej z */ y = ::z; cout << "Podaj liczbe typu int: " ; cin >> x; cout << "Wartosc x wynosi: " << x << endl; cout << "Wartosc zmiennej lokalnej z wynosi: " << z << endl; cout << "Wartosc zmiennej globalnej z wynosi: " << y << endl; cout << "Podaj dowolny znak, po czym ENTER: " ; cin >> znak; } Dyskusja. W podanym przykadzie deklaracja definiujca int z; czyni zmienn z widzialn dla caego programu. Inaczej mwic, zasig zmiennej z typu int obejmuje cay plik z programem rdowym. Tak okrelona zmienna z moe by uywana w bloku funkcji main a do deklaracji double z;, ktra przesonia (ukrya) zmienn globaln z typu int. Mimo to, dostp do zmiennej globalnej nie zosta bezpowrotnie stracony, a to dziki operatorowi zasigu o symbolu ::. Identyfikator poprzedzony operatorem zasigu zapewnia dostp do zmiennej globalnej. Po uruchomieniu programu i wprowadzeniu z klawiatury wartoci x, np. 16, a nastpnie znaku, np. 'a', wygld ekranu monitora bdzie taki: Podaj liczbe typu int: 16 Wartosc x wynosi: 16 Wartosc zmiennej lokalnej z = 1.25816 Wartosc zmiennej globalnej z = 12 Podaj dowolny znak, po czym ENTER: a Zwrmy uwag na nastpujce nowe elementy w programie:

Funkcja main jest typu void, a wic nie zwraca adnej wartoci do otoczenia (systemu operacyjnego). Wpisane jako typ argumentu sowo kluczowe void oznacza, e do funkcji main nie jest przekazywany z otoczenia aden argument. W jzyku C++ zapisy fun(); oraz fun(void); s rwnowane i oznaczaj, e lista argumentw funkcji jest pusta. W zwizku z tym druga z deklaracji zawiera rozwleko (redundancj) zapisu. Tym niemniej argument void umieszcza si niekiedy wewntrz operatora wywoania funkcji dla celw dokumentacyjnych. Charakterystyczny jest zapis instrukcji przypisania y = ::z; zmiennej globalnej z do zmiennej lokalnej y. W prezentowanym programie pojawi si operator pobierania (ang. extraction operator) '>>', ktry czyta wartoci ze standardowego strumienia wejciowego cin. Strumie cin, podobnie jak cout, jest zdefiniowany w pliku iostream.h. Analogicznie jak operator wstawiania ('<<'), operator pobierania wskazuje kierunek przepywu danych: z postaci wyraenia cin >> x; atwo si domyli, e chodzi tutaj o przesanie danych do obiektu x.

3. Instrukcje i wyraenia
W jzyku C++ kade dziaanie jest zwizane z pewnym wyraeniem. Termin wyraenie oznacza sekwencj operatorw i operandw (argumentw), ktra okrela operacje, tj. rodzaj i kolejno oblicze. Operandem nazywa si wielko, poddan operacji, ktra jest reprezentowana przez odpowiedni operator. Np. test na rwno jest reprezentowany przez operator ==. Operatory, ktre oddziaywuj tylko na jeden operand, nazywa si jednoargumentowymi (unarnymi). Przykadem moe by wyraenie *wsk, ktrego wynikiem jest warto, zapisana pod adresem wskazywanym przez zmienn wsk. Operatory dwuargumentowe nazywa si binarnymi; ich argumenty okrela si jako operand lewy i operand prawy. Niektre operatory reprezentuj zarwno operacje jednoargumentowe, jak i dwuargumentowe; np. operator *, ktry wystpi w wyraeniu *wsk, w innym wyraeniu, np. zmienna1* zmienna2 reprezentuje binarny operator mnoenia. Najprostszymi postaciami wyrae s wyraenia stae. W tym przypadku operand wystpuje bez operatora. Przykadami takich wyrae mog by: 3.14159 "abcd" Wynikiem wartociowania 3.14159 jest 3.14159 typu double; wynikiem wartociowania abcd jest adres pamici pierwszego elementu acucha (typu char*). Wyraeniem zoonym nazywa si takie wyraenie, w ktrym wystpuje dwa lub wicej operatorw. Wartociowanie wyraenia przebiega w porzdku, okrelonym pierwszestwem operatorw i w kierunku, okrelonym przez kierunek wizania operatorw. Instrukcja jest najmniejsz samodzieln, wykonywaln jednostk programow. Kada instrukcja prosta jzyka C++ koczy si rednikiem, ktry jest dla niej symbolem terminalnym. Najprostsz jest instrukcja pusta, bdca pustym cigiem znakw, zakoczonym rednikiem: ; //instrukcja pusta Instrukcja pusta jest uyteczna w przypadkach gdy skadnia wymaga obecnoci instrukcji, natomiast nie jest wymagane adne dziaanie. Nadmiarowe instrukcje puste nie s traktowane przez kompilator jako bdy syntaktyczne, np. zapis int i;; skada si z dwch instrukcji: instrukcji deklaracji int i; oraz instrukcji pustej. Instrukcj zoon nazywa si sekwencj instrukcji ujt w par nawiasw klamrowych: { instrukcja-1 instrukcja-2 ... instrukcja-n } W skadni jzyka taka sekwencja jest traktowana jako jedna instrukcja. Instrukcje zoone mog by zagniedane, np.

{ instrukcja-1 ... instrukcja-i { instrukcja-j ... instrukcja-n } } Jeeli pomidzy nawiasami klamrowymi wystpuj instrukcje deklaracji identyfikatorw (nazw), to tak instrukcj zoon nazywamy blokiem. Kada nazwa zadeklarowana w bloku ma zasig od punktu deklaracji do zamykajcego blok nawiasu klamrowego. Jeeli nadamy identyczn nazw identyfikatorowi, ktry by ju wczeniej zadeklarowany, to poprzednia deklaracja zostanie przesonita na czas wykonania danego bloku; po opuszczeniu bloku jej wano zostanie przywrcona.

3.1.

Instrukcja przypisania

Dowolne wyraenie zakoczone rednikiem jest nazywane instrukcj wyraeniow (ang. expression statement) lub krtko instrukcj, poniewa wikszo uywanych instrukcji stanowi instrukcje-wyraenia. Jedn z najprostszych jest instrukcja przypisania o postaci: zmienna = wyraenie; gdzie symbol = jest operatorem przypisania. Nastpujce napisy s instrukcjami jzyka C++: int liczba; liczba = 2 + 3; cout << liczba; Pierwsza z nich jest instrukcj deklaracji; definiuje ona obszar pamici zwizany ze zmienn liczba, w ktrym mog by przechowywane wartoci cakowite. Drug jest instrukcja przypisania. Umieszcza ona wynik dodawania (warto wyraenia) dwch liczb w obszarze pamici przeznaczonym na zmienn liczba. Trzeci jest poznana ju instrukcja wyprowadzania zawartoci obszaru pamici, zwizanego ze zmienn liczba, na terminal uytkownika. Zauwamy, e jeeli w instrukcji nie wystpuje operator przypisania, to jej wykonanie moe, ale nie musi, zmieni wartoci adnej zmiennej. Wemy dla przykadu nastpujcy cig instrukcji: int int i + i = i = 10; j = 20; j; i + j;

W instrukcjach deklaracji int i = 10; , int j = 20; znaki = nie s operatorami przypisania, lecz operatorami inicjowania zmiennych i oraz j wartociami pocztkowymi 10 oraz 20. Zakoczone rednikiem wyraenie i+j jest instrukcj, ktrej wykonanie nie zmieni wartoci adnej ze zmiennych. Natomiast w instrukcji i = i + j; znak = jest operatorem przypisania wartoci wyraenia i + j do zmiennej i, a wic po wykonaniu tej instrukcji warto i wyniesie 30. Przy okazji zwrmy uwag na tzw. efekty uboczne wyrae. Termin ten odnosi si do wyrae, ktrych wartociowanie zmienia zawarto komrki pamici (lub pliku). Np. wyraenie i + j nie daje efektw ubocznych, poniewa nie umieszcza w pamici wyniku dodawania. Natomiast wyraenie i = i + j daje efekt uboczny, poniewa zmienia zawarto pamici, przypisujc

now warto do zmiennej i, za wynik dodawania i + j (tj. 30), ju niepotrzebny, zostanie odrzucony. Z powyszego wynika, e samo przypisanie jest wyraeniem, ktrego warto jest rwna wartoci przypisywanej argumentowi z lewej strony operatora przypisania. Instrukcja przypisania powstaje przez dodanie rednika po zapisie przypisania. Skoro tak, to jest oczywiste, e przypisania mona wykorzystywa w wyraeniach, co pozwala pisa zwize, klarowne i atwe w czytaniu programy. Ilustracj jest poniszy przykad. Przykad 3.1. a a a a a a = = = = = = b; b + c; (b + c)/d; e > f; (e > f && c < d) + 1; a << 3;

Jednak skorzystanie z wymienionej cechy przypisania moe rwnie da efekt odwrotny dla czytelnoci programu; np. instrukcja a = (b = c + d)/(e = f + g); jest raczej mao czytelna, podczas gdy rwnowana sekwencja instrukcji przypisania b = c + d; e = f + g; a = b/e; jest bardziej elegancka i atwo czytelna. W instrukcji przypisania zmienna = wyraenie; zmienna jest nazywana l-wartoci lub modyfikowaln l-wartoci. Nazwa ta wywodzi si std, e zmienna, umieszczona po lewej stronie operatora przypisania, jest wyraeniem (tutaj po prostu identyfikatorem) reprezentujcym w sposb symboliczny pewien obszar (adres) pamici. Umieszczone w tym obszarze dane mog by zmieniane przypisaniem wartoci wyraenie; warto t nazywa si czasem r-wartoci. Dodatkow ilustracj wprowadzonej terminologii moe by przypisanie: a = a + b Tutaj a oraz b po prawej stronie s r-wartociami, odczytywanymi pod adresami symbolicznymi a i b; natomiast a po lewej stronie jest adresem, pod ktrym zostaje zapisany wynik dodawania poprzedniej zawartoci a i zawartoci b. F Uwaga. l-warto jest modyfikowalna, jeeli nie jest ona nazw funkcji, nazw tablicy, bd const.

3.2.

Operatory

Jzyk C++ oferuje ogromne bogactwo operatorw, zarwno dla argumentw typw podstawowych,

jak i typw pochodnych. Jest to jedna z przyczyn, dla ktrej jzyk ten szybko si rozpowszechnia i staje si de facto standardem przemysowym.

3.2.1. Operatory arytmetyczne


Operatory arytmetyczne su do tworzenia wyrae arytmetycznych. W jzyku C++ przyjto jako norm stosowanie tzw. arytmetyki mieszanej, w ktrej warto argumentu operatora jest automatycznie przeksztacana przez kompilator do typu, podanego w deklaracji tego argumentu. W zwizku z tym nie przewidziano oddzielnych operatorw dla typw cakowitych i typw zmiennopozycyjnych, za wyjtkiem operatora %, stosowalnego jedynie dla typw short int, int i long int. W tablicy 3.1 zestawiono dwuargumentowe operatory arytmetyczne jzyka C++. Tablica 3.1 Zestawienie operatorw arytmetycznych Symbol operatora + * / % Funkcja dodawanie odejmowanie mnoenie dzielenie operator reszty z dzielenia Zastosowanie wyraenie + wyraenie wyraenie - wyraenie wyraenie * wyraenie wyraenie / wyraenie wyraenie % wyraenie

Wszystkie operatory za wyjtkiem operatora % mona stosowa zarwno do argumentw cakowitych, jak i zmiennopozycyjnych. Operatory + i - mona rwnie stosowa jako operatory jednoargumentowe. Jeeli przy dzieleniu liczb cakowitych iloraz zawiera cz uamkow, to jest ona odrzucana; np. wynik dzielenia 18/6 i 18/5 jest w obu przypadkach rwny 3. Operator reszty z dzielenia (%) mona stosowa tylko do argumentw cakowitych; np. 18 % 6 daje wynik 0, a 18 % 5 daje wynik 3. W pewnych przypadkach wartociowanie wyraenia arytmetycznego daje wynik niepoprawny lub nieokrelony. S to tzw. wyjtki. Mog one by spowodowane niedopuszczalnymi operacjami matematycznymi (np. dzieleniem przez zero), lub te mog wynika z ogranicze sprztowych (np. nadmiar przy prbie reprezentacji zbyt duej liczby). W takich sytuacjach stosuje si wasne, lub predefiniowane metody obsugi wyjtkw.

3.2.2. Operatory relacji


Wszystkie operatory relacji s dwuargumentowe. Jeeli relacja jest prawdziwa, to jej wartoci jest 1; w przypadku przeciwnym wartoci relacji jest 0. Warto zwrci uwag na zapis operatora rwnoci ==, ktry pocztkujcy programici czsto myl z operatorem przypisania =. Tablica 3.2 Zestawienie operatorw relacji Symbol operatora < <= > mniejszy mniejszy lub rwny wikszy Funkcja Zastosowanie wyraenie < wyraenie wyraenie <= wyraenie wyraenie > wyraenie

>= == !=

wikszy lub rwny rwny nierwny

wyraenie >= wyraenie wyraenie == wyraenie wyraenie != wyraenie

F Uwaga. Argumenty operatorw relacji musz by typu arytmetycznego lub wskanikowego.

3.2.3. Operatory logiczne


Wyraenia poczone dwuargumentowymi operatorami logicznymi koniunkcji i alternatywy s zawsze wartociowane od strony lewej do prawej. Dla operatora && otrzymujemy warto 1 (prawda) wtedy i tylko wtedy, gdy wartociowanie obydwu operandw daje 1. Dla operatora || otrzymujemy warto 1, gdy co najmniej jeden z operandw ma warto 1. Tablica 3.3 Symbol operatora ! && || negacja koniunkcja alternatywa Zestawienie operatorw logicznych Funkcja !wyraenie wyraenie && wyraenie wyraenie || wyraenie Skadnia

3.2.4. Bitowe operatory logiczne


Jzyk C++ oferuje sze tzw. bitowych operatorw logicznych, ktre interpretuj operand(-y) jako uporzdkowany cig bitw. Kady bit moe przyjmowa warto 1 lub 0. Tablica 3.4 Zestawienie bitowych operatorw logicznych Symbol operatora & | ^ << >> ~ Funkcja bitowa koniunkcja bitowa alternatywa bitowa rnica symetryczna przesunicie w lewo przesunicie w prawo bitowa negacja Skadnia wyraenie & wyraenie wyraenie | wyraenie wyraenie ^ wyraenie wyraenie << wyraenie wyraenie >> wyraenie ~wyraenie

Argumenty (synonim operandw) tych operatorw musz by cakowite, a wic typu char, short int, int i long int, zarwno bez znaku, jak i ze znakiem. Ze wzgldu na rnice pomidzy reprezentacjami liczb ze znakiem w rnych implementacjach, zaleca si uywanie operandw bez znaku. Bitowy operator koniunkcji & stosuje si czsto do zasaniania (maskowania) pewnego zbioru bitw; np. instrukcja n = n & 0177; //Liczba oktalna 0177==127 dec

zeruje wszystkie oprcz 7 najniszych bitw zmiennej n. Bitowy operator alternatywy | stosuje si do ustawiania bitw; np. instrukcja n = n | MASK; ustawia jedynki na tych bitach zmiennej n, ktre w MASK s rwne 1. Bitowy operator rnicy symetrycznej (ang. exclusive OR) ustawia jedynk na kadej pozycji, gdzie jego operandy si rni (np. 0 i 1 lub 1 i 0) i zero tam, gdzie s takie same. Np. 0177 ^ 0176 daje po obliczeniu warto 1. Operatory przesunicia w lewo << i w prawo >> przesuwaj reprezentujc dan liczb sekwencj bitw odpowiednio w lewo lub w prawo, wypeniajc zwolnione bity zerami. Np. dla i = 6; instrukcja i = i << 2; zmieni warto i na 24. Jednoargumentowy operator bitowej negacji ~ daje uzupenienie jedynkowe swojego cakowitego argumentu. Np. instrukcja n = n & 077; ustawia ostatnie sze bitw zmiennej n na zero.

3.2.5. Operatory przypisania


Przypadkiem szczeglnym instrukcji przypisania jest instrukcja: a = a op b; gdzie op moe by jednym z dziesiciu operatorw: +, -, *, /, %, <<, >>, &, |, ^. Dla bardziej zwizego zapisu wprowadzono w jzyku C++ zoenia znaku przypisania = z symbolem odpowiedniego operatora, co pozwala zapisa powysz instrukcj w postaci: a op= b; Wemy dla przykadu instrukcj przypisania a = a << 3;, ktrej wykonanie przesuwa warto zmiennej a o trzy pozycje w lewo, a nastpnie przypisuje wynik do a. Instrukcj t mona przepisa w postaci: a <<= 3; Operatory powstae ze zoenia operatora przypisania = z kadym z wymienionych 10 operatorw zestawiono w Tablicy 3.5. Tablica 3.5 Symbol operatora += -= *= /= %= Zestawienie wieloznakowych operatorw przypisania Zapis skrcony a += b a -= b a *= b a /= b a %= b Zapis rozwinity a = a + b; a = a - b; a = a * b; a = a / b; a = a % b;

<<= >>= &= |= ^=

a <<= b a >>= b a &= b a |= b a ^= b

a = a << b; a = a >> b; a = a & b; a = a | b; a = a ^ b;

F Uwaga.Wszystkie operatory wymagaj l-wartoci jako ich lewego argumentu, za typ wyraenia przypisania jest typem jego lewego argumentu. Wynikiem przypisania jest warto, zapisana w lewym argumencie; jest to l-warto.

3.2.6. Operator sizeof


Rozmiary dowolnego obiektu (staej, zmiennej, etc.) jzyka C++ wyraa si wielokrotnoci rozmiaru typu char; zatem, z definicji sizeof(char) == 1 Operator sizeof jest jednoargumentowy. Skadnia jzyka przewiduje dwie postacie wyrae z operatorem sizeof: sizeof(nazwa-typu) sizeof wyraenie Dla podstawowych typw danych obowizuj nastpujce relacje: 1 == sizeof(char) <= sizeof(short int) <= sizeof(int) <= sizeof(long int) sizeof(float) <= sizeof(double) <= sizeof(long double) sizeof(I) == sizeof(signed I) == sizeof(unsigned I) gdzie I moe by char, short int, int, lub long int. Ponadto dla dowolnej platformy sprztowej mona by pewnym, e typ char ma co najmniej 8 bitw, short int co najmniej 16 bitw, a long int co najmniej 32 bity. Wiemy ju, e kadej zmiennej typu char mona przypisa jeden znak. Jeeli zatem znak jest zapisywany na 8 bitach w maszynowym zbiorze znakw (np. peny zbir ASCII), to operator sizeof zastosowany do dowolnego argumentu bdzie zwraca liczb bajtw zajmowanych przez jego argument. F Uwaga.Operatora sizeof nie mona stosowa do funkcji (ale mona do wskanika do funkcji), pola bitowego, niezdefiniowanej klasy, typu void oraz tablicy bez podanych wymiarw.

3.2.7. Operator warunkowy ?:


Jest to jedyny operator trjargumentowy w jzyku C++. Wyraenie warunkowe, utworzone przez zastosowanie operatora "?:" ma posta: wyraenie1 ? wyraenie2 : wyraenie3 Warto tak utworzonego wyraenia jest obliczana nastpujco. Najpierw wartociowane jest wyraenie1. Jeeli jest to warto niezerowa (prawda), to wartociowane jest wyraenie2 i wynikiem oblicze jest jego warto. Przy zerowej wartoci (fasz) wyraenia wyraenie1

wynikiem oblicze bdzie warto wyraenia wyraenie3. Przykad 3.2. #include <iostream.h> int main() { int a,b,z; cin >> a >> b; z = (a > b) ? a : b; // z==max(a,b) cout << z; return 0; }

3.2.8. Operatory zwikszania/zmniejszania


W jzyku C++ istniej operatory, suce do zwizego zapisu zwikszania o 1 (++) i zmniejszania o 1 (--) wartoci zmiennej. Zamiast zapisu n=n+1 (lub n+=1) piszemy krtko ++n, bd n++ --n, bd n-n=n-1 (lub n-=1)

przy czym nie jest obojtne, czy dwuznakowy operator ++ lub -- zapiszemy przed, bd za nazw zmiennej. Notacja przedrostkowa (++n) oznacza, e wyraenie ++n zwiksza n zanim warto n zostanie uyta, natomiast n++ zwiksza n po uyciu dotychczasowej wartoci n. Tak wic wyraenia ++n oraz n++ (i odpowiednio --n oraz n--) s rne. Ilustruje to poniszy przykad. Przykad 3.3. #include <iostream.h> int main() { int i,j = 5; i = j++ ; // przypisz 5 do i, po czym przypisz 6 do j cout << "i=" << i << ", j=" << j << endl; i = ++j; // przypisz 7 do j, po czym przypisz 7 do i cout << "Teraz i=" << i << ", j=" << j << endl; // j++ = i; zle! j++ nie jest l-wartoscia return 0; } F Uwaga. Argumentami operatorw ++ oraz -- musz by modyfikowalne l-wartoci typu arytmetycznego lub wskanikowego. Np. zapis: n = (n + m)++ jest bdny, poniewa (n + m) nie jest l-wartoci. Typ wyniku jest taki sam, jak typ argumentu. Dla obu postaci: przedrostkowej (np. ++n) i przyrostkowej (np. n++) wynik nie jest l-wartoci.

3.2.9. Operator przecinkowy


Operator przecinkowy ',' pozwala utworzy wyraenie, skadajce si z cigu wyrae skadowych, rozdzielonych przecinkami. Wartoci takiego wyraenia jest warto ostatniego z prawej elementu cigu, za wartociowanie przebiega od elementu skrajnego lewego do skrajnego prawego. Przykadem wyraenia z operatorem przecinkowym moe by: num++, num + 10

gdzie num jest typu int. Wartociowanie powyszego wyraenia z operatorem przecinkowym przebiega w nastpujcy sposb: Najpierw jest wartociowane wyraenie num++, w wyniku czego zostaje zmieniona zawarto komrki pamici o nazwie num (efekt uboczny). Nastpnie jest wartociowane wyraenie num + 10 i ta warto jest wartoci kocow. Wemy inny przykad: double x, y, z; z = (x = 2.5, y = 3.5, y++); Wynikiem wartociowania wyraenia z dwoma operatorami przecinkowymi bd wartoci: x==2.5, y==4.5 oraz z==3.5 (warto z nie bdzie rwna 4.5, poniewa do y przyoono przyrostkowy operator '++').

3.2.10.

Rzutowanie (konwersja) typw

W jzyku C++ raczej norm ni wyjtkiem jest arytmetyka mieszana, tj. sytuacja, gdy w instrukcjach i wyraeniach argumenty operatorw s rnych typw. Moemy np. dodawa wartoci wyrae typu int do wartoci typu long int, wartoci typu float do wartoci typu double, etc. W takich przypadkach, jeszcze przed wykonaniem danej operacji, argumenty kadego operatora musz zosta przeksztacone do jednego, tego samego typu, dopuszczalnego dla danego operatora. Operacj tak nazywa si rzutowaniem lub konwersj (ang. cast). Konwersja typw moe by niejawna, wykonywana automatycznie przez kompilator bez udziau programisty. Jzyk pozwala programicie dokonywa take konwersji jawnych, oferujc mu odpowiednie operatory konwersji. Zarwno konwersje jawne, jak i niejawne, musz by bezpieczne, tzn. takie, aby w wyniku konwersji nie bya tracona adna informacja. Jest pewnym, e jeli liczba bitw w maszynowej reprezentacji wielkoci podlegajcej konwersji nie ulega zmianie, bd wzrasta po konwersji, to taka konwersja jest bezpieczna. Bezpieczna konwersja argumentu wszego typu do szerszego jest nazywana promocj typu. Typowym przykadem promocji jest automatyczna konwersja typu char do typu int. Powd jest oczywisty: wszystkie predefiniowane operacje na literaach i zmiennych typu char s faktycznie wykonywane na liczbach porzdkowych znakw, pobieranych ze zbioru kodw maszynowych (np. ASCII). Powysze dotyczy rwnie typu short int oraz enum. Podane niej zestawienie typw od najwszego do najszerszego okrela, ktry argument operatora binarnego bdzie przeksztacany. int unsigned int long int unsigned long int float double long double Typ, ktry wystpuje jako pierwszy w wykazie, podlega promocji do typu, wystpujcego jako nastpny. Np. jeli argumenty operatora s int i long int, to argument int zostanie przeksztacony do typu long int; jeeli typy argumentw s long int i long double, to operand typu long int bdzie przeksztacony do typu long double, etc. Wemy dla przykadu nastpujc sekwencj instrukcji:

int n = 3; long double z; z = n + 3.14159; W ostatniej instrukcji najpierw zmienna n zostanie przeksztacona do typu double; jej warto stanie si rwna 3.0. Warto ta zostanie dodana do 3.14159, dajc wynik 6.14159 typu double. Otrzymany wynik zostanie nastpnie przeksztacony do typu long double. Przykadem konwersji zwajcej moe by wykonanie nastpujcej sekwencji instrukcji: int n = 10; n *= 3.1; W drugiej instrukcji mamy dwie konwersje. Najpierw n jest przeksztacane do typu double i wartoci 10.0. Po wymnoeniu przez 3.1 otrzymuje si wynik 31.0, ktry zostanie nastpnie zawony do typu int. Ta zawona warto (31) zostanie przypisana do zmiennej n. Konwersja jawna moe by wymuszona przez programist za pomoc jednoargumentowego operatora konwersji '()' o skadni (typ) wyraenie lub typ (wyraenie) W obu przypadkach wyraenie zostaje przeksztacone do typu typ zgodnie z reguami konwersji. Przykadowo, obie ponisze instrukcje (double) 25; double (25); dadz warto 25.0 typu double. Konwersja jawna bywa stosowana dla uniknicia zbdnych konwersji niejawnych. Np. wykonanie instrukcji (n jest typu int) n = n + 3.14159; wymaga konwersji n do double, a nastpnie zawenia sumy do int. Modyfikujc t instrukcj do postaci n = n + int(3.14159); mamy tylko jedn konwersj z typu double do int. Wynik konwersji nie jest l-wartoci (wyjtek typ referencyjny, ktry zostanie omwiony pniej), a wic mona go przypisywa, np float f = float(10); F Uwaga. Gdy konwersja jawna nie jest niezbdnie potrzebna, naley jej unika, poniewa programy, w ktrych uywa si jawnych konwersji, s trudniejsze do zrozumienia. Przykad 3.4.

/* rzutowanie(konwersja typow) #include <iostream.h> // Deklaracje zmiennych globalnych char a; int b;

*/

int main() { cout << "int(a)== " << int(a) << '\n'; cout << "b== " << b << '\n'; a = 'A'; cout << "a po przypisaniu== " << a << endl; b = int (a); cout << " (b = int (a))== " << b << endl; a = (char) b; cout << " (a = (char) b)== " << a << endl; return 0; } Dyskusja. Zadeklarowanym zmiennym globalnym a i b kompilator nada automatycznie zerowe wartoci pocztkowe (a=='\0', b==0). Wydruk z programu ma posta: a== 0 b== 0 a po przypisaniu== A (b = int (a))== 65 (a = (char) b)== A

3.2.11.

Hierarchia i czno operatorw.

Dla poprawnego posugiwania si operatorami w wyraeniach istotna jest znajomo ich priorytetw i kierunku wizania (cznoci). Przy wartociowaniu wyrae obowizuje zasada wykonywania jako pierwszej takiej operacji, ktrej operator ma wyszy priorytet i w tym kierunku, w ktrym operator wie swj argument (argumenty). Programista moe zmieni kolejno wartociowania, zamykajc cz wyraenia (podwyraenie) w nawiasy okrge. Wwczas jako pierwsze bd wartociowane te podwyraenia, ktre s zawarte w nawiasach najgbiej zagniedonych (zanurzonych). Poniej zestawiono operatory jzyka C++ wedug ich priorytetw; bardziej szczegowy wykaz zamieszczono w Dodatku B. Hierarchi operatorw naley rozumie w ten sposb, e wysze pozycje w podanej niej tablicy oznaczaj wyszy priorytet: wyraenia, w ktrych argumenty s powizane operatorami o wyszym priorytecie, s wykonywane jako pierwsze. Tablica 3.6 Hierarchia operatorw Operatory :: zasig globalny (unarny) :: zasig klasy (binarny) -> . () przyrostkowy++ przyrostkowy-przedrostkowy ++ przedrostkowy -- ~ ! unarny + Kierunek wizania od prawej do lewej od lewej do prawej od lewej do prawej od prawej do lewej

unarny - unarny & (typ) sizeof new delete ->* .* * / % + << >> < <= > >= == != & ^ | && || ?: = *= /= %= += -= <<= >>= &= ^= |= , od lewej do prawej od lewej do prawej od lewej do prawej od lewej do prawej od lewej do prawej od lewej do prawej od lewej do prawej od lewej do prawej od lewej do prawej od lewej do prawej od lewej do prawej od lewej do prawej od prawej do lewej od lewej do prawej

3.3.

Instrukcje selekcji

Instrukcje selekcji (wyboru) wykorzystuje si przy podejmowaniu decyzji. Konieczno ich stosowania wynika std, e kodowane w jzyku programowania algorytmy tylko w bardzo prostych przypadkach s czysto sekwencyjne. Najczciej kolejne kroki algorytmu s zalene od spenienia pewnego warunku lub kilku warunkw. Rysunek 3-1 ilustruje dwie takie typowe sytuacje.
a) b)

test

czynno 1 czynno 1

test czynno 2

czynno 2 czynno 3

Rys. 3-1 Sie dziaa warunkowych W sytuacji z rys. 3-1a) spenienie testowanego warunku oznacza wykonanie sekwencji dziaa: czynno 1 - czynno 2; przy warunku niespenionym wykonywana jest czynno 2 (czynno 1 jest pomijana). W sytuacji z rys. 3-1b) pozytywny wynik testu oznacza wykonanie sekwencji dziaa: czynno 1 - czynno 3, a wynik negatywny: czynno 2 - czynno 3.

3.3.1. Instrukcja if
Instrukcja if jest implementacj schematw podejmowania decyzji, pokazanych na rysunku 6-1.

Formalny zapis jest nastpujcy: if(wyraenie) instrukcja lub if(wyraenie) instrukcja1 else instrukcja2 gdzie wyraenie musi wystpi w nawiasach okrgych, za adna z instrukcji nie moe by instrukcj deklaracji. F Uwaga. Pamitajmy, e instrukcje proste jzyka C++ kocz si rednikami! Wykonanie instrukcji if zaczyna si od obliczenia wartoci wyraenia. Jeeli wyraenie ma warto rn od zera (prawda), to bdzie wykonana instrukcja (lub instrukcja1); jeeli wyraenie ma warto zero (fasz), to w pierwszym przypadku instrukcja jest pomijana, a w drugim przypadku wykonywana jest instrukcja2. Kada z wystpujcych tutaj instrukcji moe by instrukcj prost lub zoon, bd instrukcj pust. Poniewa if sprawdza jedynie warto numeryczn wyraenia, dopuszcza si pewne skrtowe zapisy. Np. zamiast pisa if(wyraenie != 0) pisze si czsto if(wyraenie) Przykad 3.5. //Instrukcje if #include <iostream.h> int main() { int a, b, c = 0; cout << "Wprowadz dwie liczby calkowite: " << endl; cin >> a >> b; if(a*b > 0) if (a > b) c = a; else c = b; cout << "Pierwszy test daje c= " << c << endl; if (a*b > 0) { if (a > b) c =a; } else c = b; cout << "Drugi test daje c= " << c << endl; return 0; } Dyskusja. Mamy tutaj przykady zagniedonych instrukcji if. Jeeli wprowadzimy z klawiatury dwie liczby o rnych znakach, np. a == 5, b == -2, to po sprawdzeniu, e a*b < 0 wbudowana instrukcja if razem ze swoj opcj else zostanie pominita i cout wydrukuje Pierwszy test daje c== 0, tj. inicjaln warto c. W drugiej konstrukcji opcja else dotyczy zewntrznej instrukcji if, zatem cout wydrukuje: Drugi test daje c== -2 Niektre proste konstrukcje if mona z powodzeniem zastpi wyraeniem warunkowym, wykorzystujcym operator warunkowy "?:" . Np.

if (a > b) max = a; else max = b; mona zastpi przez max = (a > b) ? a : b; Nawiasy okrge w ostatnim wierszu nie s konieczne; dodano je dla lepszej czytelnoci.

3.3.2. Instrukcja switch


Istrukcja switch suy do podejmowania decyzji wielowariantowych, gdy zastosowanie instrukcji if-else prowadzioby do zbyt gbokich zagniede i ewentualnych niejednoznacznoci. Skadnia instrukcji jest nastpujca: switch (wyraenie) instrukcja; gdzie instrukcja jest zwykle instrukcj zoon (blokiem), ktrej instrukcje skadowe s poprzedzane sowem kluczowym case z etykiet; wyraenie, ktre musi przyjmowa wartoci cakowite, spenia tutaj rol selektora wyboru. W rozwinitym zapisie switch (wyraenie) { case etykieta-1 : instrukcje ... case etykieta-n : instrukcje default : instrukcje } Etykiety s cakowitymi wartociami staymi lub wyraeniami staymi. Nie moe by dwch identycznych etykiet. Jeeli jedna z etykiet ma warto rwn wartoci wyraenia wyraenie, to wykonanie zaczyna si od tego przypadku (ang. case przypadek) i przebiega a do koca bloku. Instrukcje po etykiecie default s wykonywane wtedy, gdy adna z poprzednich etykiet nie ma aktualnej wartoci selektora wyboru. Przypadek z etykiet default jest opcj: jeeli nie wystpuje i adna z pozostaych etykiet nie przyjmuje wartoci selektora, to nie jest podejmowane adne dziaanie i sterowanie przechodzi do nastpnej po switch instrukcji programu. F Uwaga 1. Etykieta default i pozostae etykiety mog wystpowa w dowolnym porzdku. F Uwaga 2. Kada z instrukcji skadowych moe by poprzedzona wicej ni jedn sekwencj case etykieta:, np. case et1: case et2: case et3: cout << "Trzy etykiety\n"; Z powyszego opisu wynika, e jak na razie przedstawiona wersja instrukcji switch jest co najwyej dwualternatywna i mwi: wykonuj wszystkie instrukcje od danej etykiety do koca bloku, albo: wykonuj instrukcje po default do koca bloku (bd adnej, jeli default nie wystpuje). Wersja ta nie wychodzi zatem poza moliwoci instrukcji if, bd if-else. Dla uzyskania wersji z wieloma wzajemnie wykluczajcymi si alternatywami musimy odseparowa poszczeglne przypadki w taki sposb, aby po wykonaniu instrukcji dla wybranego przypadku sterowanie opucio blok instrukcji switch. Moliwo tak zapewnia instrukcja break. Zatem dla selekcji

wycznie jednego z wielu wariantw skadnia instrukcji switch bdzie miaa posta: switch(wyraenie) { case et-1 : instrukcje break; ... case et-n : instrukcje break; default : instrukcje break; } Przykad 3.6. #include <iostream.h> int main() { char droga; int czas; cout << "Wprowadz litere A, B, lub C : " ; cin >> droga; cout << endl; if((droga=='A')||(droga=='B')||(droga=='C')) switch (droga) { case 'A': case 'B': czas = 3; cout << czas << endl; break; case 'C': czas = 4; cout << czas << endl; break; default: droga = 'D'; czas = 5; cout << czas << endl; } else cout << "Zostan w domu\n"; return 0; }

3.4.

Instrukcje iteracyjne

Instrukcje powtarzania (ptli) lub iteracji pozwalaj wykonywa dan instrukcj, prost lub zoon, zero lub wicej razy. W jzyku C++ mamy do dyspozycji trzy instrukcje iteracyjne (ptli): while, do-while i for. Rni si one przede wszystkim metod sterowania ptl. Dla ptli while i for sprawdza si warunek wejcia do ptli (rys. 3-2a), natomiast dla ptli do-while sprawdza si warunek wyjcia z ptli (rys. 3-2b).

a)

b)

Czy spenione s warunki wejcia do ptli?

NIE NIE

Czy spenione s warunki wyjcia z ptli?

TAK

TAK tre ptli

Rys. 3-2

Struktury ptli: a) z testem na wejciu, b) z testem na wyjciu

Instrukcje iteracyjne, podobnie jak instrukcje selekcji, mona zagnieda do dowolnego poziomu zagniedenia. Jednak przy wielu poziomach zagniedenia program staje si mao czytelny. W praktyce nie zaleca si stosowa wicej ni trzech poziomw zagniedenia.

3.4.1. Instrukcja while


Skadnia instrukcji while jest nastpujca: while (wyraenie) instrukcja gdzie instrukcja moe by instrukcj pust, instrukcj prost, lub instrukcj zoon. Sekwencja dziaa przy wykonywaniu instrukcji while jest nastpujca: 1. Oblicz warto wyraenia i sprawd, czy jest rwne zeru (fasz). Jeeli tak, to pomi krok 2; jeeli nie (prawda), przejd do kroku 2. 2. Wykonaj instrukcj i przejd do kroku 1. Jeeli pierwsze wartociowanie wyraenia wykae, e ma ono warto zero, to instrukcja nigdy nie zostanie wykonana i sterowanie przejdzie do nastpnej instrukcji programu. Rysunek 3-3 ilustruje w sposb pogldowy dziaanie instrukcji while.
while(wyraenie) instrukcja

Czy wyraenie==0?

TAK

NIE Instrukcja

Rys. 3-3

Sie dziaa instrukcji while

Przykad 3.7. #include <iostream.h> #include <iomanip.h> int main() { const int WIERSZ = 5; const int KOLUMNA = 15; int j, i = 1; while(i <= WIERSZ) { cout << setw(KOLUMNA - i) << '*'; j = 1; while( j <= 2*i-2 ) { cout << '*'; j++; } cout << endl; i++; } return 0; } Wydruk z programu bdzie mia posta: * *** ***** ******* ********* Dyskusja. W programie mamy zagniedon ptl while. Pierwsze sprawdzenie wyraenia j <= 2*i-2 w ptli wewntrznej daje warto 0 (fasz), zatem w pierwszym obiegu ptla wewntrzna zostaje pominita. W drugim sprawdzeniu ptla wewntrzna dorysowuje dwie gwiazdki, itd., a do wyjcia z ptli zewntrznej. Nowym elementem programu jest wczenie pliku nagwkowego iomanip.h, w ktrym znajduje si deklaracja funkcji setw(int w). Funkcja ta suy do ustawiania szerokoci pola wydruku na podan liczb w znakw.

3.4.2. Instrukcja do-while


Skadnia instrukcji do-while ma posta: do instrukcja while (wyraenie) gdzie instrukcja moe by instrukcj pust, instrukcj prost lub zoon. Ptla do-while funkcjonuje wedug schematu, pokazanego na rysunku 3-4.

do { I1 I1 I2 ... In I1, I2, ...,In - instrukcje NIE Czy w==0? TAK w - wyraenie I2 ... In } while(w);

Rys. 3-4

Sie dziaa instrukcji do-while

W ptli do-while instrukcja (ciao ptli) zawsze bdzie wykonana co najmniej jeden raz, poniewa test na rwno zeru wyraenia jest przeprowadzany po jej wykonaniu. Ptla koczy si po stwierdzeniu, e wyraenie jest rwne zeru. Przykad 3.8. #include <iostream.h> int main() { char znak; cout << "Wprowadz dowolny znak;\ * oznacza koniec.\n"; do { cout << ": "; cin >> znak; } while (znak != '*'); return 0; }

3.4.3. Instrukcja for


Jest to, podobnie jak instrukcja while, instrukcja sterujca powtarzaniem ze sprawdzeniem warunku zatrzymania na pocztku ptli. Stosuje si j w tych przypadkach, gdy znana jest liczba obiegw ptli. Skadnia instrukcji for jest nastpujca: for(instrukcja-inicjujca wyraenie1;wyraenie2) instrukcja gdzie instrukcja moe by instrukcj pust, instrukcj prost lub zoon. Algorytm oblicze dla ptli for jest nastpujcy: 1. Wykonaj instrukcj o nazwie instrukcja-inicjujca. Zwykle bdzie to zainicjowanie jednego lub kilku licznikw ptli (zmiennych sterujcych), ewentualnie inicjujca instrukcja deklaracji, np. for (i = 0; ... for (i =0, j = 1; ...

for (int i = 0; ... Instrukcja inicjujca moe by rwnie instrukcj pust, jeeli zmienna sterujca zostaa ju wczeniej zadeklarowana i zainicjowana, np. int i = 1; for (; ... 2. Oblicz warto wyraenia wyraenie1 i porwnaj j z zerem. Jeeli wyraenie1 ma warto rn od zera (prawda) przejd do kroku 3. Jeeli wyraenie1 ma warto zero, opu ptl. 3. Wykonaj instrukcj instrukcja i przejd do kroku 4. 4. Oblicz warto wyraenia wyraenie2 (zwykle oznacza to zwikszenie licznika ptli) i przejd do kroku 2. Ilustracj opisanego algorytmu jest rysunek 3-5.

instrukcja-inicjujca

Czy w != 0? oblicz w2 TAK Instrukcja

NIE

Rys. 3-5

Sie dziaa instrukcji for

F Uwaga 1. Jeeli instrukcja inicjujca jest instrukcj deklaracji, to zasig wprowadzonych nazw rozciga si do koca bloku instrukcji for. F Uwaga 2. Wyraenie1 musi by typu arytmetycznego lub wskanikowego. Instrukcja for jest rwnowana nastpujcej instrukcji while: instrukcja-inicjujca while (wyraenie1) { instrukcja wyraenie2; } Wanym elementem skadni instrukcji for jest sposb zapisu instrukcji inicjujcej oraz wyrae skadowych, gdy mamy kilka zmiennych sterujcych. W takich przypadkach przecinek pomidzy wyraeniami peni rol operatora. Np. w instrukcji for ( ii = 1, jj = 2; ii < 5; ii++, jj++ ) cout << "ii = " << ii << " jj = " << jj << "\n"; instrukcja inicjujca zawiera dwa wyraenia: ii = 1 oraz jj = 2 poczone przecinkiem.

Operator przecinkowy ',' wie te dwa wyraenia w jedno wyraenie, wymuszajc wartociowanie wyrae od lewej do prawej. Tak wic najpierw ii zostaje zainicjowane do 1, a nastpnie jj zostanie zainicjowane do 2. Podobnie wyraenie2, ktre skada si z dwch wyrae ii++ oraz jj++, poczonych operatorem przecinkowym; po kadym wykonaniu instrukcji cout najpierw zwiksza si o 1 ii, a nastpnie jj. Przykad 3.9. //Program Piramida #include <iostream.h> #include <iomanip.h> int main() { const int WIERSZ = 5; const int KOLUMNA = 15; for (int i = 1; i <= WIERSZ; i++) { cout << setw(KOLUMNA - i) << '*'; for (int j = 1; j <= 2 * i -2; j++) cout << '*'; cout << endl; } return 0; } Dyskusja. W przykadzie, podobnie jak dla instrukcji while, wykorzystano funkcj setw() z pliku iomanip.h. Identyczne s rwnie definicje staych symbolicznych WIERSZ i KOLUMNA. Taka sama jest rwnie posta wydruku. Natomiast program jest nieco krtszy; wynika to std, e instrukcja for jest wygodniejsza od instrukcji while dla znanej z gry liczby obiegw ptli. Zauwamy te, e w ptli wewntrznej umieszczono definicj zmiennej j typu int. Zasig tej zmiennej jest ograniczony: mona si ni posugiwa tylko od punktu definicji do koca bloku zawierajcego wewntrzn instrukcj for. F Uwaga 1. Syntaktycznie poprawny jest zapis for (; ;). Jest to zdegenerowana posta instrukcji for, rwnowana for(;1;) lub while (1), czyli ptli nieskoczonej; np. instrukcja for(;;) cout << "wiersz\n"; bdzie drukowa podany tekst a do zatrzymania programu, lub wymuszenia instrukcj break zatrzymania ptli. F Uwaga 2. W ciele instrukcji iteracyjnych uywa si niekiedy instrukcji continue;. Wykonanie tej instrukcji przekazuje sterowanie do czci testujcej warto wyraenia w ptlach while i do-while (krok 1), lub do kroku 4 w instrukcji for. F Uwaga 3. W jzyku C++ istnieje instrukcja skoku bezwarunkowego goto o skadni goto etykieta:. Instrukcji tej nie omawiano, poniewa jej uywanie nie jest zalecane.

Typy pochodne
Uytkownik moe w swoim programie deklarowa i definiowa typy pochodne od typw podstawowych: wskaniki, referencje, tablice, struktury, unie oraz klasy, a take typy pochodne od tych struktur; moe rwnie definiowa nowe operacje, wykonywane na tych strukturach danych. Typ wskanikowy Wskaniki (ang. pointers) s tym mechanizmem, ktry uczyni jzyk C i jego nastpc jzyk C++ tak silnym narzdziem programistycznym. W jzyku C++ dla kadego typu X istnieje skojarzony z nim typ wskanikowy X*. Zbiorem wartoci typu X* s wskaniki do obiektw typu X. Do zbioru wartoci typu X* naley rwnie wskanik pusty, oznaczany jako 0 lub NULL. Wystpieniem typu wskanikowego jest zmienna wskanikowa. Deklaracja zmiennej wskanikowej ma nastpujc posta: nazwa-typu-wskazywanego* nazwa-zmiennej-wskanikowej; Zgodnie z powyszymi okreleniami, wartociami zmiennej wskanikowej mog by wskaniki do uprzednio zadeklarowanych obiektw (zmiennych, staych) typu wskazywanego. Wartoci zmiennej wskanikowej nie moe by staa. Jeeli damy, aby zmienna wskanikowa nie wskazywaa na aden obiekt programu, przypisujemy jej wskanik 0 (NULL), np. int* wski;//Typ wskazywany:int.Typ wskanikowy:int* wski = 0; W deklaracji zmiennej wskanikowej typem wskazywanym moe by dowolny typ wbudowany, typ pochodny od typu wbudowanego, lub typ zdefiniowany przez uytkownika. W szczeglnoci moe to by typ void, np. void* wsk; //Typ wskazywany: void. Typ wsk: void* uywany wtedy, gdy typ wskazywanego obiektu nie jest dokadnie znany w chwili deklaracji, lub moe si zmienia w fazie wykonania. Zmiennej wsk typu void* moemy przypisa wskanik do obiektu dowolnego typu. Podobnie jak zmienne typw wbudowanych, zmienne wskanikowe mog by deklarowane w zasigu globalnym lub lokalnym. Globalne zmienne wskanikowe s alokowane w pamici statycznej programu, bez wzgldu na to, czy s poprzedzone sowem kluczowym static, czy te nie. Jeeli globalna zmienna wskanikowa nie jest jawnie zainicjowana, to kompilator przydziela jej niejawnie warto 0 (NULL). Tak samo bdzie inicjowana statyczna zmienna lokalna, tj. zmienna wskanikowa zadeklarowana w bloku funkcji, przy czym jej deklaracja jest poprzedzona sowem kluczowym static. Wskaniki i adresy W oglnoci wskanik zawiera informacj o lokalizacji wskazywanej danej oraz informacj o typie tej danej. Typow zatem jest implementacja wskanikw jako adresw pamici; wartoci zmiennej wskanikowej moe by wtedy adres pocztkowy obiektu wskazywanego. Wemy pod uwag nastpujcy cig deklaracji:

int i = 1, int* wski; wski = &i; j = *wski;

j = 10; // deklaracja zmiennej wski typu int* // Teraz wski wskazuje na i. *wski==1 // Teraz j==1

Deklaracje te wprowadzaj zainicjowane zmienne i oraz j, a nastpnie zmienn wskanikow wski, ktrej nastpna instrukcja przypisuje wskanik do zmiennej i. Unarny operator adresacji & przyoony do istniejcego obiektu (np. &i) daje wskanik do tego obiektu. Wskanik ten w instrukcji przypisania wski= &i; zosta przypisany zmiennej wskanikowej wski. Unarny operator dostpu poredniego * (nazywany te operatorem wyuskania) transformuje wskanik w warto, na ktr on wskazuje. Inaczej mwic, wyraenie *wski oznacza zawarto zmiennej wskazywanej przez wski. Zmienn wskanikow wski mona te bezporednio zainicjowa wskanikiem do zmiennej i w jej deklaracji: int* wski = &i; FUwaga 1. Zarwno zmienne wskanikowe, jak i przyjmowane przez nie wartoci przyjto nazywa wskanikami; konwencj t bdziemy czsto stosowa w dalszych partiach tekstu. FUwaga 2. Ze wzgldu na wykorzystywane w tej pracy implementacje jzyka C++, bdziemy czsto utosamia wskanik do obiektu z adresem tego obiektu. Rysunek 4-1 ilustruje deklaracj i przypisanie wskanikowi adresu istniejcej zmiennej.
double* wsk; wsk double db = 127.53; db 127.53 wsk = &db; wsk *wsk 127.53

Rys. 4-1 Zmienna wskanikowa i zmienna wskazywana Wskanik moe by rwnie inicjowany innym wskanikiem tego samego typu, np. int ii = 38; int* wsk1 = &ii; int* wsk2 = wsk1; Natomiast deklaracja inicjujca o postaci: int* wsk3 = &wsk1; jest bdna, poniewa wsk3 jest wskanikiem do zmiennej typu int, podczas gdy wartoci &wsk1 jest adreswskanika do zmiennej typu int. Ostatni deklaracj mona jednak zmieni na poprawn, piszc:

int** wsk3 = &wsk1; poniewa wsk3 jest teraz wskanikiem do wskanika do zmiennej typu int. Jeeli int* wski wskazuje na zmienn i, to *wski moe wystpi w kadym kontekcie dopuszczalnym dla i. Np. *wski = *wski + 3; zwiksza *wski (czyli zawarto zmiennej i) o 3, a instrukcja przypisania: j = *wski + 5; pobierze zawarto spod adresu wskazywanego przez wski (tj. aktualn warto zmiennej i), doda do niej 5 i przypisze wynik do j (warto *wski pozostanie bez zmiany). Powysze instrukcje dziaaj poprawnie, poniewa *wski jest wyraeniem, a operatory * i & wi silniej ni operatory arytmetyczne. Podobnie instrukcja: j = ++*wski; zwikszy o 1 warto *wski i przypisze t zwikszon warto do j, za instrukcja: j = (*wski)++; przypisze biec warto *wski do j, po czym zwikszy o 1 warto *wski. W ostatnim zapisie nawiasy s konieczne, poniewa operatory jednoargumentowe, jak * i ++ wi od prawej do lewej. Bez nawiasw mielibymy zwikszenie o 1 wartoci wski zamiast zwikszenia wartoci *wski. Zauwamy take, e skoro wartoci wskanika jest adres obszaru pamici przeznaczonego na zmienn danego typu, to wskaniki rnych typw bd mie taki sam rozmiar. Jest to oczywiste, poniewa system adresacji komrek pamici dla okrelonej platformy sprztowej i okrelonego systemu operacyjnego jest zunifikowany i niezaleny od interpretacji (wymuszonej typem) cigu bitw zapisanego pod danym adresem. Dodajmy na zakoczenie kilka uwag dotyczcych wskanikw do typu void. Zmiennej typu void* mona przypisa wskanik dowolnego typu, ale nie odwrotnie. I tak np. poprawne s deklaracje: void* wskv; double db; double* wskd = &db; wskv = wskd; // konwersja niejawna do void* ale nie mona si odwoa do *wskv. Bdem byaby te prba przypisania wskanika dowolnego typu do wskv, np. wskd = wskv;. Sprbujmy obecnie krtko podsumowa nasze rozwaania. Zmienna wskanikowa (wskanik) jest szczeglnym rodzajem zmiennej, przyjmujcej wartoci ze

zbioru, ktrego elementami s adresy. Przypomnijmy, e zwyka zmienna jest obiektem o nastpujcych atrybutach: Typ zmiennej; Nazwa zmiennej. Zmienna moe mie zero, jedn, lub kilka nazw. Nazwy nie musz by prostymi identyfikatorami, jak np. a, b, znak; mog one by rwnie wyraeniami, oznaczajcymi zmienn, np. oznaczenie zmiennej indeksowanej tablicy tab moe mie posta tab[i + 5]. Wyraenie oznaczajce zmienn jest l-wartoci, poniewa tylko takie wyraenie moe si pojawi po lewej stronie operatora przypisania. Lokalizacja zmiennej, tj. adres w pamici, pod ktrym s przechowywane jej wartoci. Warto, zapisana pod adresem, okrelonym lokalizacj zmiennej, nazywana niekiedy rwartoci. Deklaracja wskanika niezainicjowanego przypisuje mu zadeklarowany typ, natomiast wskazywany przez niego adres i przechowywane pod tym adresem dane s nieokrelone. Jeeli wskanik zainicjujemy w jego deklaracji lub przypiszemy mu adres wczeniej zadeklarowanej zwykej zmiennej, to zostanie wyposaony we wszystkie wymienione wyej atrybuty zmiennej symbolicznej. Przykad 4.1. // Wskazniki i adresy #include <iostream.h> int main() { int i = 5, j = 10; int* wsk; wsk = &i; *wsk = 3; // ten sam efekt co i = 3; j = *wsk + 25; // j==28, *wsk==3 wsk = &j; // *wsk==28 i = j; // i==28, &i bez zmiany j = i; // j==28, &j bez zmiany cout << "*wsk= " << *wsk << endl << "i= " << i << endl; return 0; } Dynamiczna alokacja pamici W rodowisku programowym C++ kady program otrzymuje do dyspozycji pewien obszar pamici dla alokacji obiektw tworzonych w fazie wykonania. Obszar ten, nazywany pamici swobodn jest zorganizowany w postaci tzw. kopca lub stogu. Na kopcu (ang. heap) alokowane s obiekty dynamiczne, tj. takie, ktre tworzy si i niszczy przez zastosowanie operatorw new i delete. Operator new alokuje (przydziela) pami na kopcu, za operator delete zwalnia pami alokowan wczeniej przez new. Uproszczona skadnia instrukcji z operatorem new jest nastpujca: wsk = new typ; lub wsk = new typ (warto-inicjalna);

gdzie wsk jest wczeniej zadeklarowan zmienn wskanikow, np. int* wsk; Zmienn wsk mona te bezporednio zainicjowa adresem tworzonego dynamicznie obiektu: typ* wsk = new typ; lub wsk = new typ(warto inicjalna); np. int* wsk = new int(10); Skadnia operatora delete ma posta: delete wsk; gdzie wsk jest wskanikiem do typu o nazwie typ. Zadaniem operatora new jest prba (nie zawsze pomylna) utworzenia obiektu typu typ; jeeli prba si powiedzie i zostanie przydzielona pami na nowy obiekt, to new zwraca wskanik do tego obiektu. Zauwamy, e alokowany dynamicznie obszar pamici nie ma nazwy, ktra miaaby charakter l-wartoci. Po udanej alokacji zastpcz rol nazwy zmiennej peni *wsk, za adres obszaru jest zawarty w wsk. Operator delete niszczy obiekt utworzony przez operator new i zwraca zwolnion pami do pamici swobodnej. Po operacji delete warto zmiennej wskanikowej staje si nieokrelona. Tym niemniej zmienna ta moe nadal zawiera stary adres, lub, zalenie od implementacji, wskanik moe zosta ustawiony na zero (lub NULL). Jeeli przez nieuwag skorzystamy z takiego wymazanego wskanika, ktry nadal zawiera stary adres, to prawdopodobnie program bdzie nadal wykonywany, zanim dojdzie do miejsca, gdzie ujawni si bd. Tego rodzaju bdy s na og trudne do wykrycia. FUwaga. Poniewa alokowane dynamicznie zmienne istniej a do chwili zakoczenia wykonania programu, brak instrukcji z operatorem delete spowoduje zbdn zajto pamici swobodnej. Natomiast powtrzenie instrukcji dealokacji do ju zwolnionego obszaru pamici swobodnej moe spowodowa niekontrolowane zachowanie si programu. Przykad 4.2. #include <iostream.h> int main() { int* wsk; // wsk jest wskaznikiem do int; wsk = new int; delete wsk; wsk = new int (9); // *wsk == 9 cout << "*wsk= " << *wsk << endl; delete wsk; return 0; } Zastosowanie operatora delete do pamici niealokowanej prawie zawsze grozi nieokrelonym zachowaniem si programu podczas wykonania. Mona temu zapobiec, uzaleniajc dealokacj

od powodzenia alokacji pamici na stogu. Podane niej przykady ilustruj kilka sposobw zabezpieczenia si przed niepowodzeniem alokacji pamici za pomoc operatora new. Przykad 4.3. #include <iostream.h> int main() { int* wsk; if ((wsk = new int) == 0) //alternatywny zapis: if (!(wsk = new int)) { cout << "Nieudana alokacja\n"; return 1; } *wsk = 9; delete wsk; return 0; } Przykad 4.4. #include <iostream.h> int main() { int* wsk = new int; if (wsk == 0) //alternatywny zapis: if (!wsk) { cout << "Nieudana alokacja\n"; return 1; } *wsk = 9; delete wsk; return 0; } Przykad 4.5. #include <iostream.h> #include <stdlib.h> int main() { int* wsk = new int; if (!wsk) { cout << "Nieudana alokacja\n"; abort(); // lub exit(-1) } *wsk = 9; delete wsk; return 0; }

Przykad 4.6. #include <iostream.h> #include < assert.h> int main() { int* wsk = new int (8); assert (wsk != 0); cout << "*wsk= " << *wsk << endl; delete wsk; wsk = 0; //lub NULL return 0; } Wskaniki stae W C++ mona take deklarowa wskaniki z tzw. modyfikatorem const. Tak wic wskanik moe adresowa sta symboliczn: const double cd = 3.50; const double* wskd = &cd; Przy zapisie jak wyej, wskd jest wskanikiem do staej symbolicznej cd typu double. Poniewa staa symboliczna moe by traktowana jako zmienna tylko odczyt (ang. read-only variable), zatem jej warto nie moe by zmieniona ani bezporednio, ani porednio, tj. przez zmian wartoci *wskd. Natomiast wskanik wskd mona zmienia, przypisujc mu adres zmiennej symbolicznej, np. double d = 1.5; wskd = &d; Chocia d nie jest sta, powysze przypisanie zapewnia nam, e warto d nie moe by modyfikowana poprzez wskd. Ilustruje to niej podany przykad. Przykad 4.7. #include <iostream.h> int main() { const double cd = 1.50; const double* wskd = &cd; cout << "cd = " << cd << endl; cout << "*wskd = " << *wskd << endl; double d = 2.50; wskd = &d; // *wskd = 3.50; Niedopuszczalne cout << "*wskd = " << *wskd << endl; return 0; } Wydruk z programu ma posta:

cd = 1.5 *wskd = 1.5 *wskd = 2.5 Mona te zadeklarowa wskanik stay. Np. int i = 10; int* const wski = &i; Tutaj wski jest wskanikiem staym do zmiennej i typu int. Przy takiej deklaracji mona zmienia warto zmiennej i zmieniajc warto *wski, ale nie mona zmieni adresu wskazywanego przez wski. Dopuszczalne jest rwnie zadeklarowanie wskanika staego do staej symbolicznej, np. const int ci = 7; const int* const wskci = &ci; W tym przypadku bdem syntaktycznym byaby zarwno prba zmiany wartoci *wskci jak i adresu wskazywanego przez wskci. Omawiane deklaracje ilustruje kolejny przykad. Przykad 4.8. #include <iostream.h> int main() { int i = 10; int* const wski = &i; *wski = 20; cout << "*wski = " << *wski << endl; // wski++; Niedopuszczalne const int ci = 7; const int* const wskci = &ci; cout << "*wskci = " << *wskci << endl; return 0; } Typ tablicowy Tablica jest struktur danych, zoon z okrelonej liczby elementw tego samego typu. Elementy tablicy mog by typu int, double, float, short int, long int, char, wskanikowego, struct, union; mog by rwnie tablicami oraz obiektami klas. Deklaracja tablicy skada si ze specyfikatora (okrelnika) typu, identyfikatora i wymiaru. Wymiar tablicy, ktry okrela liczb elementw zawartych w tablicy, jest ujty w par nawiasw prostoktnych [] i musi by wikszy lub rwny jednoci. Jego warto musi by wyraeniem staym typu cakowitego, moliwym do obliczenia w fazie kompilacji; oznacza to, e nie wolno uywa zmiennej dla okrelenia wymiaru tablicy. Par nawiasw [], poprzedzonych nazw tablicy, np. Arr[], nazywa si deklaratorem tablicy. Elementy tablicy s dostpne poprzez obliczenie ich pooenia w tablicy. Taka posta dostpu jest nazywana indeksowaniem. Np. zapis: double tabd[4]; deklaruje tablic 4 elementw typu double: tabd[0], tabd[1], tabd[2] i tabd[3], gdzie tabd[i] s

nazywane zmiennymi indeksowanymi. Inaczej mwic, zmienne tabd[0], ..., tabd[3] bd sekwencj czterech kolejnych komrek pamici, w kadej z ktrych mona umieci warto typu double. Inicjowanie tablic Deklaracj tablicy mona powiza z nadaniem wartoci inicjalnych jej elementom. Tablic mona zainicjowa na kilka sposobw: Umieszczajc deklaracj tablicy na zewntrz wszystkich funkcji programu. Taka tablica globalna (ewentualnie poprzedzona sowem kluczowym static) zostanie zainicjowana automatycznie przez kompilator. Np. kady element tablicy globalnej o deklaracji double tabd[3]; zostanie zainicjowany na warto 0.0. Deklarujc tablic ze sowem kluczowym static w bloku funkcji. Jeeli nie podamy przy tym wartoci inicjalnych, to uczyni to, jak w poprzednim przypadku, kompilator. Definiujc tablic, tj. umieszczajc po jej deklaracji wartoci inicjalne, poprzedzone znakiem =. Wartoci inicjalne, oddzielone przecinkami, umieszcza si w nawiasach klamrowych po znaku =. Np. tablic tabd[4] mona zainicjowa instrukcj deklaracji: double tabd[4] = { 1.5, 6.2, 2.8, 3.7 }; W deklaracji inicjujcej mona pomin wymiar tablicy: double tabd[] = { 1.5, 6.2, 2.8, 3.7 }; W tym przypadku kompilator obliczy wymiar tablicy na podstawie liczby elementw inicjalnych. Deklarujc tablic, a nastpnie przypisujc jej elementom pewne wartoci w kolejnych instrukcjach przypisania, np. umieszczonych w ptli. Zwrmy w tym miejscu uwag na rnic pomidzy inicjowaniem tablicy w jej definicji, a przypisaniem, ktre moe mie miejsce po uprzednim zadeklarowaniu tablicy (chocia w obu przypadkach uywany jest ten sam symbol =). Podobnie jak stae typw wbudowanych, mona zdefiniowa tablic o elementach staych, np. const int tab[4] = { 10, 20, 30, 40 }; lub const int tab[] = { 10, 20, 30, 40 }; W takim przypadku zmienne indeksowane staj si staymi symbolicznymi, ktrych wartoci nie mona zmienia adnymi instrukcjami przypisania. FUwaga. Tablica nie moe by inicjowana inn tablic ani te tablica nie moe by przypisana do innej tablicy. Jeeli w definicji tablicy podaje si wartoci inicjalne skadowych, to kompilator inicjuje tablic wedug rosncych adresw jej elementw. W przypadku gdy liczba wartoci inicjalnych jest mniejsza od maksymalnego indeksu, podanego w deklaratorze ([]) tablicy, zostan zainicjowane pocztkowe skadowe, a pozostae kompilator zainicjuje na 0. Bdem syntaktycznym jest podanie wikszej od wymiaru tablicy liczby wartoci inicjalnych.

Przykad 4.9. // Tablica liczb typu int. #include <iostream.h> void main() { int i; const int WYMIAR = 10; int tab[WYMIAR]; /* tab[WYMIAR] - tablica 10 liczb calkowitych: tab[0],tab[1],..., tab[9] */ for (i = 0; i < WYMIAR; i++) { tab[i] = i; cout << "tab[" << i << "]= " << tab[i] << endl; } } Dyskusja. Deklaracja const WYMIAR = 10; definiuje identyfikator WYMIAR, ktry w programie bdzie zastpowany sta 10. Zmienn i typu int zadeklarowano na uytek ptli for; zmienn t mona byo take zadeklarowa bezporednio w ptli for: for(int i = 0; ... ) Nadawanie elementom tablicy tab[] wartoci rwnych ich indeksom mona byo zastpi zainicjowaniem tablicy w chwili jej deklaracji: int tab[WYMIAR] = { 0,1,2,3,4,5,6,7,8,9 }; lub int tab[] = { 0,1,2,3,4,5,6,7,8,9 }; Przykad 4.10. // Tablica znakow #include <iostream.h> void main() { const int BUF = 10; char a[BUF] = "ABCDEFGHIJ"; // a[BUF]-tablica 10 znakow: a[0],a[1],...,a[9] /* Dopuszczalne jest zainicjowanie: char a[BUF] = {'A','B','C','D','E','F','G','H','I','J'}; */ cout << a<< endl; }

Przykad 4.11. // Tablica znakow, instrukcja for #include <iostream.h> void main() { const int BUF = 10; char znak; char a[BUF] = {'A','B','C','D','E','F','G','H','I','J'}; // a[BUF]-tablica 10 znakow: a[0],a[1],...,a[9] for (znak = 'A'; znak <= 'J'; znak++) cout << a[znak - 65] << endl; } Wskaniki i tablice W jzyku C++ istnieje cisa zaleno pomidzy wskanikami i tablicami. Zaleno ta jest ta silna, e kada operacja na zmiennej indeksowanej moe by take wykonana za pomoc wskanikw. Przy tym operacje z uyciem wskanikw s w oglnoci wykonywane szybciej. Podczas kompilacji nazwa tablicy, np. tab, jest automatycznie przeksztacana na wskanik do pierwszego jej elementu, czyli na adres tab[0]. We fragmencie programu: int tab[4] = { 10, 20, 30, 40 }; int* wsk; wsk = tab; instrukcja: wsk = tab; jest rwnowana instrukcji: wsk = &tab[0]; poniewa tab == &a[0]. Inaczej mwic, wsk oraz tab wskazuj teraz na element pocztkowy tab[0] tablicy tab[]. Istnieje jednak istotna rnica pomidzy wsk i tab. Identyfikator tablicy (tab) jest inicjowany adresem jej pierwszego elementu (tab[0]); adres ten nie moe ulec zmianie w programie (nazwa tablicy nie jest modyfikowaln l-wartoci). Tak wic identyfikator tablicy jest rwnowany wskanikowi staemu i nie mona go zwiksza czy zmniejsza. natomiast wsk jest zmienn wskanikow; w programie musimy najpierw ustawi wskanik na adres wczeniej alokowanego obiektu (np. tablicy tab), a nastpnie moemy zmienia jego wskazania. Zwrmy uwag na to, e zwikszenie wartoci wskanika wsk np. o 2 zwikszy w tym przypadku wskazywany adres o tyle bajtw, ile zajmuj dwa elementy tablicy tab. Tak wic instrukcje: tab = tab+1; tab = wsk; s bdne, natomiast instrukcja: wsk = wsk + 1;

jest poprawna (jest ona rwnowana instrukcji: wsk = &tab[1];, zatem nowa warto *wsk == tab[1]). Przykad 4.12. // Wskazniki i tablice #include <iostream.h> void main() { int t1[10] = { 0,1,2,3,4,5,6,7,8,9 }; int t2[10], *wt1, *wt2; wt1 = t1; // to samo, co wt1 = &t1[0], poniewaz t1==&t1[0] wt2 = t2; // to samo, co wt2 = &t2[0], poniewaz t2==&t2[0] for (int i = 0; i < 10; i++) cout << "t1[" << i << "]= " << *wt1++ << endl; } Dyskusja. W bloku funkcji main mamy kolejno: Deklaracj tablicy t1[10] typu int, zainicjowanej wartociami 0..9, rwnymi indeksom tablicy. Przypomnijmy, e podanie wymiaru tablicy nie byo w tym przypadku konieczne. Deklaracj tablicy t2[10] typu int oraz dwch wskanikw: wt1 i wt2, wskazujcych na typ int. Dwie kolejne instrukcje przypisania: wt1 = t1; wt2 = t2;. Przedyskutujmy je nieco bardziej szczegowo. Bezporednio po przypisaniu wt1 = 1; zawarto *wt1 jest rwna t1[0]. Zawarto pod adresem wt1+1, czyli *(wt1+1) jest rwna t1[1], itd. Wyjania to uycie wyraenia *wt1++ w ptli for: zamiast cout << *wt1++ moglibymy napisa cout << *(wt1+i) lub cout << wt1[i]. Ten ostatni zapis jest poprawny, poniewa wt1[i] jest rwnowane *(wt1+i). Zapis *wt1++ daje kolejne wartoci t1[i] dziki hierarchii operatorw: operator ++ wie silniej ni operator * i w rezultacie otrzymujemy *wt1, *(wt1+1), *(wt1+2), ... , *(wt1+9), czyli zawartoci t1[0], t1[1], ... , t1[9]. Przykad 4.13. // Wskazniki i tablice void main() { int t1[10], *wt1, *wt2; int t2[10] = {0,1,2,3,4,5,6,7,8,9}; wt1 = t1; wt2 = t2; while (wt2 <= &t2[9]) *wt1++ = *wt2++ ; } Dyskusja. Przykad ilustruje kopiowanie tablicy t2 do tablicy t1. Zauwamy, e zamiast instrukcji wt1 = t1; moglibymy napisa: wt1 = &t1[0];, poniewa t1==&t1[0]. To samo dotyczy wskanika wt2 i tablicy t2. Kopiowanie jest wykonywane w ptli while na wskanikach. W instrukcji przypisania *wt1++ = *wt2++; mona byoby umieci nawiasy dla lepszego pokazania, i najpierw jest zwikszany wskanik, a nastpnie stosowany operator dostpu poredniego *, tj. napisa t instrukcj w postaci:

*(wt1++) = *(wt2++); Nie jest to jednak konieczne poniewa przyrostkowy operator ++ wie od prawej do lewej i ma wyszy priorytet ni *. Omawiana instrukcja jest idiomem jzyka C++. Jest ona rwnowana sekwencji instrukcji: *wt1 = *wt2; wt1 = wt1 + 1; wt2 = wt2 + 1; FUwaga. Zapamitajmy: int* wsk[10] deklaruje tablic 10 wskanikw do typu int, natomiast int (*wsk)[10] deklaruje wskanik wsk do tablicy o 10 elementach typu int. Nawiasy s tutaj konieczne, poniewa deklarator tablicy [] ma wyszy priorytet ni *. Wskaniki i acuchy znakw W jzyku C++ wszystkie operacje na acuchach znakw s wykonywane za pomoc wskanikw do znakw acucha. Przypomnijmy, e acuch jest cigiem znakw, zakoczonym znakiem '\0'. Jeeli zadeklarujemy tablic znakw alfa1[] z wartoci inicjaln "ABCD" char alfa1[] = "ABCD"; to kompilator doda do acucha "ABCD" terminalny znak zerowy i obliczy, e wymiar tablicy alfa1[] wynosi 5. wynika std, e deklaracja: char alfa1[4] = "ABCD"; jest bdna , poniewa nie przewiduje miejsca na koczcy zapis znak '\0'. Bdn bdzie take prba przypisania acucha znakw do niezainicjowanej tablicy typu char: char alfa2[5]; alfa2 = "ABCD"; //niedopuszczalne! poniewa w jzyku nie zdefiniowano operacji przypisania dla tablicy jako caoci. Formalnie dopuszczalnym jest zainicjowanie tablicy znakw w sposb analogiczny do innych typw, np. char alfa3[] = { 1, 2, 3, 4 }; char alfa4[] = { 'A', 'B', 'C', 'D' } Tak zainicjowane tablice alfa3[] oraz alfa4[] s tablicami 4 a nie 5 znakw i powysze zapisy s oczywistym bdem programistycznym. Oczywistym dlatego, poniewa zdecydowana wikszo funkcji bibliotecznych operujcych na acuchach znakw zakada istnienie terminalnego znaku zerowego. Mona, rzecz jasna, napisa deklaracj char alfa4[] = { 'A', 'B', 'C', 'D', '\0' }; rwnowan innej poprawnej deklaracji char alfa4[] = "ABCD"; ale, przyznajmy, jest to raczej kopotliwe. Wyjciem z tych kopotw jest pokazany ju wczeniej mechanizm automatycznej konwersji nazwy

tablicy na wskanik do jej pierwszego elementu. Zatem wykonanie instrukcji deklaracji char alfa[] = "ABCD"; spowoduje przydzielenie przez kompilator wskanika do alfa[0], tj do znaku 'A'. Moemy wic zdefiniowa wskanik do typu char, np. char* wsk; i przypisa mu nazw tablicy alfa[] wsk = alfa; co jest rwnowane sekwencji instrukcji: char* wsk; wsk = &alfa[0]; lub krtszemu zapisowi: char* wsk = &alfa[0]; Z arytmetyki wskanikw wynika, e jeeli wsk==&alfa[0], to wsk+i== &alfa[i]. Pamitajc o przypisaniu wsk = alfa; mamy rwnowano wsk + i == &wsk[i]. Wobec tego znak, zapisany pod adresem &wsk[i] (lub &alfa[i]), znajdziemy przez odwrcenie ostatniej relacji: *(wsk + i) == wsk[i], np. *(wsk + 2) == 'C' poniewa wsk[i] == alfa[i]. Konkluzja z tej nieco rozwlekej dyskusji jest prosta: wszdzie tam, gdzie mamy zamiar wykonywa operacje na znakach, zamiast deklarowa tablic char alfa[] = "ABCD"; wystarczy zadeklarowa wskanik char* alfa = "ABCD"; Pokazane niej trzy przykady ilustruj zastosowanie wskanikw do tablic (acuchw) znakw.

Przykad 4.14. // Kopiowanie ze "start" do "cel" // Wersja ze zmiennymi indeksowanymi #include <iostream.h> int main() { char *start = "ABCD"; char *cel = "EFGH"; int i = 0; while ((cel[i] = start[i]) != '\0') i++; cout << "Lancuch cel: " << cel << endl ; return 0; } Dyskusja. Wskanik start zosta ustawiony na adres pierwszego znaku acucha "ABCD", tj. *start == start[0] == 'A'. Analogicznie *cel == cel[0] == 'E'. Kolejne skadowe acucha "ABCD" s kopiowane do acucha "EFGH" w ptli while. Poniewa operacje te s prowadzone na zmiennych indeksowanych, adresy wskazywane przez start i cel nie ulegaj zmianie. Mona si o tym przekona, deklarujc je jako wskaniki stae: char* const start = "ABCD"; char* const cel = "EFGH"; Przykad 4.15. // Kopiowanie ze "start" do "cel" // Wersja ze wskaznikami #include <iostream.h> int main() { char *start = "ABCD"; char *cel = "EFGH"; char *pomoc = cel; while ((*cel = *start) != '\0') { cel++ ; start++ ; } cout << "Lancuch pomoc: " << pomoc << endl ; return 0; } Dyskusja. W bloku instrukcji while wskaniki start i cel s przesuwane a do znaku '\0' koczcego acuch. Skadowe acuchw s kopiowane przypisaniem *cel = *start, co jest rwnowane cel[0]=start[0]. Po wyjciu z ptli wskanik cel bdzie ustawiony na ostatniej skadowej skopiowanego acucha, tj. na '\0'. Dla wydrukowania zawartoci tego acucha musielibymy najpierw cofn wskanik cel o liczb elementw acucha cel = cel - 4; i dopiero potem wydrukowa acuch, piszc np. cout << "cel = " << cel << endl; W programie przyjto inne rozwizanie. Wskanik pomoc jest na stae ustawiony na adres pierwszego elementu acucha cel i wydruk kopii nie wymaga adnych dodatkowych operacji. Zatem, podobnie jak w poprzednim przykadzie, wskanik pomoc mona zadeklarowa jako

wskanik stay char* const pomoc = cel; Przykad 4.16. // Kopiowanie ze "start" do "cel" // Inna wersja ze wskaznikami #include <iostream.h> int main() { char *start = "ABCD"; char *cel = "EFGH"; char *pomoc = cel; while (*cel++ = *start++) ; cout << "Lancuch pomoc: " << pomoc << endl ; return 0; } Dyskusja. W tym przykadzie wykorzystano wzmiankowany ju wczeniej idiom jzyka C++. Porwnujc ten program z poprzednim zauwaymy, e jedyna rnica wystpuje w konstrukcji ptli while. Poniewa instrukcja przypisania *cel++ = *start++; jest rwnowana sekwencji instrukcji: *cel = *start; cel++; start++; to przypisanie *cel = *start mona wykorzysta w wyraeniu testowanym w ptli, co te uczyniono w poprzednim przykadzie. W stosunku do poprzedniego przykadu wystpuje tutaj jeszcze jedna rnica. Ze wzgldu na to, e zwikszanie wskanikw o 1 nastpuje na wejciu do ptli, kocowy adres wskazywany przez wskanik cel bdzie teraz przesunity w stosunku do pocztkowego o 5 jednostek (a nie o 4, jak poprzednio). Tak wic tym razem mona by wydrukowa acuch skopiowany, cofajc najpierw wskanik cel o 5 jednostek: cel = cel - 5; FDla kopiowania acuchw mona wykorzysta funkcj biblioteczn strcpy(char* do, const char* z) z pliku nagwkowego <string.h>, dostarczanego standardowo z kadym kompilatorem jzyka C++. Naley wwczas w programie umieci dyrektyw #include <string.h>, a w bloku main() wywoa wspomnian funkcj, piszc: strcpy(cel, start);. Tablice wielowymiarowe Poniewa elementy tablicy mog by tablicami, moliwe jest deklarowanie tablic wielowymiarowych. Np. zapis: int tab[4][3]; deklaruje tablic o czterech wierszach i trzech kolumnach. W tym przypadku dopuszcza si notacje: tab tablica 12-elementowa, tab[i] tablica 3-elementowa, w ktrej kady element jest tablic 4-elementow, tab[i][j] element typu int. Inaczej mwic, tablica tab[4][3] jest tablic zoon z czterech elementw tab[0], tab[1], tab[2] i tab[3], przy czym kady z tych

czterech elementw jest tablic trjelementow liczb cakowitych. Podobnie jak dla tablic jednowymiarowych, identyfikator tab jest niejawnie przeksztacany we wskanik do pierwszego elementu tablicy, czyli do pierwszej spord czterech tablic trjelementowych. Proces dostpu do elementw tablicy przebiega nastpujco: Jeeli mamy wyraenie tab[i], ktre jest rwnowane *(tab+i), to tab jest najpierw przeksztacane do wymienionego wskanika; nastpnie (tab+i) zostaje przeksztacone do typu tab, co obejmuje mnoenie i przez dugo elementu na ktry wskazuje wskanik, tj. przez trzy elementy typu int. Otrzymany wynik jest dodawany do tab, po czym zostaje przyoony operator dostpu poredniego *, dajc w wyniku tablic (trzy liczby cakowite), ktra z kolei zostaje przeksztacona we wskanik do jej pierwszego elementu, tj. tab[i][0]. Wynikaj std dwa wnioski: 1) Tablice wielowymiarowe s w jzyku C++ zapamitywane wierszami (ostatni z prawej indeks zmienia si najszybciej). 2) Pierwszy z lewej wymiar w deklaracji tablicy pomaga wyznaczy wielko pamici zajmowanej przez tablic, ale nie odgrywa adnej roli w obliczaniu indeksw. Tablice wielowymiarowe inicjuje si podobnie, jak jednowymiarowe, zamykajc wartoci inicjalne w nawiasy klamrowe. Jednak ze wzgldu na zapamitywanie wierszami przewidziano dodatkowe, zagniedone nawiasy klamrowe, w ktrych umieszcza si wartoci inicjalne dla kolejnych wierszy. Przykad 4.17. #include <iostream.h> int main() { int tab[4][2] = // W [4] mozna opuscic 4 { { 1, 2 }, // inicjuje tab[0], // tj. tab[0][0] i tab[0][1] { 3, 4 }, // inicjuje tab[1] { 5, 6 }, // inicjuje tab[2] { 7, 8 } // inicjuje tab[3] }; for (int i = 0; i < 4; i++) { cout<<"tab["<<i<<"][0]: "<<tab[i][0]<<'\t'; cout<<"tab["<<i<<"][1]: "<<tab[i][1]<< '\n'; } return 0; } Dyskusja. W programie zadeklarowano tablic (macierz) o 4 wierszach i 2 kolumnach. Poniewa tablica jest inicjowana jawnie, w jej deklaracji mona byoby opuci podawanie pierwszego wymiaru. Nie s rwnie konieczne zagniedone nawiasy klamrowe, zawierajce wartoci inicjalne dla kolejnych wierszy umieszczono je tutaj dla pokazania, e tablice wielowymiarowe s zapamitywane wierszami. Wida to wyranie na wydruku:

tab[0][0]: 1 tab[1][0]: 3 tab[2][0]: 5 tab[3][0]: 7

tab[0][1]: 2 tab[1][1]: 4 tab[2][1]: 6 tab[3][1]: 8

Wydruk byby identyczny, gdyby definicja tablicy miaa posta: int tab[4][2] = { 1, 2, 3, 4, 5, 6, 7, 8 }; Podobnie jak dla tablic jednowymiarowych mona poda mniejsz od stopnia tablicy (tj. iloczynu wszystkich jej wymiarw) liczb wartoci inicjalnych. Wwczas ta cz elementw tablicy, dla ktrej zabraknie wartoci inicjalnych, zostanie zainicjowana zerami. T wasno naley rwnie mie na uwadze przy opuszczaniu zagniedonych nawiasw klamrowych. Np. wykonanie instrukcji deklaracji int tab[4][2] = lub int tab[4][2] = { { 1, 2 }, { 3, 4 }, { 5, 6 } }; { 1, 2, 3, 4, 5, 6 };

Nada wartoci inicjalne pierwszym trzem wierszom i spowoduje wyzerowanie czwartego wiersza. Natomiast wykonanie instrukcji deklaracji int tab[4][2] = { { 1 }, { 2 }, { 3 }, { 4 } };

spowoduje wyzerowanie drugiej kolumny macierzy tab[4][2]. Dynamiczna alokacja tablic Operatory new i delete mona rwnie stosowa do dynamicznej alokacji i dealokacji tablic w pamici swobodnej programu, tj. na kopcu. Skadnia instrukcji powoujcej do ycia tablic dynamiczn jest nastpujca: wsk = new typ[wymiar]; gdzie wsk jest wskanikiem do typu o nazwie typ, za wymiar okrela liczb elementw tablicy. Jeeli alokacja si powiedzie, to new zwraca wskanik do pierwszego elementu tablicy. W przeciwiestwie do statycznej alokacji tablic z jednoczesnym inicjowaniem, jak w przykadowej deklaracji int tab[10] = { 0,1,2,3,4,5,6,7,8,9 }; tablica alokowana dynamicznie nie moe by inicjowana. Przykadowa instrukcja dynamicznej alokacji tablicy moe mie posta: int *wsk = new int[150]; Powysza instrukcja definiuje wskanik wsk do typu int i ustawia go na pierwszy element tworzonej tablicy 150 elementw typu int. W rezultacie wsk[0] bdzie wartoci pierwszego elementu tablicy, wsk[1] drugiego, etc. Skadnia operatora delete dla uprzednio alokowanej (z sukcesem!) tablicy dynamicznej ma posta:

delete [] wsk; FUwaga 1. Niektre starsze wersje kompilatorw jzyka C++ wymagaj napisania instrukcji usuwajcej tablic z pamici w postaci delete wsk; lub podania wymiaru tablicy, np. delete [150] wsk;. FUwaga 2. Poniewa alokowane dynamicznie tablice istniej a do chwili zakoczenia wykonania programu, brak instrukcji z operatorem delete spowoduje zbdn zajto pamici swobodnej. Natomiast powtrzenie instrukcji dealokacji do ju zwolnionego obszaru pamici swobodnej moe spowodowa niekontrolowane zachowanie si programu. Przykad 4.18. // Dynamiczna alokacja tablicy #include <iostream.h> int main() { int* wsk; wsk = new int[5]; if (!wsk) { cout << "Nieudana alokacja\n"; return 1; } for (int i = 0; i < 5; i++) wsk[i] = i; for(int j = 0; j < 5; j++) { cout << "wsk[" << j << "]: "; cout << wsk[j] << endl; } delete [] wsk; return 0; } Analiza programu. Po wykonaniu instrukcji deklaracji int* wsk; wskanik wsk zostanie zainicjowany przypadkowym adresem; oczywicie warto *wsk bdzie rwnie przypadkowa. Natomiast sama zmienna wsk otrzyma konkretny adres podczas kompilacji. Po wykonaniu instrukcji wsk = new int[5]; warto wsk bdzie ju konkretnym adresem pierwszego elementu tablicy. Oczywicie &wsk, tj. miejsce na stosie programu, gdzie ulokowany jest adres samego wsk, nie ulegnie zmianie. Poniewa nie jest dozwolone inicjowanie tablicy w chwili powoania jej do ycia, uczynilimy to w pierwszej ptli for. Po wyjciu z ptli otrzymamy wartoci: wsk[0]==0, wsk[1]==1, wsk[2]==2, wsk[3]==3 i wsk[4]==4. Adres wskazywany przez wsk, tj. warto samego wsk, bdzie rwny &wsk[0]. Nastpne adresy, tj. &wsk[1], &wsk[2], &wsk[3] i &wsk[4], bd odlege od siebie o tyle bajtw, ile wynosi sizeof(int). Referencje Referencja jest specjalnym, niejawnym wskanikiem, ktry dziaa jak alternatywna nazwa dla zmiennej. Gwnym zastosowaniem referencji jest uycie ich w charakterze argumentw i wartoci zwracanych przez funkcje. Tym niemniej mona rwnie deklarowa i uywa w programie referencje niezalene. T wanie moliwo wykorzystamy do omwienia wasnoci referencji. Zmienn o nazwie podanej po nazwie typu z przyrostkiem &, np. T& nazwa, gdzie T jest nazw typu, nazywamy referencj do typu T. Referencja musi by zainicjowana do pewnej l-wartoci, po czym moe by uywana jako inna nazwa dla l-wartoci zmiennej. Poniewa referencja spenia rol innej nazwy dla zmiennej, zatem

jest ona take l-wartoci. Przykadowe deklaracje: int n = 10; int& rn = n; rn = 2; // Ten sam efekt co n = 2; int *wsk = &rn; int& rr = rn; Pierwsza deklaracja, int n = 1; definiuje zmienn n typu int oraz inicjuje jej warto na 1. Druga deklaracja, int& rn = n; definiuje zmienn referencyjn rn, inicjujc j do wartoci bdcej adresem zmiennej n. Tak wic, skoro rn oraz n maj ten sam adres, to rn jest dodatkow, alternatywn nazw dla tego obszaru pamici, ktremu wczeniej nadano symboliczn nazw n. Wida to wyranie w trzeciej instrukcji rn = 2;, ktra moe mie rwnowan posta n = 2;. Zauwamy przy tym, e pierwsze dwie instrukcje deklaracji uywaj znaku rwnoci do zainicjowania deklarowanych wielkoci; natomiast w instrukcji rn = 2; znak rwnoci jest operatorem przypisania. Czwarta instrukcja deklaracji int *wsk = &rn; definiuje wskanik wsk, inicjujc go adresem zmiennej rn. Zatem wsk bdzie wskazywa na adres n (ten sam adres ma rn), za *wsk bdzie t sam wartoci, ktr zostaa zainicjowana zmienna n. Ostatnia instrukcja deklaracji int& rr = rn; inicjuje zmienn rr adresem zmiennej rn. Poniewa adres zmiennej rn jest identyczny z adresem zmiennej n, zatem rr jest kolejn (czwart) zastpcz nazw dla tej samej komrki pamici (n, rn, *wsk i rr). Referencje do zmiennych danego typu T mog by inicjowane jedynie l-wartociami typu T. Natomiast referencje do staych symbolicznych typu T mog by inicjowane l-wartociami typu T, l-wartociami sprowadzalnymi do typu T, lub nawet r-wartociami, np. sta 1024. W takich przypadkach kompilator realizuje nastpujcy algorytm: 1. Wykonaj, jeeli to konieczne, konwersj typu. 2. Uzyskan warto wynikow umie w zmiennej tymczasowej. 3. Wykorzystaj adres zmiennej tymczasowej jako warto inicjaln. Dla ilustracji wemy deklaracj const double& refcd = 10; dla ktrej wskanikowa interpretacja algorytmu jest nastpujca: double double temp = refcdp *refcdp; // referencja jako wskaznik temp; double(10); = &temp;

gdzie temp reprezentuje zmienn tymczasow generowan przez kompilator dla wykonania konwersji z int do double. Czas ycia utworzonej w ten sposb zmiennej tymczasowej jest okrelony przez jej zasig (np. do wyjcia z bloku, pliku lub programu). Implementacja tego typu konwersji musi zapewni istnienie zmiennej tymczasowej dotd, dopki istnieje zwizana z ni referencja. Kompilator musi take zapewni usunicie zmiennej tymczasowej z pamici, gdy nie

jest ju potrzebna. Ze sposobu tworzenia niezalenej referencji wynika, e po zainicjowaniu nie mona zmieni obiektu, z ktrym zostaa zwizana. Poniewa referencje nie tworz prawdziwych obiektw w sensie uywanym w jzyku C++, nie istniej tablice referencji. Nie zdefiniowano rwnie dla referencji operatorw, podobnych do uywanych dla wskanikw. Powysze wzgldy zdecydoway o niewielkiej uytecznoci niezalenych referencji. Natomiast zastosowanie referencji jako parametrw formalnych i wartoci zwracanych dla funkcji jest w wielu przypadkach wygodne i zalecane. Struktury Struktura jest jednostk syntaktyczn grupujc skadowe rnych typw, zarwno podstawowych, jak i pochodnych. Ponadto skadowa struktury moe by tzw. polem bitowym. Struktury s deklarowane ze sowem kluczowym struct. Deklaracja referencyjna struktury ma posta: struct nazwa; za jej definicja struct nazwa { /*...*/ }; gdzie nazwa jest nazw nowo zdefiniowanego typu. Zwrmy uwag na rednik koczcy definicj: jest to jeden z nielicznych przypadkw w jzyku C++, gdy dajemy rednik po nawiasie klamrowym. Przykad 4.19. #include <iostream.h> struct skrypt { char *tytul; char *autor; float cena; long int naklad; char status; }; int main() { skrypt stary, nowy; skrypt *wsk; wsk = &nowy; stary.autor = "Jan Kowalski"; stary.cena = 12.55; wsk->naklad = 50000; wsk->status = 'A'; cout << "stary.autor = " << stary.autor << '\n'; cout << "stary.cena = " << stary.cena << '\n'; cout << "wsk->naklad = " << wsk->naklad << '\n'; cout << "wsk->status = " << wsk->status << '\n'; cout << "(*wsk).status = " << (*wsk).status << '\n'; return 0; } Dyskusja. Definicja struktury na pocztku programu wprowadza nowy typ o nazwie skrypt. Zmienne stary, nowy tego typu deklaruje si tak samo, jak zmienne typw podstawowych. Dostp do skadowych (pl) struktury uzyskuje si za pomoc operatora . (kropka) lub operatora -> (minus i znak wikszoci). Np. dla zmiennej nowy typu skrypt moemy napisa:

nowy.autor = "Jan Nowak"; nowy.cena = 21.30; a dla zmiennej wskanikowej wsk: wsk->tytul = "Podstawy Informatyki"; Operatory . i '-> s wic operatorami selekcji. Poniewa wskanik wsk zosta zainicjowany adresem zmiennej nowy, zatem *wsk jest synonimem dla zmiennej nowy. Wobec tego zamiast np. s->status moglimy napisa (*wsk).status. Uycie nawiasw dla *wsk byo konieczne, poniewa operatory . i -> s lewostronnie czne (wi od prawej do lewej). Struktury mog by zagniedane; ilustruje to nastpny przykad. Przykad 4.20. #include <iostream.h> struct A { int i; char znak; }; struct B { int j; A aa; double d; }; int main() { B s1, *s2 = &s1; s1.j = 4; s1.aa.i = 5; s2->d = 1.254; (s2->aa).znak = 'A'; cout << (s2->aa).znak << endl; return 0; } Dyskusja. W definicji struktury B umieszczono deklaracj zmiennej aa wczeniej zdefiniowanego typu A. Dostp do skadowych typu A dla zmiennych typu B uzyskuje si wwczas przez dwukrotne zastosowanie operatorw selekcji, np. s1.aa.i lub (s2->aa).znak dla wskanika (znowu konieczne nawiasy okrge). Kada deklaracja struktury wprowadza nowy, unikatowy typ, np. struct s1 { int i ; }; struct s2 { int j ; }; s dwoma rnymi typami; zatem w deklaracjach s1 x, y ; s2 z ; zmienne x oraz y sa tego samego typu s1, ale x oraz z s rnych typw. Wobec tego przypisania

x = y; y = x; s poprawne, podczas gdy x = z; z = y; s bdne. Dopuszczalne s natomiast przypisania skadowych o tych samych typach, np. x.i = z.j; Pola bitowe Obszar pamici zajmowany przez struktur jest rwny sumie obszarw, alokowanych dla jej skadowych. Jeeli np. struktura ma trzy skadowe typu int, a implementacja przewiduje 2 bajty na zmienn tego typu, to reprezentacja struktury w pamici zajmie 6 bajtw. Dla duych struktur (np. takich, ktrych skadowe s duymi tablicami) obszary te mog by znacznej wielkoci. W takich przypadkach moliwe jest cilejsze upakowanie pl struktury poprzez zdefiniowanie tzw. pl bitowych, ktre zawieraj podan w deklaracji liczb bitw. W pamici komputera pole bitowe jest zbiorem ssiadujcych ze sob bitw w obrbie jednej jednostki pamici zdefiniowanej w implementacji, a nazywanej sowem. Rozmieszczenie w pamici struktury z polami bitowymi jest take zalene od implementacji. Jeeli dane pole bitowe zajmuje mniej ni jedno sowo, to nastpne pole moe by umieszczone albo w nastpnym sowie (wtedy nic nie oszczdzamy), albo czciowo w wolnej czci pierwszego sowa, a pozostae bity w nastpnym sowie. Przy tym, zalenie od implementacji, alokacja pola bitowego moe si zaczyna od najmniej znaczcego lub od najbardziej znaczcego bitu sowa. Deklaracja skadowej bdcej polem bitowym ma posta: typ nazwa_pola : wyraenie; gdzie: typ oznacza typ pola i musi by jednym z typw cakowitych, tj. char, short int, int, long int ze znakiem (signed) lub bez (unsigned) oraz enum; wystpujce po dwukropku wyraenie okrela liczb bitw zajmowan przez dane pole. Pola bitowe mog by rwnie deklarowane bez nazwy (tzn. tylko typ, po nim dwukropek, a nastpnie szeroko pola). Takie pole nie moe by inicjowane, ale jest uyteczne, poniewa przy zadeklarowanej szerokoci 0 (zero) wymusza dopenienie do caego sowa wczeniej zadeklarowanego pola bitowego. Dziki temu alokacja nastpnego pola bitowego moe si zacz od pocztku nastpnego sowa. Pola bitowe zachowuj si jak mae liczby cakowite i mog wystpowa w wyraeniach arytmetycznych, w ktrych przeprowadza si operacje na liczbach cakowitych. Przykadowo, deklaracj struct sygnalizatory { unsigned int sg1 : 1; unsigned int sg2 : 1; unsigned int sg3 : 1; } s;

moemy wykorzysta do wczania lub wyczania sygnalizatorw s.sg1 = 1; s.sg2 = 0; s.sg3 = 1; lub testowania ich stanu if (s.sg1 == 0 && s.sg3 == 0) s.sg2 = 1; Zwrmy uwag na skadni dostpu do pola bitowego: jest ona taka sama, jak dla zwykego pola. Przykad 4.21. #include <iostream.h> struct flagi { unsigned int flaga1 : 1; unsigned int flaga2 : 1; unsigned char flaga3 : 6; }; int main() { flagi bit1; cout << sizeof bit1 << endl; bit1.flaga1 = 1; bit1.flaga2 = bit1.flaga1 ^ 1; bit1.flaga3 = '*'; cout << bit1.flaga1 << ' ' << bit1.flaga2 << endl; cout << bit1.flaga3 << endl; return 0; } Wydruk z programu, dla sizeof(int)==2, ma posta: 1 10 * Przykad 4.22. #include <iostream.h> struct bity { int b1 : 8; int b2 : 16; int b3 : 16; }; int main() { bity bit1; bit1.b1 = 0; bit1.b2 = ~bit1.b1 & 1; bit1.b3 = 32767; cout << sizeof bit1 << endl; cout << bit1.b1 << ' '; cout << bit1.b2 << endl; cout << bit1.b3 << endl; return 0; }

Wydruk z programu, dla sizeof(int)==2, ma posta: 5 01 32767 Komentarz. W pierwszym przykadzie zaoszczdzilimy 4 bajty, a w drugim 1 bajt. Wynik wydaje si by niezy, cho ju na pierwszy rzut oka wida, e oszczdno opacilimy duszym kodem programu (dwukropki i wielkoci pl). Tak wic na pewno wikszy bdzie kod wykonalny i nieco duszy czas wykonania. I tak jest w wikszoci przypadkw, przy czym czsto jeszcze operowanie na polach bitowych wymaga dodatkowych instrukcji. Wniosek std oczywisty: nie warto oszczdza kilku bajtw pamici, ale gdy w gr wchodz dziesitki czy setki kilobajtw, to stosowanie pl bitowych moe si opaca. Unie Unia, podobnie jak struktura, grupuje skadowe rnych typw. Jednak w odrnieniu od struktury tylko jedna ze skadowych unii moe by aktywna w danym momencie. Wynika to std, e kada ze skadowych unii ma ten sam adres pocztkowy w pamici, za obszar pamici zajmowany przez uni jest rwny rozmiarowi jej najwikszej skadowej. Definicja unii ma posta: union nazwa { ... }; Przykad 4.23. #include <iostream.h> union test { long int i; double d; char znak; }; int main() { test uu; test* uwsk; uwsk = &uu; uu.d = 14.85; cout << uu.d << endl; cout << uu.i << endl; uu.i = 123456789; cout << uu.i << endl; cout << uu.d << endl; uwsk->i = 79; cout << uu.i << endl; return 0; } Dyskusja. W powyszym przykadzie druga instrukcja cout jest poprawna, ale uu.i odpowiada czci danej typu double uprzednio przypisanej i moe nie mie sensownej interpretacji, poniewa w tym momencie aktywn skadow bya uu.d. Uni mona zainicjowa albo wyraeniem prostym tego samego typu, albo ujt w nawiasy klamrowe wartoci pierwszej zadeklarowanej skadowej. Np. uni uu mona zainicjowa deklaracj test uu = { 1 };

Podobnie jak dla struktur, mona stosowa instrukcj przypisania dla unii tego samego typu, np. test uu, uu1, uu2; uu2 = uu1 = uu; W definicji unii mona pomin nazw po sowie kluczowym union, a deklaracje zmiennych umieci pomidzy zamykajcym nawiasem klamrowym a rednikiem, np. union { int i; char *p; } uu, *uwsk = &uu; Powysza definicja rwnie tworzy unikatowy typ. Poniewa typ wystpuje tutaj bez nazwy, tak definicj stosuje si wtedy, gdy unia ma by wykorzystana tylko jeden raz. Natomiast dostp do skadowych jest taki sam, jak poprzednio. Przykad 4.24. #include <iostream.h> int main() { union { int i; char *p; } uu, *uwsk = &uu; uu.i = 14; cout << uu.i << endl; uwsk->p = "abcd"; cout << uwsk->p << endl; return 0; } Specyficzn dla jzyka C++ jest unia bez nazwy i bez deklaracji zmiennych, o skadni: union { wykaz-skadowych }; Taki zapis, nazywany uni anonimow, nie tworzy nowego typu, a jedynie deklaruje szereg skadowych, ktre wspdziel ten sam adres w pamici. Poniewa unia nie ma nazwy, jej elementy s dostpne bezporednio nie ma potrzeby stosowania operatorw . i ->. Przykad 4.25. #include <iostream.h> int main() { union { int i; char *p; } ; i = 14; cout << i << endl; p = "abcd"; cout << p << endl; return 0; }

5. Funkcje
Przed wprowadzeniem klas i obiektw podprogramy-funkcje (i podprogramy-procedury) byy najwaniejszymi jednostkami modularyzacji programw. Funkcj mona uwaa za operacj zdefiniowan przez programist i reprezentowan przez nazw funkcji. Operandami funkcji s jej argumenty, ujte w nawiasy okrge i oddzielone przecinkami. Typ funkcji okrela typ wartoci zwracanej przez funkcj.

5.1.

Deklaracja, definicja i wywoanie funkcji

5.1.1. Deklaracje funkcji


Deklaracja funkcji ma posta: typ nazwa(deklaracje argumentw); Wystpujce tutaj trzy elementy: typ zwracany, nazwa funkcji i wykaz argumentw nazywane s cznie prototypem funkcji. W jzyku C++ obowizuje zasada deklarowania prototypu kadej funkcji przed jej uyciem. Prototyp danej funkcji moe wystpi w tym samym programie wiele razy, natomiast brak prototypu funkcji wywoywanej w programie jest bdem syntaktycznym. Argumenty w deklaracji funkcji nazywa si rwnie argumentami formalnymi lub parametrami formalnymi. Wykonanie instrukcji deklaracji funkcji nie alokuje adnego obszaru pamici dla parametrw formalnych. Przykad 5.1. int f1(); void f3(); void f3(void); int* f5(int); int (*fp6) (const char*, const char*); extern double sqrt(double); extern char *strcpy(char *to, const char *from); extern int strlen(const char *st); extern int strcmp(const char *s1, const char *s2); int printf(const char *format, ...); Dyskusja. Warto w tym miejscu zaznaczy, e opuszczanie identyfikatora typu int jest dopuszczalne dla starszych wersji kompilatorw. Najnowsze ustalenia standardu ANSI/ISO dla jzyka C++ stanowi, e podobnie jak dla zmiennych i staych take typ zwracany funkcji musi by podawany jawnie (nie ma niejawnego int). Deklaracje void f3(); i void f3(void); s rwnowane; funkcja o nazwie f3 ma pust list argumentw i nie zwraca adnej wartoci do programu, w ktrym bdzie wywoywana (jest odpowiednikiem procedury bezparametrowej, uywanej np. w jzykach Pascal i Modula-2). Funkcja f5 przyjmuje jeden argument typu int i zwraca wskanik do typu int. Zapis int (*fp6) (const char*, const char*);

deklaruje wskanik fp6 do funkcji *fp6, ktra przyjmuje dwa wskaniki do staych typu char i zwraca warto typu int. Nawiasy w (*fp6) s konieczne dla poprawnego wizania czci skadowych zapisu, poniewa zapis bez nawiasw

int *fp6(const char*, const char*); mwi, e fp6 jest funkcj (a nie wskanikiem) zwracajc wskanik do int, podobnie jak f5. Omwione dotd deklaracje stwierdzay niejawnie, e definicje podanych prototypw funkcji s dostpne w plikach wchodzcych w skad programu. Inaczej mwic, definicje te s zewntrzne w stosunku do tej funkcji (np. main), z ktrej deklarowane funkcje bd woane. Poniewa w jzyku C++ funkcje nie mog by zagniedane, zatem kada funkcja jest zewntrzna w stosunku do pozostaych funkcji wchodzcych w skad programu. T zewntrzno definicji funkcji mona wyrazi jawnie, poprzedzajc prototyp funkcji sowem kluczowym extern. Cztery kolejne deklaracje korzystaj z tej wanie konwencji (poprzednie deklaracje miay specyfikator extern nadawany domylnie). Zauwamy, e trzy spord czterech zadeklarowanych ze sowem extern funkcji operuj na acuchach znakw: strcpy kopiuje acuch from do to, strlen zwraca dugo acucha, za strcmp zwraca 0 jeeli acuchy s rwne. Zauwamy te, e wymienione trzy funkcje zadeklarowano z nazwami argumentw formalnych (to, from, st, s1, s2). Nazwy te s opcjonalne, poniewa kompilator je pomija. Mog one jednak uatwi zrozumienie programu, szczeglnie gdy funkcje maj wiele argumentw. Znaczenie modyfikatora const w wykazie argumentw jest widoczne z kontekstu: zapobiega on zmianie argumentu wywoania funkcji tam, gdzie byoby to niepodane. Ostatni zapis: int printf(const char *format, ...); deklaruje funkcj typu int, ktr mona wywoywa ze zmieniajc si liczb i typami argumentw, co sygnalizuje kompilatorowi symbol .... Inaczej mwic, w wywoaniu musi wystpi co najmniej jeden argument typu char*. N.b. funkcja printf zadeklarowana w pliku stdio.h generuje formatowane wyjcie pod kontrol acucha formatujcego format, np. printf("Hej, jestem tutaj\n"); printf("Moje nazwisko : %s %s\n", nazwisko, imie); printf("Moja pensja : %d\n", pensja);

5.1.2. Wywoanie funkcji


Wywoanie funkcji jest poleceniem obliczenia wartoci wyraenia, zwracanej przez nazw funkcji. Instrukcja wywoania ma skadni: nazwa (argumenty aktualne); gdzie nazwa jest zadeklarowan wczeniej nazw funkcji, argumenty aktualne s wartociami argumentw formalnych, za para nawiasw okrgych () jest operatorem wywoania. Liczba, kolejno i typy argumentw aktualnych powinny dokadnie odpowiada zadeklarowanym argumentom formalnym. Przy niezgodnoci typw argumentw kompilator stara si wykona konwersje niejawne; jeeli nie moe dokona sensownej konwersji, sygnalizuje bd. Zastosowanie operatora wywoania do nazwy funkcji powoduje alokacj pamici dla argumentw formalnych i przekazanie im wartoci argumentw aktualnych. Od tej chwili argumenty formalne staj si zmiennymi lokalnymi o wartociach inicjalnych rwnych przesanym do nich wartociom argumentw aktualnych. Zasadniczym sposobem przekazywania argumentw do funkcji jest przekazywanie przez warto: do kadego argumentu formalnego jest przesyana kopia argumentu aktualnego. Poniewa argumenty formalne po wywoaniu staj si zmiennymi lokalnymi funkcji, zatem wszelkie wykonywane na nich operacje nie zmieniaj wartoci argumentw aktualnych. Wyjtkiem od tej zasady jest przesanie do funkcji adresw argumentw aktualnych za pomoc wskanikw lub referencji. Sytuacje, w ktrych jest to podane, omwimy pniej.

5.1.3. Definicja funkcji


Skadnia definicji funkcji jest nastpujca: typ nazwa (deklaracje argumentw) { instrukcje } Podobnie jak w deklaracji funkcji, definicja podaje typ zwracany przez funkcj, jej nazw oraz argumenty formalne. Argumentami formalnymi funkcji mog by zmienne wszystkich typw podstawowych, struktury, unie oraz wskaniki i referencje do tych typw, a take zmienne typw definiowanych przez uytkownika. Nie mog by nimi tablice, ale mog by wskaniki do tablic. Typ funkcji nie moe by typem tablicowym ani funkcyjnym, ale moe by wskanikiem do jednego z tych typw. Zarwno typ zwracany, jak i typy argumentw musz by podawane w postaci jednoznacznych identyfikatorw. Identyfikatory mog by nazwami typw wbudowanych (np. char, int, long int) lub zdefiniowanych wczeniej przez uytkownika. Inaczej mwic, w nagwku funkcji nie mog wystpowa definicje typw. Instrukcje w bloku (nazywanym rwnie ciaem funkcji) mog by instrukcjami deklaracji. Ostatni instrukcj przed nawiasem klamrowym zamykajcym blok funkcji musi by instrukcja return; jedynie dla funkcji typu void instrukcja return; jest opcj. Zatem definicja int f() { }; jest bdna, natomiast definicja void f() { }; jest poprawna. Instrukcja return; wystpuje czsto w postaci: return wyraenie;, gdzie wyraenie okrela warto zwracan przez funkcj. Jeeli typ tego wyraenia nie jest identyczny z typem funkcji, to kompilator bdzie prbowa osign zgodno typw drog niejawnych konwersji. Jeeli okae si to niemoliwe, to kompilacja zostanie zaniechana. Zgodno typu zwracanego z zadeklarowanym typem funkcji mona rwnie wymusi drog konwersji jawnej. Deklaracje argumentw musz podawa oddzielnie typ kadego argumentu; nazwy argumentw s opcjonalne. Definicja, ktra nie zawiera nazw argumentw, a jedynie ich typy, jest syntaktycznie poprawna. Oznacza ona, e argumenty formalne nie s uywane w bloku funkcji. Taka sytuacja moe wystpi wtedy, gdy przewidujemy wykorzystanie argumentw formalnych w bloku funkcji w przyszoci, a nie chcemy dopuci do zmiany postaci wywoania funkcji. W bloku funkcji moe wystpi wicej ni jedna instrukcja return;. Ilustruje to poniszy przykad.

Przykad 5.2. #include <iostream.h> //deklaracja funkcji - prototyp int dods(int, int); int main() { int i, j, k; cout << "Wprowadz dwie liczby typu int: "; cin >> i >> j; cout << '\n'; k = dods (i,j); //wywolanie funkcji cout << "i= " << i << "\tj= " << j << '\n'; cout << "dods(i,j)= " << k << '\n'; return 0; } int dods (int n, int m) { if (n + m > 10) return n + m; else return n; }

5.2.

Przekazywanie argumentw

Wywoanie funkcji zawiesza wykonanie funkcji woajcej i powoduje zapamitanie adresu nastpnej instrukcji do wykonania po powrocie z funkcji woanej. Adres ten, nazywany adresem powrotnym, zostaje umieszczony w pamici na stosie programu (ang. run-time stack). Wywoana funkcja otrzymuje wydzielony obszar pamici na stosie programu, nazywany rekordem aktywacji lub stosem funkcji. W rekordzie aktywacji zostaj umieszczone argumenty formalne, inicjowane jawnie w deklaracji funkcji, lub niejawnie przez wartoci argumentw aktualnych. Jawne inicjowanie argumentw w deklaracji (nie w definicji!) funkcji mona traktowa jako przykad przecienia funkcji; tematem tym zajmiemy si pniej bardziej szczegowo. Wemy nastpujcy przykad: w prototypie funkcji, ktra symuluje ekran monitora, wprowadmy inicjalne wartoci domylne dla szerokoci, wysokoci i ta ekranu char* ekran(int x=80, int y=24, char bg = ' '); Wprowadzone wartoci pocztkowe argumentw x, y oraz bg s domylne w tym sensie, e jeeli w wywoaniu funkcji nie podamy argumentu aktualnego, to na stosie funkcji zostanie pooona warto domylna argumentu formalnego. Jeeli teraz zadeklarujemy zmienn char* kursor, to wywoanie kursor = ekran(); jest rwnowane wywoaniu kursor = ekran(80, 24, ' '); Jeeli w wywoaniu podamy inn od domylnej warto argumentu, to zastpi ona warto domyln, np. wywoanie kursor = ekran(132); jest rwnowane wywoaniu kursor = ekran(132, 24, ' '); za wywoanie kursor = ekran(132, 66); jest rwnowane wywoaniu kursor = ekran(132, 66, ' ');

Skadnia ostatniego wywoania pokazuje e nie mona poda wartoci pierwszego z prawej argumentu nie podajc wszystkich wartoci po lewej. Deklaracja funkcji nie musi zawiera wartoci domylnych dla wszystkich argumentw, ale podawane wartoci musz si zaczyna od skrajnego prawego argumentu, np. char* ekran( int x, int y, char bg = ' ' ); char* ekran( int x, int y = 24, bg = ' ' ); Wobec tego deklaracje char* ekran (int x = 80, int y, char bg = ' '); char* ekran ( int x, int y = 24, char bg ); s bdne.

5.2.1. Przekazywanie argumentw przez warto


W jzyku C++ przekazywanie argumentw przez warto jest domylnym mechanizmem jzykowym. Przy braku takiego mechanizmu kada zmiana wartoci argumentu formalnego nie poprzedzonego modyfikatorem const wywoaaby tak sam zmian argumentu aktualnego. Zaoenie to zostao podyktowane tym, e ewentualne zmiany wartoci argumentw aktualnych, bdce wynikiem wykonania jakiej funkcji, s na og traktowane jako niepodane efekty uboczne. Przekazywanie argumentw przez warto pokazano ju w ostatnim przykadzie. Przykad pokazany niej ilustruje znaczenie prototypu oraz niejawnej konwersji przy wywoywaniu funkcji. Przykad 5.3. #include <iostream.h> int imax(int x, int y); int main() { float zf = 35.7; double zd = 11.0; int ii; ii = imax( zf, zd ); cout << "ii = " << ii << endl; return 0; } int imax(int x, int y) { if (x > y) return x; else return y; } Dyskusja. Poniewa funkcja imax() ma prototyp z parametrami typu int, to wykonanie operacji wywoania rozpocznie si od dwch niejawnych konwersji zmiennych zf i zd do typu int. Po przeprowadzeniu konwersji zmienne te zostan pooone na stos funkcji imax. Przekazywanie argumentw przez warto nie bdzie dogodnym mechanizmem w dwch przypadkach: 1. gdy wartoci argumentu aktualnego musz by przez funkcj zmodyfikowane,

2. gdy przekazywany argument reprezentuje duy obszar pamici (np. tablica, struktura). Dla tablic mamy na szczcie mechanizm, ktry nakazuje kompilatorowi przekazanie argumentw aktualnych w postaci adresu pierwszego elementu tablicy zamiast kopii caej tablicy. Natomiast w pierwszym przypadku naley znale wasne rozwizanie. Klasycznym przykadem nieprzydatnoci przekazywania argumentw przez warto jest operacja zamiany wartoci dwch zmiennych. Funkcja void { int y = x = } zamiana(int x, int y) pomoc = y; x; pomoc;

zamieni miejscami wartoci x oraz y, ktre po wywoaniu stan si lokalnymi kopiami wartoci argumentw aktualnych. Niestety nie zdoamy tych zamienionych wartoci przesa do funkcji woajcej.

5.2.2. Przekazywanie argumentw przez adres


Podniesione w kocowej czci p. 5.2.1 niedostatki przekazywania argumentw przez warto mona przezwyciy dwoma sposobami. Pierwszy z nich polega na zastosowaniu wskanikw. Sposb ten ilustruje poniszy przykad. Przykad 5.4. #include <iostream.h> void zamiana1 (int*, int* ); int main() { int i = 10; int j = 20; zamiana1( &i, &j ); cout << "Po zamianie i=" << i << "\tj=" << j << endl; return 0; } void zamiana1(int* x, int* y) { int pomoc = *y; *y = *x; *x = pomoc; } Dyskusja. Powyszy program ze wskanikow wersj argumentw funkcji zamiana1 daje nastpujcy wydruk: Po zamianie i=20 j=10

Otrzymujemy zatem wynik, o ktry nam chodzio, ale program wydaje si by mao czytelny. Alternatywne rozwizanie tego samego zadania wykorzystuje referencje zamiast wskanikw, dajc prostsz i bardziej czyteln notacj.

Przykad 5.5. #include <iostream.h> void zamiana2 (int& i, int& j ); int main() { int i = 10; int j = 20; zamiana2( i, j ); cout << "Po zamianie i=" << i << "\tj= " << j << endl; return 0; } void zamiana2(int& x, int& y) { int pomoc = y; y = x; x = pomoc; }

5.3.

Komunikacja funkcji main z otoczeniem

Kady program w C++ musi mie dokadnie jedn funkcj o nazwie main i kady program zaczyna si wykonywa od wywoania tej funkcji. Funkcja main nie jest predefiniowana przez kompilator, nie moe by przeciana, a jej typ jest zaleny od implementacji. W wersji wzorcowej jzyka zaleca si dwie nastpujce definicje funkcji main: int main() { /* ... */ } lub int main(int argc, char *argv[]) { /* ... */ } przy czym dopuszcza si moliwo dodawania dalszych argumentw po argv[]. W drugiej postaci funkcji argument argc oznacza liczb parametrw przekazywanych do programu z otoczenia, w ktrym program jest uruchamiany. Parametry te s przekazywane jako zakoczone znakiem '\0' acuchy znakw argv[]. Tak wic do parametru formalnego argv[0] jest przekazywany pierwszy acuch znakw, a do parametru argv[argc - 1] ostatni acuch. Parametr argv[0] jest nazw uywan przy wywoaniu programu z wiersza rozkazowego. Poniewa argc jest liczb przekazywanych parametrw, to argv[argc]==0. Zgodnie z terminologi przyjt dla acuchw znakw, typem argv jest char*[argc + 1]. Funkcja main nie moe by wywoywana w obrbie programu; nie jest moliwe odwoanie do adresu main; funkcja main nie moe by deklarowana ze sowem kluczowym inline lub static. Zamy, e wiersz rozkazowy wyglda nastpujco: prog1 17.5 22.9

gdzie prog1 jest nazw pliku, w ktrym umieszczony jest kod binarny naszego programu, a cigi znakw 17.5 i 22.9 s danymi, potrzebnymi dla wykonania programu. Wwczas parametry funkcji main bd nastpujce: argc argv[0] argv[1] argv[2] 3 "prog1" "17.5" "22.9"

argv[3]

Podawane w wierszu rozkazowym parametry s czsto nazwami plikw, na ktrych ma operowa nasz program. Zamy, e kod binarny programu kopiujcego pliki jest umieszczony w pliku prog2. Parametrami, ktre naley poda, s: nazwa pliku kopiowanego plik1 i nazwa kopii plik2. Wwczas napiszemy nastpujcy wiersz rozkazowy: prog2 plik1 plik2

Przykad 5.6. #include <iostream.h> #include <stdlib.h> int main(int argc, char *argv[]) { if ( argc != 6 ) { cerr << "Niepoprawna liczba parametrow\n"; exit ( 1); } int i ; cout << "Wartosc argc wynosi: " << argc << endl << endl; cout << "Przekazano " << argc << " argumentow do main: " << endl << endl; for (i = 0; i < argc; i++) cout << " argv[" << i << "]: " << argv[i] << endl; return 0; /* Unix, Solaris 2.4, plik p81.cc, kompilator CC lub g++ : Po wykonaniu polecenia: CC p81.cc -o p81 wywolaj P81 z linii rozkazowej: P81 arg1 arg2 arg3 arg4 arg5 (6 argumentow, wliczajac nazwe programu) i zanotuj wynik */ /* MS-DOS, Borlandc, plik P81.CPP, kompilator BCC: Po uzyskaniu pliku P81.exe wywolaj P81 z linii rozkazowej: P81 arg1 arg2 arg3 arg4 arg5 (6 argumentow, wliczajac nazwe programu) i zanotuj wynik */ } Posta wydruku: Wartosc argc wynosi: 6 Przekazano 6 argumentow do main: argv[0]: p82 argv[1]: arg1 argv[2]: arg2 argv[3]: arg3 argv[4]: arg4 argv[5]: arg5

( Pod DOS, zamiast argv[0]: p81, byoby: argv[0]: C:\BORLANDC\P81.EXE). Dyskusja. W programie umieszczono dyrektyw wczenia pliku nagwkowego stdlib.h, ktry zawiera deklaracj funkcji exit(). Instrukcja if zabezpiecza nas przed podaniem niewaciwej liczby parametrw, przekazywanych do programu. Standardowy strumie cerr jest zadeklarowany, podobnie jak cin i cout, w pliku nagwkowym iostream.h. Gdybymy wywoali program p82 z inn ni 6 liczb parametrw, to wykonanie programu zostanie zaniechane, a operator wyjcia << wyprowadzi na ekran napis: Niepoprawna liczba parametrow Zauwamy ponadto, e podawane w wierszu rozkazowym parametry s niepodzielnymi cigami znakw (sowami), oddzielonymi jedn lub kilku spacjami. Gdyby program wymaga podania parametru w postaci kilku sw oddzielonych spacjami, to taki parametr naley uj w podwjne apostrofy, np. prog3 "argument ze spacjami" arg2 arg3

Wspomniana wyej moliwo dodawania dalszych argumentw po argv[] moe by wykorzystana do uzyskania informacji o zmiennych rodowiska, nazywanych rwnie acuchami otoczenia. W takim przypadku nagwek funkcji main bdzie mia posta: int main(int argc, char *argv[], char *env[]) a wartoci zmiennych rodowiska moemy wywietli sekwencj instrukcji cout << endl <<"Lancuchy otoczenia w tym systemie:\n\n"; for (i = 0; env[i] != NULL; i++) cout << " env[" << i << "]: " << env[i] << endl;

5.4.

Funkcje rozwijalne

Modularyzacja programw prowadzi do definiowania wymaganych operacji w postaci definicji funkcji, czsto bardzo prostych, zawierajcych jedn lub kilka instrukcji. Jeeli takie funkcje s czsto wywoywane, to narzut czasowy na wywoanie i powrt staje si znaczny i moe obniy efektywno programu. Narzut ten obejmuje m. in. czas potrzebny na alokacj pamici dla argumentw i zmiennych lokalnych woanej funkcji i czas na skopiowanie argumentw aktualnych do przydzielonych miejsc pamici. W przypadku, gdy funkcja wykonuje zoone zadania, czas na wykonanie i powrt bdzie may w porwnaniu do czasu oblicze. Natomiast gdy funkcja wykonuje jedn lub dwie instrukcje, narzut czasowy na wywoanie i powrt moe stanowi znaczny procent w stosunku do czasu oblicze. Definicj takich krtkich funkcji moemy poprzedzi sowem kluczowym inline. Specyfikator inline jest wskazwk (nie poleceniem!) dla kompilatora, aby kade wywoanie takiej funkcji stara si zastpi instrukcjami, ktre definiuj funkcj. W wyniku eliminacji wielu wywoa i powrotw otrzymuje si program szybszy, ale zajmujcy wikszy obszar pamici, poniewa instrukcje, ktre definiuj funkcj, s powielane przez kompilator w kadym punkcie, gdzie funkcja jest wywoywana. Jednym z typowych przykadw funkcji, definiowanej jako rozwijalna, moe by funkcja, ktrej jedyn instrukcj jest wywoanie innej funkcji. Inne przykady takich funkcji podano niej.

inline int abs(int i) { return i < 0 ? -i : i; } inline int min( int a, int b) { return a < b ? a : b; } inline int podzielne(int i, int j) { return i%j; } inline double pole(double a, double b) {return a*b;} inline void f() { char z = '\0';if(cin >> z && z != 'q') f();} Przykad 5.7. #include <iostream.h> inline int podzielne(int n, int m) { return !(n%m); } int main() { int i,j; int k; cin >> i >> j ; k = podzielne(i,j); cout << "k= " << k << '\n'; return 0; } Przykad 5.8. #include <iostream.h> inline void f() { char z= '\0'; if(cin >> z && z !='q') f(); } int main() { f(); return 0; } Dyskusja. Zdefiniowana w tym krtkim programie funkcja f() moe by wygodn wstawk w duszych programach, w ktrych chcemy uzaleni wyjcie z programu od wprowadzenia okrelonego znaku z klawiatury (tutaj znak 'q'). Przykad 5.9. #include <iostream.h> inline int parzyste(int n) {return !(n%2); } int main() { if(parzyste(10)) cout << "Liczba 10 jest parzysta\n"; else if(parzyste(11)) cout << "Liczba 11 jest parzysta\n"; return 0; } Dyskusja. W tym przykadzie funkcja parzyste(), ktra zwraca prawd (warto rn od zera) jeli jej argument jest parzysty, zostaa zdefiniowana jako rozwijalna. Oznacza to, e instrukcja

if(parzyste(10)) cout << "Liczba 10 jest parzysta\n"; jest funkcjonalnie rwnowana instrukcji if(!(10%2)) cout << "Liczba 10 jest parzysta\n"; Zauwamy, e definicja funkcji rozwijalnej zostaa umieszczona przed funkcj main(). Jest to konieczne, gdy inaczej kompilator nie mgby si dowiedzie, jak ma wyglda rozwinicie parzyste(10) w instrukcji if. Zwrmy jeszcze uwag na fakt, e jeeli zmienimy definicj funkcji rozwijalnej, to wszystkie wywoania musz by powtrnie kompilowane. Jak ju wspomniano, poprzedzenie definicji funkcji rozwijalnej sowem kluczowym inline jest jedynie yczeniem programisty, aby kompilator potraktowa j jako funkcj rozwijaln. Kompilator zignoruje to yczenie i wywoa j dopiero w fazie wykonania programu, jeeli funkcja zdefiniowana z inline: zawiera zbyt wiele instrukcji jest funkcj rekurencyjn jest wywoywana przed jej definicj jest wywoywana dwa lub wicej razy w tym samym wyraeniu zawiera ptl, instrukcj switch, lub goto jest wywoywana przez wskanik.

F Uwaga. W jzyku C++ istnieje moliwo niejawnego (bez inline) definiowania funkcji rozwijalnych, wykorzystywana w szczeglnoci w bibliotekach klas. Gdyby rozwijanie funkcji nie byo moliwe, to teksty rdowe wielu prostych, ale wanych i czsto uywanych funkcji bibliotecznych byyby niedostpne dla kompilatora.

5.5.

Funkcje rekurencyjne

Funkcja, ktra wywouje sama siebie, bezporednio lub porednio, jest nazywana rekurencyjn. Kade kolejne wywoanie funkcji powoduje utworzenie nowej kopii jej argumentw i zmiennych lokalnych (automatycznych). Funkcje rekurencyjne musz zawsze zawiera warunek stopu (zatrzymania). W przeciwnym razie funkcja bdzie wywoywa sama siebie bez koca, tworzc ptl nieskoczon. Klasycznym przykadem rekurencji jest definicja silni n! = 1 * 2 * 3 * ... * (n - 1) * n ktr mona zapisa w rwnowanej postaci rekurencyjnej 1 n! = n*(n-1)! dla n > 0 Podane niej dwa przykady pokazuj iteracyjn i rekurencyjn wersj silni, za rysunek 5-1 ilustruje mechanizm wywoa dla n rwnego 3. dla n = 0

Przykad 5.10. //Iteracyjne obliczanie silni #include <iostream.h> long int sil(long int n); int main() { long int j = 3; long int k = sil(j); cout << "sil(" << j << ") =" << k << endl; return 0; } long int sil(long int n) { long int pomoc = 1; for ( int i = 1; i <= n; i++) pomoc = pomoc * i; return n == 1 ? n : pomoc; } Przykad 5.11. // Rekurencyjne obliczanie silni #include <iostream.h> long int silnia(long int n); int main() { long int j = 3; long int k = silnia(j); cout << "silnia(" << j << ") =" << k << endl; return 0;

} long int silnia(long int n) { if ( n > 0) return n * silnia(n - 1); else return 1; } Dyskusja. Wywoanie funkcji iteracyjnej sil(long int n) ma miejsce tylko jeden raz. Na czas wykonania funkcji zostaje zawieszone wykonanie funkcji woajcej. Po powrocie wykonywana jest nastpna, zapamitana na stosie, instrukcja funkcji woajcej. Wykonanie funkcji rekurencyjnej silnia(long int n) przebiega nastpujco: 1. Wywoanie o postaci long int k = silnia(3); zawiesza wykonanie funkcji woajcej z jednoczesnym zapamitaniem adresu nastpnej do wykonania instrukcji funkcji woajcej. Kopia argumentu aktualnego (tj. warto 3) zostanie przekazana do funkcji woanej. 2. Po sprawdzeniu, e n>0, wykonanie funkcji silnia(3) zostanie zawieszone, poniewa prba wykonania instrukcji return 3*silnia(2); spowoduje wywoanie kopii silnia(2). W rezultacie na stosie funkcji silnia (nazywanym stosem rekursji) zostan umieszczone kolejno: kopia funkcji silnia(3) i adres powrotny do silnia(3). 3. Wykonanie silnia(2) rwnie zostanie zawieszone w rezultacie wywoania silnia(1) w instrukcji return. silnia(2) oraz adres powrotny zostan pooone na stos rekursji. 4. silnia(1) wywoa silnia(0), tj. wykonaln kopi funkcji. Przed wykonaniem

silnia(0) funkcja silnia(1) oraz adres powrotny zostan umieszczone na stosie rekursji. 5. W rezultacie wywoania silnia(0) zostanie wykonana instrukcja return 1. Teraz moliwy jest powrt, reprezentowany przez dolne uki na rys. 5-1(b), tj. wykonanie kolejnych instrukcji return i przypisanie ich wartoci wyraeniom silnia(1), silnia(2) i silnia(3).

sil(3) a) funkcja woajca funkcja woajca

funkcja woajca silnia(3) funkcja b) woajca silnia(2) silnia(1) silnia(0)

Rys. 5-1 Wywoanie funkcji: (a) nierekurencyjnej, (b) rekurencyjnej Podobnie konstruuje si funkcj, ktra znajduje najwikszy wsplny podzielnik dwch liczb cakowitych. Podane niej przykady prezentuj iteracyjn gcd i rekurencyjn rgcd wersj takiej funkcji. Przykad 5.12. // gcd - greatest common denominator // wersja iteracyjna #include <iostream.h> int gcd ( int, int ); int main() { int i = 24; int j = 32; int k = gcd ( i, j ); cout << k << endl; return 0; } int gcd (int x, int y) { int temp; while ( y ) { temp = y; y = x % y; x = temp; } return x; }

Przykad 5.13. // rgcd - greatest common denominator // wersja rekurencyjna #include <iostream.h> int rgcd ( int, int ); int main() { int i = 24; int j = 32; int k = rgcd ( i, j ); cout << k << endl; return 0;

} int rgcd (int x, int y) { if (y == 0) return x; return rgcd (y, x % y); } Kolejny przykad prezentuje funkcj long int cyfra(long int n, long int k), ktra generuje warto k-tej cyfry dziesitnej liczby cakowitej n, liczc od prawej, tj. od najmniej znaczcej cyfry. Przykadowe wywoanie cyfra(28491,2); zwrci warto 2, a cyfra(4788,5); zwrci warto 0. Przykad 5.14. #include <iostream.h> long int cyfra(long int n, long int k); int main() { long int i = 1234; long int j = 4; long int m = cyfra(i, j); cout << "cyfra(" << i << "," << j << ") =" << m << endl; return 0;

} long int cyfra(long int n, long int k) { if ( k == 0) return 0; else if( k == 1) return n % 10; else return cyfra(n / 10, k - 1); } Nastpny przykad ilustruje jedn z moliwoci przeksztacenia dziesitnego zapisu liczby cakowitej na zapis binarny. Funkcja zapisbinarny jest typow funkcj rozwijaln, za funkcja drukujbity rekurencyjn. Obydwie funkcje s typu void.

Przykad 5.15. #include <iostream.h> void drukujbity (int n); void zapisbinarny ( int m); int main() { int j = 15; zapisbinarny(j); cout << " jest zapisem binarnym liczby " << j ; cout << '\n'; return 0; } void drukujbity (int iloraz) { if ((iloraz / 2) != 0) drukujbity (iloraz / 2); cout << iloraz % 2; } void zapisbinarny (int n) { drukujbity (n); }

5.6.

Funkcje w programach wieloplikowych

Kody rdowe wikszych programw umieszcza si zwykle w kilku plikach. Jest to racjonalny sposb postpowania, poniewa pliki skadowe programu mona oddzielnie sprawdza, oddzielnie kompilowa i dopiero po tych operacjach scali w jeden binarny kod wykonalny. Poniewa w tej pracy przyjto zaoenie, e wszystkie koncepcje bd ilustrowane raczej krtkimi programami, zatem i konstruowanie, a nastpnie przetwarzanie programu wieloplikowego pokaemy na prostym przykadzie. Dla ustalenia uwagi zamy, e naszym zadaniem jest posortowanie w kolejnoci od najmniejszej do najwikszej cigu liczb typu double. Sortowanie przeprowadzimy za pomoc algorytmu zamiany parami. W algorytmie tym bada si (przeglda) pary ssiadujcych ze sob liczb i ewentualnie zamienia je miejscami. Schemat dziaania algorytmu ilustruje tablica 5.1. Tablica 5.1. Schemat algorytmu zamiany parami 4 zamiany tab[0] tab[1] tab[2] tab[3] tab[4] 7.8 5.4 2.5 1.6 3.9 5.4 7.8 2.5 7.8 1.6 7.8 3.9 7.8 5.4 2.5 1.6 3.9 7.8 2.5 5.4 1.6 5.4 3.9 5.4 3 zamiany 2.5 1.6 3.9 5.4 7.8 1 zamiana 1.6 2.5 1.6 2.5 3.9 5.4 7.8

stan pocztkowy

stan po pierwszym przejrzeniu

stan po drugim przejrzeniu

stan kocowy

par

par

Implementacja algorytmu w postaci kodu rdowego programu niestrukturalnego, tj. bez wydzielonych funkcji, moe wyglda jak w poniszym przykadzie. Przykad 5.16. // Sortowanie - jeden plik // Pod DOS - SORT.CPP, pod Unix - sort.c #include <iostream.h> const int rozmiar = 5; double tab[rozmiar]; int main() { //Czytanie cout<<"Podaj "<<rozmiar<<" liczb typu double:\n"; for ( int i = 0; i < rozmiar; i++) cin >> tab[i]; cout << "Tablica przed sortowaniem: \n"; //Pisanie for (i = 0; i < rozmiar; i++) cout<<"tab["<< i << "] : "<<tab[i]<<endl; //Sortowanie int licznik; double pomoc; do { licznik = 0; for ( i = 0; i < rozmiar - 1; i++) { if ( tab[i] > tab[i+1]) { pomoc = tab[i]; tab[i] = tab[i+1]; tab[i+1] = pomoc; ++licznik; } } } while (licznik); cout << "Tablica po sortowaniu: \n"; //Ponownie pisanie for (i = 0; i < rozmiar; i++) cout<<"tab["<<i<<"] : "<<tab[i]<<endl; return 0; } Dyskusja. W programie zadeklarowano zmienn licznik, ktra jest licznikiem zamian w kolejnych przegldach par; zmienna pomoc suy do chwilowego przechowywania wartoci ab[i] w kolejnych zamianach. Sortowanie jest wykonywane w ptli do-while. Postpujc metodycznie, wydzielimy teraz operacje czytania, sortowania i pisania w oddzielne funkcje. Przeksztacony w ten sposb program pokazano w kolejnym przykadzie. Przykad 5.17.

// Sortowanie - jeden plik, wydzielone funkcje // Pod DOS - SORT1.CPP, pod Unix - sort1.c #include <iostream.h> void czytaj(double *tc, int rc); void pisz(double *tp, int rp); void sortuj(double *ts, int rs); const int rozmiar = 5; double tab[rozmiar]; int main() { cout << "Podaj " << rozmiar << " liczb typu double:\n"; czytaj(tab, rozmiar); pisz(tab, rozmiar); sortuj(tab,rozmiar); pisz(tab, rozmiar); return 0; } void czytaj(double *tc, int rc) { for (int i = 0; i < rc; i++) cin >> tc[i]; cout << endl; cout << "Tablica przed sortowaniem: " << endl; } void pisz(double *tp, int rp) { for (int i = 0; i < rp; i++) cout << "tp[" << i << "] = " << tp[i] << endl; } void sortuj(double *ts, int rs) { int licznik,i; double pomoc; do { licznik = 0; for ( i = 0; i < rs - 1; i++) { if ( ts[i] > ts[i+1]) { pomoc = ts[i]; ts[i] = ts[i+1]; ts[i+1] = pomoc; ++licznik; } } } while (licznik); cout << "Tablica po sortowaniu: \n"; }

Dalszy tok postpowania zaley od dostpnego rodowiska programowego. Dla ustalenia uwagi zaoymy, e dostpnym rodowiskiem programowym jest C++ firmy Borland pod systemem MSDOS, bd kompilator-konsolidator CC pod systemem Unix. Przy tych zaoeniach kolejnym krokiem moe by umieszczenie definicji funkcji czytaj(), sortuj(), pisz() oraz main() w oddzielnych plikach. Plikom tym moemy np. nada nazwy CZYTAJ.CPP, SORTUJ.CPP, PISZ.CPP i MAIN.CPP dla kompilatora Borland C++ pod MS-DOS, lub czytaj.c, sortuj.c, pisz.c i main.c dla kompilatora CC pod systemem Unix. Nastpnie pliki rdowe moemy oddzielnie skompilowa odpowiednimi poleceniami: bcc - c nazwa-pliku pod MS-DOS lub CC -c nazwa-pliku pod systemem Unix. Podanie opcji -c oznacza, e kompilator nie wywoa automatycznie konsolidatora. Zauwamy, e kada z trzech definicji funkcji czytaj(), sortuj() i pisz() wykorzystuje strumie cout, a ponadto funkcja czytaj() pobiera znaki ze strumienia cin. Zatem warunkiem powodzenia kompilacji bdzie dopisanie w kadym z plikw rdowych dyrektywy #include <iostream.h>. W wyniku kompilacji otrzymamy nieskonsolidowane pliki tymczasowe: CZYTAJ.OBJ, itd., pod systemem MS-DOS lub czytaj.o, itd., pod systemem Unix. Kolejnym krokiem bdzie scalenie plikw tymczasowych. Polecenie konsolidacji moe mie posta: bcc main.obj czytaj.obj sortuj.obj pisz.obj

pod systemem MS-DOS, lub CC main.o czytaj.o sortuj.o pisz.o

pod systemem Unix. W wyniku konsolidacji otrzymamy binarny kod wykonalny programu (plik MAIN.EXE pod MSDOS lub a.out pod systemem Unix). Kod wykonalny moemy rwnie otrzyma za pomoc jednego polecenia, np. bcc main.cpp czytaj.cpp sortuj.cpp pisz.cpp

dla kompilatora bcc, lub CC main.c czytaj.c sortuj.c pisz.c

dla kompilatora CC. Moliwa jest rwnie kompilacja pliku rdowego, np. main.c (MAIN.CPP) z nastpujc po niej konsolidacj plikw tymczasowych. Przykadowy wiersz rozkazowy moe mie posta: CC main.c czytaj.o sortuj.o pisz.o

Programista ma rwnie moliwo utworzenia wasnych plikw bibliotecznych. Jeeli mamy ju pliki tymczasowe (.o lub .OBJ) dla przykadowych trzech funkcji czytaj(), sortuj() i pisz(), to moemy z nich utworzy wasn bibliotek. W systemie Borland C++ wykorzystamy do tego celu program TLIB.EXE. Po wykonaniu polecenia:

tlib

biblsort +czytaj

+ sortuj

+pisz

zostanie utworzony plik biblioteczny BIBLSORT.LIB, ktry moemy konsolidowa z dowolnym programem, zawierajcym prototypy i wywoania tych trzech funkcji, np. bcc main.cpp biblsort Pod systemem Unix bibliotek moemy utworzy poleceniem ar (za archiwum) z opcj -r: ar -r biblsort.a czytaj.o sortuj.o pisz.o

Utworzony w ten sposb plik biblioteczny biblsort.a konsoliduje si z funkcj main() w analogiczny sposb, jak dla kompilatora Borland C++: CC main.c biblsort.a

F Uwaga. W niektrych wersjach systemu Unix istnieje polecenie ranlib, ktre mona zastosowa do naszego pliku bibliotecznego biblsort.a piszc: ranlib biblsort.a. Wykonanie polecenia indeksuje plik biblioteczny, dziki czemu uzyskuje si szybszy dostp do jego skadowych. Jeeli system nie zawiera polecenia ranlib, to prawdopodobnie nie jest ono potrzebne.
Mona w tym miejscu zapyta: jak korzy (poza oczywistym skrceniem wiersza rozkazowego) uzyskuje si z tworzenia bibliotek, zamiast bezporedniego wyliczenia wszystkich scalanych plikw tymczasowych? Nasz przykad jest zbyt prosty, aby ukaza jakie korzyci. Gdyby jednak funkcja main() wywoywaa funkcj sortuj(), a ta z kolei funkcj pisz(), to polecenie konsolidacji CC main.c sortuj.o nie zostanie wykonane, poniewa konsolidator nie znajdzie pliku pisz.o. Natomiast gdy posiadamy bibliotek biblsort.a, to konsolidator wie jak ze zbioru wszystkich plikw .o wycign tylko te, ktre s potrzebne. Tak wic biblioteka moe w oglnoci zawiera wiele definicji funkcji i zmiennych, uywanych przez umieszczone w niej funkcje, a niewidocznych dla uytkownika. Program uytkowy, ktry odwouje si do takiej biblioteki, bdzie zawiera minimaln liczb doczonych definicji.

5.6.1. Pliki nagwkowe i funkcje


Wydawa by si mogo, e oto mamy recept na konstruowanie programw wieloplikowych, w ktrych zapewniono integralno danych i niezawodne wywoania potrzebnych funkcji. Tak jednak nie jest, o czym atwo si przekona na naszym prostym przykadzie. Zauwamy przede wszystkim, e po zaoeniu biblioteki jedynymi informacjami dotyczcymi sposobu wywoania funkcji bibliotecznych bd prototypy tych funkcji, zadeklarowane przed blokiem main(). Podczas kompilacji wywoania z bloku bd sprawdzane na zgodno z prototypami. Ewentualna pomyka w ktrej z tych deklaracji, bd przypadkowe jej usunicie bdzie kadorazowo zwizane z dodatkowymi operacjami wycigania definicji funkcji z biblioteki. Po drugie, przy pracy zespoowej nad duym systemem moe si zdarzy, e implementacja ktrej z naszych funkcji zostanie zmieniona wcznie z nagwkiem, okrelajcym sposb jej wywoania. Jeeli plik z t zmienion definicj zostanie ponownie skompilowany i doczony do biblioteki, to prototyp tej funkcji w pliku main.c (MAIN.CPP) przestanie by aktualny... Nie bez znaczenia jest rwnie fakt, e dla oddzielnego kompilowania naszych funkcji czytaj(), sortuj() i pisz() musielimy dopisa w kadym pliku rdowym dyrektyw #include <iostream.h>, zwikszajc objto tekstu. Dostatecznie pewnym, a przy tym prostym remedium na wyliczone niedostatki jest uzupenienie naszego programu o wasny plik nagwkowy, np. o nazwie sorth.h. W pliku tym umiecimy

deklaracje kadej nazwy, uywanej wicej ni w jednym pliku rdowym. Jeeli tak przygotowany plik nagwkowy wczymy dyrektyw #include do kadego pliku rdowego, to plik ten bdzie spenia rol interfejsu pomidzy wchodzcymi w skad programu jednostkami kompilacji (plikami z kodem rdowym). Prezentowany niej przykad jest prb realizacji tej koncepcji. Przykad 5.18. // Plik CZYTAJ.CPP pod DOS, czytaj.c pod Unix #include "sorth.h" void czytaj(double *tc, int rc) { for (int i = 0; i < rc; i++) cin >> tc[i]; cout << endl; cout << "Tablica przed sortowaniem: " << endl; } // Plik SORTUJ.CPP pod DOS, sortuj.c pod Unix #include "sorth.h" void sortuj(double *ts, int rs) { int licznik,i; double pomoc; do { licznik = 0; for ( i = 0; i < rs - 1; i++) { if ( ts[i] > ts[i+1]) { pomoc = ts[i]; ts[i] = ts[i+1]; ts[i+1] = pomoc; ++licznik; } } } while (licznik); cout << "Tablica po sortowaniu: \n"; } // Plik PISZ.CPP pod DOS, pisz.c pod Unix #include "sorth.h" void pisz(double *tp, int rp) { for (int i = 0; i < rp; i++) cout << "tp[" << i << "] = " << tp[i] << endl; } // Plik MAIN.CPP pod DOS, main.c pod Unix #include "sorth.h" //extern void czytaj(double *tc, int rc); //extern void pisz(double *tp, int rp);

//extern void sortuj(double *ts, int rs); const int rozmiar = 5; double tab[rozmiar]; int main() { cout << "Podaj " << rozmiar << " liczb typu double:\n"; czytaj(tab, rozmiar); pisz(tab, rozmiar); sortuj(tab,rozmiar); pisz(tab, rozmiar); return 0; } // Plik SORTH.H pod DOS, sorth.h pod Unix #include <iostream.h> extern void czytaj(double *tc, int rc); extern void pisz(double *tp, int rp); extern void sortuj(double *ts, int rs); Programy wieloplikowe mog si skada z wicej ni jednego pliku nagwkowego. Naley jednak by ostronym w mnoeniu plikw nagwkowych, poniewa w pewnym momencie moe si okaza, e ju nie konsolidator, lecz programista zacznie mie trudnoci z decyzj, ktre pliki nagwkowe naley wczy do poszczeglnych plikw rdowych. Nie mona tu poda jakiej oglnej reguy, poniewa zarwno korzystanie z plikw nagwkowych, jak te ich liczba, zale od stylu programowania. Mona natomiast poda oglne wskazwki, odnoszce si do zawartoci plikw nagwkowych. W plikach nagwkowych nie naley umieszcza: Definicji zwykych funkcji, np. char get(){ return *wsk++; } Definicji zmiennych, np. int i; Definicji zoonych struktur staych, np. const int tab[] = { /*...*/ }; Z drugiej strony, plik nagwkowy moe zawiera: Dyrektywy inkluzji, np. #include < string.h> Deklaracje funkcji, np. extern int strlen( const char*); Deklaracje zmiennych, np. extern int i; Definicje staych, np. const double pi = 3.1415926; Wyliczenia, np. Bool { false, true }; Definicje typw, np. struct punkt { int i,j; }; Deklaracje nazw, np. class wektor; Szablony, np. template<class T> class A { /* ... */ }; Komentarze, np. /* Ten plik jest interfejsem dla ... */

5.7. Wskaniki do funkcji


Jak nietrudno zauway, po zdefiniowaniu funkcji moemy na niej wykonywa zaledwie jedn operacj, ktr jest wywoanie funkcji. Definiujc wskaniki do funkcji, gwatownie rozszerzamy repertuar moliwych operacji: Wskaniki do funkcji, podobnie jak wskaniki do zmiennych, pozwalaj uzyska adres funkcji. Wskaniki do funkcji mog by argumentami formalnymi funkcji i wartociami zwracanymi przez funkcje; mog by przekazywane do funkcji jako argumenty aktualne w wywoaniach. Wskaniki do funkcji mona uywa w instrukcjach przypisania; mona je rwnie umieszcza w tablicach. Ostatnia moliwo jest wykorzystywana np. w systemach okienkowych, bazujcych na X-Window (Unix) i MS-Windows (DOS). Jeeli tworzymy system okienkowy z

rozwijalnymi menu, to jest naturalne, e kolejne pozycje menu powinny by uoone w tablic. Poniewa pozycje te s odwoaniami do pewnych operacji, zatem bdzie to tablica wskanikw do funkcji. Omawianie wskanikw do funkcji zaczniemy od najprostszych przykadw. Deklaracja void (*wskv)(); wprowadza wskanik wskv typu void (*)() do jeszcze nieokrelonej funkcji typu void o pustym wykazie parametrw. Jeeli zdefiniujemy trzy funkcje z takimi wanie sygnaturami void f1() { cout void f2() { cout void f3() { cout << "To jest pierwsza funkcja.\n"; }; << "To jest druga funkcja.\n"; }; << "To jest trzecia funkcja.\n"; };

to wskanikowi wskv moemy przypisa adres dowolnej z tych funkcji, np. wskv wskv = = &f2; &f3;

Wskanik wskv mona take bezporednio zainicjowa adresem funkcji w jego deklaracji: void (*wskv)() = &f1;

Funkcje f1(), f2(), f3() mona w programie wywoywa bezporednio, np. f1(); f2(); f3(); lub porednio poprzez wskaniki. W tym drugim przypadku istniejce kompilatory dopuszczaj skadni wywoa o dwch postaciach. Jeeli wskanikowi wskv przypisano np. adres funkcji f1(), to wywoanie moe mie posta: (*wskv)(); lub wskv(); Zanotujmy wniosek, ktry mona ju wysnu z powyszych przykadw: wskaniki do funkcji musz mie takie same sygnatury (liczb i typy argumentw) i typy zwracane, jak wskazywane przez nie funkcje. Prezentowane niej przykady ilustruj wymienione wyej notacje wywoa dla wskanika void (*wskv)() do funkcji void ff() oraz wskanika int (*wski)(int) do funkcji int abs(int). Przykad 5.19.

#include <iostream.h> void ff(); void (*wskv)(); void main() { cout << "Wywolanie bezposrednie ff:\n"; ff(); cout << "Wywolanie posrednie ff:\n"; wskv = &ff; (*wskv)(); } void ff() { cout << "To jest funkcja ff" << endl; } Przykad 5.20. #include <iostream.h> int abs(int); int (*wski)(int); int main() { cout << "Wywolanie bezposrednie abs:\n"; cout << abs(5) << endl; wski = &abs; cout << "Wywolanie posrednie abs:\n"; cout << wski(-10) << endl; return 0; } int abs(int a) { if (a > 0) return a; else return -a; } W nastpnym przykadzie zadeklarowano wskanik void (*wskc)(char*) typu void(*)(char*) do funkcji void ff(char*). Zwrmy uwag na brak operatora adresu ('&') w instrukcji przypisania wskc = ff;. Jest to dopuszczalne skadniowo i analogiczne, jak w przypadku tablic. Przypomnijmy, e podczas kompilacji nazwa tablicy (bez deklaratora []) jest automatycznie przeksztacana na wskanik (adres) do jej pierwszego elementu. Podobnie nazwa funkcji bez operatora wywoania () jest przeksztacana na wskanik do tej funkcji. Np. nazwa funkcji void ff(char*), tj. ff, jest przeksztacana na wskanik bez nazwy, ktrego typem jest void (*)(char*). Poniewa wskanik (*wskc)(char*) jest tego samego typu, zatem przypisanie wskc = ff jest uzasadnione. Przykad 5.21.

#include <iostream.h> void ff(char*); void (*wskc)(char*); void main() { ff("Hello! "); wskc = ff; wskc("Goodbye! "); } void ff(char* str) { cout << str << endl; }

5.7.1. Synonimy nazw typw


Zdaje si nie ulega wtpliwoci, e mimo sygnalizowanych korzyci z wprowadzenia wskanikw do funkcji, sposb ich deklarowania jest raczej uciliwy. N.b. podobne kopoty notacyjne sprawiaj wskaniki, kojarzone z typem tablicowym. Np. deklaracja int* wskt[5]; jest czytelna: wprowadza ona tablic 5 wskanikw wskt do typu int (wskt jest typu int*). Natomiast deklaracja int (*wsktab)[10]; jest ju mniej czytelna: wprowadza ona wskanik wsktab do tablicy o 10 elementach typu int (wsktab jest typu (*)[]). W jzyku C++ istnieje mechanizm, ktry pozwala zdefiniowa now nazw dla istniejcego typu. Mechanizm ten polega na poprzedzeniu deklaracji, w ktrej wystpuje nazwa typu, sowem kluczowym (specyfikatorem) typedef. Np. dla typw podstawowych o dugich nazwach moemy wprowadzi nazwy-synonimy typedef typedef unsigned char uchar; unsigned long int ulong;

a nastpnie deklarowa zmienne, posugujc si nowymi nazwami uchar ulong znak1, znak2; ul1, ul2;

W jednej deklaracji, poprzedzonej przez typedef, mona umieci kilka nazw-synonimw, np. typedef int droga, dystans, *rozmiar;

a nastpnie deklarowa zmienne (typu int oraz int*) z uyciem nowych nazw: dystans rozmiar km, m; rozx, rozy;

Takie deklaracje stosuje si czsto w celu ukrycia szczegw implementacji, co jest bardzo istotne w systemach obiektowych. Podobne deklaracje mona stosowa w deklaracjach, wprowadzajcych wskaniki do funkcji (nie wolno ich uywa w definicjach funkcji). Np. deklaracje typedef typedef int (*wski)(char*); void (*wskc)(double*, int);

wprowadzaj zastpcze (i o wiele prostsze) nazwy wski oraz wskc dla typw wskanik do funkcji. Otrzymane w ten sposb nazwy-synonimy mona wielokrotnie wykorzystywa dla deklarowania zmiennych tych typw, np. wski wskc wsk1, wsk2, wsk3; wsf1, wsf2;

Podobnie jak dla typw podstawowych, nazwy zastpcze mog suy do ukrywania informacji, a ponadto uatwiaj ycie programicie. Przykad 5.22. #include <iostream.h> extern int min(int*, int); typedef int (*wski)(int*, int); const int rozmiar = 5; int tab[rozmiar] = { 2, 5, 9, 4, 6 }; int main() { wski wsk1, wsk2; cout << "Wywolanie bezposrednie min: " << min(tab,rozmiar) << endl; wsk1 = min; wsk2 = wsk1; cout << "Wywolanie posrednie min: " << wsk2(tab,rozmiar) << endl; return 0;

} Definicja funkcji min (int*, int) moe mie posta: int min(int* tab, int r) { int pomoc = tab[0]; for (int i = 1; i < r; ++i) if (pomoc > tab[i]) pomoc = tab[i]; return pomoc; }

5.8.

Przecianie funkcji

W jzykach proceduralnych kada funkcja musi mie unikatow nazw. Jest to zasadne, jeeli

rne funkcje wykonuj rne operacje. Jednak w przypadku funkcji, ktre wykonuj podobne operacje na rnych obiektach programu, byoby korzystnym nada im t sam nazw. Funkcje mona wtedy zrnicowa unikatowymi sygnaturami, tj. typami i/lub liczb argumentw. Skadnia jzyka C++ dopuszcza taki sposb deklarowania i definiowania funkcji; nazywa si go przecieniem nazwy funkcji lub krcej przecieniem funkcji. Obowizuj przy tym nastpujce zasady: 1. Funkcje przecione (o takiej samej nazwie) musz mie taki sam zasig (np. bloku, pliku, programu). 2. Funkcje, ktre rni si tylko typem zwracanym, nie mog mie takiej samej nazwy. 3. Funkcje o takiej samej nazwie musz si rni sygnaturami i mog si rni typem zwracanym. 4. Jeeli argumenty dwch funkcji rni si tylko tym, e jedna ma argument typu T, a druga T&, to funkcje te nie mog mie takiej samej nazwy. Wynika to std, e zarwno T, jak i T& s inicjowane tym samym zbiorem wartoci i wywoanie nie potrafi ich rozrni. 5. Dwie funkcje, ktrych argumenty rni si tylko tym, e jedna ma argument typu T, a druga const T, nie mog mie takiej samej nazwy. Przyczyna jest analogiczna, jak dla argumentw typu T i T&. Przykad 5.23. #include <iostream.h> extern int min(int, int); extern double min(double, double); extern int min(int, int, int); int main() { cout << min( 2, 7 ) << endl; cout << min( 2.5, 7.5 ) << endl; cout << min( 9, 6, 4 ) << endl; return 0; } Definicje kolejnych funkcji min() mog mie posta: int min( int a, int b ) { return a < b ? a : b; } double min(double x, double y) { return x < y ? x : y; } int min(int u, int v, int w) { if (u < v) return u < w ? u : w; else return v < w ? v : w; } Dyskusja. W wywoaniu min(2,7) liczba i typy argumentw aktualnych s porwnywane z liczb i typami argumentw formalnych kolejnych prototypw funkcji min(), zadeklarowanych przed blokiem funkcji main(). Po dopasowaniu przez kompilator prototypu int min(int,int) konsolidator wczy do pliku z kodem wykonalnym definicj tej funkcji. Analogicznie bd przetwarzane pozostae dwa wywoania. Tak oto wprowadzilimy funkcje przecione. Jest to realizacja nastpujcej zasady: jeden oglny

interfejs, wiele metod (implementacji). Funkcje przecione zalicza si do metod o wizaniu wczesnym, poniewa caa informacja adresowa, potrzebna do ich wywoania, jest znana po zakoczeniu kompilacji. Dziki temu wywoania funkcji o wizaniu wczesnym (nie tylko przecionych) nale do najszybszych, poniewa narzut czasowy, zwizany z ich wywoaniem, jest minimalny. Zauwamy te, e przecianie funkcji pozwala na rozszerzanie rodowiska programowego C++ w miar potrzeb.

5.8.1. Dopasowanie argumentw


W fazie kompilacji programu, zawierajcego wywoania funkcji przecionych, uruchamiana jest do zoona procedura dopasowania argumentw. Procedura ta ma na celu moliwie najlepsze dopasowanie argumentw wywoania do argumentw formalnych i w rezultacie wybranie odpowiedniej funkcji. Szukane dopasowanie ma by najlepsze w tym sensie, e wybrana funkcja musi mie przynajmniej jeden argument lepiej dopasowany ni kada z pozostaych, moliwych do zaakceptowania funkcji. Proces dopasowania argumentw jest to najkrtszy cig przeksztace (konwersji) typu argumentu (-w) aktualnego w typ argumentu(-w) formalnego. W cigu tym dopuszcza si co najwyej jedn konwersj jawn, tj. zadan przez programist. W procesie dopasowania maj miejsce zestawione niej reguy. 1. Dopasowanie dokadne. Typy argumentw aktualnych i formalnych s te same lub przechodz w siebie drog konwersji trywialnych: typ T w T&, typ T& w typ T[] w T*, typ T w const T, typ T*, w const T*, typ T(argumenty) w T(*)(argumenty). 2. Dopasowanie z promocj. Jeeli nie powiedzie si dopasowanie dokadne, to kompilator stosuje wszelkie moliwe promocje. Dla typw cakowitych, argumenty typu char, unsigned char, enum oraz short int, s promowane do typu int. Jeeli rozmiar sizeof(short int) <= sizeof(int), to argument typu unsigned short int jest promowany do typu int. Dla typw zmiennopozycyjnych stosowana jest specjalna promocja z float do double. Pokazane niej deklaracje funkcji przecionych i ich wywoania ilustruj niektre promocje. Przykad 5.24. #include <iostream.h> void ff(char); void ff(int); void ff(unsigned int); void ff(float); void ff(double); void main() { ff('A'); ff(25); ff(25u); ff(3.14); ff(3.14F); // // // // // dopasowuje dopasowuje dopasowuje dopasowuje dopasowuje ff(char) ff(int) ff(unsigned int) ff(double) ff(float)

} void void void void ff(char c) { cout << "char\n"; } ff(int i) { cout << "int\n"; } ff(unsigned int u) { cout << "unsigned\n"; } ff(double d) { cout << "double\n"; }

void ff(float f) { cout << "float\n";

3. Konwersje standardowe. Jeeli nie powiedzie si dopasowanie z promocj, to kompilator prbuje zastosowa standardowe konwersje. S to konwersje niejawne typu kadego argumentu aktualnego do typu odpowiedniego argumentu formalnego. Przykad 5.25. void ff(char*); void ff(double); void ff(void*); void konwersje() { ff("A"); // dopasowuje ff(char*) int i = 65; ff(&i); // dopasowuje ff(void*) ff('A'); // dopasowuje ff(double) } 4. Konwersje jawne. Jeeli nie powiod si wszystkie poprzednie prby dopasowania argumentw, to kompilator zastosuje konwersje zdefiniowane przez programist. Konwersje te mog by kombinowane z promocjami i standardowymi konwersjami niejawnymi. Kada sekwencja konwersji moe zawiera co najwyej jedn konwersj zdefiniowan przez programist. 5. Niejednoznaczno. Dla deklaracji, jak w przykadzie poniej, nie mona rozstrzygn, do ktrej z funkcji przecionych odnosz si podane wywoania. Kompilator generuje komunikat o bdzie. Przykad 5.26. void ff(char); void ff(unsigned char); void ff(void*); void niejednoznaczne() { ff(65); /* niejednoznaczne: ff(char) lub ff(unsigned char) */ ff(0); //niejednoznaczne: ff(char) lub ff(void*) } 6. Brak dopasowania. Argument aktualny nie da si dopasowa do argumentu formalnego. Kompilator generuje komunikat o bdzie. Przykad 5.27. extern void zamiana(int*, int*); void brak() { int i, j;

zamiana(i, j); // brak dopasowania }

5.8.2. Adresy funkcji przecionych


Podobnie jak dla funkcji bez przeciania nazwy, moemy deklarowa wskaniki do funkcji i przypisywa im adresy funkcji przecionych. W pokazanym niej przykadzie zadeklarowano dwie funkcje o nazwie znaki; pierwsza z nich, znaki(int), wyprowadza na ekran zadan liczb spacji, za druga, znaki(int, char) zadan liczb znakw rnych od spacji (tutaj 10 znakw 'x'). Przykad 5.28. #include <iostream.h> void znaki(int licznik) { for( ; licznik; licznik-- ) cout << ' '; } void znaki(int licznik, char znak) { for( ; licznik; licznik-- ) cout << znak; } int main() { void (*wsk1) ( int ); void (*wsk2) ( int, char ); wsk1 = znaki; wsk2 = znaki; wsk1(5); cout << "|\n"; wsk2(10, 'x'); cout << "|\n"; return 0; } Wydruk z programu ma posta: | xxxxxxxxxx| Dyskusja. W programie zadeklarowano dwa wskaniki, wsk1 i wsk2, do funkcji typu void. Wykonanie pierwszej instrukcji przypisania wsk1 = znaki; dopasowuje do nazwy znaki funkcj znaki(int), za wykonanie instrukcji wsk2=znaki; dopasowuje do nazwy znaki funkcj znaki(int,char). W instrukcjach for obu funkcji opuszczono instrukcj inicjujc, poniewa zmienna sterujca licznik jest inicjowana w wywoaniach funkcji.

7. Klasy
Klasa jest kluczow koncepcj jzyka C++, realizujc abstrakcj danych na bardzo wysokim poziomie. Odpowiednio zdefiniowane klasy stawiaj do dyspozycji uytkownika wszystkie istotne mechanizmy programowania obiektowego: ukrywanie informacji, dziedziczenie, polimorfizm z wizaniem pnym, a take szablony klas i funkcji. Klasa jest deklarowana i definiowana z jednym z trzech sw kluczowych: class, struct i union. Chocia na pierwszy rzut oka moe to wyglda na rozwleko, to jednak, jak pokaemy pniej, jest ona zamierzona i celowa. Elementami skadowymi klasy mog by struktury danych rnych typw, zarwno podstawowych, jak i zdefiniowanych przez uytkownika, a take funkcje dla operowania na tych strukturach. Dostp do elementw klasy okrela zbir regu dostpu.

7.1.

Deklaracja i definicja klasy

Klasa jest typem definiowanym przez uytkownika. Deklaracja klasy skada si z nagwka, po ktrym nastpuje ciao klasy, ujte w par nawiasw klamrowych; po zamykajcym nawiasie klamrowym musi wystpi rednik, ewentualnie poprzedzony list zmiennych. W nagwku klasy umieszcza si sowo kluczowe class (lub struct albo union), a po nim nazw klasy, ktra od tej chwili staje si nazw nowego typu. Klasa moe by deklarowana: Na zewntrz wszystkich funkcji programu. Zakres widzialnoci takiej klasy rozciga si na wszystkie pliki programu. Wewntrz definicji funkcji. Klas tak nazywa si lokaln, poniewa jej zakres widzialnoci nie wykracza poza zasig funkcji. Wewntrz innej klasy. Klas tak nazywa si zagniedon, poniewa jej zakres widzialnoci nie wykracza poza zasig klasy zewntrznej. Deklaracji klas nie wolno poprzedza sowem kluczowym static. Przykadowe deklaracje klas mog mie posta: class Pusta {}; class Komentarz { /* Komentarz class Niewielka { int n; }; */};

Wystpienia klasy deklaruje si tak samo, jak zmienne innych typw, np. Pusta pusta1, pusta2; Niewielka nw1, nw2; Niewielka* wsk = &nw1; Zmienne pusta1, pusta2, nw1, nw2 nazywaj si obiektami, za wsk jest wskanikiem (zmienn typu Niewielka*) do typu Niewielka, zainicjowanym adresem obiektu nw1. N.b. atwo mona si przekona, e obiekty klasy Pusta maj niezerowy rozmiar. Klasy w rodzaju Pusta i Komentarz uywa si czsto jako klasy-makiety podczas opracowywania programu. Jak ju wspomniano, dopuszczalna jest rwnie deklaracja obiektw bezporednio po deklaracji klasy, np. class Gotowa { double db; char znak; } ob1, ob2; Dostp do skadowej obiektu danej klasy uzyskuje si za pomoc operatora dostpu ., np. nw1.n; dla zmiennej wskanikowej uywa si operatora ->, np. wsk->n, przy czym ostatni zapis jest rwnowany (*wsk).n.

Przy deklarowaniu skadowych obowizuj nastpujce ograniczenia: deklarowana skadowa nie moe by inicjowana w deklaracji klasy; nazwy skadowych nie mog si powtarza; deklaracje skadowych nie mog zawiera sw kluczowych auto, extern i register; natomiast mog by poprzedzone sowem kluczowym static; Deklaracje elementw skadowych klasy mona poprzedzi etykiet public:, protected:, lub private:. Jeeli sekwencja deklaracji elementw skadowych nie jest poprzedzona adn z tych etykiet, to kompilator przyjmuje domyln etykiet private:. Oznacza to, e dana skadowa moe by dostpna jedynie dla funkcji skadowych i tzw. funkcji zaprzyjanionych klasy, w ktrej jest zadeklarowana. Wystpienie etykiety public: oznacza, e wystpujce po niej nazwy deklarowanych skadowych mog by uywane przez dowolne funkcje, a wic rwnie takie, ktre nie s zwizane z deklaracj danej klasy. Znaczenie etykiety protected: przedstawimy przy omawianiu dziedziczenia. Ze wzgldu na sterowanie dostpem do skadowych klasy, sowa kluczowe public, protected i private nazywa si specyfikatorami dostpu. Przykad 6.1. #include <iostream.h> class Niewielka { public: int n; }; void main() { Niewielka nw1, nw2, *wsk; nw1.n = 5; nw2.n = 10; wsk = &nw1; cout << nw1.n << endl; cout << wsk ->n << endl; wsk = &nw2; cout << (*wsk).n << endl; } Dyskusja. W klasie Niewielka zadeklarowano tylko jedn zmienn skadow n typu int. Poniewa jest to skadowa publiczna, mona w obiektach nw1 i nw2 bezporednio przypisywa jej wartoci 5 i 10. Zwrmy jeszcze uwag na instrukcj deklaracji wskanika wsk do klasy Niewielka: wskanikiem jest identyfikator wsk, a nie *wsk. Jak wida z przykadu, wskanikowi do danej klasy moemy przypisywa kolejno dowolne obiekty tej klasy. Operator dostpu do zmiennej skadowej n dla wskanika wsk jest w programie zapisywany w obu rwnowanych postaciach: jako wsk->n i jako (*wsk).n. W drugiej postaci konieczne byy nawiasy, poniewa operator dostpu do skadowej . ma wyszy priorytet ni operator dostpu poredniego *. Zakres widzialnoci zmiennych skadowych obejmuje cay blok deklaracji klasy, bez wzgldu na to, w ktrym miejscu bloku znajduje si ich punkt deklaracji. Jeeli klasa zawiera funkcje skadowe, to ich deklaracje musz wystpi w deklaracji klasy. Funkcje skadowe mog by jawnie deklarowane ze sowami kluczowymi inline, static i virtual; nie mog by deklarowane ze sowem kluczowym extern. W deklaracji klasy mona rwnie umieszcza definicje krtkich (1-2 instrukcje) funkcji skadowych; s one wwczas traktowane przez kompilator jako funkcje rozwijalne, tj. tak, jakby byy poprzedzone sowem

kluczowym inline. W programach jednoplikowych dusze funkcje skadowe deklaruje si w nawiasach klamrowych zawierajcych ciao klasy, za definiuje si bezporednio po zamykajcym nawiasie klamrowym. Deklaracja klasy wraz z definicjami funkcji skadowych (i ewentualnie innymi wymaganymi definicjami) stanowi definicj klasy. Funkcje skadowe klasy mog operowa na wszystkich zmiennych skadowych, take prywatnych i chronionych. Mog one rwnie operowa na zmiennych zewntrznych w stosunku do definicji klasy. Przykad 6.2. #include <iostream.h> class Punkt { public: int x,y; void init(int a, int b) {x = a; y = b; } void ustaw(int, int); }; void Punkt::ustaw(int a, int b) { x = x + a; y = y + b; } int main() { Punkt punkt1; Punkt* wsk = &punkt1; punkt1.init(1,1); cout << punkt1.x << endl; cout << wsk -> y << endl; punkt1.ustaw(10,15); cout << punkt1.x << endl; cout << (*wsk).y << endl; return 0; } Dyskusja. W klasie Punkt zarwno atrybuty x, y, jak i funkcje init() oraz ustaw() s publiczne, a wic dostpne na zewntrz klasy. Funkcja init() suy do zainicjowania obiektu punkt1 klasy Punkt. Jej definicja wewntrz klasy jest rwnowana definicji poza ciaem klasy o postaci: inline void Punkt::init(int a, int b) {x = a; y = b; } Dwuargumentowy (binarny) operator zasigu :: poprzedzony nazw klasy ustala zasig funkcji skadowych ustaw() i init(). Napis Punkt:: informuje kompilator, e nastpujca po nim nazwa jest nazw funkcji skadowej klasy Punkt; wywoanie takiej funkcji w programie moe mie miejsce tylko tam, gdzie jest dostpna deklaracja klasy Punkt. Zwrmy rwnie uwag na sposb zapisu wywoania funkcji skadowej: zapis ten ma tak sam posta, jak odwoanie do atrybutu x, bd y.

7.1.1.

Autoreferencja: wskanik this

Funkcje skadowe s zwizane z definicj klasy, a nie z deklaracjami obiektw tej klasy. Pociga to za sob nastpujce konsekwencje: Istnieje tylko jeden egzemplarz kodu definicji danej funkcji skadowej, bdcej wasnoci

klasy. Kod ten nie wchodzi w skad adnego obiektu. W kadym wywoaniu funkcji skadowej klasy musi by wskazany obiekt woajcy. Wskazanie obiektu woajcego jest realizowane przez przekazanie do funkcji skadowej ukrytego argumentu aktualnego, ktrym jest wskanik do tego obiektu. Wskanikiem tym jest inicjowany niejawny argument wskanik w definicji funkcji skadowej. Do wskanika tego mona si rwnie odwoywa jawnie, uywajc sowa kluczowego this. Tak wic mamy tutaj autorekursj: poprzez wskanik this odwoujemy si do obiektu, dla ktrego wywoywana jest pewna operacja (wywoanie funkcji). W podanym niej przykadzie pokazano dwie rwnowane deklaracje, przy czym druga z nich ilustruje moliwo jawnego uycia wskanika this. Przykad 6.3. class Niejawna { int m; int funkcja() { return m; } }; class Jawna { int m; int funkcja() { return this->m; } }; Poniewa this jest sowem kluczowym, nie moe by jawnie deklarowany jako zmienna skadowa. Mona natomiast wydedukowa, e w funkcji funkcja() klasy Niejawna wskanik this jest niejawnie zadeklarowany jako Niejawna* const this; a inicjowany adresem obiektu, dla ktrego woana jest funkcja skadowa funkcja(). Poniewa wskanik this jest deklarowany jako *const, czyli jako wskanik stay, nie moe on by zmieniany. Mona natomiast wprowadza zmiany we wskazywanym przez niego obiekcie. Zobaczmy jeszcze, jak wyglda zastosowanie wskanika this w nieco zmodyfikowanym programie, zawierajcym definicj klasy Punkt.

Przykad 6.4. #include <iostream.h> class Punkt { public: int init(int a, int b) { this->x = a; this->y = b; return x; } int ustaw(int, int); private: int x, y; }; int Punkt::ustaw(int a, int b) { this->x = this->x + a; this->y = y + b; return x ; } int main() { Punkt punkt1; cout << punkt1.init(0,0) << endl; cout << punkt1.ustaw(10,15) << endl; return 0; } Z pokazanych wyej przykadw wida, e jawnych odwoa do wskanika this nie opaca si uywa, skoro poprawny syntaktycznie jest krtszy zapis. Wyjtkami od tego zalecenia s sytuacje, gdy jawne odwoanie do wskanika this jest nie tylko opacalne, ale i konieczne; s to definicje funkcji, ktre operuj na elementach tzw. list jedno- i dwukierunkowych.

7.1.2.

Statyczne elementy klasy

Ciao klasy moe zawiera deklaracje struktur danych i funkcji poprzedzone sowem kluczowym static, bdcym, podobnie jak auto, register i extern, specyfikatorem klasy pamici. Sowo static (a take extern) moe poprzedza jedynie deklaracje zmiennych i funkcji oraz unii anonimowych; nie moe poprzedza deklaracji funkcji wewntrz bloku ani deklaracji argumentw formalnych funkcji. W oglnoci specyfikator static ma dwa znaczenia, ktre s czsto mylone. Pierwsze z nich mwi kompilatorowi: przydziel danej wielkoci ustalony adres w pamici i zachowuj go przez cay czas trwania programu. Drugie mwi o zasigu: ustal zakres widzialnoci danej wielkoci poczwszy od miejsca, w ktrym zostaa zadeklarowana. Inaczej mwic: wielko zadeklarowana jako static istnieje przez cay czas wykonania programu, ale jej zasig zaley od miejsca deklaracji. Obydwa znaczenia odnosz si do statycznych zmiennych skadowych, natomiast tylko drugie z nich odnosi si do statycznych funkcji skadowych. Przykad 6.5. static int nn; int main() { nn = 10; return 0; } Dyskusja. W powyszym przykadzie zmienna globalna nn istnieje przez cay czas wykonania programu, ale jej zakres widzialnoci jest ograniczony do pliku, w ktrym zostaa zadeklarowana (tutaj plik z funkcj main()). Oznacza to, e jest dopuszczalne uywanie nazwy nn w innych plikach programu i w innym znaczeniu. Zmienna nn ma zasig od punktu deklaracji do koca

pliku. Gdybymy umiecili deklaracj zmiennej nn np. po bloku main, to kada prba wykonania na niej operacji wewntrz bloku byaby bdem syntaktycznym. Zmienn nn mona inicjowa w jej deklaracji; poniewa tutaj tego nie uczyniono, kompilator nada jej domyln warto pocztkow 0. Zmienne skadowe klasy s uywajc terminologii jzyka Smalltalk zmiennymi wystpienia (ang. instance variables); dla kadego obiektu danej klasy tworzone s oddzielne ich kopie. Deklaracje takich zmiennych s jednoczenie ich definicjami; s to zmienne lokalne o zasigu klasy i przypadkowych wartociach inicjalnych (deklarowane w bloku klasy zmienne nie mog by tam inicjowane). Jeeli deklaracj zmiennej skadowej poprzedzimy specyfikatorem static, to taka deklaracja staje si deklaracj referencyjn, a wprowadzona ni zmienna zmienn klasy. Konsekwencj tego jest fakt, e definicj takiej zmiennej musimy umieci na zewntrz deklaracji klasy. Statyczna zmienna klasy podlega tym samym reguom dostpu (sterowanym etykietami public:, protected: i private:) co zmienna wystpienia. Przy definiowaniu zmiennej statycznej mona jej nada warto pocztkow rn od zera; na czas tej operacji ograniczenia dostpu zostaj wyczone. W rezultacie otrzymamy zmienn o wasnociach obiektu globalnego i zasigu pliku, skojarzon z sam klas, a nie z jakimkolwiek jej wystpieniem, tj. obiektem. Poniewa zmienna klasy jest zwizana z klas, a nie z jej obiektami, mona na niej wykonywa operacje w ogle bez tworzenia obiektw. W tym sensie jest ona samodzielnym obiektem. Jeeli jednak zadeklarujemy obiekty, to kady z nich bdzie mia dostp do tego samego adresu w pamici, ktry zosta przydzielony przez kompilator statycznej zmiennej skadowej. Tak wic mamy tylko jedn kopi zmiennej skadowej, dostpn bez istnienia obiektw, bd wspdzielon przez zadeklarowane obiekty. Reasumujc: statyczna zmienna klasy jest to zmienna o zasigu ograniczonym do klasy, w ktrej jest zadeklarowana. Wprowadzenie statycznych zmiennych skadowych pozwala unikn uywania zewntrznych zmiennych globalnych wewntrz klasy, co czsto daje niepodane efekty uboczne i narusza zasad ukrywania informacji. Skadni deklaracji, definicji i operacji na zmiennej statycznej ilustruje poniszy przykad. Przykad 6.6. #include <iostream.h> class Test { public: int m; static int n; // Tylko deklaracja }; // Definicja obiektu statycznego n int Test::n = 10; void main() { Test::n = 25; cout << "Test::n= " << Test::n << endl; Test t1,t2; t1.m = 5; t2.m = 0; cout << "t1.m= " << t1.m << endl; cout << "t2.m= " << t2.m << endl; cout << "t2.n= " << t2.n << endl; } Wydruk z programu ma posta:

Test::n= 25 t1.m= 5 t2.m= 0 t2.n= 25 Dyskusja. W deklaracji klasy Test mamy deklaracj zmiennej statycznej static int n;. Definicj tej zmiennej, o postaci int Test::n = 10; umieszczono bezporednio po deklaracji klasy. Pierwsz instrukcj w bloku funkcji main() jest instrukcja przypisania Test::n = 25;. Przypisan do zmiennej statycznej warto 25 wyprowadzaj na ekran instrukcje: cout << "Test::n= " << Test::n << endl; oraz cout << "t2.n= " << t2.n << endl; Obiekty t1 i t2 maj swoje prywatne kopie zmiennej m oraz dostp do jednej kopii zmiennej statycznej n. FUwaga 1. Typ statycznej zmiennej skadowej nie obejmuje nazwy jej klasy; tak wic typem zmiennej Test::n jest int. FUwaga 2. Statyczne skadowe klasy lokalnej (zadeklarowanej wewntrz bloku funkcji) nie maj okrelonego zakresu widocznoci i nie mog by definiowane na zewntrz deklaracji klasy. Wynika std, e klasa lokalna nie moe mie zmiennych statycznych. FUwaga 3. Zauwamy, e sowo static nie jest ani potrzebne, ani dozwolone w definicji statycznej skadowej klasy. Gdyby byo dopuszczalne, to mogaby powsta kolizja pomidzy znaczeniem static stosowanym do skadowych klasy, a znaczeniem, stosowanym do globalnych obiektw i funkcji. Sowo kluczowe static moe rwnie poprzedza deklaracj (nie definicj!) funkcji skadowej klasy. Taka statyczna funkcja skadowa moe operowa jedynie na zmiennych statycznych nie ma dostpu do zmiennych wystpienia (zwyke, niestatyczne funkcje skadowe mog operowa zarwno na zmiennych niestatycznych, jak i statycznych). Wynika to std, e statyczna funkcja skadowa nie ma wskanika this, tak e moe ona mie dostp do zmiennych niestatycznych swojej klasy jedynie przez zastosowanie operatorw selekcji . lub -> do obiektw klasy. Przykad 6.7. #include <iostream.h> class Punkt { public: static int init( int ); // Deklaracja init() private: static int x; // Deklaracja x }; int Punkt::x; // Definicja x // Definicja init() int Punkt::init( int a) { x = a; return x; } int main() { cout << Punkt::init(10) << endl; return 0; } Dyskusja. Przykad pokazuje dowodnie, e dostp do zmiennej statycznej i wywoanie funkcji statycznej danej klasy nie wymagaj istnienia obiektw tej klasy.

7.2. Konstruktory i destruktory


Konstruktory i destruktory nale do grupy specjalnych funkcji skadowych. Grupa ta obejmuje: konstruktory i destruktory inicjujce, konstruktor kopiujcy oraz tzw. funkcje operatorowe. Konstruktor jest funkcj skadow o takiej samej nazwie, jak nazwa klasy. Nazw destruktora jest nazwa klasy, poprzedzona znakiem tyldy (~). Kada klasa zawiera konstruktor i destruktor, nawet gdy nie s one jawnie zadeklarowane. Zadaniem konstruktora jest konstruowanie obiektw swojej klasy; jego wywoanie w programie pociga za sob: alokacj pamici dla obiektu; przypisanie wartoci do niestatycznych zmiennych skadowych; wykonanie pewnych operacji porzdkujcych (np. konwersji typw). Jeeli w klasie nie zadeklarowano konstruktora i destruktora, to zostan one wygenerowane przez kompilator i automatycznie wywoane podczas tworzenia i destrukcji obiektu. Przykad 6.8. #include <iostream.h> class Punkt { public: Punkt(int, int); void ustaw(int, int); private: int x,y; }; Punkt::Punkt(int a, int b) { x = a; y = b; } void Punkt::ustaw( int c, int d) { x = x + c; y = y + d; } int main() { Punkt punkt1(3,4); return 0; } Dyskusja. Poniewa klasa Punkt zawiera dwie prywatne zmienne skadowe: x oraz y, celowym jest zdefiniowanie konstruktora, ktry nadaje im wartoci pocztkowe a oraz b. Wartoci te (3 i 4) s przekazywane w wywoaniu konstruktora w bloku funkcji main(). Korzyci z wprowadzenia konstruktora s oczywiste; w poprzednich przykadach, w ktrych wystpowaa klasa Punkt, musielimy zapisywa w bloku funkcji main() dwie instrukcje: instrukcj deklaracji obiektu, a nastpnie instrukcj wywoania funkcji init() dla przypisania zmiennym danych wartoci. Teraz te dwie operacje wykonuje jedna instrukcja deklaracji Punkt punkt1(3,4); Instrukcja ta jest rwnowana instrukcji Punkt punkt1 = Punkt(3,4); ktra woa konstruktor Punkt::Punkt(int, int) dla zainicjowania atrybutw obiektu punkt1 wartociami 3 i 4. Poniewa klasa Punkt nie zawiera destruktora, obiekt punkt1 zostanie zniszczony przez

destruktor wywoywany w chwili zakoczenia programu, a wygenerowany w fazie kompilacji. W nastpnych definicjach konstruktorw bdziemy najczciej wykorzystywa notacj, zapoyczon przez C++ z jzyka Simula. Dla zainicjowania zmiennej jednego z typw podstawowych warto inicjaln moemy umieci po znaku rwnoci, bd w nawiasach okrgych, np. int i = 7; int i(7); Wykorzystujc drug z rwnowanych notacji moemy zapisa definicj konstruktora klasy Punkt w postaci: Punkt::Punkt(int a, int b):x(a),y(b) {}

W powyszej definicji po nagwku funkcji mamy list zmiennych skadowych z wartociami inicjalnymi w nawiasach okrgych, poprzedzon pojedynczym dwukropkiem. Poniewa ten prosty konstruktor nie wykonuje niczego wicej poza inicjowaniem zmiennych skadowych, jego blok jest pusty.

7.2.1. Wasnoci konstruktorw i destruktorw


Konstruktory i destruktory posiadaj szereg charakterystycznych wasnoci i podlegaj pewnym ograniczeniom. Zacznijmy od ogranicze. 1. Deklaracja konstruktora i destruktora nie moe zawiera typu zwracanego (nawet void). W przypadku konstruktora jawna warto zwracana nie jest wskazana, poniewa jest on przeznaczony do tworzenia obiektw, czyli przeksztacania pewnego obszaru pamici w zorganizowane struktury danych. Gdyby dopuci do zwracania jakiej wartoci, byby to fragment informacji o implementacji obiektu, ktra powinna by niewidoczna dla uytkownika. Podobne argumenty dotycz destruktora. 2. Nie jest moliwy dostp do adresu konstruktora i destruktora. Zasadniczym powodem jest ukrycie szczegw fizycznej alokacji pamici, ktre powinny by niewidoczne dla uytkownika. 3. Deklaracje konstruktora i destruktora nie mog by poprzedzone sowami kluczowymi const, volatile i static. (Modyfikator volatile wskazuje, e obiekt moe by w kadej chwili zmodyfikowany i to nie tylko instrukcj programu uytkownika, lecz take przez zdarzenia zewntrzne, np. przez procedur obsugi przerwania). Deklaracja konstruktora nie moe by poprzedzona sowem kluczowym virtual. 4. Konstruktory i destruktory nie s dziedziczone. 5. Destruktor nie moe mie parametrw formalnych. Parametrami formalnymi konstruktora nie mog by zmienne skadowe wasnej klasy. Wyliczymy teraz kolejno gwne wasnoci konstruktorw i destruktorw. 1. Konstruktory i destruktory generowane przez kompilator s public. Konstruktory definiowane przez uytkownika rwnie powinny by public, aby istniaa moliwo tworzenia obiektw na zewntrz deklaracji klasy. W pewnych przypadkach szczeglnych konstruktory mog wystpi po etykiecie protected:, a nawet private:. 2. Konstruktor jest woany automatycznie, gdy definiuje si obiekt; destruktor gdy obiekt jest niszczony. 3. Konstruktory i destruktory mog by wywoywane dla obiektw const i volatile. 4. Z bloku konstruktora i destruktora mog by wywoywane funkcje skadowe ich klasy. 5. Deklaracja destruktora moe by poprzedzona sowem kluczowym virtual. 6. Konstruktor moe zawiera referencj do wasnej klasy jako argument formalny. W takim

przypadku jest on nazywany konstruktorem kopiujcym. Deklaracja konstruktora kopiujcego dla klasy X moe mie posta X(const X&); lub X(const X&, int=0);. Jest on wywoywany zwykle wtedy, gdy definiuje si obiekt, inicjowany przez wczeniej utworzony obiekt tej samej klasy, np. X ob2 = ob1;. Konstruktor jest woany wtedy, gdy ma by utworzony obiekt jego klasy. Obiekt taki moe by tworzony na wiele sposobw: jako zmienna globalna; jako zmienna lokalna; przez jawne uycie operatora new; jako obiekt tymczasowy; jako zmienna skadowa, zagniedona w innej klasie. Wprawdzie konstruktory i destruktory nie mog by funkcjami statycznymi, ale mog operowa na statycznych zmiennych skadowych swojej klasy. W podanym niej przykadzie wykorzystano zmienn statyczn licznik do rejestrowania istniejcych w danej chwili obiektw. Klas Status mona uwaa za fragment podsystemu zarzdzania pamici programu. Przykad 6.9. #include <iostream.h> class Status { public: Status() { licznik++; } ~Status() { licznik--} int odczyt() { return licznik; } private: static licznik; //deklaracja }; int Status::licznik = 0; // Definicja int main() { Status ob1,ob2, ob3; cout << "Mamy " << ob1.odczyt() << " obiekty\n"; Status *wsk; wsk = new Status; //Alokuj dynamicznie if (!wsk) { cout << "Nieudana alokacja\n"; return 1; } cout << "Mamy " << ob1.odczyt() << " obiekty po alokacji\n"; // skasuj obiekt delete wsk; cout << "Mamy " << ob1.odczyt() << " obiekty po destrukcji\n"; return 0; } Dyskusja. Wygld ekranu po wykonaniu programu bdzie nastpujcy:

Mamy 3 obiekty Mamy 4 obiekty po alokacji Mamy 3 obiekty po destrukcji Obiekty programu s tworzone przez kolejne wywoania konstruktora Status(){licznik++;}. Najpierw s tworzone obiekty ob1, ob2 i ob3, a nastpnie obiekt dynamiczny *wsk. Po destrukcji wskanika wsk mamy trzeci wiersz wydruku. Oczywicie pierwsze trzy obiekty zostan zniszczone przy wyjciu z programu przez trzykrotne wywoanie tego samego co dla wsk destruktora ~Status() { licznik--}.

7.2.2.

Przecianie konstruktorw

Konstruktory, podobnie jak zwyke funkcje (take funkcje skadowe klasy), mog by przeciane. Najczciej ma to na celu tworzenie obiektw inicjowanych zadanymi wartociami zmiennych skadowych, lub te obiektw bez podanych wartoci inicjalnych. Dziki temu mona zadeklarowa wicej ni jeden konstruktor dla danej klasy. Wywoanie konstruktora w deklaracji obiektu pozwala kompilatorowi ustali, w zalenoci od liczby i typw podanych argumentw, ktr wersj konstruktora naley wywoa. W podanym niej przykadzie obiektowi punkt1 nadano wartoci pocztkowe (3 i 4), za obiektowi punkt2 nie. Gdyby usun z programu konstruktor domylny Punkt() o pustym wykazie argumentw, program nie zostaby skompilowany, poniewa brakoby konstruktora, ktry mogby by dopasowany do wywoania Punkt punkt2. Przykad 6.10. #include <iostream.h> class Punkt { public: Punkt() {} Punkt(int a, int b): x(a), y(b) {} int x,y; }; int main() { Punkt punkt1(3,4); cout << punkt1.x << endl; Punkt punkt2; cout << punkt2.x << endl; return 0; }

7.2.3.

Konstruktory z argumentami domylnymi

Alternatyw dla przeciania konstruktora jest wyposaenie go w argumenty domylne, podobnie jak czynilimy to w stosunku do zwykych funkcji. Przypomnijmy, e argument domylny jest przyjmowany przez kompilator wtedy, gdy w wywoaniu funkcji nie podano odpowiadajcego mu argumentu aktualnego. Wartoci domylne argumentw formalnych w deklaracji (nie w definicji!) zapisujemy w nastpujcy sposb: Jeeli w deklaracji konstruktora umieszczono nazwy argumentw formalnych, to po nazwie argumentu piszemy znak rwnoci, a po nim odpowiedni warto, np. Punkt ( int a = 0, int b = 0 ); Jeeli deklaracja nie zawiera nazw argumentw, a jedynie ich typy, to znak rwnoci i nastpujc po nim warto piszemy po identyfikatorze typu, np. Punkt ( int = 0,

int = 0 ); Przykad 6.11. #include <iostream.h> class Punkt { public: int x,y; Punkt(int = 0, int = 0); }; Punkt::Punkt(int a, int b): x(a), y(b) { if (a==0 && b==0) cout << "Obiekt bez inicjowania...\n"; else cout << "Obiekt z inicjowaniem...\n"; } int main() { Punkt punkt1(3,4); cout << punkt1.x << endl; Punkt punkt2; cout << punkt2.x << endl; return 0; } Dyskusja. Powyszy program zawiera wywoania, identyczne jak w przypadku konstruktorw przecionych, ale tylko jedn definicj konstruktora. W definicji konstruktora Punkt() z zerowymi wartociami domylnymi umieszczono instrukcj if jedynie w tym celu, aby pokaza, e instrukcja deklaracji Punkt punkt1(3,4); wywouje konstruktor z inicjowaniem, a instrukcja deklaracji Punkt punkt2; wywouje konstruktor bez inicjowania. Wydruk z programu ma posta: Obiekt z inicjowaniem... 3 Obiekt bez inicjowania... 0 Zauwamy e konstruktor Punkt(int = 0, int = 0) nie jest rwnowany konstruktorowi bez argumentw, np. Punkt(). Prezentowana niej wersja klasy Punkt zawiera taki wanie konstruktor bezargumentowy. Przykad nawizuje take do przeprowadzonej w p. 5.2 dyskusji, w ktrej zwrcono uwag na zwizki pomidzy przecianiem funkcji, a uywaniem argumentw domylnych.

Przykad 6.12. #include <iostream.h> class Punkt { public: Punkt(): x(0), y(0) {} Punkt(int a, int b): x(a), y(a) {} int fx() { return x; } int fy() { return y; } private: int x,y; }; int main() { Punkt punkt1; cout << "punkt1.x= " << punkt1.fx() << "\n"; Punkt punkt2(3,2); cout << "punkt2.x= " << punkt2.fx() << "\n"; Punkt *wsk = new Punkt(2,2); if (!wsk) return 1; cout << "wsk->fx()=" << wsk->fx() << "\n"; if (wsk) { delete wsk; wsk = NULL; } return 0; } Dyskusja. W klasie Punkt zmienne skadowe x, y s teraz prywatne, w zwizku z czym zadeklarowano publiczny interfejs w postaci dwch funkcji skadowych fx() oraz fy(), ktre daj dostp do tych zmiennych. Obiekty punkt1 i punkt2 s alokowane na stosie funkcji main(), natomiast obiekt wskazywany przez wsk na kopcu (w pamici swobodnej). Zwrmy uwag na instrukcj if w bloku main(), w ktrej wystpuje symbol NULL, oznaczajcy wskanik pusty. Zapis if(wsk), rwnowany if(wsk == NULL) jest testem, ktry zapobiega prbie niszczenia tego samego obiektu dwa razy (jeeli wsk == NULL, to adna z dwch instrukcji w bloku if nie zostanie wykonana).

7.3. Przecianie operatorw


Wrd zdefiniowanych w jzyku C++ operatorw wystpuj operatory polimorficzne. Moemy tu wyodrbni dwa rodzaje polimorfizmu: Koercj, gdy dopuszcza si, e argumenty operatora mog by mieszanych typw. Ten rodzaj polimorfizmu jest charakterystyczny dla operatorw arytmetycznych: +, -, *, /; np. operator + moe suy do dodania dwch liczb cakowitych, liczby cakowitej do zmiennopozycyjnej, etc. Przecienie operatora, gdy ten sam symbol operatora stosuje si w operacjach nie zwizanych semantycznie. Typowymi przykadami mog by operatory inicjowania i przypisania, oznaczane tym samym symbolem =, lub symbole >> oraz <<, ktre, zalenie od kontekstu, s bitowymi operatorami przesunicia lub operatorami wprowadzania/wyprowadzania. Wymienione rodzaje wbudowanego polimorfizmu wystpuj w wielu nowoczesnych jzykach programowania. Wspln dla nich cech jest to, e s one predefiniowane, a ich definicje stanowi integraln i niezmienn cz definicji jzyka. Uytkownikowi jzyka C++ dano znacznie wiksze moliwoci, spotykane w nielicznych

wspczesnych jzykach programowania. Ma on do dyspozycji mechanizm, ktry pozwala tworzy nowe definicje dla istniejcych operatorw. Dziki temu programista moe tak zmodyfikowa dziaanie operatora, aby wykonywa on operacje w narzucony przez niego sposb. Jest to szczeglnie istotne w programach czysto obiektowych. Np. obiekty wielokrotnie redefiniowanej klasy Punkt moemy uwaa za wektory w prostoktnym ukadzie wsprzdnych. W geometrii i fizyce dla takich obiektw s np. okrelone operacje dodawania i odejmowania. Byoby wskazane zdefiniowa podobne operacje dla deklarowanych przez nas klas. Wykorzystamy do tego celu nastpujc ogln posta definicji tzw. funkcji operatorowej, tj. funkcji, ktra definiuje pewien operator @: typ klasa::operator@(argumenty) { // wykonywane operacje } gdzie sowo typ oznacza typ zwracany przez funkcj operatorow, sowo klasa jest nazw klasy, w ktrej funkcja definiujca operator jest funkcj skadow, dwa dwukropki oznaczaj operator zasigu, za symbol @ bdzie zastpowany w konkretnej definicji przez symbol operatora (np. =, ==, +, ++, new). Nazwa funkcji definiujcej operator skada si ze sowa kluczowego operator i nastpujcego po nim symbolu operatora; np., jeeli jest przeciany operator +, to nazw funkcji bdzie operator+. Przecienie operatora przypomina przecienie funkcji. I faktycznie jest to przypadek szczeglny przecienia funkcji. Poniej zestawiono waniejsze reguy przeciania i wasnoci operatorw przecianych. 1. Operator @ jest zawsze przeciany wzgldem klasy, w ktrej jest zadeklarowana jego funkcja operator@. Zatem w innych kontekstach operator nie traci adnego ze swych oryginalnych znacze, ustalonych w definicji jzyka, natomiast zyskuje znaczenia dodatkowe. 2. Funkcja definiujca operator musi by albo funkcj skadow klasy, albo mie co najmniej jeden argument bdcy obiektem lub referencj do obiektu klasy (wyjtkiem s funkcje, ktre redefiniuj znaczenie operatorw new i delete). Ograniczenie to gwarantuje, e wystpienie operatora z argumentami typw nie bdcych klasami, jest jednoznaczne z wykorzystaniem jego standardowej, wbudowanej w jzyk definicji. Jego intencj jest rozszerzanie jzyka, a nie zmiana wbudowanych definicji. 3. Nie mog by przeciane operatory ., .*, ::, ?:, sizeof oraz symbole # i ##. 4. Nie jest moliwa zmiana priorytetu, regu cznoci, ani liczby argumentw operatora. 5. Funkcje definiujce operatory, za wyjtkiem funkcji operator=(), s dziedziczone. 6. Przeciany operator nie moe mie argumentw domylnych. 7. Funkcje: operator=(), operator[](), operator() i operator->() nie mog by statycznymi funkcjami skadowymi. 8. Funkcja definiujca operator nie moe posugiwa si wycznie wskanikami. 9. Operatory: = (przypisania), & (pobrania adresu) i , (przecinkowy) maj predefiniowane znaczenia w odniesieniu do obiektw klas (o ile nie zostay na nowo zdefiniowane). Za wyjtkiem operatorw wymienionych wyej, moemy przecia wszystkie operatory wbudowane w jzyk zarwno operatory jednoargumentowe (unarne), jak i dwuargumentowe (binarne). Podane niej proste przykady ilustruj przecianie kilku takich operatorw.

Przykad 6.13. // Operator dodawania + #include <iostream.h> class Punkt { public: Punkt(): x(0),y(0){} Punkt(int a, int b): x(a),y(a) {} int fx() { return x; } int fy() { return y; } Punkt operator+(Punkt); private: int x,y; }; Punkt Punkt::operator+(Punkt p) { return Punkt(x + p.x, y + p.y); } int main() { Punkt punkt1, punkt2, punkt3; punkt1 = Punkt(2,2); punkt2 = Punkt(3,1); punkt3 = punkt1 + punkt2; cout << "punkt1.x= " << punkt1.fx() << endl; cout << "punkt3.x= " << punkt3.fx() << endl; int i = 10, j; j = i + 15; // Predefiniowany '+' cout << "j= " << j << endl; return 0; } Wydruk z programu ma posta: punkt1.x= 2 punkt3.x= 5 j= 25 Dyskusja. Instrukcja Punkt punkt1, punkt2, punkt3; wywouje trzykrotnie konstruktor Punkt(). Instrukcja przypisania punkt1 = Punkt(2,2); wywouje konstruktor Punkt(int,int). Nastpnie kompilator generuje niejawny operator przypisania, ktry przypisuje obiektowi punkt1 obiekt, wieo utworzony przez konstruktor Punkt(int,int). Tak samo jest wykonywana druga instrukcja przypisania. Instrukcja dodawania punkt3 = punkt1 + punkt2; woa najpierw generowany przez kompilator konstruktor kopiujcy, ktry tworzy kopi obiektu punkt2 i przekazuje j jako parametr aktualny do funkcji operatorowej Punkt Punkt::operator+(Punkt p). Konieczno wykonania tej operacji byaby bardziej oczywista, gdybymy instrukcj dodawania obiektw zapisali w drugiej dopuszczalnej postaci punkt3 = punkt1.operator+(punkt2); Nastpnie jest wywoywana funkcja operatorowa +, ktra z kolei woa konstruktor Punkt(int,int), aby utworzy obiekt, bdcy sum obiektw punkt1 i punkt2.

Kocow operacj w tej instrukcji jest wywoanie generowanego przez kompilator operatora przypisania =, aby przypisa wynik do obiektu punkt3. Omawiane niejawne operacje przypisania i kopiowania moglibymy przeledzi, uzupeniajc definicj klasy Punkt o wasne konstrukcje programowe. I tak, operator przypisania dla klasy Punkt mgby mie posta: Punkt& Punkt::operator=(const Punkt& p) { this->x = p.x; this->y = p.y; return *this } za konstruktor kopiujcy: Punkt::Punkt(const Punkt& p) { x = p.x; y = p.y; } Dalsze instrukcje s wykonywane w sposb standardowy i nie wymagaj komentarzy. Zauwamy jedynie, e predefiniowany operator + nie straci swojego znaczenia, co pokazano na operacjach ze zmiennymi i oraz j. Przykad 6.14. // Operator relacyjny == #include <iostream.h> class Punkt { public: Punkt(): x(0),y(0) {} Punkt(int a, int b): x(a),y(a) {} int fx() { return x; } int fy() { return y; } int operator==(Punkt); private: int x,y; }; int Punkt::operator==(Punkt p) { if( p.fx() == fx() && p.fy() == fy() ) return (-1); else return (0); } int main() { Punkt punkt1, punkt2; punkt1 = Punkt(2,2); punkt2 = Punkt(3,1); if ( punkt1 == punkt2 ) cout << "Rowne\n"; else cout << "Nierowne\n"; int i = 5, j = 6, k; k = i == j; // predefiniowany '==' cout << "k= " << k << endl; return 0;

} Dyskusja. Instrukcja deklaracji Punkt punkt1, punkt2; wywouje dwukrotnie bezargumentowy konstruktor Punkt(). Instrukcja punkt1 = Punkt(2,2); najpierw woa konstruktor Punkt::Punkt(int,int), a nastpnie generowany przez kompilator operator przypisania (porwnaj poprzedni przykad). Podobnie przebiega wykonanie drugiej instrukcji przypisania. Poniewa instrukcj if mona zapisa

w postaci: if(punkt1.operator==(punkt2))... zatem, podobnie jak w poprzednim przykadzie, woany jest niejawny konstruktor kopiujcy dla utworzenia kopii obiektu punkt2, a nastpnie funkcja operatorowa ==, ktra wywouje funkcj skadow fx(). Poniewa skadowe x obu obiektw s rne (2 i 3), test na rwno na tym si koczy, poniewa pierwszy argument koniunkcji ma warto zero. Podobnie jak w poprzednim przykadzie pokazano, e operator == nie straci swojego znaczenia, wbudowanego w jzyk. Wydruk z programu ma posta: Nierowne k= 0 Przytoczone przykady ilustroway przecianie operatorw binarnych. Dla operatorw unarnych skadnia deklaracji i definicji funkcji operatorowej jest taka sama z tym, e funkcja przeciajca musi by bezargumentowa. Ilustracj tego jest poniszy przykad. Przykad 6.15. // Unarne operatory + i #include <iostream.h> class Firma { public: Firma(char* n, char s): nazwa(n),stan(s) {} void operator+() { stan = '1'; } void operator-() { stan = '0'; } void podaj() { cout << "stan: " << stan << endl;} private: char* nazwa; char stan; }; int main() { Firma frm1("firma1", '1'); -frm1; +frm1; frm1.podaj(); return 0; } FUwaga. Poprawne syntaktycznie stosowanie operatorw przecionych moe nie mie sensu nawet w stosunku do obiektw tej samej klasy, jeeli obiekt ma by dobr abstrakcj rzeczywistoci fizycznej. Wemy dla przykadu dwa obiekty klasy Potrawa; niech kady z nich ma skadow char* smak. Jeeli warto tej skadowej w pierwszym obiekcie jest "sony", a w drugim "sodki", to ma sens porwnanie (==), ale nie ma sensu np. dodawanie tych dwch obiektw do siebie. Warto o tym pamita przy tworzeniu systemw obiektowych.

7.3.1. Przecianie operatorw new i delete


Przypomnijmy (nigdy za wiele przypomnie), e obiekty jzyka C++ mog by alokowane na trzy sposoby: na stosie (jest to tzw. pami automatyczna, a zmienne w niej lokowane s nazywane zmiennymi automatycznymi lub lokalnymi), w pamici statycznej pod ustalonym adresem,

w pamici swobodnej (dynamicznej), zorganizowanej w postaci listy komrek, nazywanej kopcem, stogiem, lub stert. Obiektom lokalnym jest przydzielana pami na stosie w chwili, gdy wywoywana jest funkcja z niepustym wykazem argumentw, lub w chwili, gdy obiekt jest tworzony w bloku funkcji. Pami dla obiektw statycznych jest przydzielana w fazie konsolidacji (po kompilacji, a przed faz wykonania). Obiekty dynamiczne s alokowane w pamici przez wywoanie operatora new. Przy tworzeniu obiektw, w kadym z tych przypadkw najpierw jest przydzielany odpowiedni obszar pamici, a nastpnie jest woany konstruktor, ktry inicjuje w tym obszarze obiekt o danych wasnociach. Wyrane rozdzielenie alokacji pamici od inicjowania jest najlepiej widoczne przy alokacji dynamicznej. Np. deklaracja Test* wsk = new Test(10); oznacza, e operator new wywouje pewn (niejawn) procedur alokacji dla uzyskania pamici, a nastpnie woa konstruktor klasy Test z parametrem 10, ktry inicjuje t pami. Ta sekwencja operacji jest nazywana tworzeniem obiektu, czyli wystpienia klasy. W wikszoci przypadkw uytkownik nie musi si interesowa wspomnian procedur alokacji. Mona jednak wymieni pewne szczeglne sytuacje, gdy uytkownik powinien sam decydowa o sposobie przydziau pamici: Program tworzy i niszczy bardzo wiele maych obiektw (np. wzy drzewa, powizania listy jednokierunkowej, punkty, linie, komunikaty). Alokacja i dealokacja takich licznych obiektw moe atwo zdominowa czas wykonania programu, a take szybko wyczerpa zasoby pamici. Duy narzut czasowy jest pochodn niskiej efektywnoci standardowego alokatora. Narzut pamiciowy jest powodowany fragmentacj pamici swobodnej przy alokacji mieszaniny obiektw o rnych rozmiarach. Program, ktry musi dziaa nieprzerwanie przez dugi czas przy bardzo ograniczonych zasobach. Jest to sytuacja typowa dla systemw czasu rzeczywistego, ktre wymagaj gwarantowanego kwantum pamici z minimalnym narzutem. Istniejce rodowisko nie zapewnia procedur zarzdzania pamici (np. piszemy program, ktry ma obsugiwa mikroprocesor bez systemu operacyjnego). W takiej sytuacji programista musi opracowa procedury niskiego poziomu, odwoujce si nawet do adresw fizycznych komrek pamici. W pierwszych dwch przypadkach zastosowanie wasnych mechanizmw przydziau pamici moe jak pokazuje praktyka przynie popraw efektywnoci od dwch do nawet dziesiciu razy. Operatory new i delete (podobnie jak pozostae operatory standardowe) mona przecia wzgldem danej klasy. Ich prototypy maj wtedy posta: class X { //... void* operator new(size_t rozmiar); void operator delete(void* wsk); void* operator new [] (size_t rozmiar); void operator delete [] (void* wsk); }; gdzie typ size_t jest zalenym od implementacji typem cakowitym, uywanym do

utrzymywania rozmiarw obiektw; jego deklaracja znajduje si w pliku nagwkowym <stddef.h> Pierwsza para operatorw odnosi si do alokacji pojedynczych obiektw, za druga do tablic. Poniewa funkcja X::operator new() jest wywoywana przed konstruktorem, jej typ zwracany musi by void*, a nie X* (jeszcze nie ma obiektu X). Natomiast destruktor, wywoywany przed funkcj operatorow X::operator delete(), dekonstruuje obiekt, pozostawiajc do zwolnienia niezorganizowan pami. Dlatego argumentem funkcji X::operator delete() nie jest X*, lecz void*. Ponadto definiowane w klasie X::operator new() i X::operator delete() s statycznymi funkcjami skadowymi, bez wzgldu na to, czy s jawnie zadeklarowane ze sowem kluczowym static, czy te nie. Wasno ta jest konieczna z tych samych powodw, co wymienione wyej: wywoanie statycznej funkcji skadowej klasy nie wymaga istnienia obiektu tej klasy. W trzecim z wyej przytoczonych przypadkw programista podaje adres fizyczny lub adres symboliczny alokowanego obiektu. Mona si wtedy posuy prototypem funkcji operatorowej new() z dwoma argumentami i z pokazanym niej przykadowym blokiem: void* operator new(size_t, void* wsk) { return wsk; } gdzie wskanik wsk podaje adres, pod ktrym jest alokowany dany obiekt. Poniewa jest to konstrukcja programowa, ktra moe si odwoa bezporednio do sprztu, ustalono dla niej specjaln skadni wywoania. Jeeli np. adres pamici jest podany w postaci: void* bufor = (void*) 0xF00F; to wywoanie alokatora X::operator new() moe mie posta: X* wskb = new(bufor)X; Zauwamy, e kady operator new() przyjmuje size_t jako swj pierwszy argument; zatem w ostatnim wywoaniu rozmiar alokowanego obiektu jest dostarczany niejawnie. Podany niej przykad ilustruje skadni deklaracji, definicji i wywoa przecionych operatorw new() i delete(). Jest to jedynie ilustracja, jako e ani nie tworzy si w programie wielu obiektw, ani te nie jest to nawet makieta systemu czasu rzeczywistego. W klasie Nowa zadeklarowano zmienn statyczn licznik, ktra suy do zliczania obiektw po konstrukcji i destrukcji. Doliczanie i odliczanie odbywa si odpowiednio w konstruktorze i destruktorze klasy. W bloku main() wyjcie z ptli do-while nastpuje po wprowadzeniu z klawiatury znaku q albo Q; niezaleno od maej lub duej litery zapewnia funkcja toupper(char), ktra jest zadeklarowana w pliku nagwkowym ctype.h>. Przykad 6.16. # include <iostream.h> # include < stddef.h> # include < ctype.h> class Nowa { public: char znak_zliczany; int liczba_znakow;

Nowa(char znak); ~Nowa() { cout << "Destruktor...\n"; licznik--; } void dodaj_znak() { liczba_znakow++; } void* operator new(size_t rozmiar); void operator delete(void* wsk); static licznik; }; int Nowa::licznik = 0; Nowa::Nowa(char z) { cout << "Konstruktor...\n"; znak_zliczany = z; liczba_znakow = 0; licznik++; } void* Nowa::operator new(size_t rozmiar) { cout << "Alokacja: new...\n"; void* wsk = new char[rozmiar]; return wsk; } void Nowa::operator delete(void* wsk) { cout << "Dealokacja: delete... "; delete (void*) wsk; } int main() { char we; Nowa* wskNowa = new Nowa('x'); cout << "Napisz kilka liter 'x'; 'q'-koniec:\n"; do { //wczytujemy iksy cin >> we; if(we == wskNowa->znak_zliczany) wskNowa->dodaj_znak(); } while(toupper(we) != 'Q'); cout << "\nLiczba znakow " << wskNowa->znak_zliczany << ": "; cout << wskNowa->liczba_znakow << endl; cout << "Liczba obiektow: " << Nowa::licznik << endl; delete wskNowa; return 0; } Przykadowy wydruk moe mie posta: Alokacja: new... Konstruktor... Napisz kilka liter 'x'; 'q' - koniec: x x q

Liczba znakow x: 2 Liczba obiektow: 1 Destruktor... Dealokacja: delete...

7.4. Funkcje i klasy zaprzyjanione


Funkcje skadowe klasy mona uwaa za implementacj koncepcji, nazywanej w jzykach obiektowych przesyaniem komunikatw. Przy wywoaniu takiej funkcji obiekt, dla ktrego jest wywoywana, peni rol odbiorcy komunikatu; wartoci zmiennych, adresy, czy te obiekty, przekazywane jako argumenty aktualne funkcji, stanowi tre komunikatu. Np. wywoanie punkt1.ustaw(c,d); mona uwaa za adresowany do obiektu punkt1 komunikat o nazwie ustaw, ktrego treci s wartoci dwch zmiennych: c oraz d. Taki styl programowania jest charakterystyczny dla jzykw czysto obiektowych, jak np. Smalltalk. Styl ten zapewnia pen hermetyczno (ukrywanie informacji) klas, ktrych obiekty s dostpne wycznie za porednictwem funkcji skadowych, stanowicych ich publiczny interfejs. Do czsto jednak taki sztywny gorset, pod ktrym ukrywa si informacje prywatne, okazuje si zbyt ciasny i niewygodny. Jzyk hybrydowy, jakim jest C++, daje moliwo rozlunienia tego gorsetu. Pozwala on zwykym funkcjom i operatorom wykonywa operacje na obiekcie w podobny sposb, jak na obiektach typw podstawowych. Przy takim podejciu moglibymy nasz funkcj skadow ustaw() uczyni zwyk funkcj, ze zmienn punkt1 jako jednym z argumentw formalnych. Wtedy jej wywoanie miaoby posta: ustaw(punkt1,c,d); Tutaj punkt1 jest traktowany na rwni z pozostaymi argumentami. Zauwamy jednak, e teraz funkcja ustaw() bdzie operowa na kopii argumentu punkt1, a wic nie bdzie moga zmieni wartoci zmiennych skadowych obiektu punkt1. Mona temu atwo zaradzi, przesyajc parametr punkt1 przez referencj, a nie przez warto. Co wicej, funkcj ustaw() mona przecia, podajc rne definicje, np. dla ustawienia punktu na jednej z osi ukadu wsprzdnych, na paszczynie, czy w przestrzeni trjwymiarowej. Mona rwnie pomyle o rozszerzeniu definicji funkcji ustaw() tak, aby moga oddziaywa na stan kilku obiektw jednoczenie; np. wywoanie ustaw(punkt1, punkt2, c, d); mogoby przesya wartoci c oraz d z obiektu punkt1 do obiektu punkt2. W podobnych do opisanego wyej przypadkach, gdy decydujemy si odsoni cz informacji ukrytych w deklaracji klasy, moemy wykorzysta mechanizm tzw. funkcji zaprzyjanionych jzyka C++. Jak sugeruje nazwa, funkcje zaprzyjanione maj te same przywileje, co funkcje skadowe, chocia same nie s funkcjami skadowymi klasy, w ktrej zadeklarowano je jako zaprzyjanione. W szczeglnoci maj one dostp do tych elementw klasy, ktrych deklaracje poprzedzaj etykiety private: i protected:. Deklaracj funkcji zaprzyjanionej (a take klasy zaprzyjanionej) poprzedza sowo kluczowe friend. Przy tym jest obojtne, czy tak deklaracj umieszcza si w publicznej, czy w prywatnej

czci deklaracji klasy. Poniewa funkcja zaprzyjaniona nie jest funkcj skadow klasy, zatem jej lista argumentw nie zawiera ukrytego wskanika this. Zasig funkcji zaprzyjanionej jest inny ni zasig funkcji skadowych klasy: funkcja zaprzyjaniona jest widoczna w zasigu zewntrznym, tj. takim samym zasigu, jak klasa, w ktrej zostaa zadeklarowana. Przykad 6.17. // Funkcja zaprzyjazniona ustaw() #include <iostream.h> class Punkt { public: Punkt(int, int); int fx() { return x; } int fy() { return y; } friend void ustaw(Punkt&, int,int); private: int x,y; }; Punkt::Punkt(int a, int b): x(a),y(b) {} void ustaw(Punkt& p, int c, int d) { p.x += c; p.y += d; } int main() { Punkt punkt1(3,4); cout << "punkt1.x przed ustaw():" << punkt1.fx() << endl; ustaw(punkt1, 5, 5); cout << "punkt1.x po ustaw():" << punkt1.fx() << endl; return 0; } Wydruk z programu ma posta: punkt1.x przed ustaw(): 3 punkt1.x po ustaw(): 8 Dyskusja. Poniewa funkcja ustaw() nie jest funkcj skadow, jej definicja nie jest poprzedzona nazw klasy i operatorem zasigu. Zauwamy, e w definicji nie wystpuje specyfikator friend. Zwrmy take uwag na fakt, e ze wzgldu na brak niejawnego wskanika this, funkcja zaprzyjaniona nie moe si odwoywa do zmiennych skadowych bezporednio, lecz poprzez obiekt p klasy Punkt. Argument aktualny punkt1 jest przekazywany przez referencj; dziki temu stan obiektu punkt1 (tj. wartoci jego zmiennych skadowych) moe by modyfikowany przez funkcj ustaw(). Funkcja skadowa jednej klasy moe by zaprzyjaniona z inn klas; ilustracj tej moliwoci jest poniszy cig deklaracji:

class Pierwsza { // ... void f(); }; class Druga { // ... friend void Pierwsza::f(); }; Deklaracje funkcji, poprzedzone specyfikatorem friend pozwalaj take deklarowa klasy, ktre odwouj si do siebie nawzajem. Charakterystycznym przykadem moe by deklaracja operatora mnoenia wektora przez macierz, jak pokazano niej na przykadowym cigu deklaracji. Zauwamy, e w tym przypadku konieczne jest uycie deklaracji referencyjnej klasy Wektor przed waciwymi deklaracjami klas. class Wektor; class Macierz // ... friend Wektor }; class Wektor // ... friend Wektor }; { operator*(Macierz&, Wektor&); { operator*(Macierz&, Wektor&);

7.4.1.

Zaprzyjaniony operator '<<'

Dotychczas nie pomylelimy o tym, jak uatwi sobie wyprowadzanie stanu obiektu. Wprawdzie dla nasze przykadowej klasy Punkt zdefiniowalimy funkcje skadowe fx() i fy() dla uzyskania dostpu do zmiennych prywatnych, ale kade wyprowadzenie wartoci x oraz y do strumienia cout wymagao pisania oddzielnych instrukcji z odpowiednimi argumentami dla operatora wstawiania <<. Obecnie wykorzystamy moliwo przecienia operatora << dla wyprowadzenia penego stanu obiektu jedn instrukcj. W pliku nagwkowym <iostream.h> znajduje si deklaracja klasy strumieni wyjciowych ostream oraz szereg definicji przeciajcych operator <<, w ktrych pierwszym jego argumentem jest obiekt klasy ostream. Definicje te pozwalay nam uywa operatora << do wyprowadzania wartoci rnych typw, np. char, int, long int, double, czy char* (acuchw). Projektujc wasne klasy, uytkownik moe wprowadza wasne definicje operatora << (w razie potrzeby take >>). Maj one nastpujc posta ogln: ostream& operator<<(ostream& os, nazwa-klasy& { // Ciao funkcji operator<<() return os; } ob)

Pierwszym argumentem funkcji operator<<() jest referencja do obiektu typu ostream. Oznacza to, e os musi by strumieniem wyjciowym. Do drugiego argumentu, ob, przesya si w wywoaniu obiekt (adres) typu nazwa-klasy, ktry bdzie wyprowadzany na standardowe wyjcie. Zauwamy, e strumie wyjciowy os musi by przekazywany przez referencj, poniewa jego wewntrzny stan bdzie modyfikowany przez operacj wyprowadzania. Funkcja operator<<() zawsze zwraca referencj do swojego pierwszego argumentu, tj. strumienia

wyjciowego os; ta wasno oraz fakt, e operator << wie od lewej do prawej, pozwala uywa go wielokrotnie w tej samej instrukcji wyprowadzania. Np. w instrukcji cout << ob1 << "\n"; wyraenie jest wartociowane tak, jak gdyby byo zapisane w postaci: ( cout << ob1 ) << "\n" Wartociowanie wyraenia w nawiasach okrgych wstawia ob1 do cout i zwraca referencj do cout; ta referencja staje si pierwszym argumentem dla drugiego operatora <<. Tak wic drugi operator << jest przykadany tak, jak gdyby napisano: cout << "\n"; Wyraenie cout << "\n" rwnie zwraca referencj do cout, a wic moe po nim wystpi nastpny operator <<, i.t.d. Funkcja operator<<() nie powinna by funkcj skadow klasy, na ktrej obiektach ma operowa. Wynika to std, e gdyby bya funkcj skadow, to jej pierwszym z lewej argumentem, przekazywanym niejawnie poprzez wskanik this, byby obiekt, ktry generuje wywoanie tej funkcji. Tymczasem w naszej definicji pierwszym z lewej argumentem musi by strumie klasy ostream, natomiast prawy operand jest obiektem, ktry chcemy wyprowadzi na standardowe wyjcie (kolejnoci tej nie mona zmieni, poniewa tak kolejno argumentw narzucaj definicje w <iostream.h>). Wobec tego funkcj operator<<() musimy zadeklarowa jako funkcj zaprzyjanion klasy, na ktrej obiektach ma operowa. Ilustruje to pokazany niej prosty przykad. Przykad 6.18. // Zaprzyjazniony operator << #include <iostream.h> class Punkt { public: Punkt(int, int); friend ostream& operator<<(ostream&, Punkt&); private: int x,y; }; Punkt::Punkt(int a, int b): x(a),y(b) {} ostream& operator<<(ostream& os, Punkt& ob) { os << ob.x << ", " << ob.y << "\n"; return os; } int main() { Punkt punkt1(3,4), punkt2(10,15); cout << punkt1 << punkt2; return 0; }

Wydruk z programu bdzie mia posta: 3, 4 10, 15 Dyskusja. Moe si wydawa dziwnym, e w definicji operatora << uywa si tego samego symbolu. Zauwamy jednak, e operatory << uywane w definicji wyprowadzaj liczby cakowite i acuchy; zatem wywouj one ju istniejce w pliku <iostream.h> definicje, w ktrych drugim operandem jest liczba typu int lub acuch (typu char*). Drugim godnym uwagi faktem jest to, e instrukcja wyprowadzania w definicji operatora << wysya wartoci x oraz y do zupenie dowolnego strumienia klasy ostream, przekazywanego do funkcji operator<<() jako parametr aktualny. W wywoaniu operatora << w bloku main() uylimy cout jako parametru aktualnego. Rwnie dobrze moglibymy jednak skierowa wyjcie naszego programu do pliku, zamiast na konsol doczon do cout. W takim przypadku naleaoby wykorzysta definicje, zawarte w pliku nagwkowym <fstream.h>.

7.4.2.

Klasy zaprzyjanione

Kada klasa moe mie wiele funkcji zaprzyjanionych; jest zatem naturalne, aby ca klas uczyni zaprzyjanion z inn klas. Jeeli klasa A ma by zaprzyjaniona z klas B, to deklaracja klasy A musi poprzedza deklaracj klasy B. Schemat deklaracji ilustruje poniszy przykad: kada funkcja skadowa klasy Pierwsza staje si funkcj zaprzyjanion klasy Druga. W rezultacie wszystkie skadowe prywatne, publiczne, i chronione klasy Druga staj si dostpne dla klasy zaprzyjanionej Pierwsza. class Pierwsza { // ... }; class Druga { public: Druga(int i = 0, int j = 0): x(i),y(j) {} friend class Pierwsza; private: int x, y; };

7.5. Obiekty i funkcje


Prawie wszystkie prezentowane dotd przykady klas zawieray dwa rodzaje funkcji skadowych: Funkcje, ktre mogy zmienia wartoci zmiennych skadowych, tj. stan obiektu. Funkcje takie w jzykach obiektowych nazywa si operacjami mutacji (ang. mutator operations). Funkcje, ktre jedynie podaway aktualne wartoci zmiennych skadowych, tj. biecy stan obiektu. Funkcje takie w jzykach obiektowych nazywa si operacjami dostpu (ang. accessor operations, lub field accessors); w jzyku C++ nazywamy je funkcjami staymi. Obecnie zajmiemy si bardziej szczegowo zagadnieniem zmian i zachowania stanu obiektu w aspekcie obu rodzajw funkcji.

7.5.1.

Obiekty i funkcje stae

Obiekty klas, podobnie jak obiekty typw wbudowanych, mona deklarowa jako stae symboliczne, poprzedzajc deklaracj sowem kluczowym const. Np. dla klasy Punkt z konstruktorem domylnym Punkt() moemy utworzy obiekt stay

punkt1 instrukcj deklaracji: const Punkt punkt1; Do tego samego celu mona wykorzysta konstruktor z parametrami: const Punkt punkt2(3,2); W obu przypadkach kompilator C++ zaakceptuje definicje zmiennych, ktre zostan odpowiednio zainicjowane przez konstruktory. Natomiast kompilator odrzuci kad prb zmiany stanu obiektw punkt1 i punkt2 zarwno przez bezporednie przypisanie, jak i przez przypisanie za pomoc wskanikw, np. Punkt p1; punkt1 = p1; Punkt* wsk = &punkt1; Gdyby jednak zadeklarowano wskanik stay const Punkt* wsk; to oczywicie mona mu przypisa adres obiektu staego punkt1 wsk = &punkt1; Pozostaje jednak otwarte pytanie: czy wolno dla obiektu staego wywoywa funkcje (np. funkcj ustaw()), ktre mog zmieni jego stan? Logika mwi, e takie operacje nie powinny by dopuszczalne. Dla tak prostej klasy jak Punkt, kompilator mgby prawdopodobnie odrni funkcje, ktre zmieniaj wartoci zmiennych skadowych x oraz y od funkcji, ktre pozostawiaj je bez zmian. W oglnoci jednak nie jest to moliwe. Tak wic w praktyce programista musi pomc kompilatorowi przez odpowiednie zadeklarowanie i zdefiniowanie tych funkcji, ktre zachowuj stan obiektu. Wyrnienie takich funkcji jest moliwe przez dodanie sowa kluczowego const do ich deklaracji i definicji. Zasig tych funkcji bdzie si pokrywa z zasigiem klasy, a ich deklaracje maj posta: typ-zwracany nazwa-funkcji(parametry) const;

Sowo kluczowe const musi rwnie pojawi si po nagwku w definicji funkcji. Np. definicja funkcji staej fx(), umieszczona wewntrz deklaracji klasy Punkt bdzie mie posta: int fx() const { return x; } Podany niej przykad ilustruje przeprowadzon dyskusj.

Przykad 6.19. #include <iostream.h> class Punkt { public: Punkt(): x(0),y(0) {} Punkt( int a, int b ): x(a),y(a) {} void ustaw(int c, int d) { x = x + c; y = y + d; } int fx() const { return x; } int fy() const { return y; } private: int x,y; }; int main() { const Punkt p0; cout << "p0.x= " << p0.fx() << "\n"; cout << "p0.y= " << p0.fy() << "\n"; Punkt p1; // p0 = p1; Niedopuszczalne // Punkt* wsk = &p0; Niedopuszczalne // p0.ustaw(3,4); Niedopuszczalne return 0; } Analiza programu. Niedopuszczalno przypisania p0 = p1 oraz wywoania p0.ustaw(3,4); jest oczywista, poniewa obiekt p0 jest obiektem staym i jego skadowe nie mog by zmieniane w programie przez adn operacj. W drugiej bdnej instrukcji, Punkt* wsk = &p0; prbuje si wskanikowi przypisa adres obiektu staego, co, podobnie jak dla staych typw wbudowanych, jest niedopuszczalne. FUwaga 1. Niektre kompilatory (np. Borland C++,v.3.1) akceptuj kod rdowy, w ktrym funkcje, nie wyrnione sowem kluczowym const, mog by wywoywane dla obiektu staego. Kompilatory te wprawdzie ostrzegaj uytkownika, ale i tak produkuj kod wykonalny, ktry zmienia stan obiektw staych! Przypomnijmy jeszcze, e kada niestatyczna funkcja skadowa zawiera niejawny argument this, ktry (domylnie) wystpuje jako pierwszy z lewej w wykazie argumentw. Domylna deklaracja tego wskanika dla kadej funkcji skadowej pewnej klasy X ma posta: X *const this;. Dla staych funkcji skadowych domylna deklaracja bdzie: const X *const this;.

7.5.2.

Kopiowanie obiektw

W zasadzie istniej dwa przypadki, w ktrych wystpuje potrzeba kopiowania obiektw: gdy deklarowany obiekt pewnej klasy inicjuje si innym, wczeniej utworzonym obiektem; gdy obiektowi przypisuje si inny obiekt w instrukcji przypisania. Obiekt jest rwnie kopiowany wtedy, gdy jest przekazywany przez warto jako parametr aktualny funkcji oraz gdy jest wynikiem zwracanym przez funkcj. Kopiowanie wystpie (obiektw) typw wbudowanych, np. char, int, etc. oznacza po prostu kopiowanie ich wartoci. Przykadowo moemy napisa: int i(10); // to samo co int i = 10; int j = i; W obu przypadkach operator = jest operatorem inicjowania, a nie przypisania.

Dla obiektw klasy, w ktrej nie zdefiniowano specyficznych dla niej operacji, kopiowanie i przypisywanie obiektw tej klasy bdzie wykonywane za pomoc generowanego przez kompilator konstruktora kopiujcego i generowanego operatora przypisania. Np. dla klasy X bd generowane automatycznie: konstruktor kopiujcy X::X(const X&) oraz operator przypisania X& operator=(const X&). Funkcje te wykonuj kopiowanie obiektw skadowa po skadowej. Np. dla klasy: class Punkt { int x,y; public: Punkt(int, int); }; moemy zadeklarowa obiekt: Punkt p1(3,4); a nastpnie obiekt p2, ktrego pola x, y bd inicjowane wartociami pl obiektu p1: Punkt p2 = p1; Zwrmy uwag na nastpujcy fakt: gdyby deklaracja obiektu p2 miaa posta: Punkt p2;, to deklaracj klasy Punkt musielibymy rozszerzy o konstruktor domylny, np. Punkt() { x = 0; y = 0; }. Tymczasem poprawna deklaracja Punkt p2 = p1; nie wymaga istnienia konstruktora domylnego. Wniosek std taki, e obiekt p2, inicjowany w deklaracji obiektem p1, nie jest tworzony przez aden zadeklarowany konstruktor klasy! I tak jest istotnie: obiekt p2 jest tworzony, skadowa po skadowej, przez generowany konstruktor kopiujcy Punkt::Punkt(const Punkt& p1). Konstruktor ten jest wywoywany niejawnie przez kompilator, przy czym obiekt p1 przechodzi przez referencj jako parametr aktualny. Zauwamy te, e wprawdzie konstruktor kopiujcy operuje bezporednio na obiekcie p1, a nie na jego kopii, to jednak obiekt p1 nie ulegnie adnym zmianom, poniewa argument formalny konstruktora kopiujcego jest poprzedzony sowem kluczowym const. Kopiowanie obiektw przez niejawne wywoanie automatycznie generowanego konstruktora kopiujcego podlega, niestety, bardzo istotnemu ograniczeniu. Operacja ta sprawdza si jedynie dla obiektw, ktre nie zawieraj wskaza na inne obiekty. Jeeli obiekt jest wystpieniem klasy, w ktrej zadeklarowano wskanik do jej wasnego obiektu lub obiektu innej klasy, to przy wyej opisanej procedurze zostan wprawdzie skopiowane wskaniki, ale nie bd skopiowane obiekty wskazywane. Dlatego te kopiowanie z niejawnym wywoaniem generowanego przez kompilator konstruktora kopiujcego okrela si jako kopiowanie pytkie (ang. shallow copy). Pytkie kopiowanie ma jeszcze jedn wad: jeeli w klasie zdefiniowano destruktor, to po kadej operacji kopiowania zmiennych wskanikowych bdziemy mie po dwa wskazania na ten sam adres. Wwczas wywoywany przed zakoczeniem programu (lub funkcji) destruktor bdzie dwukrotnie niszczy ten sam obiekt! Pokazany niej przykad ilustruje taki wanie przypadek. Przykad 6.20. #include <iostream.h> class Niepoprawna { public: Niepoprawna() { wsk = new int(10); } ~Niepoprawna() { delete wsk; } private: int* wsk; }; int main() { Niepoprawna z1; Niepoprawna z2 = z1; return 0; }

Dyskusja. Druga instrukcja deklaracji w bloku main() wywouje generowany konstruktor kopiujcy, inicjujc obiekt z2 obiektem z1. Wskanik wsk typu int* obiektu z2 zostaje zainicjowany kopi wskanika wsk obiektu z1. W rezultacie wsk obu obiektw wskazuj na ten sam obiekt typu int o wartoci 10. Przed zakoczeniem programu wykonywany jest dwukrotnie destruktor, odpowiednio dla obiektw z1 i z2. Za kadym razem niszczony jest ten sam obiekt typu int*, co moe przynie niepodane konsekwencje. Podobna sytuacja powstaje w przypadku kopiowania obiektw przez przypisanie. Dla wikszoci klas rozwizaniem ukazanego problemu jest zdefiniowanie wasnego konstruktora kopiujcego i operatora przypisania. Waciwe zdefiniowanie tych funkcji pozwoli nam na tzw. kopiowanie gbokie (ang. deep copy), przy ktrym bd kopiowane nie tylko wskaniki, ale i obiekty przez nie wskazywane. Prawidowo bdzie te przebiega destrukcja obiektw. Przykad 6.21. #include <iostream.h> class Poprawna { public: Poprawna(): wsk(new int(10)) {} ~Poprawna() { delete wsk; } Poprawna(const Poprawna& nz) { wsk = new int(*nz.wsk); } Poprawna& operator=(const Poprawna& nz) { if(this != &nz) { delete wsk; wsk = new int(*nz.wsk); } return *this; } private: int* wsk; }; int main() { Poprawna z1; Poprawna z2 = z1; return 0; } Dyskusja. W programie zdefiniowano wasny konstruktor kopiujcy i wasny operator przypisania (nie uywany). Pierwsza instrukcja w bloku main() wywouje konstruktor Poprawna() { wsk = new int(10); }, ktry tworzy obiekt z1, a w nim podobiekt typu int, na ktry wskazuje wsk. Druga instrukcja wywouje konstruktor kopiujcy Poprawna(const Poprawna&) z parametrem aktualnym z1; konstruktor ten tworzy nowy podobiekt typu int i umieszcza go pod innym adresem ni pierwszy (oczywicie oba podobiekty maj t sam warto 10). Dziki temu wywoywany dwukrotnie przed zakoczeniem programu destruktor niszczy za kadym razem inny obiekt.

FUwaga. Instrukcj Poprawna z2 = z1; mona take zapisa w postaci Poprawna z2(z1);, pokazujcej wyranie, e obiekt z2 jest inicjowany obiektem z1.

7.5.3.

Przekazywanie obiektw do/z funkcji

Syntaktyka i semantyka przekazywania obiektw do funkcji jest identyczna dla obiektw typw predefiniowanych (np. int, double), jak i obiektw klas definiowanych przez uytkownika. Dla typu zdefiniowanego przez uytkownika parametr formalny funkcji bdzie klas, wskanikiem, lub referencj do klasy. Funkcj tak wywoujemy z parametrem aktualnym, bdcym odpowiednio obiektem danej klasy, adresem obiektu, lub zmienn referencyjn. Podobnie jak dla typw wbudowanych, obiekty klas s domylnie przekazywane przez warto, a semantyka przekazywania jest taka sama, jak semantyka inicjowania. Obiekty przekazywane do funkcji tworzone s jako automatyczne, tzn. takie, ktre tworzy si za kadym razem gdy jest wykonywana ich instrukcja deklaracji, i niszczy za kadym razem, gdy sterowanie opuszcza blok zawierajcy deklaracj. Jeeli funkcja jest wywoywana wiele razy (co czsto si zdarza), to tyle samo razy jest wykonywane tworzenie i niszczenie obiektw, a wic za kadym razem mamy nowy obiekt, z nowymi inicjalnymi wartociami zmiennych skadowych. Pokazany niej przykad ilustruje przekazywanie obiektu do funkcji przez warto. Przykad 6.22. #include <iostream.h> class Test { public: Test(int a): x(a) { cout << "Konstrukcja...\n"; } Test(const Test& t) { this->x = t.x; cout << "Konstrukcja kopii...\n"; } ~Test() { cout << "Destrukcja...\n"; } int podaj() { return x; } void ustaw(int i) { x = i; } private: int x; }; void f(Test arg) { arg.ustaw(50); cout << "Funkcja f: "; cout << "t1.x == " << arg.podaj() << '\n'; } int main() { Test t1(10); cout << "t1.x == " << t1.podaj() << '\n'; f(t1); cout << "t1.x == " << t1.podaj() <<'\n'; return 0; } Wydruk z programu ma posta:

Konstrukcja... t1.x == 10 Konstrukcja kopii... Funkcja f: t1.x == 50 Destrukcja... t1.x == 10 Destrukcja... Analiza programu. Wykonanie instrukcji deklaracji Test t1(10); tworzy obiekt t1 za pomoc konstruktora Test(int), od ktrego pochodzi pierwszy wiersz wydruku. W instrukcji cout wywouje si funkcj skadow podaj(), ktra wywietla drugi wiersz wydruku. Instrukcja wywoania funkcji f(t1), z argumentem przekazywanym przez warto, wywouje konstruktor kopiujcy Test(const Test&), ktry generuje trzeci wiersz wydruku. Czwarty wiersz jest generowany przez funkcj f(); wypisuje ona warto pola x lokalnej kopii argumentu, tj. obiektu utworzonego przez konstruktor kopiujcy. Zauwamy, e warto x w kopii obiektu zostaa zmieniona na 50 funkcj skadow ustaw(), wywoan z bloku funkcji f(), co pokazuje czwarty wiersz wydruku. Po wydrukowaniu czwartego wiersza sterowanie opuszcza blok funkcji f(), wywoujc destruktor ~Test(), ktry niszczy obiekt, utworzony przez konstruktor kopiujcy (pity wiersz z tekstem Destrukcja...). Po oprnieniu stosu funkcji f() sterowanie wraca do bloku main(), wywoujc w instrukcji cout funkcj skadow podaj() obiektu t1. Wykonanie tej instrukcji pokazuje, e warto pola x pozostaa bez zmiany, jako e zmiana x na 50 bya wykonywana przez funkcj f() na kopii obiektu t1, a nie na samym obiekcie. Ostatni wiersz wydruku sygnalizuje destrukcj obiektu t1, gdy sterowanie opuszcza blok funkcji main(). W powyszym przykadzie warto zwrci uwag na dwa momenty. Gdy jest tworzona kopia obiektu przekazywanego do argumentu formalnego funkcji, nie wywouje si konstruktora obiektu, lecz konstruktor kopiujcy. Powd jest oczywisty: poniewa konstruktor jest w oglnoci uywany do inicjowania obiektu (np. nadania wartoci pocztkowych zmiennym skadowym), to nie moe by woany gdy wykonuje si kopi ju istniejcego obiektu. Jeeli kopia ma zosta przesana do funkcji, to zaley nam przecie na aktualnym stanie obiektu, a nie na jego stanie pocztkowym. Natomiast jest celowe i konieczne wywoanie destruktora, gdy funkcja koczy dziaanie. Jest to konieczne, poniewa obiekt w bloku funkcji mgby by uyty do jakiej operacji, ktra musi zosta uniewaniona, gdy obiekt wychodzi poza swj zasig. Przykadem moe by alokacja pamici dla kopii; pami ta musi by zwolniona po wyjciu z bloku funkcji. Destrukcja kopii obiektu uywanej do wywoania funkcji moe by rdem pewnych kopotw, szczeglnie w przypadku dynamicznej alokacji pamici. Jeeli np. obiekt uyty jako argument alokuje pami na kopcu (ang. heap) i zwalnia j po destrukcji, to i jego kopia bdzie zwalnia t sam pami, gdy zostanie wywoany jej destruktor. Jednym ze sposobw na uniknicie tego rodzaju niespodzianek jest przekazywanie do funkcji adresu obiektu zamiast samego obiektu. Wtedy, co jest oczywiste, nie bdzie tworzony aden nowy obiekt, i nie bdzie woany aden destruktor przy wyjciu z bloku funkcji. Adresy obiektw mona przesya do funkcji albo za pomoc wskanikw, albo referencji. Przy tym, jeeli chcemy unikn zmiany argumentu aktualnego przez operacje wykonywane w bloku funkcji, to wystarczy w definicji funkcji zadeklarowa argument formalny ze sowem kluczowym const. Podany niej przykad ilustruje wariant ze wskanikiem i z referencj. Przykad 6.23.

#include <iostream.h> class Test { public: Test(int a): x(a){ cout << "Konstrukcja...\n"; } Test(const Test& t) { this->x=t.x; cout<<"Konstrukcja kopii...\n"; } ~Test() { cout << "Destrukcja...\n"; } int podaj() { return x; } void ustaw(int i) { x = i; } private: int x; }; void fwsk(Test* arg) { arg->ustaw(50); cout << "Funkcja fwsk: "; cout << "arg.x == " << arg->podaj() << '\n'; } void fref(Test& arg) { arg.ustaw(60); cout << "Funkcja fref: "; cout << "arg.x == " << arg.podaj() << '\n'; } int main() { Test t1(10); cout << "t1.x == " << t1.podaj() << '\n'; fwsk(&t1); // fref(t1); cout << "t1.x == " << t1.podaj() << '\n'; return 0; } Wydruk z programu ma posta: Konstrukcja... t1.x == 10 Funkcja fwsk: arg.x == 50 t1.x == 50 Destrukcja... Dyskusja. Jak pokazuje wydruk, tworzony i niszczony jest tylko jeden obiekt. Argumentem funkcji fwsk() jest wskanik do obiektu klasy Test; wobec tego jej argument aktualny w wywoaniu musi by adresem obiektu tej klasy. Poniewa w bloku funkcji fwsk() jest wywoywana funkcja skadowa ustaw() zmieniajca warto x, to po wykonaniu funkcji fwsk() warto ta (50) zostaa wywietlona przez bezporednie wywoanie funkcji podaj() dla obiektu t1. Gdyby w funkcjach fwsk() i fref() wyeliminowa wywoanie funkcji, zmieniajcej stan obiektu, to ich prototypy mogyby mie posta: void fwsk(const Test* arg); i void fref(const Test& arg); FUwaga 1. W podanych wyej przykadach konstruktory kopiujce i destruktory

wprowadzono dla lepszego zobrazowania wydrukw (obiekty nie zawieraj wskanikw do innych obiektw). Gdyby ich nie zadeklarowano, kopiowanie i destrukcj wykonayby funkcje, generowane przez kompilator. FUwaga 2. W dobrze skonstruowanym programie mae obiekty mog by przesyane przez warto, a due przez wskaniki lub referencje. Podobnie jak dla typw wbudowanych, wynikiem prowadzonych w bloku funkcji oblicze moe by obiekt klasy zdefiniowanej przez uytkownika. Typowa definicja takiej funkcji moe mie jedn z postaci: klasa nazwa-funkcji(klasa obiekt) { //... return obiekt; } lub klasa& nazwa-funkcji(klasa& obiekt) { //... return obiekt; } W pierwszym przypadku, zarwno przy wywoaniu funkcji, jak i powrocie z funkcji, bdzie wywoywany konstruktor kopiujcy (domylny lub zdefiniowany w klasie). W drugim przypadku obiekt bdzie przesany do funkcji przez referencj (lub sta referencj), i w taki sam sposb przekazany z funkcji. Stosujc technik referencji naley pamita o niebezpieczestwie przekazania referencji do obiektu, ktry przestaje istnie po wyjciu z bloku funkcji, np. Test& g() { Test ts; return ts; } Tutaj zmienna lokalna ts przestaje istnie po wykonaniu funkcji, a wic funkcja zwraca wiszc referencj referencj do nieistniejcego obiektu. Przykad 6.24. #include <iostream.h> class Test { public: Test(int a): x(a) { cout << "Konstrukcja...\n"; } Test(const Test&) { this->x = t.x; cout << "Konstrukcja kopii...\n"; } ~Test() { cout << "DESTRUKCJA...\n"; } Test& operator=(const Test& t) { this->x = t.x; cout << "Przypisanie...\n"; return *this; } int podaj() { return x; } private: int x; };

Test g(Test arg) { cout << "Funkcja g: "; cout << "arg.x == " << arg.podaj() << '\n'; return arg; } int main() { Test t1(10), t2(20); t1 = g(t2); cout << "t1.x == " << t1.podaj() << '\n'; return 0; } Wydruk z programu ma posta: Konstrukcja... Konstrukcja... Konstrukcja kopii... Funkcja g: arg.x == 20 Konstrukcja kopii... DESTRUKCJA... Przypisanie... DESTRUKCJA... t1.x == 20 DESTRUKCJA... DESTRUKCJA... Analiza programu. Pierwsze dwa wiersze wydruku to (jedyne) dwa wywoania konstruktora Test(int). Wykonanie instrukcji t1 = g(t2); pociga za sob nastpujc sekwencj czynnoci: wywoanie konstruktora kopiujcego (Konstrukcja kopii...), ktry tworzy obiekt tymczasowy dla parametru arg wywoanie funkcji g() z argumentem utworzonym przez konstruktor kopiujcy wywietlenie napisu: Funkcja g: arg.x == 20, w ktrym liczba 20 jest wynikiem wywoania funkcji skadowej podaj() wywoanie konstruktora kopiujcego dla utworzenia obiektu tymczasowego (wyraenia, przekazywanego przez instrukcj return) destrukcj pierwszego obiektu tymczasowego przypisanie wartoci funkcji g() do t1 wykonywane przez wywoanie przecionego operatora przypisania destrukcj drugiego obiektu tymczasowego wywietlenie odpowiedzi z wywoania funkcji skadowej podaj() destrukcj obiektw t1 i t2.

7.5.4. Konwersje obiektw


Kompilacja i wykonanie programw w jzyku C++ prawie zawsze wymaga wykonania wielu konwersji typw. Zdecydowana wikszo tych konwersji wykonywana jest niejawnie, bez udziau programisty. Typowym przykadem moe by proces dopasowania argumentw funkcji, w szczeglnoci argumentw funkcji przecionych. Oczywicie programista moe dokonywa konwersji jawnych za pomoc operatora konwersji (), ale naley to czyni raczej oszczdnie i tylko w przypadkach naprawd koniecznych.

Natomiast w dotychczasowej dyskusji o klasach i obiektach klas w zasadzie nie zwracalimy uwagi na fakt, e konwersja towarzyszy kreowaniu obiektw. Wemy najbliszy przykad z p. 6.5.3. Utworzenie obiektu klasy Test wymagao uycia konstruktora Test::Test(int y), ktry zmienn y typu int przeksztaca w obiekt typu Test. Przykad ten mona uoglni: jednoargumentowy konstruktor klasy X mona traktowa jako przepis, ktry z argumentu konstruktora tworzy obiekt klasy X. Zauwamy, e argument konstruktora nie musi by typu wbudowanego; moe nim by zmienna innej klasy, o ile tylko potrafimy zdefiniowa metod przeksztacenia obiektu danej klasy w obiekt innej klasy. Wykorzystanie konstruktora do konwersji nie jest moliwe, gdy chcemy dokona konwersji w drug stron, tj. przeksztaci obiekt klasy do typu wbudowanego. W takich przypadkach definiujemy specjaln funkcj skadow konwersji. Oglna posta funkcji konwersji dla klasy X jest nastpujca: X::operator T() { return w; } gdzie T jest typem, do ktrego dokonujemy konwersji, za w wyraeniem, ktrego argumentami musz by skadowe klasy, poniewa funkcja konwersji operuje na obiekcie, dla ktrego jest woana. Zauwamy, e w definicji funkcji konwersji nie podaje si ani typu zwracanego, ani argumentw. Przykad 6.25. #include <iostream.h> class Test { public: Test(int i): x(i) {} operator int() { return x*x; }; private: int x; }; int main() { int num1 = 2, num2 = 3; Test t1(num1), t2(num2); int ii; ii = t1; cout << ii << endl; ii = 10 + t2; cout << ii << endl; return 0; } Wydruk z programu ma posta: 4 19 Dyskusja. W przykadzie mamy konwersje w obie strony: jednoparametrowy konstruktor Test(int i) przeksztaca argument i typu int w obiekt klasy Test, a funkcja konwersji operator int() { return x*x; }; pozwala uywa obiekty typu Test w taki sam sposb, jak obiekty typu int.

Przykad 6.26. #include <iostream.h> class Boolean { public: enum logika { false = 0, true = 1 }; //konstruktory Boolean(): z(true) {} Boolean(int num): z(num != 0) { } Boolean(double d) { z = (d != 0); } Boolean (void* wsk): z(wsk != 0) { } //Konwersja operator int() const { return z; } //Negacja Boolean operator!() const { return !z; } private: char z; }; Boolean pierwiastki(double a, double b, double c) { int pr = b*b >= 4*a*c; cout << pr << endl; return pr; } int main() { Boolean b1(Boolean::true); Boolean b2(5); int ii = !b1 || b2; cout << "ii = " << ii << endl; int* wsk = new int(10); Boolean b3(wsk); Boolean b4(3.5); double a = 1, b = 4, c = 3; if(pierwiastki(a,b,c)) cout << "Dwa" << endl; return 0; } Wydruk z programu ma posta: ii = 1 1 Dwa Dyskusja. Klasa Boolean imituje predefiniowany typ bool, ktry dopiero ostatnio wprowadzono do standardu jzyka C++. W klasie zdefiniowano cztery konstruktory, ktre mog tworzy obiekty typu Boolean. Pierwszy z nich, domylny konstruktor bezparametrowy, wychodzi poza opisan wczeniej konwencj, tym niemniej bywa uyteczny, np. przy tworzeniu tablic wartoci logicznych. W definicji konstruktora z parametrem typu int, Boolean(int num): z(num != 0) { } uyto czciej obecnie stosowanej notacji, ni starsza, w ktrej napisalibymy: Boolean(int num) { z = num != 0; }

Wartoci logiczne prawda i fasz zostay zdefiniowane jako typ wyliczeniowy logika z jawnie zainicjowanymi staymi true i false. Zdefiniowano take przeciony operator negacji logicznej. Funkcja konwersji do typu int jest funkcj sta, podobnie jak funkcja operator!(). Obiekty klasy Boolean s wykorzystywane w instrukcji przypisania int ii = !b1 || b2; Wykonanie tej instrukcji przebiega nastpujco: a) Dla obiektu b1 zostaje wywoana funkcja operator!(); powrt z tej funkcji wymaga utworzenia obiektu tymczasowego. Obiekt taki jest tworzony przez wywoanie konstruktora Boolean(int). b) Po wykonaniu negacji zostaje dwukrotnie wywoana funkcja konwersji operator int() dla obiektw !b1 oraz b2, co pozwala obliczy warto alternatywy logicznej. c) Warto alternatywy logicznej zostaje przypisana do ii. Obiekt b3(wsk) jest tworzony przez wywoanie konstruktora Boolean(void* wsk) { z = wsk != 0; }. Wykorzystuje si tutaj wasno typu void*, do ktrego moe by automatycznie (niejawnie) przeksztacony wskanik dowolnego typu. W instrukcji if wywoywana jest funkcja pierwiastki(). Poniewa funkcja jest typu Boolean, przy powrocie tworzony jest obiekt tymczasowy wywoaniem konstruktora Boolean(int), a nastpnie zostaje wywoana dla tego obiektu funkcja konwersji, jako e wyraenie w instrukcji if musi dawa warto liczbow.

7.5.5. Klasa String


Konwersje typw okazuj si szczeglnie przydatne w operacjach na acuchach znakw i dlatego powicimy temu tematowi osobny podrozdzia. Przypomnijmy, e podstawowe operacje dla typu char* (obliczanie dugoci acucha, kopiowanie, konkatenacja acuchw, etc.) s zadeklarowane w pliku nagwkowym string.h, za kilka operacji konwersji acuchw na liczby zadeklarowano w stdlib.h. Operacje te s zapoyczone z ANSI C. Zadeklarowane w pliku string.h bardzo uyteczne funkcje operuj na zakoczonych znakiem '\0' acuchach znakw jzyka C. Korzystanie z nich bywa jednak do uciliwe, szczeglnie w odniesieniu do zarzdzania pamici. Wemy dla przykadu funkcj, ktra bierze dwa acuchy jako argumenty i scala je w jeden, pozostawiajc spacj pomidzy acuchami wejciowymi: char* SPkonkat(const char* wyraz1, const char* wyraz2) { unsigned int rozmiar=strlen(wyraz1)+strlen(wyraz2)+1; char* wynik = new char[rozmiar]; return strcat(strcat(strcpy(wynik,wyraz1), " "),wyraz2); } Pierwsza instrukcja w bloku funkcji oblicza dugo wynikowego acucha, uwzgldniajc rozdzielajc wyrazy spacj i terminalny znak zerowy ('\0'), a druga alokuje niezbdn pami. Po przydzieleniu pamici trzecia instrukcja wykorzystuje dwie funkcje biblioteczne ( ze string.h): najpierw kopiuje acuch wyraz1 do zmiennej wynik, docza rozdzielajc spacj, i na koniec docza drugi acuch wyraz2. Kada z funkcji bibliotecznych zwraca wskanik do swojego pierwszego argumentu (tj. acucha docelowego), dziki czemu moglimy ustawi w sekwencj wywoania kolejnych funkcji. Funkcja woajca jest odpowiedzialna za usunicie wynikowego acucha:

char* wsk = SPkonkat("Jan", "Kowalski"); // ... delete wsk; Jak wida z powyszego przykadu, celowym byoby zdefiniowa klas, ktra stanowiaby swego rodzaju opakowanie dla istniejcych funkcji jzyka C, uatwiajc uytkownikowi operowanie na acuchach znakw za pomoc kilku prostych operatorw. Termin opakowanie odpowiada prawie dokadnie temu, co okrelilimy wczeniej jako hermetyzacj albo ukrywanie informacji. Tutaj nasz intencj jest taka abstrakcja danych, aby szczegy reprezentacji acuchw jzyka C, tj. typu char*, zostay ukryte przed uytkownikiem. Zamiast nich uytkownik powinien mie do dyspozycji dobrze zaprojektowany interfejs (cz publiczn) do klasy, ktrej obiekty zachowywayby si podobnie, jak acuchy jzyka C. Podane niej deklaracje, definicje i komentarze mona traktowa jako prost implementacj takiej klasy. Przykad 6.27. class String { public: //Publiczny interfejs uzytkownika: //Redefinicja "+" dla konkatenacji - trzy przypadki: String operator+(const String&) const; friend String operator+(const char*, const String&); friend String operator+(const String&, const char*); int length() const; // Length of string in chars String(); String(const char*); String(const String&); ~String(); String& operator=(const String&); operator const char*() const; friend ostream& operator<<(ostream&, const String&); char& operator [] (int); private: char* dane; }; Dyskusja. Pierwsze spostrzeenie: zmienna char* dane, reprezentujca acuch znakw jzyka C, jest ukryta w czci prywatnej klasy String i jest dostpna jedynie dla jej funkcji skadowych i operatorw. Klasa ma trzy konstruktory. a) Konstruktor domylny String::String(); String::String() { dane = new char[1]; dane[0] = '\0'; } ktry tworzy acuch pusty. Konstruktor ten bdzie woany np. przy tworzeniu wektora

obiektw klasy String: String wektor[10]; b) Konstruktor String::String(const char*); String::String(const char* st) { int rozmiar = ::strlen(st) + 1; dane = new char[rozmiar]; ::strcpy(dane, s); } ktry dokonuje wspomnianej uprzednio konwersji acucha st w obiekt klasy String. W tej i w nastpnych definicjach bdziemy uywa unarnego operatora zasigu :: przy odwoaniach do zmiennych i funkcji globalnych (z pliku string.h), aby unikn konfliktu nazw. Rozmiar tworzonego dynamicznie podobiektu dane jest o 1 wikszy od dugoci acucha st, aby zmieci terminalny znak '\0'. c) Konstruktor kopiujcy String::String(const String&); String::String(const String& st); dane = new char[st.length() + 1]; // kopiuj stare dane na nowe ::strcpy(dane, st.dane); { }

ktry jest wywoywany przy przesyaniu parametrw przez warto i niekiedy przy powrocie z funkcji. Destruktor: String::~String() { delete [] dane; } Jego zadaniem jest zwolnienie wszystkich zasobw, ktre pozyska obiekt dziki wykonaniu konstruktorw lub funkcji skadowych. Operatorowa funkcja przypisania: String& String::operator=(const String& st) { if(dane != st.dane) { delete [] dane; int rozmiar = st.length() + 1; dane = new char[rozmiar]; ::strcpy(dane, st.dane); } return *this; // referencja do obiektu } Funkcja zwraca referencj do klasy String, a wic nie jest tworzony aden obiekt tymczasowy. W bloku funkcji najpierw sprawdza si, czy nie jest to prba przypisania obiektu do samego siebie; jeeli nie, to usuwa si stare dane. Kolejna instrukcja tworzy obiekt w pamici swobodnej, a nastpna kopiuje zawarto pola st.dane argumentu funkcji do nowego obiektu dane. Wynik operacji, *this, jest referencj do klasy String. Funkcja konwersji z typu String do typu char*:

String::operator const char*() const { return dane; } pozwala uywa obiekty klasy String w tych samych kontekstach, co zwyke acuchy znakw. Zwrmy uwag na dwukrotne wystpienie modyfikatora const. Pierwszy z lewej ustala, e zawarto obszaru pamici wskazywanego przez warto do ktrej nastpuje konwersja (typu char*) nie moe by zmieniona przez adn operacj zewntrzn w stosunku do klasy. Drugi const oznacza, e zdefiniowana wyej funkcja skadowa operator const char*() const nie zmienia stanu obiektu klasy String, na ktrym operuje. Definicje przecionych operatorw '<<' i '[]' oraz funkcji przekazujcej dugo acucha s typowe i nie wymagaj komentarzy: ostream& operator<<(ostream& os, const String& cs) { return os << cs.dane; } char& String::operator [] (int indeks) { return dane[indeks]; } int String::length() const { return ::strlen(dane); } Natomiast szerszego komentarza wymagaj operatory konkatenacji. a) Operator konkatenacji dla klasy String String::operator+(const String& st) const { char* buf = new char[st.length() + length() + 1]; ::strcpy(buf, dane); ::strcat(buf, st.dane); String retval(buf); //Obiekt tymczasowy delete [] buf; return retval; } Pierwsza instrukcja alokuje pami na wynikowy acuch; druga kopiuje dane do tego acucha, a trzecia scala te dane z danymi przekazanymi przez argument st. Nastpnie ze zmiennej buf jest tworzony nowy, lokalny obiekt retval klasy String, usuwany ju niepotrzebny buf, a przy powrocie funkcja przekazuje kopi retval, tworzon przez konstruktor kopiujcy. Korzysta si z tej definicji w nastpujcy sposb: jeeli s1 i s2 s obiektami klasy String, to moemy je doda do siebie, piszc: s1 + s2; lub s1.operator+(s2); b) Zaprzyjanione operatory konkatenacji W klasie String zadeklarowano dwa operatory konkatenacji, aby byo moliwe dodawanie obiektw klasy String do zwykych acuchw znakw, z obiektami klasy String zarwno po prawej, jak i po lewej stronie operatora +.

String operator+(const char* sc, const String& st) { String retval; retval.dane = new char[::strlen(sc) + st.length()]; ::strcpy(retval.dane, sc); ::strcat(retval.dane, st.dane); return retval; } String operator+(const String& st, const char* sc) { String retval; retval.dane = new char[::strlen(sc) + st.length()]; ::strcpy(retval.dane, st.dane); ::strcat(retval.dane, sc); return retval; } Jak ju wspomniano, musimy mie dwie funkcje operatorowe dla zapewnienia symetrii. Dziki tym definicjom mog zosta wykonane instrukcje: String s1; "abcd" + s1; s1 + "abcd"; Symetri mona te zapewni dla obiektw klasy, definiujc dodatkowy konstruktor dwuargumentowy: String::String(const String& st1, const String& st2): dane(strcat(strcpy(new char[st1.strlen() + st2.strlen()+1],st1.dane),st2.dane)) { } i redefiniujc funkcj skadow operator+(): String operator+(const String& st1, const String& st2) { return String( st1, st2); } Konstruktor bierze dwa obiekty klasy String, alokuje pami dla poczonego acucha, kopiuje pierwszy argument do przydzielonego obszaru pamici i na koniec dokonuje konkatenacji drugiego argumentu z poprzednim wynikiem. Taki specjalizowany konstruktor bywa nazywany konstruktorem operatorowym. Definiuje si go jedynie w celu implementacji danego operatora. Symetryczny operator + po prostu wywouje ten konstruktor dla wykonania konkatenacji swoich argumentw. Podsumowanie. Zaprezentowana wyej klasa String nie moe pretendowa do przyjcia jej jako standardowej klasy bibliotecznej dla acuchw jzyka C++, poniewa przyjlimy zbyt wiele zaoe upraszczajcych, np. nie sprawdzalimy powodzenia alokacji dynamicznej, etc. Pominlimy rwnie wiele moliwych do wprowadzenia uytecznych funkcji, np. operatory porwnania acuchw, kopiowania fragmentw acuchw, wyszukiwania znakw i cigw znakw w acuchach, etc. Tym niemniej struktura tej klasy powinna uatwi uytkownikowi zrozumienie podobnych konstrukcji bibliotecznych. W charakterze wstpnego treningu mona sprawdzi dziaanie podanego niej programu, lokujc

przedtem deklaracj klasy String wraz z definicjami funkcji skadowych i zaprzyjanionych w pliku nagwkowym "wpstring.h" (lub w dwch plikach: w jednym, "wpstring.h", deklaracj klasy, a w drugim, np. "wpstring.cc" albo "wpstring.cpp", definicje funkcji). #include <iostream.h> #include "wpstring.h" int main() { String s1("ABC"); String s2 = s1; String s3;// pusty s3 = s2; String s4;//pusty s4 = s2 + s3; // lub s4 = s2.operator+(s3); "DEF" + s1; s1 + "ghi"; cout << s1[1] << endl; cout << s1 + "ghi" << endl; return 0; } Wydruk ma posta: B ABCghi

7.6.

Tablice obiektw

Kilkakrotnie ju zwracalimy uwag na fakt, e obiekty klas s zmiennymi typw definiowanych przez uytkownika i maj analogiczne wasnoci, jak zmienne typw wbudowanych. Tak wic nic nie stoi na przeszkodzie, aby umieszcza obiekty w tablicy. Deklaracja tablicy obiektw jest w peni analogiczna do deklaracji tablicy zmiennych innych typw. Take sposb dostpu do elementw takiej tablicy niczym si nie rni od poznanych ju zasad. Jedyn istotn cech wyrniajc tablic obiektw od innych tablic jest sposb ich inicjowania. Tablic obiektw danej klasy inicjuje si przez jawne, bd niejawne wywoywanie konstruktora klasy tyle razy, ile elementw zawiera tablica. Jeeli w klasie zdefiniowano konstruktory, to mona je uy w wywoaniu jawnym w taki sam sposb, jak dla indywidualnych obiektw; parametry aktualne wywoa konstruktora bd wartociami inicjalnymi dla zmiennych skadowych kadego obiektu tablicy. Dla konstruktorw domylnych, tj. z pustym wykazem argumentw, wartoci inicjalne zmiennych skadowych bd zalene od tego, czy w bloku konstruktora podano wartoci domylne: jeeli tak, to wartoci inicjalne bd rwne wartociom domylnym; jeeli nie, to bd przypadkowe. Innym wygodnym sposobem jest zainicjowanie kadego elementu tablicy wczeniej utworzonym obiektem, lub przypisanie kademu elementowi tablicy wczeniej utworzonego obiektu. W pierwszym przypadku wywoywany jest (niejawnie) konstruktor kopiujcy generowany przez kompilator lub (jawnie) konstruktor kopiujcy, zdefiniowany w klasie. W drugim przypadku bdzie to generowany lub zdefiniowany operator przypisania. Ponadto jednowymiarowe tablice obiektw, ktrych konstruktor zawiera tylko jeden parametr, mona inicjowa dokadnie tak samo, jak tablice, ktrych elementami s zmienne typw podstawowych, podajc wartoci inicjalne w nawiasach klamrowych. Dla wprowadzenia w zagadnienie posuymy si obiektami wielokrotnie ju eksploatowanej klasy

Punkt. Przykad 6.28. #include <iostream.h> class Punkt { public: Punkt() {} Punkt(int a): x(a) {} int fx() { return x; } private: int x; }; int main() { Punkt p1[] = { Punkt(10), Punkt(20), Punkt(30) }; for (int i = 0; i < 3; i++) cout << "p1[" << i << "].x = " << p1[i].fx() << '\t'; cout << '\n'; Punkt p2[] = { 15, 25, 35 }; Punkt p3[] = { Punkt(), Punkt(), Punkt() }; for (i = 0; i < 3; i++) cout << "p3[" << i << "].x = " << p3[i].fx() << '\t'; cout << '\n'; Punkt p4[] = { Punkt(), Punkt(40), Punkt() }; for (i = 0; i < 3; i++) cout << "p4[" << i << "].x = " << p4[i].fx() << '\t'; cout << '\n'; return 0; } Dyskusja. Klasa Punkt ma dwa konstruktory: bezparametrowy konstruktor domylny Punkt() oraz konstruktor z jednym parametrem Punkt(int a). Pierwsza instrukcja deklaracji tworzy tablic zoon z trzech obiektw, wywoujc trzykrotnie konstruktor z parametrem. W instrukcji: Punkt p2[] = { 15, 25, 35 }; mamy uproszczon posta zapisu wywoa konstruktora Punkt(15), Punkt(25) i Punkt(35). Instrukcja, tworzca tablic p3[] wywouje trzykrotnie konstruktor domylny, za instrukcja definiujca tablic p4[] wywouje dwukrotnie konstruktor domylny i jeden raz konstruktor z parametrem. Poniewa wszystkie tablice byy inicjowane dan liczb obiektw, mona byo opuci w deklaracji wymiary tablic. Wydruk z przedstawionego programu moe mie posta: p1[0].x = 10 p1[1].x = 20 p1[2].x = 30 p3[0].x = 1160 p3[1].x = -14 p3[2].x = 8607 p4[0].x = 12950 p4[1].x = 40 p4[2].x = 0 (Uyto tutaj sowa moe, poniewa wartoci inicjalne, uzyskiwane przez wywoanie konstruktora domylnego, bd przypadkowe).

Przykad 6.29. #include <iostream.h> class Punkt { public: Punkt(): x(0) {} Punkt( int a ): x(a) {} Punkt(const Punkt& p) { x = p.x; } Punkt& operator=(const Punkt& p) { this->x = p.x; return *this; } int fx() { return x; } private: int x; }; int main() { Punkt p0(7); Punkt p1[] = { p0, p0, p0 }; Punkt p2[3]; for (int i = 0; i < 3; i++) p2[i] = p1[i]; return 0; } Dyskusja. W programie wykorzystano trzy sposoby inicjowania tablicy obiektw. Instrukcja Punkt p1[]={p0,p0,p0}; inicjuje zmienn skadow x kadego obiektu trzyelementowej tablicy p1[] wartoci 7, skopiowan z obiektu p0. Kopiowanie kadego obiektu tablicy odbywa si przez wywoanie konstruktora kopiujcego Punkt(const Punkt& p). Zauwamy, e parametr aktualny (argument) dla konstruktora kopiujcego musi by stay i przekazywany przez referencj (gdyby przyj przekazywanie przez warto, to mielibymy wywoanie konstruktora kopiujcego, ktry wanie definiujemy). Z kolei kady obiekt tablicy p2[] jest inicjowany wartoci zero przez konstruktor domylny Punkt(){x=0;}. Nastpnie w ptli for kolejnym obiektom tablicy p2[] jest przypisywany obiekt p1[i] uprzednio zainicjowanej tablicy p1[]. W tym przypadku za kadym razem jest wywoywany przeciony operator przypisania Punkt& Punkt::operator=(const Punkt&) Jak dla kadego operatora skadowego klasy, pierwszym argumentem funkcji operator=() jest wskanik do obiektu, dla ktrego jest wywoywana (zamiast x = p.x mona napisa this->x = p.x). Drugi argument jest przekazywany przez referencj i stay, co gwarantuje jego niezmienno. Funkcja operator=() zwraca referencj do obiektu klasy Punkt, wobec czego w instrukcji return wystpuje jawnie nazwa tego obiektu, *this. Konstruktor kopiujcy i przeciony operator przypisania zostay uyte w tym przykadzie gwnie dla celw dydaktycznych. Gdyby zabrako ich definicji, odpowiednie operacje zostayby wykonane przez operatory generowane. Faktyczna potrzeba takich definicji pojawia si dopiero wtedy, gdy obiekt zawiera wskaniki do innych obiektw. Wwczas kopiowanie skadowych, wykonywane przez operatory generowane, przestaje by w oglnoci wystarczajce, poniewa kopiuje si wskaniki, a nie obiekty, do ktrych wskaniki si odwouj. Przykad 6.30.

#include <iostream.h> class Punkt { public: Punkt(): x(0) {} Punkt( int a ): x(a){} void ustaw(int b) { x = b; } int fx() { return x; } private: int x; }; int main() { int i,j; Punkt p1[2][3]; for (i = 0; i < 2; i++) { for (j = 0; j < 3; j++) p1[i][j].ustaw(10 + j); } for(i = 0; i < 2; i++) { for (j = 0; j < 3; j++) { cout << "p1[" << i << "][" << j << "].x = "; cout << p1[i][j].fx() << '\n'; } } cout << endl; return 0;

} Dyskusja. W programie zadeklarowano tablic p[2][3] o dwch wierszach i trzech kolumnach. Instrukcja deklaracji: Punkt p1[2][3]; wywouje szeciokrotnie konstruktor domylny Punkt() { x = 0; }, ktry ustawia zmienn skadow x na warto zero. Tworzona w ten sposb tablica jest inicjowana wierszami. W pierwszej ptli for dla kadego obiektu tablicy jest wywoywana funkcja skadowa Punkt::ustaw(), ktra ustawia zmienne Punkt::x w kolejnych obiektach na wartoci 10+j. Wydruk z programu ma posta: p[0][0].x = 10 p[0][1].x = 11 p[0][2].x = 12 p[1][0].x = 10 p[1][1].x = 11 p[1][2].x = 12 Zauwamy, e i w tym przypadku moglibymy kademu obiektowi tablicy p1[2][3] przypisa wczeniej utworzony obiekt. Np. tworzc obiekt p0(7) instrukcj deklaracji Punkt p0(7); tj. wywoujc konstruktor z parametrem, moemy zamiast instrukcji p1[i][j].ustaw(10 + j); uy w ptli for instrukcj

p1[i][j] = p0; W tym przypadku dla kadego i,j bdzie woany generowany przez kompilator operator przypisania dla obiektw klasy Punkt. Innym moliwym wariantem omawianej instrukcji przypisania mogaby by instrukcja: p1[i][j] = p0(i); W tym przypadku dla kadego obiektu tablicy p1[][] bd wykonywane kolejno dwie operacje: Wywoanie konstruktora Punkt::Punkt( int i ) { x = i; } Wywoanie generowanego przez kompilator domylnego operatora przypisania Punkt& Punkt::operator=(const Punkt&). Operator przypisania, podobnie jak zdefiniowany w poprzednim przykadzie, przypisuje skadowej x obiektu p[i][j] warto skadowej x obiektu p0. Przykad 6.31. // Alokacja dynamiczna - 1 #include <iostream.h> class Punkt { public: Punkt() {}; void ustaw(int c, int d) { x = c; y = d; } int fx() { return x; } int fy() { return y; } private: int x,y; }; int main() { Punkt* wsk; wsk = new Punkt [3]; if (!wsk) { cerr << "Nieudana alokacja\n"; return 1; } for (int i = 0; i < 3; i++) wsk[i].ustaw(i,i); delete [] wsk; return 0; } Dyskusja. Program tworzy tablic trzech obiektw typu Punkt w pamici swobodnej. Poniewa dla tablicy dynamicznej nie mona poda wartoci inicjujcych w jej deklaracji, w klasie Punkt zdefiniowano tylko konstruktor domylny. Inicjowanie tablicy odbywa si w ptli for, za pomoc funkcji skadowej Punkt::ustaw(). Poniewa w klasie Punkt nie zdefiniowano destruktora, zastosowano skadni operatora delete bez symbolu [].

Przykad 6.32. // Alokacja dynamiczna - 2 #include <iostream.h> class Punkt { public: Punkt(): x(0),y(0) {}; ~Punkt() { cout << "Destrukcja...\n"; } int fx() { return x; } int fy() { return y; } private: int x,y; }; int main() { Punkt* wsk; wsk = new Punkt [3]; if (!wsk) { cerr << "Nieudana alokacja\n"; return 1; } for (int i = 0; i < 3; i++) cout << wsk[i].fx() << '\t' << wsk[i].fy() << endl; delete [] wsk; return 0;

} Dyskusja. W tym przykadzie klasa Punkt zawiera definicj destruktora; teraz odzyskiwanie pamici przydzielonej dla wsk jest nastpuje przez wywoanie destruktora dla kadego obiektu skadowego tablicy. Ilustruje to wydruk z programu: 0 0 0 0 0 0 Destrukcja... Destrukcja... Destrukcja...

7.7.

Wskaniki do elementw klasy

Poznane dotd konwencje notacyjne nie daway nam moliwoci wyraenia w sposb jawny wskanika do elementu skadowego klasy. Dla zmiennej skadowej klasy moliwo tak stwarza deklaracja o postaci: klasa::*wsk = &klasa::zmienna; gdzie: klasa jest nazw klasy, w ktrej jest zadeklarowana zmienna o nazwie zmienna, za wsk jest wskanikiem do tej zmiennej. Wskanik jest inicjowany wyraeniem z prawej strony operatora =. Dla funkcji skadowej klasy stosuje si nastpujc posta deklaracji wskanika:

typ (klasa::*wskf)(arg) = &klasa::funkcja; gdzie: typ jest typem wartoci obliczonej przez funkcj funkcja, wskf jest wskanikiem do tej funkcji, za arg jest wykazem argumentw (sygnatur), z opcjonalnymi identyfikatorami. Wskanik jest inicjowany wyraeniem z prawej strony operatora =. Zauwamy, e w powyszych deklaracjach uywa si binarnego operatora ::*, ktry informuje kompilator, i wskaniki wsk i wskf maj zasig klasy. Wskaniki s inicjowane adresami wskazywanych skadowych klasy. Przypomnijmy, e dla dostpu bezporedniego do elementw klasy uywalimy operatora ::. Inicjowanie wskanikw adresami elementw skadowych bezporednio w ich deklaracji nie jest, rzecz jasna, obowizkowe. W instrukcji deklaracji wskanik mona zainicjowa zerem (0 lub NULL), lub te mona mu przypisa odpowiedni adres w instrukcji przypisania. Tak wic moemy np. zadeklarowa: klasa::*wsk1 = 0; // lub NULL klasa::*wsk2 = &klasa::zmienna; wsk1 = wsk2; Wskaniki do zmiennych skadowych klasy okazay si uyteczn metod wyraania topografii klasy, tj. wzgldnego pooenia elementw w klasie w sposb niezaleny od implementacji. Podany niej przykad ilustruje deklaracj wskanika oraz sposb dostpu do zmiennej skadowej x klasy Punkt. Przykad 6.33. #include <iostream.h> class Punkt { public: int x; }; int Punkt::*wskx = &Punkt::x; int main() { Punkt punkt1; punkt1.*wskx = 10; cout << punkt1.x << endl; return 0; } Dyskusja. Zauwamy, e odwoanie do zmiennej skadowej Punkt::x jest moliwe tylko poprzez wystpienie tej klasy, tj. obiekt punkt1. Posta odwoania poredniego (.*) rni si od bezporedniego (.) dodaniem symbolu *, za typem wskanika wskx jest Punkt::*. Zwrmy te uwag na fakt, e zmienna skadowa x jest publiczna w klasie. Gdyby zmienna skadowa bya prywatna, to prba dostpu do adresu tej skadowej byaby zasygnalizowana przez kompilator jako bd. W takim przypadku naleaoby rozszerzy deklaracj klasy o odpowiedni funkcj zaprzyjanion, doda w klasie Punkt definicj funkcji, przekazujcej warto x: friend int f(Punkt& p) { int Punkt::*wskx = &Punkt::x; p.*wskx = 10;

return p.x; } i wywoa j dla obiektu punkt1, np w instrukcji: cout << f(punkt1); Przy deklarowaniu wskanika do statycznej zmiennej (a take funkcji) skadowej klasy nie mona mu nada typu klasa::*, jak dla skadowej niestatycznej, poniewa kompilator nie przydziela pamici dla skadowej statycznej w adnym obiekcie danej klasy. Tak wic w tym przypadku mamy zwyky wskanik. Przykad 6.34. #include <iostream.h> class Punkt { public: static int x; }; int Punkt::x = 10; int* wskx = &Punkt::x; int main() { cout << *wskx << endl; return 0; } Dyskusja. Zgodnie z obowizujcymi zasadami skadowa statyczna x zostaa zainicjowana poza blokiem klasy Punkt wartoci 10. W deklaracji wskanika do tej skadowej, wskx, nie wystpi operator ::*, lecz tylko *. Poniewa x jest samodzielnym obiektem typu int, zatem wskx jest typu int*, a odwoanie do wartoci zmiennej x ma posta *wskx. Znacznie wicej korzyci daje zadeklarowanie wskanika do funkcji skadowej klasy. Jest to jeden ze sposobw wprowadzenia zmiany zachowania si funkcji w fazie wykonania. Wskanik do niestatycznej funkcji skadowej moemy wprowadzi w deklaracji klasy i uywa go do wywoa funkcji po uprzednim waciwym zainicjowaniu. W deklaracji wskanika naley poda zarwno typ klasy zawierajcej wskazywan funkcj skadow, jak i sygnatur tej funkcji. Podanie tych informacji jest wymagane przez kompilator, jako e C++ jest jzykiem o silnej typizacji i sprawdza nawet wskaniki do funkcji. Majc zadeklarowany wskanik moemy mu przypisywa adresy innych funkcji skadowych pod warunkiem, e funkcje te maj t sam liczb i typy parametrw oraz typ obliczanej wartoci. Podany niej przykad ilustruje uywan w tym przypadku notacj.

Przykad 6.35. #include <iostream.h> class Firma { public: char* nazwa; Firma(char* z): nazwa(z) {}; // Konstruktor ~Firma() {}; // Destruktor void podaj(int x) { cout << nazwa << " " << x << '\n'; } }; typedef void(Firma::*WSKFI) (int); int main() { Firma frm1("firma1"); Firma* wsk = &frm1; WSKFI wskf = &Firma::podaj; frm1.podaj(1); (frm1.*wskf)(2); (wsk->*wskf)(3); return 0;

} Wydruk z programu ma posta: firma1 1 firma1 2 firma1 3 Dyskusja. Deklaracja typedef void(Firma::*WSKFI) (int); wprowadza nazw WSKFI dla wskanika do funkcji typu void o zasigu klasy Firma i jednym argumencie typu int. Identyfikator WSKFI uywa si nastpnie dla zadeklarowania wskanika wskf, zainicjowanego adresem funkcji skadowej podaj() klasy Firma. W programie pokazano trzy sposoby drukowania zawartoci pola Firma::nazwa obiektu frm1: instrukcja frm1.podaj(1); korzysta z operatora . dostpu bezporedniego; instrukcja (frm1.*wskf)(2); korzysta z operatora .* dostpu poredniego; instrukcja (wsk->*wskf)(3); korzysta z operatora ->*, ktry jest operatorem dostpu poredniego dla wskanika wsk do obiektu frm1. Operatory .* i ->* wi wskaniki z konkretnym obiektem, dajc w wyniku funkcj, ktra moe by uyta w wywoaniu. Binarny operator .* wie swj prawy operand, ktry musi by typu "wskanik do skadowej klasy T" z lewym operandem, ktry musi by typu "klasy T". Wynikiem jest funkcja typu okrelonego przez drugi operand. Analogicznie ->*. Priorytet () jest wyszy ni .* i ->*, tak e nawiasy s konieczne. Gdyby pomin nawiasy, to np. wyraenie:

frm1.*wskf(2); byoby potraktowane jako frm1.*(wskf(2)) czyli jako warto skadowej obiektu frm1, na ktr wskazuje warto zwracana przez zwyk funkcj wskf(). FUwaga. Wartociowanie wyraenia z operatorem '.*' lub '->*' daje l-warto, jeeli prawy operand jest l-wartoci. Kolejny przykad mona potraktowa jako fragment oprogramowania systemu nadzorowania obiektu, w ktrym sygna przychodzcy do miejsca oznaczonego jako przycisk wyzwala odpowiedni reakcj systemu. Sygna 0 oznacza stan normalny, sygna 1 ewentualne zagroenie (alert), za sygna 2 alarm. Stany te mog by wywietlane na ekranie (jak w programie), lub powodowa inn reakcj (np. odpowiednie sygnay dwikowe).

Przykad 6.36. #include <iostream.h> class Ochrona { public: char* nazwa; Ochrona(char* z): nazwa(z) {}; // Konstruktor ~Ochrona() {}; // Destruktor void (Ochrona::*przycisk) (int j); // Wskaznik void kontrola(int); void norma(int x) { cout << "STAN NORMALNY" << endl; } void alert(int y) { cout << "POGOTOWIE" << endl; } void alarm(int z) { cout << "ALARM!!!" << endl; } }; void Ochrona::kontrola(int w) { switch (w) { case 0: przycisk = &Ochrona::norma; norma(w); break; case 1: przycisk = &Ochrona::alert; alert(w); break; case 2: przycisk = &Ochrona::alarm; alarm(w); break; } } int main() { Ochrona ob("System1"); typedef void (Ochrona::*WSKFI) (int); WSKFI wskf = &Ochrona::kontrola; int ii; cin >> ii; (ob.*wskf)(ii); Ochrona* wsk = &ob; (wsk->*wskf)(ii); (ob.*(ob.przycisk))(ii); return 0;

} Przykadowy wydruk z programu dla ii==1, ma posta: ALARM!!! ALARM!!! ALARM!!! Dyskusja. Podobnie jak w poprzednim przykadzie uyto deklaracji typedef dla atwiejszego odwoywania si do wskanikw funkcji. W klasie Ochrona zadeklarowano funkcj skadow przycisk, ktra posuya w ostatniej przed return instrukcji programu do sprawdzenia, jaki sygna zosta przekazany do systemu nadzorowania. Termin funkcja skadowa ujlimy w znaki cudzysowu, poniewa przycisk nie jest funkcj, lecz wskanikiem funkcji, ustawianym w bloku funkcji kontrola na adres funkcji norma(), alert(), lub alarm(). W instrukcji

switch jest podejmowana decyzja o tym, ktr z funkcji skadowych naley wywoa dla zadanego parametru aktualnego ii. Wykonanie programu przebiega nastpujco: Pierwsza instrukcja w bloku main() woa konstruktor Ochrona(char*) i tworzy obiekt ob. W instrukcji WSKFI wskf = &Ochrona::kontrola; wskanik wskf jest inicjowany na adres funkcji skadowej kontrola(). Po wczytaniu wartoci ii (np. 2), wykonywana jest instrukcja (ob.*wskf)(ii);, tzn. przez wskanik wskf zostaje wywoana funkcja skadowa kontrola(2). W funkcji kontrola() wskanikowi przycisk zostaje przypisany adres funkcji skadowej alarm(), po czym zostaje wywoana funkcja alarm() z parametrem aktualnym 2. Po wykonaniu funkcji alarm(), a nastpnie instrukcji break; program opuszcza blok funkcji kontrola(). W programie zadeklarowano rwnie wskanik do obiektu klasy Ochrona i zainicjowano go adresem obiektu ob. Posuyo to do wywoania funkcji kontrola() z instrukcji (wsk->*wskf)(ii), w ktrej wskanik wsk odwouje si do wskanika wskf, a ten wywouje funkcj kontrola(2) i dalej proces biegnie jak poprzednio. Ostatnia instrukcja, (ob.*(ob.przycisk))(ii) odwouje si bezporednio do funkcji alarm(), poniewa wskanik przycisk zosta ju wczeniej ustawiony na adres tej funkcji. Program koczy wykonanie wywoaniem destruktora ~Ochrona().

7.8.

Klasy w programach wieloplikowych

Przyjtym standardem dla programw wieloplikowych jest umieszczanie deklaracji klas w pliku (plikach) nagwkowym. Definicje funkcji skadowych oraz definicje inicjujce zmienne statyczne klas s umieszczane w pliku (plikach) rdowym. Moliwa jest wtedy oddzielna kompilacja plikw rdowych, ktra oszczdza czas, poniewa w przypadku zmian w programie powtrna kompilacja jest wymagana tylko dla plikw, ktre zostay zmienione. Ponadto wiele implementacji zawiera program o nazwie make, ktry zarzdza ca kompilacj i konsolidacj dla projektu programistycznego; program make rekompiluje tylko te pliki, ktre zostay zmienione od czasu ostatniej kompilacji. Zauwamy te, e rozdzielna kompilacja zachca programistw do tworzenia oglnie uytecznych plikw tymczasowych (pliki z rozszerzeniem nazwy .o pod systemem Unix, lub .obj pod systemem MS-DOS) i bibliotek, ktre mog wielokrotnie wykorzystywa w swoich wasnych programach i dzieli je z innymi uytkownikami. Podany niej przykad ilustruje wykorzystanie w programie wieloplikowym tzw. bezpiecznych tablic. Przymiotnika bezpieczny uywamy tutaj nie bez powodu. Dotychczas pomijalimy milczeniem fakt, e jzyk C++ nie ma wbudowanego mechanizmu kontroli zakresu tablic. Wskutek tego jest moliwe np. wpisywanie wartoci do elementw tablicy poza zakresem wczeniej zadeklarowanych indeksw. Ten niepodany efekt odnosi si zarwno do przekroczenia zakresu od dou (ang. underflow), jak i od gry (ang. overflow), i to w rwnym stopniu do tablic alokowanych w pamici statycznej, na stosie funkcji, czy te w przypadku alokacji dynamicznej w pamici swobodnej. I tutaj, jak w wielu innych przypadkach, przychodzi nam z pomoc mechanizm klas. W podanym niej przykadzie zdefiniowano trzy klasy tablic z elementami typu int, double oraz char*. Wszystkie trzy klasy zawieraj dwa rodzaje funkcji skadowych pierwsza, wstaw(), suy do zapisywania informacji w tablicy, za druga, pobierz(), suy do wyszukiwania informacji w tablicy. Te dwie funkcje s w stanie sprawdza w fazie wykonania programu, czy zakresy tablicy nie zostay przekroczone. Przykad 6.37.

// Plik TABLICA.H pod DOS, tablica.h pod Unix // Bezpieczna tablica: elementy typu int class tabint { public: tabint(int liczba); int& wstaw(int i); int pobierz(int i); private: int rozmiar, *wsk; }; // Bezpieczna tablica: elementy typu double class tabdouble { double* wsk; public: tabdouble(int liczba); double& wstaw(int i); double pobierz(int i); private: int rozmiar; }; // Bezpieczna tablica: elementy typu char class tabchar { public: tabchar(int liczba); char& wstaw(int i); char pobierz(int i); private: int rozmiar; char* wsk; }; // Plik Tablica.CPP pod DOS, tablica.c pod Unix // Definicje funkcji klas tabint, tabdouble, tabchar #include "tablica.h" #include <iostream.h> #include <stdlib.h> // Funkcje klasy tabint // Konstruktor tabint::tabint(int liczba) { wsk = new int [liczba]; if (!wsk) { cout << "Brak alokacji\n"; exit (1); } rozmiar = liczba; }

// Wstaw element do tablicy. int& tabint:: wstaw (int i) { if(i < 0 || i >= rozmiar) { cout << "Przekroczenie zakresu!!!\n"; exit (1); } return wsk[i]; // zwraca referencje do wsk[i] } // Pobierz element z tablicy. int tabint::pobierz(int i) { if (i < 0 || i > rozmiar) { cout << "Przekroczenie zakresu!!!\n"; exit(1); } return wsk[i]; // zwraca element } // Funkcje klasy tabdouble tabdouble::tabdouble(int liczba) { wsk = new double [liczba]; if (!wsk) { cout << "Brak alokacji\n"; exit (1); } rozmiar = liczba; } double& tabdouble:: wstaw (int i) { if(i < 0 || i >= rozmiar) { cout << "Przekroczenie zakresu!!!\n"; exit (1); } return wsk[i]; // zwraca referencje do wsk[i] } double tabdouble::pobierz(int i) { if (i < 0 || i > rozmiar) { cout << "Przekroczenie zakresu!!!\n"; exit(1); } return wsk[i]; // zwraca element }

// Funkcje klasy tabchar tabchar::tabchar(int liczba) { wsk = new char [liczba]; if (!wsk) { cout << "Brak alokacji\n"; exit (1); } rozmiar = liczba; } char& tabchar:: wstaw (int i) { if(i < 0 || i >= rozmiar) { cout << "Przekroczenie zakresu!!!\n"; exit (1); } return wsk[i]; // zwraca referencje do wsk[i] } char tabchar::pobierz(int i) { if (i < 0 || i > rozmiar) { cout << "Przekroczenie zakresu!!!\n"; exit(1); } return wsk[i]; // zwraca element } // Plik MAINTAB.CPP pod DOS, maintab.c pod Unix #include "tablica.h" #include <iostream.h> int main() { tabint tab(5); tab.wstaw(3) = 13; tab.wstaw(2) = 12; cout << tab.pobierz(3) << endl << tab.pobierz(2) << endl; // Teraz przekroczenie zakresu tab.wstaw(6) = '!'; return 0; }

7.9.

Szablony klas i funkcji

Szablony lub wzorce (ang. template), nazywane rwnie typami parametryzowanymi, pozwalaj definiowa wzorce dla tworzenia klas i funkcji. Dla lepszego zrozumienia podstawowych koncepcji posuymy si analogi z matematyki. Matematyka operuje czsto pojciem rwnania parametrycznego, ktre pozwala generowa jednolub wieloparametrowe rodziny krzywych i prostych. W takich rwnaniach wystpuj oglne wyraenia matematyczne, ktrych wartoci s zalene od zmiennych lub staych, nazywanych parametrami. Tak wic parametr mona okreli jako zmienn lub sta, ktra wyrnia przypadki szczeglne oglnego wyraenia. Np. oglna posta rwnania prostej y = mx + b gdzie m jest sta, pozwala generowa rodzin prostych rwnolegych o nachyleniu m. Podobnie rwnanie

(x-a)2+(y-b)2=r2 przy ustalonej wartoci r, suy do generacji rodziny okrgw o pooeniu rodka zalenym od parametrw a i b. Koncepcja klasy nawizuje w pewnym stopniu do tych idei. Klas mona traktowa jako generator rodziny obiektw, w ktrej kademu obiektowi przydziela si niejawny parametr, ustalajcy jego tosamo. Ponadto rodzin obiektw mona parametryzowa przez nadawanie rnych wartoci ich zmiennym skadowym. Mona wtedy wyrni dwa charakterystyczne przypadki. 1. Obiekt jest tworzony za pomoc konstruktora generowanego przez kompilator. 2. Obiekt jest tworzony za pomoc konstruktora zdefiniowanego przez programist. W pierwszym przypadku zmiennym skadowym rnych obiektw mona nadawa rne wartoci po uprzednim utworzeniu obiektw z wartociami domylnymi. W przypadku drugim uytkownik ma wicej moliwoci: moe on np. zdefiniowa konstruktor z pust list argumentw, z argumentami domylnymi, bd definiowa konstruktory przecione. Wszystko to jednak dzieje si na poziomie jednej rodziny obiektw, w ramach jednej definicji klasy. Nasuwa si w zwizku z tym pytanie: czy mona zmieni deklaracj klasy w taki sposb, aby stworzy sobie moliwo generowania wielu rozcznych rodzin obiektw? Skoro zaczlimy od analogii matematycznej dla schematu klasa-rodzina obiektw, sprbujmy poszuka nastpnej analogii. W geometrii analitycznej rozwaa si m.in. przekroje stoka koowego paszczyznami. W zalenoci od nachylenia paszczyzny tncej otrzymuje si szereg rodzin parametryzowanych krzywych: rodzin okrgw, rodzin elips, rodzin parabol i rodzin hiperbol. By moe, i takie wanie analogie nasuway si twrcom obowizujcej obecnie wersji 4.0 kompilatora jzyka C++. Wprowadzona w tej wersji deklaracja szablonu ma nastpujc posta ogln: template<argumenty> deklaracja gdzie: sowo kluczowe (specyfikator) template sygnalizuje kompilatorowi wystpienie deklaracji szablonu, sowo argumenty oznacza list argumentw (parametrw) szablonu, sowo deklaracja oznacza deklaracj klasy lub funkcji. Lista argumentw szablonu moe si skada z rozdzielonych przecinkami argumentw, przy czym kady argument musi by postaci class nazwa, lub deklaracj argumentu. Wynika std, e nazwy argumentw mog si odnosi zarwno do klas, jak i do typw wbudowanych, bd zdefiniowanych przez uytkownika. Deklaracja szablonu moe wystpowa jedynie jako deklaracja globalna, a nazwy uywane w szablonie stosuj si do wszystkich regu dostpu i kontroli.

7.9.1. Szablony klas


Deklaracj szablonu mona wykorzystywa do definiowania klas wzorcowych. W takim przypadku po zamykajcym nawiasie ktowym umieszcza si definicj odpowiedniej klasy, np. template <class T> class stream { /* ... */ }; Wystpujca we wzorcu nazwa klasy stream moe by nastpnie uywana wszdzie tam, gdzie dopuszcza si uywanie nazwy klasy, np. w bloku funkcji moemy zadeklarowa wskaniki do tej klasy: class stream<char> *firststream, *secondstream;

Przykad 6.38. #include <iostream.h> template<class Typ> class Tablica { // Klasa parametryzowana typem elementw tablicy public: Tablica(int); // Deklaracja konstruktora ~Tablica() { delete [] tab; } // Destruktor Typ& operator [] (int j) { return tab[j]; } private: Typ* tab; // Wskaz do tablicy int rozmiar; }; template<class Typ> Tablica<Typ>::Tablica(int n) // Konstruktor { tab = new Typ[n]; rozmiar = n; } int main() { // Tablica 10-elementowa. Elementy typu int Tablica<int> x(10); for(int i = 0; i < 10; ++i) x[i] = i; for(i = 0; i < 10; ++i) cout << x[i] << ' '; cout << endl; // Tablica 5-elementowa. Elementy typu double Tablica<double> y(5); // Tablica 6-elementowa. Elementy typu char Tablica<char> z(6); return 0; } Dyskusja. Kade wystpienie nazwy klasy Tablica wewntrz deklaracji klasy jest skrtem zapisu Tablica<Typ>, np. gdyby deklaracj konstruktora zastpi definicj, to miaaby ona posta: Tablica<int n> {tab = new Typ[n]; rozmiar = n; } Deklaracja Tablica<int> x(10); wywouje konstruktor z parametrem 10 przekazywanym przez warto. Konstruktor tworzy 10-elementow tablic dynamiczn o elementach typu int. Deklarator tablicy, [], zosta zdefiniowany w treci klasy jako przeciony operator[]. Dziki temu obiekty klasy Tablica<Typ> mona indeksowa tak samo, jak zwyke tablice. Warto rwnie zwrci uwag na zwizo zapisu: deklaracje tablic typu double oraz char korzystaj z tego samego szablonu, co tablica typu int adna z nich nie wymaga uprzedniej definicji. Gdyby pisanie nazwy klasy wzorcowej z obowizujcymi nawiasami ktowymi okazao si nuce dla uytkownika, mona uy konstrukcji z typedef dla wprowadzenia synonimw. Mona np. wprowadzi zastpcze nazwy deklaracjami typedef Tablica<int> tabint; typedef Tablica<char> tabchar; i stosowa te nazwy dla tworzenia odpowiednich tablic

tabint tabchar

x(10); z(6);

7.9.2. Szablony funkcji


Szablon funkcji okrela sposb konstrukcji poszczeglnych funkcji. Np. rodzina funkcji sortujcych moe by zadeklarowana w postaci: template <class Typ> void sort(Tablica<Typ>);

Szablon funkcji wyznacza de facto nieograniczony zbir (przecionych) funkcji. Generowan z szablonu funkcj nazywa si funkcj wzorcow. Przy wywoaniach funkcji wzorcowych nie podaje si jawnie argumentw wzorca. Zamiast tego uywa si mechanizmu rozpoznawania (ang. resolution) wywoania. Dopuszcza si przy tym przecianie funkcji wzorcowej przez zwyke funkcje o takiej samej nazwie, lub przez inne funkcje wzorcowe o takiej samej nazwie. W omawianych przypadkach stosowany jest nastpujcy algorytm rozpoznawania. 1. Sprawd, czy istnieje dokadne dopasowanie wywoania do prototypu funkcji. Jeeli tak, to wywoaj funkcj. 2. Poszukaj szablonu funkcji, z ktrego moe by wygenerowana funkcja dokadnie dopasowana do wywoania. Jeeli znajdziesz taki szablon, wywoaj go. 3. Wyprbuj zwyke metody dopasowania argumentw (promocja,konwersja) dla funkcji przecionych. Jeeli znajdziesz odpowiedni funkcj, wywoaj j. Taki sam proces rozpoznawania jest stosowany dla wskanikw do funkcji. Podany niej elementarny przykad jest ilustracj kroku 2 algorytmu. Jedyn, jak si wydaje, korzyci z zastosowanego tutaj szablonu jest krtki kod rdowy. Gdybymy zastosowali poznany wczeniej mechanizm przeciania funkcji, to byoby konieczne podanie trzech oddzielnych definicji odpowiednio dla typw int, double oraz char. Przykad 6.39. #include <iostream.h> // Szablon template <class T> T max( T a, T b ) { return a > b ? a : b; } int main() { int i = 2, j = max(i,0); cout << "Max integer: " << j << endl; double db = 1.7, dc = max(db,3.14); cout << "Max double: " << dc << endl; char z1 = 'A', z2 = max(z1,'B'); cout << "Max char: " << z2 << endl; return 0; } Interesujcym przykadem zastosowania szablonw moe by definicja funkcji, ktrej zadaniem jest kopiowanie sekwencyjnych struktur danych. Definicj t zastosujemy do kopiowania sekwencji znakw.

Przykad 6.40. #include <iostream.h> template < class We, class Wy > Wy copy ( We start, We end, Wy cel ) { while ( start != end ) *cel++=*start++; return cel; } void main () { char* hello = "Hello "; char* world = "world"; char komunikat[15]; char* wsk = komunikat; wsk = copy ( hello, hello+6, wsk ); wsk = copy ( world, world+5, wsk ); *wsk = '\0'; cout << komunikat << endl; } Analiza programu. Pierwsze wywoanie funkcji copy kopiuje warto "Hello" do pierwszych szeciu znakw (zwrmy uwag na spacj po znaku 'o') tablicy komunikat, za druga kopiuje warto "world" do nastpnych piciu znakw tablicy. Po tych operacjach zmienna p wskazuje na znak (nieokrelony, poniewa tablica komunikat nie zostaa zainicjowana) wystpujcy bezporednio po literze 'd' sowa "world". Nastpnie do wskazania *p przypisujemy znak zerowy '\0', aby otrzyma normaln posta acucha stosowan w jzyku C++. Ostatni instrukcj wstawiamy zmienn komunikat do strumienia cout.

7.10. Klasy zagniedone


Klasa moe by deklarowana wewntrz innej klasy. Jeeli klas B zadeklarujemy wewntrz klasy A, to klas B nazywamy zagniedon w klasie A. Klasa zagniedona jest widoczna tylko w zasigu zwizanym z deklaracj klasy wewntrznej. Miejscem deklaracji klasy zagniedonej moe by cz publiczna, prywatna, lub chroniona klasy otaczajcej. Zauwamy, e zagniedenie jest przykadem asocjacji: klasa wewntrzna jest deklarowana jedynie w tym celu, aby umoliwi wykorzystanie jej zmiennych i funkcji skadowych klasie zewntrznej. Typowym przykadem moe tu by asocjacja grupujca; np. w klasie Komputer moemy zagniedzi klasy Procesor i Pami. Jeeli pami potraktujemy jako tablic jednowymiarow, to wykonanie operacji dodaj liczb a do liczby b bdzie wymaga zadeklarowania obiektu klasy Procesor z funkcjami pobierz() i wykonaj() oraz z licznikiem rozkazw, ktry bdzie wskazywa kolejne adresy pamici.

Przykad 6.41. #include <iostream.h> class Otacza { public: class Zawarta { public: Zawarta(int i): y(i) {} //Konstruktor int podajy() { return y; } private: int y; }; Otacza(int j): x(j){} //Konstruktor int podajx() { return x; } private: int x; }; int main() { Otacza zewn(7); int m,n; m = zewn.podajx(); cout << m << endl; Otacza::Zawarta wewn(9); n = wewn.podajy(); cout << n << endl; return 0; } Dyskusja. Instrukcja deklaracji Otacza zewn(7); wywouje konstruktor klasy zewntrznej Otacza(int j){x=j;} z parametrem aktualnym j=7, inicjujc x na warto 7. Instrukcja m=zewn.podajx(); wywouje funkcj skadow podajx() klasy zewntrznej. Obiekt wewn klasy Zawarta jest tworzony instrukcj deklaracji Otacza::Zawarta wewn(9);.

7.11. Struktury i unie jako klasy


W jzyku C++ struktury i unie s w zasadzie penoprawnymi klasami. Poniej wyliczono te cechy struktur i unii, ktre wyrniaj je w stosunku do klas, deklarowanych ze sowem kluczowym class. Struktura jest klas, deklarowan ze sowem kluczowym struct; elementy skadowe struktury oraz jej klasy bazowe s domylnie publiczne. Unia jest klas, deklarowan ze sowem kluczowym union; elementy skadowe unii s domylnie publiczne; unia utrzymuje w danym momencie czasu tylko jedn skadow. Struktura moe zawiera funkcje skadowe, wcznie z konstruktorami i destruktorami. Unia nie moe dziedziczy adnej klasy, ani nie moe by klas bazow dla klasy pochodnej. Unia nie moe zawiera adnej skadowej statycznej, tj. deklarowanej ze sowem kluczowym static. Unia nie moe zawiera obiektu, ktry ma konstruktor lub destruktor. Tym niemniej unia moe mie konstruktor i destruktor. Niektre kompilatory nie akceptuj prywatnych skadowych unii. Tak wic struktura rni si od klasy, deklarowanej ze sowem kluczowym class jedynie tym, e jeeli jej skadowe nie s poprzedzone etykiet private:, to s one publiczne. Oznacza to, e

wszystkie zmienne i funkcje skadowe s dostpne za pomoc operatora selekcji '.' lub w przypadku wskanika do obiektu operatora ->. Wobec tego deklaracje: class Punkt { public: int x,y; }; oraz struct Punkt { int x,y; }; s rwnowane. Podobnie rwnowane bd deklaracje: class Punkt { int x,y; }; oraz struct Punkt { private: int x,y; }; Pokazany niej przykad jest nieznaczn modyfikacj poprzedniego; w programie wyeliminowano funkcje skadowe podajx() oraz podajy(), poniewa skadowe x oraz y s teraz publiczne, a wic dostpne bezporednio. Przykad 6.42. #include <iostream.h> struct Otacza { int x; struct Zawarta { int y; Zawarta(int i): y(i) {} //Konstruktor }; Otacza(int j): x(j) {} //Konstruktor }; int main() { Otacza zewn(7); int m,n; cout << zewn.x << endl; Otacza::Zawarta wewn(9); cout << wewn.y << endl; return 0; } Chocia struktury maj w zasadzie te same moliwoci co klasy, wikszo programistw stosuje struktury bez funkcji skadowych. S one wtedy odpowiednikami struktur jzyka C, bd rekordw, definiowanych w jzykach Pascal, czy Modula-2. Zalet takiego stylu programowania jest wiksza czytelno programw: tam, gdzie nie jest wymagane ukrywanie informacji, uywa si struktur w przeciwnym przypadku klas. Pamitamy z wczeniejszego wprowadzenia, e wprawdzie unia moe grupowa skadowe rnych typw, ale tylko jedna ze skadowych moe by aktywna w danym momencie. Jest to cecha, istotna w aspekcie ukrywania informacji: uywajc unii moemy tworzy klasy, w ktrych wszystkie dane wspdziel ten sam obszar w pamici. Jest to co, czego nie da si zrobi, uywajc klas, deklarowanych ze sowem kluczowym class.

Przykad 6.43. #include <iostream.h> union bity { bity (int n); //Deklaracja konstruktora void podajbity(); int d; unsigned char z[sizeof(int)]; }; bity::bity(int n): d(n) {} //Definicja konstruktora void bity::podajbity() { int i,j; for (j = sizeof(int)-1; j >= 0; j--) { cout << "Wzorzec bitowy w bajcie " << j << ": "; for (i = 128; i; i >>= 1) if (i & z[j]) cout << "1"; else cout << "0"; cout << "\n"; } } int main() { bity obiekt(255); obiekt.podajbity(); return 0; } Dyskusja. Przykad prezentuje wykorzystanie unii do wywietlenia ukadu bitw w kolejnych bajtach liczby (255), podanej w wywoaniu konstruktora. Wykonanie programu powinno da wydruk o postaci: Wzorzec bitowy w bajcie 1: 00000000 Wzorzec bitowy w bajcie 0: 11111111

7. Dziedziczenie i hierarchia klas


Definiowane dotd klasy byy jednostkowymi konstrukcjami programistycznymi. Obiekty takich klas funkcjonoway niezalenie jeden od drugiego, podobnie jak zmienne innych typw. Jednake w praktyce, gdy zaczynamy analizowa klasy, ktre maj jedn lub wicej cech wsplnych, to dochodzimy czsto do wniosku, e warto byoby je w jaki sposb uporzdkowa. Narzucajcym si natychmiast sposobem uporzdkowania jest generalizacja albo uoglnienie: naley zdefiniowa tak klas, ktra bdzie zawiera tylko te atrybuty i operacje, ktre s wsplne dla pewnej grupy klas. W jzykach obiektowych tak uoglnion klas nazywa si superklas lub klas rodzicielsk, a kad z klas zwizanej z ni grupy nazywa si podklas lub klas potomn.

7.1.

Klasy pochodne

W jzyku C++ odpowiednikami tych terminw s klasa bazowa (ang. base class) i klasa pochodna (ang. derived class). Tak wic klasa bazowa moe zawiera tylko te elementy skadowe, ktre s wsplne dla wyprowadzanych z niej klas pochodnych. Wasno t wyraa si zwykle w inny sposb: mwimy, e klasa pochodna dziedziczy wszystkie cechy swojej klasy bazowej. Gdyby dziedziczenie ograniczy jedynie do przekazywania klasie pochodnej cech klasy bazowej, to taki mechanizm byby raczej mao przydatnym kopiowaniem, albo czym, co przypomina klonowanie. Dlatego te w jzyku C++ mechanizm dziedziczenia wzbogacono o nastpujce moliwoci. 1. W klasie pochodnej mona dodawa nowe zmienne i funkcje skadowe. 2. W klasie pochodnej mona redefiniowa funkcje skadowe klasy bazowej. Tak okrelony mechanizm pozwala w naszej abstrakcji zbliy si do tego, co dziedziczenie oznacza w jzyku potocznym: sensownie okrelone klasy pochodne zawieraj cechy klasy (lub kilku klas) bazowej oraz nowe cechy, ktre wyranie odrniaj je od klasy bazowej. FUwaga. Terminy superklasa i podklasa mog by mylce dla osb, ktre obserwuj, e obiekt klasy pochodnej zawiera obiekt klasy bazowej jako jeden ze swoich elementw i e klasa pochodna jest wiksza ni jej klasa bazowa w tym sensie, e mieci w sobie wicej danych i funkcji. Zatem to klasa pochodna jest nadzbiorem dla klasy bazowej, a nie odwrotnie. Byoby trudno przeceni znaczenie dziedziczenia w programowaniu obiektowym. Kad klas pochodn mona wykorzysta jako klas bazow dla nastpnej klasy pochodnej. Zatem schemat dziedziczenia pozwala budowa hierarchiczne, wielopoziomowe struktury klas, w ktrych kada klasa pochodna ma swoj bezporedni klas bazow. Jeeli kada klasa pochodna dziedziczy cechy tylko jednej klasy bazowej, to otrzymuje si struktur drzewiast. Jeeli klasa pochodna dziedziczy cechy wielu klas bazowych, to otrzymuje si struktur, nazywan grafem acyklicznym. W obu tych hierarchicznych strukturach przejcie od poziomu najwyszego do najniszego oznacza przejcie od klasy najbardziej oglnej do klasy najbardziej szczegowej. Wymienione struktury hierarchiczne przechowuje si zwykle w bibliotekach klas, co pozwala na wielokrotne ich wykorzystanie. Hierarchie klas s rwnie wygodnymi narzdziami dla tzw. szybkiego prototypowania (ang. rapid prototyping). Jest to technika szybkiego opracowania modelu systemu, jeszcze przed rozpoczciem gwnych prac implementacyjnych. Taki dziaajcy model systemu pozwala skonfrontowa wymagania uytkownika z propozycjami projektanta, skraca czas projektowania i z reguy znacznie obnia koszty.

7.1.1. Dziedziczenie pojedyncze


Deklaracja klasy pochodnej, ktra dziedziczy cechy tylko jednej klasy bazowej, ma nastpujc posta:

class pochodna : specyfikator-dostpu bazowa { //... }; gdzie: dwukropek po nazwie klasy pochodnej wskazuje, e klasa pochodna wywodzi si od wczeniej zdefiniowanej klasy bazowa; specyfikator-dostpu moe by jednym z trzech sw kluczowych: public, private, lub protected. Zasig klasy pochodna mieci si wewntrz zasigu klasy bazowa. Podobnie, jak dla klas nie zwizanych relacj dziedziczenia, widoczno funkcji skadowych klas bazowa i pochodna nie wykracza poza zasig ich klas. Umieszczony przed nazw klasy bazowej specyfikator dostpu ustala, jak elementy klasy bazowej s dziedziczone przez klas pochodn. 1. Jeeli specyfikatorem jest public, to informujemy kompilator, e klasa pochodna ma dziedziczy wszystkie elementy klasy bazowej i e wszystkie elementy publiczne w klasie bazowej bd take publicznymi elementami klasy pochodnej. Natomiast wszystkie elementy prywatne klasy bazowej pozostan jej wyczn wasnoci i nie bd bezporednio dostpne w klasie pochodnej. 2. Jeeli specyfikatorem jest private, to wszystkie elementy publiczne klasy bazowej stan si prywatnymi elementami klasy pochodnej. Podobnie, jak w pierwszym przypadku, elementy prywatne klasy bazowej pozostan jej wyczn wasnoci i nie bd bezporednio dostpne w klasie pochodnej. Dodajmy jeszcze, e specyfikator private jest specyfikatorem domylnym; jeeli przed nazw klasy bazowej nie umiecimy adnego specyfikatora, to kompilator przyjmie private. Dla czytelnoci zapisu nie naley jednak pomija sowa private. 3. Uycie specyfikatora protected wymaga nieco szerszego omwienia. Z punktw 1 i 2 wynika, e klasa pochodna ma dostp tylko do elementw publicznych klasy bazowej. W przypadku, gdy chcemy pozostawi element klasy bazowej prywatnym, ale da do niego dostp klasie pochodnej, wtedy deklaracj takiego elementu w klasie bazowej poprzedzamy etykiet protected:, np. class bazowa { int pryw; protected: int chron; public: int publ; }; Jeeli tak zadeklarowana klasa bazowa jest dla klasy pochodnej: publiczna, to zmienna chron stanie si elementem chronionym w klasie pochodnej, tzn. jej elementem prywatnym, ale dostpnym dla jej wasnych klas pochodnych; prywatna, to zmienna chron stanie si prywatnym elementem klasy pochodnej (podobnie, jak zmienna publ); chroniona, to chronione (chron) i publiczne (publ) elementy klasy bazowej stan si chronionymi elementami klasy pochodnej. Z powyszej dyskusji wynikaj dwa wnioski oglne. Po pierwsze, wszystkie elementy publiczne kadej klasy w hierarchii s dostpne dla kadej klasy, lecej niej w hierarchii, i dla dowolnej funkcji, nie majcej zwizku z dan hierarchi klas. Po drugie, elementy prywatne pozostaj wasnoci klasy, w ktrej zostay zadeklarowane. Elementy chronione s rwnowane prywatnym

w danej klasie bazowej, ale s dostpne dla jej klas pochodnych.

7.1.2. Dziedziczenie z konstruktorami generowanymi


Konstruktory i destruktory nie s dziedziczone (podobnie jak funkcje i klasy zaprzyjanione). Natomiast konstruktory niejawne s zawsze generowane przez kompilator dla obiektw kadej klasy w hierarchii dziedziczenia. S to zawsze funkcje, poprzedzone domylnym specyfikatorem public. Przykad 7.1. #include <iostream.h> class Bazowa { public: void ustawx(int n): x(n) {} void podajx() { cout << x << "\n"; } private: int x,z; }; class Pochodna : public Bazowa { public: void ustawy(int m) { y = m; } void podajy() { cout << y << "\n"; } private: int y; }; int main() { Pochodna ob; ob.ustawx(10); ob.ustawy(20); ob.podajx(); ob.podajy(); cout << sizeof(Bazowa) << endl; cout << sizeof(Pochodna) << endl; cout << sizeof ob << endl; return 0; } Dyskusja. Wydruk z powyszego programu ma posta: 10 20 4 6 6 Obiekt ob klasy Pochodna nie ma dostpu do prywatnej skadowej x klasy Bazowa, ale ma dostp do publicznych funkcji ustawx() oraz podajx(). Funkcje te s dostpne z wntrza obiektu ob. Operatory sizeof wykorzystano tutaj dla pokazania, e obiekt klasy bazowej jest czci obiektu klasy pochodnej (w pokazanej implementacji zmienna cakowita zajmuje 2 bajty w pamici).

7.1.3. Dziedziczenie z konstruktorami definiowanymi


Przeledzimy teraz przypadki, gdy klasy bazowa i pochodna maj konstruktory i destruktory zdefiniowane przez uytkownika. Mona wtedy pokaza, e konstruktory s wykonywane w porzdku dziedziczenia (najpierw jest wykonywany konstruktor klasy bazowej, a nastpnie pochodnej). Destruktory s wykonywane w kolejnoci odwrotnej. Kolejno ta jest oczywista: poniewa klasa bazowa nie ma adnej informacji o jakiejkolwiek klasie pochodnej, musi inicjowa swoje obiekty niezalenie. W odniesieniu do klasy pochodnej jest to czynno przygotowawcza dla zainicjowania jej wasnego obiektu. Dlatego konstruktor klasy bazowej musi by wykonywany jako pierwszy. Z drugiej strony, destruktor klasy pochodnej musi by wykonany przed destruktorem klasy bazowej. Gdyby zamieni kolejno, to destruktor klasy bazowej zniszczyby cz obiektu klasy pochodnej, uniemoliwiajc jego prawidow destrukcj. Przykad 7.2. #include <iostream.h> class Bazowa { public: Bazowa() { cout<<"Konstruktor klasy bazowej\n"; } ~Bazowa(){ cout<<"Destruktor klasy bazowej\n"; } }; class Pochodna : public Bazowa { public: Pochodna(){ cout<<"Konstruktor klasy pochodnej\n"; } ~Pochodna(){ cout<<"Destruktor klasy pochodnej\n"; } }; int main() { Pochodna ob; return 0; } Wydruk z powyszego programu ma posta: Konstruktor klasy bazowej Konstruktor klasy pochodnej Destruktor klasy pochodnej Destruktor klasy bazowej W podanym wyej przykadzie mielimy konstruktory z pustymi listami argumentw. Na og jednak konstruktory wystpuj z parametrami, ktrych zadaniem jest ustalenie stanu pocztkowego obiektu. W takich przypadkach uywa si rozszerzonej postaci deklaracji konstruktora klasy pochodnej: Pochodna(arg1) : Bazowa(arg2) { ciao konstruktora klasy Pochodna }

gdzie: arg1 jest cznym wykazem argumentw dla konstruktorw klas Pochodna i Bazowa, za arg2 jest wykazem argumentw konstruktora klasy Bazowa. Pokazana wyej posta deklaracji pozwala przekaza do konstruktora klasy Pochodna zarwno parametry aktualne, specyficzne dla obiektw tej klasy, jak i parametry aktualne dla wywoania konstruktora klasy Bazowa.

Przykad 7.3. #include <iostream.h> class Bazowa { public: Bazowa(int i, int j): x(i),y(j) {} void podaj(); private: int x,y; }; void Bazowa::podaj() { cout << "x= " << x << " y= " << y << endl; class Pochodna : public Bazowa { public: Pochodna(int m, int n) : Bazowa(m,n) }; int main() { Bazowa ba(3,4); ba.podaj(); Pochodna po(5,9); po.podaj(); return 0; }

{}

Dyskusja. Poniewa klasa Pochodna nie zawiera adnych rozszerze w stosunku do klasy Bazowa, ciao konstruktora klasy Pochodna jest puste, za przyjmowane przez t funkcj dwa parametry s przekazywane do konstruktora klasy Bazowa. Przesanie parametrw aktualnych do konstruktora klasy Bazowa jest konieczne, poniewa, jak pamitamy, konstruktory nie s dziedziczone. Instrukcja deklaracji Bazowa ba(3,4); wywouje konstruktor Bazowa::Bazowa(int,int). Natomiast instrukcja deklaracji Pochodna po(5,9); wywouje najpierw konstruktor klasy Pochodna, ktry przekazuje parametry aktualne do konstruktora klasy Bazowa. Zgodnie z kolejnoci wykonywania konstruktorw, najpierw zostanie wykonany konstruktor Bazowa::Bazowa(5,9), a nastpnie konstruktor klasy Pochodna. Poniewa funkcja podaj() jest publiczn funkcj skadow klasy Bazowa, jest ona wykorzystywana w programie zarwno dla obiektu ba klasy Bazowa, jak i dla obiektu po klasy Pochodna. Poprzedni przykad mia znaczenie czysto dydaktyczne, poniewa klasa pochodna nie zawieraa adnych zmian w stosunku do klasy bazowej. W nastpnym przykadzie deklaracj klasy pochodnej rozszerzono o dodatkowe elementy. Przykad 7.4. #include <iostream.h> class Bazowa { public: Bazowa(int i, int j): x(i),y(j) {} void podajxy(); private:

int x,y; }; void Bazowa::podajxy() { cout << "x= " << x << " y= " << y << endl; class Pochodna : public Bazowa { public: Pochodna(int k, int m, int n) : Bazowa(m,n) { z = k; } void podaj() { podajxy(); //Bazowa::podajxy() cout << "z= " << z << endl; } private: int z; }; int main() { Pochodna po(3,100,200); po.podaj(); return 0; } Dyskusja. Konstruktor klasy Bazowa wymaga dwch argumentw dla zainicjowania zmiennych skadowych x oraz y. Klasa Pochodna zawiera dodatkowo zmienn z. Dlatego konstruktor klasy Pochodna jest funkcj o trzech argumentach, poniewa dwa spord nich musz by przesane do konstruktora klasy Bazowa. Tak wic wykonanie instrukcji deklaracji: Pochodna po(3,100,200); powoduje przekazanie parametrw aktualnych 100 i 200 do argumentw konstruktora klasy Bazowa, wykonanie konstruktora Bazowa::Bazowa(i,j), a nastpnie wykonanie konstruktora klasy Pochodna. Zwrmy uwag na funkcje skadowe podajxy() oraz podaj(). Zaoylimy tutaj, e dla obiektu klasy Pochodna naley poda wartoci wszystkich zmiennych skadowych. Dlatego w definicji funkcji void Pochodna::podaj() musielimy umieci wywoanie funkcji Bazowa::podajxy(), ktra jest zdefiniowana w czci publicznej klasy Bazowa, a wic dostpna w klasie Pochodna. Podobne postpowanie musimy zastosowa dla identycznych sygnatur funkcji w klasach Bazowa i Pochodna; jeeli definicja funkcji podaj() z klasy bazowej zostaa przesonita przez definicj funkcji o tej samej nazwie w klasie pochodnej, to moemy j odsoni przy pomocy operatora zasigu. Jak pokazano w poniszym przykadzie, dotyczy to rwnie zmiennych skadowych klasy bazowej. Przykad 7.5. #include <iostream.h> class Bazowa { public: int x,y; Bazowa(int i, int j): x(i),y(j) {} void podaj(); }; }

void Bazowa::podaj() { cout << "Funkcja podaj() klasy Bazowa" << endl; } class Pochodna : public Bazowa { public: int x; Pochodna(int k,int m,int n):Bazowa(m,n) { x = k; } void podaj() { Bazowa::podaj(); cout << "Funkcja podaj() klasy Pochodna" << endl; } }; int main() { Pochodna po(3,100,200); po.podaj(); po.Bazowa::podaj(); cout << "po.x= " << po.x << endl; cout<<"po.Bazowa::x= "<<po.Bazowa::x<<endl; return 0; } Wydruk z programu bdzie mia posta: Funkcja podaj() klasy Bazowa Funkcja podaj() klasy Pochodna Funkcja podaj() klasy Bazowa po.x= 3 po.Bazowa::x= 100

7.2.

Konwersje w hierarchii klas

Jeeli tworzymy obiekty rnych klas, to tworzone obiekty s zmiennymi wystpienia swoich klas. Kada taka zmienna jest skojarzona z pewnym obszarem pamici na tyle duym, aby mg pomieci obiekt danej klasy. Obiekty rnych klas bd w oglnoci mie rne rozmiary, a zatem nie mona zmieci obiektu danej klasy w obszarze pamici, alokowanym dla obiektu innej klasy, ktra jest jej podzbiorem. Wemy dla przykadu deklaracje: Przykad 7.6. class Bazowa { public: int a,b; // ... }; class Pochodna: public Bazowa { public: int c,d; // ... }; Obiekty klasy Bazowa maj dwie zmienne skadowe: a oraz b. Obiekty klasy Pochodna maj cztery zmienne skadowe: a oraz b odziedziczone od klasy Bazowa i c oraz d zadeklarowane w klasie Pochodna. Z zasad dziedziczenia wynika, e typ obiektu, wskanika, lub referencji, moe

by niejawnie przeksztacony z typu Pochodna do publicznego typu Bazowa. Jeli wic zadeklarujemy: Bazowa baz; Pochodna poch; to dopuszczalne jest przypisanie baz = poch; lub rwnowane przypisanie zmiennych skadowych baz.a = poch.a baz.b = poch.b; W powyszych instrukcjach przypisania warto typu Pochodna podlega niejawnej konwersji do typu Bazowa przez odrzucenie zmiennych wystpienia c i d. Tak wic odwoanie do skadowej obiektu klasy pochodnej (w omawianym przypadku byo to odczytanie wartoci i nastpnie przypisanie) wymagao zmiany (konwersji) obiektu. Przykad 7.7. // Konwersja niejawna z Pochodnej do Bazowej #include <iostream.h> class Pochodna; class Bazowa { public: Bazowa(int i): x(i) {} void podajx() { cout << "x: " << x << endl; } Bazowa& operator=(const Bazowa& b) { this>x = b.x; return *this; } protected: int x; }; class Pochodna: public Bazowa { public: Pochodna(int j, int k): Bazowa(j) { y = k; } void podaj() { Bazowa::podajx(); cout << "y: " << y << endl; } private: int y; }; int main() { Bazowa ba(10); Pochodna po(20,30); cout << sizeof ba << '\t' << sizeof po << endl; ba.podajx();

po.podaj(); ba = po; ba.podajx(); Bazowa& refbaz = Pochodna(100,200); refbaz.podajx(); return 0; } Wydruk z programu ma posta: 2 4 x: 10 x: 20 y: 30 x: 20 x: 100 Dyskusja. Obiekt ba jest tworzony przez konstruktor Bazowa(int), za obiekt po jest tworzony przez konstruktor Pochodna(int, int), ktry wywouje ten sam konstruktor Bazowa(int) dla zainicjowania swojego podobiektu klasy Bazowa. Z pozostaych instrukcji komentarza wymagaj dwie: ba = po; Jest to konwersja standardowa (niejawna) obiektu klasy Pochodna w obiekt klasy Bazowa, przy czym przypisanie wykonuje funkcja operatorowa Bazowa& operator=(const Bazowa& b) wywoywana dla obiektu ba (niejawny argument this, ulokowany przed argumentem jawnym const Bazowa& b) z argumentem referencyjnym b. Bazowa& refbaz = Pochodna(100,200); Jest to rwnie konwersja standardowa, przy ktrej referencja do klasy Bazowa jest inicjowana obiektem (tj. jedynie skadow x==100) klasy Pochodna. Przykad 7.8. // Konwersje jawne #include <iostream.h> class Pochodna; class Bazowa { public: Bazowa(int i): x(i) { } Bazowa(const Bazowa& b) { this>x = b.x; } Bazowa& operator=(const Bazowa& b) { this>x = b.x; return *this; } operator Pochodna(); private: int x; }; class Pochodna: public Bazowa { public: Pochodna(int j, int k): Bazowa(j) { y = k; } private: int y;

}; Bazowa::operator Pochodna() { return Pochodna(x, 5); } int main() { Pochodna po(20,30); Pochodna poch = po;//zgodne Pochodna& refpo = po;//zgodne Pochodna* wskpo = &po;//zgodne Bazowa ba(50); Bazowa bazo = ba;//zgodne Bazowa& refba = ba;//zgodne Bazowa* wskbaz = &ba;//zgodne bazo = po;//konwersja standardowa refba = po;//konwersja standardowa wskbaz = &po;//konwersja standardowa ba = Bazowa(poch);//konwersja standardowa poch = ba;//operator konwersji poch = Pochodna(ba);//operator konwersji refpo = ba;//operator konwersji // wskpo = &ba; Niedozwolone wskpo = &Pochodna(ba);//operator konwersji return 0; } Dyskusja. W przykadzie pokazano sposb deklarowania i wykorzystania operatorowej funkcji konwersji z klasy pochodnej do klasy bazowej. Do duga sekwencja instrukcji w bloku main() ilustruje rne warianty inicjowania i przypisywania obiektw. Ze wzgldu na t dug sekwencj instrukcji, zajmiemy si tylko tymi, ktre wnosz nowe elementy. Instrukcja: poch = ba; woa operatorow funkcj konwersji Bazowa::operator Pochodna() { return Pochodna(x, 5); } ktra przeksztaca obiekt klasy Bazowa w obiekt klasy Pochodna. Jest to funkcja operatorowa w zasigu klasy Bazowa, zwracajca obiekt typu Pochodna. Dla wykonania tego zadania, a konkretnie wykonania instrukcji return Pochodna(x, 5); funkcja ta wywouje konstruktor klasy Pochodna, ktry z kolei woa konstruktor Bazowa(int), po czym koczy wykonanie swojego bloku i ponownie przekazuje sterowanie do funkcji konwersji. Ostatni czynnoci przed opuszczeniem bloku tej funkcji jest wywoanie operatora przypisania dla wieo utworzonego obiektu klasy Pochodna. Dokadnie tak samo s wykonywane instrukcje: poch = Pochodna(ba); refpo = ba; Podobnie jest wykonywana ostatnia instrukcja wskpo = &Pochodna(ba);. Jedyn rnic jest brak wywoania przecionego operatora przypisania. Instrukcja wskpo = &ba; jest nielegalna, poniewa nie ma konwersji z Bazowa* do Pochodna* (wskpo++ adresowaby nastpny obiekt klasy Pochodna, a nie nastpny obiekt klasy Bazowa).

7.3.

Dziedziczenie mnogie

Klasa pochodna moe mie wicej ni jedn klas bazow. Moliwe s wtedy dwa schematy dziedziczenia. Schemat pierwszy to proces kaskadowy: klasa C dziedziczy od klasy B, ktra z kolei dziedziczy od klasy A. W tym przypadku klasa C ma dwie klasy bazowe: bezporedni klas bazow B i poredni klas bazow A. W schemacie drugim klasa pochodna ma dwie lub wicej bezporednich klas bazowych, ktre z kolei mog mie t sam bezporedni klas bazow, lub

rne klasy bazowe. Schemat pierwszy sprowadza si do dziedziczenia pojedynczego w ukadzie klas A-B, a nastpnie B-C. Podobnie jak dla ukadu dwupoziomowego, konstruktory wszystkich trzech klas s woane w porzdku dziedziczenia: najpierw konstruktor klasy A, nastpnie B, a na kocu konstruktor klasy C. Przykad 7.9. #include <iostream.h> class Bazowa { public: Bazowa() { cout<<"Konstrukcja obiektu klasy Bazowa\n"; } ~Bazowa() { cout << "Destrukcja obiektu klasy Bazowa\n"; } }; class Pochodna1: public Bazowa { public: Pochodna1() { cout<<"Konstrukcja obiektu klasy Pochodna1\n"; } ~Pochodna1() { cout<<"Destrukcja obiektu klasy Pochodna1\n"; } }; class Pochodna2: public Pochodna1 { public: Pochodna2() { cout<<"Konstrukcja obiektu klasy Pochodna2\n"; } Pochodna2() { cout<<"Destrukcja obiektu klasy Pochodna2\n"; } }; int main() { Pochodna2 obiekt; return 0; } Dyskusja. W programie mamy trzy poziomy hierarchii dziedziczenia: Bazowa-Pochodna1-Pochodna2. Instrukcja deklaracji Pochodna2 obiekt; wywouje konstruktor bezparametrowy Pochodna2(). Poniewa jedn ze skadowych obiektu klasy Pochodna2 bdzie obiekt klasy Pochodna1, wykonanie tego konstruktora zostaje zawieszone do czasu wykonania konstruktora Pochodna1(). Z kolei wykonanie tego konstruktora zostaje zawieszone do czasu utworzenia obiektu klasy Bazowa. Po wykonaniu funkcji Bazowa(), zostanie dokoczone wykonanie funkcji Pochodna1(), a nastpnie funkcji Pochodna2(). W rezultacie obiekt klasy Pochodna2, bdzie zawiera dwa pod-obiekty: pod-obiekt klasy Bazowa oraz pod-obiekt klasy Pochodna1. Wydruk z programu bdzie mia posta: Konstrukcja obiektu klasy Bazowa Konstrukcja obiektu klasy Pochodna1 Konstrukcja obiektu klasy Pochodna2 Destrukcja obiektu klasy Pochodna2 Destrukcja obiektu klasy Pochodna1 Destrukcja obiektu klasy Bazowa

Omwimy teraz pokrtce zagadnienia, zwizane z drugim schematem dziedziczenia mnogiego. Zacznijmy od notacji. Gdy klasa pochodna dziedziczy bezporednio od kilku klas bazowych, to instrukcja deklaracji takiej klasy ma posta: class Pochodna: dostp Bazowa1, dostp Bazowa2, ..., dostp BazowaN { //... ciao klasy Pochodna }; gdzie Bazowa1 do BazowaN s nazwami klas bazowych, za dostp jest specyfikatorem dostpu (private, protected, public), ktry moe by rny dla kadej klasy bazowej. Przy dziedziczeniu od kilku klas bazowych konstruktory s wykonywane w takiej kolejnoci, w jakiej podano klasy bazowe. Destruktory s wykonywane w kolejnoci odwrotnej. Jeeli klasy bazowe maj konstruktory definiowane, ktre wymagaj podania argumentw, to przekazywanie argumentw z klas pochodnych odbywa si za pomoc rozszerzonej postaci konstruktora: Pochodna(argumenty): Bazowa1(argumenty), Bazowa2(argumenty), ... BazowaN(argumenty) { // ciao konstruktora klasy Pochodna } Gdy klasa pochodna dziedziczy hierarchi klas, to kada klasa pochodna w acuchu dziedziczenia musi przekaza wstecz, do swojej bezporedniej klasy bazowej, tyle i takich argumentw, jakie s wymagane. W przedstawionym niej przykadzie przyjto schemat dziedziczenia, pokazany na rysunku 7-1. Przyjte na nim piktogramy klas (prostokty z nazw klasy) i relacji dziedziczenia (trjkty skierowane wierzchokami ku klasom bazowym i poczone odcinkami linii prostych z tymi klasami) s zgodne z powszechnie obecnie stosowanymi oznaczeniami OMT (Object Modeling Technique) wprowadzonymi przez J. Rumbaugh i in. w pracy [10].
Schemat dziedziczenia od dwch klas bazowych Bazowa1 Bazowa2

Pochodna

Rys. 7-1 Przykad 7.10.

Dziedziczenie mnogie od dwch klas bazowych

#include <iostream.h> class Bazowa1 { protected: int x; public: Bazowa1(int i): x(i) {} // Definicja konstruktora int podajx() { return x; } }; class Bazowa2 { protected: int y; public: Bazowa2(int j): y(j) {} // Definicja konstruktora int podajy() { return y; } }; class Pochodna: public Bazowa1, public Bazowa2 { public: Pochodna (int, int, int); //Deklaracja konstruktora void przedstaw() { cout << podajx() << ' ' << podajy() << ' '; cout << z << endl; } private: int z; }; //Konstruktor klasy pochodnej: Pochodna::Pochodna(int a, int b, int c): Bazowa1(a), Bazowa2(b) { z = c; } int main() { Pochodna obiekt(1,2,3); obiekt.przedstaw(); //podajx(),podajy()publiczne w klasie Pochodna cout << obiekt.podajx() << ' ' << obiekt.podajy() << endl; return 0; } Wydruk z programu bdzie mia posta: 123 12 Dyskusja. Instrukcja deklaracji Pochodna obiekt(1,2,3); woa konstruktor Pochodna::Pochodna(a,b,c). Parametry a==1 oraz b==2 zostan przekazane w odpowiedniej kolejnoci do konstruktorw klas Bazowa1 i Bazowa2. Po utworzeniu obiektw tych klas zostanie dokoczone wykonanie konstruktora Pochodna(1,2,3), tj. zostanie utworzony i zainicjowany obiekt klasy Pochodna z pod-obiektami dwch klas bazowych. Zwrmy uwag na fakt, e ze wzgldu na publiczne dziedziczenie klas Bazowa1 i Bazowa2, funkcje publiczne podajx() oraz podajy() pozostaj publicznymi w klasie Pochodna. Wobec tego wartoci x oraz y mona odczyta zarwno za pomoc funkcji skadowej

przedstaw(), jak i funkcji podajx() i podajy() obiektu klasy Pochodna. W dziedziczeniu mnogim, podobnie jak w pojedynczym, moe wystpi przypadek identycznych nazw zmiennych lub funkcji w dziedziczonych klasach. Podany niej przykad ilustruje taki wanie przypadek. Przykad 7.11. //Identyczne nazwy zmiennych skladowych int n #include <iostream.h> class Bazowa1 { public: int n; Bazowa1(int i): n(i) {} }; class Bazowa2 { public: int n; Bazowa2 (int j): n(j) {} }; class Pochodna: public Bazowa1, public Bazowa2 { public: int k; Pochodna (int a, int b, int c); }; Pochodna::Pochodna(int a, int b, int c) : Bazowa1(a), Bazowa2(b) { k = c; } int main() { Pochodna obiekt(1,2,3); // cout << obiekt.n << endl; Niejednoznaczne cout << obiekt.Bazowa1::n << ' '; cout << obiekt.Bazowa2::n << endl; return 0; } Wydruk z programu bdzie mia posta: 12 Dyskusja. W klasach Bazowa1 i Bazowa2 wystpuje taka sama nazwa zmiennej skadowej int n. Bezporednie odwoanie do n w obiekcie klasy Pochodna byoby niejednoznaczne. Odczytanie wartoci n jest moliwe dopiero po jego skojarzeniu z odpowiedni klas za pomoc operatora zasigu. Przy dziedziczeniu mnogim klasa bazowa nie moe wystpi wicej ni jeden raz w wykazie klas bazowych klasy pochodnej. Np. w cigu deklaracji class Bazowa { //... }; class Pochodna: Bazowa, Bazowa { /... };

druga z nich jest bdna. Natomiast klasa bazowa moe by przekazana porednio do klasy pochodnej wicej ni jeden raz. Ilustruje to pokazany niej przykad. Przykad 7.12. #include <iostream.h> class Bazowa { public: int a; Bazowa(int i): a(i) {} // Definicja konstruktora }; class Pochodna1: public Bazowa { public: int b; Pochodna1 (int j, int k):Bazowa(j) { b = k; } }; class Pochodna2: public Bazowa { public: int c; Pochodna2 (int m, int n):Bazowa(m) { c = n; } }; class Pochodna12: public Pochodna1, public Pochodna2 { public: int d; Pochodna12(int, int, int, int, int); }; // Konstruktor klasy Pochodna12: Pochodna12::Pochodna12(int u,int w,int x,int y,int z) :Pochodna1(u,w), Pochodna2(x,y) { d = z; } int main() { Pochodna12 obiekt(1,2,3,4,5); // cout << obiekt.a << endl; Niejednoznaczne cout << obiekt.Pochodna1::a << ' '; cout << obiekt.Pochodna1::b << ' '; cout << obiekt.Pochodna2::a << endl; return 0; } Dyskusja. Przyjty w programie schemat dziedziczenia pokazano na rysunku 7-2.

Bazowa

Pochodna1

Pochodna2

Pochodna12

Rys. 7-2

Przykad niejednoznacznoci w hierarchii dziedziczenia

Jak wida z rysunku, kady obiekt klasy Pochodna12 bdzie mia dwa pod-obiekty klasy Bazowa: jeden, dziedziczony za porednictwem klasy Pochodna1 i drugi, dziedziczony za porednictwem klasy Pochodna2. Taka konstrukcja bywa nazywana krat klas, lub skierowanym grafem acyklicznym (ang. DAG akronim od Directed Acyclic Graph), w ktrym krawdzie grafu s skierowane od klas pochodnych do klas bezporednio rodzicielskich. W kracie dziedziczenia klasy Pochodna1 i Pochodna2 s nazywane klasami siostrzanymi (ang. sibling classes), poniewa komunikuj si przez wspln klas klas bazow, a dziedziczone przez nie obiekty klasy Bazowa rni si jedynie wartociami zmiennej a. W oglnoci klasy siostrzane s to takie klasy, ktre mog si komunikowa za porednictwem wsplnej klasy bazowej, poprzez zmienne globalne, lub poprzez jawne wskaniki.

8. Klasy i funkcje wirtualne


Dziedziczenie mnogie moe by rodkiem dla organizacji bibliotek wok prostszych klas z mniejsz liczb zalenoci pomidzy klasami, ni w przypadku dziedziczenia pojedynczego. Gdyby ograniczy dziedziczenie do pojedynczego, to kada biblioteka byaby jednym drzewem dziedziczenia, w oglnoci bardzo wysokim i rozgazionym. Mechanizm dziedziczenia mnogiego pozwala budowa biblioteki w postaci lasu mieszanego, w ktrym drzewa i grafy dziedziczenia mog mie zmienn liczb poziomw i rozgazie. Z przeprowadzonej w r. 7 dyskusji wynika, e takie struktury mona tworzy stosunkowo atwo, gdy klasa pochodna dziedziczy wasnoci kilku niezalenych (rozcznych) klas bazowych, nie majcych wsplnej superklasy. Jeeli jednak bezporednie klasy bazowe danej klasy pochodnej s zalene, naley zastosowa omwione niej mechanizmy jzykowe.

8.1.

Wirtualne klasy bazowe

Przedstawiony w p.7.3 przykad ilustruje niejednoznacznoci, jakie mog si pojawi w hierarhii dziedziczenia, gdy klasa pochodna dziedziczy t sam klas bazow kilkakrotnie, idc po rnych krawdziach grafu dziedziczenia. Odwoania do elementw skadowych takiej klasy bazowej s wwczas moliwe, ale kopotliwe (np. obiekt.Pochodna1::a). Jzyk C++ oferuje tutaj mechanizm, dziki ktremu klasy siostrzane wspdziel informacj (w tym przypadku jeden obiekt wsplnej klasy bazowej) bez wpywu na inne klasy w grafie dziedziczenia. Mechanizm ten polega na potraktowaniu wsplnej klasy bazowej jako klasy wirtualnej w klasach siostrzanych, a przywouje si go, piszc sowo kluczowe virtual przed lub po specyfikatorze dostpu, a przed nazw klasy bazowej. Wirtualno wsplnej klasy bazowej jest wasnoci stosowanego schematu dziedziczenia, a nie samej klasy, ktra poza tym niczym si nie rni od klasy niewirtualnej. Jeeli przy dziedziczeniu mnogim klasa pochodna dziedziczy t sam klas bazow jako wirtualn i idc po innej gazi jako niewirtualn, to oczywicie niejednoznacznoci nie usuniemy. Ilustracj tego jest rysunek 8-1, ktry pokazuje schemat dziedziczenia z wirtualnymi i niewirtualnymi klasami bazowymi.

virtual B F C

virtual D E

Z
Rys. 8-1 Dziedziczenie mnogie z wirtualnymi klasami bazowymi

W prezentowanym grafie dziedziczenia leca najniej w hierarchii klasa pochodna Z dziedziczy cechy szeciu swoich klas bazowych, przy czym klasy F, C, D i E s jej bezporednimi klasami bazowymi, za A i B porednimi. Klasy B i E wspdziel jeden obiekt klasy A, poniewa klasa A jest w kadej z nich deklarowana jako wirtualna klasa bazowa. Natomiast kady obiekt klas C i D bdzie zawiera wasn kopi zmiennych skadowych klasy A. W rezultacie kady obiekt klasy Z bdzie zawiera trzy kopie zmiennych skadowych klasy A: jedn przez dwie gazie wirtualne (przez E i B/F) i po jednej z gazi C i D. Pokazany schemat mona opisa przykadowymi deklaracjami: class A { public: void f() { cout << "A::f()\n"; } }; class B: virtual public A { }; class c: public A { }; class D: public A { }; class E: virtual public A { }; class F: public B { }; class Z: public F, public C, public D, public E { }; Gdyby zadeklarowa obiekt klasy Z: Z obiekt; to kade bezporednie wywoanie funkcji f() z tego obiektu Z.f(); bdzie niejednoznaczne, a wic bdne. Wywoania funkcji f() mona uczyni jednoznacznymi, odwoujc si do niej poprzez obiekty klas porednich, ktre zawieraj dokadnie po jednej kopii obiektu klasy A: obiekt.C::f(); obiekt.D::f(); obiekt.E::f(); obiekt.F::f(); Niejednoznaczne bdzie rwnie wywoanie za pomoc wskanika do klasy Z: Z* wsk = new Z; wsk->f(); chocia i w tym przypadku moemy woa funkcj f() poprzez adresy obiektw klas porednich: wsk->C::f(); wsk->D::f(); wsk->E::f(); wsk->F::f(); Wszystkie powysze wywoania porednie maj skadni raczej mao zachcajc. Gdyby w klasie A zadeklarowa zmienne skadowe, to odwoania do nich byyby podobne.

Oczywistym sposobem usunicia niejednoznacznoci z dyskutowanego schematu byoby zadeklarowanie klasy A jako wirtualnej klasy bazowej w pozostaych klasach porednich, tj. C i D. Takie wanie zaoenie przyjto w prezentowanym niej programie, ktry korzysta ze znacznie prostszego schematu dziedziczenia. Przykad 8.1. Schemat dziedziczenia: Bazowa / \ / \ Pochodna1 Pochodna2 \ / \ / DwieBazy #include <iostream.h> class Bazowa { public: Bazowa(): a(0) {} int a; }; class Pochodna1: virtual public Bazowa { public: Pochodna1(): b(0) {} int b; }; class Pochodna2: virtual public Bazowa { public: Pochodna2(): c(0) {} int c; }; class DwieBazy: public Pochodna1, public Pochodna2 { public: DwieBazy() {} int iloczyn() { return a*b*c; } }; int main() { DwieBazy obiekt; obiekt.a = 4; obiekt.b = 5; obiekt.c = 6; cout << "Iloczyn wynosi: " << obiekt.iloczyn() << endl; return 0; } Dyskusja. Instrukcja deklaracji DwieBazy obiekt; wywouje konstruktor domylny DwieBazy() {}. Konstruktor ten najpierw wywouje konstruktor Bazowa(){ a = 0; }, a nastpnie konstruktory domylne Pochodna1() i Pochodna2(). W rezultacie obiekt klasy DwieBazy bdzie zawiera po jednym pod-obiekcie klas Bazowa, Pochodna1 i Pochodna2. Pozostaa cz programu nie wymaga obszerniejszego komentarza. Zauwamy jedynie, e w definicji funkcji iloczyn() wyraenie a*b*c jest rwnowane: Bazowa::a*Pochodna1::b*Pochodna2::c. Rwnie poprawny byby zapis

obiekt.Bazowa::a, ale duszy od obiekt.a. Jeeli wirtualna klasa bazowa zawiera konstruktory, to jeden z nich musi by konstruktorem domylnym, albo konstruktorem z inicjalnymi wartociami domylnymi dla wszystkich argumentw. Konstruktor domylny bdzie woany bez argumentw, jeeli aden konstruktor klasy bazowej nie jest wywoywany jawnie z listy inicjujcej konstruktora klasy pochodnej. Ponadto dla wirtualnej klasy bazowej obowizuj nastpujce reguy: Konstruktor wirtualnej klasy bazowej musi by wywoywany z tej klasy pochodnej, ktra faktycznie tworzy obiekt; wywoania z porednich klas bazowych bd ignorowane. Jeeli deklaruje si wskanik do obiektw wirtualnej klasy bazowej, to nie mona go przeksztaci we wskanik do obiektw klasy pochodnej, poniewa w klasie bazowej nie ma informacji o obiektach klas pochodnych. Natomiast konwersja wskanika w kierunku odwrotnym, tj. z klasy pochodnej do klasy bazowej jest dopuszczalna, gdy kady obiekt klasy pochodnej zawiera wskanik do wirtualnej klasy bazowej. Ta wasno wskanikw jest bardzo wana, poniewa odgrywa ona kluczow rol przy definiowaniu i wykorzystaniu funkcji polimorficznych, nazywanych w jzyku C++ funkcjami wirtualnymi. Podany niej przykad ilustruje wymienione cechy wirtualnych klas bazowych. Klasa Bazowa jest teraz wyposaona w konstruktor z domyln wartoci argumentu, za wszystkie klasy pochodne maj konstruktory domylne. Konstrukcja obiektu klasy DwieBazy zaczyna si od wywoania konstruktora DwieBazy():Bazowa(300){}, ktry najpierw wywouje konstruktor klasy Bazowa, a nastpnie konstruktory klas Pochodna1 i Pochodna2. aden z tych konstruktorw nie wywouje konstruktora klasy Bazowa, poniewa podobiekt tej klasy zosta ju utworzony po wywoaniu konstruktora klasy Bazowa z bloku DwieBazy(). Sprawdzeniu tego faktu suy instrukcja cout << obiekt.a << endl;, ktra wydrukuje warto 300. Deklaracja wskanika wskb suy do ilustracji konwersji z Pochodna1* do Bazowa*, za potraktowana jako komentarz instrukcja wskp = (Pochodna1*)wskb; ilustruje brak konwersji z typu Bazowa* do Pochodna1*. Wynika to bezporednio z arytmetyki wskanikw: wartoci wyraenia wskb++ byby adres nastpnego obiektu klasy Bazowa, za wskp++ powinno si odnosi do nastpnego obiektu klasy Pochodna. Zauwamy, e gdyby doda deklaracje: Pochodna1 obiekt1; Pochodna2 obiekt2; to kady z tych obiektw zawieraby podobiekt klasy Bazowa z a==100 i a==200, poniewa za kadym razem z bloku konstruktora klasy pochodnej byby wywoany konstruktor klasy Bazowa. Przykad 8.2. #include <iostream.h> class Bazowa { public: Bazowa(int i = 0): a(i) {} int a; }; class Pochodna1: virtual public Bazowa { public:

Pochodna1(): Bazowa(100) {} int b; }; class Pochodna2: virtual public Bazowa { public: Pochodna2(): Bazowa(200) {} int c; }; class DwieBazy: public Pochodna1, public Pochodna2 { public: DwieBazy(): Bazowa(300) {} }; int main() { DwieBazy obiekt; obiekt.b = 5; obiekt.c = 6; cout << obiekt.a << endl; Bazowa* wskb;// wskb jest typu Bazowa* Pochodna1* wskp;//wskp jest typu Pochodna* wskb = (Bazowa*)wskp; //Brak konwersji z Bazowa* do Pochodna1* // wskp = (Pochodna1*)wskb; return 0; }

8.2. Funkcje wirtualne


Omawiajc przecianie funkcji oraz operatorw stwierdzilimy, e mechanizm ten realizuje polimorfizm, rozumiany jako jeden interfejs (operacja), wiele metod (funkcji). Jest to polimorfizm z wizaniem wczesnym (nazywanym take statycznym), poniewa rozpoznanie waciwego wywoania i ustalenie adresu funkcji przecionej nastpuje w fazie kompilacji programu. Dziki temu wywoania funkcji z wizaniem wczesnym nale do najszybszych. Wizanie wczesne zachodzi rwnie dla zwykych funkcji oraz nie-wirtualnych funkcji skadowych klasy i klas zaprzyjanionych. W jzyku C++ istnieje ponadto bardziej finezyjny i gitki mechanizm, znany pod nazw funkcji wirtualnych. Mechanizm ten odnosi si do tzw. wizania pnego, czyli sytuacji, gdy adres wywoywanej funkcji nie jest znany w fazie kompilacji, lecz jest ustalany dopiero w fazie wykonania. Wizanie wywoania z definicj funkcji wirtualnej nie jest moliwe w fazie kompilacji, poniewa funkcje wirtualne maj dokadnie takie same prototypy w caej hierarchii klas, a rni si jedynie ciaem funkcji. W schemacie dziedziczenia wizane statycznie zwyke funkcje skadowe klasy rwnie mog mie takie same prototypy w klasach bazowych i pochodnych, a rni si tylko zawartoci swoich blokw. W takich przypadkach funkcja klasy pochodnej nie zastpuje funkcji klasy bazowej, lecz j ukrywa. Sytuacja ta jest podobna do ukrywania nazw zmiennych w blokach zagniedonych. Poniewa funkcje wirtualne nie speniaj kryteriw wymaganych dla funkcji przecionych, nie mog by rozrnione w omawianym w rozdziale 5 procesie rozpoznawania i dopasowania. Tym niemniej kompilator pozwala je rozrni dziki omawianej dalej regule dominacji; ponadto programista moe uy w tym celu operatora zasigu :: dla klasy. Technika ta sprawdza si w przypadku dziedziczenia pojedynczego. Jednak dla dziedziczenia mnogiego, jak pokazano na pocztku tego rozdziau (nawet dla wirtualnych klas bazowych), kontrola wywoa staje si kopotliwa, a dla zoonych grafw dziedziczenia zawodzi.

Mechanizm funkcji wirtualnych mona wi okreli jako polimorfizm z wizaniem pnym, a programowanie, oparte o hierarchi klas i funkcje wirtualne jest czsto utosamiane z obiektowym stylem programowania. Dogodnym narzdziem dla operowania funkcjami wirtualnymi s wskaniki i referencje do obiektw. Im te powicimy obecnie wicej uwagi.

8.2.1.

Wskaniki i referencje w hierarchii klas

Wskaniki i referencje uywalimy wielokrotnie i w rnych kontekstach. Nie zwracalimy natomiast uwagi na szczeglne wasnoci wskanikw w hierarchii klas. Tymczasem dla efektywnego posugiwania si funkcjami wirtualnymi potrzebujemy takiego sposobu odwoywania si do obiektw rnych klas, ktry nie wymaga faktycznej zmiany obiektu, do ktrego si odwoujemy. Sposb taki istnieje i opiera si na nastpujcej wasnoci wskanikw i referencji: zmienn wskanikow (referencyjn) zadeklarowan jako wskanik do klasy bazowej mona uy dla wskazania na dowoln klas pochodn od tej klasy bazowej bez uywania jawnej konwersji typu. Jeeli wic wemiemy deklaracje: class Bazowa { /* ... */ }; class Pochodna: public Bazowa { /* ... */ }; Bazowa baz; Pochodna po; to moemy zapisa nastpujce poprawne instrukcje: Bazowa* wskb = &baz; wskb = &po; Bazowa& refb = po; Przy tych samych deklaracjach poprawne bd rwnie instrukcje: Bazowa* wskb = new Bazowa; wskb = &po; Przykad 8.3. #include <iostream.h> class Bazowa { public: void ustawx(int n) { x = n; } void podajx() { cout << x << '\t'; } private: int x; }; class Pochodna : public Bazowa { public: void ustawy(int m) { y = m; } void podajy() { cout << y << '\t'; } private: int y; }; int main() { Bazowa *wsk; //wskaz do klasy Bazowa Bazowa obiekt1; // Obiekt klasy Bazowa Pochodna obiekt2; //Obiekt klasy Pochodna wsk = &obiekt1; //Przypisz do wsk adres obiekt1

wsk->ustawx(10); //Ustaw x w obiekt1 obiekt1.podajx(); //Alternatywa: wsk->podajx() wsk = &obiekt2; //Przypisz do wsk adres obiekt2 wsk->ustawx(20); //Ustaw x w podobiekcie obiekt2 wsk->podajx(); // wsk->ustawy(30); Nielegalna obiekt2.ustawy(30); // wsk->podajy(); Nielegalna obiekt2.podajy(); return 0; } Wydruk z programu bdzie mia posta: 10 20 30

Komentarz. Instrukcje wsk->ustawy(30); oraz wsk->podajy(); byyby nielegalne, mimo e wskanik wsk jest ustawiony na adres obiektu klasy pochodnej. Jest to oczywiste, poniewa wsk pozwala na dostp tylko do tych skadowych obiektu klasy pochodnej, ktre s dziedziczone z klasy bazowej. Zanotujmy w tym miejscu kilka uwag. FWskaniki i referencje odwouj si do obiektw poprzez ich adresy, ktre maj ten sam rozmiar bez wzgldu na klas, do ktrej naley wskazywany obiekt. FZ arytmetyki wskanikw wynika, e zwikszenie o 1 wartoci wskanika brane jest w odniesieniu do zadeklarowanego typu danych. Tak wic, jeeli wskanik wskb typu Bazowa* wskazuje na obiekt klasy Pochodna, to warto wskb++ bdzie si odnosi do nastpnego obiektu klasy Bazowa, a nie klasy Pochodna. FMimo, e moemy uywa wskanika wskb do wskazywania obiektu klasy pochodnej, to jednak w tym przypadku uzyskamy dostp jedynie do tych elementw (zmiennych i funkcji) klasy pochodnej, ktre odziedziczya od klasy bazowej (zarwno bezporedniej, jak i poredniej). Powodem jest to, e wskanik do klasy bazowej dysponuje jedynie informacj o tej klasie, a nie wie nic o elementach, dodanych przez klas pochodn. Wprawdzie jest dopuszczalne, aby wskanik do klasy bazowej wskazywa obiekt klasy pochodnej, to jednak twierdzenie odwrotne nie jest prawdziwe. Jest to spowodowane faktem, e kompilatory nie maj mechanizmu sprawdzania dla fazy wykonania czy konwersje w instrukcjach przypisania: wskb = wskp; gdzie wskp jest wskanikiem do klasy pochodnej, pozostawiaj wynik, wskazujcy na obiekt oczekiwanego typu.

8.2.2.

Deklaracje funkcji wirtualnych

Funkcja wirtualna jest to funkcja skadowa klasy, zadeklarowana w klasie bazowej i redefiniowana w klasach pochodnych. Deklaracja funkcji wirtualnej odrnia si od deklaracji zwykej funkcji skadowej jedynie tym, e przed nazw typu zwracanego umieszcza si sowo kluczowe virtual, np. virtual void f(); virtual char* g() const; Nazwy funkcji wirtualnych, wywoywanych za porednictwem wskanikw lub referencji do

obiektw, wizane s z ich adresami w fazie wykonania. Jest to omawiane wczeniej wizanie pne (dynamiczne). Natomiast funkcje zadeklarowane jako wirtualne, a wywoywane dla obiektw, s wizane w fazie kompilacji (wizanie wczesne, albo statyczne). Przyczyn tego jest fakt, e typ obiektu jest ju znany po kompilacji. Np. dla deklaracji: class Test { public: virtual void ff(); }; Test t1; wywoanie: t1.ff(); bdzie wizane statycznie, a wi funkcja ff() dostanie adres w fazie kompilacji i straci cech wirtualnoci. Prezentowany niej program wydrukuje warto x: 10. Zwrmy uwag na definicj funkcji podaj(): sowo kluczowe virtual wystpuje tylko w jej deklaracji; bdem syntaktycznym byoby umieszczenie go w definicji funkcji, umieszczonej poza ciaem klasy. Przykad 8.4. #include <iostream.h> class Bazowa { public: int x; Bazowa(int i): x(i) {} virtual void podaj(); }; void Bazowa::podaj() { cout << "x: " << x << endl; } int main() { Bazowa *wsk; Bazowa obiekt(10); wsk = &obiekt; wsk->podaj(); return 0; } Przykad 8.5. #include <iostream.h> class Bazowa { public: int x; Bazowa(int i): x(i) {} virtual void podaj(); }; void Bazowa::podaj() { cout << "x = "<< x << endl; } class Pochodna1 : public Bazowa { public:

Pochodna1(int x): Bazowa(x) {} void podaj(); }; void Pochodna1::podaj() { cout << "x + x = "<< x + x << endl; } class Pochodna2 : public Bazowa { public: Pochodna2(int x): Bazowa(x) {} }; int main() { Bazowa* wsk; Bazowa obiekt(10); Pochodna1 obiekt1(20); Pochodna2 obiekt2(30); wsk = &obiekt; wsk->podaj(); wsk = &obiekt1; wsk->podaj(); wsk = &obiekt2; wsk->podaj(); return 0; } Dyskusja. Wirtualna funkcja skadowa podaj() jest redefiniowana jedynie w klasie pochodnej Pochodna1; w klasie Pochodna2 musi by uywana definicja z klasy Bazowa. W rezultacie wydruk z programu bdzie mia posta: x = 10 x + x = 40 x = 30 W programie wszystkie wizania funkcji wirtualnej podaj() byy wizaniami dynamicznymi, realizowanymi w fazie wykonania programu. Gdyby wywoania funkcji podaj() zwiza z tworzonymi obiektami, a nie ze wskanikami do tych obiektw, np. obiekt1.podaj(), to wizania miayby miejsce w fazie kompilacji, a wi byyby wizaniami wczesnymi (statycznymi). Dokonamy teraz przegldu podstawowych wasnoci funkcji wirtualnych. Funkcje wirtualne mog wystpowa jedynie jako funkcje skadowe klasy. Zatem, podobnie jak funkcje skadowe rozwijalne i statyczne, nie mog by deklarowane ze specyfikatorem extern. Specyfikatora virtual nie wolno uywa w deklaracjach statycznych funkcji skadowych. Nie wolno, poniewa wywoanie funkcji wirtualnej odnosi si do konkretnego obiektu, ktry uywa zredefiniowanej treci funkcji. Inaczej mwic, funkcja wirtualna wykorzystuje niejawny wskanik this, ktrego nie ma funkcja statyczna. Funkcja wirtualna moe by zadeklarowana jako zaprzyjaniona (friend) w innej klasie. Funkcja wirtualna, zdefiniowana w klasie bazowej, nie musi by redefiniowana w klasie

pochodnej. W takim przypadku wywoanie z obiektu klasy pochodnej bdzie korzysta z definicji, zamieszczonej w klasie bazowej. Jeeli definicja funkcji wirtualnej z klasy bazowej zostanie przesonita now definicj w klasie pochodnej, to funkcja przesaniajca jest rwnie uwaana za wirtualn. Specyfikator virtual moe by uyty dla funkcji przesaniajcej w klasie pochodnej, ale bdzie to redundancja. Jednak taka rozwleko moe by korzystna dla celw dokumentacyjnych. Jeeli w trakcie czytania tekstu programu chcemy sprawdzi, czy dana definicja odnosi si do funkcji wirtualnej, czy te nie, specyfikator virtual zaoszczdzi nam czas na poszukiwanie. Jzyk C++ wymaga dokadnej zgodnoci deklaracji pomidzy funkcj wirtualn w klasie bazowej a funkcj, ktra j przesania w klasie pochodnej. Inaczej mwic, funkcje te musz mie identyczne sygnatury, tj. liczb, kolejno i typy argumentw oraz ten sam typ zwracany. Ewentualne naruszenie tej zgodnoci spowoduje, e kompilator nie bdzie traktowa funkcji zredefiniowanej jako wirtualnej, lecz jako funkcj przecion, bd now funkcj skadow. Ostatnio komitet normalizacyjny ANSI X3J16 rozluni powysze ograniczenie. Teraz dopuszcza si niezgodno typw funkcji w nastpujcym przypadku: jeeli oba typy zwracane s wskanikami lub referencjami do klas i jeeli klasa w typie zwracanym przez funkcj wirtualn klasy pochodnej jest publicznie dziedziczona od klasy w typie zwracanym przez funkcj wirtualn klasy bazowej. Dla tej nowej wykadni podane niej deklaracje (bdne przy poprzedniej) bd poprawne: class X { class Y : class A { public: virtual }; class B : public: virtual }; }; public X { }; X& fvirt(); public A { Y& fvirt();

Podsumujmy powysze uwagi. Skadnia wywoania funkcji wirtualnej jest taka sama, jak skadnia wywoania zwykej funkcji skadowej. Interpretacja wywoania funkcji wirtualnej zaley od typu obiektu, dla ktrego jest woana; jeeli np. mamy klas Test z zadeklarowan w niej funkcj wirtualn virtual void ff(); to cig instrukcji Test* wsk = new Test; Test t1; Test& tref = t1; wsk->ff(); tref.ff(); mwi: hej, adresowany obiekcie, wybierz swoj wasn funkcj ff() i wykonaj j. Inaczej mwic: poniewa obiekty s powoywane do ycia w fazie wykonania, zatem wywoanie funkcji wirtualnej musi by wizane z jedn z jej definicji (metod) dopiero w fazie wykonania. Mona w tym miejscu postawi pytanie: w jaki sposb informacja o typie obiektu dla wywoania

funkcji wirtualnej jest przekazywana przez kompilator do rodowiska wykonawczego? Odpowied na to pytanie nie moe abstrahowa od implementacji jzyka C++. Przyjtym w jzyku C++ rozwizaniem jest implementacja funkcji wirtualnych za pomoc tablicy wskanikw do funkcji wirtualnych. Np. przy dziedziczeniu pojedynczym kada klasa, w ktrej zadeklarowano funkcje wirtualne, utrzymuje tablic wskanikw do funkcji wirtualnych, a kady obiekt takiej klasy bdzie zawiera wskanik do tej tablicy. Wemy dla ilustracji nastpujcy schemat dziedziczenia: class X { public: virtual void f(); virtual void g(int); virtual void h(char*); private: int a; }; class Y { public: void g(int); virtual void r(Y*); private: int b: }; class Z { public: void h(char*); virtual void s(Z*) private: int c; }; Przy powyszych deklaracjach struktura obiektu klasy Z bdzie podobna do pokazanej na rysunku 8-2.

a vtpr b c wskazanie na vtbl

&X::f &Y::g &Z::h &Y::r &Z::s

Obiekt klasy Z
Rys. 8-2

Tablica funkcji wirtualnych (vtbl)


Wskanik do tablicy funkcji wirtualnych

Kady obiekt klasy Z bdzie zawiera ukryty wskanik do tablicy funkcji wirtualnych, nazywany w implementacjach vptr. Wywoanie funkcji wirtualnej jest transformowane przez kompilator w wywoanie porednie. Np. wywoanie funkcji g() z bloku funkcji f

void f(Z* wsk) { wsk->g(10); } wygeneruje kod w rodzaju: (*(wsk->vptr[1]))(wsk,10); Zasada jest taka, e przy dziedziczeniu pojedynczym dla kadej klasy z funkcjami wirtualnymi utrzymywana jest dokadnie jedna tablica funkcji wirtualnych. Przy dziedziczeniu mnogim klasa z funkcjami wirtualnymi, pochodna np. od dwch klas bazowych bdzie miaa dwie takie tablice; obiekt tej klasy bdzie mia odpowiednio dwa ukryte wskaniki, po jednym do kadej z tablic. Powd jest oczywisty: kady obiekt klasy pochodnej od kilku klas bazowych bdzie zawiera podobiekty tych klas, a z kadym podobiektem bdzie skojarzona jego wasna tablica funkcji wirtualnych.

8.2.3.

Zasig i regua dominacji

Jak pamitamy, kada klasa wyznacza swj wasny zasig, a jej zmienne i funkcje skadowe mieszcz si w zasigu swojej klasy. Nazwy w zasigu klasy mog by dostpne dla kodu zewntrznego w stosunku do klasy, jeeli uyjemy do tego celu operatora zasigu ::. Zasig odgrywa kluczow rol w mechanizmie dziedziczenia. Z punktu widzenia klasy pochodnej dziedziczenie wprowadza nazwy z zasigu klasy bazowej w zasig klasy pochodnej. Inaczej mwic, nazwy zadeklarowane w klasach bazowych s dziedziczone przez klasy pochodne. Nazwy w zasigu klasy pochodnej s w podobnej relacji do nazw w klasie bazowej, jak nazwy w bloku wewntrznym do nazw w bloku go otaczajcym. W bloku wewntrznym zawsze moemy uy nazwy zadeklarowanej w bloku zewntrznym. Jeeli w bloku wewntrznym zdefiniujemy tak sam nazw, jak zdefiniowana w bloku zewntrznym, to nazwa w bloku wewntrznym ukryje nazw z bloku zewntrznego. Przy dziedziczeniu mnogim tworzenie obiektu klasy pochodnej zaczyna si zawsze od utworzenia podobiektw wczeniej zadeklarowanych klas bazowych. W tym przypadku zasig klasy pochodnej jest zagniedony w zasigach wszystkich jej klas bazowych. Jeeli dwie klasy bazowe zawieraj t sam nazw, wtedy albo jedna nazwa musi dominowa nad drug, albo klasa pochodna musi usun niejednoznaczno przez ukrycie deklaracji z klas bazowych. Dopuszczalno przesaniania nazw z rnych gazi drzewa lub grafu dziedziczenia wymaga sformuowania zasady okrelajcej, jakie kombinacje mog by akceptowane, a jakie naley odrzuci jako bdne. Zasada taka, nazywana regu dominacji, zostaa sformuowana przez B.Stroustrupa i A.Koeniga; brzmi ona nastpujco: Nazwa B::f dominuje nad nazw A::f, jeeli klasa B, w ktrej f jest skadow, jest klas pochodn od klasy A. Jeeli pewna nazwa dominuje nad inn, to nie ma midzy nimi kolizji. Nazwa dominujca zostanie uyta wtedy, gdy istnieje wybr. Zauwamy, e regua dominacji stosuje si zarwno do funkcji, jak i zmiennych skadowych. Prezentowany niej przykad ilustruje dziaanie tej zasady w dziedziczeniu pojedynczym. Przykad 8.6. // Dominacja w dziedziczeniu pojedynczym // bez klas i funkcji wirtualnych #include <iostream.h> class Bazowa { public:

void f() { cout void g() { cout }; class Pochodna1: public: void f() { cout }; class Pochodna2: public: void g() { cout }; int main() { Pochodna1 po; po.f(); po.g(); return 0; }

<< "Bazowa::f()\n"; } << "Bazowa::g()\n" << endl; } public Bazowa { << "Pochodna1::f()\n"; } public Bazowa { << "Pochodna2::g()\n" << endl; }

Z programu otrzymuje si wydruk o postaci: Pochodna1::f() Bazowa::g() Dyskusja. W instrukcji po.f(); wywoywana jest funkcja f() klasy Pochodna1, poniewa nazwa Pochodna1::f dominuje nad nazw Bazowa::f. Podobny efekt daoby wywoanie funkcji g() dla obiektu klasy Pochodna2. Natomiast instrukcja po.g(); wywouje Bazowa::g(), poniewa w klasie Pochodna1 nazwa g nie wystpuje, ale klasa ta ma dostp do funkcji skadowej g() klasy Bazowa. Przykad 8.7. /* Dominacja w dziedziczeniu mnogim z klasami wirtualnymi, lecz bez funkcji wirtualnych */ #include <iostream.h> class Bazowa { public: void f() { cout << "Bazowa::f()\n"; } }; class Pochodna2: virtual public Bazowa { public: void f() { cout << "Pochodna2::f()\n"; } }; class Pochodna1: virtual public Bazowa, virtual public Pochodna2 { }; int main() { Pochodna1 po1;

po1.f(); return 0; } Dyskusja. Wydruk z programu ma posta: Pochodna2::f(). Nazwa f nie wystpuje w deklaracji klasy Pochodna1, natomiast wystpuje w klasach Bazowa i Pochodna2, ktre s publicznymi wirtualnymi klasami bazowymi klasy Pochodna1. Z reguy dominacji wynika, e nazwa f w klasie Pochodna2 dominuje nad t sam nazw w jej wirtualnej klasie bazowej. Nastpny przykad ilustruje w peni korzyci, wynikajce z reguy dominacji. Schemat dziedziczenia jest tutaj grafem acyklicznym, w ktrym dwie klasy dziedzicz od wsplnej publicznej, wirtualnej klasy bazowej. Klasy te s bezporednimi publicznymi klasami bazowymi dla najniszej w hierarchii klasy pochodnej. Przyjty schemat dziedziczenia, wraz z deklaracjami funkcji wirtualnych we wsplnej klasie bazowej i wywoaniami tych funkcji przez wskaniki zapewnia jednoznaczno odwoa. Przykad 8.8. //Dominacja: klasy i funkcje wirtualne #include <iostream.h> class Bazowa { public: virtual void f() { cout << "Bazowa::f()\n"; } virtual void g() { cout << "Bazowa::g()\n"; } }; class Pochodna1: virtual public Bazowa { public: void g() { cout << "Pochodna1::g()\n"; } }; class Pochodna2: virtual public Bazowa { public: void f() { cout << "Pochodna2::f()\n"; } }; class Pochodna12: public Pochodna1, public Pochodna2 { }; int main() { Pochodna12 obiekt; Pochodna12* wsk12 = &obiekt; wsk12->f(); wsk12->g(); Pochodna1* wsk1 = wsk12; wsk1->f(); Pochodna2* wsk2 = wsk12; wsk2->g(); return 0; } Wykonanie programu da wydruk o postaci: Pochodna2::f Pochodna1::g() Pochodna2::f() Pochodna1::g()

Analiza programu. Wywoanie wsk12->f() zostanie rozpoznane jako odnoszce si do nazwy f w klasie Pochodna2, co wynika z reguy dominacji i z faktu, e funkcja f() zostaa zadeklarowana jako wirtualna. To samo odnosi si do pozostaych wywoa.

8.2.4.

Wirtualne destruktory

Destruktor jest, podobnie jak konstruktor, specjaln funkcj skadow klasy. Jak wiadomo, zadaniem konstruktora jest inicjowanie zmiennych skadowych przy tworzeniu obiektu; destruktor wykonuje wszelkie czynnoci zwizane z usuwaniem obiektu, jak dealokacja uprzednio przydzielonej pamici operatorem new, itp. Jeeli w klasie nie zadeklarowano destruktora, to bdzie niejawnie wywoywany konstruktor domylny generowany przez kompilator. Wywoanie to ma miejsce, gdy koczy si okres ycia obiektu, np. gdy sterowanie opuszcza blok, w ktrym zadeklarowano obiekt lokalny lub dla obiektw statycznych gdy koczy si cay program. Destruktor moe by rwnie zdefiniowany w klasie; wwczas bdzie on wywoywany niejawnie zamiast destruktora generowanego przez kompilator. Taki destruktor mona te wywoywa jawnie; np. dla deklaracji: class Test { public: Test() { }; ~Test() { }; }; destruktor ~Test() moe by wywoany dla obiektu klasy Test z kwalifikatorem zawierajcym nazw klasy i operator zasigu lub bez, zalenie od kontekstu: Test t1; t1.~Test(); t1.Test::~Test(); Konieczno definiowania wasnych destruktorw zachodzi wtedy, gdy przy tworzeniu obiektu s alokowane jakie oddzielnie zarzdzane zasoby, np. gdy wewntrz obiektu jest tworzony w pamici swobodnej podobiekt. Wtedy zadaniem destruktora bdzie zwolnienie tego obszaru pamici. Jawne wywoanie destruktora na rzecz jakiego obiektu powoduje jego wykonanie, ale niekoniecznie zwalnia pami przydzielon zmiennym obiektu i nie zawsze oznacza cakowite zakoczenie ycia obiektu; co wicej, niewaciwie zaprojektowany destruktor moe prbowa zwalnia ju uprzednio zwolnione zasoby. Podany niej przykad ilustruje tak wasnie sytuacj. Przykad 8.9. #include <iostream.h> class Test { public: enum { n = 10 }; double* wsk; Test() { wsk = new double[n]; } ~Test() { if(wsk) {

delete [] wsk; cout << "Zniszczenie wsk\n"; // wsk = NULL; } else cout << "wsk==NULL\n"; } }; int main() { Test t1; t1.~Test(); return 0; } Dyskusja. Jeeli instrukcja wsk = NULL; bdzie traktowana jako komentarz, to wykonanie programu da wydruk: Zniszczenie wsk Zniszczenie wsk W tym przypadku destruktor by wywoywany dwukrotnie: raz jawnie instrukcj t1.~Test(); i drugi raz niejawnie przy ostatecznym usuwaniu obiektu t1 tu przed zakoczeniem programu. Dopuszczenie do takiej sytuacji jest oczywistym bdem programistycznym. Jeeli z programu usuniemy symbol komentarza przed instrukcj wsk = NULL; to zostanie ona wykonana, a wydruk bdzie mia posta: Zniszczenie wsk wsk==NULL Teraz destrukcja obiektu t1 przebiega poprawnie: przy pierwszym wywoaniu destruktor zwalnia pami zajmowan przez tablic dziesiciu liczb typu double i nastpnie przypisuje wskanikowi wsk adres pusty. Przy drugim wywoaniu drukuje napis wsk==NULL i usuwa to, co jeszcze pozostao z obiektu t1 (w t czynno nie wchodzimy, poniewa zaley ona od implementacji, a wic mieci si na niszym poziomie abstrakcji w stosunku do klas i obiektw). Problemy z destrukcj obiektw uzyskuj dodatkowy wymiar, gdy uwzgldnimy dziedziczenie. Ilustruje to nastpny przykad. Przykad 8.10. #include <iostream.h> class Bazowa { public: Bazowa() { cout << "Konstruktor klasy bazowej\n"; } ~Bazowa() { cout << "Destruktor klasy bazowej\n"; } }; class Pochodna: public Bazowa { public: Pochodna() { cout << "Konstruktor klasy pochodnej\n"; } ~Pochodna() { cout << "Destruktor klasy pochodnej\n"; }

}; int main() { Bazowa* wskb = new Pochodna; delete wskb; return 0; } Analiza programu. W przykadzie posuono si wskanikiem wskb do klasy bazowej, ktremu przypisano obiekt klasy pochodnej. Wydruk z programu ma posta nieoczekiwan: Konstruktor klasy bazowej Konstruktor klasy pochodnej Destruktor klasy bazowej Konstrukcja obiektu klasy pochodnej przebiega prawidowo (najpierw jest tworzony obiekt klasy bazowej, a nastpnie pochodnej). Natomiast po wykonaniu instrukcji delete wskb; w sytuacji gdy wskanik by ustawiony na adres obiektu klasy pochodnej mona si byo spodziewa wywoania destruktora klasy pochodnej, a nie bazowej. Rzecz w tym, e wywoanie delete nic nie wie o tym, i obiekt jest klasy Pochodna, a w klasie Bazowa nie ma adnej wskazwki, e wywoania destruktora maj przeszukiwa hierarchi klas. Konsekwencje opisanej wyej sytuacji zostay ju zasygnalizowane wczeniej. Jeeli konstruktor klasy pochodnej przydzieli obiektowi tej klasy pewne zasoby, ktre mia zwolni jej destruktor, to dziaanie takie nie zostanie wykonane i zasoby te stan si tzw. nieuytkami ( ang. garbage). Rozwizaniem tego problemu jest wprowadzenie destruktorw wirtualnych. Nastpny przykad ilustruje sposb definiowania destruktora wirtualnego w klasach bazowej i pochodnej oraz skadni jego wywoania. Przykad 8.11. // Destruktor wirtualny #include <iostream.h> class Bazowa { public: Bazowa() { cout << "Konstruktor klasy bazowej\n"; } virtual ~Bazowa() { cout << "Destruktor klasy bazowej\n"; } }; class Pochodna: public Bazowa { public: Pochodna() { cout << "Konstruktor klasy pochodnej\n"; } ~Pochodna() { cout << "Destruktor klasy pochodnej\n"; } }; int main() { Bazowa* wskb = new Pochodna; delete wskb; return 0; }

Dyskusja. Program wydrukuje nastpujce napisy: Konstruktor klasy bazowej Konstruktor klasy pochodnej Destruktor klasy pochodnej Destruktor klasy bazowej Teraz konstrukcja i destrukcja obiektu przebiega poprawnie. Dzieje si to za spraw destruktora klasy bazowej, zadeklarowanego ze sowem kluczowym virtual. Wirtualny destruktor klasy bazowej zosta zredefiniowany w klasie pochodnej; musielimy uy w tym celu nazwy klasy pochodnej, czyli zrobi odstpstwo od zasad definiowania funkcji wirtualnych. Jest to (na szczcie) odstpstwo dopuszczalne, jeli chcemy zachowa zasady nazewnictwa konstruktorw i destruktorw. Funkcja wirtualna, jak jest wirtualny destruktor, jest woana dla wskanika do obiektu; zatem jej definicja bdzie wizana z wywoaniem dynamicznie, w fazie wykonania. Poniewa wskanik wskb adresuje obiekt klasy Pochodna, zatem instrukcja delete wskb; wywoa destruktor tej klasy. Oczywicie natychmiast po tym zostanie wywoany destruktor klasy Bazowa, zgodnie z obowizujc kolejnoci wywoania destruktorw dla klas pochodnych. W przykadach destrukcji obiektw w drzewie dziedziczenia pominlimy spraw zwalniania dodatkowych zasobw, alokowanych dla obiektu przez konstruktor. Uczyniono tak w celu uproszczenia zapisu, aby pokaza inny aspekt problemu destrukcji. Oczywicie i w przypadku dziedziczenia kopoty ze zwalnianiem zasobw, czy te usuwaniem zasobw ju usunitych, s podobne, a czsto zwielokrotnione wskutek zastosowania mechanizmu dziedziczenia. Innym pominitym aspektem, o ktrym warto wspomnie, jest problem rozpoznania obiektu, dla ktrego jest wywoywany destruktor klasy bazowej. Jeeli usuwamy obiekt klasy pochodnej, to najpierw jest woany destruktor tej klasy, a nastpnie destruktor klasy bazowej. Tak wic w chwili wywoania destruktora klasy bazowej obiekt klasy pochodnej ju nie istnieje; istniej tylko jego skadowe, odziedziczone z klasy bazowej. W tej fazie destruktor nie moe si dowiedzie, czy nale one do obiektu klasy pochodnej, czy bazowej...

8.2.5.

Klasy abstrakcyjne i funkcje czysto wirtualne

Funkcje wirtualne, definiowane w klasach bazowych na szczycie hierarchii klas, bardzo czsto s jedynie makietami, umieszczonymi z myl o napisaniu dla nich sensownych definicji w klasach pochodnych. Jest to sytuacja typowa, poniewa klasa bazowa czsto jest projektowana jako pewien prototyp dla klas pochodnych. Zwyke funkcje skadowe mog mie w takiej klasie puste bloki definicji. Jeeli funkcja wirtualna w klasie bazowej nie wykonuje adnego dziaania, to kada klasa pochodna musi przesoni t funkcj. Dla takiego przypadku przewidziano w jzyku C++ funkcje czysto wirtualne. W klasie bazowej wymagana jest jedynie deklaracja, wprowadzajca prototyp funkcji czysto wirtualnej (nie ma definicji). Ma ona posta: virtual typ nazwa(wykaz parametrw) = 0;

Istotn czci tej deklaracji jest zwizanie ciaa (bloku) funkcji ze wskanikiem zerowym. Jest to informacja dla kompilatora, e nie istnieje ciao tej funkcji dla klasy bazowej. Po otrzymaniu takiej informacji kompilator bdzie wymusza redefinicje funkcji czysto wirtualnej w kadej klasie pochodnej, ktra ma mie wystpienia (obiekty), poniewa funkcja czysto wirtualna jest dziedziczona jako czysto wirtualna. Klasa bazowa, ktra zawiera conajmniej jedn funkcj czysto wirtualn, jest nazywana

abstrakcyjn klas bazow. Nazwa jest uzasadniona tym, e taka klasa nie moe mie swoich wystpie, tj. obiektw; natomiast mona jej uywa dla tworzenia klas pochodnych. Klasa abstrakcyjna nie moe mie swoich wystpie, poniewa w sensie technicznym nie jest kompletnym typem ze wzgldu na brak definicji funkcji czysto wirtualnej. Zauwamy take, e jeli w klasie pochodnej od klasy abstrakcyjnej nie redefiniuje si odziedziczonej funkcji czysto wirtualnej, to taka klasa pochodna bdzie rwnie klas abstrakcyjn, pozbawion moliwoci tworzenia wasnych obiektw. Ponadto mona wymieni nastpujce wasnoci klas abstrakcyjnych: Dopuszcza si deklarowanie wskanikw i referencji do klas abstrakcyjnych. Klasa abstrakcyjna nie moe by uyta jako typ argumentu, jako typ zwracany oraz jako typ w jawnej konwersji (zmiennymi, bd staymi tych typw musz by obiekty). Funkcja czysto wirtualna w deklaracji abstrakcyjnej klasy bazowej wystpuje jedynie w postaci specyficznego prototypu. Tym niemniej mona zdefiniowa ciao tej funkcji na zewntrz deklaracji tej klasy i wywoywa j za pomoc operatora zasigu. Przykad 8.12. #include <iostream.h> class Bazowa { public: virtual void czysta() = 0; }; class Pochodna: public Bazowa { public: Pochodna() { } void czysta() { cout << "Pochodna::czysta" << endl; } void ff() { Bazowa::czysta(); } }; void Bazowa::czysta() { cout << "Bazowa::czysta" << endl; } int main() { Pochodna obiekt; obiekt.Bazowa::czysta(); obiekt.ff(); obiekt.czysta(); Bazowa* wsk = &obiekt; wsk->czysta(); return 0; } Wydruk z programu ma posta: Bazowa::czysta Bazowa::czysta Pochodna::czysta Pochodna::czysta

Podany niej rysunek i przykad ilustruje konstrukcj i wartociowanie wyrae arytmetycznych, reprezentowanych przez tzw. drzewa wyrae. W informatyce drzewem nazywa si struktur zoon z wzw i gazi. Takie informatyczne drzewo przedstawia si graficznie w taki sposb, e najwyszym wzem u gry jest korze drzewa, za wzy u samego dou to jego licie, a wic odwrotnie, ni to czynimy przy rysowaniu drzew, wystpujcych w przyrodzie. Drzewa wyrae nale do podstawowych struktur, stosowanych przy konstrukcji kompilatorw i interpretatorw. Jeeli odniesiemy t struktur do terminologii obiektowej, to stwierdzimy, e korze drzewa odpowiada pewnej pierwotnej klasie bazowej, a kolejne wzy bd klasami pochodnymi, powizanymi w hierarchi dziedziczenia. Poniewa ywe drzewo rozgazia si w ten sposb, e zawsze z gazi grubszej wyrasta jedna lub wicej gazi cieszych, zatem w naszym drzewie bdziemy mie schemat dziedziczenia pojedynczego. Na rysunku 8-3 pokazano kilka przykadowych drzew wyrae. (a) 5 5 (b) 5 5+10 (c) + (d) + + 10

* 5 10 20

* 15 60 -

* 3 30 80

5*10+20*15 Rysunek 8-3

(60-30)*3+80/4

Przykady drzew wyrae

Najprostszym wyraeniem jest staa, np. 5. Jej reprezentacj w drzewie wyraenia jest cz (a) rysunku 8-3. Drzewo staej skada si z jednego wza, ktry jest zarazem korzeniem i liciem. Proste wyraenie arytmetyczne, 5 + 10, jest reprezentowane przez drzewo 8-3(b), w ktrym operator + odpowiada obiektowi klasy Suma, a stae 5 i 10 obiektom klasy constant. Czci (c) i (d) rysunku prezentuj wyraenia o rosncym stopniu zoonoci. Przykad 8.13. #include <iostream.h> class Wrn { public: virtual int wart() = 0; }; class Constant: public Wrn { public:

Constant(int k): x(k) {} int wart() { return x; } private: int x; }; class Suma: public Wrn { public: Suma(Wrn* l, Wrn* p) { lewy = l; prawy = p; } int wart() { return lewy->wart()+prawy->wart(); } private: Wrn* lewy; Wrn* prawy; }; class Odejm: public Wrn { public: Odejm(Wrn* l, Wrn* p) { lewy = l; prawy = p; } int wart() { return lewy->wart()-prawy->wart(); } private: Wrn* lewy; Wrn* prawy; }; class Iloczyn: public Wrn { public: Iloczyn(Wrn* l, Wrn* p) { lewy = l; prawy = p; } int wart() { return lewy->wart() * prawy->wart(); } private: Wrn* lewy; Wrn* prawy; }; class Iloraz: public Wrn { public: Iloraz(Wrn* l, Wrn* p) { lewy = l; prawy = p; } int wart() { return lewy->wart() / prawy->wart(); } private: Wrn* lewy; Wrn* prawy; }; int main() { Constant a(5); Constant b(10); Constant c(15); Constant d(20); Iloczyn e(&a, &b); Iloczyn f(&c, &d); Suma g(&e, &f); Iloraz h(&f, &e); cout << "a*b + c*d = " << g.wart() << endl; cout << "c*d / a*b = " << h.wart() << endl;

return 0; } Wydruk z programu ma posta: a*b + c*d = 350 c*d / a*b = 6 Dyskusja. W powyszym przykadzie starano si pokaza: Zastosowanie abstrakcyjnej klasy bazowej (tutaj klasa Wrn) z czysto wirtualn funkcj skadow (funkcja wart()) do konstruowania drzewa dziedziczenia. Zastosowanie wskanikw do tak okrelonej klasy bazowej; zauwamy, e wskaniki do klasy Wrn w klasach pochodnych s skadowymi tych klas. Konstrukcj drzewa wyraenia za pomoc struktury powizanej wskanikami, w ktrej wzami s obiekty, a gaziami wskaniki. Dla tworzenia rnych wyrae zadeklarowano klasy pochodne Constant, Suma, Odejm, Iloczyn, Iloraz. Wartociowanie wyraenia arytmetycznego za pomoc przekazywania komunikatw; komunikat (wywoanie funkcji) przesany do obiektu-korzenia drzewa wyzwala przesyanie kolejnych komunikatw do odpowiednich wzw drzewa. Po zakoczeniu wszystkich wywoa obiektkorze jest w stanie odpowiedzie na pocztkowy komunikat, podajc warto wyraenia. Klasa Wrn zawiera funkcj czysto wirtualn wart(), ktra jest redefiniowana w kadej z klas pochodnych. Dla dowolnego obiektu klasy pochodnej funkcja wart() zwraca warto wyraenia reprezentowanego przez swj obiekt i jego obiekty potomne. Oznacza to, e jeeli wywoamy wart()dla korzenia drzewa lub poddrzewa, to funkcja zwrci warto wyraenia, reprezentowanego przez cae drzewo lub poddrzewo.

9. Strumienie i pliki
Strumie jest pewn abstrakcj, opisujc urzdzenie logiczne, ktre albo produkuje albo konsumuje informacj. W jzyku C++ operujemy na strumieniach danych, tzn. sekwencjach wartoci tego samego typu, dostpnych w porzdku sekwencyjnym. Oznacza to, e dostp do n-tej wartoci w strumieniu danych jest moliwy po uzyskaniu dostpu do poprzednich (n-1) wartoci. Przez dostp rozumiemy zarwno czytanie wartoci, jak i wpisywanie wartoci do strumienia. Strumie moe by doczony do urzdzenia fizycznego przez system wejcia/wyjcia dziki odpowiednio zdefinowanym funkcjom czytania i zapisu. Doczenie strumienia do urzdzenia fizycznego jest realizowane w ten sposb, e strumie jest kojarzony z systemowym urzdzeniem logicznym, w ktrym s zdefiniowane wymienione wyej funkcje czytania i zapisu. W jzyku C++ wszystkie strumienie zachowuj si w ten sam sposb, co pozwala na doczanie ich do urzdze fizycznych o rnych wasnociach. Tak wic moemy wykorzysta t sam metod do wyprowadzenia informacji na ekran, na plik dyskowy, czy drukark. Np. wejcie jest sekwencj zdarze, ktre pojawiaj si w systemie: znaki pisane na klawiaturze, wcinicie klawisza myszki, etc. Taka sekwencja zdarze moe by wprowadzona do strumienia wejciowego. Uruchomienie programu w jzyku C++ powoduje automatyczne otwarcie czterech strumieni: cin cout cerr clog standardowe wejcie (domylnym urzdzeniem fizycznym jest klawiatura) standardowe wyjcie (domylnym urzdzeniem fizycznym jest ekran monitora) standardowy bd (ekran) buforowana wersja cerr (ekran)

Ich deklaracje zawarte s w pliku iostream.h. Jeeli uytkownik ma zamiar wprowadza dane do programu z klawiatury i wyprowadza wyniki na ekran, to musi wczy ten plik do swojego programu.

9.1.

Klasy strumieni wejcia/wyjcia

Strumienie jzyka C++ s niczym wicej, ni cigami bajtw. Sposb interpretacji kolejnych bajtw w cigu dla typw wbudowanych jest zawarty w definicjach klas strumieni. Dla typu (klasy) definiowanego w programie uytkownik moe wykorzysta operacje dostpne w klasach strumieni, bd przeciy te operacje na rzecz wasnej klasy. Podstawowe klasy strumieni wejcia/wyjcia s zdefiniowane w dwch plikach nagwkowych: iostream.h oraz fstream.h. Uproszczony schemat hierarchii tych klas pokazano na rysunku 9-1.
ios virtual istream iostream virtual ostream

iostream.h fstream.h fstreambase

ifstream

fstream

ofstream

Rys. 9-1 Klasy strumieni we/wy W pliku nagwkowym iostream.h zawarte s deklaracje czterech podstawowych klas we/wy: ios, istream, ostream i iostream. Klasa ios jest klas bazow dla istream i ostream, ktre z kolei s klasami bazowymi dla iostream. Klasa ios musi by wirtualn klas bazow dla klas istream i ostream, aby tylko jedna kopia jej skadowych bya dziedziczona przez iostream: class istream : virtual public ios { //... } class ostream : virtual public ios { //... } class iostream:public istream,public ostream { //... } W klasie ios jest zadeklarowany wskanik do klasy streambuf, ktra jest abstrakcyjn klas bazow dla caej rodziny klas buforw strumieni. Bufory te su jako chwilowa pami dla danych z wejcia i wyjcia, a take jako sprzgi czce strumienie z urzdzeniami fizycznymi. Poniewa klasy istream i ostream zawieraj wskaniki do innych klas, kada z nich ( bd klasa od niej pochodna) ma zdefiniowany wasny operator przypisania. Obiektem klasy istream jest wymieniony uprzednio strumie cin, za obiektami klasy ostream s strumienie cout, cerr i clog. W klasie istream deklaruje si funkcje operatorowe operator>>(). Przeciony operator pobrania '>>' suy do wprowadzania danych do programu ze strumienia cin, standardowo zwizanego z klawiatur. Przykadowe prototypy tych funkcji maj posta: istream& operator>>(signed char*); istream& operator>>(int&); istream& operator>>(double&); Instrukcj wprowadzania danej ze strumienia cin zapisuje si w postaci: cin >> zmienna; gdzie zmienna zadeklarowanego typu okrela wywoanie odpowiedniego przecionego operatora '>>'. F Uwaga. Operator >> pomija (przeskakuje) przy czytaniu tzw. biae znaki, czyli spacje, znaki tabulacji i znaki nowego wiersza. Naley o tym pamita przy wczytywaniu danych do zmiennych typu char i char*. Przeciony operator wstawiania << jest skojarzony z buforowanym strumieniem cout i suy do wyprowadzania danych na ekran monitora (lub drukark). Przykadowe prototypy funkcji operatorowych << maj posta: ostream& operator<<(short int); ostream& operator<<(unsigned char); ostream& operator<<(long double); Instrukcj wyprowadzania (wstawiania do strumienia cout) wartoci wyraenia zapisuje si w postaci:

cout << wyraenie; Zwrmy uwag na fakt, e funkcje operatorowe dla operatorw << i << zwracaj referencje do obiektw klas istream i ostream, dla ktrych s wywoywane; dziki temu moliwa jest konkatenacja operacji strumieniowych. W pliku nagwkowym fstream.h zadeklarowano klasy strumieni, kierowane do/z plikw: fstreambase, ifstream, fstream i ofstream. Deklaracje tych klas i sposoby korzystania z ich obiektw omwimy w osobnym podrozdziale.

9.1.1. Funkcje skadowe


W klasach ios, istream i ostream znajdujemy deklaracje szeregu funkcji skadowych. W praktyce uywa si kilku do kilkunastu z nich. W podanym niej przykadzie wykorzystano funkcje o nastpujcych prototypach: int get(); zadeklarowan w klasie istream oraz ostream& put(char); zadeklarowan w klasie ostream. Funkcja get() pobiera i przekazuje nastpny znak ze strumienia wejciowego, za funkcja put(char) wstawia znak do strumienia wyjciowego. S to funkcje niszego poziomu ni funkcje operatorowe << i >>, bardzo przydatne w przypadku, gdy strumienie s traktowane jako cigi bajtw, bez dodatkowych interpretacji okrelonych podcigw bajtw. Przykad 9.1. #include <iostream.h> int main() { char znak; while((znak = cin.get()) != '$') cout.put(znak); return 0; } Jeeli z klawiatury wprowadzimy acuch znakw "abcd$" to wygld ekranu bdzie nastpujcy: abcd$ abcd Jeeli w skad acucha znakw wchodz spacje, to bd one rwnie wczytywane do zmiennej znak, co pokazuje nastpny wydruk: a b c d$ abcd Przykad 9.2. #include <iostream.h> int main() { char z, bufor[5]; cin.get(bufor, 5, '\n'); cin.getline(bufor, 5);//ten sam efekt

cin >> bufor; // brak kontroli rozmiaru bufora cin.putback(bufor[2]); z = cin.peek(); for(int i = 0; i < 5; i++) cout.put(bufor[i]); cout.put('\n'); cout << z << endl; return 0; } Jeeli wprowadzimy acuch znakw "abcdef", to wygld ekranu bdzie: abcdef abcd c Dla acucha zawierajcego spacje: "a b c d e f" otrzymamy wydruk: abcdef ab b Dyskusja. W programie wykorzystano inn, przecion wersj funkcji skadowej get() klasy istream o prototypie: istream& get(char* buf, int num, char delim='\n'); ktra czyta znaki do tablicy wskazywanej przez buf dotd, dopki nie wczyta num znakw lub dopki nie napotka znaku, podanego jako delim. Cig znakw w buf zostanie zakoczony przez funkcj znakiem zerowym. Jeeli nie podamy wartoci delim, to domylnym znakiem bdzie '\n'. Jeeli w strumieniu wejciowym znajdzie si taki znak, to nie zostanie on z niego pobrany, lecz pozostanie w strumieniu a do nastpnej operacji wprowadzania. Funkcja getline() ma podobny prototyp: istream& getline(char* buf, int num, char delim='\n'); i dziaa analogicznie za wyjtkiem tego, e pobiera i usuwa znak koczcy wprowadzanie ze strumienia wejciowego. Funkcja putback() o prototypie: istream& putback(char z); zwraca ostatnio pobrany (lub dowolnie wybrany z tablicy, jak w podanym przykadzie) znak do tego samego strumienia, z ktrego zosta pobrany. Funkcja int istream::peek(), ktra pozwala zaglda do wntrza strumienia klasy istream, przekazuje nastpny znak (lub znak koca pliku EOF) bez usuwania go ze strumienia. Zwrmy uwag na posta wydrukw. Jeeli wprowadzamy cig szeciu znakw "abcdef", to funkcja get() wczyta do tablicy bufor[5] tylko pierwsze cztery z nich (pitym bdzie znak zerowy '\0'). Jeeli za podamy znaki ze spacjami, jak w "a b c d e f", to rwnie zostan wczytane do tablicy cztery pierwsze znaki, a wic "a b ". Teraz bufor[0] == 'a', za bufor[2]== 'b' i instrukcja cin.putback(bufor[2]); zwrci 'b' do strumienia cin. Oferowane przez funkcje get() i put() moliwoci mona rozszerzy, stosujc funkcje read() i write() o prototypach: istream& read(char* buf, int num); ostream& write(char* buf, int num);

Funkcja read() czyta num bajtw ze skojarzonego z ni strumienia i wstawia je do bufora, wskazywanego przez buf. Funkcja write() zapisuje num bajtw z bufora wskazywanego przez buf do skojarzonego z ni strumienia. Jeeli funkcja read() napotka znak koca pliku (EOF) zanim przeczyta num bajtw, to skoczy dziaanie, a bufor bdzie zawiera tyle znakw, ile zostao wczytane. Do kontroli wczytywania mona wykorzysta funkcj klasy istream o prototypie int gcount();, ktra przekazuje liczb znakw przeczytanych przez ostatni operacj wprowadzania danych.

9.2.

Formatowanie wejcia i wyjcia

Klasa ios zawiera szereg dwuwartociowych sygnalizatorw formatu (ang. flags), ktre mog by albo wczone (on) albo wyczone (off). Wartoci te decyduj o sposobie interpretacji danych pobieranych ze strumienia wejciowego lub wysyanych do strumienia wyjciowego. Zestawione niej sygnalizatory zwizane s z kadym strumieniem (cin, cout, cerr, clog, strumienie plikowe). ios::skipws przeskocz biae znaki na wejciu ios::left justuj wyjcie do lewej ios::right justuj wyjcie do prawej ios::internal uzupenij pole liczby spacjami ios::dec konwersja na system dziesitny ios::oct konwersja na system semkowy ios::hex konwersja na system szesnastkowy ios::showbase wywietl podstaw systemu liczenia ios::showpoint wywietl kropk dziesitn ios::uppercase wywietl 'X' dla notacji szesnastkowej ios::showpos dodaj '+' przed dodatni liczb dziesitn ios::scientific notacja wykadnicza ios::fixed zastosuj notacj z kropk dziesitn ios::unitbuf oprniaj kady strumie po wstawieniu danych ios::stdio oprniaj stdout i stderr po kadym wstawieniu danych Wszystkie wartoci sygnalizatorw s przechowywane w postaci okrelonego ukadu bitw danej typu long int. Gdy zaczyna si wykonanie programu, z kadym ze strumieni zostaje zwizany oddzielny zbir sygnalizatorw z okrelonymi wartociami domylnymi. Np. dla strumienia cout sygnalizatory skips i unitbuf s ustawione na "on", za pozostae na "off". Uytkownik moe sprawdzi ich ustawienie, wywoujc funkcj long int flags() dla danego strumienia, np. w instrukcji: long int li = cout.flags(); moe te ustawi okrelone sygnalizatory na "on", korzystajc z alternatywnej postaci funkcji long int flags(long int), np. instrukcj: cout.flags(ios::dec | ios::showpos); Przeledmy t instrukcj. Funkcja skadowa flags() klasy ios jest wywoywana z argumentem, bdcym bitow alternatyw. Dziki temu zostan ustawione na "on" obydwa sygnalizatory, tj. dec i showpos. Zastosowanie funkcji flags() do ustawiania sygnalizatorw bywa niezbyt wygodne, poniewa wczajc jeden lub kilka z nich, jednoczenie wycza pozostae, ktrych nie podano w jej argumencie. W takich razach naley raczej korzysta z funkcji skadowej long int setf(long) i komplementarnej do niej funkcji long int unsetf(long int), ktre nie daj wymienionego wyej efektu ubocznego. Tak wic np. dla wczenia w strumieniu cout sygnalizatorw showbase i showpos, nie zmieniajc ustawienia pozostaych, wystarczy napisa

cout.setf(ios::showbase | ios::showpos); Sygnalizatory te mona nastpnie wyczy instrukcj: cout.unsetf(ios::showbase | ios::showpos); Podobnie jak flags(), funkcje setf() i unsetf() mog przekaza aktualne ustawienia sygnalizatorw. Np. wykonanie instrukcji long int li = cout.setf(ios::showbase); zachowa biece ustawienia sygnalizatorw w zmiennej li, po czym ustawi na "on" sygnalizator showbase. F Uwaga. Funkcje flags(), setf() i unsetf() s funkcjami skadowymi klasy ios, a zatem oddziaywuj na strumienie tworzone przez t klas. Dlatego wszelkie wywoania tych funkcji naley wykonywa dla konkretnego strumienia. Funkcja ios::setf() wystpuje rwnie w alternatywnej postaci z dwoma argumentami: long int setf(long int, long int). T posta funkcji wykorzystuje si do ustawiania sygnalizatorw, ktre s skojarzone z tzw. polami bitowymi. W klasie ios zdefiniowano trzy takie pola bitowe typu static const long int: dla sygnalizatorw left, right i internal jest to pole adjustfield dla sygnalizatorw dec, oct i hex jest to pole basefield dla sygnalizatorw scientific i fixed jest to pole floatfield. Sygnalizatory skojarzone z polami bitowymi wykluczaj si wzajemnie tylko jeden z nich moe by wczony, a pozostae wyczone. Tak wic instrukcja cout.setf(ios::oct, ios::basefield); wczy oct i wyczy pozostae sygnalizatory (dec i hex) w tym polu, pozostawiajc bez zmiany wszystkie inne sygnalizatory. Podobnie instrukcja cout.setf(ios::left, ios::adjustfield); wczy left, wyczy right oraz internal i pozostawi bez zmiany pozostae. Jeeli funkcj setf(long int, long int) wywoamy z pierwszym argumentem rwnym zeru, to wyczy ona wszystkie sygnalizatory w podanym polu. Np. instrukcja cout.setf(0, ios::floatfield); wyczy wszystkie sygnalizatory w ios::floatfield, a pozostawi bez zmiany wszystkie pozostae. W klasie ios znajdujemy szereg dalszych funkcji formatujcych. Trzy z nich: fill(), precision() i width(), wykorzystano w poniszym przykadzie. Przykad 9.3.

#include <iostream.h> const double PI = 3.14159265353; int main() { cout.fill('.'); cout.setf(ios::left, ios::adjustfield); cout.width(12); cout << "Wyraz" << '\n'; cout.setf(ios::right, ios::adjustfield); cout.width(12); cout << "Wyraz" << '\n'; cout.width(10); cout << cout.width() << '\n'; cout.setf(ios::showpos); cout.precision(9); cout << PI << '\n'; return 0; } Wydruk z programu ma posta: Wyraz....... .......Wyraz ........10 +3.141592654 Funkcje ios::fill(), ios::precision() i ios::width() su do formatowania wyjcia. Kada z nich wystpuje rwnie w postaci przecionej. Przy wyprowadzaniu dowolnej wartoci, zajmuje ona na ekranie tyle miejsca, ile potrzeba na wywietlenie wszystkich jej znakw. Moemy jednak ustali minimaln szeroko w pola wydruku, wywoujc funkcj o prototypie int width(int w); ktra ustala now szeroko pola w i przekazuje do funkcji woajcej dotychczasow szeroko. Wywoanie przecionej wersji tej funkcji int width() const; przekazuje jedynie aktualn szeroko pola wydruku. Jeeli ustawimy szeroko pola wydruku na w, to przy wyprowadzaniu wartoci, ktra zajmuje mniej ni w znakw, pozostae pozycje znakowe zostan uzupenione aktualnie ustawionym znakiem wypeniajcym. Domylnym znakiem wypeniajcym jest spacja. Jeeli jednak wyprowadzana warto zajmuje wicej ni w znakw, to bd wyprowadzone wszystkie znaki, a wic w tym przypadku ustawiona szeroko pola zostanie zignorowana. Znak wypeniajcy wolne miejsca w polu wydruku mona ustali za pomoc funkcji char fill(char z); ktra ustala nowy znak na z i przekazuje do funkcji woajcej znak dotychczasowy. Wersja bezparametrowa tej funkcji

char fill() const; przekazuje jedynie aktualny znak wypeniajcy. Przy wyprowadzaniu wartoci zmiennopozycyjnych s one drukowane z domyln dokadnoci szeciu miejsc po kropce dziesitnej. Jeeli chcemy mie inn dokadno wydruku, wywoujemy funkcj skadow int precision(int p); ktra ustala dokadno na p miejsc po kropce dziesitnej i przekazuje do funkcji woajcej dotychczasow liczb miejsc. W wersji bezparametrowej int precision() const; funkcja ta przekazuje jedynie aktualn liczb miejsc po kropce dziesitnej.

9.2.1. Manipulatory
Formatowanie wejcia i wyjcia, tj. wprowadzanie zmian stanu strumieni cin i cout za pomoc sygnalizatorw jest w praktyce do kopotliwe. Wemy dla ilustracji nastpujcy przykad. Przykad 9.4. #include <iostream.h> int main() { int ii; cin.setf(ios::hex, ios::basefield); cin >> ii; cout << ii << endl; cout.setf(ios::oct, ios::basefield); cout << ii << endl; return 0; } Wydruk z programu po wprowadzeniu ii==10 ma posta: 10 16 20 Dyskusja. W momencie startu programu jest wczony (ustawienie domylne) sygnalizator dec podstawy liczenia, a pozostae sygnalizatory w polu basefield (oct i hex) s wyczone. Pierwsza instrukcja wywoujca funkcj setf() wcza sygnalizator hex, a wycza pozostae w tym polu. Dlatego warto wczytana do zmiennej ii bdzie traktowana jako liczba szesnastkowa, co wida w drugim wierszu wydruku (strumie cout ma nadal stan domylny, z wczonym sygnalizatorem dec). Po zmianie stanu strumienia cout wprowadzonej wykonaniem instrukcji cout.setf(ios::oct, ios::basefield); wstawiana do strumienia warto ii bdzie interpretowana jako liczba semkowa, tj. liczba 20. Dla uproszczenia notacji przy formatowaniu wejcia i wyjcia wprowadzono w jzyku C++

alternatywn metod zmiany stanu strumieni. Metoda ta wykorzystuje specjalne funkcje, nazywane manipulatorami strumieniowymi lub manipulatorami wejcia/wyjcia. Manipulatory dziel si na bezargumentowe, zadeklarowane w pliku iostream.h oraz jednoargumentowe, zadeklarowane w pliku iomanip.h. Tablica 9.1 dec hex oct endl flush ws setbase(int b) setfill(int z) setprecision(int p) setw(int w) setiosflags(long int f) resetioflags(long int f) Manipulatory strumieniowe Konwersja na liczb dziesitn Konwersja na liczb szesnastkow Konwersja na liczb semkow Przelij znak NL i oprnij strumie Oprnij strumie Pomi spacje Ustal typ konwersji na b Ustal znak dopeniajcy pole na z Ustal liczb miejsc po kropce dziesitnej Ustal szeroko pola na w Wcz sygnalizatory podane w f Wycz sygnalizatory podane w f

Manipulatory strumieniowe wywouje si w ten sposb, e po prostu wstawia si ich nazwy (ewent. z parametrem) w acuch operacji wejcia/wyjcia, np. cout << oct << 127 << hex << 127; cout << setw(4) << 100 << endl; Zauwamy przy okazji, e wczeniej poznalimy ju manipulator endl, ktry wstawia znak nowego wiersza i oprnia bufor wyjciowy oraz manipulator z parametrem setw(int). Manipulator setw(int) jest szczeglnie uyteczny przy wczytywaniu acuchw znakw. Np. sekwencja instrukcji: char buffer[8]; cin >> setw(8) >> buffer; powoduje wczytanie acucha znakw do tablicy znakw buffer. Manipulator setw(8), ktry wyznacza rozmiar tej tablicy znakw, zapobiega przepenieniu bufora. Inaczej mwic, do buffer zostanie wczytane co najwyej 7 znakw, dziki czemu pozostanie miejsce na terminalny znak zerowy ('\0'), ktry wystpuje na kocu kadego acucha znakw. Przykad 9.5. #include <iostream.h> #include <iomanip.h> int main() { int ii;

cout << setiosflags(0x200); cout << hex; cout << 15 << endl; cin >> ii; cout << ii << endl; cout << dec << ii << endl; cout << 127 << setw(4) << hex << 127 << oct << setw(4) << 127 << endl; return 0; } Wydruk z programu ma posta: F 10 A 10 127 7F 177 F Komentarz. Sygnalizatory w klasie ios s zadeklarowane w postaci wyliczenia (enum), w ktrym np. ios::uppercase ma przypisan warto 0x0200 (dziesitnie 512, oktalnie 01000); std warto argumentu funkcji setiosflags(0x200), ktra ustawia due litery dla notacji szesnastkowej.

9.3.

Pliki

We wprowadzeniu do tego rozdziau stwierdzono, e strumienie, czyli obiekty klas strumieniowych, mona kojarzy z predefiniowanymi urzdzeniami logicznymi. Urzdzenia te, nazywane niekiedy plikami specjalnymi, su do komunikacji programu z otoczeniem, tj. z reprezentowanymi przez nie urzdzeniami fizycznymi. Zauwamy przy okazji, e jedynymi plikami specjalnymi, bezporednio dostpnymi z obiektw klas zadeklarowanych w iostream.h s nienazwane urzdzenia, doczane automatycznie do strumieni cin, cout, cerr i clog. Dla dostpu do plikw nazwanych, takich jak pliki dyskowe, musimy korzysta z klas strumieni zadeklarowanych w pliku nagwkowym fstream.h (rys. 9-1): class class class class fstreambase : virtual public ios { }; ifstream: public fstreambase,public istream {}; ofstream: public fstreambase,public ostream {}; fstream: public fstreambase,public iostream {};

Poniewa klasy te s klasami pochodnymi od ios, istream, ostream i iostream, zatem maj one dostp do wszystkich elementw publicznych i chronionych swoich klas bazowych. Strumienie wejciowe musz by obiektami klasy ifstream; strumienie wyjciowe obiektami klasy ofstream. Strumienie, ktre mog wykonywa zarwno operacje wejciowe, jak i wyjciowe, musz by obiektami klasy fstream. Jeeli zadeklarujemy jaki obiekt (strumie) jednej z klas, np. ifstream iss; to moemy go skojarzy z konkretnym plikiem za pomoc funkcji skadowej tej klasy, w tym przypadku ifstream::open(), np.

iss.open("plikwe.doc", ios::in); Funkcja open() wykonuje szereg operacji, okrelanych jako otwarcie pliku. Prototyp funkcji open() ma nastpujc posta: void open(char* nazwa, int tryb, int dostp); gdzie: zmienna nazwa jest nazw otwieranego pliku, staa tryb okrela sposb otwarcia pliku, zmienna dostp okrela prawa dostpu do pliku.

Wartoci argumentw funkcji open() mog by nastpujce. Wartociami zmiennej nazwa mog by acuchy znakw, zapisywane zgodnie z zasadami obowizujcymi w danym systemie operacyjnym. Staa tryb moe by jedn ze staych, zdefiniowanych w klasie ios: ios::app Dopisuj nowe dane na kocu istniejcego pliku. Utwrz plik, jeeli nie istnieje. Moe wystpi tylko dla obiektw klas ofstream i fstream. ios::ate Wymusza, po otwarciu, przejcie na koniec pliku. Moe wystpi dla obiektw wszystkich trzech klas. ios::in Otwrz plik do odczytu. Moe wystpi dla obiektw klas ifstream i fstream. ios::nocreate Powoduje nieudane wykonanie funkcji open(), jeeli plik nie istnieje. ios::noreplace Powoduje nieudane wykonanie funkcji open(), jeeli plik ju istnieje, chyba e podano rwnie app lub ate. ios::out Otwrz plik do zapisu. Jeeli plik ju istnieje, wyzeruj jego zawarto; utwrz plik, jeeli nie istnieje. Moe wystpi dla obiektw klas ofstream i fstream. ios::trunc Wyzeruj zawarto istniejcego pliku o takiej samej nazwie, jak podana dla zmiennej nazwa. Z wyliczonych wyej staych mona tworzy alternatywy za pomoc bitowego operatora |. Np. tryb ios::in | ios::out pozwala zarwno na odczyt, jak i zapis (tylko dla obiektu klasy fstream). Jeeli chcemy zachowa dane w istniejcym ju pliku, to ustawimy tryb:

ios::in | ios::out | ios::ate Zmienna dostp ma warto domyln filebuf::openprot, gdzie static const int openprot jest liczb, okrelajc prawa dostpu. Dla systemu Unix openprot==0644 (read i write dla waciciela pliku i tylko read dla pozostaych); zgodnie z reguami dostpu warto ta moe by dowoln liczb z zakresu 0000-0777. Dla systemu MS-DOS warto domylna openprot==0; moe ona wynosi: 0 dla swobodnego dostpu do pliku, 1 dla pliku tylko do czytania, 2 dla pliku ukrytego, 4 dla pliku systemowego i 8 dla ustawienia bitu archiwizacji. Deklaracj strumienia mona poczy z instrukcj otwarcia pliku podajc nazw pliku i tryb dostpu jako argumenty konstruktora odpowiedniej klasy. Np. instrukcja ifstream obin("plikwe.doc",ios::in, filebuf::openprot); deklaruje obiekt obin klasy ifstream, wie go z plikiem o nazwie plikwe.doc, ustala tryb na ios::in, a dostp na filebuf::openprot. Poniewa konstruktory omawianych klas s zdefiniowane z domylnymi wartociami argumentw (staej tryb i zmiennej dostp), to podan wyej deklaracj wystarczy napisa w postaci: ifstream obin("plikwe.doc"); (tryb==ios::in, dostp==filebuf::openprot) a deklaracj otwarcia pliku na pisanie np. w postaci: ofstream obout("plikwy.txt"); (tryb==ios::out, dostp==filebuf::openprot) W deklaracji strumienia nazw pliku mona poprzedzi nazw katalogu, np. "c:\borlandc\plikwe.doc" (MS-DOS), czy "/home/mike/plikwe.doc" (Unix). Podobnie jak dla zwykych plikw, strumienie mona kojarzy z plikami specjalnymi, reprezentujcymi inne urzdzenia fizyczne. Np. deklaracja (MS-DOS: ofstream druk("lpt1"); kieruje dane, wstawiane do strumienia druk na drukark. Przykad 9.6. #include <fstream.h> #include <stdlib.h> int main() { ofstream ofs; ofs.open("plik1.doc",ios::out,filebuf::openprot); if ( !ofs ) { cerr << "Nieudane otwarcie pliku do zapisu\n"; exit( 1 ); } ofs << "To jest pierwszy wiersz tekstu, \n"; ofs << "a to drugi.\n"; */ ofs.close();

return 0; } Dyskusja. Program otwiera do zapisu plik plik1.doc. Jeeli nie byo pliku o takiej nazwie na dysku, to zostanie zaoony. Zwrmy uwag na kilka szczegw. Mimo e nie doczylimy pliku iostream.h, uywany jest operator wstawiania << i to nie do strumienia cout, lecz do zadeklarowanego przez nas strumienia os. Moglimy tak zrobi, poniewa klasa ofstream odziedziczya ten operator od klasy ostream. W programie umieszczono wywoanie funkcji skadowej close() zamknicia pliku os. Jest to funkcja skadowa klasy fstreambase, odziedziczona od niej przez klas ofstream. Wywoanie to nie byo konieczne, poniewa jest ono wykonywane automatycznie przy zakoczeniu programu. W instrukcji if wykorzystano przeciony na rzecz klasy ios logiczny operator ! do sprawdzenia, czy otwarcie pliku zakoczyo si powodzeniem. Zauwamy te, e utworzony (lub na nowo zapisany) plik ma zawarto zero (0). Gdyby wymaza znaki komentarza (/* i */), to dwie ostatnie instrukcje wpisayby do pliku podane dwa wiersze tekstu. Przykad 9.7. #include <fstream.h> int main() { char znak; char* tekst1 = "Tekst w pliku plik1.txt"; char* tekst2 = "Tekst w pliku plik2.txt\n"; char* tekst3 = "Tekst dodawany"; ofstream ofs1("plik1.txt"); ofstream ofs2("plik2.txt"); ofs1 << tekst1; ofs1.close(); ofs2 << tekst2 << tekst3; ofs2.close(); ifstream ifs1("plik2.txt"); ofstream ofs3("plik3.txt"); while (ofs3&&ifs1.get(znak)) ofs3.put(znak); ifs1.close(); ofs3.close(); return 0; } Dyskusja. Program tworzy pliki plik1.txt i plik2.txt. Do pierwszego z nich wpisuje acuch tekst1, za do drugiego najpierw acuch tekst2, a nastpnie (konkatenacja) acuch tekst3. Zauwamy, e acuch tekst3 jest dopisywany po znaku nowego wiersza, ktrym koczy si acuch tekst2. Obydwa pliki s jawnie zamykane, po czym plik plik2.txt zostaje otwarty do odczytu, a plik plik3.txt (chwilowo pusty) do zapisu. W instrukcji while wykonywane jest kopiowanie zawartoci pliku plik2.txt do pliku plik3.txt; funkcja get() czyta kolejne znaki z ifs1, a funkcja put() wpisuje je do ofs3. W wyraeniu instrukcji while wykonywana jest konwersja strumienia do wartoci prawda (warto rna od zera), jeeli nie zdarzy si bd zapisu. Warto ifs.get(znak) jest referencj do ifs, ktra jest przeksztacana na warto prawda, jeeli nie wystpi bd podczas czytania znaku ze strumienia ifs. Te dwie wartoci s w logicznej koniunkcji, a zatem kopiowanie bdzie biego tak dugo, jak dugo obydwie bd rne od zera. Gdy get() napotka koniec pliku wejciowego, to wystpi bd w operacji czytania, warto ifs.get(znak) zmieni si na fasz (zero) i program wyjdzie z ptli while. Ptl while mona te zapisa w

postaci: while(!ifs1.eof()&&ifs1.get(znak)) ofs3.put(znak); w ktrej funkcja eof() przekazuje warto niezerow (prawda) tylko wtedy, gdy zostanie napotkany koniec pliku.

9.3.1.

Plik jako parametr funkcji main

W rozdziale 5 przedyskutowano komunikacj funkcji main() z otoczeniem, tj. z systemem operacyjnym. Przypomnijmy, e wykonanie kadego programu zaczyna si od wykonania pierwszej instrukcji funkcji main(), a ostatni wykonywan instrukcj jest instrukcja return tej funkcji. Prawie we wszystkich naszych programach funkcja main() wystpowaa z pustym wykazem argumentw; wiadomo jednak, e moe ona mie wiele argumentw, poniewa jej prototyp ma posta: int main(int argc, char* argv[]); gdzie argument argv jest tablic acuchw znakw, a argc jest w chwili uruchomienia programu inicjowany liczb tych acuchw. Poniewa nazwy plikw s acuchami znakw, zatem nic nie stoi na przeszkodzie, aby nazwy te byy argumentami aktualnymi funkcji main(). Ilustruj to pokazane niej dwa przykady. Przykad 9.8. #include <fstream.h> int main(int argc, char* argv[]) { char znak; if(argc != 2) { cerr << "Napisz: czytaj <nazwa-pliku>\n"; return 1; } ifstream ifs(argv[1]); if(!ifs) { cerr << "Nieudane otwarcie pliku do odczytu\n"; return 1; } while(!ifs.eof()) { ifs.get(znak); cout << znak; } return 0; } Dyskusja. Program wywietla zawarto dowolnego pliku na ekranie. Jeeli nazwa skompilowanego pliku adowalnego (po konsolidacji) z naszym programem jest czytaj (lub czytaj.exe pod MS-DOS), to program wywoamy z wiersza rozkazowego systemu operacyjnego piszc: czytaj nazwa-pliku

Zauwamy, e czytanie zawartoci pliku odbywa si w ptli while, a warunkiem zakoczenia jest wystpienie znaku koca pliku, gdy warto przekazywana z funkcji int ios::eof() stanie si rna od zera (prawda). Znaki z otwartego do czytania pliku pobierane s ze strumienia ifs do zmiennej znak za pomoc funkcji get(), a nie operatora >> poniewa ten ostatni pomija znaki spacji. Przykad 9.9. #include <fstream.h> #include <stdlib.h> int main(int argc, char* argv[]) { char znak; if(argc != 3) { cerr << "Niepoprawna liczba parametrow\n"; exit (1); } ifstream ifs(argv[1]); if(!ifs) { cerr << "Nieudane otwarcie pliku do odczytu\n"; exit(1); } ofstream ofs(argv[2], ios::noreplace); if(!ofs) { cerr << "Nieudane otwarcie pliku do zapisu\n"; exit(1); } while(ofs && ifs.get(znak)) ofs.put(znak); return 0; } Dyskusja. Program kopiuje zawarto pliku wejciowego do pliku wyjciowego. Wywoujemy go z trzema parametrami w wierszu rozkazowym; jeeli np. plik wykonalny z programem ma nazw "kopiuj", plik do skopiowania "plik.we", a plik-kopia "plik.wy", to wywoanie ma posta: kopiuj plik.we plik.wy Zauwamy, e plik wyjciowy otwarto w trybie noreplace, a wic nie jest moliwe skopiowanie pliku "plik.we" na ju istniejcy plik "plik.wy".

9.3.2.

Dostp swobodny

W podanych do tej chwili przykadach plikowych operacji wejcia/wyjcia wykorzystywalimy dostp sekwencyjny: np. odczytanie n-tej danej w pliku byo moliwe po odczytaniu (n-1) poprzednich danych. W zastosowaniach plikw, szczeglnie w bazach danych, bardzo przydatna byaby moliwo zajrzenia w dowolne miejsce pliku bez koniecznoci przegldania pliku od pocztku. Jzyk C++ stwarza tak moliwo dziki wprowadzeniu w systemie wejcia/wyjcia dwch wskanikw skojarzonych z plikiem. Pierwszym z tych wskanikw jest wskanik pobierania (ang. get pointer), ktry wskazuje miejsce nastpnej operacji wejciowej. Drugim jest wskanik wstawiania (ang. put pointer), ktry wskazuje miejsce nastpnej operacji wyjciowej. Wskaniki

te s przesuwane automatycznie o jedn pozycj w kierunku koca pliku po kadej operacji wejcia lub wyjcia. Jednake uytkownik moe przej kontrol nad jednym lub obydwoma wskanikami za pomoc zadeklarowanych w klasach istream i ostream (plik nagwkowy iostream.h) funkcji skadowych o prototypach: istream& seekg(streamoff offset, ios::seek_dir origin); streampos tellg(); ostream& seekp(streamoff offset, ios::seek_dir origin); streampos tellp(); Parametr offset podajemy dla okrelenia, o ile bajtw w stosunku do pozycji origin chcemy przesun jeden lub obydwa wskaniki pliku. Typy parametrw s nastpujce: typ streamoff jest wprowadzony deklaracj typedef long int streamoff; typ streampos jest wprowadzony deklaracj typedef long int streampos; typ ios::seek_dir jest zdefiniowanym w klasie ios wyliczeniem enum seek_dir { beg=0, cur=1, end=2 }; Dla obiektw klasy ifstream moemy wywoywa funkcj seekg(), np. ifstream obin("plik.we"); obin.seekg(7, ios::beg); oznacza ustawienie wskanika pobierania o 7 bajtw w prawo od pocztku pliku plik.we. Podobnie dla obiektw klasy ofstream wywoujemy funkcj seekp(), np. ofstream obout("plik.wy"); obout.seekp(-7, ios::cur); oznacza ustawienie wskanika wstawiania o 7 bajtw w lewo od biecej pozycji w pliku plik.wy. Dla obiektw klasy fstream moemy wywoywa obie funkcje, w zalenoci od kontekstu. Funkcje tellg() i tellp() typu streampos (synonim long int) su do odczytu biecego pooenia kadego ze wskanikw. Przykadowe wywoania: long int l1 = obin.tellg(); long int l2 = obout.tellp(); Przykad 9.10. #include <fstream.h> int main() { char znak; ifstream ifs("plik.we"); if(!ifs) { cerr<<"Nieudane otwarcie pliku do odczytu\n"; return 1; } ifs.seekg( 0, ios::beg); streampos poz1 = ifs.tellg(); cout << poz1 << endl; do {

ifs.get(znak); if(znak != EOF) { cout << znak << ' '; poz1 = ifs.tellg(); cout << poz1 << ' '; } } while(!ifs.eof()); cout << endl; ifs.seekg(0, ios::end); int i = ifs.tellg() ; cout << i << endl; ifs.close(); return 0; } Dyskusja. Po otwarciu pliku plik.we do odczytu, ustawiamy wskanik pobierania na pierwszy bajt tego pliku, a jego pooenie zapisujemy w zmiennej poz1. W ptli do przesuwamy wskanik pobrania instrukcj ifs.get(znak); notujc w zmiennej poz1 jego kolejne pooenia. Ptla koczy si testem na warto funkcji skadowej eof(); warto ta przed osigniciem koca pliku wynosi cay czas zero; na kocu pliku funkcja eof() przekazuje warto niezerow. Kocowa sekwencja instrukcji suy do pokazania, e wskanik pobrania po odczytaniu zawartoci ustawi si na kocu pliku. Jeeli w utworzonym wczeniej pliku plik.we umiecimy cig znakw "abcdefghij", to wygld ekranu po wykonaniu programu bdzie nastpujcy (liczby po literach s kolejnymi odlegociami w bajtach wskanika pobrania od pocztku pliku): 0 a 1 b 2 c 3 d 4 e 5 f 6 g 7 h 8 i 9 j 10 10 Nastpny program ilustruje dostp swobodny na przykadzie pliku klasy fstream, otwieranego w trybie nocreate. Oznacza to, e prba otwarcia do zapisu pliku nieistniejcego nie spowoduje jego utworzenia, lecz spowoduje przerwanie wykonania programu. Przykad 9.11. #include <fstream.h> int main() { char tekst[] = "Tekst w pliku"; fstream plik; char* nazwa; int i = 0; char znak; cout << "Podaj nazwe pliku: "; cin >> nazwa; //Otwieramy plik do odczytu i zapisu. plik.open(nazwa,ios::in|ios::out|ios::nocreate); if (!plik) { cerr << "\nNieudane otwarcie pliku " << nazwa << endl;

return 1; } cout << "Tekst wejsciowy: " << tekst << endl; //Teraz zapisujemy tekst do pliku. while (znak = tekst[i++]) plik.put (znak); //A teraz wypisujemy tekst od konca. cout << "Tekst odwrotny: "; plik.seekg (-1, ios::end); long int l; do { if ((znak = plik.get())!= EOF) cout << znak; plik.seekg (-2, ios::cur); l = plik.tellg(); } while (l != -1); cout << endl; plik.close(); return 0; } Dyskusja. Program najpierw zastpuje zawarto istniejcego pliku cigiem znakw "Tekst w pliku" (instrukcja while). Nastpnie ustawia wskanik pobrania na ostatnim znaku w pliku (plik.seekg (-1, ios::end);) i odczytuje t now zawarto w odwrotnym kierunku. Jeeli istniejcym plikiem by plik o nazwie "plik1.we", to wygld ekranu bdzie nastpujcy: Podaj nazwe pliku: plik1.we Tekst wejsciowy: Tekst w pliku Tekst odwrotny: ukilp w tskeT

10.Obsuga wyjtkw
W jzyku potocznym przyjo si mwi, e wyjtek potwierdza regu. O ile maksyma ta raczej nie ma odniesienia do jzyka programowania, o tyle moe si odnosi do osb piszcych programy oraz do ograniczonych zasobw systemu. W oglnoci wyjtkiem (ang. exception) nazywamy zdarzenie, spowodowane przez anormaln sytuacj, wymagajc przywrcenia normalnego stanu. Dobrze zaprojektowany system obsugi wyjtkw powoduje wwczas zawieszenie normalnego wykonywania programu i przekazanie sterowania do odpowiedniej procedury obsugi wyjtku (ang. exception handler). Wyjtki w rodowisku programowym mog pochodzi z rnych rde i wystpowa na rnych poziomach: sprztowym, programu i systemu. Na najniszym poziomie, nazwijmy go sprztowym, mog wystpowa rne tego typu zdarzenia, w tym: bdy parzystoci pamici, generujce niemaskowalne przerwanie niskiego poziomu; niesprawno urzdzenia zewntrznego, np. wyczenie drukarki lub brak papieru, otwarte drzwiczki napdu dyskw, brak dyskietki w napdzie; uszkodzenie urzdzenia zewntrznego. Bdy te s asynchroniczne wzgldem programu i nie maj zwizku z tym, co akurat wykonuje program, a wic nie bd przechwytywane przez mechanizm obsugi wyjtkw. Zdarzenia wymagajce reakcji na poziomie programu. Wystpujce tutaj bdy mog mie rne przyczyny: bdny format danych wprowadzanych przez uytkownika, prba usunicia nieistniejcego pliku, prba obliczenia logarytmu lub pierwiastka z liczby ujemnej, prba dzielenia przez zero, prba pobrania elementu z pustego stosu, prba wywoania nieistniejcej funkcji wirtualnej, etc. Na poziomie systemowym do najczciej wystpujcych bdw moemy zaliczy brak pamici przy prbie utworzenia nowego obiektu, albo brak miejsca na dysku. Przy tradycyjnym podejciu do programowania reakcje na bdy mona pogrupowa w nastpujce kategorie. 1. Zaniechanie wykonania programu i wysanie komunikatu o bdzie. 2. Przekazanie do programu wartoci reprezentujcej bd. 3. Zignorowanie bdu. 4. Przekazanie do programu poprawnej wartoci i przesanie informacji o bdzie przez specjaln zmienn. 5. Wywoanie procedury obsugi bdu, napisanej przez programist. Kade z tych rozwiza ma trudne do zaakceptowania wady. Pierwsze z nich moe by stosowalne w takich programach jak edytory, kompilatory, gry, etc. Z reguy wystarcza wtedy wyczyszczenie pamici, zwolnienie wykorzystywanych zasobw systemu (np. zamknicie plikw), wydruk komunikatu i wyjcie z programu. Jednak jest ono nie do przyjcia w takich programach interakcyjnych, w ktrych program reagujcy na najmniejsze potknicie operatora zakoczeniem dziaania mgby go pozbawi wynikw dugotrwaej pracy. Drugie rozwizanie jest na og nieatwe w implementacji, poniewa w wielu przypadkach trudno jest odrni warto poprawn od bdnej. atwym przypadkiem jest odrnienie bdnego wskanika. Np. funkcja czytajca dane z pliku moe albo zwrci wskanik do nastpnej pozycji,

albo wskanik zerowy; podobnie funkcja typu char* moe zwraca pusty acuch dla sygnalizacji niepowodzenia. Nie ma natomiast sposobu okrelenia bdnego kodu dla funkcji typu int, poniewa kada warto zwracana moe by uwaana za poprawn. Zreszt nawet w przypadkach, gdy takie rozwizanie jest dopuszczalne, moe si okaza nieopacalne, poniewa sprawdzanie poprawnoci wyniku przy kadym wywoaniu funkcji pociga za sob due narzuty czasowe i pamiciowe. Trzecie rozwizanie jest trudne w implementacji i na og niebezpieczne. Nie jest atwym brak reakcji na bd, szczeglnie w odniesieniu do funkcji, ktra zwraca warto inn ni void, czy void*. Zdarzaj si take sytuacje, w ktrych brak reakcji jest niedopuszczalny, np. gdy konstruktor kopiujcy ustali, e nie ma do pamici dla utworzenia kopii obiektu (w tym przypadku nie bdzie to, rzecz jasna, bd uytkownika). Rozwizanie czwarte jest stosowane w standardowych bibliotekach jzyka C. Wiele funkcji z tych bibliotek (np. funkcje matematyczne) sygnalizuje bd, ustawiajc warto EDOM (bd dziedziny) lub ERANGE (bd zakresu) w zmiennej errno, np. double sqrt(double x) { if(x < 0) { errno = EDOM; return 0; } //... } Jest to mechanizm niezbyt pomocny dla uytkownika. Komunikat o bdzie zawiera w tym przypadku jedynie typ bdu, a nie nazw bdnie wywoanej funkcji. Co wicej, jeli zdarz si dwa kolejne bdy, to drugi moe przysoni komunikat o pierwszym. Oczywicie takim sytuacjom mona zapobiec, ale znowu kosztem sporego narzutu pamiciowego i czasowego. Rozwizanie pite spotyka si w dwch wariantach. Uywane w programie klasy mona wyposay w domylne funkcje obsugi bdw. Zwykle s to funkcje, ktre drukuj komunikat o bdzie i powoduj zakoczenie programu, tak jak to czynilimy w wielu przykadowych programach. Moliwe jest take zadeklarowanie funkcji, ktra np. zapisuje komunikat o bdzie do pliku i pozwala na kontynuacj wykonywania programu. Jednak takie rozwizanie nie bdzie mie cech oglnoci, poniewa w zasadzie kad klas naleaoby wyposay w jej wasny mechanizm obsugi bdw.

10.1. Model obsugi wyjtkw w jzyku C++


Przeprowadzona wyej krytyka tradycyjnych sposobw reakcji na wyjtki sugeruje, e optymalnym rozwizaniem byoby rozszerzenie skadni jzyka o nastpujce konstrukcje: Przekazanie od procedury obsugi wyjtku do funkcji, wywoujcej t procedur, informacji o rodzaju bdu oraz ewentualnych dodatkowych informacji. Generalizacj wyjtkw w postaci hierachii klas (np. wyjtki bd dziedziny i bd zakresu s szczeglnymi przypadkami wyjtku bd matematyczny). Doczenie do funkcji wywoujcej niezalenego kodu obsugi dla poszczeglnych bdw. Automatyczne przekazanie sterowania do odpowiedniego fragmentu kodu obsugi bdu w przypadku zgoszenia wyjtku. Zastosowany w jzyku C++ mechanizm obsugi wyjtkw spenia powysze postulaty. Zaakceptowany przez komitety ANSI X3J16/ISO WG-21 w roku 1990 sta si po raz pierwszy dostpny we wzorcowym kompilatorze AT&T wersji 3.0 we wrzeniu 1991, a pierwsze implementacje przemysowe firm DEC i IBM weszy na rynek na pocztku 1992. Dane te przytaczamy nie bez powodu: jzyk, ktry zapewnia skuteczn obsug wyjtkw, moe suy do budowy systemw odpornych na bdy (ang. fault-tolerant systems), a wic ma szans sta si standardem przemysowym. W jzyku C++ dla obsugi wyjtkw zastosowano model z terminacj. Oznacza to, e procesy

obsugi wyjtkw przebiegaj w sekwencji: zgoszenie wyjtku przez funkcj wyjcie z jej bloku przechwycenie przez procedur obsugi obsuga zakoczenie programu (lub przejcie do nastpnej instrukcji w bloku funkcji zawierajcej procedur obsugi). Nie jest to jedyne moliwe rozwizanie: wielokrotnie w innych jzykach prbowano zastosowa bardziej oglny model ze wznowieniem. Jest to bardzo atrakcyjna alternatywa: zakada ona, e procedura obsugi wyjtku powinna by tak zaprojektowana, aby moga da wznowienia programu od punktu, w ktrym zosta zgoszony wyjtek. Model taki mgby by szczeglnie obiecujcy dla unifikacji obsugi wyjtkw na poziomie programu z obsug wyjtkw na poziomie systemu (wyczerpanie zasobw). Jednak wieloletnia praktyka pokazaa, e model z terminacj jest prostszy, bardziej przejrzysty oraz taszy prowadzi do atwiejszego zarzdzania systemami.

10.1.1.

Deklaracje wyjtkw

Mechanizm obsugi wyjtkw jzyka C++ wprowadza trzy nowe sowa kluczowe. Pierwsze z nich, try oznacza blok kodu, w ktrym mog wystpi sytuacje wyjtkowe. Ich zgoszenie nastpuje za pomoc instrukcji throw, a s one obsugiwane w blokach poprzedzonych sowem kluczowym catch. Bloki catch, ktre musz wystpowa bezporednio za blokiem try, mog wystpowa wielokrotnie. Cig blokw catch, wystpujcych bezporednio za blokiem try, zawiera procedury obsugi wyjtkw. Konstrukcj t zapisuje si w postaci: try{ }catch() { } ... catch() { } Wyjtki mog by zgaszane wycznie wewntrz bloku try, ktry, podobnie jak bloki instrukcji zoonych lub funkcji, moe zawiera deklaracje, definicje i instrukcje. Najprostsza skadniowo instrukcja throw ma posta: throw; i oznacza ponowne zgoszenie wyjtku aktualnie obsugiwanego w bloku catch. Wywoanie throw bez parametru w chwili gdy aden wyjtek nie jest obsugiwany powoduje (domylnie) zakoczenie programu. Instrukcja throw najczciej wystpuje z parametrem: throw wrn; gdzie wrn moe by dowolnym wyraeniem traktowanym przez kompilator tak, jak wyraenia bdce argumentem wywoania funkcji lub instrukcji return, np. throw 10; throw "abc"; throw obiekt; przy czym obiekt jest wystpieniem wczeniej zdefiniowanej klasy. Typ obiektu bdcego wynikiem obliczenia wyraenia wrn okrela rodzaj wyjtku, za sam obiekt jest przekazywany do tego bloku catch, ktry wystpuje za ostatnio napotkanym blokiem try. Jeeli zgoszony wyjtek nie jest obsugiwany przez dan procedur w bloku catch, to jest on przekazywany do nastpnej. Jeeli dla danego wyjtku nie zostaa znaleziona procedura jego obsugi, to wykonanie programu zostanie zakoczone. W procesie zakoczenia programu wywoywana jest wwczas funkcja terminate(), ktra z kolei wywouje funkcj abort(). Przykad 10.1. #include <iostream.h> #include <excpt.h> int main() { char znak = '\0'; while (znak != '*') { try

{ cout << "Znak '*'- koniec. " << "Podaj dowolny znak: "; cin >> znak; switch (znak) { case 'a': throw 1; case 'b': throw "tekst"; case 'c': throw 2.0; default : throw 'x'; } //Koniec switch } // Koniec try catch(int) { cout << "Przypadek 1\n"; } catch(char*) { cout << "Przypadek 2\n"; } catch(double) { cout << "Przypadek 3\n"; } catch(...) { cout << "Wymagana kolejna procedura catch! "; return 1; } // Koniec catch(...) } // Koniec while return 0; } Przykadowy wydruk z programu ma posta: Znak '*' - koniec. Podaj dowolny znak: a Przypadek 1 Znak '*' - koniec. Podaj dowolny znak: c Przypadek 3 Znak '*' - koniec. Podaj dowolny znak: * Wymagana kolejna procedura catch! Dyskusja. W powyszym programie wyjtki s zgaszane w bloku try, umieszczonym w funkcji main(). Plik nagwkowy <excpt.h> zawiera niezbdne deklaracje, pozwalajce na dostp do mechanizmu obsugi wyjtkw. Procedury obsugi przechwytuj wyjtki typu int, char* i double. Blok oznaczony catch(...) {} obsuguje wszystkie nieobsuone wyjtki dowolnego typu. W sekwencji try{ }catch() { } ... catch() { } blok catch(...) { }, jeeli wystpuje, musi by umieszczony jako ostatni. Sekwencj try-catch mona przenie do oddzielnej funkcji, wywoywanej nastpnie z bloku funkcji main(), jak pokazano w kolejnym przykadzie. Przykad 10.2. #include <iostream.h> #include <excpt.h> void fun() { char znak = '\0'; while (znak != '*') {

try { cout << "Znak '*'- koniec. "; cout << "Podaj dowolny znak: "; cin >> znak; switch (znak) { case 'a': throw 1; case 'b': throw "tekst"; case 'c': throw 2.0; default : throw 'x'; } //Koniec switch } // Koniec try catch(int) { cout << "Przypadek 1\n"; } catch(char*) { cout << "Przypadek 2\n"; } catch(double) { cout << "Przypadek 3\n"; } catch(...) { cout << "Wymagana kolejna procedura catch!\n"; } } // Koniec while }// Koniec fun int main() { fun(); return 0; } Jeeli wprowadzimy t sam sekwencj znakw co poprzednio, to otrzymamy identyczny obraz interakcji uytkownika z programem.

10.2.

Wyjtek jako obiekt

W praktyce programy mog zawiera wiele moliwych bdw w fazie wykonania. Bdy takie mog by odwzorowane na wyjtki o rozrnialnych nazwach. Ponadto wskazane jest, aby w przypadku wystpienia bdu zgoszony wyjtek zawiera maksimum informacji o przyczynie bdu. Jeeli typ zgaszanego instrukcj throw wyjtku jest typem wbudowanym, to moliwoci s stosunkowo niewielkie. Rozwizaniem jest zdefiniowanie klasy wyjtkw z odpowiednim publicznym interfejsem i traktowanie wyjtku jako obiektu. Ilustruje to poniszy przykad. Przykad 10.3. #include <iostream.h> class Liczba { public: class Zakres { }; Liczba(int); }; Liczba::Liczba(int i) { if (i > 10) throw Zakres(); } int main() { int x; char znak; try { cout << "Podaj liczbe typu int: "; cin >> x; Liczba num(x);

} //Koniec try catch (Liczba::Zakres) { cout << endl << "Przechwycony wyjatek!\n"; }; // Koniec catch cout << "Kontynuacja programu.\n" << "Wcisnij klawisz litery lub cyfry: "; cin >> znak; return 0; } Przykadowa interakcja z uytkownikiem: Podaj liczbe typu int: 19 Przechwycony wyjatek! Kontynuacja programu. Wcisnij klawisz litery lub cyfry: a Moliwo traktowania wyjtku jako obiektu typu zdefiniowanego przez uytkownika prowadzi do koncepcji hierarchii wyjtkw, w ktrej pewne wyjtki mog by typami pochodnymi od wyjtkw oglniejszych. Np. dla biblioteki matematycznej mona zdefiniowa klas bazow BdMat i klasy od niej pochodne Nadmiar, Niedomiar, DzielZero. W takich przypadkach istotna jest kolejno, w jakiej wystpuj bloki catch. Wiadomo, e prby przechwycenia wyjtkw zgaszanych z bloku try odbywaj si w takiej kolejnoci, w jakiej wystpuj kolejne bloki catch. Zatem procedur obsugi dla klasy bazowej naley umieszcza jako ostatni (albo przedostatni, jeeli wystpuje catch(...){}); w przeciwnym przypadku procedura dla klasy pochodnej nie zostaaby nigdy wywoana. Zwrmy jeszcze uwag na nastpujcy moment. Jeeli wemiemy cig deklaracji: class WyjOglny { public: virtual void ff() { /* instrukcje */ } }; class WyjSzczeglny: public WyjOgolny { public: void ff() { /* instrukcje */ } }; void funkcja() { try { // Wywoanie funkcji, ktra zgasza // wyjtek typu WyjSzczeglny } catch(WyjOglny wo) { wo.ff(); } } to w tym przypadku zostanie wykonana funkcja WyjOglny::ff(), pomimo e zgoszony wyjtek by typu WyjSzczeglny, a funkcja ff() jest funkcj wirtualn. Wynika to std, e obiekt typu WyjSzczeglny jest przekazywany przez warto (za pomoc konstruktora kopiujcego klasy WyjSzczeglny) jako parametr aktualny procedury catch(). Poniewa

parametrem formalnym jest obiekt typu WyjOglny, to obiekt przesany z bloku try zostanie obcity na wymiar wo. W obiekcie wo bdzie wic dostpny jedynie wskanik do funkcji wirtualnej ff() klasy WyjOglny. Mona temu zapobiec, stosujc wskaniki lub referencje, np. catch(WyjOglny& wo) { wo.ff(); }

10.3.

Sygnalizacja wyjtkw w deklaracji funkcji

Konstrukcje throw-try-catch zwykle wystpuj w bloku oddzielnej funkcji, wywoywanej w ciele innej funkcji. Interakcj takiej funkcji z innymi funkcjami mona uczyni bardziej czyteln, podajc jawnie w jej nagwku moliwe do zgoszenia wyjtki, np. void ff(int i) throw(A, B); Powysza deklaracja mwi, e funkcja ff moe zgosi wyjtki tylko dwch podanych typw. Taki sposb deklarowania stosuje si rwnie, gdy podajemy definicj funkcji, a nie tylko jej prototyp. W obu przypadkach w bloku funkcji mog (ale nie musz) wystpi odpowiednie instrukcje throw lub wywoania funkcji generujcych wyjtki z bloku try. Gdyby z bloku tej funkcji zosta zgoszony wyjtek rny od A lub B, to funkcja nie bdzie w stanie obsuy wyjtku samodzielnie, ani te przekaza go do funkcji woajcej. Po wystpieniu takiego nieoczekiwanego wyjtku zostanie automatycznie wywoana funkcja void unexpected(). Funkcja ta wywouje opisan uprzednio funkcj terminate(), ktra z kolei wywouje abort() i koczy program. Jednak wywoaniem domylnym dla funkcji unexpected() jest wywoanie funkcji, zdefiniowanej przez uytkownika, a rejestrowanej jako argument funkcji set_unexpected(). Funkcja ta jest wprowadzona w pliku nagwkowym <except.h> deklaracjami: typedef void (*PFV)(); PFV set_unexpected(PFV); Stwarza ona uytkownikowi pewn moliwo wpywania na obsug wyjtku nieoczekiwanego. Jak wida z deklaracji, wskanik PFV do bezargumentowej funkcji typu void jest typem zwracanym przez funkcj set_unexpected(), tj. typem funkcji, ktra bya parametrem aktualnym w ostatnim wywoaniu funkcji set_unexpected(). W deklaracji (definicji) funkcji mona jej zakaza zgaszania wyjtkw, dodajc w jej nagwku throw(), np. void ff(int i) throw(); Jeeli, mimo zakazu, powysza funkcja zgosi wyjtek, to musi on zosta przechwycony i obsuony w jej bloku. W przeciwnym przypadku zostanie wywoana funkcja unexpected() i dalszy bieg zdarze bdzie analogiczny, jak w poprzednim przypadku. Przykad 10.4. #include <iostream.h> #include <excpt.h> class Nowa { }; Nowa obiekt;

void f3(void) throw (Nowa) { cout << "Wywolana f3()" << endl; throw(obiekt); } void f2(void) throw() { try { cout << "Wywolana f2()" << endl; f3(); } catch ( ... ) { cout << "Przechwycony wyjatek w f2()!" << endl; } } int main() { try { f2(); return 0; } catch ( ... ) { cout << "Potrzebna kolejna procedura catch! "; return 1; } } Wydruk z programu bdzie mia posta: Wywolana f2() Wywolana f3() Przechwycony wyjatek w f2()! Dyskusja. W przykadzie pokazano wpyw specyfikacji wyjtkw w nagwku funkcji na dziaanie programu. Zdefiniowano w nim klas wyjtkw Nowa i jej wystpienie o nazwie obiekt. Prototyp funkcji f3() void f3(void) throw (Nowa); mwi, e jedynymi wyjtkami, ktre moe ona zgasza, s obiekty klasy Nowa. Natomiast funkcja f2(), co wynika z postaci jej prototypu void f2(void) throw(); nie powinna zgasza adnych wyjtkw. Jednak z jej bloku jest wywoywana funkcja f3(), ktra moe i zgasza wyjtek. Tak wic wykonanie programu po wywoaniu f2() z bloku main() nie koczy si wykonaniem instrukcji return 0; lecz return 1; po przechwyceniu wyjtku zgoszonego z bloku funkcji f3().

10.4.

Propagacja wyjtkw

Funkcje, wywoywane w bloku try, mog rwnie zawiera bloki try; pozwala to tworzy hierarchie obsugi wyjtkw. Jeeli funkcja zgaszajca wyjtek jest wywoywana z bloku innej, nadrzdnej funkcji, to proces obsugi wyjtku moe przebiega w sposb, zilustrowany rysunkiem 10-1. Schemat wywoa

jest tutaj nastpujcy: z bloku funkcji A zostaa wywoana funkcja B, z jej bloku zostaa wywoana funkcja C, a z jej bloku funkcja D, ktra zgosia wyjtek.

Blok A Blok B Blok C Blok D

Zgoszenie wyjtku

Rys. 10-1 Obsuga wyjtku przy zagniedonych wywoaniach funkcji W chwili zgoszenia wyjtku zamykany jest blok funkcji D, to znaczy usuwany jest ze stosu jego rekord aktywacyjny i usuwane s wszystkie zmienne lokalne (automatyczne) utworzone w tym bloku. Jeeli w bloku D istnieje odpowiednia procedura obsugi zgoszonego wyjtku, to sterowanie zostanie przekazane do tej procedury. Zamy, e tak nie jest, i e odpowiedni blok catch znajduje si w funkcji nadrzdnej B. Wobec tego, po zakoczeniu bloku D, zostan zakoczone w taki sam sposb bloki C i B, po czym sterowanie zostanie przekazane do procedury obsugi wyjtku z bloku B. Po zakoczeniu obsugi zostanie wznowione wykonanie bloku funkcji A od nastpnej po wywoaniu funkcji B instrukcji. Gdyby w adnym bloku z acucha wywoa nie zosta znaleziony odpowiedni blok catch, to zostayby zakoczone wszystkie bloki i sterowanie zostaoby przekazane do funkcji terminate(). Standardowo funkcja ta powoduje zakoczenie programu. Dokadniej mwic, funkcja void terminate() wykonuje ostatni funkcj, przekazan jako parametr aktualny (wskanik) do funkcji set_terminate(), wprowadzonej deklaracjami: typedef void (*PFV) (); PFV set_terminate(PFV); Jak wida z deklaracji, wskanik PFV do bezargumentowej funkcji typu void jest i argumentem, i typem zwracanym przez funkcj set_terminate(). Podany niej przykad ilustruje opisany mechanizm. Przykad 10.5.

//Propagacja wyjatkow #include <iostream.h> class Nowa { };//Deklaracja wyjatku Nowa obiekt; void B() throw(); void C() throw(Nowa); void D() throw (Nowa); void A() throw() { try { cout << "Blok try funkcji A()\n"; B(); } catch(...) { cout << "catch() w A()"; } cout << "Kontynuacja A()\n"; } void B() throw() { try { cout << "Blok try funkcji B()\n"; C(); } catch(Nowa) { cout << "Przechwycony wyjatek z D()!\n"; } cout << "Zamykany blok B()\n"; } void C() throw(Nowa) { try { cout << "Blok try funkcji C()\n"; D(); } catch(int) { cout << "catch w C()\n"; throw; } cout << "Kontynuacja C()\n"; } void D() throw (Nowa) { try { cout << "Blok try funkcji D()\n"; throw(obiekt); } catch(int) { cout << "catch w D()\n"; } cout << "Kontynuacja D()\n"; } int main() { try { cout << "Wywolana A()\n"; A(); cout << "Po A()\n"; return 0; } catch(...) { cout<<"Potrzebny kolejny blok catch\n"; } cout << "Kontynuacja main()\n"; return 0; } Wydruk z programu ma posta:

Blok try funkcji A() Blok try funkcji B() Blok try funkcji C() Blok try funkcji D() Przechwycony wyjatek z D()! Zamykany blok B() Kontynuacja A() Po A()

10.5.

Wyjtki i zasoby systemowe

Moliwo wystpienia wyjtkw wymaga starannej uwagi programisty, poniewa burzy ona liniowy przebieg wykonania programu. Jeeli np. funkcja rezerwuje pewne zasoby (otwiera plik, przydziela pami z kopca, itp.), to powinna je w odpowiedni sposb zwolni, gdy w przeciwnym przypadku moe to spowodowa nieoczekiwany przebieg wykonania programu. Zazwyczaj funkcja zwalnia zasoby przy wyjciu ze swojego bloku, tu przed przekazaniem sterowania do funkcji woajcej. Jednake odnosi si to jedynie do zmiennych lokalnych; jeeli funkcja operuje na zmiennych globalnych, to nie mamy takiej gwarancji. Wemy dla przykadu sekwencj instrukcji: ifs.open("we.doc"); fun(ifs); ifs.close(); Jeeli ifs jest zmienn globaln (obiektem) klasy ifstream, to funkcja fun (lub funkcja przez ni wywoywana) moe zgosi wyjtek i instrukcja ifs.close(); nie zostanie nigdy wykonana! Opanowanie takiej sytuacji jest technicznie moliwe przez przechwycenie dowolnego z moliwych wyjtkw, zamknicie pliku i ponowne zgoszenie przechwyconego wyjtku: ifs.open("we.doc"); try { fun(ifs); } catch(...) { ifs.close(); throw; } ifs.close(); Jednak stosowanie takiej strategii byoby nadzwyczaj kopotliwe. Zamiast takiego podejcia naley wykorzysta fakt, e w C++ przy wyjciu z funkcji nastpuje automatyczne wywoanie destruktorw dla wszystkich obiektw lokalnych. Dotyczy to rwnie tych obiektw, dla ktrych zostay zarezerwowane zasoby w funkcjach, wywoywanych przez funkcj fun. W pokazanym wyej przypadku wystarczy zadeklarowa ifs jako obiekt lokalny klasy ifstream: ifstream ifs("we.doc"); fun(ifs); poniewa destruktor klasy bibliotecznej ifstream automatycznie zamknie plik we.doc w momencie, gdy wykonanie dojdzie do koca otaczajcego bloku, lub gdy wyjtek jest obsugiwany w bloku zewntrznym. Powysze uwagi odnosz si w rwnym stopniu do alokacji pamici.

Przykad 10.6. #include <iostream.h> #include <fstream.h> class GetMemory { public: int* wskmem; GetMemory(int m) { wskmem = new int[m];} ~GetMemory() { delete[] wskmem;} }; class MojaKlasa { public: class Rozmiar { }; MojaKlasa(const char* filename, int sizemem); }; MojaKlasa::MojaKlasa(const char* filename, int sizemem) { ofstream os(filename); if (sizemem < 0 || 30 < sizemem) throw Rozmiar(); GetMemory obiekt1(sizemem); cout << "Przydzielona zadana pamiec " << sizemem << " bajtow " << "i otwarty plik\n"; } int main() { int x; try { cout << "Podaj rozmiar pamieci w bajtach: "; cin >> x; MojaKlasa obiekt2("zasob.txt",x); } catch ( MojaKlasa::Rozmiar ) { cout << " Niepoprawny rozmiar zadanej pamieci \n"; } return 0; } Dyskusja. Dwukrotne uruchomienie programu moe da nastpujce wydruki: Podaj rozmiar pamieci w bajtach: 25 Przydzielona pamiec 25 bajtow i otwarty plik Podaj rozmiar pamieci w bajtach: -100 Niepoprawny rozmiar zadanej pamieci W przykadzie pokazano przechwytywanie bdw powstajcych w konstruktorze obiektu klasy

MojaKlasa. Jeeli z klawiatury podamy rozmiar alokowanej pamici w granicach od 0 do 30, to wykonanie programu przebiega liniowo: program przydzieli zadan pami i utworzy (otworzy i zamknie) plik zasob.txt o zerowej zawartoci. W drugim przypadku mamy nastpujc sekwencj czynnoci: utworzenie obiektu os i otwarcie pliku zasob.txt zgoszenie wyjtku typu MojaKlasa::Rozmiar zamknicie pliku zasob.txt przez niejawnie wywoany destruktor klasy ofstream (destrukcja obiektu os) przechwycenie wyjtku przez blok catch obsug wyjtku zakoczenie programu.

10.6. Naduywanie wyjtkw


Wyjtki powinny by wyjtkowe. To oczywiste stwierdzenie nie zawsze jest respektowane: programici do czsto ulegaj pokusie wykorzystania mechanizmu wyjtkw do przekazywania sterowania z jednego punktu programu do innego. Takie postpowanie moe w najlepszym razie wiadczy o zym stylu programowania. Wyjtki powinny by zarezerwowane dla takich przypadkw, ktre nie mog si zdarzy w normalnym przebiegu oblicze i ktrych wystpienie tworzy sytuacj, z ktrej nie ma wyjcia w aktualnym zasigu. Dobrym przykadem koniecznoci uycia mechanizmu wyjtkw jest wyczerpanie si pamici. Nikt nie moe z gry przewidzie kiedy zabraknie pamici, a w punkcie detekcji tego faktu rzadko jest moliwe zrobi co wicej. Przeciwiestwem dla tego przypadku moe by wykrycie koca pliku, z ktrego wanie czytamy dane. Wiadomo, e w kadym pliku dojdziemy do jego koca, a zatem kod dla czytania z pliku musi by na to przygotowany. To samo dotyczy pobierania danych z kolejki: przed kad prb usunicia elementu naley si upewni, czy kolejka nie jest pusta. Podany niej przykad ilustruje naduywanie mechanizmu wyjtkw do przkazywania sterowania w sytuacjach, w ktrych cakowicie wystarczajce byoby warunkowe wywoywanie funkcji. Przykad 10.7. #include <iostream.h> #include <string.h> void wykonanie1() { cout << "Wykonanie a\n"; } void wykonanie2() { cout << "Wykonanie b\n"; } int main() { char rozkaz[80]; cout << "Napisz znak lub sekwencje znakow.\n"; cout << "Zacznij od znakow 'a' i' 'b': "; while(cin >> rozkaz) { try { if (strcmp(rozkaz, "a") == 0) wykonanie1(); else if(strcmp(rozkaz, "b") == 0) wykonanie2(); else cout << "Nieznany rozkaz: " << rozkaz << endl; } // Koniec try catch (char* komunikat) { cout << komunikat << endl;

} // Koniec catch catch(...) { cout << "Koniec\n"; } } // Koniec while return 0; } Wydruk dla przykadowego wykonania programu: Napisz znak lub sekwencje znakow. Zacznij od znakow 'a' i 'b': a Wykonanie a b Wykonanie b abc Nieznany rozkaz: abc

11.Dynamiczna i statyczna kontrola typw


Dynamiczna kontrola typw, znana pod akronimem RTTI (ang. Run-Time Type Information), zostaa wprowadzona do standardu jzyka w roku 1993. W skad mechanizmu RTTI wchodz: Operator dynamic_cast (sowo kluczowe), ktry suy do otrzymania wskanika do obiektu klasy pochodnej przy danym wskaniku do klasy bazowej tego obiektu. Operator dynamic_cast daje ten wskanik jedynie wtedy, gdy wskazywany obiekt jest rzeczywicie wystpieniem podanej klasy pochodnej; w przeciwnym przypadku przekazuje 0. Operator typeid (sowo kluczowe), ktry pozwala zidentyfikowa dokadny typ obiektu na podstawie wskanika do jego klasy bazowej. Klas biblioteczn type_info, dostarczajc dalszych informacji o typie dla fazy wykonania programu. Deklaracja tej klasy znajduje si w pliku nagwkowym <typeinfo.h>. Mechanizm RTTI uzupeniaj trzy dalsze operatory: static_cast, const_cast i reinterpret_cast, suce do konwersji statycznej. Wprowadzenie dynamicznej kontroli typw wyniko z naturalnej potrzeby. Podstawowym sposobem kontroli typw jest w jzyku C++ kontrola statyczna (silna), wykonywana w fazie kompilacji. Jest to mechanizm bardzo efektywny, poniewa nie wprowadza adnych narzutw czasowych w fazie wykonania. Np. kontrola statyczna wywoania funkcji skadowej klasy obejmuje jej peny typ: typy argumentw i typ zwracany. Silna kontrola typw ma rwnie miejsce w przypadku funkcji przecionych i funkcji z argumentami domylnymi. Jedynym odstpstwem (nie zalecanym do stosowania) jest moliwo deklarowania funkcji z wielokropkiem ('...') podanym zamiast typu argumentu. Silna typizacja statyczna jest korzystna z wielu wzgldw. Jeeli tworzymy obiekt konkretnego typu, np. double, char*, czy Test, to prba uycia tego obiektu w sposb niezgodny z jego typem oznacza naruszenie systemu typw. Jzyk, w ktrym takie naruszenie nie moe si nigdy zdarzy, jest jzykiem o typizacji silnej. Przykadem takiego jzyka jest Pascal. Silna typizacja nie moga by wbudowana w jzyk C++ ze wzgldu na jego cechy, odziedziczone z jzyka C. Konstrukcje programowe takie jak unie, konwersje i tablice nie pozwalaj na wykrycie kadego naruszenia systemu typw w fazie kompilacji. Postpiono wobec tego inaczej. Kade jawne naruszenie systemu typw generuje komunikat o bdzie i powoduje zaniechanie kompilacji. Kade niejawne naruszenie (lub nawet podejrzenie o naruszenie) systemu typw powoduje wysanie ostrzeenia przez kompilator. Po przejciu przez faz kompilacji wykonywana jest nastpna kontrola typw w fazie konsolidacji (czenia). Np. dla wywoa funkcji oznacza to, e program przejdzie konsolidacj tylko wtedy, gdy kada wywoana funkcja ma swoj definicj i typy argumentw podane w jej deklaracji takie same (lub zgodne) jak typy podane w definicji. Jest to szczeglnie wane w programach wieloplikowych, gdy konsolidator musi sprawdzi na zgodno typy funkcji we wszystkich jednostkach kompilacji (ang. type-safe linkage). Zamiast wykorzystywa niepewne cechy pochodzce z jzyka C, proponuje si uytkownikowi korzystanie z cech, podlegajcych cisej kontroli typw. Przykadami mog by klasy pochodne, bezpieczne tablice, etc. Mimo i typizacja statyczna z towarzyszc jej bezpieczn konsolidacj jest bardzo efektywna, pozbawia ona jzyk gitkoci, waciwej jzykom z typizacj dynamiczn, takim jak np. Smalltalk i Eiffel. W jzykach tych wizanie obiektu z konkretnym typem i kontrola typw s odkadane do fazy wykonania. Pozwala to m.in. na zmian typu obiektu w rnych momentach fazy wykonania. W jzyku C++ nie dopuszcza si takich przecze. Nawet w przypadku klas z funkcjami wirtualnymi kompilator i konsolidator gwarantuj jednoznaczn odpowiednio pomidzy

obiektami, a wywoanymi dla nich funkcjami, generujc dla kadej takiej klasy tablic funkcji wirtualnych. Klasy z funkcjami wirtualnymi s czsto nazywane klasami polimorficznymi. S to jedyne klasy, ktre pozwalaj bezpiecznie operowa na swoich obiektach za pomoc wskanikw do ich klasy bazowej. Sowo bezpiecznie jest rozumiane jako gwarancja ze strony jzyka, e obiekty mog by uywane jedynie zgodnie ze swoim zdefiniowanym typem. Jednak nawet klasy polimorficzne maj pewne uomnoci. Wiadomo przecie, e przypisujc wskanikowi do klasy bazowej adres obiektu klasy pochodnej moemy operowa tylko tymi skadowymi obiektu klasy pochodnej, ktre odziedziczy z klasy bazowej. Wiadomo take, e nie jest dopuszczalna jawna konwersja wskanika do klasy bazowej na wskanik do klasy pochodnej. Mechanizm RTTI wychodzi naprzeciw tym problemom, pozwalajc wykonywa jawn kontrol i konwersj typw w fazie wykonania programu.

11.1. Konwersja dynamiczna


Zwyke konwersje s jednym z gwnych rde bdw w jzyku C++. Ponadto maj one do zagmatwan skadni i w wielu przypadkach nie s bezpieczne; konwersja jest operacj na typach danych i na og nie zaley od wartoci obiektw, na ktrych operuje. Dlatego zwyka konwersja nie moe si nie uda po prostu wyprodukuje now warto. T niekorzystn sytuacj w znacznym stopniu zmienio na lepsze wprowadzenie dwch nowych operatorw: dynamic_cast i typeid. Pierwszy z nich mona stosowa tylko do klas z funkcjami wirtualnymi, ktre atwo mog dostarczy informacji o swoim typie w fazie wykonania programu. Skadnia tego operatora ma posta: dynamic_cast<T>(wsk) W wyraeniu dynamic_cast<T>(wsk) T musi by wskanikiem lub referencj do wczeniej zdefiniowanej klasy lub void*. Argument wsk musi by wskanikiem lub referencj. Jeeli T jest typu void*, wwczas wsk musi take by wskanikiem. W tym przypadku konwersja daje wskanik, ktry moe mie dostp do dowolnego elementu klasy lecej najniej w hierarchii klas. Konwersja z klasy pochodnej do bazowej jest wizana statycznie (w fazie kompilacji/konsolidacji). Jeeli T jest wskanikiem i wsk jest wskanikiem do klasy pochodnej, to wynik konwersji jest wskanikiem do klasy pochodnej. Przy takim zaoeniu mona dokonywa konwersji z klasy pochodnej do bazowej i z danej klasy pochodnej do innej klasy pochodnej. Analogiczna relacja zachodzi dla referencji, gdy T i wsk s referencjami. Konwersja z klasy bazowej do pochodnej jest moliwa jedynie dla klas polimorficznych. W tym przypadku mamy wizanie dynamiczne (w fazie wykonania). Udana konwersja dynamiczna przeksztaca wsk do danego typu. Jeeli T i wsk s wskanikami, to nieudana konwersja zwraca wskanik o wartoci 0; w przypadku referencji niepowodzenie konwersji zgasza wyjtek Bad_cast (klasa Bad_cast jest zdefiniowana w pliku nagwkowym <typeinfo.h>). W podanym niej przykadzie klasa Bazowa zawiera wirtualn funkcj skadow, a wic jest klas polimorficzn. W prezentowanym programie konwersja wskanika do klasy bazowej we wskanik do klasy pochodnej jest bezpieczna; oznacza to, e po konwersji moemy bezpiecznie (w sensie typizacji) operowa na elementach obiektw klasy pochodnej. Przykad 11.1. #include <iostream.h> class Bazowa { public: int x;

virtual void podaj() { cout<<"Bazowa::podaj()\n"; } }; class Pochodna: public Bazowa { public: int y; void podaj() { cout << "Pochodna::podaj()\n"; } }; int main() { Pochodna po, *wskp; Bazowa* wskb = &po; if((wskp = dynamic_cast<Pochodna*>(wskb)) != 0) cout << "Konwersja udana\n"; wskp->x = 10; wskp->y = 20; cout << "wskp->x = " << wskp->x << endl; cout << "wskp->y = " << wskp->y << endl; cout << "wskb->x = " << wskb->x << endl; wskp->podaj(); wskp->Bazowa::podaj(); return 0; } Wydruk z programu ma posta: Konwersja udana wskp->x = 10 wskp->y = 20 wskb->x = 10 Pochodna::podaj() Bazowa::podaj() Przykad 11.2. #include <iostream.h> class Bazwirt1 { public: Bazwirt1() { } virtual void f() { cout << "Bazwirt1::f()\n"; } }; class Bazwirt2 { public: Bazwirt2() { } virtual void g() { cout << "Bazwirt2::g()\n"; } }; class Bazowa3 {}; class Pochodna: public Bazwirt1, public virtual Bazwirt2, public virtual Bazowa3 {}; void g(Pochodna& po) {

Bazwirt1* wskb1 = &po; wskb1->f(); Pochodna* wskp1 = (Pochodna*)wskb1; wskp1->g(); Pochodna* wskp2 = dynamic_cast<Pochodna*>(wskb1); Bazwirt2* wskw = &po; //Nie ma konwersji z wirtualnej bazy: // Pochodna* wskp3 = (Pochodna*)wskw; Pochodna* wskp4 = dynamic_cast<Pochodna*>(wskw); Bazowa3* wskb3 = &po; //Nie ma konwersji z wirtualnej bazy: // Pochodna* wskp5 = (Pochodna*)wskb3; //Nie ma konwersji z klasy nie-polimorficznej: // Pochodna* wsk6 = dynamic_cast<Pochodna*>(wskb3); } int main() { Pochodna pdn; g(pdn); return 0; } Dyskusja. Wydruk z programu ma posta; Bazwirt1::f() Bazwirt2::g() W powyszym przykadzie klasa Pochodna dziedziczy od dwch klas polimorficznych Bazwirt1 i Bazwirt2 oraz od klasy Bazowa3, przy czym ostatnie dwie s dla niej wirtualnymi klasami bazowymi. W bloku funkcji g(), ktrej argumentem jest referencja do klasy Pochodna, zadeklarowano szereg konwersji, przy czym zapisy niedopuszczalne skadniowo s potraktowane jako komentarze. Zwrmy uwag na nastpujce: Pochodna* wskp1 = (Pochodna*)wskb1; jest konwersj dopuszczaln, ale bez gwarancji powodzenia operacji na obiekcie *wskp1. Dwie konwersje dynamiczne: Pochodna* wskp2 = dynamic_cast<Pochodna*>(wskb1); Pochodna* wskp4 = dynamic_cast<Pochodna*>(wskw); s bezpieczne, poniewa bd sygnalizoway niepowodzenie, a ponadto mona sprawdzi ich typy wynikowe.

11.2. Dynamiczna identyfikacja typw


Drug gwn cech RTTI jest moliwo wyznaczenia dokadnego typu obiektu. Do identyfikacji typu obiektu suy wbudowany operator typeid(). Gdyby operator typeid() by funkcj, jego deklaracja wygldaaby jak niej: class type_info; const type_info& typeid(nazwa-typu); const type_info& typeid(wyraenie); Operator typeid() mona uywa zarwno do typw wbudowanych, jak i typw definiowanych przez uytkownika. Zwraca on referencj do nieznanego typu nazwanego type_info. Jeeli jego operandem jest nazwa-typu, to zwraca on referencj do klasy type_info, ktra

reprezentuje argument nazwa-typu. Jeeli operandem jest wyraenie, to typeid() zwraca referencj do klasy type_info, ktra wtedy reprezentuje typ obiektu oznaczonego przez wyraenie. Jeeli operandem jest referencja lub poprzedzony gwiazdk wskanik do klasy polimorficznej, to typeid() zwraca typ dynamiczny aktualnego obiektu tej klasy. Jeeli operand nie jest polimorficzny, typeid() zwraca obiekt, ktry reprezentuje typ statyczny. Jeeli wsk jest wskanikiem do klasy, a w wyraeniu typeid(*wsk) warto wsk==0, wwczas zgaszany jest wyjtek Bad_typeid (klasa Bad_typeid jest zdefiniowana w pliku nagwkowym <typeinfo.h>). Korzystanie z operatora typeid() wymaga wczenia do programu pliku nagwkowego <typeinfo.h>. Przykad 11.3. // Identyfikacja typu obiektu // dla klasy nie-polimorficznej #include <iostream.h> #include <typeinfo.h> class Info { }; int main() { Info* wsk = new Info; cout << "Typ klasy Info jest: " << typeid(Info).name() << endl; cout << "Typ *wsk jest: " << typeid(*wsk).name() << endl; if(typeid(Info) == typeid(*wsk)) cout << "Ten sam typ 'Info' i '*wsk'.\n"; else cout << "NIE te same typy 'Info' i '*wsk'\n"; return 0; } Dyskusja. Wydruk z programu ma posta: Typ klasy 'Info' jest: Info Typ *wsk jest: Info Ten sam typ 'Info' i '*wsk' W programie wykorzystano funkcj skadow name(), klasy type_info zadeklarowan w standardowym pliku nagwkowym <typeinfo.h>. Funkcja ta zwraca nazw typu, a jej prototyp ma posta: const char* name() const; Przykad 11.4. //Identyfikacja typu obiektu dla typow //wbudowanych i klas nie-polimorficznych #include <iostream.h> #include <typeinfo.h> class Bazowa { }; class Pochodna: public Bazowa { };

char* wsk1 = "True"; char* wsk2 = "False"; int main() { char znak; float x; if(typeid(znak) == typeid(x)) cout << "Ten sam typ 'znak' i 'x'.\n"; else cout << "NIE te same typy 'znak' i 'x'\n"; cout << typeid(int).name() << endl; cout << typeid(Bazowa).name(); cout << " przed " << typeid(Pochodna).name() << ":" << (typeid(Bazowa).before(typeid(Pochodna)) ? wsk1 : wsk2) << endl; return 0;

} Dyskusja. Wydruk z programu ma posta: NIE te same typy 'znak' i 'x' int Bazowa przed Pochodna: True W programie wykorzystano funkcj skadow before(), klasy type_info zadeklarowan w standardowym pliku nagwkowym <typeinfo.h>. Funkcja ta zwraca warto typu int, a jej prototyp ma posta: int before(const type_info&) const; Funkcja type_info::before() pozwala porzdkowa obiekty klasy type_info. Naley zaznaczy, e relacja porzdku, wprowadzana przez t funkcj, nie ma nic wsplnego z uporzdkowaniem w drzewie czy w grafie dziedziczenia. Nie ma rwnie gwarancji, e before() da te same wyniki dla rnych programw, czy te dla kolejnych wykona tego samego programu. Przykad 11.5. //Polimorficzna klasa bazowa #include <iostream.h> #include <typeinfo.h> class Bazowa { virtual void func() {}; }; class Pochodna: public Bazowa {}; int main() { Pochodna obiekt; Pochodna* wskp; wskp = &obiekt; try { //Testy prowadzone w fazie wykonania if (typeid(*wskp) == typeid(Pochodna)) //Pytanie: jaki jest typ *wskp? cout << "Nazwa typu jest " << typeid(*wskp).name();

if (typeid(wskp) != typeid(Bazowa)) cout << "\nWskaz nie jest typu Bazowa. "; return 0; } //Koniec try catch (Bad_typeid) { cout << "Nieudana identyfikacja typeid()."; return 1; } // Koniec catch } Wydruk z programu ma posta: Nazwa typu jest Pochodna Wskaz nie jest typu Bazowa.

11.3. Nowa notacja dla konwersji


Konwersja dynamiczna za pomoc operatora dynamic_cast jest stosowalna w omawianych wczeniej specyficznych przypadkach, w szczeglnoci dla konwersji z polimorficznej klasy bazowej do jej klasy pochodnej. Trzy dalsze operatory: static_cast, const_cast i reinterpret_cast wprowadzono dla konwersji statycznych z dwch powodw. Po pierwsze, nowa skadnia zamiast stosowania notacji (typ)wyraenie, ktra moe niejednokrotnie sugerowa, e chodzi o jak funkcj, wyranie pokazuje, e mamy do czynienia z konwersj. Po drugie, sama operacja konwersji, korzystajca z nowych operatorw, jest bezpieczniejsza. Tym niemniej uytkownikowi pozostawiono moliwo korzystania ze starej notacji, wraz z czychajcymi w niej puapkami. Mona si o tym przekona nie tylko w przypadku konwersji, co ilustruje podany niej przykad. Przykad 11.6. #include <iostream.h> const int zmienna = 10; int main() { cout << "zmienna: " << zmienna << endl; int& z = zmienna; z = 20; cout << "z: " << z << endl; return 0; } Dyskusja. Program daje si skompilowa i wykona, chocia kompilator wyle najpierw ostrzeenie: Temporary used to initialize 'z' in function 'main'. Wbrew oczekiwaniom program wydrukuje: zmienna: 10 z: 20 Wrmy jednak do konwersji. Operator static_cast ma skadni: static_cast<T>(arg)

gdzie T musi by wskanikiem, referencj, typem arytmetycznym, lub typem wyliczeniowym, za typ argumentu arg musi by zgodny z typem T i w peni znany w fazie kompilacji. Konwersja statyczna moe by w szczeglnoci stosowana do przeksztacenia wskanika do klasy bazowej we wskanik do klasy pochodnej i odwrotnie. W pierwszym przypadku wymaga si, aby klasa bazowa nie bya klas wirtualn. W drugim przypadku musi istnie jednoznaczna konwersja z klasy pochodnej do bazowej. Podany niej przykad ilustruje konwersje w obu kierunkach oraz przypomina stary styl konwersji. Przykad 11.7. #include <iostream.h> class Bazowa { }; class Pochodna: public Bazowa { }; int main() { Bazowa* wskb = new Bazowa; Pochodna* wskp = new Pochodna; Pochodna* wskp1 = (Pochodna*)wskb;//stary styl Pochodna* wskp2 = static_cast<Pochodna*>(wskb); Bazowa* wskb1 = static_cast<Bazowa*>(wskp); return 0; } Operator const_cast, podobnie jak pozostae operatory konwersji, zosta pomylany jako mechanizm, ktry respektuje stao zdefiniowanego obiektu, a jednoczenie pozwala na jego uzmiennienie, ale ju pod inn nazw i adresem. Skadnia operatora jest nastpujca: const_cast<T>(arg) gdzie T i arg musz by tego samego typu, za wyjtkiem modyfikatorw const i volatile. Wynik konwersji jest typu T. Przykad 11.8. //const_cast: wsk jest const, wsk1 nie. #include <iostream.h> const int z1 = 10; int main() { cout << "z1: " << z1 << endl; const int* wsk = &z1; int* wsk1; wsk1 = const_cast<int*>(wsk); *wsk1 = 30; cout << "*wsk1: " << *wsk1 << endl; return 0; } Wydruk z programu ma posta: z1: 10 *wski: 30

Operator reinterpret_cast ma skadni: reinterpret_cast<T>(arg) gdzie T musi by wskanikiem, referencj, typem arytmetycznym, wskanikiem do funkcji, lub wskanikiem do elementu klasy. Zgodnie ze swoj nazw, operator ten mona wykorzysta np. do konwersji z typu int* do int i odwrotnie, uzyskujc z powrotem typ int*, co pokazano w poniszym przykadzie. Przykad 11.9. //reinterpret_cast #include <iostream.h> #include <typeinfo.h> int main() { int i1 = 10; cout << typeid(i1).name() << endl; int* wski = &i1; cout << *wski << endl; cout << typeid(wski).name() << endl; //Konwersja z int* do int: i1 = reinterpret_cast<int>(wski); cout << typeid(i1).name() << endl; //Konwersja z int do int*: wski = reinterpret_cast<int*>(i1); cout << typeid(wski).name() << endl; return 0; } Wydruk z programu ma posta: int 10 int* int int* Dyskusja. Stosujc dwukrotnie operator reinterpret_cast do tej samej pary zmiennych wrcilimy do pierwotnego typu. Operator ten w oglnoci mona stosowa do konwersji wskanika dowolnego typu we wskanik dowolnego typu. Jak atwo przewidzie, nie bd to konwersje bezpieczne i na og bd zalene od implementacji, nie dajc gwarancji przenonoci programu. Jedyn konwersj bezpieczn jest konwersja do pierwotnego typu. Tak wic operator reinterpret_cast jest prawie tak samo mao pewny, jak stary (T)arg. Jednak ten nowy operator jest bardziej widoczny, nigdy nie pozwala na nawigacj w hierarchii klas i nie amie staoci obiektw z modyfikatorem const.

Dodatek A. Zbir znakw ASCII


D 0 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 o 0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17 20 21 22 23 24 25 26 27 30 31 32 33 34 35 36 37 z NUL SQH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US d 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 o 40 41 42 43 44 45 46 47 50 51 52 53 54 55 56 57 60 61 62 63 64 65 66 67 70 71 72 73 74 75 76 77 z SP ! " # $ % & ' ( ) * + , . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? d 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 o 100 101 102 103 104 105 106 107 110 111 112 113 114 115 116 117 120 121 122 123 124 125 126 127 130 131 132 133 134 135 136 137 z @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ d 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 o 140 141 142 143 144 145 146 147 150 151 152 153 154 155 156 157 160 161 162 163 164 165 166 167 170 171 172 173 174 175 176 177 z ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~ DEL

Nagwki w tabeli oznaczaj: d dziesitny kod znaku, o oktalny kod znaku, z znak.

Znaczenie znakw sterujcych:


0. 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. NUL SOH (Start Of Heading) STX (Start of Text) ETX (End of Text) EOT (End of Transm.) ENQ (Enquiry) ACK (Acknowledge) BEL (Bell) BS (Back Space) HT (Horizontal Tab) LF (Line Feed) VT (Vertical Tab) FF (Form Feed) CR (Carriage Return) SO (Switch Output) SI (Switch Input) DLE (Data Link Escape) DC1 (Device Control 1) - znak zerowy - pocztek nagwka = SOM -pocztek tekstu = EOA - koniec tekstu = EOM - koniec transmisji - wywoanie stacji - potwierdzenie - dzwonek - powrt o 1 pozycj - tabulacja pozioma - przesuw o 1 wiersz - tabulacja pionowa - przesuw o 1 stron - powrt karetki - wyjcie (przeczenie trwae) - wejcie (przeczenie powrotne) - pominicie znakw sterujcych - sterowanie urzdz.1/ start transmisji=XON - sterowanie urzdzenia 2 - sterowanie urzdz.3/ stop transmisji= XOFF - sterowanie urzdzenia 4 - potwierdz. negatywne (gdy bd) - synchronizacja - koniec bloku - anulowanie - koniec nonika (zapisu) - zastpienie - przeczenie - poprzedza dane alfanumeryczne - poprzedza dane binarne - separator rekordw - separator pozycji - spacja (odstp) - kasowanie

18. DC2 (Device Control 2) 19. DC3 (Device Control 3) 20. DC4 (Device Control 4) 21. NAK (Negative Acknowledge) 22. SYN (Sync) 23. ETB (End Transm. Block) 24. CAN (Cancel) 25. EM (End of Medium) 26. SUB (Substitute) 27. ESC (Escape) 28. FS (File Separator) 29. GS (Group Separator) 30. RS (Record Separator) 31. US (Unit Separator) 32. SP (Space) 127. DEL (Delete)

Dodatek B Priorytety i czno operatorw


Operator :: :: . -> [] () () sizeof ++ ++ --~ ! + & * new delete delete[] () .* ->* 14 15 16 Priorytet 17 czno L P L L L L L L P P P P P P P P P P P P P P L L Dziaanie Zasig globalny zasig klasy dostp do skadowej obiektu dostp do skadowej obiektu indeksowanie wywoanie funkcji konstrukcja obiektu rozmiar obiektu/typu przedrostkowe zwikszanie o 1 przyrostkowe zwikszanie o 1 przedrostkowe zmniejszanie o 1 przyrostkowe zmniejszanie o 1 negacja bitowa negacja logiczna minus jednoargumentowy plus jednoargumentowy adres argumentu/referencja dostp poredni tworzenie (przydzia pamici) usuwanie (zwalnianie pamici) usuwanie tablicy konwersja typu (rzutowanie) dostp do skadowej dostp do skadowej

Priorytet: im wiksza warto, tym wyszy priorytet czno : L lewostronna, P prawostronna

Operator * / % + << >> < <= > >= == != & ^ | && || ? : = *= /= %= += -= <<= >>= &= ^= |= ,

Priorytet 13

czno L L L L L L L L L L L L L L L L L L L P P P P P P P P P P P L

Dziaanie mnoenie dzielenie modulo (reszta z dzielenia) dodawanie odejmowanie przesuwanie w lewo przesuwanie w prawo mniejsze mniejsze lub rwne wiksze wiksze lub rwne rwne nierwne koniunkcja bitowa bitowa rnica symetryczna alternatywa bitowa koniunkcja logiczna alternatywa logiczna wyraenie warunkowe przypisanie mnoenie i przypisanie dzielenie i przypisanie modulo i przypisanie dodawanie i przypisanie odejmowanie i przypisanie przesunicie w lewo i przypisanie przesunicie w prawo i przypisanie koniunkcja bitowa i przypisanie rnica symetryczna i przypisanie alternatywa bitowa i przypisanie ustalenie kolejnoci

12 11 10

9 8 7 6 5 4 3 2

You might also like