Obiektowe programowanie w języku C++

Wielkość: px
Rozpocząć pokaz od strony:

Download "Obiektowe programowanie w języku C++"

Transkrypt

1 1/116 Kurs podstawowy Dr inż. Lucjan Miękina upel.agh.edu.pl/wimir/login/ Katedra Robotyki i Mechatroniki May 25, 2015

2 2/116 Pojęcia podstawowe Literatura: B. Stroustrup, Język C++, Addison-Wesley, 2000 L. Miękina, Inżynieria oprogramowania, AGH SU 1707 B. Stroustrup, Programming Principles and Practice Using C++, Addison-Wesley, It is an introduction to programming for people who have never programmed before. It will also be useful for people who have programmed a bit and want to improve their style and technique - or simply learn modern C++. It is designed for classroom use, but written with an eye on self study. Stroustrup s web page: bs/ B. Eckel, Thinking in C++, Prentice Hall, 2002 Literatura dodatkowa: L. Kuczmański: Język C++ J. Grębosz: Symfonia C++ Standard, Editions 2000, Kraków

3 3/116 Pojęcia podstawowe Język C został uzupełniony o tzw. rozszerzenie obiektowe, umożliwiające i wspierające programowanie zorientowane obiektowo (OOP). Realizowane to jest przez wprowadzenie szeregu nowych słów kluczowych i mechanizmów języka. Rozwój technik programowania: liniowe - brak procedur i funkcji, wykorzystanie instrukcji skoku warunkowego i bezwarunkowego jako substytutów strukturalizacji. Realizowane przez język maszynowy, assembler i wczesne wersje Basic-a proceduralne - opiera się na przepływie danych przez odpowiednio dobrane procedury i funkcje, realizujące cząstkowe zadania przetwarzania tych danych. Obowiązuje schemat: Wybierz procedurę realizującą wymaganą operację na danych, do kodowania procedury użyj najlepiej dostosowanego algorytmu. Języki: Fortran, Algol, Pascal, C, Ada, itd. modułowe - wymienione wyżej języki proceduralne zawierają wsparcie do modularnego programowania, które opiera się na wyodrębnieniu części reprezentacji problemu w postaci modułu, będącego zbiorem metod (funkcji) i danych, na których te metody operują (stałe, zmienne). Najbardziej istotną nową cechą podejścia modułowego jest możliwość ukrycia danych modułu przed nieautoryzowanym dostępem.

4 4/116 Pojęcia podstawowe Rozwój technik programowania - cd. obiektowe - gdzie położono nacisk na: tworzenie reprezentacji problemu w postaci zbliżonej do niego samego łatwą konserwację i rozbudowę oprogramowania możliwość wielokrotnego użycia modułów programowych (ang. reusability) lepsze wsparcie dla budowy dużych systemów i pracy grupowej komponentowe, będące dalszą ewolucją obiektów w kierunku zgodności na poziomie binarnym, a nie źródłowym. Oznacza to, że tworzy się komponenty poddane kompilacji i konsolidacji (a więc gotowe do użycia), które zapewniają jednolity interfejs do usług, których dostarczają. funkcyjne - funkcje należą do wartości podstawowych, a nacisk kładzie się na wartościowanie (często rekurencyjnych) funkcji, a nie na wykonywanie poleceń. W czystym programowaniu funkcyjnym, raz zdefiniowana funkcja zwraca zawsze tę samą wartość dla danych wartości argumentów, tak jak funkcje matematyczne. Języki: LISP, Haskell, Clojure, F#. aspektowe - oparte o zasadę rozdziału zagadnień (Separation of Concerns): program powinien być zdekomponowany w taki sposób, aby różne jego aspekty znajdowały się w dobrze odseparowanych od siebie częściach systemu każdy aspekt zajmuje się jednym zagadnieniem zagadnienie to szczególny cel programu, koncepcja albo funkcja Język: AspectJ (rozszerzenie języka Java)

5 5/116 Cechy obiektowego podejścia do programowania Niezależnie od języka implementacji, istnieją ogólne zasady obiektowego podejścia do tworzenia oprogramowania, nazywane modelem obiektowym. Składa się on z poniższych pojęć: obiekt każdy byt (pojęcie lub rzecz), mający znaczenie w dziedzinie zastosowania klasa uogólnienie zbioru obiektów, mających takie same atrybuty, operacje i znaczenie. Jeśli pewne klasy mają wspólną część, to powinna ona być ich wspólną klasą bazową (lub inaczej nadklasą). komunikat (ang. message) specyfikacja wymiany informacji między obiektami, zawierająca zlecenie wykonania określonej operacji lub wymiany danych. interfejs - zbiór publicznych metod klasy specjalne metody - konstruktory i destruktor, wywoływane automatycznie w momencie tworzenia obiektu i jego usuwania. dziedziczenie (ang. inheritance) - polega na przejmowaniu cech pewnych obiektów przez inne, z równoczesnym dodawaniem nowych cech - tworzenie tzw. specjalizacji. Oznacza to przyporządkowanie atrybutów i operacji do klas zgodnie z hierarchiczną zależnością, jakiej podlegają klasy. Dzięki temu można budować systemy złożone, lecz elastyczne w zastosowaniu. abstrakcja proceduralna i abstrakcja danych - polega na ukrywaniu nieistotnych cech obiektu w trakcie operacji z nim związanych, dzięki temu że obiekt może być wyposażony w wiedzę o swych operacjach. Dzięki temu ułatwia się operacje i unika błędów, a także skraca się notację - można lepiej panować nad złożonymi systemami lub algorytmami.

6 6/116 Cechy obiektowego podejścia do programowania hermetyzacja (ang. encapsulation) - polega na selektywnym dostępie do szczegółów obiektu (atrybutów i operacji) i dzięki temu zapewnieniu niezależności (braku niepotrzebnych sprzężeń). Prywatne atrybuty i operacje są niedostępne dla otoczenia, stanowiąc elementy wewnętrznego mechanizmu działania obiektu. Publiczne atrybuty i operacje są dostępne dla otoczenia, umożliwiając odwoływanie się do obiektu za pomocą komunikatów. Chronione atrybuty i operacje są dostępne w ciągu podklas danej klasy. polimorfizm (ang. polymorphism) polega na nadaniu takiej samej nazwy różnym operacjom w klasach należących do jednej hierarchii (od nadklasy przez wszystkie podklasy). Pozwala m.in. na wywołanie tzw. wirtualnej metody (procedury lub funkcji), która może mieć różne znaczenie w zależności od kontekstu wywołania. Dzięki tym założeniom program obiektowo zorganizowany może się składać prawie wyłącznie z deklaracji zmiennych obiektowych (instancji); w trakcie tych deklaracji wykonywane są wszystkie operacje inicjalizacji obiektów (definiowania ich stanu początkowego), tak by mogły one zacząć funkcjonować w otoczeniu innych obiektów. Dalsze funkcjonowanie każdego obiektu polega na odbiorze komunikatów wysyłanych przez inne obiekty i odpowiednim ich przetwarzaniu, w ramach którego możliwe jest również wysyłanie własnych komunikatów, czyli oddziaływanie na otoczenie. Taki model daje większe możliwości, co jest związane głównie z decentralizacją funkcjonalności (każda klasa odpowiada za swoją część operacji złożonego systemu) i nie jest potrzebny żaden nadrzędny mechanizm, który musiałby być odpowiednio bardziej złożony.

