Programowanie Systemów Sterowania
Transkrypt
Programowanie Systemów Sterowania
Programowanie Systemów Sterowania Dr inż. Dariusz Bismor Gliwice, 2007 Operator new a W przypadku poprzedniej klasy i definicji: Dynamiczna *tab = new Dynamiczna[ 10 ]; nie zostaną wywołane przeładowane wersje operatorów new i delete a Powodem jest istnienie drugiej wersji operatorów, wykorzystywanych w przypadku przydziału pamięci na tablice a Przydział pamięci na tablice wygląda nieco inaczej, gdyż, oprócz zapamiętania ilości potrzebnej pamięci, system musi zapamiętać liczbę elementów tablicy a Liczba elementów tablicy musi zostać zapamiętana w celu wywołania odpowiedniej liczby konstruktorów i destruktorów Operator new void * Dynamiczna::operator new[]( size_t rozmiar ){ cout << ”Tworzenie tablicy obiektów klasy Dynamiczna, rozmiar ‘’ << rozmiar << ‘’B’’; return ::new char[ rozmiar ] ); }; Jedyna różnica w kodzie operatora! void Dynamiczna::operator delete[]( void *wsk ){ cout << ”Kasowanie tablicy obiektów klasy Dynamiczna‘’ << endl; ::delete [] static_cast<char*>(wsk); }; Operator new aPrzeładować można także globalną wersję operatorów new i delete aJest to bardzo zaawansowana i potencjalnie niebezpieczna technika aZastosowanie: detekcja wycieków pamięci w programie, optymalizacja dostępu do pamięci Operator new void * operator new( size_t rozmiar ){ printf("Przydzielam %zd B pamięci… ", rozmiar ); void *wsk = malloc( rozmiar ); if( !wsk ){ printf("błąd!\n"); throw bad_alloc(); } printf("zrobione!\n"); return wsk; }; void operator delete( void *wsk ){ printf("Zwalniam przydzieloną pamięć!\n"); free(wsk); }; Operator new a Przeładowanie operatora new można wykorzystać do umieszczenia obiektu w wybranym segmencie pamięci, na przykład przy obsłudze urządzeń, których obszar wejścia-wyjścia jest mapowany w obszar pamięci (memory mapped I/O) a Technika ta nosi nazwę umiejscowiania alokowanej pamięci (ang. placement new) a Miejsce w pamięci należy przygotować przed wywołaniem operatora new a Programista ponosi całkowitą odpowiedzialność za właściwe przygotowanie miejsca (np. stronicowanie) a Utworzonego w ten sposób obiektu nie wolno usuwać operatorem delete – w zamian należy jawnie wywołać destruktor Operator new class Umieszczana{ public: void *operator new( size_t, void *gdzie ){ return gdzie; } … }; int main(){ char bufor[ 2 * sizeof(Umieszczana) ]; void *miejsce = bufor; Umieszczana *wskU = new(miejsce) Umieszczana; … Umiejscowienie obiektu w zadanym miejscu wskU->Umieszczana::~Umieszczana(); } Zamiast delete – wywołanie destruktora Operatory inkrementacji aWyróżnia się operatory preinkrementacji (++i) i postinkrementacji (i++) aOperatory te są jednoargumentowe aOperator preinkrementacji jest definiowany na zasadach ogólnych aOperator postinkrementacji jest definiowany jako dwuargumentowy, przy czym drugi argument jest argumentem nienazwanym Operatory inkrementacji Przykład przeładowania operatorów inkrementacji: class K{ public: K( const K &wzor ); K & operator+=( const int i ); K& operator++(); const K operator++( int ); }; Operatory inkrementacji K& K::operator++(){ *this += 1; return *this; } // inkrementacja // zwrócenie wartości po zmianie Argument nienazwany, nie jest const K K::operator++( int ){ wykorzystywany w ciele funkcji K tmp = *this; *this += 1; return tmp; // utworzenie kopii // inkrementacja // zwrócenie wartości sprzed zmiany } Te same uwagi dotyczą operatora dekrementacji (--i oraz i-- ) Wyczerpujący przykład Jakie błędy potrafisz wskazać w przedstawionym kodzie? class Zespolona{ float _re, _im; public: Zespolona( float re, float im = 0 ): _re(re), _im(im) {}; void operator+( Zespolona z ){ _re = _re + z._re; _im = _im + z._im; } void operator<<( ostream os ){ os << ‘’(‘’ << _re << ‘’,’’ << _im << ‘’)’’; } Zespolona operator++(){ ++_re; return *this; } Zespolona operator++(int){ Zespolona tmp = *this; ++_re; return temp; } }; Wyczerpujący przykład class Zespolona{ float _re, _im; Nie należy używać identyfikatorów rozpoczynających się od znaku podkreślenia (Standard rezerwuje niektóre z takich identyfikatorów na potrzeby implementacji) Wyczerpujący przykład class Zespolona{ float _re, _im; public: Zespolona( float re, float im = 0 ): _re(re), _im(im) {}; Tak zdefiniowany konstruktor jest jednocześnie konwerterem z typu float do typu Zespolona (może to być celowe lub nie) Wyczerpujący przykład class Zespolona{ float _re, _im; public: Zespolona( float re, float im = 0 ): _re(re), _im(im) {}; void operator+( Zespolona z ){ _re = _re + z._re; _im = _im + z._im; } Ze względu na lepszą wydajność, ten parametr powinien być referencją do stałego obiektu Wyczerpujący przykład class Zespolona{ float _re, _im; public: Zespolona( float re, float im = 0 ): _re(re), _im(im) {}; void operator+( Zespolona z ){ _re = _re + z._re; _im = _im + z._im; } Operator dodawania nie powinien być funkcją składową klasy, lecz funkcją (lub parą funkcji) globalną Przy takiej deklaracji można napisać a=b+1, lecz nie można a=1+b (Dla lepszej wydajności być może należy także zdefiniować operator+(Zespolona,int) i operator+(int,Zespolona)) Wyczerpujący przykład class Zespolona{ float _re, _im; public: Zespolona( float re, float im = 0 ): _re(re), _im(im) {}; void operator+( Zespolona z ){ _re = _re + z._re; _im = _im + z._im; } Operator dodawania nie powinien modyfikować oryginalnego obiektu, lecz tworzyć obiekt tymczasowy i zwracać go Typ zwracany const Zespolona Najlepiej,powinien z punktubyć widzenia wydajności,w celu uniknięcia zapisów operator+= a+b = c; zamiast operator+ jest zdefiniować Wyczerpujący przykład class Zespolona{ Jeszcze lepszym rozwiązaniem jest zdefiniowanie float _re, _im; funkcji składowej wypisz() i wywołanie jej public: z globalnej funkcji operatorowej Zespolona( float re, float im = 0 ): _re(re), _im(im) {}; void operator+( Zespolona z ){ _re = _re + z._re; _im = _im + z._im; } void operator<<( ostream os ){ os << ‘’(‘’ << _re << ‘’,’’ << _im << ‘’)’’; } Operator wstawiania do strumienia nie powinien być funkcją składową, a jego parametrami powinny być (ostream &, const Zespolona &) Wyczerpujący przykład class Zespolona{ float _re, _im; public: Zespolona( float re, float im = 0 ): _re(re), _im(im) {}; void operator+( Zespolona z ){ _re = _re + z._re; _im = _im + z._im; } void operator<<( ostream os ){ os << ‘’(‘’ << _re << ‘’,’’ << _im << ‘’)’’; } Typem zwracanym powinna być ostream&, a funkcja operatorowa powinna kończyć się przez return os; Dzięki temu możliwe będzie zapisanie wywołań łańcuchowych cout << z1 << z2; Wyczerpujący przykład class Zespolona{ float _re, _im; public: Operator preinkrementacji powinien zwracać Zespolona( float re, float im = 0 ): _re(re), _im(im) {}; referencję do oryginalnego void operator+( Zespolona z ){ obiektu (Zespolona Zawsze naśladować _re =należy _re + z._re; _im oryginalne = _im + z._im; } zachowanie operatorów! void operator<<( ostream os ){ os << ‘’(‘’ << _re << ‘’,’’ << _im << ‘’)’’; } Zespolona operator++(){ ++_re; return *this; } &) Wyczerpujący przykład class Zespolona{ float _re, _im; public: Zespolona( float re, float im = 0 ): _re(re), _im(im) {}; void operator+( Zespolona z ){ Operator postinkrementacji powinien zwracać _re = _re + z._re; _im = _im + z._im; } stały obiekt tymczasowy (const Zespolona) void operator<<( ostream os ){ Dzięki temu uniknie się wywołań z1++++; os << ‘’(‘’ << _re << ‘’,’’ << _im << ‘’)’’; } Zespolona operator++(){ ++_re; return *this; } Zespolona operator++(int){ Zespolona tmp = *this; ++_re; return temp; } }; Wyczerpujący przykład class Zespolona{ float _re, _im; public: Zespolona( float re, float im = 0 ): _re(re), _im(im) {}; void operator+( Zespolona z ){ _re = _re + z._re; _im = _im + z._im; } Najlepiej jest zdefiniować operator void operator<<( ostream os ){ postinkrementacji przy wykorzystaniu os << ‘’(‘’ << _re << ‘’,’’ << _im << ‘’)’’; } operatora preinkrementacji Zespolona operator++(){ ++_re; return *this; } Zespolona operator++(int){ Zespolona tmp = *this; ++_re; return temp; } }; Wyczerpujący przykład class Zespolona{ float s_re, s_im; public: explicit Zespolona( float re, float im = 0 ): s_re(re), s_im(im) {}; Zespolona& operator+=( const Zespolona& z ){ s_re += z.s_re; s_im += z.s_im; return *this; } Zespolona& operator++(){ ++s_re; return *this; } const Zespolona operator++(int){ Zespolona tmp = *this; ++(*this); return temp; } ostream& wypisz( ostream& os ) const { return os << "(" << _re << "," << _im << ")"; } }; Wyczerpujący przykład const Zespolona operator+( const Zespolona s1, const Zespolona s2){ Zespolona suma(s1); suma += s2; return s2; } ostream& operator<<( ostream &os, const Zespolona& z ){ W bibliotece standardowej istnieje szablon complex, return z.wypisz(os); } który jest prawdopodobnie bardziej wydajną i bardziej bezbłędną implementacją, niż klasa napisana przez nawet średnio zaawansowanego programistę Obsługa wyjątków a Najprostsze podejście do obsługi błędów to funkcja assert() (wyłączana w gotowym oprogramowaniu) a W języku C kod obsługi błędów musiał być przeplatany z kodem programu a Problem pojawiał się wtedy, gdy obsługa błędu nie była możliwa w bieżącym kontekście (funkcji, zakresie, itp.) a Standardowa biblioteka języka C udostępnia funkcje do „długich” skoków: setjmp() longjmp(), lecz problem odtworzenia stanu sprzed awarii pozostaje a Funkcje te nie wywołują destruktorów a Język C++ umożliwia łatwe rozdzielenie kodu obsługi błędów od „właściwego” kodu programu Obsługa wyjątków a W języku C++, w momencie napotkania problemu, którego nie można obsłużyć w danym kontekście, można wysłać informację o błędzie do wyższego kontekstu a Zazwyczaj odbywa się to przez przesłanie obiektu specjalnie zaprojektowanej do obsługi błędów klasy a Proces nosi nazwę wyrzucania wyjątków (throwing an exception) i jest wykonywany instrukcją „throw” a Wyrzucenie wyjątku wewnątrz funkcji powoduje jej natychmiastowe opuszczenie a Aby nie opuszczać bieżącego kontekstu, można posłużyć się blokiem „try” a Obsługa wyjątku następuje przez przechwycenie przesłanego obiektu i reakcję wewnątrz bloku „catch” Obsługa wyjątków class ObslugaBledow{ Specjalna klasa do obsługi błędów const char const *opis; public: ObslugaBledow( const char* info ): opis(info) {}; const char *co() const { return opis }; }; void fun(){ throw ObslugaBledow( ”Funkcja fun(): wystąpił błąd!” ); } „Wyrzucenie” błędu z utworzeniem obiektu klasy int main(){ służącej do obsługi błędów try{ „Próba”, czy dany kod wykona się poprawnie fun(); Przechwycenie „wyrzuconego } catch( ObslugaBledow &blad ){ cout << ”Niedobrze! ” << blad.co() <<obiektu endl; i reakcja exit( 1 ); } return 0; // Nigdy tutaj nie dojdzie! } Obsługa wyjątków a „Wyrzucać” można obiekty dowolnych typów, także wbudowanych (np. int) a Polecenie throw kopiuje egzemplarz wyrzucanego obiektu, który następnie zostaje zwrócony a Wszystkie automatyczne obiekty w pełni utworzone w kontekście, z którego wyjątek jest „wyrzucany” są niszczone (tzw. odwijanie stosu, stack unwinding) a „Wyrzucony” obiekt zostaje „złapany” w kodzie obsługi błędu a Składnia instrukcji catch przypomina funkcję z jednym parametrem; można użyć także parametru nienazwanego Obsługa wyjątków a „Wyrzucony” obiekt danego typu musi być „złapany” przez instrukcję catch z parametrem zgodnego typu a Kilka instrukcji catch z różnymi przechwytywanymi typami może następować po sobie: try{ … }catch( Typ1 &x1 ){ … // Obsługa wyjątków typu pierwszego }catch( Typ2 &x2 ){ … // Obsługa wyjątków typu pierwszego }catch( Typ3 &x3 ){ … // Obsługa wyjątków typu pierwszego } // Ewentualnie inne typy Obsługa wyjątków a Jedyne konwersje wykonywane na typach w instrukcji catch to konwersje związane z dziedziczeniem i konwersja wskaźników dowolnego typu na wskaźnik void a Możliwe jest złapanie obiektu dowolnego typu przez instrukcję catch(...){ // Kod obsługi wyjątku } a Wyjątki można wyłapywać przez wartość, referencję i przez wskaźnik a Wyłapywanie wyjątku przez wartość nie pozwala wykonywać konwersji z typu klasy pochodnej na typ klasy podstawowej a Dlatego najczęściej wyjątki wyłapuje się przez referencję Obsługa wyjątków a Wyjątek można „przerzucić” z kodu obsługi wyjątku, np. po dealokacji niepotrzebnych zasobów, do innego kontekstu, za pomocą instrukcji throw: catch(...){ cout << ”Wyjątek, hurra!” << endl; // Tutaj ew. dealokacja zasobów throw; } a Nie złapany lub przerzucony wyjątek przechodzi do następnego, wyższego kontekstu: bardziej „zewnętrznego” bloku try lub funkcji wywołującej daną funkcję a Także tam nie przechwycony wyjątek przechodzi do jeszcze wyższego kontekstu, itd. Obsługa wyjątków a Jeżeli wyjątek nie zostanie przechwycony na żadnym z poziomów, zostanie wywołana funkcja biblioteczna treminate(), zdeklarowana w nagłówku <exception> a Funkcja ta domyślnie wywołuje funkcję abort(), powodującą natychmiastowe zakończenie programu, a w systemach unixowych wygenerowanie pliku core a Funkcja abort() nie pozwala wykonać się destruktorom obiektów globalnych i statycznych a Możliwe jest zainstalowanie własnej funkcji obsługi nie przechwyconych wyjątków za pomocą funkcji set_terminate(), która zwraca wskaźnik do bieżącej funkcji obsługi wyjątków Obsługa wyjątków a Funkcja ta musi być bezparametrowa i musi zwracać void: void terminator(){ cout << ”Nareszcie koniec męczarni!” << endl; exit(1); } // Zmienna globalna i jej inicjalizacja void (*stary)() = set_terminate( terminator ); a Funkcja terminate() zostanie także wywołana w sytuacji, gdy nastąpi wyjątek w trakcie wykonywania kodu obsługi innego wyjątku Wyjątki w konstruktorze aKonstruktory nie posiadają wartości zwracanej aDlatego najlepszym sposobem sygnalizacji niepowodzenia pracy konstruktora są wyjątki aWyjątek, który wystąpił w konstruktorze obiektu danej klasy, nie powoduje uruchomienia destruktora tej klasy aPowodem jest brak możliwości rozróżnienia, które zasoby zostały już zaalokowane, a które jeszcze nie Wyjątki w konstruktorze class Poprawna{ public: Poprawna(){ cout << ”Poprawna: konstruktor!\n”; } ~Poprawna(){ cout << ”Poprawna: destruktor!\n”; } }; class Wadliwa{ public: Wadliwa(){ cout << ”Wadliwa: konstruktor!\n”; throw 1; } ~Wadliwa(){ cout << ”Wadliwa: destruktor!\n”; } }; class Dziedzic{ Poprawna *pop; Wadliwa *wad; public: Dziedzic( int liczba=1 ); ~Dziedzic(); }; Wyjątki w konstruktorze Dziedzic::Dziedzic( int liczba ){ cout << ”Dziedzic: konstruktor!\n”; pop = new Poprawna[ liczba ]; wad = new Wadliwa; } Dziedzic::~Dziedzic(){ cout << ”Dziedzic: destruktor!\n”; } int main(){ try{ Dziedzic dz(5); }catch(int){ cout << ”Nastąpił wyjątek!” << endl; } cout << ”Koniec programu!” << endl; return 0; } Wynik działania programu: Dziedzic: konstruktor! Poprawna: konstruktor! Poprawna: konstruktor! Poprawna: konstruktor! Poprawna: konstruktor! Poprawna: konstruktor! Wadliwa: konstruktor! Nastąpił wyjątek! Koniec programu! Wyjątki w konstruktorze a Wyjątek, który wystąpił w konstruktorze obiektu danej klasy, nie powoduje uruchomienia destruktora tej klasy, ani konstruktorów klas pochodnych a Zatem konstruktor powinien sam rozróżniać, które zasoby zostały zaalokowane, i w sytuacji wyjątkowej podejmować różne akcje a Można to osiągnąć na kilka sposobów, na przykład przez wstępną inicjalizację wskaźników zerami, lub przez zastosowanie funkcji inicjalizujących Wyjątki w konstruktorze Dziedzic::Dziedzic(): pop(0), wad(0){ cout << ”Dziedzic: konstruktor!\n”; try{ pop = new Poprawna; wad = new Wadliwa; }catch( bad_alloc ){ delete pop; delete wad; } } Dziedzic::~Dziedzic(){ delete pop; delete wad; cout << ”Dziedzic: destruktor!\n”; } Wyjątki w konstruktorze a Lepszą metodą jest zastosowanie idiomu RAII (Resource Acquisition Is Initialization) a Zakłada on, że każdy alokowany dynamicznie w konstruktorze obiekt powinien „za sobą posprzątać” sam a RAII pozwala na uniknięcie pisania wielu bloków try-catch a RAII można zrealizować używając „zręcznych” wskaźników a W pliku nagłówkowym <memory> zdefiniowano szablon auto_ptr, który posiada jeden parametr będący typem obiektu, do którego wskaźnik należy „zręcznie” zdefiniować a Szablon auto_ptr posiada przedefiniowane operatory * oraz ->, co ułatwia posługiwanie się takim wskaźnikiem Wyjątki w konstruktorze Zmiany w programie: class Dziedzic{ auto_ptr<Poprawna> pop; auto_ptr<Wadliwa> wad; public: Dziedzic(int liczba=1 ); ~Dziedzic(); }; Wynik działania programu: Dziedzic: konstruktor! Poprawna: konstruktor! Wadliwa: konstruktor! Poprawna: destruktor! Nastąpił wyjątek! Koniec programu! Dziedzic::Dziedzic( int liczba ){ cout << ”Dziedzic: konstruktor!\n”; pop.reset( new Poprawna ); wad.reset( new Wadliwa ); } Wada rozwiązania – brak możliwości zdefiniowania tablicy Wyjątki w destruktorze aNigdy nie należy dopuścić do wydostania się wyjątku z destruktora! aJeżeli podczas procedury ,,odwijania'' stosu zostanie zgłoszony wyjątek, uruchomiona zostanie funkcja terminate() aDomyślnym zachowaniem funkcji terminate() jest wywołanie abort() aSytuacja taka zajdzie, jeżeli wyjątek zostanie zgłoszony w destruktorze obiektu, który jest usuwany podczas ,,odwijania'' stosu aDlatego nigdy nie wolno dopuścić do wydostania się wyjątku z destruktora! Obsługa wyjątków Funkcja może zdeklarować, jakie wyjątki będzie „wyrzucać”: // Poniższa funkcja może „wyrzucić” każdy wyjątek void fun(); // Poniższa funkcja „wyrzuca” jedynie obiekty klas // MojBlad i BladPamieci void fun1() throw( MojBlad BladPamieci ); // Ta funkcja nigdy nie „wyrzuca” wyjątków void fun2() throw(); Obsługa wyjątków a Jeżeli funkcja „wyrzuci” wyjątek, który nie jest zadeklarowany na liście jej wyjątków, zostanie wywołana funkcja unexpected() a Możliwe jest ustawienie własnej wersji funkcji unexpected() za pomocą funkcji set_unexpected(), na zasadach takich, jak przy set_terminate() a Funkcja ustawiona zamiast funkcji unexpected() może „przerzucić” wyjątek, lub „wyrzucić” inny wyjątek a Jeśli ten inny wyjątek jest na liście wyjątków funkcji, która początkowo wyrzuciła wyjątek spoza listy, program kontynuuje działanie w miejscu wywołania tej funkcji Obsługa wyjątków aKonstruktory zwykle sygnalizują błędy poprzez zgłaszanie wyjątków aWiele istotnych działań konstruktor wykonuje za pomocą listy inicjalizacyjnej aJak wyłapać wyjątki zgłoszone podczas wykonywania operacji listy inicjalizacyjnej? aRozwiązanie: blok try{} na poziomie funkcji aCiekawostka: można także używać w stosunku do innych funkcji (nie tylko konstruktora) Obsługa wyjątków ObiektDyskretny::ObiektDyskretny( int ndA=1, int ndB=0, int nk=1) try: dA(ndA), dB(ndB), k(nk), parametryA( new TabFloat(ndA) ), parametryB( new TabFloat(ndB+1) ), pamiecA( new TabFloat(ndA) ), pamiecB( new TabFloat(ndB+k) ) { cout << "Konstruktor klasy ObiektDyskretny!"; }catch( … ){ cout << "Wyjątek w konstruktorze klasy ObiektDyskretny!"; throw; } Obsługa wyjątków a Jeśli tak nie jest, a funkcja, która naruszyła specyfikację wyjątków, ma na liście wyjątek standardowy bad_exception, wyjątek zgłaszany przez funkcję obsługi jest zamieniany na wyjątek bad_exception, a sterowanie zwracane do miejsca wywołania funkcji a Jeśli typ wyjątku zgłaszanego przez funkcję obsługi sytuacji naruszenia specyfikacji wyjątków nie występuje na liście wyjątków funkcji, która specyfikację naruszyła, i jednocześnie funkcja ta nie posiada wyjątku bad_exception na swojej liście wyjątków, wywoływana jest funkcja terminate() Obsługa wyjątków aBiblioteki języka C++ posiadają więcej definicji standardowych wyjątków, którymi można się posługiwać aGdy definicje te nie wystarczają, można klasę wyjątku odziedziczyć i pozmieniać według potrzeb aPodstawowe klasy wyjątków to logic_error i runtime_error, dziedziczące z klasy exception, zdefiniowane w <stdexcept> aKlasy te posiadają konstruktor akceptujący „string” i funkcję what() Wyjątki standardowe exception bad_alloc bad_typeid logic_error domain_error invalid_argument length_error out_of_range bad_cast bad_exception runtime_error range_error overflow_error underflow_error Obsługa wyjątków Przykład: class MojBlad: public runtime_error{ public: MojBlad( std::string & info = ”” ): runtime_error( info ){} }; int main(){ try{ throw( MojBlad( ”Przykład błędu!” ); }catch( MojBlad &blad ){ cout << ”Wystąpił błąd: ” << blad.what() << endl; } } Obsługa wyjątków a Programiści języka C++ używają trzech podstawowych określeń dotyczących bezpieczeństwa – w sensie wyjątków – pisanego kodu a Podstawowa gwarancja bezpieczeństwa oznacza, że po wystąpieniu wyjątku nie następuje wyciek zasobów a Oznacza także, że obiekty pozostają w stanie umożliwiającym ich dalsze wykorzystanie a Silna gwarancja bezpieczeństwa oznacza, że po wystąpieniu wyjątku nie zmienia się stan programu a Zapewnienie takiej gwarancji wymaga programowania w stylu transakcji (całkowite wykonanie lub wycofanie) a Gwarancja nie wyrzucania wyjątków oznacza, że funkcja nigdy nie dopuszcza do wydostania się wyjątku na zewnątrz Obsługa wyjątków a Każdy dobrze napisany, niebanalny kod powinien dostarczać jednej z powyższych gwarancji a Dostarczana gwarancja powinna być najsilniejsza lecz taka, która nie powoduje nadmiernego zużycia zasobów (czasu, pamięci). a Istnieją funkcje, które nie mogą zgłaszać wyjątków (destruktor, operator delete) a Aby zrealizować tak postawiony cel nieodzowne jest staranne zaprojektowanie polityki obsługi sytuacji wyjątkowych przed rozpoczęciem kodowania a Najlepszym sposobem realizacji tego celu jest wykorzystanie wzorców projektowych w celu zapewnienia automatycznej dealokacji zasobów (RAII, techniki transakcyjne, technika ,,jedna funkcja – jedna odpowiedzialność" Obsługa wyjątków - test Pytanie: ile niezależnych ścieżek wykonania zawiera poniższa, 3-liniowa funkcja? String EvaluateSalaryAndReturnName( Employee e ) { if( e.Title() == "CEO" || e.Salary() > 100000 ) { cout << e.First() << " " << e.Last() << " is overpaid" << endl; } return e.First() + " " + e.Last(); } Założenia: a) destruktory nie zgłaszają wyjątków b) wyjątek zgłoszony przez jedną funkcję, niezależnie od typu, liczony jest jako jedna ścieżka Obsługa wyjątków - test Ile znalazłeś? 1-2 3 4-14 15-23 – powtórz kurs podstaw języka C i C++ – przeciętna znajomość języka C++, słaba wyjątków – masz świadomość, że istnieją wyjątki – wyjątki to Twoja specjalność Obsługa wyjątków - test String EvaluateSalaryAndReturnName( Employee e ) { if( e.Title() == "CEO" || e.Salary() > 100000 ) { 4 - konstruktor przekazywanego przez wartość parametru zgłosić 12może -–e.Title() == "CEO", wykonywany jest środek instrukcji if, e.Title() !=wyjątek e.Salary() >jak 100000, wykonywany 9 – jak 6 8 – 5 10 – jak 7 11 – jak 6 i 9 cout << e.First() <<7 "– "w<< e.Last() 5 – funkcja składowa Title() może zgłosić celu dopasowania do typu argumentu 6 – operator== może zgłoscić wyjątek nie wywoływana e.Salary()! jestjest "środek" instrukcji if <<wyjątek, " is overpaid" <<zwrócić endl;== rezultat lub może przez operatora może być konieczne } wartość, a wyjątek zgłosi konstruktor wykonanie konwersji, konstruktor 3 – e.Title() != "CEO" i e.Salary() < 100000,a"środek" konwertujący może zgłosić wyjątek instrukcji if nie jest wykonywany return e.First() + " " + e.Last(); } Obsługa wyjątków - test String EvaluateSalaryAndReturnName( Employee e ) { 19-20 – funkcje if( e.Title() == "CEO" || e.Salary() > 100000 ) {First() i Last() mogą zgłosić wyjątek, lub zwrócić rezultat przez cout << e.First() << " " << e.Last() wartość, a konstruktor zgłosi << " is overpaid" << endl; wyjątek } 12-16 – każde z 5-ciu21 wywołań << – jak17-18 7 operatora i 10– funkcje First() i może zgłosić wyjątek Last() mogą zgłosić wyjątek, lub zwrócić rezultat przez return e.First() + " " + e.Last(); wartość, a konstruktor zgłosi } 22 – jak236 wyjątek –i 9jak 6 i 9 Obsługa wyjątków - epilog Pytanie: czy w języku C++ istnieje konieczność sprawdzania, czy udało się zarezerwować pamięć po każdym wywołaniu operatora new, jak w poniższym kodzie? MojaKlasa *wsk = new MojaKlasa(); if( p == NULL ){ throw bad_alloc(); } Odpowiedź: nie! Biblioteki obsługujące dynamiczny przydział pamięci same wyrzucają wyjątek std::bad_alloc() w momencie, gdy zabraknie pamięci na obiekt alokowany dynamicznie Operator new nigdy nie zwraca wartości NULL! Klasy składowe aKlasa może zawierać w sobie inne klasy nazywane klasami składowymi aOdzwierciedla to sposób myślenia człowieka typu „elementy składowe” (np. samochód składa się z karoserii, silnika, kół, itp.), i jest ważnym elementem enkapsulacji aKlasy składowe są zwykle niepublicznymi elementami składowymi aKlasa ma dostęp jedynie do publicznych składników swoich klas składowych Klasy składowe class Wielomian{ public: Wielomian( int rozmiar = 1); Wielomian( const std::vector<double> &wsp ); Wielomian( const Wielomian &wzor ); ~Wielomian(); Wielomian & operator=( const Wielomian &wzor ); double operator()( double we ); void wypisz( ostream &wy ) const; … private: std::vector<double> s_wspolczynniki; std::dequeue<double> s_pamiec; … }; Klasy składowe class ObiektDyskretny{ public: ObiektDyskretny(); ObiektDyskretny( int rzA, int rzB, int k); ObiektDyskretny( std::vector<double> wA, std::vector<double> wB ); float symuluj( float Wejscie ); … protected: int dB, dA, k; Wielomian s_A, s_B; private: int sprawdzStopnie(); int ustawStopnie( int kNowe, int dBNowe, int dANowe ) }; Klasy składowe ObiektDyskretny::ObiektDyskretny( int rzA, int rzB, int k_ ) : dA( rzA ), dB( rzB ), k( k_ ), s_A( rzA ), s_B( rzB ) { if( parametryA == NULL || parametryB == NULL ){ cout << ”Utworzono nieokreślony obiekt!”; }else{ cout << ”Utworzono obiekt i nadano parametry!”; } }; Obiekt klasy składowej może być inicjalizowany wyłącznie za pomocą listy inicjalizacyjnej Klasy składowe aJeśli klasa obiektu będącego składnikiem nie posiada konstruktora, obiektu nie inicjalizuje się za pomocą listy inicjalizacyjnej aJeśli klasa obiektu będącego składnikiem posiada konstruktor domniemany, nie ma obowiązku inicjalizacji przez listę inicjalizacyjną aJeśli klasa obiektu będącego składnikiem posiada konstruktory, ale nie posiada konstruktora domyślnego, trzeba skorzystać z listy inicjalizacyjnej (lub wystąpi błąd kompilacji) Klasy składowe – deklaracja zagnieżdżona class ObiektDyskretny{ public: ObiektDyskretny(); class Wielomian{ public: Wielomian( int rozmiar, float *parametry = NULL, float *pamiec = NULL ); float licz( float Wejscie ); ... } ... }; Klasa zagnieżdżona - definicja ObiektDyskretny::Wielomian::Wielomian( int rozmiar, float *parametry = NULL, float *pamiec = NULL ){ ... } Klasy składowe – definicja zagnieżdżona aKlasa wewnętrzna zadeklarowana wewnątrz części prywatnej klasy zewnętrznej znana jest tylko wewnątrz tej klasy aKlasa wewnętrzna zadeklarowana wewnątrz części publicznej klasy wewnętrznej jest znana na zewnątrz tej klasy jako typ zagnieżdżony: ObiektDyskretny::Wielomian w1; aDeklaracja klasy zagnieżdżonej nie tworzy jeszcze obiektu deklarowanej klasy aTaki sposób deklaracji nie zmienia zwykłych reguł dostępu (części prywatne obydwu klas są wzajemnie niedostępne, o ile nie zdeklarowano przyjaźni) Dziedziczenie class ObiektDyskretny{ public: ObiektDyskretny(); ObiektDyskretny( std::vector<double> wA, std::vector<double> wB ); float symuluj( float Wejscie ); void wypiszA( ostream &wy ) const { return s_A.wypisz( wy ); } void wypiszB( ostream &wy ) const { return s_B.wypisz( wy ); } … protected: Wielomian s_A, s_B; private: … }; Dziedziczenie class Regulator : public ObiektDyskretny { public: float wartosc_zadana; }; Lista dziedziczenia Regulator( int dR, int dS ); Przesłonięcie funkcji. float symuluj( float y); Nie jest to przeładowanie, bo int samonastrajanie(); funkcja ma taki sam zestaw parametrów Nie jest to błędem, ponieważ obydwie funkcje symuluj() mają inny zasięg. Dziedziczenie protected: Wielomian s_A, s_B; int dA, dB, k; protected: Wielomian s_A, s_B; int dA, dB, k; public: ObiektDyskretny(); ObiektDyskretny(int nA, int nB, …); float symuluj(float u); public: ObiektDyskretny(); ObiektDyskretny(int nA, int nB, …); float symuluj(float u); ObiektDyskretny ObiektDyskretny Dziedziczenie (publiczne) public: Regulator(int nR, int nS); float symuluj(float w); int samonastrajanie(); Regulator Dziedziczenie Regulator reg1( 5, 5 ); reg1.wypiszA( cout ); Wywołanie funkcji wypiszA() odziedziczonej z klasy podstawowej reg1.symuluj( 0.1 ); Wywołanie „nowej” wersji funkcji symuluj() reg1.ObiektDyskretny::symuluj( 0.5 ); Wywołanie przesłonientej funkcji symuluj() z klasy ObiektDyskretny Dziedziczenie – dostęp do składników chronionych class ObiektDyskretny{ … protected: }; Wielomian s_A, s_B; class Regulator: public ObiektDyskretny{ public: }; … void wypisz( ostream& wy ) const { } wy << "Parametry mianownika: "; s_A.wypisz( wy ); wy << endl << "Parametry licznika: "; s_B.wypisz( wy ); Reguła przesłaniania class Podst{ public: }; void fun(){} class Pochodna: public Podst{ public: }; void fun(int i){} Inny zestaw parametrów –przesłonięcie! … Pochodna obPoch; obPoch.fun(): // Błąd!!! Reguła przesłaniania class Pochodna: public Podst{ public: using Podst::fun; }; "Odsłonięcie" void fun(int i){} … Pochodna obPoch; obPoch.fun( 4 ); // OK obPoch.fun(): // OK Dziedziczenie aJest istotnym elementem programowania orientowanego obiektowo aPozwala na oszczędność pracy, bo umożliwia wykorzystanie raz napisanego (i przetestowanego) kodu – wszelkie zmiany wprowadza się w klasie pochodnej aPozwala na łatwe ustawienie hierarchii klas w sposób bliski myśleniu człowieka (np. samochód marki Opel jest rodzajem (dziedziczy z) samochodu osobowego, a posiada części składowe: silnik, koła, karoseria, itp.) Dziedziczenie aPozwala na korzystanie ze skompilowanych klas, do których dołączono pliki nagłówkowe aPozwala czasami na traktowanie obiektów pochodnych tak, jak obiekty klasy podstawowej (Opel to także samochód) aUmożliwia tworzenie klas ogólnych, które same niczego nie robią, a służą jedynie do dziedziczenia, np. klasa „kolejka”, klasa „kontrolka” (widget) Dziedziczenie class regulator : public ObiektDyskretny{ Sposób dziedziczenia Klasa podstawowa Klasa pochodna private private protected protected public public public Dziedziczenie class regulator : protected ObiektDyskretny{ Sposób dziedziczenia Klasa podstawowa Klasa pochodna private private protected public protected protected public Dziedziczenie class regulator : private ObiektDyskretny{ Sposób dziedziczenia Klasa podstawowa Klasa pochodna private private protected public private protected public Dziedziczenie publiczne aNajczęściej spotykany typ dziedziczenia aDaje dostęp do składników publicznych klasy podstawowej wszystkim użytkownikom klasy aDaje dostęp do składników chronionych klasy podstawowej wszystkim potomkom klasy aNie daje dostępu do składników prywatnych klasy podstawowej aZastosowanie tego typu dziedziczenia świadczy zazwyczaj o zamiarze wykorzystania mechanizmu polimorfizmu Dziedziczenie chronione aRzadko stosowane aDaje dostęp do składników publicznych klasy podstawowej jedynie potomkom klasy aDaje dostęp do składników chronionych klasy podstawowej wszystkim potomkom klasy aPozwala potomkom klasy na poznanie relacji dziedziczenia aNie daje dostępu do składników prywatnych klasy podstawowej Dziedziczenie prywatne aNie daje dostępu do żadnych składników klasy podstawowej nawet potomkom klasy aNie daje dostępu do składników prywatnych klasy podstawowej aUkrywa przed potomkami klasy relację dziedziczenia aJeżeli używane jest dziedziczenie, a polimorfizm nie, to prawdopodobnie dziedziczenie powinno być prywatne Dziedziczenie prywatne a Efekt dziedziczenia prywatnego jest podobny do posiadania prywatnej klasy składowej a Jednakże dziedziczenie prywatne: `pozwala mieć tylko jeden egzemplarz klasy dziedziczonej `może wprowadzić (niepotrzebne) wielokrotne dziedzictwo `daje klasie pochodnej dostęp do składników protected klasy podstawowej `pozwala klasie pochodnej na zastąpienie funkcji wirtualnych klasy bazowej `pozwala jedynie składnikom klasy pochodnej i funkcjom zaprzyjaźnionym na konwersję wskaźnika do klasy pochodnej na wskaźnik do klasy bazowej a Dziedziczenia prywatnego należy używać tylko wtedy, gdy nie jest możliwe zastosowanie klasy składowej Dziedziczenie wybiórcze class regulator : private ObiektDyskretny { public: using ObiektDyskretny::symuluj; … }; Funkcja symuluj() musi mieć zakres „public” w klasie ObiektDyskretny! W ten sposób udostępnione zostają wszystkie wersje przeładowanej funkcji symuluj()! Dziedziczenie wybiórcze może jedynie powtórzyć zakres dostępu klasy podstawowej. Nie może go ani rozluźnić, ani zaostrzyć! Dziedziczenie wybiórcze Udostępnienie tylko jednej wersji przeładowanej funkcji: class regulator : private ObiektDyskretny { public: float ObiektDyskretny::symuluj( float u); ... }; Dziedziczenie Kolejność wykonywania konstruktorów: anajpierw konstruktory klas podstawowych dziedziczonych wirtualnie apotem konstruktory innych klas podstawowych apotem konstruktory ewentualnych klas składowych ana koniec konstruktor klasy pochodnej Kolejność destruktorów jest odwrotna! Dziedziczenie Nie dziedziczy się: akonstruktora (żadnego) aoperatora przypisania (operator=) adestruktora Dziedziczenie – inicjalizacja Konstr. kopiującego (ani żadnego innego) się nie dziedziczy! Przy braku konstruktora kopiującego w klasie pochodnej kompilator wygeneruje konstruktor kopiujący automatycznie jako: klasa::klasa( klasa& ); a kopiowanie będzie przebiegało następująco: `typy wbudowane zostaną skopiowane `dla klas podstawowych zostaną uruchomione konstruktory kopiujące `dla klas składowych zostaną uruchomione konstruktory kopiujące a jeśli składnik klasy lub jej „przodek” (klasa podstawowa) posiada konstruktor kopiujący, który jest niedostępny, konstruktor kopiujący klasy pochodnej nie zostanie wygenerowany! Dziedziczenie – przypisanie Operatora przypisania się nie dziedziczy! Przy braku operatora przypisania w klasie pochodnej kompilator wygeneruje operator przypisania automatycznie jako klasa & klasa::operator=( klasa& ); a przypisanie będzie przebiegało następująco: `typy wbudowane zostaną skopiowane `dla klas składowych zostaną uruchomione ich operatory przypisania `dla klas podstawowych zostaną uruchomione ich operatory przypisania a jeśli klasa posiada składnik typu const lub referencja, operator = nie zostanie wygenerowany! a jeśli klasa posiada operator przypisania, który nie jest dostępny, operator = nie zostanie wygenerowany! Dziedziczenie – przypisanie i inicjalizacja a Automatycznie generowany konstruktor kopiujący i operator przypisania nie zawsze umożliwiają kopiowanie obiektów typu const a Aby wygenerowane funkcje umożliwiały kopiowanie obiektów typu const, muszą być zdefiniowane jako: klasa::klasa( const klasa& ); i klasa & klasa::operator=( const klasa& ); a Kompilator wygeneruje takie funkcje tylko wtedy, gdy wszystkie klasy podstawowe i składowe będą posiadały konstruktory kopiujące i operatory przypisania przyjmujące argumenty typu const Dziedziczenie – przypisanie i inicjalizacja class Regulator: public ObiektDyskretny{ public: Regulator(const Regulator &); Regulator & operator=(const Regulator &): … }; Regulator::Regulator(const Regulator &wzor): ObiektDyskretny(wzor){ Inicjalizacja klasy podstawowej // Skopiowanie pozostałej części klasy }; Dziedziczenie – przypisanie i inicjalizacja 3 sposoby wywołania operatora przypisania z klasy podstawowej: Regulator & Regulator::operator=(const Regulator &wzor){ (*this).ObiektDyskretny::operator=( wzor ); ObiektDyskretny *wskOD = this; *wskOD = wzor; // 2 sposób ObiektDyskretny &refOD = *this; refOD = wzor; // 3 sposób // Skopiowanie pozostałej części klasy }; // 1 sposób Konwersje standardowe przy dziedziczeniu aWskaźnik do obiektu klasy pochodnej może być niejawnie przekształcony na wskaźnik dostępnej jednoznacznie klasy podstawowej aReferencja do obiektu klasy pochodnej może być niejawnie przekształcony na referencję dostępnej jednoznacznie klasy podstawowej aJest to możliwe, gdy klasa podstawowa jest dziedziczona publicznie Konwersje standardowe przy dziedziczeniu – kiedy? aPrzy przesyłaniu argumentów do funkcji za pomocą referencji lub wskaźników aPrzy zwracaniu rezultatu funkcji za pomocą referencji lub wskaźników aPrzy przeładowanych operatorach – zamiast obiektu (lub referencji) klasy podstawowej można podać obiekt (lub referencję) klasy pochodnej aPrzy wyrażeniach inicjalizujących, aby zainicjalizować tę część klasy pochodnej, która jest dziedziczona Dziedziczenie class ObiektDyskretny{ public: ObiektDyskretny(); … ostream & wypiszA( ostream &wy ) const { return s_A.wypisz(wy); } ostream & wypiszB( ostream &wy ) const { return s_B.wypisz(wy); } protected: }; Wielomian s_A, s_B; … Dziedziczenie void zapiszWspWielomianow( ObiektDyskretny *ob ){ string nazwa = PytajONazwePliku(); string wiel1 = PytajONazweWielomianu( 1 ); string wiel2 = PytajONazweWielomianu( 2 ); ofstream plik( nazwa.c_str() ); if( !plik ){ throw runtime_error("Błąd otwarcia pliku!"); } plik << wiel1; ob->wypiszA( plik ); plik << wiel2; ob->wypiszB( plik ); } Dziedziczenie int main(){ class Regulator: public ObiektDyskretny{ … … zapiszWspWielomianow( &ob1 ); }; … Regulator reg1( 3, 3 ); ObiektDyskretny ob1( 2, 1, 1 ); zapiszWspWielomianow( ®1 ); } … Dzięki konwersjom standardowym! Podobna konwersja wystąpi wtedy, gdy funkcja zapiszWspWielomianow() będzie przyjmowała parametry przez referencję Dziedziczenie wielokrotne aKlasa może wywodzić się bezpośrednio od więcej niż jednej klasy podstawowej – takie dziedziczenie nazywa się dziedziczeniem wielokrotnym aZaletą takiego rozwiązania jest możliwość powiązania kilku klas o różnych właściwościach aDana klasa podstawowa może się pojawić na liście pochodzenia tylko raz aDefinicja klasy (pełna) na liście pochodzenia musi być kompilatorowi wcześniej znana Dziedziczenie wielokrotne aKażda klasa podstawowa ma określony na liście pochodzenia swój sposób dziedziczenia (private, protected, public, domyślnie private) aKonstruktory klas podstawowych zostaną uruchomione w takiej kolejności, w jakiej klasy te są umieszczone na liście pochodzenia Dziedziczenie wielokrotne class Pojazd{ public: Pojazd(); void jedź(); string silnik() const; … }; class Łódź{ public: Łódź(); void płyń(); string silnik() const; … }; Autor przeprasza za, niewłaściwe jego zdaniem, używanie polskich liter w przykładach kodu dotyczących amfibii. Jednakże odpowiedniki bez polskich liter – "Lodz", "jedz", czy "plyn" – są w tym przypadku wręcz śmieszne, i utrudniają zrozumienie sensu kodu. Dziedziczenie wielokrotne class Amfibia: public Pojazd, public Łódź{ public: Amfibia(); … }; Dziedziczenie wielokrotne aMechanizm dziedziczenia wielokrotnego można zastąpić, niemal bez zmian w składni, pewnymi ,,sztuczkami" programistycznymi aMechanizm dziedziczenia wielokrotnego przydaje się najbardziej: `w celu dziedziczenia kilku klas definiujących interfejs (zazwyczaj klas abstrakcyjnych) `w celu łączenia funkcjonalności kilku bibliotek, do których nie kodu źródłowego nie ma dostępu `w celu ułatwienia wykorzystania polimorfizmu w kilku ścieżkach dziedziczenia Ryzyko wieloznaczności a Wieloznaczność występuje wtedy, gdy składnik klasy o tej samej nazwie istnieje w więcej niż jednej klasie podstawowej: Amfibia a; cout << a.silnik() << endl; // a.Pojazd::silnik() czy a.Łódź::silnik()? a Sama definicja tak skonstruowanej klasy nie jest błędna, lecz błąd wystąpi w przypadku próby odwołania się w klasie Amfibia do funkcji składowej silnik() a Błąd wystąpi nawet wtedy, gdy funkcja składowa silnik() w jednej z klas ma zakres „private” Ryzyko wieloznaczności a W obrębie klasy pochodnej (Amfibia) można posługiwać się operatorem zakresu w celu usunięcia wieloznaczności: Pojazd::y lub Łódź::y a Takie rozwiązanie ma wady: `niejednoznaczność będzie „dziedziczona” w przypadku, gdy klasa Amfibia będzie klasą podstawową innej klasy `jeśli składnik był funkcją wirtualną, to poprzedzenie go operatorem zakresu anuluje mechanizm polimorfizmu a Lepszym rozwiązaniem jest zdefiniowanie w klasie pochodnej (Amfibia) składnika o takiej samej nazwie, który przesłoni składniki z klas podstawowych – to rozwiązanie usuwa także problemy przy dziedziczeniu klasy pochodnej Dziedziczenie wielokrotne class Amfibia: public Pojazd, public Łódź{ public: Amfibia(); string silnik() const { return Pojazd::silnik(); } … }; Uwaga! Niejednoznaczność nie wystąpi, jeżeli składniki różni długość ścieżki dziedziczenia! class Samochod: public Pojazd { … }; class Amfibia: public Samochod, public Łódź{ … }; Dziedziczenie wirtualne Dziedzic: class Podstawowa{ public: int dana; }; class Pochodna1: public Podstawowa{ }; Pochodna1: Pochodna2: Podstawowa: public: int dana Podstawowa: public: int dana class Pochodna2: public Podstawowa{ }; class Dziedzic: public Pochodna1, public Pochodna2{ public: void funkcja(){ dana = 5; } }; BŁĄD! O którą wersję składnika „dana” chodzi? Dziedziczenie wirtualne int main(){ Dziedzic *dz = new Dziedzic; Podstawowa *p; p = dz; } WIELOZNACZNOŚĆ! Na którą wersję klasy „Podstawowa” wskazać? Dziedziczenie wirtualne Rozwiązanie pierwszego problemu: class Dziedzic: public Pochodna1, public Pochodna2{ public: void funkcja(){ Oznaczenie Pochodna1::dana = 5; zakresu } }; Rozwiązanie drugiego problemu: int main(){ Dziedzic *dz = new Dziedzic; Podstawowa *p; p = (Pochodna1*)dz; } pożądanego Obydwa rozwiązania są doraźne, gdyż nie usuwają problemu istnienia dwóch wersji klasy Podstawowa wewnątrz klasy Dziedzic Jawna konwersja wskaźnika do klasy Pochodna1, potem niejawna konwersja do klasy Podstawowa Dziedziczenie wirtualne Lepsze rozwiązanie obydwu problemów: class Pochodna1 : public virtual Podstawowa{ ... Dziedziczenie }; wirtualne class Pochodna2 : public virtual Podstawowa{ ... Dziedzic: }; Pochodna1: Dziedziczenie wirtualne dotyczy klasy najbardziej podstawowej, nic nie da wirtualne odziedziczenie klas Pochodna1 i Pochodna2 Pochodna2: Podstawowa: public: dana Dziedziczenie wirtualne Pytanie: Która z klas dziedziczących wirtualnie uruchomi konstruktor klasy podstawowej? Odpowiedź: Dziedzic: Pochodna1: Pochodna2: Podstawowa: public: dana Żadna. Za uruchomienie konstruktora klasy podstawowej odpowiedzialny jest konstruktor klasy ,,najbardziej pochodnej” – w tym wypadku klasy „Dziedzic” To jedyna sytuacja, gdy za konstrukcję klasy „dziadka” odpowiada „wnuk” (lub „prawnuk”, lub „praprawnuk”, lub ...) Dziedziczenie wirtualne a Konstruktory klas wirtualnych są wykonywane zawsze przed konstruktorami innych klas podstawowych a Jeżeli klasa dziedziczy wirtualnie więcej klas, o kolejności wywołania konstruktorów decyduje lista pochodzenia a Jeżeli dziedziczenie wirtualne jest kilkupokoleniowe (wnuki, prawnuki, itd.), każdy z konstruktorów klas dziedziczących powinien zawierać wywołania konstruktora klasy dziedziczonej wirtualnie a Przy konstrukcji danego obiektu kompilator uwzględni wówczas jedynie wywołanie w klasie „najbardziej pochodnej” Dziedziczenie wirtualne a Jeżeli na liście inicjalizacyjnej klasy „najbardziej pochodnej” nie ma wywołania konstruktora klasy dziedziczonej wirtualnie, kompilator uruchomi jej konstruktor domniemany a Dlatego przy tworzeniu klasy, która będzie dziedziczona wirtualnie warto zaopatrzyć ją w domniemany konstruktor a Dziedziczenie wirtualne stosuje się zazwyczaj dziedzicząc klasy abstrakcyjne lub czysto abstrakcyjne, czyli takie, które posiadają niewiele składników-danych, lub nie posiadają ich wcale a Konstruktory klas czysto abstrakcyjnych nie posiadają zazwyczaj parametrów Dziedziczenie wirtualne aDziedziczenie wirtualne może być poprzedzone zarówno zakresem public, jak i protected i private aJeżeli klasa „wnuka” dziedziczy kilka klas, z których choć jedna dziedziczy wirtualnie w sposób publiczny, pozostałe ograniczone zakresy dziedzictwa nie mają w klasie „wnuka” znaczenia "Pimpl" - zapora ogniowa dla kompilatora Dana jest przykładowa klasa: class K{ public: // Interfejs publiczny klasy protected: // Interfejs chroniony klasy private: int s_dana; void fun(); … }; Pomimo enkapsulacji, zmiana dowolnego składnika prywatnego klasy wymusza konieczność kompilacji całego kodu, który wykorzystuje klasę K "Pimpl" - zapora ogniowa dla kompilatora Rozwiązanie: ukrycie kodu prywatnej części klasy (czyli implementacji) w oddzielnej klasie pomocniczej, dostępnej przez wskaźnik: class KImpl; class K{ public: // Interfejs publiczny klasy protected: // Interfejs chroniony klasy private: KImpl *pimpl; }; "Pimpl" - zapora ogniowa dla kompilatora a Klasa KImpl powinna zostać zapisana w oddzielnym pliku a W klasie K funkcje i dane klasy KImpl są dostępne przez wskaźnik (może to być zręczny wskaźnik) a Może istnieć konieczność zapewnienia wskaźnika zwrotnego (wskaźnika "self" w klasie KImpl wskazującego na obiekt klasy K) a Zmiana implementacji klasy KImpl nie powoduje konieczności rekompilacji kodu korzystającego z klasy K (stąd nazwa – zapora ogniowa dla kompilatora) a Technika stosowana bardzo często (wzorzec projektowy), zwana, od zwyczajowej nazwy wskaźnika do klasy implementacji, techniką "pimpl" Funkcje wirtualne class Regulator { public: float wartosc_zadana; Regulator( int dR, int dS ); virtual float symuluj( float y){ return -y; } ... }; int samonastrajanie(); Deklaracja funkcji wirtualnej Funkcje wirtualne Klasa pochodna: class RegulatorPID : public Regulator { public: virtual float symuluj( float y){ ... return P + I + D; }; ... }; może się pojawić, lecz nie jest konieczne Funkcje wirtualne Inna klasa pochodna: class RegulatorGPC : public Regulator { public: float symuluj( float y){ ... u += q*(w-y); return u; ... }; Funkcje wirtualne Funkcja pracująca z referencją do klasy Regulator: void rysuj( Regulator &dowolnyRegulator, float y ){ float u; u = dowolnyRegulator.symuluj( y ) } cout << u << endl; Funkcje wirtualne int main(){ RegulatorGPC regGPC; RegulatorPID regPID; // Obiekt klasy RegulatorGPC // Obiekt klasy RegulatorPID RegulatorMV regMV; // Obiekt klasy RegulatorMV Regulator *wskRegulatora; ... // Wskaźnik do dowolnego // regulatora wskRegulatora = ®GPC; cout << wskRegulatora->symuluj(0.1) << endl; } Która wersja funkcji symuluj() zostanie uruchomiona? Funkcje wirtualne wskRegulatora = ®PID; cout << wskRegulatora->symuluj(0.05); Czy te wywołania uruchomią tę samą wskRegulatora = ®MV; wersję funkcji cout << wskRegulatora->symuluj(0.25); symuluj()? ... rysuj( regGPC, 0.1); rysuj( regPID, 0.05); rysuj( regMV, 0.25); Którą wersję funkcji symuluj() uruchomi wywołanie wewnątrz funkcji rysuj()? Czy tę samą? Funkcje wirtualne Dzięki deklaracji funkcji symuluj(float) jako wirtualnej: wskRegulatora = ®GPC; cout << wskRegulatora->symuluj(0.1) << endl; // uruchamia funkcję RegulatorGPC::symuluj( float ) wskRegulatora = ®PID; cout << wskRegulatora->symuluj(0.05); // uruchamia funkcję RegulatorPID::symuluj( float ) wskRegulatora = ®MV; cout << wskRegulatora->symuluj(0.25); // uruchamia funkcję RegulatorMV::symuluj( float ) Gdyby nie słowo kluczowe virtual, zawsze uruchamiana byłaby funkcja Regulator::symuluj( float ) Powyższy kod wykazuje polimorfizm! Funkcje wirtualne a Funkcje wirtualne to najważniejsze, z punktu widzenia technik orientowanych obiektowo, narzędzie programistyczne a Funkcje wirtualne umożliwiają klasom pochodnym zastąpienie metod klasy bazowej swoimi własnymi wersjami a Kompilator uruchomi wersję z klasy pochodnej ilekroć obiekt, na którym pracuje, jest rzeczywiście obiektem klasy pochodnej a Ta „inteligencja” kompilatora będzie działała, gdy obiekt klasy pochodnej będzie wskazywany wskaźnikiem lub referencją do klasy podstawowej Funkcje wirtualne a W przypadku zwykłych (nie-wirtualnych) funkcji kompilator już na etapie kompilacji decyduje, jaki jest adres funkcji składowej, w zależności od wskaźnika lub referencji, przez którą następuje odwołanie (tzw. „wczesne wiązanie”) a W przypadku funkcji wirtualnych adres funkcji składowej, którą należy wykonać, nie jest określany na etapie kompilacji, lecz na etapie uruchomienia programu a Technika ta nosi nazwę „późnego wiązania” lub „dynamicznego wyboru” a Realizacja „późnego wiązania” zazwyczaj wiąże się się z obecnością dodatkowego, ukrytego wskaźnika do tablicy funkcji wirtualnych klasy, dodatkowego wskaźnika dla każdej wirtualnej funkcji oraz dwóch dodatkowych pobrań adresów Funkcje wirtualne aW efekcie stary, skompilowany do postaci binarnej, kod (np. z biblioteki), może wywołać kod nowy, nie istniejący w momencie kompilacji tego pierwszego aDzięki temu bardzo łatwo jest uzupełnić stary program o obsługę nowego typu danych aCech ta nosi nazwę rozszerzalności (ang. extensibility) aKod programu rzeczywiście może podejmować akcje zależne od obiektów, na których pracuje (orientować się obiektowo) Funkcje wirtualne Wczesne wiązanie dla funkcji wirtualnych zachodzi: a gdy funkcję wirtualną wywołuje się na rzecz konkretnego obiektu, np.: RegulatorPID regPID(1.1, 1, 0); regPID.symuluj( -0.25 ); a gdy parametr funkcji zostanie przekazany przez wartość: void zleRysuj( Regulator r, float u ){ r.symuluj( u ); … } Funkcje wirtualne Wczesne wiązanie dla funkcji wirtualnych zachodzi: a gdy wywołując funkcję przez wskaźnik lub referencję jawnie użyje się operatora zakresu, np.: wskRegulatora->Regulator::symuluj(...); (tego sposobu używa się jedynie, by wywołać funkcję wirtualną z klasy podstawowej) a gdy wywołanie funkcji wirtualnej następuje z konstruktora lub destruktora klasy podstawowej (gdy podczas konstrukcji obiektu klasy pochodnej pracuje konstruktor klasy podstawowej, obiekt klasy pochodnej jeszcze nie jest kompletny) Funkcje wirtualne a Funkcje wirtualne mogą także być umieszczone w sekcji „protected” a Deklarowanie funkcji wirtualnych w sekcji „private” rzadko ma sens a Funkcje wirtualne mogą być deklarowane jako „in-line” – mechanizm wstawiania kodu będzie dla nich uruchamiany jedynie wtedy, gdy zachodzi dla nich „wczesne wiązanie” a Funkcje wirtualne nie mogą być funkcjami statycznymi Destruktor wirtualny class RegulatorGPC : public Regulator { … }; int main(){ Regulator *wskR = new RegulatorGPC; … delete wskR; } Który destruktor zostanie uruchomiony? (Destruktor klasy pochodnej uruchamia destruktr klasy podstawowej) Destruktor wirtualny class Regulator { public: virtual ~Regulator(); ... } Pomimo różnych nazw destruktor może być wirtualny! class RegulatorGPC : public Regulator { public: ~RegulatorGPC(); ... } Zasada przybliżona: jeżeli klasa posiada jakąkolwiek funkcję wirtualną, destruktor także powinien być wirtualny! Wirtualna funkcja inline class RegulatorPID: public Regulator { public: … virtual double symuluj( double y ) { … return P+I+D; } … }; a Fakt ,,wirtualności'' przeważa nad mechanizmem inline a W przypadku, gdy dla funkcji zachodzi wczesne wiązanie, kompilator zastosuje rozwijanie kodu funkcji Funkcje wirtualne - przesłanianie class Podst { public: virtual void f( int ) { cout << "Podst::f(int)" << endl; } virtual void f( double ) { cout << "Podst::f(double)" << endl; } virtual void g( int i = 10 ) { cout << i << endl; } }; class Poch: public Podst { public: void f( vector<double> ) { cout << "Poch::f(vector)" << endl; } void g( int i = 20 ) { cout << "Poch::g() " << i << endl; } }; Funkcje wirtualne int main() { Podst pd; Poch pch; Podst* wskPd = new Poch; pd.f(1.0); pch.f(1.0); wskPd->f(1.0); pd.g(); pch.g(); wskPch->g(); delete wskPd; return 0; } Rezultat: "Podst::f(double)" Rezultat: "Poch::f(vector)"! Wyjaśnienie: deklaracja w klasie pochodnej przeładowująca funkcję wirtualną innym typem przesłoniła funkcje z klasy podstawowej W celu ,,odsłonięcia'' funkcji należy posłużyć się instrukcją ,,using" Funkcje wirtualne int main() { Podst pd; Poch pch; Podst* wskPd = new Poch; pd.f(1.0); pch.f(1.0); wskPd->f(1.0); pd.g(); pch.g(); wskPch->g(); delete wskPd; return 0; } Rezultat: "Podst::f(double)" (ponieważ dopasowanie typów funkcji przeładowanych odbywa się na statycznym typie klasy) Funkcje wirtualne int main() { Podst pd; Poch pch; Podst* wskPd = new Poch; pd.f(1.0); pch.f(1.0); wskPd->f(1.0); pd.g(); pch.g(); wskPch->g(); delete wskPd; return 0; } Rezultat: 10 Rezultat: "Poch::g() 20" Rezultat: "Poch::g() 10" (ponieważ wartości domniemane zawsze są brane ze statycznego typu klasy) Funkcje wirtualne int main() { Podst pd; Poch pch; Podst* wskPd = new Poch; pd.f(1.0); pch.f(1.0); wskPd->f(1.0); pd.g(); pch.g(); wskPch->g(); delete wskPd; return 0; } Katastrofa! Z braku wirtualnego destruktora co najmniej wyciek pamięci. Klasy abstrakcyjne class Regulator { public: virtual float symuluj( float y) = 0; ... Funkcja czysto wirtualna (ang. pure virtual) }; a Klasa zawierająca choć jedną funkcję czysto wirtualną nosi nazwę klasy abstrakcyjnej a Jeśli klasa jest abstrakcyjna, to nie można utworzyć obiektu tej klasy – taka klasa służy tylko do dziedziczenia Klasy abstrakcyjne class Regulator { public: virtual float symuluj( float y) = 0; ... }; float Regulator::symuluj( float y ){ // Tutaj, ewentualnie, wspólna część kodu } class RegulatorGPC{ public: float symuluj( float y ){ return Regulator::symuluj( y ); } ... }; Klasy abstrakcyjne a Klasa pochodna od klasy abstrakcyjnej musi zaimplementować swoje wersje wszystkich funkcji czysto wirtualnych, lub także będzie abstrakcyjna a Funkcja czysto wirtualna może mieć definicję, ale nie definicję „in-line” a Czysto wirtualny destruktor musi mieć definicję (choćby pustą)! class CzWirtualna{ public: … ~CzWirtualna() = 0; }; CzWirtualna::~CzWirtualna(){} Klasy abstrakcyjne a Definicję funkcji czysto wirtualnej można wykorzystać w celu umożliwienia twórcom klas pochodnych świadomej akceptacji domyślnego działania funkcji class K{ public: }; virtual int fun() = 0; … int K::fun(){ … } class P: public K{ public: }; int fun(){ return K::fun() } ... Klasy abstrakcyjne a Definicję funkcji czysto wirtualnej wykorzystuje się także do diagnozy sytuacji, gdy funkcja taka nie powinna zostać wywołana (dobry kompilator do tego nie dopuści) class K{ public: }; virtual int fun() = 0; … int K::fun(){ cout << "Wywołanie funkcji czysto wirtualnej, kończę!" << endl; throw logic_error(); } Klasy abstrakcyjne a Interfejs klasy (lub użytkownika) należy do najcenniejszych zasobów firmy programistycznej a Tworzenie dobrego interfejsu zazwyczaj trwa dłużej niż tworzenie klas realizujących funkcje interfejsu a Tworzeniem interfejsu zwykle zajmują się najbardziej wykwalifikowani („najdrożsi”) pracownicy firmy a Jest tak dlatego, że warto poświęcić czas na odseparowanie interfejsu od implementacji a Do tworzenia interfejsów wykorzystuje się klasy abstrakcyjne Klasy abstrakcyjne class Parametry; // Klasa przechowująca parametry regulatora class Regulator{ public: virtual float symuluj( float y) = 0; virtual float wyjscie() = 0; virtual float wartoscZadana( float w ) = 0; virtual int samonastrajanie() = 0; virtual int nastaw( Parametry *par ) = 0; }; To wszystko, co tworzy interfejs – brak składników danych! Identyfikacja typu aPozwala na określenie, w trakcie pracy programu, typu obiektu wskazywanego przez wskaźnik lub powiązanego z referencją klasy podstawowej aJest wykorzystywana poprzez operator dynamicznego rzutowania dynamic_cast oraz poprzez operator typeid aMechanizm identyfikacji typu jest rzadko (koniecznie) potrzebny Rzutowanie dynamiczne a Operator static_cast umożliwia statyczne – czyli na etapie kompilacji – przechodzenie w górę drzewa hierarchii klas (z typu klasy pochodnej na typ klasy podstawowej) a Dynamiczne – czyli na etapie wykonania programu – rzutowanie można wykonywać operatorem dynamic_cast a Dynamiczne rzutowanie można wykonywać zarówno w górę, jak i w dół drzewa hierarchii klas a Dynamiczne rzutowanie w dół drzewa hierarchii klas można wykonywać jedynie w stosunku do klas, które posiadają choć jedną funkcję wirtualną Rzutowanie dynamiczne a Operator dynamic_cast sprawdza, czy wykonywana konwersja jest poprawną konwersją w sensie przechodzenia po drzewie hierarchii klas a W przypadku, gdy konwersja nie jest prawidłowa, a konwertowany jest wskaźnik, operator zwraca wskaźnik pusty a W przypadku, gdy konwersja nie jest prawidłowa, a konwertowana jest referencja, operator zgłasza wyjątek typu bad_cast a Testy wykonywane w trakcie pracy operatora wiążą się z pewnym (zwykle nieistotnym) nakładem czasowym Rzutowanie dynamiczne void nastawy( Regulator *reg ){ … RegulatorPID *rPID = dynamic_cast<RegulatorPID*>(reg); if( rPID ){ rPID->ustawP( 1.15 ); }else{ cout << "To nie jest regulator PID!"; } } Rzutowanie dynamiczne void nastawy( Regulator ® ){ … try{ RegulatorPID &rPID = dynamic_cast<RegulatorPID&>(reg); rPID.ustawP( 1.15 ); }catch( bad_cast ){ cout << "To nie jest regulator PID!"; } } Operator typeid a Operator typeid umożliwia uzyskanie informacji o typie klasy najbardziej pochodnej wskazywanej przez pewien wskaźnik lub referencję klasy bazowej a Operator w rezultacie zwraca obiekt klasy type_info, zdefiniowanej w nagłówku <typeinfo> a Klasa type_info nie posiada publicznie dostępnych konstruktorów ani operatora przypisania a Klasa type_info definiuje metodę name(), która zwraca wskaźnik do łańcucha znakowego zawierającego nazwę klasy (format zależny od implementacji) a Klasa przeładowuje operatory == oraz !=, co umożliwia pisanie przenaszalnego kodu Operator typeid a Użycie operatora typeid na typie, który nie jest polimorficzny, powoduje zwrot statycznej informacji o typie a Użycie operatora na dereferencji wskaźnika o zerowej wartości powoduje zgłoszenie wyjątku typu bad_typeid int main(){ RegulatorGPC rGPC; Regulator *wskR = &rGPC; // Zał: klasa Regulator polimorf. cout << typeid(*wskR).name(); } if( typeid(Regulator) == typeid(*wskR) ){ cout << "Typy identyczne!"; else{ cout << "Różne typy!"; } Przestrzenie nazw (namespaces) aWydzielone przestrzenie nazw umożliwiają zgrupowanie globalnych zmiennych, funkcji i klas w jednym „pojemniku” aDzięki przestrzeniom nazw możliwe jest uniknięcie błędów wynikających z redefinicji obiektów o tych samych identyfikatorach Przestrzenie nazw (namespaces) namespace Pierwsza{ int lInt = 5; float f( int x ){ … } } namespace Druga{ float f; unsigned int lInt = 10; } int lInt = 100; int main(){ int lInt = -1; cout << Pierwsza::lInt << endl; cout << Druga::lInt << endl; cout << f; // Błąd! Brak definicji zmiennej f w tym zakr. cout << Pierwsza::f( Pierwsza::lInt ) << endl; cout << lInt << " " << ::lInt << endl; } Przestrzenie nazw (namespaces) a Przestrzenie nazw są podobne do klas a Istotne różnice to: `nie jest możliwe utworzenie obiektu przestrzeni nazw, `deklaracja przestrzeni nazw musi pojawić się w miejscu o zasięgu globalnym, bądź w innej przestrzeni nazw, `deklaracji przestrzeni nazw nie trzeba kończyć średnikiem, `definicja przestrzeni nazw może być kontynuowana w innej jednostce kompilacji, `przestrzeni nazw można nadać przezwisko (,,alias''): namespace PrzestrzenODlugiejINiewygonejNazwie{ … } namespace TaDluga=PrzestrzenODlugiejINiewygodnejNazwie; Przestrzenie nazw (namespaces) Dyrektywa using: namespace Pierwsza{ int lInt = 5; Zakres ważności to zakres bloku, float f( int x ){ … } którym dyrektywę użyto } namespace Druga{ unsigned int lInt = 10; } int main(){ using namespace Pierwsza; cout << f(lInt) << endl; // OK! cout << Druga::lInt << endl; cout << Pierwsza::lInt; // Też OK! } w Przestrzenie nazw (namespaces) Instrukcja using: namespace Pierwsza{ Zakres ważności to zakres bloku, int lInt = 5; w którym instrukcję użyto float f( int x ){ … } } namespace Druga{ unsigned int lInt = 10; } int main(){ using Pierwsza::f; cout << f(lInt) << endl; // Błąd! lInt nieznane cout << f(Druga::lInt) << endl; // OK! cout << f(Pierwsza::lInt) << endl; // Też OK! } Anonimowe przestrzenie nazw namespace{ int lInt = 5; float f( int x ){ … } } int main(){ cout << f(lInt) << endl; } // OK! a Zmienne z anonimowej przestrzeni nazw są dostępne, bez kwalifikacji, wewnątrz jednostki, w której przestrzeń zdefiniowano a Zmienne nie są dostępne w innych jednostkach kompilacji a Zaleca się używanie anonimowych przestrzeni nazw zamiast kwalifikatora static w stosunku do zmiennych globalnych Standardowe nagłówki języka C++ a Obecnie istniejący standard ISO dla języka C++ nie stosuje zaimka .h dla włączanych nagłówków standardowych a Standardowa biblioteka C++ gwarantuje 18 odpowiedników plików nagłówkowych języka C, takich jak stdio.h, stdlib.h, itp. a Pliki te mogą być włączane za pomocą #include <cxxx> lub #include <xxx.h>, gdzie xxx oznacza nazwę biblioteki (np. math, stdlib, itp.) a Dla pierwszego z tych zapisów wszystkie deklaracje są umieszczone w przestrzeni nazw std (zalecane) a W przypadku drugiego z zapisów wszystkie deklaracje są umieszczone w przestrzeni std i przestrzeni globalnej