120 likes | 223 Views
Szablony (wzorce). Przykład 1: Szablon klasy - klasa implementująca wektory (prawie) dowolnego typu. template <class T > class Wektor { protected: T * v; // właściwy wektor - tablica int sz; // rozmiar public: Wektor (int); // konstruktor z rozmiarem
E N D
Szablony (wzorce) • Przykład 1: Szablon klasy - • klasa implementująca wektory (prawie) dowolnego typu. • template <class T > • class Wektor { • protected: • T * v; // właściwy wektor - tablica • int sz; // rozmiar • public: • Wektor (int); // konstruktor z rozmiarem • T& operator [ ] (int); // dostęp do elementów z kontrolą • T& elem (int i) const { return v [ i ]; } // Uwaga na const! • .... • }; // pełna implementacja dalej • Wektor <int> v1 (20), v2(20); • Wektor <complex<float> > v3 (10); • Wektor <string> vs(10); • Wektor <Ksztalt*> figury(20); // Wektor wskaźników - to może nie działać • Wektor <int> v4 = v1+v2; // Jest zdefiniowane dodawanie • Przykład 2: Szablon funkcji - uniwersalna funkcja sortująca. • template <class T > void sort (Wektor <T> & v); • To jest rodzina nieskończenie wielu funkcji. • Funkcję przy wywołaniu wyznacza się stosując przeciążanie. • Wektor <complex<double> > cv (10); • Wektor <string > cs (30); • sort (cv); // wywołuje funkcję sort (Wektor<complex<double> >) • sort (cs); // wywołuje funkcję sort (Wektor<string>)
Licznik template<class Count_Type> class Count { public: // Konstruktory/Destruktory Count() { } virtual ~Count() { } // Funkcje implementujące akcje obiektów virtual void increment() = 0; virtual void decrement() = 0; void reset() {value = reset_value;} // Funkcje dające dostęp do prywatnych atrybutów obiektów Count_Type get_value() { return value; } void set_value(Count_Type new_value) { value = new_value; } Count_Type get_reset_value() { return reset_value; } void set_reset_value(Count_Type new_value) { reset_value = new_value; } protected: Count_Type value; Count_Type reset_value; }; class Integer_Count : public Count<int> { public: Integer_Count() { reset_value = 0; reset(); } Integer_Count (int new_value) { reset_value = 0; value = new_value; } ~Integer_Count() { } void increment(){value++;} void decrement(){value--;} char* asBase(int number_base); };
Przykład - wektory template <class T > class Wektor { protected: T *v; // właściwy wektor - tablica int sz; // rozmiar public: Wektor(int); // konstruktor z rozmiarem Wektor(const Wektor&); // konstruktor kopiujący virtual ~Wektor() { delete[] v; }; // destruktor int size() const { return sz; } void set_size ( int ); // ustawia rozmiar (nie ma tu impl.) T& operator[] (int); // dostęp do elementów z kontrolą T& elem(int i) const // dostęp do elementów bez kontroli { return v[ i ]; } // dla kompilatora ta metoda jest const (sic!) Wektor operator+(const Wektor&); // tworzy wektor będący sumą wektorów Wektor& operator=(const Wektor& ); // przypisanie }; template <class T> Wektor <T>:: Wektor(int s) { if (s < 0 ) blad ("Zły rozmiar"); sz = s; v = new T[ s ]; // Wymaga konstruktora bezargumentowego w T } template <class T> Wektor <T>:: Wektor (const Wektor <T>& w) { sz = w .size(); v = new T [sz ]; for (int i = 0; i < sz; i++) v [ i ] = w. elem (i); }
template <class T> • Wektor <T>& Wektor<T>::operator=(const Wektor <T>& w) • { • if (sz != w.size()) • blad("Niezgodne rozmiary wektorów"); • for (int i = 0; i < s; i++) • elem( i ) = w.elem( i ); • } • template <class T> • T& Wektor<T>::operator[](int i) • { • if (i < 0 || i >= sz) blad("Przekroczenie zakresu tablicy"); • return v[ i ]; • } • template <class T> • Wektor<T> Wektor<T>::operator+ (const Wektor<T>& a) • { • int s = size(); • if (s != a.size()) blad("Zły rozmiar wektora"); • Wektor<T> suma(s); // nowy wektor • for (int i = 0; i < s; i++) • suma.elem(i) = elem(i) + a.elem(i); • return suma; • } • Uwagi: • - zamiast funkcji blad lepiej użyć obsługi wyjątków, • - const w elem jest poprawne zwn C++, choć niekoniecznie • zwn na nasze oczekiwania, [] też można było zadekalrować • jako const, • - zwykle dla wskaźników wolelibyśmy mieć inną realizację, można • to dookreślić: • template <class T> • class Wektor<T*> {....};
ios istream ostream iostream Dziedziczenie wielokrotne(dziedziczenie wielobazowe) Chcemy mieć graf (acykliczny) dziedziczenia, a nie drzewo. • class A { ...}; • class B : { ... }; • class C : public A, public B { ... }; // klasa C dziedziczy po A i po B • Najważniejsze Problemy: • W jaki sposób wyszukiwać funkcje wirtualne? • Rozwiązanie w C++: • Jakakolwiek niejednoznacznośc jest błędem. • Może być tylko jedna możliwa funkcja do wyboru.
a z B b a z C c d a b c d Powtarzanie się klas. class A { int a; }; class B : public A { int b; }; class C: public A { int c; }; class D : public B, public C { int d; }; warstwa B wartstwa C Użycie zmiennej a jest niejednoznaczne. Trzeba kwalifikować zmienną a nazwą klasy (np. B :: a ). Pytanie: czy chcemy, żeby były dwa “egzemplarze” warstwy klasy A, czy jeden? Na ogół chcemy jeden. class A { int a; }; class B : virtual public A { int b; }; class C: virtual public A { int c; }; class D : public B, public C { int d; }; warstwa A warstwa B wartstwa C warstwa D class ios { .... }; class isrtream : virtual public ios { ... } class ostream : virtual public ios { ... } class iostream : public iostream, public ostream { ... }
Programowanie Obiektowe • Program = zbiór obiektów współpracujących poprzez komunikaty (funkcje) • Używamy abstrakcyjnych typów danych • Projektując program projektujemy klasy i akcje obiektów • jakie mają być klasy • jakie mają być hierarchie klas • co obiekt ma robić (jakie funkcje mają mieć klasy) • Nie interesuje nas podział na funkcje (jak w programowaniu strukturalnym). Funkcje są wyznaczone w naturalny sposób przez akcje obiektów • Atrybuty obiektów są chronione (niedostępne z zewnątrz) • Projektujemy klasy tak, aby mogły być wielokrotnie użyte (czyli dosyć ogólne). Specjalizujemy klasy poprzez dziedziczenie • Używamy funkcji wirtualnych - wtedy każdy obiekt wykonuje akcje sobie właściwe
Obsługa wyjątków • Co to jest wyjatek? • To jest wyjątkowa sytuacja w czasie wykonywania operacji • ( funkcji), której nie potrafimy obsłużyć na danym poziomie • abstrakcji, czyli w tej funkcji. • Operacja, która wykryła wyjątkową sytuację, zwykle nie rozumie • dobrze jej znaczenia. Sensowne jest więc przekazanie informacji • do “wyższej instancji”, czyli do funkcji, która tę operacją (funkcję) • wywołała. Nazywa się to zgłoszeniem wyjątku. • Funkcja, która wykonuje operację zgłaszającą wyjątek może go • obsłużyć (wyłapać), zignorować (czyli zostawić do obsłużenia przez • “jeszcze wyższą instancję”) lub propagować (przekazać jeszcze wyżej). • Wyjątki nie obsłużone są obsługiwane przez system (jako błędy) • i na ogół powodują zakończenie programu. • Po co obsługuje się wyjątki? • Zasada programu (systemu) bezpiecznego i odpornego na błędy: • Program nigdy nie ma prawa “paść” w sposób • niekontrolowany. • Program zawsze musi wypisać komunikat zrozumiały dla • użytkownika. • Przykłady wyjątków: • 1. próba pobrania czegoś z pustego stosu • funkcja używająca stosu sama powinna zdecydować, co to znaczy • 2. przepełnienie stosu (np. implementowanego w tablicy) • może funkcja działająca na stosie potrafi temu zaradzić? • 3. brak pliku otwieranego do czytania • może zwykle taki plik jest, ale jak nie ma, to trzeba go założyć
Moduł implementujący abstrakcyjną strukturę danych (np. stos) nie • powinien zawierać obsługi błędów, bo dla użytkownika tej struktury • to mogą być błędy innego rodzaju. • Przykład: • Funkcja zamieniająca wyrażenie na ONP. Przy poprawnym • wyrażeniu stos nigdy nie będzie pusty, więc nie warto • obciążać funkcji sprawdzaniem tego. • Próbę zdjęcia czegoś z pustego stosu warto obsłużyć jako • wyjątek - niepoprawne wyrażenie. • Co dają mechanizmy obsługi wyjątków? • szansę zareagowania na każdą sytuację we właściwy sposób • wydzielenie części obsługującej wyjątki z właściwego programu • C++ • Klasa wyjątków - opisuje wyjątki jakiegoś rodzaju. • throw wyjatek - zgłasza wyjątek • try { • ..... • f ( ... ) • } • catch (< klasa wyjątku1 > ) • {... • // kod obsługi wyjątku1 zgłoszonego w trakcie • // wykonywania funkcji f • } • catch ( < klasa wyjątku2 > ) • { • // kod obsługi wyjatku 2 • } • ....... // dalszy ciąg programu - wyjątki nas nie interesują
template <class T> • class Stos { • private: • T * top; • int max_size; • T * s; • public: • Stos (int n = 10 ) { s = top = new T [size = n]; } • class Empty { } ; // wyjatek - pusty stos • class Overflow { }; // wyjątek - przepełnienie stosu • int empty { return top == s ;} • void push (T & elem ) • { if (top > s + max_size - 1) throw Overflow ( ); • s [ top++ ] = elem; } • T & pop ( ) • { if (top == s) throw Empty ( ); • return s [ -- top ]; } • }; • class Koniec { }; // wyjątek powodujący zakończenie programu • void ONP ( ... ) • { • Stos < char > S (100); • try { .... • S.push (x); ... x = S.pop ( ); • } • catch (Stos <char>:: Overflow) • { cout << “Za długie wyrażenie \n“; • throw Koniec ();} • catch (Stos <char>:: Empty) • { // pominięcie wyrażenia do końca • cout << “Niepoprawne wyrażenie \n”; } • } • void main () • { • .... try { ... ONP ( ... ) ...} catch (Koniec) { } • // akcje końcowe - zamykanie plików itp • }
void G () • { try { // .... • F (... ); • // .. • } • catch (Wyjatek ) { • // ... obsługa możliwa do wykonania na poziomie funkcji G • throw ; // propagacja tego samego wyjątku wyżej • } • // ... • } • void H ( ) • { try { // .... • G ( ); • // .... • } • catch (Wyjątek ) { // ... obsługa na poziomie funkcji H } • } • Wyjątek to jest obiekt - może mieć atrybuty • class Wektor { • // .... • public: • class Zakres { // wyjatek - przekroczenie zakresu • public: • int indeks; // indeks przekraczający zakres • Zakres ( int i) { indeks = i; } • }; • int & Wektor :: operator [ ] (int i) • { if (0 <= i && i < rozm ) return p[i]; • throw Zakres (i); • }
void f (Wektor & w) • { • // ... • try { • zrób_coś (w); ... • } • catch (Wektor :: Zakres z ) • { cerr << “zły indeks “ << z.indeks << ‘\n’; } • // ... • } • Wektor :: Zakres z deklaracja wyjątku • Określa, jaki rodzaj (typ) wyjatku jest to wyłapywany. • Właściwym wyjątkiem jest z. • Wyjątek zgłoszony, ale nie obsłużony powoduje wywołanie funkcji • terminate () • kończącej program. • Można określić, jakie wyjątki może zgłaszać dana funkcja: • void f ( ...) throw (x2, x3, x4) • { ... } • Funkcja f może zgłaszać tylko wyjatki x2, x3, x4. Próba zgłoszenia • czegoś innego powoduje wywołanie funkcji unexpected () kończącej • program (przez wywołanie funkcji terminate() ). • Jeśli nie ma tego w deklaracji funkcji, to może ona zgłosić każdy wyjątek. • void f ( ) throw (); // nie zgłasza żadnych wyjątków • Tego typu deklaracje są ważne dla funkcji zewnętrznych (bibliotecznych)