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
}