Programowanie obiektowe
w C++
Wykład 07 Temat wiodący:
Wzorce (szablony) klas, polimorfizm, funkcje wirtualne
Wzorce
Co dają wzorce klas ?
n Pisząc programy często korzystamy z abstrakcyjnych typów danych, takich jak stos, kolejka czy drzewo. Implementacje takich typów mogą być prawie identyczne, na przykład klasy lista_liczb i lista_znaków mogą różnić się tylko typem elementu przechowywanego na liście.
n Wzorzec klasy to sposób na napisanie uogólnionej .
sparametryzowanej klasy — klasy której parametrem będzie typ, bądź inna klasa. Można napisać wzorzec listy, a potem w zależności od tego czego aktualnie potrzebujemy
zadeklarować listę znaków, bądź listę figur.
n Wzorce są doskonalszym i wygodniejszym sposobem (od stosowania preprocesora) tworzenia rodzin typów i funkcji.
n Wzorce nazywane są również szablonami (ang. templates).
Wzorce klas – jak deklarować?
template <class T> // wzorzec, którego argumentem jest typ T class stos // wzorzec klasy stos
{
T* v; // początek stosu T* p; // koniec stosu int rozm; // pojemność stosu public:
stos (int r) {v = p = new T[rozm=r];} // konstruktor z argumentem:
// rozmiar stosu ~stos () {delete[]v;} // destruktor
void wstaw(T a) {*p++ = a;} // wstaw na stos T zdejmij() {return *--p;} // zdejmij ze stosu
int rozmiar() const {return p-v;} // ile elementów typu T jest na stosie };
Wzorce klas – jak deklarować?
template <class T> // deklarujemy wzorzec,
// którego argumentem jest typ T class stos // wzorzec klasy stos
n Typu (klasy) T można używać do końca deklaracji klasy stos tak jak każdego innego dostępnego typu lub klasy.
n W zakresie wzorca „template<T> stos<T>” używanie pełnej nazwy typu „stos<T>” jest nadmiarowe zarówno przy
definicji metod wewnątrz klasy jak i poza nią, wystarczy „stos”
zarówno dla określenia klasy jak i nazw konstruktorów i destruktora.
Wzorce klas – jak deklarować?
n
Mając wzorzec klasy stos można deklarować stosy różnych elementów przekazując typ elementu jako aktualny parametr wzorca.
n
Składnia nazwy typu wywiedzionego ze wzorca jest następująca:
nazwa_klasy_wzorca<argument_wzorca>
n
Nazwa klasy stosu liczb:
stos<int>
n
Nazwa klasy stosu wskaźników do figur:
stos<figura *>
Wzorce klas
n
Deklaracja:
stos<int> liczby(100);
n
to deklaracja obiektu o nazwie liczby,
n
należącego do klasy stos<int>,
n
oraz wywołanie konstruktora stos<int>(100).
n
Nazwy klasy utworzonej ze wzorca można używać tak samo jak nazwy każdej innej klasy,
różnica to inna składnia nazwy.
stos<figura*> spf(200); // stos wskaźników do figur zdolny // pomieścić 200 wskaźników stos<Punkt> sp(400); // stos punktów o pojemności 400 void f(stos<complex> &sc) // funkcja f, której argumentem jest // referencja do stosu liczb zespolonych {
sc.wstaw(complex(1,2)); // wstaw do stosu przekazanego jako argument // funkcji liczbę zespoloną
complex z = 2.5 * sc.zdejmij(); // zdejm liczbe ze stosu, // pomnóż ją i przypisz stos<int> *p=0; // deklaracja wskaźnika do // stosu liczb całkowitych
p=new stos<int>(800); // konstrukcja stosu 800 liczb całkowitych for (int i=0; i<400; i++) // 400 razy
{
p->wstaw(i); // wstaw liczbe i do stosu liczb sp.wstaw(Punkt(i,i+400)); // i punkt do stosu punktów }
delete p; // destrukcja stosu liczb }
Wzorce
n
Kompilator sprawdza poprawność wzorca w momencie jego użycia, a więc błędy w samej
deklaracji wzorca mogą pozostać niezauważone aż do momentu próby wykorzystania wzorca.
n Poprawna kompilacja pliku zawierającego wzorzec nie oznacza że wzorzec nie zawiera błędów.
n Częstą praktyką jest najpierw uruchomienie konkretnej klasy, np. stos_znaków, a potem przekształcenie jej w klasę ogólną - wzorzec stos<T>.
Wzorce
n W wcześniejszej wersji wzorca wszystkie metody są inline — zdefiniowano je wewnątrz deklaracji klasy. Można we wzorcu nie definiować metod:
template <class T>
class stos {
T* v; // początek stosu T* p; // koniec stosu int rozm; // pojemność stosu public:
stos (int r); // deklaracja: konstruktor z argumentem: rozmiar stosu ~stos ();
void wstaw(T a); // deklaracja: wstaw na stos T zdejmij(); // deklaracja: zdejmij ze stosu
int rozmiar(); // deklaracja: ile elementów typu T jest na stosie };
Wzorce
n Jeżeli metody wzorca definiujemy poza definicją klasy wzorca to musimy użyć dla każdej z metod słowa kluczowego template:
// definicja metody wstaw template<class T>
void stos<T>::wstaw(T a) {
*p++ = a;
};
// konstruktor template<class T>
stos<T>::stos(int r) {
v = p = new T[rozm=r];
};
Wzorce
n Przypomnienie: W zakresie wzorca
template<T> stos<T>”
używanie pełnej nazwy typu „stos<T>” jest nadmiarowe zarówno przy definicji metod wewnątrz klasy jak i poza nią.
n Wystarczy „stos” zarówno dla określenia klasy jak i nazw konstruktora i destruktora („<T>” jest domyślne).
n Poniższy wzorzec jest błędny:
// template<class T>
// stos<T>::stos<T>(int r) // to jest traktowane jako błąd, // // powinno być stos<T>:: stos(int r) //{
// v = p = new T[rozm=r];
//};
Rozbudowywanie klas-wzorców
n
Wzorca który jest już napisany i wykorzystywany nie należy modyfikować — modyfikacje te będą
dotyczyły wszystkich klas stworzonych w oparciu o ten wzorzec.
n Gdy dodamy zmienne klasowe to powiększą się obiekty wszystkich tych klas wywiedzionych ze wzorca.
n Gdy zmienimy definicje metod to zmiany będą dotyczyły wsystkich klas wywiedzionych ze wzorca.
n
Zatem, zamiast modyfikacji wzorca danej klasy należy utworzyć wzorzec klasy pochodnej, o nowych właściwościach.
Rozbudowywanie klas-wzorców
n Np.: potrzebujemy stosu łańcuchów z możliwością zapisu i odczytu do pliku template<class T>
class stos_plik: public stos<T>
{
char * nazwa_pliku;
public:
/* konstruktor, parametry: rozmiar i nazwa pliku */
stos(int rozmiar, char * nazwa = NULL) :stos<T>(rozmiar) // konstrukcja rodzica
{ // tutaj, lub za pomocą listy inicjalzacyjnej zachowaj nazwę pliku }
void zapisz_stos();
void wczytaj_stos();
Wzorzec szczegółowy
n Jeżeli wzorzec działa niepoprawnie dla jakiegoś szczególnego parametru, to można zdefiniować inną wersję wzorca dla konkretnego parametru. Np. klasa która służy do porównywania elementów danego typu:
template<class T>
class porównywacz // wzorzec ogólny {
public:
static mniejszy(T &a, T &b) {
return a<b;
} };
n Powyższe jest poprawne dla typów takich, jak int czy char. Dla łańcuchów (char *) porównywane by były nie łańcuchy, ale ich adresy,
Wzorzec szczegółowy
n Dla łańcuchów (char *) porównywane by były nie łańcuchy, ale ich adresy, więc definiujemy szczególną postać wzorca klasy porównywacz dla łańcuchów:
class porównywacz<char *> // wzorzec szczegółowy {
public:
static mniejszy(const char * a, const char * b) {
return strcmp(a, b)<0;
} };
n Kompilator wykorzysta szczególną postać wzorca, jeżeli w miejscu gdzie będzie potrzebna, będzie widoczna (czyli zadeklarowana wcześniej). W przeciwnym przypadku zostanie rozwinięty wzorzec ogólny.
Argumenty wzorca
n Argumentów wzorca może być wiele, oprócz klas i typów mogą to być napisy, nazwy funkcji lub wyrażenia stałe.
n Np. wzorzec bufora, którego parametrem będzie rozmiar:
template<class T, int rozm>
class bufor {
T w[rozm];
// ...
}
n taki wzorzec bufora wykorzystujemy np. tak:
bufor<figura, 250> tbf; // deklaracja obiektu f będącego // buforem na 250 figur bufor<char,100> tbc; // bufor na 100 znaków
Wzorce
n Dwa typy wygenerowane ze wspólnego wzorca są identyczne jeżeli identyczne są argumenty wzorca, w przeciwnym
przypadku są różne i nie wiąże ich pokrewieństwo.
n Na przykład dla następujących deklaracji tylko obiekty tbc0 i tbc1 należą do tej samej klasy (klasy bufor<char, 100>) pozostałe obiekty do obiekty różnych klas.
bufor<char,100> tbc0;
bufor<figura, 250> tbf0;
bufor<char,100> tbc1;
bufor<figura, 300> tbf1;
Wzorce funkcji
template <class T> // jesteśmy poza deklaracją klasy void zamień(T &x, T &y) // nie metoda, a funkcja
{
T t=x;
x=y;
y=t;
}
int a=7,b=8;
zamień(a,b); // kompilator rozwinie wzorzec //(jeżeli jest widoczny)
Wzorce funkcji - przykład
n napisać rodzinę funkcji zwiększających wartość swojego pierwszego argumentu aktualnego o wartość drugiego argumentu (oba to typy liczbowe)
template <class t>
void zwieksz(t &i, double d) // zadzaiala dla wszystkich typów liczbowych
{ // ale jak ktos wywola zwieksz(1, 1)
i+=t(d); // to będą 2 automatyczne konwersje
}; // nieekologiczne --- marnotrawstwo czasu
template <class t, class d>
void zwieksz_szybciej (t &i, const d delta) // const nie zaszkodzi
{ // a może się przyda
i+=t(delta);
};
Wzorce funkcji - przykład
n napisać rodzinę funkcji zwiększających wartość swojego pierwszego argumentu aktualnego o wartość drugiego argumentu (oba to typy liczbowe), lub o 1 gdy nie podano drugiego argumentu.
// template <class t, class d>
// void zwieksz_1 (t &i, const d delta=1) // …
n Pułapka: po napotkaniu wywołania
zwieksz_1(20.30, 1);
kompilator nie ma podstaw do określenia typu d!
Wzorce funkcji - przykład
n napisać rodzinę funkcji zwiększających wartość swojego pierwszego argumentu aktualnego o wartość drugiego argumentu (oba to typy liczbowe), lub o 1 gdy nie podano drugiego argumentu.
template <class t, class d>
void zwieksz_1(t &i, const d delta) {
i+=t(delta);
};
template <class t>
void zwieksz_1(t &i) {
i+=t(1);
};
Metody wirtualne
Potrzeba metod wirtualnych
n
Przy dziedziczeniu w C++ dla wskaźników i referencji dozwolona jest konwersja, ale:
n
przez taki wskaźnik lub referencję można odwoływać się jedynie do danych
zadeklarowanych w klasie bazowej, oraz jedynie do metod klasy bazowej
n
na podstawie klasy wskaźnika/referencji
kompilator zdecyduje o wywołaniu metody kl.
bazowej nazwet jeżeli obiekt jest klasy potomnej.
class punkt {
int x,y;
public:
void pokaz(); //rysuje punkt void ukryj();
};
class okrag: public punkt {
int r;
public:
void pokaz(); //rysuje punkt void ukryj();
};
okrag o;
punkt &rp=o;
rp.pokaz(); //punkt::pokaz
Potrzeba metod wirtualnych
okrag o;
punkt &rp=o;
rp.pokaz(); // niech wywoła się okrag::pokaz
n
jak to zrealizować?
class punkt {
int x,y;
public:
char klasa;
void pokaz(); //rysuje punkt
punkt(int x, int y) :x(x), y(y), {
klasa=‘p’;
} };
n Rozwiązanie niedoskonałe
n Wadliwe
class okrag: public punkt {
int r;
public:
void pokaz(); //rysuje punkt
okrag(int x, int y, int r) :punkt(x,y), r(r) {
klasa=‘o’;
} };
okrag o;
punkt &rp=o;
if (rp.klasa==‘p’) rp.punkt::pokaz();
else
rp.okrag::pokaz();
Metody wirtualne
n
Jeżeli zadeklarujemy metodę jako wirtualną to kompilator uzupełni obiekty o pole determinujące klasę obiektu i przy wywoływaniu wybranych przez nas metod wywoła metodę z właściwej klasy.
void virtual punkt::ukryj();
n
Metodę (albo operator) wystarczy raz zadeklarować
jako wirtualny, w klasach pochodnych możemy, ale
nie musimy używać słowa kl. virtual.
class punkt {
int x,y;
public:
void virtual pokaz();
void virtual ukryj();
};
class okrag: public punkt {
int r;
public:
void pokaz(); // virtual void ukryj(); // virtual };
Metody wirtualne
Metody wirtualne - działanie
n Do pierwszej klasy w której w hierarchii klas pojawi się metoda wirtualna dodane zostanie dodatkowe niejawne pole
— adres tablicy metod wirtualnych.
n zwiększy się rozmiar obiektów tej klasy.
n Dla obiektu, którego klasy nie można jednoznacznie określić na etapie kompilacji, odwołania do metody, bądź metod zadeklarowanych jako wirtualne będą się odbywały pośrednio poprzez tablicę metod wirtualnych
n będzie to działało wolniej niż odwołanie bezpośrednie,
n metody wirtualne nie będą rozwijane inline,
n będzie to działało szybciej, niż gdybyśmy taką sztuczkę robili ręcznie,
Metody wirtualne - działanie
n
Odwołania przez wskaźnik i referencje będą pośrednie
n
Odwołania przez kwalifikację obiektem będą bezpośrednie
n a więc szybsze, metody (nawet zadeklarowane z virtual) mogą być rozwijane inline.
n
Uwaga: metody klasy mogą zostać odziedziczone i aktywowane na rzecz obiektu klasy pochodnej, a więc odwołania do wirtualnych metod danej klasy z innych metod tej klasy będą też pośrednie!
class punkt {
int x,y;
public:
void virtual pokaz();
void virtual ukryj();
void przesun(int dx, int dy) {
ukryj(); // wirtualna w punkt x+=dx;
y+=dy;
pokaz(); // wirtualna w punkt }
};
class okrag: public punkt {
int r;
public:
void pokaz();
void ukryj();
};
okrag o;
punkt &rp=o;
rp.pokaz(); //okrag::pokaz rp.przesun();
//punkt::przesun wywoła //okrag::pokaz i okrag::ukyj !!!
Klasa polimorficzna
n
klasa polimorficzna to taka w której występuje przynajmniej jedna metoda wirtualna
n
Przykład korzyści z polimorfizmu:
n deklarujemy listę przechowującą wskaźniki do punktów (klasa polimorficzna) - na liście umieszczać punkty okręgi i inne figury.
n przez wskaźniki możemy pokazać wszystkie figury (wywołując metodę wirtualną pokaz() – pośrednio), możemy też przesuwać figury – wyw. się niewirtualna metoda przesuń, ona wywoła właściwe, bo wirtualne pokaz i ukryj.
Wczesne i późne wiązanie
n
Wczesne wiązanie:
n
gdy metoda nie jest wirtualna lub jest wirtualna ale można określić z której klasy ma pochodzić to nazwa metody jest kojarzona z jej kodem
(wywołanie metody albo nawet rozwinięcie inline) już na etapie kompilacji/linkowania.
n
Późne wiązanie
n
decyzja co do wyboru klasy z zakresu której
metodę wykonać, zostaje podjęta podczas biegu
programu.
Wczesne i późne wiązanie
n
Metody nie-wirtualne – zawsze wczesne wiązanie.
n
Zadeklarowanie metody jako wirtualnej nie wyklucza jej wczesnego wiązania, nastąpi ono, gdy:
n jawnie (operatorem zakresu) podamy o którą klasę nam chodzi,
n wywołamy metodę bezpośrednio na rzecz obiektu (nie przez wskaźnik lub referencję).
Metody wirtualne
n
zaleta: ogromna łatwość rozbudowy programu,
n
Pisząc kod możemy wykorzystywać metody których jeszcze nie napisano !
n
Nie musimy powielać takiego samego kodu w metodach różnych klas (vide przesun()).
n
Nie grozi nam „uzupełnienie” już gotowego kodu
o nowe błędy.
Konstruktory i destruktory
n
Konstruktor nie może być wirtualny (dlaczego?)
n
Destruktor może i czasami powinien być virtual (dlaczego?)
n
Uwaga: Gdy w jakiejś klasie zadeklarujemy destruktor jako wirtualny, to w klasach
pochodnych destruktory też będą wirtualne (mimo że ich nazwy w klasach pochodnych będą inne).
Static i virtual
n
Metoda statyczna nie może być wirtualna
(dlaczego?).
Dziedziczenie metod wirtualnych
n meoda wirtualna może zostać odziedziczona przez klasę pochodną
n może zostać przedefiniowana, w jej ciele możemy wywołać metodę wirtualną klasy bazowej.
n np.:
void okrag::pokaz() {
punkt::pokaz(); // narysuj środek okręgu //narysuj okrąg
}
Przeciążanie a wirtualność
n
wirtualność dotyczy tylko tej metody/operatora która została w danej klasie lub przodku
zadeklarowana jako virtual,
n
inne metody (o innych parametrach) są zwykłymi metodami/operatorami.
n
np. metoda nie wirtualna:
void punkt::pokaz(char * opis){...};
Zaprzyjaźnianie a wirtualność
n
Wirtualność jest niezależna od zaprzyjaźniania.
n
Zaprzyjaźnianie nie jest przechodnie,
n
Zaprzyjaźnianie dotyczy tylko tej metody (klasa::metoda) która została zaprzyjaźniona,
n
Metoda przedefiniowana w klasie pochodnej, wirtualna czy nie, nie będzie automatycznie zaprzyjaźniona.
Widoczność metod wirtualnych
n
Widoczność jest rozstrzygana na etapie kompilacji
n zatem decyduje typ wskaźnika/referencji.
n
np., przyjmijmy, że wszystkie składowe klasy okrąg sa prywatne:
okrag o;
punkt &rp=o;
rp.pokaz(); // OK. – dlaczego?
Klasa abstrakcyjna
n
Klasa abstrakcyjna, to klasa, która nie zawiera żadnych obiektów.
n
Klasa abstrakcyjna służy do definiowania interfejsu/cech wspólnych rodziny innych klas (jej potomków)
n
np. klasa abstrakcyjna „figura” „liczba”
n
Dziedziczenie klas abstrakcyjnych
Metoda czysto wirtualna
n