7 7/116 Typ obiektowy (klasa) Klasa może być traktowana jako specjalny rodzaj typu strukturowego, składający się z: pól (jak w strukturze). Pola klasy służą do opisu stanu obiektu, a więc opisują jego własności. Każdy typ obiektowy jest zwykle opisany unikalnym zbiorem własności. metod (będących w istocie funkcjami znanymi z języka C) zadeklarowanych wewnątrz deklaracji klasy. Metody służą do wykonywania operacji zdefiniowanych dla typu obiektowego, a więc definiują zachowanie się obiektu. Metody operują w założeniu na polach dostępnych w klasie i mogą wywoływać inne metody klasy, a także funkcje zewnętrzne. UWAGA: W języku C++ struktura może mieć funkcje. Składnia deklaracji klasy Deklaracja klasy pierwotnej (czyli nie wywodzącej się z innej, już wcześniej zdefiniowanej) ma postać: 1 class Nazwa { 2 public : 3 // lista deklaracji pol / metod publicznych 4 protected : 5 // lista deklaracji pol / metod chronionych 6 private : 7 // lista deklaracji pol / metod prywatnych 8 };

8 Przykład hierarchii klas 8/116

9 9/116 Typ obiektowy (klasa) Nazwa jest nazwą klasy, czyli nowo tworzonego typu obiektowego. Ciało klasy składa się z dowolnej liczby sekcji, różniących się co do sposobu dostępu do zadeklarowanych w nich składników. Wyróżnia się sekcje: prywatne (private). Składniki zdefiniowane w sekcji prywatnej są widoczne jedynie w obrębie obiektów danej klasy, tzn. można ich używać wewnątrz metod (funkcji) tej klasy. Jeśli zdefiniowane są pola prywatne, to dla zapewnienia możliwości odczytu ich wartości spoza klasy można zdefiniować specjalne funkcje dostępu, które mogą być publiczne lub chronione. Podobne funkcje można zdefiniować dla zapewnienia zmiany wartości wybranych pól prywatnych. chronione (protected). Składniki zdefiniowane w sekcji chronionej są widoczne jedynie w obrębie obiektu danej klasy i klasy pochodnej. publiczne (public). Składniki zdefiniowane w sekcji publicznej są widoczne wszędzie (podobnie jak dla struktur). W tej sekcji zwykle muszą być zadeklarowane specjalne funkcje klasy: konstruktor i destruktor, o ile są przewidziane (jeśli nie są przewidziane, to kompilator generuje ich domniemane wersje, typowo nie wykonujące żadnych specyficznych operacji). W ciele klasy może wystąpić dowolna ilość sekcji w dowolnej kolejności. Reprezentacja obiektów w pamięci Składniki danej sekcji są umieszczone w pamięci w takiej kolejności jak je zadeklarowano, natomiast kolejność ułożenia sekcji zależy od kompilatora. Obiekty mogą być alokowane jako zmienne: lokalne modułowe lub globalne dynamiczne, tworzone z użyciem new i usuwane z użyciem delete składowe innych obiektów (klas lub tablic)

10 10/116 Konstruktor i destruktor Są to specjalne metody należące do klasy. Konstruktor nosi nazwę taką jak nazwa klasy (typu obiektowego), a destruktor taką samą, ale poprzedzoną znakiem. Wywołanie konstruktora ma na celu utworzenie i zainicjowanie obiektu. Konstruktor jest wywoływany na rzecz pewnego amorficznego (bezpostaciowego) obszaru pamięci, który jest zamieniany (formatowany) na obiekt. Wewnątrz konstruktora są dostępne wszystkie składniki klasy (pola i metody). Klasa może definiować więcej niż 0 konstruktorów (konieczne jest by były to funkcje przeciążone o różnej ilości i typie parametrów formalnych) i o funkcjonalności dostosowanej do potrzeb; każdy z nich może inicjować obiekt tej samej klasy w inny sposób. Jeśli nie jest zdefiniowany żaden konstruktor, to automatycznie generuje się konstruktor domniemany, bezparametrowy. Klasa może posiadać konstruktor kopiujący, wywoływany z jednym argumentem typu tej samej klasy. Wywołanie konstruktora może być jawne (przy dynamicznym tworzeniu obiektu operatorem new) i niejawne (w miejscu deklaracji zmiennej odpowiedniego typu). Zadaniem destruktora jest zniszczenie obiektu, czyli przekształcenie tej części pamięci operacyjnej, którą zajmował obiekt w amorficzny obszar pamięci. Wywołanie destruktora może być jawne (przy usuwaniu operatorem delete obiektu utworzonego dynamicznie) i niejawne (w miejscu wyjścia sterowania z zakresu deklaracji zmiennej odpowiedniego typu).

11 11/116 Słowo kluczowe this Słowo kluczowe this reprezentuje wskaźnik do obiektu, którego metoda jest w danej chwili wykonywana. Jest to zatem wskaźnik do bieżącego obiektu, dlatego słowo kluczowe this może się pojawić tylko w kodzie metod klasy. W każdej funkcji klasy X, ten wskaźnik jest domyślnie zadeklarowany jako: X const this ; i zainicjalizowany na obiekt, dla którego wywołana została dana funkcja. Ponieważ this jest zadeklarowany jako *const, czyli jako stały wskaźnik, jego wartość nie może się zmienić. Jednakże wskazywany obiekt może ulegać zmianom! Domyślnie, w kodzie dowolnej metody należącej do klasy kompilator traktuje identyfikator każdej składowej zdefiniowanej w tej klasie (pola lub metody) tak, jak gdyby był on poprzedzony napisem this->, czyli był składnikiem klasy. 1 class PierwszaKlasa { 2 int pole ; 3 void Ustaw ( int pole ) { 4 this -> pole = pole ; 5 }; 6 public : 7 void Metoda () { Ustaw (1); }; 8 }; Wyjątek: jeśli metoda posiada parametr o nazwie identycznej z nazwą pewnego pola klasy (jak w linii 3), to w kodzie tej metody ta nazwa reprezentuje parametr, nie zaś pole. Należy wtedy jawnie użyć wskaźnika this przed tym wystąpieniem nazwy, które ma oznaczać pole (jak w linii 4). Przy wywołaniu dowolnej metody klasy, konieczne jest podanie jej nazwy (z wymaganymi parametrami wejściowymi w nawiasie okrągłym), a jeśli wywołuje się metodę z innej klasy niż bieżąca lub na rzecz innego obiektu niż bieżący przed nazwą podaje się nazwę obiektu lub wskaźnik do niego, np.:

12 12/116 Słowo kluczowe this 1 # include " PierwszaKlasa.h" 2 3 int main () { 4 PierwszaKlasa o; // obiekt 5 PierwszaKlasa * p = &o; // wskaznik 6 // wywolanie za pomoca obiektu 7 o. Metoda (); 8 // wywolanie za pomoca wskaznika 9 p-> Metoda (); 10 } Skutkiem takiego sposobu wywołania metody jest zainicjowanie wskaźnika this wartością adresu obiektu o (w linii 7) lub wartością adresu przechowywanego przez wskaźnik p (w linii 9). Dzieje się to w prologu każdej metody należącej do klasy, tak że metoda zawsze zna aktualną wartość this (wie na rzecz którego obiektu działa). Możliwe zastosowania wskaźnika this: odczyt adresów obiektów, patrz metoda Info w klasie complex porównywanie adresów obiektów, patrz metoda Set w klasie complex odróżnianie identyfikatorów składowych klasy (pól lub metod) od innych identyfikatorów, patrz pierwszy konstruktor klasy complex.

13 Typ referencyjny Typ referencyjny Typem referencyjnym (odniesienia nie jest to ścisłe określenie, a jedynie próba dodatkowego objaśnienia) jest taki typ, którego zmienne są synonimami innych zmiennych. Każde odwołanie do zmiennej typu referencyjnego jest uznawane za odwołanie do związanej z nią przez referencję innej zmiennej. Podobnie jak w przypadku zmiennych wskaźnikowych, konieczne jest odpowiednie zainicjowanie zmiennej referencyjnej, tak by było wiadomo z jaką zmienną jest ona skojarzona (lub inaczej mówiąc jakiej zmiennej jest synonimem). Odmiennie od wskaźników, w C++ występują wyłącznie stałe referencje. Po koniecznej inicjalizacji nie mogą już być zmieniane. Deklaracja zmiennej referencyjnej wymaga użycia symbolu & (ampersand) pomiędzy nazwą typu bazowego i nazwą zmiennej referencyjnej, na przykład: 1 int Fix = 12; 2 int & Ref = Fix ; // deklaracja i inicjalizacja zmiennej referencyjnej Ref 3 Ref = Ref + 1; // ta operacja dotyczy zmiennej Fix!! Zmienna Ref jest synonimem zmiennej Fix, każda operacja na Ref dotyczy w istocie zmiennej Fix. Zastosowanie referencji Modyfikacja sposobu przekazywania argumentów wywołania do funkcji. Jeśli argument jest przekazany przez referencję, to funkcja operuje bezpośrednio na tym argumencie, a nie na jego kopii, jak w standardowym C. Modyfikacja sposobu zwracania rezultatu wywołania funkcji. 13/116

14 14/116 Przykład deklaracji klasy Poniżej zadeklarowano prostą klasę Complex, która opisuje wybrane własności i operacje zbioru liczb zespolonych. UWAGA: biblioteki standardowe dostarczane obecnie z kompilatorami C++ zawierają klasy implementujące pełną funkcjonalność właściwą dla danych typu zespolonego, w tym również funkcje operatorowe +, -, *, / i inne, które dla tych liczb definiują wymienione standardowe działania. 2 # include <fstream > 3 class Complex { 4 char Name [16]; 5 double Re, Im; 6 public : 7 Complex (); // default ctor ( parameter -less ) 8 Complex ( const char * name, double re, double im ); 9 Complex ( const Complex & c); // copy ctor 10 ~ Complex (); // destructor 11 void Set ( const Complex & c); 12 void SetRe ( double re) { this ->Re = re; }; 13 void SetIm ( double im) { Im = im; }; 14 double GetRe ( void ) { return Re; }; 15 double GetIm ( void ) { return Im; }; 16 double abs ( void ); 17 void Info (); 18 Complex & operator +( const Complex & r); 19 Complex & operator -( const Complex & r); 20 Complex & operator *( const Complex & r); 21 Complex & operator =( const Complex & r); Zwykle klasa jest zadeklarowana w pliku nagłówkowym, a zdefiniowana w pliku źródłowym. Jak widać, niektóre metody zostały zarówno zadeklarowane jak i zdefiniowane w tym pliku. Są to tzw. metody otwarte (inline), czyli takie, których kod będzie umieszczony wszędzie tam, gdzie następuje ich wywołanie.

15 15/116 Plik źródłowy klasy Complex 1 # include <stdio.h> 2 # include < string.h> 3 # include <cmath > 4 # include " Complex.h" 5 6 // default constructor ( parameter -less ) 7 Complex :: Complex () { 8 strcpy (Name, "NN"); 9 Re = 0.; Im = 0.; 10 Info (); printf (" created "); 11 }; 12 // constructor 13 Complex :: Complex ( const char * name, 14 double Re, double Im) { 15 strcpy (Name, name ); 16 this ->Re = Re; this ->Im = Im; 17 Info (); printf (" created "); 18 } 19 // copy constructor 20 Complex :: Complex ( const Complex & c) { 21 strcpy (Name, c. Name ); 22 Re = c.re; Im = c.im; 23 Info (); printf (" created "); 24 } 25 // destructor 26 Complex ::~ Complex (){ 27 Info (); printf (" deleted "); 28 } 29 // copying field -by - field 30 void Complex :: Set ( const Complex & c) { 31 if ( this == &c) 32 return ; 33 strcpy (Name, c. Name ); 34 Re = c.re; Im = c.im; 35 } 36 // evaluating the magnitude 37 double Complex :: abs ( void ) { 38 return sqrt (Re * Re + Im * Im ); 39 } 40 void Complex :: Info () { 41 printf ("\n %s: %p, (%5.1f, %5.1 f)", 42 Name, this, Re, Im ); 43 } 1 # include <stdio.h> 2 # include " Complex.h" 3 4 int main () { 5 printf (" Create objects :"); 6 Complex c; 7 Complex c2("c2", 10., 20.); 8 Complex c2bis (c2 ); 9 printf ("\nde - allocate "); 10 return 0; 11 }

16 16/116 Prosty przykład programu obiektowego Program korzysta z klasy Complex, tworząc w sposób niejawny 3 obiekty, a następnie wyświetlając o nich informacje. 1 # include <stdio.h> 2 # include " Complex.h" 3 4 int main () { 5 printf (" Create objects :"); 6 Complex c; 7 c. SetRe (1.); 8 c. SetIm (2.); 9 Complex c2("c2", 10., 20.); 10 Complex c2bis (c2 ); 11 printf ("\ ndisplay info :"); 12 c. Info (); 13 c2. Info (); 14 c2bis. Info (); 15 printf ("\nde - allocate :"); 16 return 0; 17 } 1 lm@arch >./a. out 2 Create objects : 3 NN: 0x7fff1f2f20d0, (0.0, 0.0) created 4 c2: 0x7fff1f2f20f0, (10.0, 20.0) created 5 c2: 0x7fff1f2f2110, (10.0, 20.0) created 6 Display info : 7 NN: 0x7fff1f2f20d0, (1.0, 2.0) 8 c2: 0x7fff1f2f20f0, (10.0, 20.0) 9 c2: 0x7fff1f2f2110, (10.0, 20.0) 10 De - allocate : 11 c2: 0x7fff1f2f2110, (10.0, 20.0) deleted 12 c2: 0x7fff1f2f20f0, (10.0, 20.0) deleted 13 NN: 0x7fff1f2f20d0, (1.0, 2.0) deleted

17 17/116 Prosty przykład programu obiektowego 2 Program korzysta z klasy Complex, tworząc w sposób jawny (za pomocą operatora new) 3 obiekty, a następnie wyświetlając o nich informacje. Utworzone obiekty znajdą się w obszarze sterty. Na koniec obiekty są usuwane za pomocą operatora delete. 1 # include <stdio.h> 2 # include " Complex.h" 3 4 int main () { 5 printf (" Create objects :"); 6 Complex *c = 0, *c2 = 0, * c2bis = 0; 7 c = new Complex ; 8 if (c) { 9 c-> SetRe (1.); 10 c-> SetIm (2.); 11 } 12 c2 = new Complex ("c2", 10., 20.); 13 if (c2) 14 c2bis = new Complex (* c2 ); 15 printf ("\ ndisplay info :"); 16 if (c) c-> Info (); 17 if (c2) c2 -> Info (); 18 if ( c2bis ) c2bis ->Info (); 19 printf ("\nde - allocate :"); 20 if (c) delete c; 21 if (c2) delete c2; 22 if ( c2bis ) delete c2bis ; 23 return 0; 24 } 1 lm@arch >./a. out 2 Create objects : 3 NN: 0x12ed010, (0.0, 0.0) created 4 c2: 0x12ed040, (10.0, 20.0) created 5 c2: 0x12ed070, (10.0, 20.0) created 6 Display info : 7 NN: 0x12ed010, (1.0, 2.0) 8 c2: 0x12ed040, (10.0, 20.0) 9 c2: 0x12ed070, (10.0, 20.0) 10 De - allocate : 11 NN: 0x12ed010, (1.0, 2.0) deleted 12 c2: 0x12ed040, (10.0, 20.0) deleted 13 c2: 0x12ed070, (10.0, 20.0) deleted

18 18/116 Funkcje operatorowe Funkcje te pozwalają definiować semantykę (czyli znaczenie) operatora. Język C++ posiada niejawnie zdefiniowane standardowe operatory, poznane wcześniej. Ponieważ każda funkcja może być przeciążona, można nadawać nowy sens znanym operatorom. UWAGA: nie można definiować nowych operatorów. Funkcja operatorowa ma nazwę: operator# gdzie # jest jest nazwą jednego z predefiniowanych operatorów. Operator może być zdefiniowany jako: funkcja składowa klasy. W tym przypadku lewy (albo jedyny) argument operacji pochodzi z klasy, a prawy (o ile istnieje) jest dostarczany w postaci argumentu funkcji operatorowej (tu nazwanego r) funkcja globalna, która ma tyle parametrów, ilu argumentów wymaga dany operator; ponadto musi być zadeklarowana z modyfikatorem friend. 45 Complex & Complex :: operator +( const Complex & r) { 46 Complex c("+", Re + r.re, Im + r.im ); 47 return c; 48 } Przykład operatora dodawania obiektów klasy Complex w postaci funkcji składowej klasy. Dla porównania, funkcja operatorowa nie należąca do klasy Complex i realizująca analogiczną operację ma postać: 1 Complex & operator +( const Complex & lpar, const Complex & ppar ) { 2 Complex tmp ("+", lpar.re + ppar.re, lpar.im + ppar.im ); 3 return tmp ; 4 }

19 19/116 Funkcje operatorowe Nieco inną postać musi posiadać operator kopiujący dla klasy, ponieważ musi on zwrócić wskazanie na obiekt, do którego zachodzi kopiowanie wartości z prawostronnego argumentu operacji kopiowania. Przykład: 61 Complex & Complex :: operator =( const Complex & r) { 62 Re = r.re; 63 Im = r.im; 64 return * this ; 65 } Można zauważyć, że: typ rezultatu jest referencją do klasy argumentem instrukcji return jest dereferencja obiektu wskazywanego przez this. Inną kategorią często spotykanych funkcji operatorowych są operatory porównania (==,!=, <, <=, >, >=), które mają typ rezultatu ustalony jako bool, np.: 67 bool Complex :: operator ==( const Complex & r) { 68 if (Re == r.re && Im == r.im) 69 return true ; 70 return false ; 71 }

20 20/116 Funkcje operatorowe - przykład wykorzystania Przykład programu wykorzystującego klasę Complex z działaniami wykonanymi za pomocą funkcji operatorowych: 1 # include <stdio.h> 2 # include " Complex.h" 3 4 int main () { 5 printf (" Create objects :"); 6 Complex c1("c1", 1., 1.); 7 Complex c2("c2", 2., 2.); 8 Complex add (" add ", 0., 0.); 9 printf ("\ noperations :"); 10 add = c1 + c2; 11 // add =c1. operator +( c2 ); 12 add. Info (); 13 printf ("\nde - allocate :"); 14 return 0; 15 } 1 lm@arch >./a. out 2 Create objects : 3 c1: 0x7fffcf8408d0, (1.0, 1.0) created 4 c2: 0x7fffcf8408f0, (2.0, 2.0) created 5 add : 0 x7fffcf840910, (0.0, 0.0) created 6 Operations : 7 +: 0 x7fffcf840880, (3.0, 3.0) created 8 +: 0 x7fffcf840880, (3.0, 3.0) deleted 9 add : 0 x7fffcf840910, (3.0, 3.0) 10 De - allocate : 11 add : 0 x7fffcf840910, (3.0, 3.0) deleted 12 c2: 0x7fffcf8408f0, (2.0, 2.0) deleted 13 c1: 0x7fffcf8408d0, (1.0, 1.0) deleted

21 21/116 Szablony Szablony znajdują zastosowanie w przypadkach, gdy trzeba zapisać takie same operacje w odniesieniu do różnych typów danych. Dotychczasowe rozwiązanie tego problemu mogło polegać na: napisaniu takiej funkcji dla jednego wybranego typu danej, a następnie skopiowaniu kodu i zamianie w odpowiednich miejscach typu danej na potrzebny. Prowadziło to jednak do powtarzania prawie identycznego kodu, różniącego się tylko typami danych w deklaracjach, zastosowaniu odpowiednich dyrektyw preprocesora, co nie jest w pełni bezpieczne. Zastosowanie szablonu (template) pozwala ominąć te problemy. Działanie mechanizmu szablonu polega na parametryzacji funkcji lub klasy przy użyciu typu danej (typ jest parametrem i może przyjmować różne wartości) lub wartości danej. Rozróżniamy szablony: funkcji klas. Na podstawie dostarczonych wartości parametrów szablonu, kompilator generuje nowe funkcje lub klasy; ten proces jest nazywany generowaniem instancji (template instantiation), a klasa lub funkcja wygenerowana na podstawie szablonu i jego parametrów nazywa się specjalizacją.

22 22/116 Szablony Przykład szablonu funkcji Jeśli zachodzi potrzeba znajdowania maksimum dwu wartości to tradycyjnie należałoby zdefiniować funkcje dla wybranych typów danych: 1 int Max ( int p1, int p2) { 2 return p1 > p2? p1 : p2; 3 } lub użyć makrodefinicji w postaci: 1 float Max ( float p1, float p2) { 2 return p1 > p2? p1 : p2; 3 } 1 # define Max (A,B) ((A) > (B)? (A) : (B)) // to samo ; z zast. makra Używając szablonu, wystarczy napisać jedną funkcję szablonową: 1 template < class T> 2 T Max (T p1, T p2) { 3 return p1 > p2? p1 : p2; 4 } Funkcja ta spełnia rolę dwu wyżej podanych, a także dla innych typów (long, double,...). Symbol T występujący w definicji tej funkcji jest parametrem szablonu i jest zastępowany przez kompilator w trakcie analizy kodu odpowiednim typem (docelowym), który jest ustalany na podstawie typów użytych w każdym miejscu wywołania tej funkcji.

23 23/116 Szablony Przykład szablonu klasy Klasą szablonową jest rodzina klas zadeklarowana za pomocą szablonu. Deklaracja klasy szablonowej: Przykład użycia klasy szablonowej: 1 # include <iostream > 2 3 template < class T> 4 class Liczba { 5 T P1; 6 public : 7 Liczba (T p1) : P1(p1) { 8 } 9 Liczba () { }; 10 void Info () { 11 std :: cout << "P1=" 12 << P1 13 << std :: endl ; 14 }; 15 }; 17 int main () { 18 Liczba <int > Integer (1); 19 Integer. Info (); 20 Liczba <float > Float (3.14); 21 Float. Info (); return 0; 24 } Rezultat wykonania: 1 lm@arch >./a. out 2 P1 =1 3 P1 =3.14 T jest parametrem szablonu i określa typ składowej P1 klasy Liczba i typ argumentu p1 konstruktora. Szablon może mieć więcej niż jeden parametr, stosownie do potrzeb.

24 Szablony z wieloma parametrami Klasa z dwoma parametrami szablonu Pierwszy parametr definiuje typ (jest to typ elementów tablicy), a drugi pełni funkcję stałej całkowitej (określa ilość elementów tej tablicy). Dla drugiego parametru dostarczono wartość domyślną równą 5. Deklaracja klasy szablonowej: 1 # include <iostream > 2 using namespace std ; 3 4 template < class T, int I = 5> 5 class Tablica { 6 T tab [I]; 7 public : 8 bool set ( int i, const T& t); 9 bool get ( int i, T& t) { 10 if (i < 0 i >= I) 11 return false ; // blad, zly indeks!!! 12 t = tab [i]; 13 return true ; 14 } 15 void info () { 16 cout << " tab =" << tab << endl ; 17 } 18 }; 19 template < class T, int I> 20 bool Tablica <T,I >:: set ( int i, const T& t) { 21 if (i < 0 i >= I) 22 return false ; // blad, zly indeks!!! 23 tab [i] = t; 24 return true ; 25 } Przykład użycia klasy szablonowej: 27 int main () { 28 Tablica <char, 256> tabl ; 29 for ( int i = 0; i < 256; i ++) 30 tabl. set (i, ); 31 tabl. set (0, A ); 32 tabl. set (1, B ); 33 tabl. set (2, C ); 34 tabl. set (3, \0 ); 35 tabl. info (); 36 return 0; 37 } Rezultat wykonania: 1 lm@arch >./a. out 2 tab = ABC 24/116

25 25/116 Strumieniowe operatory << i >> Dla programu w języku C++ standardowo są dostępne następujące strumienie (jako obiekty): cin kontroluje pobieranie bajtów ze standardowego wejścia. Obiekt cin jest zadeklarowany jako: extern istream cin ; cout kontroluje wysyłanie bajtów do standardowego wyjścia. Obiekt cout jest zadeklarowany jako: extern ostream cout; cerr kontroluje nie-buforowane wysyłanie bajtów do standardowego wyjścia błędów. Obiekt cerr jest zadeklarowany jako: extern ostream cerr ; clog kontroluje buforowane wysyłanie bajtów do standardowego wyjścia diagnostycznego. Obiekt clog jest zadeklarowany jako: extern ostream clog ; W większości systemów cin jest standardowo połączony z klawiaturą, a cout, cerr i clog są standardowo połączone z ekranem konsoli. Połączenia te mogą jednak być przekierowane. W celu użycia strumieni oznaczonych w.w. nazwami, należy dołączyć plik nagłówkowy iostream i ewentualnie zadeklarować użycie przestrzeni nazw std, np.: 1 # include <iostream > 2 using namespace std ;

26 26/116 Strumieniowe operatory << i >> We/wy dla danych typu standardowego Klasa ostream posiada operator «dla obsługi operacji wyjściowych dla standardowych typów danych, a klasa istream operator» dla obsługi operacji wejściowych. 1 class ostream : 2 public virtual ios 3 { 4 //... 5 public : 6 ostream & operator < <( const char *); 7 ostream & operator < <( char ); 8 ostream & operator < <( int ); 9 ostream & operator < <( const void *); 10 // }; 1 class istream : 2 public virtual ios 3 { 4 //... 5 public : 6 istream & operator > >( char *); 7 istream & operator > >( char &); 8 istream & operator > >( int &); 9 istream & operator > >( void *); 10 // }; 1 # include <iostream > 2 int main () { 3 int I; double D; 4 std :: cout << " Podaj wartosc I "; 5 std :: cin >> I; 6 std :: cout << " Podaj wartosc D "; 7 std :: cin >> D; 8 std :: cout << "I=" << I << std :: endl 9 << "D=" << D; 10 return 0; 11 } 1 lm@arch >./a. out 2 Podaj wartosc I 2 3 Podaj wartosc D I=2 5 D =3.1415

27 27/116 Strumieniowe operatory << i >> Standardowe we/wy dla danych o typach zdefiniowanych przez użytkownika (w klasach) Dla realizowania operacji we/wy dla obiektów klas, należy zadeklarować w tych klasach odpowiednie funkcje operatorowe (<< i >>) jako zaprzyjaźnione, np. dla klasy Complex: 23 friend std :: istream & operator > >( std :: istream & is, Complex & c); 24 friend std :: ostream & operator < <( std :: ostream & os, const Complex & c); Ponadto należy zdefiniować te funkcje jako globalne, np. w przypadku klasy Complex: 73 std :: istream & operator >> (std :: istream & is, Complex & c) { 74 std :: cout << std :: endl << " Enter Re and Im: "; 75 is >> c.re >> c.im; 76 return is; 77 } 78 std :: ostream & operator << (std :: ostream & os, const Complex & c) { 79 os << "Re=" << c.re << ", Im=" << c.im; 80 return os; 81 } Program główny i rezultat działania: 1 # include " Complex.h" 2 3 int main () { 4 Complex c1; // deklaracja 5 std :: cin >> c1; // wprowadzenie 6 std :: cout << c1; // wyprowadzenie 7 return 0; 8 } 1 lm@arch >./a. out 2 NN: 0x4c102cc0, (0.0, 0.0) created 3 Enter Re and Im: Re =1.1, Im =2.2 5 NN: 0x4c102cc0, (1.0, 2.2) deleted

28 28/116 Strumieniowe operatory << i >> Plikowe we/wy dla danych o typach standardowych Dodatkowo dla programu można otworzyć pewną liczbę strumieni skojarzonych z plikiem na dysku lub urządzeniem typu znakowego, posługując się w tym celu klasami: ifstream (wejście z pliku) ofstream (wyjście do pliku). Klasy te implementują strumieniowe operatory «i» dla standardowych typów danych, a więc obsługują zapis i odczyt danych tych typów w plikach lub urządzeniach. 1 class ofstream : public... { 2 public : 3 ofstream & operator << ( const char *); 4 ofstream & operator << ( char ); 5 ofstream & operator << ( int ); 6 ofstream & operator << ( const void *); 7 //... 8 }; 1 class ifstream : public... { 2 public : 3 ifstream & operator >> ( bool &); 4 ifstream & operator >> ( char &); 5 ifstream & operator >> ( int &); 6 ifstream & operator >> ( void *&); 7 //... 8 };

29 29/116 Strumieniowe operatory << i >> Przykład realizacji plikowego we/wy dla danych o typach standardowych 1 # include <iostream > 2 # include <fstream > 3 using namespace std ; 4 int main () { 5 int I; double D; 6 cout << " Enter I "; 7 cin >> I; 8 cout << " Enter D "; 9 cin >> D; 10 cout << " Entered : "; 11 cout << "I=" << I << ", D=" << D; 12 // WRITE 13 ofstream os; 14 os. open (" Std. dat "); 15 os << I << " " << D; 16 os. close (); 17 I = 0; D = 0.; // reset 18 cout << endl << " Reset : "; 19 cout << "I=" << I << ", D=" << D; 20 // READ 21 ifstream is; 22 is. open (" Std. dat "); 23 is >> I >> D; 24 is. close (); 25 cout << endl << " Read : "; 26 cout << "I=" << I << ", D=" << D; 27 } 1 lm@arch >./a. out 2 Podaj wartosc I Podaj wartosc D Wprowadzone : I =22222, D = Wyzerowane : I=0, D=0 6 Wczytane : I =22222, D =3.1415

30 30/116 Strumieniowe operatory << i >> Plikowe we/wy dla danych o typach zdefiniowanych przez użytkownika (w klasach) Dla realizowania plikowych operacji we/wy dla obiektów klas, należy zadeklarować w tych klasach odpowiednie funkcje operatorowe ( «i») jako zaprzyjaźnione, np. dla klasy Complex: 26 friend std :: ifstream & operator >> (std :: ifstream & is, Complex & c); 27 friend std :: ofstream & operator << (std :: ofstream & os, const Complex & c); Ponadto należy zdefiniować te funkcje jako globalne, np. w przypadku klasy Complex: 83 std :: ifstream & operator >> (std :: ifstream & is, Complex & c) { 84 is >> c.re >> c.im; 85 return is; 86 } 87 std :: ofstream & operator << (std :: ofstream & os, const Complex & c) { 88 os << c.re << " " << c.im; 89 return os; 90 } Zapis w pliku Complex.dat: Odczyt z pliku Complex.dat: 1 # include " Complex.h" 2 using namespace std ; 3 int main () { 4 Complex c("c", 1.1, 1.1); 5 ofstream os(" Complex.dat "); 6 os << c; 7 os. close (); 8 return 0; 9 } 1 # include " Complex.h" 2 using namespace std ; 3 int main () { 4 Complex c; 5 ifstream is(" Complex.dat "); 6 is >> c; 7 is. close (); 8 return 0; 9 }

31 31/116 Zaprzyjaźnienia Zaprzyjaźnienie jest deklaracją, która nadaje wybranej funkcji zewnętrznej lub innej klasie takie same przywileje dostępu do składników pewnej klasy, jakie mają jej własne metody. Przykład: 1 class ExtClass ; // deklaracja zapowiadajaca klasy 2 class C { 3 int Priv ; // pole prywatne 4 friend class ExtClass ; // inna klasa zaprzyjazniona z klasa C 5 friend int ExtFun ( void ); // zewn. funkcja zaprzyjazniona z klasa C 6 }; Mechanizm ten definiuje dodatkowy poziom udostępniania składników klasy wybranym funkcjom lub klasom, stąd należy go używać z ostrożnością. Deklaracja funkcji lub klasy zaprzyjaźnionej może być umieszczona w sekcji publicznej lub prywatnej, bez zmiany znaczenia. Podobnie jak metoda klasy, funkcja zaprzyjaźniona jest zadeklarowana wewnątrz deklaracji klasy i dlatego staje się elementem zewnętrznego interfejsu klasy. W przypadku gdy wszystkie metody danej klasy są zaprzyjaźnione z inną klasą, można zdefiniować zaprzyjaźnienie dla całej klasy; w przykładzie powyżej klasa ExtClass jest zaprzyjaźniona z klasą C.

32 32/116 Zaprzyjaźnienia Przykład funkcji zaprzyjaźnionej z klasą Funkcja Info, będąca składową klasy Rysunek jest zaprzyjaźniona z klasą Punkt i dzięki temu może mieć dostęp do jej prywatnych pól X i Y : 1 # include <iostream > 2 using namespace std ; 3 4 class Punkt ; 5 class Rysunek { 6 public : 7 void Info ( Punkt & p); 8 }; 9 class Punkt { 10 int X, Y; 11 friend void Rysunek :: Info ( Punkt & pt ); 12 public : 13 Punkt () : X(1), Y (2) { } 14 }; 15 void Rysunek :: Info ( Punkt & p) { 16 cout << "X = " << p.x << endl ; 17 cout << "Y = " << p.y << endl ; 18 } 20 int main () { 21 Punkt pt; 22 Rysunek rys ; 23 rys. Info (pt ); 24 } Rezultat działania: 1 X = 1 2 Y = 2 UWAGA: nie tylko funkcje składowe klasy mogą być zaprzyjaźnione z innymi klasami - także funkcje globalne.

33 33/116 Zaprzyjaźnienia Przykład klasy zaprzyjaźnionej z inną klasą Klasa Rysunek jest zaprzyjaźniona z klasą Punkt i dzięki temu każda z jej metod może mieć dostęp do prywatnych składników klasy Punkt, w tym przykładzie pól X i Y : 1 # include <iostream > 2 using namespace std ; 3 4 class Punkt ; 5 class Rysunek { 6 public : 7 void info ( const Punkt & p); 8 void rysuj ( const Punkt & p); 9 }; 10 class Punkt { 11 int X, Y; 12 friend class Rysunek ; 13 public : 14 Punkt () : X(1), Y (2) { } 15 }; 16 void Rysunek :: info ( const Punkt & p) { 17 cout << "X = " << p.x << endl ; 18 cout << "Y = " << p.y << endl ; 19 } 20 void Rysunek :: rysuj ( const Punkt & p) { 21 } 23 int main () { 24 Punkt pt; 25 Rysunek rys ; 26 rys. info (pt ); 27 rys. rysuj (pt ); Rezultat działania: 1 X = 1 2 Y = 2

34 34/116 Statyczne składniki klasy Klasa jest typem danych, a nie obiektem. Każdy obiekt danej klasy standardowo posiada swoją kopię pól. Jednak w pewnych zastosowaniach należy się posłużyć innym modelem, w którym wybrane pola są wspólne dla wszystkich obiektów danej klasy. Należy je wtedy zadeklarować wewnątrz klasy z użyciem modyfikatora static: Plik nagłówkowy: 1 class Klasa1 { 2 // deklaracja pola statycznego 3 static char * NazwaKlasy ; 4 public : 5 void info (); 6 }; Plik źródłowy: 1 # include <iostream > 2 # include " Klasa1.h" 3 4 // definicja pola statycznego 5 char * Klasa1 :: NazwaKlasy = " Klasa1 "; 6 7 void Klasa1 :: info () { 8 std :: cout << "\ nnazwa klasy =" 9 << NazwaKlasy ; 10 } 1 # include " Klasa1.h" 2 3 int main () { 4 Klasa1 k1; 5 k1. info (); 6 7 return 0; 8 } Rezultat działania: 1 lm@arch >./a. out 2 Nazwa klasy = Klasa1

35 35/116 Klasy zagnieżdżone 1 # include <iostream > 2 class List { 3 // prywatna deklaracja klasy wewn. 4 class Element { 5 int Value ; // dane przech. w elem. 6 Element * pnext ; 7 public : 8 Element ( int v, Element * next ) { 9 Value = v; pnext = next ; 10 } 11 Element * Next () { return pnext ; } 12 void Info () { 13 std :: cout << "\ nvalue : " << Value ; 14 } 15 }; // koniec kl. Element 16 Element * First ; 17 public : 18 List () { First = 0; }; 19 void insert ( int v) { 20 First = new Element (v, First ); }; 21 void Info () { 22 Element * p = First ; 23 while (p) { 24 p-> Info (); 25 p=p-> Next (); 26 } } 27 }; // koniec kl. List Można umieścić deklarację klasy wewnątrz deklaracji innej klasy. Deklaracja klasy wewnętrznej pozostaje ukryta na zewnątrz klasy ją zawierającej. W związku z tym, używa się zagnieżdżonych deklaracji klas w przypadku, gdy reprezentują one część funkcjonalności wymaganą tylko w klasie zawierającej. 1 # include " Element.h" 2 3 int main () { 4 List list ; 5 list. insert (1); 6 list. insert (2); 7 list. insert (3); 8 list. Info (); 9 return 0; 10 } Rezultat działania: 1 lm@arch >./a. out 2 Value : 3 3 Value : 2 4 Value : 1

36 Tablice i ciągi obiektów Typy obiektowe (klasy) stosuje się identycznie w konstrukcji tablic i ciągów obiektów. 1 # include <stdio.h> 2 # include " Complex.h" 3 # define CNT int main () { 6 printf (" Create array of objects :"); 7 int i; 8 Complex arr [ CNT ]{{ "C1", 1., 1.}, 9 {"C2", 2., 2.}}; 10 printf ("\ ntheir info :"); 11 for (i =0; i<cnt ; i ++) 12 arr [i]. Info (); 13 printf ("\nde - allocate "); 14 return 0; 15 } W poleceniu kompilacji należy użyć opcji -std=c++0x: 1 lm@arch > g++ Complex. cpp ComplexArrays. cpp -std =c ++0 x 1 # include <stdio.h> 2 # include " Complex.h" 3 # define CNT int main () { 6 printf (" Create sequence of objects :"); 7 int i; 8 Complex * parr = new Complex [ CNT ] \ 9 {{"C1", 1., 1.}, {"C2", 2., 2.}}; 10 if ( parr ) { 11 printf ("\ ntheir info :"); 12 for (i =0; i<cnt ; i ++) 13 ( parr +i)-> Info (); 14 printf ("\nde - allocate :"); 15 delete [] parr ; 16 } 17 return 0; 18 } 1 Create array of objects : 2 C1: 0x7fffee21eaa0, ( 1.0, 1.0) created 3 C2: 0x7fffee21eac0, ( 2.0, 2.0) created 4 Their info : 5 C1: 0x7fffee21eaa0, ( 1.0, 1.0) 6 C2: 0x7fffee21eac0, ( 2.0, 2.0) 7 De - allocate 8 C2: 0x7fffee21eac0, ( 2.0, 2.0) deleted 9 C1: 0x7fffee21eaa0, ( 1.0, 1.0) deleted 36/116

37 37/116 Dziedziczenie Jest to mechanizm, który umożliwia definiowanie nowych typów jako potomków już istniejącego typu obiektowego. Funkcjonuje jeszcze inne określenie tej relacji: klasa bazowa - klasa pochodna. Istotą dziedziczenia jest przejmowanie pól i metod przodka przez jego potomka, z możliwością dodania własnych pól i metod lub zmiany znaczenia metod przodka (tzw. przedefiniowanie lub pokrycie). Przedefiniowanie polega na napisaniu metody o takiej samej nazwie i takim samym zbiorze parametrów formalnych, ale działającej odmiennie, a więc modyfikującej zachowanie obiektu. Metoda przedefiniowana w klasie potomnej może mieć dostęp do metody oryginalnej z nadklasy, np. w celu wywołania jej jako pewnej fazy swego działania (by nie powtarzać raz napisanego kodu). Relacja generalizacja-specjalizacja: klasa potomna jest tworzona w celu konkretyzacji funkcjonowania obiektu. Osiąga się to przez wprowadzenie nowych pól (dodatkowe parametry) i metod (dodatkowe lub zmienione operacje). Jest to zatem mechanizm coraz większej specjalizacji obiektu. Metody wirtualne stosuje się w celu wykorzystania kolejnego ważnego mechanizmu obiektowego - polimorfizmu. Polega on na zdefiniowaniu w obrębie ciągu klas od bazowej do pochodnej, rodziny metod o takich samych nazwach i zestawach parametrów formalnych, lecz o innej treści (a więc zmodyfikowanym działaniu). Każda z metod ma w nagłówku słowo kluczowe virtual i jej adres jest przechowywany w tablicy metod wirtualnych VMT. W zależności od tego na rzecz jakiego obiektu dana metoda jest wywołana, następuje wybór odpowiedniego jej aspektu. Odbywa się to dynamicznie - w czasie działania programu. Stąd mówi się o dynamicznym wiązaniu.

38 38/116 Dziedziczenie Relację generalizacja-specjalizacja na schematach struktury obiektowej przedstawia się specjalnym symbolem. Klasy TMessage, TTrigData, TDataVector i TDataDiscrete są klasami pochodnymi (potomkami) klasy bazowej TDataSegment (przodka). Klasy TTimeData, TFreqOctData i TFreqNBData są klasami pochodnymi (potomkami) klasy TDataVector.

39 39/116 Dziedziczenie - klasy pochodne Klasą pochodną jest klasa, która wywodzi się od innych klas, zwanych jej klasami bazowymi. Definicja klasy pochodnej ma ogólną postać: 1 class Nazwa : Lista_dziedziczenia 2 { 3 // Cialo klasy, w poznanej postaci 4 }; gdzie Lista_dziedziczenia jest ciągiem nazw zdefiniowanych wcześniej klas, każda z tych nazw może być poprzedzona kwalifikatorem wirtualności (słowo kluczowe virtual) lub dostępności (słowa kluczowe public, protected lub private). Jeśli nazwa pewnej klasy bazowej występuje w liście dziedziczenia klasy pochodnej, to każdy obiekt klasy pochodnej składa ze wszystkich składników tej klasy bazowej (dziedziczonych) i dodatkowo z własnych składników pól i metod. Jeśli posłużono się kwalifikatorem wirtualności, dana klasa bazowa jest odziedziczona wirtualnie w obiekcie klasy pochodnej występuje tylko jeden podobiekt (pole) reprezentujący tę klasę bazową. Reguły dostępności składników klasy bazowej prywatne składniki klasy bazowej są niedostępne w klasach pochodnych, niezależnie od sposobu dziedziczenia tej klasy jako całości. przy dziedziczeniu prywatnym (private) wszystkie składniki klasy bazowej stają się prywatnymi w klasie pochodnej przy dziedziczeniu chronionym (protected) składniki chronione klasy bazowej pozostają chronionymi w klasie pochodnej, a składniki publiczne stają się chronionymi, dziedziczenie publiczne klasy bazowej nie zmienia dostępności składników w klasie lub klasach pochodnych składniki publiczne pozostają publicznymi, a chronione pozostają chronionymi.

40 40/116 Dziedziczenie - klasy pochodne Klasa CDerived dziedziczy z trzech klas bazowych: wirtualnie i publicznie z klasy CBase1, w sposób chroniony z klasy CBase2 i prywatnie z klasy CBase3. 1 class CDerived : 2 virtual public CBase1, 3 protected CBase2, 4 private CBase3 5 { 6 // deklaracje wlasnych pol i metod, jak w kazdej klasie 7 }; Sens takiej deklaracji jest następujący: klasa CDerived łączy w sobie funkcjonalność zawartą w trzech klasach z których dziedziczy, a dodatkowo dostarcza funkcjonalności zdefiniowanej wewnątrz siebie za pomocą własnych pól i metod. Konstruktory klas pochodnych Konstruktory te, poza inicjatorami własnych pól mogą zawierać inicjatory podobiektów, czyli obiektów klas bazowych zawartych w danej klasie. Inicjator podobiektu ma postać: 1 Base ( lista_argumentow_inicjatora ) gdzie Base jest nazwą klasy bazowej, a lista_argumentow_inicjatora to ciąg wartości oddzielonych przecinkami, zgodny z listą argumentów odpowiedniego konstruktora klasy Base. Wartości te są nadawane poszczególnym argumentom konstruktora klasy Base, który w tym miejscu będzie wywoływany.

41 41/116 Dziedziczenie - klasy pochodne Kolejność tworzenia obiektu złożonego 1 tworzenie i inicjalizacja obiektów odziedziczonych wirtualnie 2 w kolejności określonej przez listę dziedziczenia tworzy się i inicjuje wszystkie podobiekty niewirtualnych klas bazowych 3 w kolejności określonej przez deklaracje pól klasy tworzy się i wstępnie inicjuje wszystkie pola obiektu 4 wykonuje się ciało konstruktora, gdzie ostatecznie inicjuje się (jeśli to konieczne) wybrane pola obiektu Destruktory klas pochodnych Niszczenie obiektu zachodzi w kolejności odwrotnej do jego tworzenia: 1 najpierw wykonuje się ciało destruktora 2 następnie w kolejności odwrotnej do określonej przez deklaracje pól klasy niszczy się jej pola obiektowe 3 następnie w kolejności odwrotnej do określonej przez listę dziedziczenia niszczy się wszystkie podobiekty niewirtualnych klas bazowych 4 następnie w kolejności odwrotnej do przyjętej w trakcie tworzenia obiektu niszczy się wszystkie podobiekty wirtualnych klas bazowych

42 42/116 Dziedziczenie - przykład 1 # include <iostream > 2 using namespace std ; 3 4 // klasa bazowa 5 class P2D { 6 protected : 7 int X, Y; 8 public : 9 P2D ( int x, int y) : X(x), Y(y) { 10 } 11 virtual void Info ( void ) { 12 cout << endl << "<P2D >" 13 << endl << " <X>" << X << " </X>" 14 << endl << " <Y>" << Y << " </Y>" 15 << endl << " </P2D >"; 16 } 17 }; 1 # include " P2D.h" 2 3 // klasa pochodna 4 class P3D : public P2D { 5 int Z; 6 public : 7 P3D ( int x, int y, int z) : 8 P2D (x, y), Z(z) { 9 } 10 void Info ( void ) { 11 cout << endl << "<P3D >"; 12 P2D :: Info (); 13 cout << endl 14 << " <Z>" << Z << " </Z>" 15 << endl << " </P3D >"; 16 } 17 }; 1 # include " P3D.h" 2 3 main () { 4 // dekl. obiektu p2 klasy P2D 5 P2D p2 (1, 1); 6 p2. Info (); 7 // dekl. obiektu p3 klasy P3D 8 P3D p3 (2, 2, 2); 9 p3. Info (); 10 return 0; 11 } 1 <P2D > 2 <X >1 </X> 3 <Y >1 </Y> 4 </P2D > 5 <P3D > 6 <P2D > 7 <X >2 </X> 8 <Y >2 </Y> 9 </P2D > 10 <Z >2 </Z> 11 </P3D >

43 43/116 Dziedziczenie - przykład nieco większej hierarchii klas 1 # include <iostream > 2 # ifndef _ZWIERZE_H 3 # define _ZWIERZE_H 4 class Zwierze { 5 protected : 6 char Glos [16]; 7 public : 8 Zwierze () { 9 } 10 virtual void DajGlos () { 11 std :: cout << Glos ; 12 } 13 }; 14 # endif /* _ZWIERZE_H */ 1 # include " Zwierze.H" 2 # ifndef _PIES_H 3 # define _PIES_H 4 class Pies : public Zwierze { 5 public : 6 Pies () : Zwierze () { 7 strcpy (Glos, " HAU!"); 8 } 9 void DajGlos () { 10 for ( int i = 0; i < 3; i ++) 11 Zwierze :: DajGlos (); 12 } 13 }; 14 # endif /* _PIES_H */ 1 # include " Zwierze.H" 2 # ifndef _KOT_H 3 # define _KOT_H 4 5 class Kot : public Zwierze { 6 public : 7 Kot () : Zwierze () { 8 strcpy (Glos, " MIAU!"); 9 } 10 }; 11 # endif /* _KOT_H */ 1 # include " Pies.H" 2 # ifndef _PIESMYSLI_H 3 # define _PIESMYSLI_H 4 // pies mysliwski 5 class PiesMysli : public Pies { 6 int IloscZdobyczy ; 7 public : 8 PiesMysli () : Pies () { 9 IloscZdobyczy = 0; 10 strcpy (Glos, " HAUuUu!"); 11 } 12 }; 13 # endif /* _PIESMYSLI_H */

44 44/116 Dziedziczenie - metody wirtualne i wskaźniki polimorficzne w akcji 1 # include < stdlib.h> 2 # include " Kot.H" 3 # include " PiesMysli.H" 4 int main ( int argc, char * argv []) { 5 // tablica wskaznikow typu klasy bazowej 6 Zwierze * rejestr [1024]; 7 for ( int i = 0; i < 1024; i ++) 8 rejestr [i] = 0; // inicjalizacja wsk. zero 9 Pies * p = new Pies (); 10 rejestr [0] = p; 11 Kot * k = new Kot (); 12 rejestr [1] = k; 13 PiesMysli * pm = new PiesMysli (); 14 rejestr [2] = pm; 15 // niech wszystkie dadza kolejno glos 16 for ( int i = 0; i < 1024; i ++) 17 if ( rejestr [i]!= 0) 18 rejestr [i]->dajglos (); 19 // usuwanie obiektow z pamieci 20 for ( int i = 0; i < 1024; i ++) 21 if ( rejestr [i]!= 0) 22 delete rejestr [i]; 23 std :: cout << std :: endl ; 24 return ( EXIT_SUCCESS ); 25 } Rezultat działania: 1 HAU! HAU! HAU! MIAU! HAUuUu! HAUuUu! HAUuUu!

45 Dziedziczenie - różnica między wiązaniem statycznym i dynamicznym Poniższe przykłady ilustrują różnice między wiązaniem statycznym i dynamicznym. Statyczne Dynamiczne 1 # include <iostream > 2 using namespace std ; 3 class A { 4 public : 5 void f() { 6 cout << " Klasa A" << endl ; } 7 }; 8 class B : A { 9 void f() { 10 cout << " Klasa B" << endl ; } 11 }; 12 void g(a& arg ) { 13 arg.f (); 14 } 15 int main () { 16 B x; 17 g(x); 18 } 1 # include <iostream > 2 using namespace std ; 3 class A { 4 public : 5 virtual void f() { 6 cout << " Klasa A" << endl ; } 7 }; 8 class B : A { 9 void f() { 10 cout << " Klasa B" << endl ; } 11 }; 12 void g(a& arg ) { 13 arg.f (); 14 } 15 int main () { 16 B x; 17 g(x); 18 } 1 Rezultat wykonania : Klasa A Gdy funkcja g jest wywoływana, wywołuje się funkcja A::f, mimo że argument wskazuje na obiekt typu B. W fazie kompilacji, kompilator wie tylko, że argument funkcji g jest referencją do obiektu typu pochodnego od A; nie może zaś określić typu rzeczywistego obiektu (A lub B). 1 Rezultat wykonania : Klasa B Typ aktualnego obiektu może być określony w trakcie wykonywania programu. Słowo virtual określa, że kompilator powinien wybrać odpowiedni aspekt funkcji f nie na podstawie typu referencji, ale na podstawie typu obiektu identyfikowanego przez tę referencję. 45/116

46 46/116 Klasy abstrakcyjne Służą one do zdefiniowania tzw. interfejsu, który będzie użyty we wszystkich klasach pochodnych wywodzących się z danej klasy bazowej. Interfejs Interfejs oznacza zbiór publicznie dostępnych metod danej klasy, za pomocą których można z poziomu innych obiektów lub z ciała funkcji globalnych wywoływać operacje zdefiniowane w klasie przez jej metody. Klasę abstrakcyjną w języku C++ definiuje się w sposób pośredni, poprzez zadeklarowanie co najmniej jednej funkcji składowej jako czysto wirtualnej, np.: virtual void fcw() = 0; Funkcja taka nie ma w danej klasie ciała, a więc nie jest znany jej kod. Dlatego nie można: utworzyć obiektu o typie zgodnym z typem klasy abstrakcyjnej (nie ma ona tej części funkcjonalności, jaka jest kojarzona z jej czysto wirtualnymi funkcjami) użyć klasy abstrakcyjnej jako typu parametru funkcji użyć klasy abstrakcyjnej jako typu zwracanej wartości użyć klasy abstrakcyjnej jako typu docelowego w przekształceniu typu (rzutowaniu).

47 47/116 Klasy abstrakcyjne Można jednak: deklarować wskaźniki i referencje do obiektów klasy abstrakcyjnej. użyć klas abstrakcyjnych jako nadklasy dla klas konkretnych. W trakcie dziedziczenia podklasy muszą zdefiniować wszystkie funkcje czysto wirtualne; dopiero w momencie zdefiniowania wszystkich f. czysto wirtualnych uzyskuje się klasę konkretną, dla której można utworzyć obiekt/y. Przykład System operacyjny powinien udostępniać w ograniczony sposób informacje o swoich sterownikach. W tym celu można zastosować klasę abstrakcyjną, która tylko definiuje interfejs do wszystkich sterowników, tym samym wymuszając ich zgodność: 1 class Device { 2 public : 3 virtual int open () = 0; 4 virtual int close () = 0; 5 virtual int read ( const char *) = 0; 6 virtual int write ( const char *) = 0; 7 virtual int ioctl (int,...) = 0; 8 //... 9 };

48 48/116 Zawieranie obiektów (relacja całość-część) Relacja całość-część określa zawieranie się jednych obiektów w innych. Na schematach struktury obiektowej stosuje się pokazane niżej oznaczenia, przy czym napisy 0-1 i 0..n oznaczają, że klasa Pojemnik, o ile istnieje jej obiekt, zawiera 0 lub więcej obiektów klasy Element jako tzw. pole obiektowe (obiekt zawarty w innym obiekcie).

49 49/116 Biblioteka strumieni we/wy C++ Biblioteka wejścia-wyjścia języka C++ pozwala na użycie abstrakcji nazywanej strumieniami (streams), przeznaczonej do wykonywania operacji we/wy na ciągach bajtów (pliki lub łańcuchy). Ta funkcjonalność jest realizowana przez szereg klas, pokazanych na diagramie z zaznaczeniem nazw ich plików nagłówkowych:

50 50/116 Klasa string - obsługa łańcuchów znaków 1 # include <iostream > 2 using namespace std ; 3 4 string dodaj () { 5 string firstlevel (" com "); 6 string secondlevel (" cplusplus "); 7 string scheme (" http :// "); 8 string hostname ; 9 string url ; hostname = " + secondlevel \ firstlevel ; 13 url = scheme + hostname ; 14 return url ; 15 } int main () { 18 string url = dodaj (); 19 cout << " url : " << url << endl ; 20 size_t found = url. find (" plus "); 21 if ( found!= string :: npos ); 22 url. erase (found, 4); 23 cout << " url : " << url << endl ; 24 if ( found!= string :: npos ); 25 url. erase (found, 4); 26 url. insert (found, "pp"); 27 cout << " url : " << url << endl ; 28 cout << " url : " << url. size () 29 << " characters " << endl ; 30 return 0; 31 } Standardowa Biblioteka C++ dostarcza klasę string, która obsługuje łańcuchy znaków i operacje na nich. Przykład pokazuje tylko wybrane konstrukcje, m.in: różne sposoby tworzenia obiektów, łączenie łańcuchów za pomocą funkcji operator+, wyszukiwanie, wstawianie i usuwanie łańcuchów, określanie długości, itd. Rezultat działania: 1 lm@arch >./a. out url : http :// www. cplusplus.com 2 url : http :// www. cplus.com 3 url : http :// www. cpp. com 4 url : 18 characters

51 51/116 Reagowanie na sytuacje wyjątkowe Nawet najlepiej zaprojektowany program zwykle posiada pewną liczbę (nieznaną) błędów, wynikających z błędnego modelu przetwarzania i z błędnej implementacji dobrego modelu. Ponadto program jest narażony na błędy obsługi i danych wejściowych. W celu zapewnienia odpowiedniego poziomu niezawodności oprogramowania, stosuje się dwa główne podejścia: tradycyjne, mające na celu zabezpieczenie się przed powstaniem sytuacji wyjątkowych, w rodzaju dzielenia przez zero, dostępu do elementu tablicy o niewłaściwym indeksie lub użycia niezainicjowanego wskaźnika. W tej metodzie bada się wartości odpowiednich zmiennych lub wyrażeń przed wykonaniem krytycznych operacji. Jest to jednak zawodna i uciążliwa technika, ponieważ wymaga ona przewidywania wszystkich anormalnych sytuacji, a ponadto generuje ciąg instrukcji sprawdzających niezależnie od rzeczywistej potrzeby na wszelki wypadek. oparte o mechanizm obsługi wyjątku. Lepiej i na ogół prościej jest zlecić wykonanie operacji, a dopiero w razie zaistnienia sytuacji wyjątkowej odpowiednio na nią zareagować. Ten ostatni problem nie jest prosty i nie ma ogólnego rozwiązania często nie wiadomo co zrobić w określonych sytuacjach awaryjnych i czy można kontynuować wykonywanie się programu wtedy lepiej zapisać dane i zakończyć program.

52 52/116 Reagowanie na sytuacje wyjątkowe Kategorie sytuacji wyjątkowych Wyróżnia się następujące kategorie sytuacji wyjątkowych: tzw. systemowe, generowane przez system operacyjny i/lub sprzęt. Należą do nich wyjątki dzielenia przez zero, niedomiaru lub nadmiaru zmiennopozycyjnego, dostępu do pamięci, itp. W ramach każdego systemu operacyjnego jest dokładnie określona lista generowanych (a zatem i obsługiwanych) wyjątków tego rodzaju. Np. w systemie Windows NT zaimplementowano system obsługi tej kategorii wyjątków pod nazwą Structured Exception Handling. programowe, o dowolnej potrzebnej kategorii. Kategoria ta zależy od przewidywanej przez programistę niesprawności kodu, np. nieodpowiednia ilość odczytanych danych, niespełnienie istotnego warunku integralności danych, brak danej w skończonym czasie timeout, itp. Wymienione kategorie wymagają odmiennej obsługi na poziomie kodu źródłowego. Jednak w obu wymienionych przypadkach należy zadbać o obsługę wszystkich możliwych do wystąpienia wyjątków, w przeciwnym razie normalną akcją oprogramowania systemowego jest awaryjne zakończenie wykonania programu, połączone z utratą nie zapisanych danych. Jednakowy jest za to mechanizm działania systemowych procedur obsługi wyjątku: zostaje przywrócony stan stosu programu, jaki istniał w momencie zgłoszenia wyjątku (tzw. stack unwinding), co oznacza usunięcie wszystkich zmiennych lokalnych utworzonych w funkcjach wywołanych od tego momentu (w tym obiektów, przy czym wykonują się ich destruktory).

53 53/116 Reagowanie na sytuacje wyjątkowe Obsługa wyjątków programowych Osiąga się to przez zastosowanie tzw. instrukcji nadzorującej o postaci: 1 try { 2 <Body > 3 } 4 <Handler1 > 5 <Handler2 > <HandlerN > gdzie Body jest ciałem instrukcji złożonej, a HandlerX jest definicją nienazwanej funkcji obsługi przewidzianej do wywołania w razie wystąpienia wyjątku. Postać funkcji obsługi: 1 catch ( Par ) { 2 Body 3 } Parametr Par jest deklarowany podobnie jak zwykły parametr funkcji, z podaniem typu i nazwy (to drugie jest opcjonalne konieczne wtedy, gdy wewnątrz funkcji obsługi używa się tego parametru). Jeśli nie wystąpią wyjątki, to wykonanie instrukcji Body polega na wykonaniu jej ciała i zignorowaniu wszystkich następujących po niej funkcji obsługi wyjątków, nie ma więc żadnych dodatkowych obciążeń związanych z istnieniem kodu nadzorującego.

54 54/116 Reagowanie na sytuacje wyjątkowe Natomiast jeśli w instrukcji nadzorującej zostanie wysłany wyjątek, co jest realizowane przez wykonanie instrukcji: throw Exp; // operacja wyslania wyjatku gdzie Exp jest pewnym wyrażeniem, to sterowanie jest przekazane do tej funkcji obsługi, której specyfikacja zgadza się z typem wyrażenia Exp. Po wykonaniu instrukcji zawartych w funkcji obsługi, nie jest kontynuowane wykonywanie się instrukcji nadzorującej. UWAGA: Jeśli wyjątek zostanie wysłany za pomocą throw, a nie dostarczono żadnej lub odpowiedniej funkcji obsługi, to wywoływana jest funkcja specjalna terminate(), która powoduje zakończenie programu. Specyfikowanie wyjątków dla funkcji Deklaracja każdej funkcji może być uzupełniona o specyfikację wyjątków, jakie może ona wygenerować. Specyfikacja ta ma postać: throw (Lista ) gdzie Lista jest listą oznaczeń dopuszczalnych wyjątków, np.: void fun(void) throw (complex, int ); Deklaracja ta ma następujące skutki: Podana funkcja może wygenerować co najwyżej wyjątki podanych typów. Jeśli w trakcie wykonywania tej funkcji wysłany byłby wyjątek typu nie zawartego w liście, to wywołana będzie specjalna funkcja unexpected(), która typowo powoduje zakończenie programu. Jeśli lista dopuszczalnych wyjątków jest pusta (występuje samo słowo throw), to funkcja nie może generować żadnych wyjątków.

55 55/116 Reagowanie na sytuacje wyjątkowe Przykład Funkcja operatorowa [] (czyli operator indeksowania), która może być zdefiniowana dla klasy Tablica, w celu zapewnienia dostępu do elementów przechowywanych w prywatnym polu tab typu tablicowego. Pozwoli to zrealizować odczyt lub zapis wartości. Funkcja ta generuje w pewnych sytuacjach wyjątek typu int, który zawiera wartość indeksu wykraczającą poza zakres tablicy. 1 T& operator []( int i) { 2 if (i < 0 i >= I) 3 throw i; // wyslanie wyjatku 4 return tab [i]; 5 } Wywołując tą funkcję, należy użyć kodu przedstawionego poniżej: 1 int main ( int argc, char * argv []) { 2 Tablica <char, 10> w; 3 ind = 10; 4 try { 5 w[ ind ] = A ; // proba dostepu 6 } 7 catch ( int i) { // obsluga wyjatku typu int 8 cerr << " Zly indeks tablicy : " << i << endl ; 9 return -1; 10 } 11 catch (...) { // wyjatek innego, nieznanego typu 12 cerr << " Nieznany wyjatek!"; 13 return -1; 14 } 15 return 0; 16 }

56 56/116 Reagowanie na sytuacje wyjątkowe Klasa wektora o zmiennej liczbie elementów Klasa ta jest podobna do klasy Tablica, ale udostępnia możliwość zmiany ilości elementów przechowywanych w instancji klasy. Wymaga to: zdefiniowania bufora w inny sposób niż w poprzednim przykładzie jako wskaźnika na typ zgodny z pierwszym parametrem szablonu, wprowadzenia w klasie pola m_size do przechowywania bieżącej ilości elementów, wprowadzenia w klasie metody SetSize do zmiany ilości elementów, która działa w ten sposób, że zapewnia przechowanie istniejących danych, wprowadzenia w klasie funkcji operatorowej operator [] (tzw. operator indeksowania), której zadaniem jest zapewnienie dostępu do elementów danych przechowywanych w obiekcie w sposób imitujący dostęp do tablicy. Funkcja ta posiada dodatkową i bardzo istotną zaletę: kontroluje ona poprawność dostępu do elementów wektora poprzez sprawdzanie wartości indeksu (numeru elementu). W przypadku indeksu wykraczającego poza dolny lub górny zakres, generowany jest wyjątek zawierający wartość tego indeksu, co pozwala po przechwyceniu wyjątku odpowiednio zareagować i zapobiega błędom adresowania. Usuwa to podstawowy problem dotyczący operacji na tablicach i wskaźnikach w językach C i C++, wiążący się z faktem, że tablice i wskaźniki nie przechowują informacji o rozmiarze pamięci przez nie zajmowanym lub adresowanym, zmian metod konstruktora i destruktora, które obecnie zajmują się dynamicznym zarządzaniem pamięcią, w której przechowywane są elementy wektora (przydział i zwalnianie).

57 Reagowanie na sytuacje wyjątkowe 1 template < class T, int I = 2> 2 class Wektor { 3 T* m_pdata ; 4 int m_size ; 5 public : 6 Wektor () { 7 m_pdata = new T[I]; 8 if ( m_pdata!= 0) m_size = I; 9 else m_size = 0; 10 } 11 virtual ~ Wektor () { 12 if ( m_pdata!= 0) 13 delete [] m_pdata ; 14 } 15 inline T& operator []( int i) { 16 if (i < 0 i >= m_size ) 17 throw i; 18 return m_pdata [i]; 19 } 20 bool SetSize ( int size ) { 21 if ( size == m_size ) return true ; 22 T * tmp = new T[ size ]; 23 if ( tmp == 0) return false ; 24 int i; 25 for (i = 0; i < ( size < m_size? size : m_size ); i ++) 26 tmp [i] = m_pdata [i]; 27 m_size = size ; 28 delete [] m_pdata ; 29 m_pdata = tmp ; 30 return true ; 31 } 32 }; 57/116

58 1 # include " Wektor.h" 2 # include < iostream > 3 using namespace std ; 4 5 int main ( int argc, char * argv []) { 6 Wektor <char, 16> v; 7 int krok = 0; 8 try { 9 cout << "v [15]= " << v [15] << endl ; krok ++; 10 v [15] = A ; krok ++; 11 cout << "v [15]= " << v [15] << endl ; krok ++; 12 v [16] = B ; krok ++; 13 cout << "v [16]= " << v [16] << endl ; krok ++; 14 } 15 catch ( int & i) { 16 cout << " WYJATEK " << i << ", KROK " << krok << endl ; 17 if (i < 0) 18 return -1; // calkiem zle, nic tu po nas 19 v. SetSize (i + 1); 20 switch ( krok ) { 21 case 0: 22 cout << "v [15]= " << v [15] << endl ; 23 case 1: 24 v [15] = A ; 25 case 2: 26 cout << "v [15]= " << v [15] << endl ; 27 case 3: 28 v [16] = B ; 29 case 4: 30 cout << "v [16]= " << v [16] << endl ; 31 } // end switch 32 } // end catch v [15]= v [15] = A WYJATEK 16, KROK v [16]= B /116

59 59/116 Reagowanie na sytuacje wyjątkowe Blok try obejmujący całość kodu funkcji - function try block Konstrukcji typu function try block używa się, aby umożliwić detekcję wyjątków w całym zakresie kodu funkcji: 1 void functiontryblock () try { 2 if (1) 3 throw 2; // generowanie wyjatku typu int 4 } 5 catch ( int & e) { 6 cout << " Exception thrown in functiontryblock (): " << e << endl ; 7 } 8 9 int main () { 10 functiontryblock (); 11 return 0; 12 } Rezultat działania: 1 Exception thrown in functiontryblock (): 2

60 60/116 Reagowanie na sytuacje wyjątkowe Obsługa wyjątków systemowych Wyjątki te są generowane automatycznie w trakcie działania programu (przez sprzęt, programy systemowe lub biblioteki języka C), więc bez udziału programisty, w związku z tym powstaje jedynie problem ich przechwycenia i odpowiedniego obsłużenia. Aby przechwycić ten rodzaj wyjątku, w systemach operacyjnych Microsoft Windows używa się kodu o ogólnej postaci: 1 _try { 2 Body 3 } 4 _except (< wyrazenie_filtrujace >) { // blok obslugi wyjatku 5 Handler 6 } gdzie Body jest ciałem instrukcji złożonej, w której spodziewane jest wystąpienie błędu, _except rozpoczyna blok obsługi tego błędu, wyrażenie zaś jest nazywane filtrującym, jako że jego wartość steruje sposobem obsługi błędu: EXCEPTION_CONTINUE_EXECUTION ( 1) - wyjątek jest odrzucony i program jest kontynuowany bez zmian od miejsca wysłania wyjątku EXCEPTION_CONTINUE_SEARCH (0) - wyjątek jest nieznany na danym poziomie, należy kontynuować poszukiwanie innego bloku jego obsługi EXCEPTION_EXECUTE_HANDLER (1) - wyjątek jest rozpoznany i rozpoczyna się jego obsługa poprzez skok do bloku obsługi zlokalizowanego za słowem _except. Wyrażenie filtrujące jest obliczane jako wyrażenie języka C. Jeśli jest wymagane bardziej skomplikowane przetwarzanie, można w tym miejscu wywołać funkcję, która zwraca jedną z wyżej wymienionych wartości.

61 61/116 Reagowanie na sytuacje wyjątkowe Identyfikatory wyjątków systemowych Wszystkie wyjątki obsługiwane przez system operacyjny zostały opatrzone odpowiednimi identyfikatorami. Poniżej podano identyfikatory wybranych wyjątków: EXCEPTION_ACCESS_VIOLATION - naruszenie dostępu do pamięci EXCEPTION_ARRAY_BOUNDS_EXCEEDED - niepoprawny indeks do tablicy EXCEPTION_FLT_DIVIDE_BY_ZERO - dzielenie przez zero zmiennoprzecinkowe EXCEPTION_FLT_OVERFLOW - nadmiar zmiennopozycyjny EXCEPTION_FLT_UNDERFLOW - niedomiar zmiennopozycyjny EXCEPTION_INT_DIVIDE_BY_ZERO - dzielenie przez zero całkowite EXCEPTION_INT_OVERFLOW - nadmiar całkowitoliczbowy EXCEPTION_IN_PAGE_ERROR - błąd strony.

62 62/116 Reagowanie na sytuacje wyjątkowe Funkcje związane z obsługą wyjątków systemowych GetExceptionCode() zwraca identyfikator wyjątku jako liczbę typu int GetExceptionInformation() - zwraca wskaźnik do struktury zawierającej dodatkową informację o wyjątku i o stanie procesora w momencie wystąpienia wyjątku sprzętowego. Struktura ma postać: 1 struct _EXCEPTION_POINTERS { 2 EXCEPTION_RECORD * ExceptionRecord, 3 CONTEXT * ContextRecord 4 } Przykład obsługi wyjątku typu naruszenie dostępu do pamięci 1 # include <iostream > 2 # include < windows.h> // dekl. EXCEPTION_ACCESS_VIOLATION 3 # include <excpt.h> 4 int filter ( unsigned int code, struct _EXCEPTION_POINTERS *ep) { 5 if ( code == EXCEPTION_ACCESS_VIOLATION ) 6 return EXCEPTION_EXECUTE_HANDLER ; 7 else 8 return EXCEPTION_CONTINUE_SEARCH ; 9 } 10 int main ( int argc, char * argv []) { 11 int * p = 0; 12 _try { 13 *p = 13; // spowoduje wyjatek typu naruszenie dostepu do pamieci 14 } 15 _except ( filter ( GetExceptionCode (), GetExceptionInformation ())) { 16 cout << endl << " Obsluga wyjatku!"; 17 } }

63 63/116 Reagowanie na sytuacje wyjątkowe Unifikacja obsługi wyjątków systemowych i programowych Dla ujednolicenia metod obsługi tych dwu kategorii wyjątków można zastosować następujące konstrukcje: 1 zdefiniować własną klasę lub rodzinę klas dedykowaną do obsługi wyjątków systemowych 2 zdefiniować własną funkcję dedykowaną do obsługi wyjątków systemowych, która musi być zgodna z poniższą specyfikacją: 1 typedef void (* _se_translator_function ) 2 ( unsigned int, struct _EXCEPTION_POINTERS *); Funkcja ta powinna tworzyć obiekt wyjątku odpowiedniego typu i wysyłać go instrukcją throw. 3 zainstalować tę funkcję dla każdego wątku programu, używając funkcji set_se_translator z argumentem wskazującym na własną funkcję dedykowaną do obsługi wyjątków systemowych. Od tego miejsca w momencie wystąpienia wyjątku systemowego będzie wywołana ta właśnie funkcja.

64 Reagowanie na sytuacje wyjątkowe Przykład zunifikowanej obsługi wyjątków systemowych i programowych 1 // kompilowac z opcja : / EHsc 2 # include <stdio.h> 3 # include < windows.h> 4 # include <eh.h> 5 6 class SE_Exception { 7 unsigned int nse ; 8 public : 9 SE_Exception ( unsigned int n) : nse (n) {} 10 ~ SE_Exception () {} 11 unsigned int SeNumber () { return nse ; } 12 }; 13 // funkcja uzytkowa 14 void Fun () { 15 char *p = 0; 16 *p = A ; 17 } 18 // wlasna funkcja obslugi wyjatkow systemowych 19 void Handler ( unsigned int u, EXCEPTION_POINTERS * pexp ) { 20 cout << "f. Handler ()" << endl ; 21 throw new SE_Exception (u); // utworzenie i wyslanie wyjatku 22 } 23 int main ( int argc, char * argv []) { 24 try { 25 _set_se_translator ( Handler ); 26 Fun (); 27 } 28 catch ( SE_Exception * e) { 29 cout << " Zlapano wyjatek systemowy " << e->senumber (); 30 delete e; 31 } } 64/116

65 Reagowanie na sytuacje wyjątkowe Standardowe klasy wyjątków Zostały one zdefiniowane w bibliotece standardowej C++ i są gotowe do użycia lub mogą stanowić podstawę do tworzenia klas pochodnych. Wszystkie te klasy dziedziczą z klasy exception, zdefiniowanej w pliku nagłówkowym <exception>. Dzielą się na 2 kategorie, zdefiniowane w pliku nagłówkowym <stdexcept>: logic_error, która reprezentuje błędy w logice programu, takie jak przekazanie niepoprawnego argumentu. runtime_error, która reprezentuje błędy pojawiające się jako rezultat nieprzewidzianych zdarzeń w trakcie wykonywania programu, takich jak awaria sprzętu lub brak pamięci. Zarówno runtime_error jak i logic_error dostarczają konstruktora, który pobiera argument typu std::string, co umożliwia wprowadzenie opisu wyjątku w reprezentującym go obiekcie i poźniejsze odczytanie go przy użyciu metody exception::what, co ilustruje poniższy program. 1 # include <stdexcept > 2 # include <iostream > 3 using namespace std ; 4 class MojWyjatek : public runtime_error { 5 public : 6 MojWyjatek ( const string & opis = "") : runtime_error (opis ) {} 7 }; 8 int main ( int argc, char * argv []) { 9 try { 10 throw MojWyjatek (" Wyjatek 007 "); 11 } 12 catch ( MojWyjatek & x) { 13 cout << x. what () << endl ; 14 } } 65/116

66 66/116 Reagowanie na sytuacje wyjątkowe Standardowe klasy wyjątków Klasy wyjątków dziedziczące z logic_error domain_error Naruszenie warunku wstępnego invalid_argument Niepoprawny argument podany przy wywołaniu funkcji length_error Próba wygenerowania obiektu o rozmiarze większym niż maksymalna wa out_of_range Argument spoza zakresu bad_cast Niepoprawne wyrażenie z użyciem dynamic_cast bad_typeid Wystąpienie wskaźnika p o wartości NULL w wyrażeniu typeid(*p) Klasy wyjątków dziedziczące z runtime_error range_error Wynik operacji zmiennoprzecinkowej jest za duży lub za mały overflow_error Wystąpienie nadmiaru przy operacji arytmetycznej bad_alloc Wystąpienie błędu przy próbie alokacji pamięci

67 Zapis i odczyt danych w trybie binarnym Zapis i odczyt złożonych danych Metody służące do zapisu i odczytu danych w trybie binarnym używają wskaźnika typu char, ale nie znaczy to, że ich zastosowanie ogranicza się do tablic znaków. Można obsługiwać dane dowolnych typów, posługując się rzutowaniem: 1 # include < iostream > 2 # include <fstream > 3 using namespace std ; 4 class Data { 5 int key=0; // non - static data member initializers 6 double value =0.; // only available with -std=c public : 8 Data () { }; 9 Data ( int k, double v) : key (k), value (v) { }; 10 friend ostream & operator <<( ostream & os, const Data & st ); 11 }; 12 ostream & operator <<( ostream & os, const Data & d) { 13 os << "key : " << d.key << ", value " << d. value ; 14 return os; 15 } 16 int main () { 17 Data pi (314, 3.14), pi2 ; 18 cout << endl << pi; 19 ofstream fout ("Data.bin ", ios :: out ios :: binary ); 20 fout. write (( char *)&pi, sizeof (Data )); 21 fout. close (); 22 ifstream fin ("Data.bin ", ios :: in ios :: binary ); 23 fin.read (( char *)& pi2, sizeof (Data )); 24 fin. close (); 25 cout << endl << pi2 ; key : 314, value key : 314, value /116

68 Zapis i odczyt danych w trybie binarnym 1 # include <iostream > 2 # include <fstream > 3 using namespace std ; 4 5 class Student { 6 char Name [32]; 7 int Grade ; 8 public : 9 friend istream & operator >>( istream & is, Student & st ); 10 friend ostream & operator <<( ostream & os, const Student & st ); 11 friend ifstream & operator >>( ifstream & is, Student & st ); 12 friend ofstream & operator <<( ofstream & is, const Student & st ); 13 }; 14 istream & operator >>( istream & is, Student & st) { 15 cout << " Enter student name :" << endl ; 16 is >> st. Name ; 17 cout << " Enter student grade :" << endl ; 18 is >> st. Grade ; 19 return is; 20 } 21 ostream & operator <<( ostream & os, const Student & st) { 22 os << endl << " Student " << st.name << ": Grade " << st. Grade ; 23 return os; 24 } 25 ifstream & operator >>( ifstream & is, Student & st) { 26 is.read (( char *) &st, sizeof ( Student )); 27 return is; 28 } 29 ofstream & operator <<( ofstream & os, const Student & st) { 30 os. write (( char *) &st, sizeof ( Student )); 31 return os; 32 } 68/116

69 Zapis i odczyt danych w trybie binarnym 33 void save ( Student & s) { 34 ofstream fout (" student.dat ", ios :: binary ); 35 if (fout. is_open ()) { 36 char ans = y ; 37 do { 38 cin >> s; 39 fout << s; 40 cout << endl << " Continue? "; 41 cin >> ans ; 42 } while ( ans == y ); 43 fout. close (); 44 } } 45 void read ( Student & s) { 46 ifstream fin (" student.dat ", ios :: binary ); 47 if ( fin. is_open ()) { 48 while (! fin. eof ()) { 49 fin >> s; 50 if (! fin. eof ()) 51 cout << s; 52 } 53 fin. close (); 54 } } 55 int main ( int argc, char * argv []) { 56 char ans ; 57 Student stud ; 58 cout << endl << " Choose <R> - Read, <W> - Write? "; 59 cin >> ans ; 60 if ( ans == r ans == R ) 61 read ( stud ); 62 if ( ans == w ans == W ) 63 save ( stud ); 64 return 0; 65 } 69/116

70 70/116 Inteligentne wskaźniki Dla wspierania nowoczesnego programowania w C++, bibliteka STL udostępnia klasy inteligentnych wskaźników (smart pointers), które pozwalają uniknąć przecieków pamięci i zasobów, a także zapewniają odpowiednią obsługę w obecności wyjątków. Inteligentne wskaźniki w Bibliotece Standardowej C++ Inteligentne wskaźniki są podstawowym sposobem opakowania zwykłych wskaźników w obiekty języka C++. unique_ptr (deklaracja w pliku <memory>) Pozwala na zdefiniowanie dokładnie jednego obiektu opakowującego zwykły wskaźnik. Jest to podstawowy wybór, o ile nie przewiduje się współużywania zwykłego wskaźnika -> wtedy stosuje się shared_ptr. Możliwe jest przeniesienie do innego obiektu, ale nie kopiowanie czy współużywanie. Klasa unique_ptr jest mała i efektywna; obiekt ma rozmiar zwykłego wskaźnika. shared_ptr (deklaracja w pliku <memory>) Klasa inteligentnego wskaźnika zliczająca ilość odwołań do niego z różnych miejsc programu. Używa się jej gdy zwykły wskaźnik musi być widoczny z wielu miejsc w kodzie, na przykład gdy zwraca się kopię wskaźnika z pojemnika, ale należy zachować oryginał. Zwykły wskaźnik nie jest usuwany do momentu, gdy wszystkie zawierające go obiekty typu shared_ptr zostaną usunięte lub przekażą swoją zawartość. Rozmiar jest równy rozmiarowi dwu wskaźników; jeden do obiektu, drugi do wspólnego bloku kontrolnego, który przechowuje ilość odwołań. weak_ptr (deklaracja w pliku <memory>) Specjalny rodzaj używany łącznie z klasą shared_ptr. weak_ptr zapewnia dostęp do obiektu identyfikowanego przez jeden lub więcej obiektów shared_ptr, bez związku z ilością odwołań. Używany gdy należy obserwować obiekt, ale nie wpływać na jego istnienie.

71 Inteligentne wskaźniki - klasa unique_ptr jako typ zmiennej lokalnej 1 # include <memory > 2 # include "../ Complex / Complex.h" 3 4 void UseRawPointer () { 5 // Using a raw pointer -- not recommended. 6 Complex * p = new Complex (" Raw ", 1., 1.); 7 p-> Info (); 8 // Don t forget to delete! 9 if (p) delete p; 10 } void UseSmartPointer () { 13 // Declare a smart pointer on stack and pass it the raw pointer. 14 std :: unique_ptr <Complex > p( new Complex (" Smart ", 2., 2.)); 15 p-> Info (); 16 } // p is deleted automatically here int main () { 19 UseRawPointer (); 20 UseSmartPointer (); 21 return 0; 22 } Jak widać, unique_ptr jest klasą szablonową, która jest użyta do deklaracji zmiennej lokalnej inicjalizowanej zwykłym wskaźnikiem, który zna adres obiektu ze sterty. Po inicjalizacji unique_ptr posiada zwykły wskaźnik. Znaczy to, że unique_ptr jest odpowiedzialny za usunięcie bloku pamięci, który zajmuje obiekt wskazywany przez zwykły wskaźnik. Destruktor klasy unique_ptr zawiera wywołanie operatora delete, a ponieważ unique_ptr jest na stosie, destruktor jest wywoływany gdy obiekt wychodzi z zakresu (bloku), nawet gdy nastąpi sytuacja wyjątkowa. 71/116

72 72/116 Inteligentne wskaźniki - klasa unique_ptr jako typ pola klasy 1 # include <memory > 2 # include "../ Complex / Complex.h" 3 4 using namespace std ; 5 6 class WithUniquePtrs { 7 unique_ptr <Complex > p1; 8 public : 9 WithUniquePtrs ( const char * name, double re, double im) 10 : p1( new Complex (name, re, im )) { 11 } 12 }; int main () { 15 WithUniquePtrs w("p1", 1., 1.); 16 return 0; 17 } 1 P1: 0 x , (1.0, 1.0) created 2 P1: 0 x , (1.0, 1.0) deleted Pole p1 klasy ClassWithUniquePtrs typu unique_ptr<complex> jest przeznaczone do przechowywania i zarządzania obiektem typu Complex. Po inicjalizacji, która odbywa się jeszcze przed wykonaniem kodu konstruktora, unique_ptr posiada zwykły wskaźnik do obiektu typu Complex. Ponieważ w trakcie wykonywania destruktora klasy ClassWithUniquePtrs wywołany zostanie destruktor pola p1, automatycznie wykona się destruktor obiektu klasy unique_ptr, usuwając zarządzany obiekt typu Complex.

73 73/116 Inteligentne wskaźniki - klasa shared_ptr Klasa shared_ptr jest zaprojektowana dla scenariuszy, w których czas życia obiektu jest zarządzany z wielu miejsc programu. Po inicjalizacji obiektu shared_ptr można go kopiować, przekazywać jako argument do funkcji i przypisywać do innych instancji shared_ptr. Wszystkie te instancje odwołują się do tego samego obiektu i wspólnie używają tego samego bloku kontrolnego, który śledzi ilość obiektów nadzorujących docelowy obiekt. Kiedy ilość obiektów nadzorujących spada do zera, blok kontrolny usuwa docelowy obiekt i samego siebie.

74 Inteligentne wskaźniki - klasa shared_ptr 1 # include <memory > 2 # include "../ Complex / Complex.h" 3 using namespace std ; 4 5 void SmartInfo ( const char * name, shared_ptr <Complex >& ptr ) { 6 ptr ->Info (); cout << " <- " << name << ", CNT : " << ptr. use_count (); 7 } 9 int main () { 10 Complex * c = new Complex ("c", 1., 1.); 11 shared_ptr <Complex > p1(c); 12 shared_ptr <Complex > p2 = p1; 13 // p1 and p2 now own the same object 14 SmartInfo ("p1", p1 ); 15 SmartInfo ("p2", p2 ); 16 cout << endl << " p1. reset ()"; 17 p1. reset (); 18 // Object still exists, due to p2 19 if (p1) 20 SmartInfo ("p1", p1 ); 21 if (p2) 22 SmartInfo ("p2", p2 ); 23 cout << endl << " p2. reset ()"; 24 p2. reset (); // Object is deleted, 25 // as the last owner was reset 26 if (p2) 27 SmartInfo ("p2", p2 ); 28 else 29 cout << " <- p2, CNT : " 30 << p2. use_count (); 31 return 0; 32 } c: 0 x11d9010,(1.0,1.0) created c: 0 x11d9010,(1.0,1.0) <- p1, CNT : 2 c: 0 x11d9010,(1.0,1.0) <- p2, CNT : 2 p1. reset () c: 0 x11d9010,(1.0,1.0) <- p2, CNT : 1 p2. reset () c: 0 x11d9010,(1.0,1.0) deleted <- p2, CNT : 0 Kompilacja i łączenie wymaga opcji -std=c > g++ main. cpp Complex. cpp -std =c /116

75 75/116 Dynamiczne struktury danych Dynamiczne struktury danych są to struktury, które charakteryzują się zmienną ilością elementów, zmienną konfiguracją wzajemną lub zmiennymi typami. Należą do nich: wektor (vector) stos (stack) kolejka (queue) lista (list) zbiór (set) drzewo (tree) mapa, słownik (map, dictionary) graphs and others. Każda z wymienionych kategorii cechuje się odmienną organizacją wewnętrzną, sposobem dostępu do elementów i zastosowaniem. Dynamiczne struktury danych są implementowane przez biblioteki C++, takie jak: Standard C++ Lib, Boost, Qt, itp.

76 76/116 Dynamiczne struktury danych Standardowa biblioteka C++ Jest to biblioteka klas, które dostarczają implementacji podstawowych algorytmów i struktur danych potrzebnych w zastosowaniach. Zawiera następujące pakiety klas: The Language Support Library The Diagnostics Library The General Utilities Library The Standard String Templates Localization Classes and Templates The Containers, Iterators and Algorithms Libraries (the Standard Template Library) The Standard Numerics Library The Standard Input/Output Library C++ Headers for the Standard C Library Zastosowanie STL do dynamicznych struktur danych STL jest biblioteką C++ implementującą pojemniki, algorytmy i iteratory; dostarcza ona wielu podstawowych konstrukcji stosowanych w informatyce. STL jest to tzw. biblioteka generyczna, co oznacza, że jej składniki są sparametryzowane za pomocą szablonów i dzięki temu współpracują z typami wbudowanymi w język, typami wbudowanymi w samą bibliotekę i dowolnymi typami zdefiniowanymi przez użytkownika.

77 77/116 Dynamiczne struktury danych Główne składniki STL Klasy pojemników Ich przeznaczeniem jest przechowywanie obiektów dowolnych typów. STL zawiera klasy vector, list, queue, deque, set, multiset, map, multimap, hash_set, hash_multiset, hash_map, and hash_multimap. Każda z tych klas jest szablonem, którego instancja może reprezentować dowolny typ. Na przykład, można użyć klasy vector<int> do przechowywania ciągu danych całkowitych (jak w tablicy), z tym że vector eliminuje problemy związane z dynamicznym zarządzaniem pamięcią: 1 vector <int > v (3); // Deklaracja wektora o 3 elementach 2 v [0] = 7; 3 v [1] = v [0] + 3; 4 v [2] = v [0] + v [1]; // v [0] == 7, v [1] == 10, v [2] == 17 5 // dodanie nowych elementow 6 v. push_back (11); 7 v. push_back (12); 8 v. push_back (13); 9 // usuniecie pierwszego elementu 10 v. pop_front ();

78 Dynamiczne struktury danych Algorytmy Algorytmy wykonują operacje na danych przechowywanych w pojemnikach. Na przykład, aby odwrócić uporządkowanie elementów w obiekcie klasy vector, można użyć algorytmu reverse. 1 reverse (v. begin (), v.end ()); // v [0] == 17, v [1] == 10, v [2] == 7 Można zauważyć, że reverse jest funkcją globalną, a nie składową. Ponadto, wymaga ona dwu argumentów, ponieważ wykonuje działanie na elementach z zadanego zakresu, a nie na pojemniku. W tym szczególnym przypadku zakres jest całym pojemnikiem v. Uzasadnienie dla tych rozwiązań: funkcja reverse, podobnie jak inne algorytmy STL, jest niezależna od klas pojemników. Oznacza to, że reverse można użyć nie tylko do zmiany porządku elementów wektora, ale również listy, a nawet elementów tablic, na przykład: 1 const int N =5; 2 double A[N] = { 1.1, 1.2, 1.3, 1.4, 1.5 }; 3 reverse (A, A + N); 4 int i; 5 for (i = 0; i < N; ++i) 6 cout << "A[" << i << "] = " << A[i]; Rezultat działania: 1 A [0] = A [1] = A [2] = A [3] = A [4] = 1.1 Podobnie jak przy odwracaniu porządku elementów wektora, użyto tu zakresu. Pierwszy argument reverse jest wskaźnikiem do początku zakresu, a drugi argument wskazuje jeden element za końcem zakresu. Zakres wynosi [A, A + N); w notacji asymetrycznej. Inne algorytmy implementowane przez STL to: insert, copy, remove, sort, find, etc. 78/116

79 Dynamiczne struktury danych Iteratory Iteratory zapewniają dostęp do elementów pojemnika bez wnikania w jego wewnętrzną organizację. Iteratory są uogólnieniem wskaźników. Wskaźniki są więc szczególnym przypadkiem iteratora, dlatego można użyć algorytmu reverse do elementów tablicy. Podobnie, vector deklaruje wbudowane typy iterator i const_iterator. W powyższym przykładzie, typ zwracany przez v.begin() i v.end() to vector<int>:: iterator. Iteratory pozwalają uniezależnić algorytmy od pojemników: algorytmy są szablonami, parametryzowanymi typem iteratora, więc nie są ograniczone do typu pojemnika. Na przykład, liniowe przeszukiwanie zakresu, implementowane w STL przez algorytm find: 1 template <class Iterator, class T> 2 Iterator find ( Iterator first, Iterator last, const T& value ) { 3 while ( first!= last && * first!= value ) ++ first ; 4 return first ; 5 } find ma trzy argumenty: dwa iteratory definiujące zakres, i wartość poszukiwaną w zakresie. find sprawdza każdy obiekt wskazywany przez iterator w zakresie [first, last), idąc od początku do końca, zatrzymuje się gdy znajdzie iterator wskazujący value lub gdy dojdzie do końca zakresu. first i last są zadeklarowane jako typ Iterator, który jest parametrem szablonu. Tzn, że nie ma konkretnego typu Iterator: przy każdym wywołaniu find, kompilator zastępuje parametry Iterator i T aktualnymi typami argumentów. Na przykład, jeśli dwa pierwsze argumenty find są typu int i trzeci jest typu int, to w istocie wywołana będzie funkcja: 1 int * find ( int * first, int * last, const int & value ) { 2 while ( first!= last && * first!= value ) ++ first ; 3 return first ; 4 } 79/116

80 80/116 Dynamiczne struktury danych Dostępne są zwykle iteratory: iterator swobodny dostęp do elementu dla zapewnienia odczytu lub modyfikacji jego wartości const_iterator swobodny dostęp do elementu dla zapewnienia odczytu jego wartości. Próba modyfikacji elementu udostępnionego tym iteratorem skutkuje błędem. reverse_iterator jak iterator, ale dostęp jest realizowany w kolejności odwrotnej const_reverse_iterator jak const_iterator, ale dostęp jest realizowany w kolejności odwrotnej

81 Dynamiczne struktury danych Zastosowanie klasy vector do elementów o typach standardowych Pojemnik typu vector przechowuje dane w liniowym obszarze pamięci, co zapewnia swobodny dostęp do nich można ten pojemnik indeksować liczbą całkowitą, podobnie jak zwykłe tablice. Niestety, wstawienie nowego elementu gdziekolwiek indziej, niż na końcu jest operacją liniowego czasu, gdyż trzeba "rozsunąć" elementy, żeby zrobić miejsce na nowy. Ilość elementów może się zmieniać, dzięki automatycznemu zarządzaniu pamięcią. 1 # include <iostream > 2 # include <vector > 3 # include <algorithm > 4 using namespace std ; 5 void print ( vector <int >& v) { 6 vector <int >:: const_iterator it; 7 cout << "v: [ "; 8 for (it=v. begin (); it!=v.end (); it++) 9 cout << *it << " "; 10 cout << "]" << endl ; 11 } 12 int main ( int argc, char * argv []) { 13 // Initially v contains 0 elements. 14 vector <int > vec ; 15 // Insert an element 1 at the end 16 vec. push_back (1); 17 // Insert 2 other elements at the end 18 vec. push_back (2); 19 vec. push_back (0); print (vec ); 20 sort (vec. begin (), vec.end ()); print (vec ); 21 // remove the last element 22 vec. pop_back (); print (vec ); 23 } 19 v: [ ] 20 v: [ ] v: [ 0 1 ] /116

82 82/116 Dynamiczne struktury danych vector jako pojemnik przechowujący wskaźniki do obiektów 1 # include <iostream > 2 # include <vector > 3 # include "../ Complex / Complex.h" 4 using namespace std ; 5 void print ( vector < Complex * >& v) { 6 // declaration of a suitable iterator 7 vector <Complex * >:: const_iterator it; 8 cout << endl << "# vector contains " 9 << v. size () << " elements :"; 10 for (it=v. begin (); it!=v.end (); it++) 11 cout << endl << ** it; 12 } 13 void erase ( vector < Complex * >& v, int i) 14 { 15 vector <Complex * >:: iterator it; 16 it=v. begin ()+i; 17 if (it!= v. end ()) { 18 delete *it; 19 v. erase (it ); 20 } 21 } 22 void erase ( vector < Complex * >& v) { 23 while (v. size ()) { 24 Complex * c = v. back (); 25 if (c) 26 delete c; 27 v. pop_back (); 28 } 29 } 30 int main ( int argc, char * argv []) { 31 vector <Complex *> v; 32 Complex * c; 33 v. push_back ( new Complex ("1",1.,1.)); 34 c = new Complex ("2",2.,2.); 35 v. push_back (c); 36 v. push_back ( new Complex ("3",3.,3.)); 37 print (v); 38 erase (v, 1); // remove the 2nd element 39 print (v); 40 erase (v); // remove all elements 41 print (v); 42 return 0; 43 } 1 1: 0xd96010, (1.0, 1.0) created 2 2: 0xd96060, (2.0, 2.0) created 3 3: 0xd960b0, (3.0, 3.0) created 4 vector contains 3 elements : 5 Re =1, Im =1 6 Re =2, Im =2 7 Re =3, Im =3 8 2: 0xd96060, (2.0, 2.0) deleted 9 vector contains 2 elements : 10 Re =1, Im =1 11 Re =3, Im =3 12 3: 0xd960b0, (3.0, 3.0) deleted 13 1: 0xd96010, (1.0, 1.0) deleted 14 vector contains 0 elements :

83 83/116 Dynamiczne struktury danych vector jako pojemnik przechowujący obiekty 1 # include <iostream > 2 # include <vector > 3 # include "../ Complex / Complex.h" 4 using namespace std ; 5 void print ( vector <Complex >& v) { 6 // declaration of a suitable iterator 7 vector <Complex >:: const_iterator it; 8 cout << endl << " vector contains " 9 << v. size () << " elements :"; 10 for (it=v. begin (); it!=v.end (); it++) 11 cout << endl << *it; 12 } 13 void erase ( vector <Complex >& v, int i) { 14 vector <Complex >:: iterator it; 15 it=v. begin ()+i; 16 if (it!= v. end ()) { 17 v. erase (it ); 18 } 19 } 20 int main ( int argc, char * argv []) { 21 vector <Complex > v; 22 Complex c("1",1.,1.); 23 v. push_back (c); 24 c. Set ("2", 2., 2.); 25 v. push_back (c); 26 c. Set ("3", 3., 3.); 27 v. push_back (c); 28 print (v); 29 erase (v, 1); // remove the 2nd element 30 print (v); 31 v. clear (); // remove all elements 32 print (v); 33 return 0; 34 } 1 1: 0 x7fffa13c2d60, (1.0, 1.0) created 2 1: 0x1e8f010, (1.0, 1.0) created 3 2: 0x1e8f060, (2.0, 2.0) created 4 1: 0x1e8f040, (1.0, 1.0) created 5 1: 0x1e8f010, (1.0, 1.0) deleted 6 3: 0x1e8f0d0, (3.0, 3.0) created 7 1: 0x1e8f090, (1.0, 1.0) created 8 2: 0x1e8f0b0, (2.0, 2.0) created 9 1: 0x1e8f040, (1.0, 1.0) deleted 10 2: 0x1e8f060, (2.0, 2.0) deleted 11 vector contains 3 elements : 12 Re =1, Im =1 13 Re =2, Im =2 14 Re =3, Im =3 15 3: 0x1e8f0d0, (3.0, 3.0) deleted 16 vector contains 2 elements : 17 Re =1, Im =1 18 Re =3, Im =3 19 1: 0x1e8f090, (1.0, 1.0) deleted 20 2: 0x1e8f0b0, (2.0, 2.0) deleted 21 vector contains 0 elements : 22 3: 0 x7fffa13c2d60, (3.0, 3.0) deleted

84 Dynamiczne struktury danych vector jako przykład kompletnego standardowego pojemnika Dalej dyskutowana jest klasa vector: typy wbudowane, iteratory, dostęp do elementów, konstruktory, operacje naśladujące stos i listę. Klasa vector jest szablonem zdefiniowanym w przestrzeni nazw std w pliku <vector>. Typy wbudowane w klasę vector W pierwszej kolejności zdefiniowano zbiór standardowych nazw typów: 1 template <class T, class A = allocator <T> > 2 class std :: vector { 3 public : 4 // types : 5 typedef T value_type ; // type of element 6 typedef A allocator_type ; // type of memory manager 7 typedef typename A:: size_type size_type ; 8 typedef typename A:: difference_type difference_type ; 9 typedef implementation_dependent1 iterator ; // T* 10 typedef implementation_dependent2 const_iterator ; // const T* 11 typedef std :: reverse_iterator <iterator > reverse_iterator ; 12 typedef std :: reverse_iterator < const_iterator > const_reverse_iterator ; 13 typedef typename A:: pointer pointer ; // pointer to element 14 typedef typename A:: const_pointer const_pointer ; 15 typedef typename A:: reference reference ; // reference to element 16 typedef typename A:: const_reference const_reference ; 17 // }; Każdy standardowy pojemnik definiuje te nazwy typów w specyficzny, najbardziej odpowiedni do implementacji sposób. Typ elementów pojemnika jest pierwszym parametrem szablonu i ma nazwę value_type. 84/116

85 85/116 Dynamiczne struktury danych Wbudowane nazwy typów pozwalają pisać kod wykorzystujący pojemniki bez znajomości aktualnych typów elementów. W szczególności, można pisać kod poprawnie współdziałający z każdym pojemnikiem. Na przykład, poniżej zdefiniowana metoda sum może służyć do obliczania sumy elementów dowolnego pojemnika: 1 # include <iostream > 2 # include <vector > 3 # include <list > 4 using namespace std ; 5 6 template < class C> 7 typename C:: value_type sum ( const C& c) { 8 typename C:: value_type s = 0; 9 typename C:: const_iterator p; 10 // start at the beginning 11 p = c. begin (); 12 // continue until the end 13 while (p!=c. end ()) { 14 s += *p; // get value of element 15 ++p; // make p point to next element 16 } 17 return s; 18 } 20 int main () { 21 vector <int > vec ; // vector of ints 22 // Insert an element 1 at the end 23 vec. push_back (1); 24 // Insert 2 other elements 25 vec. push_back (2); 26 vec. push_back (3); 27 cout << endl << " sum of VECTOR : " 28 << sum ( vec ); 29 // LIST 30 list <int > lst ; // list of ints 31 lst. push_back (1); 32 lst. push_back (2); 33 lst. push_back (3); 34 cout << endl << " sum of LIST : " 35 << sum ( lst ); 36 return 0; 37 } 1 sum of VECTOR : 6 2 sum of LIST : 6

86 86/116 Dynamiczne struktury danych Iteratory klasy vector Iteratory są używane do nawigacji między elementami pojemnika bez znajomości typów przechowywanych w nim elementów. Klasy pojemników posiadają metody pozwalające uzyskać dostęp do elementów na końcach sekwencji: 1 template <class T, class A = allocator <T> > 2 class std :: vector { 3 public : 4 // iterators : 5 iterator begin (); // points to first element 6 const_iterator begin () const ; 7 iterator end (); // points to one -past - last element 8 const_iterator end () const ; 9 reverse_iterator rbegin (); // points to first element of reverse sequence 10 const_reverse_iterator rbegin () const ; 11 reverse_iterator rend (); // points to one -past - last element of reverse sequence 12 const_reverse_iterator rend () const ; 13 // }; Para metod begin()/end() pozwala udostępnić elementy pojemnika w naturalnej kolejności. Tzn., element 0, element 1, element 2, itd. Para metod rbegin()/rend() pozwala udostępnić elementy pojemnika w odwrotnej kolejności. Tzn., element n-1, element n-2, element n-3, itd.

87 87/116 Dynamiczne struktury danych Konstruktory klasy vector vector posiada kompletny zestaw konstruktorów, destruktor i operatory kopiowania: 1 template <class T, class A = allocator <T> > 2 class std :: vector { 3 public : 4 //... 5 // constructors, etc.: 6 explicit vector ( const A& = A ()); 7 explicit vector ( size_type n, const T& val = T(), const A& = A ()); // n copies of val 8 template < class In > // In must be an input iterator 9 vector (In first, In last, const A& = A ()); // copy from [ first :last ) 10 vector ( const vector & x); 11 ~ vector () ; 12 vector & operator =( const vector & x); 13 template < class In > // In must be an input iterator 14 void assign (In first, In last ); // copy from [ first :last ) 15 void assign ( size_type n, const T& val ); // n copies of val 16 // }; vector zapewnia szybki dostęp do wybranego elementu, ale zmiana ilości elementów jest kosztowna. W konsekwencji, dobrze jest ustalić ilość elementów w trakcie tworzenia wektora, na przykład: 1 vector <Record > vr (1024); 2 void f( int n1, int n2) { 3 vector <int > vi(n1 ); 4 vector <double >* p = new vector <double >( n2 ); 5 } Każdy element wektora utworzonego w ten sposób jest zainicjalizowany konstruktorem domyślnym dla typu elementu.

88 Dynamiczne struktury danych Konstruktory klasy vector Inicjalizacja oznacza że każdy z 1024 elementów wektora vr jest zainicjalizowany wywołaniem Record(), a każdy z n1 elementów wektora vi jest zainicjalizowany wywołaniem int (). UWAGA: domyślny konstruktor dla typów standardowych wykonuje inicjalizację wartością 0 odpowiedniego typu. Jeśli typ nie posiada domyślnego konstruktora, nie można utworzyć wektora złożonego z elementów tego typu bez jawnego podania wartości elementów. Na przykład: 1 class Num { // infinite precision 2 public : 3 Num ( long ); 4 // no default constructor 5 //... 6 }; 7 vector <Num > v1 (10000); // error : no default Num 8 vector <Num > v2 (1000, Num (0)); // ok Rozmiar obiektu typu vector może być ustalony domyślnie, na podstawie zbioru elementów. Odbywa się to poprzez wywołanie konstruktora z argumentem zawierającym sekwencję wartości, stanowiących elementy tworzonego wektora. Na przykład: 1 void f( const list <X>& lst ) { 2 vector <X> v1(lst. begin (), lst.end ()); // copy elements from list 3 char p[] = " STL "; 4 vector <char > v2(p, &p[ sizeof (p ) -1]); // copy characters from C- style string 5 } W obu przypadkach, konstruktor klasy vector ustala rozmiar wektora w trakcie kopiowania elementów z sekwencji wejściowej. 88/116

89 89/116 Dynamiczne struktury danych Funkcja assign Funkcje assign zapewniają alternatywę w stosunku do konstruktorów z wieloma argumentami. Są one potrzebne, gdyż operator= ma jeden argument, więc assign() jest używany w sytuacjach, gdy należy przekazać argument domyślny lub zakres wartości. Na przykład: 1 class Book { 2 //... 3 }; 4 void f( vector <Num >& vn, vector <char >& vc, vector <Book >& vb, list <Book >& lb) { 5 vn. assign (1000, Num (0)); // assign vector of 10 copies of Num (0) to vn 6 char s[] = " literal "; 7 vc. assign (s, &s[ sizeof (s ) -1]); // assign " literal " to vc 8 vb. assign (lb. begin (),lb.end ()); // assign list elements 9 // } Użycie assign () kompletnie zmienia elementy wektora. Wszystkie istniejące elementy zostają usunięte i nowe są utworzone. Po wykonaniu funkcji, rozmiar wektora jest równy ilości utworzonych elementów. Na przykład: 1 void f{ 2 vector <char > v(20, z ); // v. size ()==20, each element has the value z 3 v. assign (10, a ); // v.size ()==10, each element has the value a 4 }

90 90/116 Dynamiczne struktury danych Operacje naśladujące stos Zwykle rozumiemy vector jako zwartą strukturę danych, która pozwala na indeksowanie w celu dostępu do elementów - jak w tablicach. Można jednak traktować vector jako bardziej uniwersalny pojemnik. Biorąc pod uwagę typowe przypadki użycia pojemnika, dostrzegamy użyteczność operacji naśladujących działanie stosu: 1 template <class T, class A = allocator <T> > 2 class std :: vector { 3 public : 4 //... 5 // stack operations : 6 void push_back ( const T& x); // add to end 7 void pop_back (); // remove last element 8 //... 9 }; Funkcje te pozwalają uzyskać dostęp do pojemnika poprzez ostatni element (w celu dołączenia lub usunięcia elementu). Na przykład: 1 void f( vector <char >& s) { 2 s. push_back ( a ); 3 s. push_back ( b ); 4 s. push_back ( c ); 5 s. pop_back (); 6 if (s[s. size () -1]!= b ) 7 error (" impossible!") ; 8 s. pop_back (); 9 if (s[s. size () -1]!= a ) 10 error (" impossible!") ; 11 }

91 91/116 Dynamiczne struktury danych Operacje naśladujące stos Skąd potrzeba operacji naśladujących stos w klasie vector? Po pierwsze - by zaimplementować stos - często wykorzystywany mechanizm. Po drugie - by móc stopniowo, w razie potrzeby, uzupełniać wektor danymi. Na przykład, wczytując dane punktów z pewnego źródła, które udostępnia je w przypadkowy sposób. Nie można wtedy utworzyć od razu całego wektora, trzeba wprowadzać dane nowych punktów interaktywnie. W takiej sytuacji można użyć kodu: 1 vector <Punkt > miasta ; 2 3 void add_points ( Punkt koniec ) { 4 Punkt buf ; 5 while ( cin >> buf ) { 6 if ( buf == koniec ) 7 return ; 8 // wstaw nowy punkt 9 miasta. push_back (buf ) ; 10 } 11 }

92 92/116 Dynamiczne struktury danych Operacje naśladujące listę Często trzeba dodać elementy w środku obiektu typu vector lub usunąć elementy z dowolnej lokalizacji w obrębie obiektu typu vector: 1 template <class T, class A = allocator <T> > 2 class std :: vector { 3 public : 4 //... 5 // list operations : 6 iterator insert ( iterator pos, const T& x); // add x before pos 7 void insert ( iterator pos, size_type n, const T& x); 8 template < class In > // In must be an input iterator 9 void insert ( iterator pos, In first, In last ); // insert elements from sequence 10 iterator erase ( iterator pos ); // remove element at pos 11 iterator erase ( iterator first, iterator last ); // erase sequence 12 void clear (); // erase all elements 13 // }; 1 int main ( int argc, char * argv []) { 2 vector <string > owoce ; 3 owoce. insert ( owoce.end (), " gruszka "); 4 owoce. insert ( owoce.end (), " porzeczka "); 5 vector <string > fruits ; 6 fruits. insert ( fruits.end (), " peach "); 7 fruits. insert ( fruits.end (), " orange "); 8 fruits. insert ( fruits.end (), " grape "); 9 sort ( fruits. begin (), fruits.end ()); 10 fruits. insert ( fruits.end (), 2, " apple "); 11 fruits. insert ( fruits.end (), owoce. begin (), owoce.end ()); 12 }

93 Dynamiczne struktury danych Klasa list list implementuje listę dwukierunkową (doubly linked list). Znaczy to, że możliwe jest przechodzenie do następnego lub poprzedniego elementu, wstawianie i usuwanie elementów z dowolnej pozycji. Wymienione operacje mają złożoność O(1) - wykonują się w stałym czasie. Dodatkowo STL wspiera listy jednokierunkowe (za pomocą klasy slist), zapewniające jedynie nawigację w przód. W takich sytuacjach slist jest bardziej efektywny niż list. 1 # include <string > 2 # include <list > 3 # include <iostream > 4 using namespace std ; 5 void print (list <string >& l) { 6 list <string >:: iterator it; 7 cout << " list :"; 8 for (it=l. begin (); it!=l.end (); it++) 9 cout << " " << *it; 10 cout << endl ; 11 } 12 int main ( int argc, char * argv []) { 13 list <string > lst ; // a list of strings 14 lst. insert ( lst. end (), " one "); 15 lst. insert ( lst. end (), " three "); 16 print ( lst ); 17 list <string >:: iterator it = lst. begin (); it; // it points now to number 2 19 it=lst. insert (it, "two "); print (lst ); 20 lst. insert (it, 3, "new "); print (lst ); 21 lst. remove ("new "); print (lst ); 22 } 16 list : one three list : one two three 20 list : one new new new two three 21 list : one two three /116

94 94/116 Dynamiczne struktury danych Klasa stack Klasa stack umożliwia wstawianie, usuwanie i inspekcję elementu znajdującego się na szczycie stosu. stack jest strukturą typu last-in-first-out (LIFO): element na szczycie jest tym, który został jako ostatni dołączony do pojemnika. 1 # include <stack > 2 # include <iostream > 3 using namespace std ; 4 void print ( stack <int >& s) { 5 cout << " top : " << s. top () << endl ; 6 } 7 int main ( int argc, char * argv []) { 8 stack <int > st; // stack of ints 9 st. push (1); print (st ); 10 st. push (2); print (st ); 11 st. push (3); print (st ); 12 // Modify the top (to become 4) 13 st.top ()=4; print (st ); 14 // Repeat until is empty 15 while (! st. empty ()) { 16 print (st ); 17 st.pop (); 18 } 19 } 9 top : 1 10 top : 2 11 top : top : top : 4 17 top : 2 18 top : 1 19.

95 95/116 Dynamiczne struktury danych Klasa queue Klasa queue implementuje pojemnik, specjalnie zaprojektowany do działania w trybie FIFO (first-in first-out). Elementy są wstawiane przez koniec pojemnika (back) i mogą być pobierane z jego początku (front). 1 # include <iostream > 2 # include < string > 3 # include <queue > 4 using namespace std ; 5 6 int main () { 7 queue <string > q; 8 9 q. push (" first "); 10 q. push (" second "); 11 q. push (" next "); 12 q. push (" last "); cout << " front ( oldest ): " << q. front () << endl ; 15 cout << " back ( newest ): " << q.back () << endl ; 16 q. pop (); // Remove the oldest element 17 cout << " front ( oldest ): " << q. front () << endl ; 18 cout << " back ( newest ): " << q.back () << endl ; 19 q. push (" new "); // Insert a new element 20 cout << " front ( oldest ): " << q. front () << endl ; 21 cout << " back ( newest ): " << q.back () << endl ; return 0; 24 } 14 front ( oldest ): first 15 back ( newest ): last front ( oldest ): second 18 back ( newest ): last front ( oldest ): second 21 back ( newest ): new

96 96/116 Dynamiczne struktury danych Klasa priority_queue Klasa priority_queue implementuje pojemnik działający tak, że pierwszy dostępny element jest zawsze największy ze wszystkich, zgodnie z wybranym warunkiem uporządkowania. 1 # include <iostream > 2 # include <queue > 3 using namespace std ; 4 5 int main () { 6 priority_queue <int > q; 7 8 q. push (2); 9 q. push (0); 10 q. push (1); cout << "top ( greatest ): " << q.top () << endl ; 13 q. pop (); // Remove the greatest element 14 cout << "top ( greatest ): " << q.top () << endl ; 15 q. push (1111); // Insert a new element 16 cout << "top ( greatest ): " << q.top () << endl ; return 0; 19 } 12 top ( greatest ): top ( greatest ): top ( greatest ):

97 97/116 Dynamiczne struktury danych Klasa map Klasa map jest sortowanym pojemnikiem asocjacyjnym, w którym klucz identyfikuje dane (obiekty). Powiązania między kluczami i danymi są reprezentowane przez pary typu pair<const key, data>. 1 # include <iostream > 2 # include <string > 3 # include <map > 4 using namespace std ; 5 int main ( int argc, char * argv []){ 6 map <int, string > mp; 7 // Insert the elements into the map 8 mp [1] = " One "; 9 mp [2] = " Two "; 10 mp [3] = " Three "; 11 // Print the map. 12 // declaration of a suitable iterator 13 map <int, string >:: iterator it; 14 for (it = mp. begin (); it!= mp.end (); it++) { 15 cout << " Key : " << it -> first ; 16 cout << "\ nvalue : " << it -> second ; 17 cout << endl ; 18 cout << endl ; 19 } 20 // Find the element by using the key value 21 it = mp. find (2); 22 if (it!= mp.end ()) { 23 cout << " FOUND : " << endl ; 24 cout << " Key : " << it -> first ; 25 cout << "\ nvalue : " << it -> second ; 26 } } map ma cechę unikalności, tzn. że nie mogą istnieć dwa elementy mające ten sam klucz. 12 Key : 1 13 Value : One Key : 2 16 Value : Two Key : 3 19 Value : Three FOUND : 22 Key : 2 23 Value : Two

98 Dynamiczne struktury danych Klasa multimap multimap jest wielokrotnym pojemnikiem asocjacyjnym, w którym nie ma ograniczenia ilości elementów o tym samym kluczu. 1 # include <iostream > 2 # include <string > 3 # include <map > 4 using namespace std ; 5 // Print the map. 6 void print ( multimap <int, string >& mp) { 7 multimap <int, string >:: iterator it; 8 cout << " Elements :" << endl ; 9 for (it = mp. begin (); it!= mp.end (); it++) 10 cout << " Key = " << it -> first 11 << ", Value = " << it -> second << endl ; 12 } 13 int main ( int argc, char * argv []){ 14 multimap <int, string > mp; 15 mp. insert (pair <int,string >(1, "1" )); 16 mp. insert (pair <int,string >(2, "2" )); 17 mp. insert (pair <int,string >(2, "21")); 18 mp. insert (pair <int,string >(3, "3" )); 19 print (mp ); 20 mp. erase (mp.find (2)); 21 print (mp ); 22 // Find the element by using the key value 23 multimap <int, string >:: iterator it = mp.find (2); 24 if (it!= mp.end ()) { 25 cout << " FOUND :" << endl 26 << " Key = " << it -> first 27 << ", Value = " << it -> second << endl ; 28 } } 12 Elements : 13 Key = 1, Value = 1 14 Key = 2, Value = 2 15 Key = 2, Value = Key = 3, Value = 3 17 Elements : 18 Key = 1, Value = 1 19 Key = 2, Value = Key = 3, Value = 3 21 FOUND : 22 Key = 2, Value = 21 98/116

99 99/116 z użyciem biblioteki Qt Klasy pojemników Qt zawiera następujące pojemniki sekwencyjne: QList, QLinkedList, QVector, QStack i QQueue. QList<T> jest implementowany jako tablica, z dodatkowo zarezerwowaną pamięcią na obu końcach. Pojemnik jest optymalizowany pod kątem swobodnego dostępu za pomocą indeksowania, a dla list o mniej niż 1000 elementach zapewnia dobrą wydajność operacji prepend() i append(). QStringList jest wygodną klasą opartą na QList<QString>. QLinkedList<T> jest optymalizowany pod kątem sekwencyjnego dostępu za pomocą iteratorów, zapewnia też szybkie wstawianie (rzędu O(1)) w dowolne miejsce listy. Sortowanie i wyszukiwanie są stosunkowo wolne. Klasa posiada szereg funkcji ułatwiających często wykonywane operacje. QVector<T> przechowuje dane w ciągłym obszarze pamięci i jest optymalizowany pod kątem swobodnego dostępu za pomocą indeksowania. Zwykle przy tworzeniu obiektów klasy QVector określa się ich rozmiar początkowy. Klasa nie zapewnia automatycznej rezerwacji pamięci na przyszłe elementy na końcach pojemnika, więc wstawianie, dołączanie na końcu i początku jest kosztowne. QStack<T> publicznie dziedziczy z QVector<T>, więc publiczny interfejs QVector jest dostępny w QStack. Ponadto, operacje push(), pop() i top() realizują tryb last-in-first-out. QQueue<T> - kolejka Dla większości zastosowań, najlepszym wyborem jest QList. Jeśli potrzebna jest tradycyjna lista jednokierunkowa, można użyć QLinkedList.

100 z użyciem biblioteki Qt Klasy pojemników Qt zawiera następujące pojemniki asocjacyjne: QMap, QMultiMap, QHash, QMultiHash i QSet. QMap<Key,T> jest uporządkowanym pojemnikiem asocjacyjnym, który przechowuje pary (klucz, wartość) i jest przeznaczony do szybkiego wyszukiwania wartości związanej z kluczem, a także do łatwego wstawiania. Przechowuje on klucze w posortowany sposób, dla przyspieszenia wyszukiwania i obsługi zakresów, za pomocą listy pomijanej (skip-list dictionary), co przyczynia się do efektywnej gospodarki pamięcią. Typ Key musi posiadać operator<() i operator==(). QHash<Key, T> jest pojemnikiem asocjacyjnym, który używa tablicy haszującej dla wyszukiwania kluczy. Zapewnia bardzo szybkie wyszukiwanie po kluczu i wstawianie, ale wolne wyszukiwanie wartości, nie obsługuje sortowania. Typ Key musi posiadać operator==(). QMultiMap<Key, T> jest podklasą QMap, a QMultiHash<Key, T> jest podklasą QHash. Te klasy pozwalają związać wiele wartości z pojedynczym kluczem. QCache<Key, T> jest pojemnikiem asocjacyjnym, który zapewnia najszybszy dostęp do ostatnio używanych wartości i automatyczne usuwanie najmniej używanych wartości oparte na funkcji kosztu. QSet<T> przechowuje wartości typu T używając QHash z kluczem typu T i puste wartości związane z kluczem. To podejście optymalizuje wyszukiwanie i wstawianie. QSet posiada funkcje implementujące typowe operacje na zbiorach (unie, przekroje, różnice, etc.). Domyślny konstruktor tworzy zbiór pusty. Pojemniki haszujące zapewniają szybkie wyszukiwanie za pomocą f. haszujących, 100/116

101 101/116 z użyciem biblioteki Qt Narzędzie qmake, plik Makefile i plik projektu Aplikacje tworzone w języku C++ zwykle składają się z wielu plików nagłówkowych, plików źródłowych i zewnętrznych bibliotek. W trakcie normalnego rozwoju projektu, pliki źródłowe i nagłówkowe podlegają częstym zmianom. W trakcie uruchamiania i testowania, projekt jest wielokrotnie rekompilowany i ponownie łączony. Aby utworzyć plik EXE, należy rekompilować wszystkie zmienione pliki źródłowe, po czym połączyć wszystkie pliki obiektowe. Kontrolowanie tych zmian i ich wpływu na całość wymaga mechanizmu, który uwzględnia pliki wejściowe, narzędzia stosowane do ich przetwarzania, rezultaty pośrednie i występujące zależności, a także końcowy cel - program wykonywalny lub bibliotekę.

102 102/116 z użyciem biblioteki Qt Narzędzie qmake, plik Makefile i plik projektu Do budowania projektu powszechnie używa się narzędzia make, które wczytuje informacje dotyczące projektu i instrukcje dla kompilatora i pozostałych narzędzi z pliku tekstowego Makefile, przypominającego skrypt powłoki. Plik Makefile zawiera (co najmniej): reguły budowania poszczególnych plików składających się na projekt (np., aby otrzymać plik obiektowy.o z pliku źródłowego.cpp, należy uruchomić g++ z argumentem będącym plikiem.cpp) docelowe rezultaty (takie jak pliki wykonywalne lub biblioteki) które mają być wytworzone zależności, określające które rezultaty muszą być przebudowane w następstwie zmian wprowadzonych w plikach projektu Narzędzie make domyślnie ładuje plik Makefile znajdujący się w bieżącym katalogu roboczym, a następnie wykonuje zdefiniowane w nim kroki kompilacji i łączenia. Podstawową korzyścią zastosowania narzędzia make jest to, że rekompilacji podlegają tylko te pliki, które zostały zmienione od ostatniej poprawnej kompilacji, a nie wszystkie istniejące w projekcie.

103 103/116 z użyciem biblioteki Qt Etapy działania (q)make Diagram ilustrujący etapy budowania aplikacji Qt:

104 104/116 z użyciem biblioteki Qt Klasa QObject Klasa QObject jest klasą bazową wszystkich obiektów w bibliotece Qt. Poniżej podano skróconą definicję tej klasy: Jak widać, klasa QObject nie posiada publicznego konstruktora kopiującego, ponieważ obiekty QObject nie mają być kopiowane. Przyjęto zasadę, że obiekty klasy QObject są unikalne i posiadają "tożsamość"; tzn., mają swoje 1 class QObject { 2 public : 3 QObject ( QObject * parent =0); 4 virtual ~QObject (); 5 QString objectname () const ; 6 QObject * parent () const ; 7 void setparent ( QObject * parent ); 8 const ObjectList & children () const ; 9 //... more }; odpowiedniki w realnym świecie, które również cechują się pewną unikalnością i trwałością. Wobec braku publicznego konstruktora kopiującego, obiekty typu QObject nie mogą być przekazane do funkcji. Każdy obiekt typu QObject może mieć co najwyżej jeden obiekt rodzicielski (parent) i dowolnie dużą liczbę obiektów potomnych typu QObject*. Obiekt typu QObject przechowuje wskaźniki do obiektów potomnych w polu QObjectList. Każdy QObject zarządza swoimi dziećmi. Oznacza to, że destruktor klasy QObject automatycznie usuwa obiekty swoich dzieci. Lista obiektów potomnych tworzy dwukierunkowe powiązanie typu jeden-do-wielu (one-to-many) między obiektami. Ustanowienie rodzica dla obiektu dodaje jego adres do listy potomków, na przykład obja >setparent(objb); dodaje wskaźnik obja do listy dzieci w objb. Jeśli później wykonamy instrukcję obja >setparent(objc); to wskaźnik obja zostanie usunięty z listy dzieci w objb i dodany do listy dzieci w objc.

105 105/116 z użyciem biblioteki Qt QObject - zarządzanie obiektami potomnymi Klasa QObject może być użyta do budowy złożonych struktur obiektów. W takich strukturach występują dwa rodzaje obiektów: złożone (composite) obiekty, które mogą zawierać obiekty potomne (części) składniki (component), które mogą mieć właściciela (rodzica). QObject może reprezentować zarówno obiekty złożone jak i składniki. Przynależność (lub zawieranie) obiektów możemy wyrazić za pomocą relacji parent-child między obiektami klasy QObject. 4 # include <QObject > 5 6 class Part : public QObject { 7 public : 8 Part ( QObject * parent, QString name ); 9 virtual ~ Part (); 10 }; 1 # include <QTextStream > 2 # include " Part.h" 3 4 static QTextStream cout (stdout, QIODevice :: WriteOnly ); 5 6 Part :: Part ( QObject * parent, QString name ) 7 : QObject ( parent ) { 8 setobjectname ( name ); 9 cout << QString (" Create Part : %1"). arg (name ) << endl ; 10 } 11 Part ::~ Part () { 12 cout << QString (" Destroy Part : %1"). arg ( objectname ()) << endl ; 13 }

106 106/116 z użyciem biblioteki Qt QObject - zarządzanie obiektami potomnymi 1 # include <QTextStream > 2 # include " Part.h" 3 4 static QTextStream cout ( stdout, 5 QIODevice :: WriteOnly ); 6 7 int main ( int, char **) { 8 cout << " CREATE a root object." << endl ; 9 Part root (0, " Computer "); 10 // other objects are created on the heap 11 Part *main = new Part (& root, "Main board "); 12 Part * devices = new Part (& root, " Devices "); 13 new Part (main, " CPU "); 14 new Part (main, " Memory "); 15 new Part (devices, " Storage "); 16 new Part ( devices, "I/O"); 17 new Part (0, " Power supply "); 18 cout << "\ ndisplay the object tree " << endl ; 19 root. dumpobjecttree (); 20 cout << "\ ndestroy all objects." << endl ; 21 return 0; 22 } UWAGA: "Power supply" nie jest częścią wymienioną w liście generowanej przez dumpobjecttree() i nie zostanie usunięta (wystąpi przeciek pamięci). 1 CREATE a root object. 2 Create Part : Computer 3 Create Part : Main board 4 Create Part : Devices 5 Create Part : CPU 6 Create Part : Memory 7 Create Part : Storage 8 Create Part : I/O 9 Create Part : Power supply DISPLAY the object tree : 12 QObject :: Computer 13 QObject :: Main board 14 QObject :: CPU 15 QObject :: Memory 16 QObject :: Devices 17 QObject :: Storage 18 QObject ::I/O DESTROY all objects : 21 Destroy Part : Computer 22 Destroy Part : Main board 23 Destroy Part : CPU 24 Destroy Part : Memory 25 Destroy Part : Devices 26 Destroy Part : Storage 27 Destroy Part : I/O

107 107/116 z użyciem biblioteki Qt Sygnały i sloty Sygnały i sloty są używane do komunikacji między obiektami. Mechanizm sygnałów i slotów jest jednym z głównych filarów Qt. Motywacja Programując GUI, należy zwykle przekazać informację o zmianie stanu kontrolki ekranowej (widget), np. przycisku czy suwaka, do innego elementu programu (kontrolki lub funkcji obsługi zdarzenia). Na przykład, gdy użytkownik klika przycisk Close, zwykle należy wywołać funkcję close() tego okna. Bardziej ogólnie, potrzebny jest mechanizm komunikacji między obiektami dowolnego typu. Podejście alternatywne - funkcje typu callback Starsze biblioteki realizują ten rodzaj komunikacji za pomocą wywołań zwrotnych (callback). Użytkownik jedynie rejestruje funkcję (w postaci wskaźnika do niej) do późniejszego wywołania, natomiast funkcje biblioteki wywołają ją w stosownym czasie. Mechanizm ten ma dwie podstawowe wady: (1) wywołania zwrotne nie są realizowane z kontrolą typów (nie ma pewności, że przekazane zostaną poprawne argumenty). (2) funkcja wywoływana zwrotnie jest silnie związana z kodem użytkownika poprzez wskaźnik do niej dostarczony w chwili rejestracji.

108 108/116 z użyciem biblioteki Qt Sygnały i sloty Qt oferuje mechanizm sygnałów i slotów, jako następcę funkcji callback. Sygnał jest emitowany, kiedy występuje określone zdarzenie. Sygnał jest komunikatem zdefiniowanym w klasie jako funkcja o rezultacie typu void. Funkcja ta posiada listę parametrów, ale nie ma ciała. Sygnał jest częścią interfejsu klasy. Przypomina on funkcję, ale nie może być wywołany musi być wysłany przez obiekt tej klasy. Slot jest funkcją wywoływaną w odpowiedzi na określony sygnał. Np. kontrolki Qt (widget) mają wiele pre-definiowanych slotów, ale powszechną praktyką jest dziedziczenie z kontrolek i dodawanie własnych slotów w celu obsługi specyficznych sygnałów. Mechanizm sygnałów i slotów używa kontroli typów: sygnatura sygnału musi się zgadzać z sygnaturą slotu. Dzięki temu kompilator wykrywa tego rodzaju błędy. Sygnały i sloty są luźno związane: klasa która emituje sygnał, nie zna slotów odbierających go. Mechanizm sygnałów i slotów zapewnia, że po połączeniu sygnału ze slotem, będzie on wywołany z parametrami przekazanymi w sygnale w odpowiednim momencie. Sygnały i sloty mogą mieć dowolną liczbę argumentów dowolnego typu. Wszystkie podklasy QObject, w tym QWidget mogą zawierać sygnały i sloty. Sygnały są emitowane przez obiekty, gdy zmieniają one swój stan w sposób interesujący dla innych obiektów. To wszystko co musi zrobić obiekt, by zrealizować sterowanie innymi obiektami, bez wnikania czy istnieje jakikolwiek slot odbiorczy. Sloty są używane do odbioru sygnałów, ale są to normalne funkcje składowe klasy. Slot może być odbiorcą wielu sygnałów. Sygnał może być połączony z wieloma slotami, jak również z innymi sygnałami (emitowanie innego sygnału).

109 109/116 Sygnały i sloty Connecting sygnałs to slots

110 110/116 Sygnały i sloty Przykład Klasa Register przechowuje swój stan w polu (m_value), dostarczając publicznej metody do jego odczytu, a dodatkowo umożliwia programowanie z użyciem mechanizmu sygnałów i slotów. Każdy obiekt tej klasy może "obwieścić" reszcie świata, że jego stan uległ zmianie, emitując sygnał valuechanged(). Każdy obiekt tej klasy może również odebrać taki sygnał i obsłużyć w kodzie slotu setvalue( int ). Klasy korzystające z sygnałów lub slotów muszą użyć makra Q_OBJECT na początku bloku deklaracji. Muszą też bezpośrednio lub pośrednio dziedziczyć z QObject. 4 # include <QObject > 5 6 class Register : public QObject { 7 Q_OBJECT 8 public : 9 explicit Register (); 10 int value () const ; 11 public slots : 12 void setvalue ( int value ); 13 signals : 14 void valuechanged ( int newvalue ); 15 private : 16 int m_value ; 17 }; 1 # include " Register.h" 2 3 Register :: Register () : 4 QObject (0) { 5 m_value = 0; 6 } 7 int Register :: value () const { 8 return m_value ; 9 } 10 void Register :: setvalue ( int value ) { 11 if ( value!= m_value ) { 12 m_value = value ; 13 emit valuechanged ( value ); 14 } 15 }

111 111/116 Przykład c.d. Sygnały i sloty W funkcji main() tworzone są dwa obiekty (a i b). Początkowo są one niezależne, następnie definiowane jest połączenie od a do b, a w końcu ponownie stają się niezależne. W każdej z tych faz, wywoływane są funkcje slotu obiektu a i b. 1 # include <QCoreApplication > 2 # include <iostream > 3 using namespace std ; 4 # include " Register.h" 5 void print ( const Register & a, const Register & b) { 6 cout << endl << "a:" << a. value () 7 << ", b:" << b. value (); 8 } 9 int main ( int argc, char * argv []) { 10 QCoreApplication app (argc, argv ); 11 Register a, b; print (a, b); 12 a. setvalue (1); print (a, b); 13 QObject :: connect (&a, SIGNAL ( valuechanged ( int )), 14 &b, SLOT ( setvalue ( int ))); 15 cout << endl << " connect a --> b"; 16 a. setvalue (2); print (a, b); 17 b. setvalue (3); print (a, b); 18 QObject :: disconnect (&a, SIGNAL ( valuechanged ( int )), 19 &b, SLOT ( setvalue ( int ))); 20 cout << endl << " disconnect a --> b"; 21 a. setvalue (0); print (a, b); 22 cout << endl ; 23 return app. exec (); 24 } a:0, b:0 12 a:1, b: connect a --> b 16 a:2, b:2 17 a:2, b: disconnect a --> b 21 a:0, b:3

112 z użyciem biblioteki Qt QApplication i pętla komunikatów Interaktywne aplikacje Qt posiadające GUI mają inną organizację niż aplikacje konsolowe czy filtry (sekwencyjne), ponieważ są one sterowane zdarzeniami i często są wielowątkowe. Obiekty wymieniają komunikaty aby synchronizować swoje działanie. Klasa QEvent Klasa QEvent reprezentuje ogólnie pojęcie zdarzenia, będąc klasą bazową dla specjalizowanych klas, takich jak QActionEvent, QFileOpenEvent, QHoverEvent, QInputEvent, QMouseEvent, itd. Obiekty typu QEvent są tworzone w systemie okien w odpowiedzi na akcje użytkownika (QMouseEvent), w określonych odstępach czasu (QTimerEvent) lub jawnie w programie. Metoda type() zwraca wartość typu enum, identyfikującą rodzaj zdarzenia. Typowa struktura programu zbudowanego z użyciem Qt Typowy program Qt tworzy obiekty, łączy je ze sobą (sygnał/slot), a następnie wywołuje metodę exec(), która obsługuje pętlę zdarzeń. Od tego momentu obiekty mogą wysyłać i odbierać komunikaty, przekazywane w różny sposób. Obiekty graficzne (QWidget) wysyłają QEvent-y do innych QObject-ów w odpowiedzi na akcje użytkownika. QWidget-y odpowiadają na akcje zarządcy okien, takie jak repaint (), resize (), czy close (). Obiekty klasy QObject komunikują się za pomocą sygnałów i slotów. Pętla zdarzeń jest strukturą programową, która priorytetuje, kolejkuje i wysyła zdarzenia do obiektów. Tworzenie aplikacji sterowanej zdarzeniami polega na zdefiniowaniu interfejsu do funkcji wywoływanych w odpowiedzi na konkretne zdarzenia. Pętla zdarzeń wykonuje się dopóki nie wystąpi zdarzenie końcowe (np. użytkownik naciśnie klawisz QUIT). 112/116

113 113/116 z użyciem biblioteki Qt QApplication i pętla komunikatów Prosta aplikacja GUI, tworząca obiekt w klasy QWidget, który reprezentuje główne okno aplikacji. Wewnątrz tworzony jest obiekt klasy QVBoxLayout aby ułożyć obiekty graficzne w pionowy stos. Następnie tworzone są obiekty klas QTextEdit (do edycji tekstu) i QPushButton (przycisk uruchamiający obsługę zdarzenia). Ostatecznie predefiniowany slot clicked () zostaje skojarzony z sygnałem generowanym przez obiekt pushbutton. 1 # include <QApplication > 2 # include <QWidget > 3 # include <QVBoxLayout > 4 # include <QTextEdit > 5 # include <QPushButton > 6 7 int main ( int argc, char * argv []) { 8 QApplication app (argc, argv ); 9 QWidget w; 10 QLayout * layout = new QVBoxLayout ; 11 w. setlayout ( layout ); 12 QTextEdit *te = new QTextEdit ; 13 layout ->addwidget (te ); 14 te ->sethtml ("<b>int </b> main (<b>void </b>) {<br>" 15 "&nbsp ;& nbsp ;<i>// indicates success </i><br>" 16 "&nbsp ;& nbsp ;<b>return </b> 0;<br>" 17 "}"); 18 QPushButton * quitbtn = new QPushButton ("OK"); 19 layout ->addwidget ( quitbtn ); 20 QObject :: connect (quitbtn, SIGNAL ( clicked ()), &app, SLOT (quit ())); 21 w. show (); 22 return app. exec (); 23 }

114 114/116 z użyciem biblioteki Qt Klasa QWidget Widgety są to obiekty klas pochodnych QWidget, które są podstawowymi elementami składowymi graficznego interfejsu aplikacji. QWidget jest klasą opartą na dwu klasach bazowych: QWidget dziedziczy z QObject, dzięki czemu może być w relacji parent-child, generować sygnały i posiadać sloty do komunikacji z innymi obiektami QWidget dziedziczy z QPaintDevice, klasy bazowej dla wszystkich obiektów graficznych przedstawianych na ekranie. Obiekty klasy QWidget współdziałają ze swoimi potomkami. Obiekt nie mający rodzica nazywa się oknem (window). Jeśli pewien obiekt jest rodzicem innego, krawędzie potomka leżą w całości wewnątrz obszaru rodzica. Elementy potomne są wyświetlane zgodnie z regułami rozmieszczenia, definiowanymi przez klasy pochodne QLayout. Obiekt klasy QWidget może obsługiwać zdarzenia przekazywane za pomocą sygnałów od różnych obiektów istniejących w systemie okien (e.g., mysz, klawiatura, procesy, itp.). Może również wyświetlać swój obraz w prostokątnym obszarze ekranu, a na koniec usunąć się z ekranu nie zaburzając stanu innych obiektów. Typowa aplikacja pulpitu z GUI może posiadać wiele (setki) różnych obiektów klasy QWidget i pochodnych, związanych relacją parent-child i rozmieszczonych zgodnie z wymaganiami estetyki, ergonomii i zastosowaniem programu.

115 115/116 z użyciem biblioteki Qt Okna dialogowe Okno dialogowe pojawia się na ekranie, wyświetlając użytkownikowi istotną informację i pobiera od niego odpowiedź. Tym samym zapewnia krótką interakcję, w wyniku której użytkownik może wpływać na działanie programu. QDialog jest klasą bazową dla wszystkich okien dialogowych; posiada ona pole typu bool o nazwie modal. If modal ma wartość true, to okno dialogowe blokuje dostęp do wszystkich innych okien danej aplikacji. Takie modalne okno dialogowe musi być zamknięte, aby aplikacja mogła kontynuować swoje działanie. Dlatego modalne okna dialogowe stosuje się tylko wtedy, gdy użytkownik musi wprowadzić informację niezbędną dla dalszego działania programu lub zostać poinformowany o pewnym zdarzeniu. Klasa QMessageBox (pochodna od QDialog) wyświetla modalne okno dialogowe służące do powiadamiania użytkownika. Zawiera ono informację tekstową (lub pytanie), ikonę i jeden do trzech przycisków. Ikony informują o istotności zdarzenia (ostrzeżenie, błąd, awaria) i korespondują z typem powiadomienia.