PARADYGMATY PROGRAMOWANIA Wykład 4 Metody wirtualne i
Transkrypt
PARADYGMATY PROGRAMOWANIA Wykład 4 Metody wirtualne i
PARADYGMATY PROGRAMOWANIA Wykład 4 Metody wirtualne i polimorfizm Metoda wirualna - metoda używana w identyczny sposób w całej hierarchii klas. Wybór funkcji, którą należy wykonać po wywołaniu metody wirtualnej jest określony nie na etapie kompilacji, ale w momencie wykonania. Taka sytuacja jest możliwa, gdy pod wskaźnik typu klasy bazowej podstawiony jest obiekt jednej z klas pochodnych. Polimorfizm - zachowanie polegające na tym, że ta sama instrukcja kodu źródłowego wywołująca pewną metodę może w trakcie wykonywania w rzeczywistości wywoływać różne funkcje. Stosowane w celu: • stworzenie kodu ogólnego i jednorodnego, używanego w jednorodny sposób dla wszystkich klas hierarchii, • zwiększenie możliwości ponownego wykorzystania i dalszego rozszerzania. class Personel { public: ... void print() const; virtual float oblicz_place() = 0; ... } class PracAkord(): public Personel { int il_godz_przepr; float stawka; virtual float oblicz_place() { return il_godz_przepr * stawka; } } class PracEtat(): public Personel { float placa_mies; virtual float oblicz_place() { return placa_mies; } } class TablePersonel { public: TablePersonel( int _rozmiar ); void add( Personel *p); void print() const; float suma_wyplat() const; private: Personel **table; int rozmiar; int index; } TablePersonel::TablePersonel( int _rozmiar ): rozmiar(_rozmiar), index(0) { table = new Personel[rozmiar]; } TablePersonel::add( Personel *p ) { if ( index < rozmiar ) table[index++] = p; } void TablePersonel::print() const { for (int i=0; i < index; i++) table[i]->print(); } float TablePersonel::suma_wyplat() const { float suma; for (int i=0; i < index; i++) float += table[i]->oblicz_place(); return( suma ); } Zachowanie kompilatora związane ze statycznym zakresem wywołania funkcji: przy wykorzystaniu wskaźnika do klasy bazowej ( table[i] ) kompilator przeszukuje zbiór metod tej właśnie klasy • table[i] -> oblicz_płace() - błąd bo nie ma metody oblicz_place() w klasie Personel, • table[i] -> print() - wywołanie funkcji klasy bazowej, również wtedy gdy wskażnik table[i] wskazuje na obiekt klasy pochodnej. Polimorfizm zapewnia, że rzeczywisty typ obiektu na który wskazuje wskaźnik (a nie typ wskaźnika) określa wywołaną funkcję. Zamiast wywołania statycznego kompilator stosuje wywołanie dynamiczne wiążące wywołanie z konkretną funkcja w czasie wykonania. Zasady stosowania funkcji wirtualnych: • Funkcje wirtualne stosujemy, gdy można przewidzieć, że w każdej z klas pochodnych zostaną one przedefiniowane. • Nie ma obowiązku definiowania funkcji wirtualnej o danej nazwie w klasie pochodnej. W przypadku przetwarzania obiekt z klasy pochodnej nie redefiniującej danej metody wirtualnej zostanie wywołana metoda klasy bazowej. • Mechanizm ten działa tylko w odniesieniu do odwołania do obiektu przez wskaźnik. Składnia: • przed zwykłą deklaracją funkcji w klasie bazowej umieścić słowo kluczowe virtual, • nie ma obowiązku umieszczania tego słowa przed odpowiadającymi funkcjami wirtualnymi w klasach pochodnych, składniowo jest to jednak dozwolone i dla przejrzystości kodu ZALECANE, • prototyp funkcji wirtualnych w klasie bazowej i we wszystkich klasach pochodnych musi być taki sam. class Bazowa { public: virtual void f() ( printf( "f() - klasa bazowa\n") ); void g() ( printf( "g() - klasa bazowa\n"); } class Pochodna1 : public { public: virtual void f() ( void g() ( printf( } class Pochodna2 : public { public: virtual void f() ( void g() ( printf( } void main() { Bazowa b; Pochodna1 p1; Pochodna2 p2; Bazowa *p = &b; Bazowa printf( "f() - klasa Pochodna1\n") ); "g() - klasa Pochodna1\n"); Bazowa printf( "f() - klasa Pochodna2\n") ); "g() - klasa Pochodna2\n"); p->f(); p->g(); p = &p1; p->f(); p->g(); p = &p2; p->f(); p->g(); } Uzdatnienie poprzedniego przykładu: class Personel { public: virtual void print() const; virtual float oblicz_place() const (return 0); } Destruktor wirtualny Zaleca się dla każdej klasy bazowej posiadającej metody wirtualne zadeklarowanie również wirtualnego destruktora. - także jeśli w klasie bazowej nie wykonuje on żadnej czynności. class Bazowa { public: virtual void f() (); // brak destruktora wirtualnego } class Pochodna : public Bazowa { public: Pochodna( int _rozmiar ); ~Pochodna(); private: int *pi; } Pochodna::Pochodna( int _rozmiar) { pi = new int[ _rozmiar ]; } Pochodna::~Pochodna() { delete [] pi; } void main() { Bazowa *pb; pb = new Pochodna(10); delete pb; // mylne zwolnienie obszaru pamięci } korekta: class Bazowa { public: virtual void f() (); virtual ~Bazowa*() (); } Czyste funkcje wirtualne oraz klasy abstrakcyjne Nie ma obowiązku definiowania funkcji wirtualnej w klasie bazowej (jej deklaracja jest tylko podstawą dla kompilatora do zignorowania pozornego błędu). Taka metoda nazywa się czystą metodą wirtualną. Czystą metodę wirtualną oznaczamy pisząc po jej deklaracji =0 class Personel { public: virtual void print() const = 0; virtual float oblicz_place() const = 0; } Jeśli klasa zawiera czystą metodę wirtualną to jest ona klasą abstrakcyjną. Nie można tworzyć obiektów klas wirtualnych, ale można tworzyć wskaźniki do nich. Konwersja typów W przypadku niezgodności typów w określonym kontekście (operandy operatora, podstawienie parametrów, zwracanie wartości funkcji) gdy zgodność wymaga pojawienia się obiektu danej klasy kompilator usiłuje dokonać konwersji stosując jeden z dostępnych konstruktorów klasy. String ... nazwisko, inne_nazwisko; if (nazwisko == "KAIN") { inne_nazwisko = "KOWAL"; nazwisko = inne_nazwisko + "SKI"; } if (nazwisko.operator== String("KAIN")) { inne_nazwisko = String("KOWAL"); nazwisko = inne_nazwisko + String("SKI"); } Rodzaje konwersji automatycznych wykonywanych przez kompilator: 1. konwersje typu predefiniowanego na jedną z utworzonych klas, 2. konwersje klasy na typ predefiniowany 3. konwersje pomiędzy różnymi klasami. ad. 1) konwersje typu predefiniowanego na jedną z utworzonych klas konstruktor pewnej klasy X pobierający tylko jeden argument typu T może być wykorzystany jako funkcja konwertująca typ T w typ X, np class String { String( char *_stg); // konstruktor konwertujący typ char * w typ String ... } Reguły konwersji: 1. Najpierw sprawdzane jest czy istnieje funkcja (ewentualnie [przeciążona) lub przeciążony operator, który zaakceptuje bezpośrednio przekazane argumenty, 2. Sprawdzane jest czy można doprowadzić do zgodności typów stosując predefiniowane w C++ konwersje (np. wszelkie konwersje liczbowe int->float, float->int itd.) 3. Sprawdzana jest możliwość konwersji za pomocą funkcji zdefiniowanych w programie, przy czym sprawdzany jest tylko jeden poziom konwersji (tzn. jeśli wymagany Z a przekazany X to musi być zdefiniowane bezpośrednia konwersja X->Z, nie wystarczy łańcuch konwersji X->Y oraz Y->Z). ad. 2) konwersje klasy na typ predefiniowany Dla każdego typu T kompilator pozwala zdefiniować w klasie X funkcję skłądową operator T(), wykonującą konwersję obiektu X w obiekt typu T. class Zespolona { public: double re, im, Zespolona( double _re); Zespolona( double _re, double _im); Zespolona( void ); // konwersja typu Zespolona na typ double operator double() { return re; } } double a, b, c; Zespolona z1, z2; a a a a = = = = b b b b + + + + z1; // a = b + (double)z1 // double(z1) // z1.double(); // z1.double(); - KONWERSJA NIEJAWNA konwersja jawna - składnia klasyczna; konwersja jawna - składnia funkcyjna jawne wywołanie funkcji konwersji Funkcja konwersji nie przyjmuje żadnego argumentu ani nie deklaruje się zwracanej przez nią wartości. ad 3) konwersje pomiędzy różnymi klasami Można w typ przypadku stosować dowolną z metod 1. lub 2. Np. przy kowersji pomiędzy klasami Zespolona i T: class Zespolona { public: Zespolona( T _t ); // Konwersja T -> Zespolona operator T() // Konwersja Zespolona -> T }