w07-szablony

Transkrypt

w07-szablony
Programowanie
obiektowe
w C++
Wykład 07
Temat wiodący:
Wzorce (szablony) klas, polimorfizm,
funkcje wirtualne
Wzorce
1
Co dają wzorce klas ?
n
n
n
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.
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.
Wzorce są doskonalszym i wygodniejszym sposobem (od
stosowania preprocesora) tworzenia rodzin typów i funkcji.
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
};
2
Wzorce klas – jak deklarować?
template <class T>
class stos
// deklarujemy wzorzec,
// którego argumentem jest typ T
// 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
n
n
n
Mając wzorzec klasy stos można deklarować stosy
różnych elementów przekazując typ elementu jako
aktualny parametr wzorca.
Składnia nazwy typu wywiedzionego ze wzorca jest
następująca:
nazwa_klasy_wzorca<argument_wzorca>
Nazwa klasy stosu liczb:
stos<int>
Nazwa klasy stosu wskaźników do figur:
stos<figura *>
3
Wzorce klas
n
Deklaracja:
stos<int> liczby(100);
n
n
n
n
to deklaracja obiektu o nazwie liczby,
należącego do klasy stos<int>,
oraz wywołanie konstruktora stos<int>(100).
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<Punkt> sp(400);
// stos wskaźników do figur zdolny
// pomieścić 200 wskaźników
// 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
}
4
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
n
Poprawna kompilacja pliku zawierającego wzorzec nie
oznacza że wzorzec nie zawiera błędów.
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
};
5
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
n
Wystarczy „stos” zarówno dla określenia klasy jak i nazw konstruktora i
destruktora („<T>” jest domyślne).
Poniższy wzorzec jest błędny:
// template<class T>
// stos<T>::stos<T>(int r)
//
//{
// v = p = new T[rozm=r];
//};
// to jest traktowane jako błąd,
// powinno być stos<T>:: stos(int r)
6
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
n
n
Gdy dodamy zmienne klasowe to powiększą się obiekty
wszystkich tych klas wywiedzionych ze wzorca.
Gdy zmienimy definicje metod to zmiany będą dotyczyły
wsystkich klas wywiedzionych ze wzorca.
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();
};
7
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.
8
Argumenty wzorca
n
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.
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;
bufor<char,100> tbc;
// deklaracja obiektu f będącego
// buforem na 250 figur
// 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;
9
Wzorce funkcji
template <class T>
void zamień(T &x, T &y)
{
T t=x;
x=y;
y=t;
}
// jesteśmy poza deklaracją klasy
// nie metoda, a funkcja
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)
{
i+=t(d);
};
// zadzaiala dla wszystkich typów liczbowych
// ale jak ktos wywola zwieksz(1, 1)
// to będą 2 automatyczne konwersje
// nieekologiczne --- marnotrawstwo czasu
template <class t, class d>
void zwieksz_szybciej (t &i, const d delta)
{
i+=t(delta);
};
// const nie zaszkodzi
// a może się przyda
10
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);
};
11
Metody wirtualne
Potrzeba metod wirtualnych
n
Przy dziedziczeniu w C++ dla wskaźników i
referencji dozwolona jest konwersja, ale:
n
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
na podstawie klasy wskaźnika/referencji
kompilator zdecyduje o wywołaniu metody kl.
bazowej nazwet jeżeli obiekt jest klasy potomnej.
12
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ć?
13
class punkt
{
int x,y;
public:
char klasa;
class okrag: public punkt
{
int r;
public:
void pokaz(); //rysuje punkt
void pokaz(); //rysuje punkt
punkt(int x, int y)
:x(x), y(y),
{
klasa=‘p’;
}
};
n
n
Rozwiązanie niedoskonałe
Wadliwe
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.
14
Metody wirtualne
class punkt
{
int x,y;
class okrag: public punkt
{
int r;
public:
void virtual pokaz();
void virtual ukryj();
};
public:
void pokaz(); // virtual
void ukryj(); // virtual
};
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
n
zwiększy się rozmiar obiektów tej klasy.
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
n
n
n
będzie to działało wolniej niż odwołanie bezpośrednie,
metody wirtualne nie będą rozwijane inline,
będzie to działało szybciej, niż gdybyśmy taką sztuczkę robili ręcznie,
kompilator nie pomyli się (człowiek – wiadomo).
15
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
n
a więc szybsze, metody (nawet zadeklarowane z virtual)
mogą być rozwijane inline.
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;
class okrag: public punkt
{
int r;
public:
void virtual pokaz();
void virtual ukryj();
public:
void pokaz();
void ukryj();
};
void przesun(int dx, int dy)
{
ukryj(); // wirtualna w punkt
x+=dx;
y+=dy;
pokaz(); // wirtualna w punkt
}
};
okrag o;
punkt &rp=o;
rp.pokaz(); //okrag::pokaz
rp.przesun();
//punkt::przesun wywoła
//okrag::pokaz i okrag::ukyj !!!
16
Klasa polimorficzna
n
klasa polimorficzna to taka w której występuje
przynajmniej jedna metoda wirtualna
n
Przykład korzyści z polimorfizmu:
n
n
deklarujemy listę przechowującą wskaźniki do punktów
(klasa polimorficzna) - na liście umieszczać punkty okręgi i
inne figury.
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
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.
Późne wiązanie
n
decyzja co do wyboru klasy z zakresu której
metodę wykonać, zostaje podjęta podczas biegu
programu.
17
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
n
jawnie (operatorem zakresu) podamy o którą klasę nam
chodzi,
wywołamy metodę bezpośrednio na rzecz obiektu (nie
przez wskaźnik lub referencję).
Metody wirtualne
n
zaleta: ogromna łatwość rozbudowy programu,
n
n
n
Pisząc kod możemy wykorzystywać metody
których jeszcze nie napisano !
Nie musimy powielać takiego samego kodu w
metodach różnych klas (vide przesun()).
Nie grozi nam „uzupełnienie” już gotowego kodu
o nowe błędy.
18
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?).
19
Dziedziczenie metod wirtualnych
n
n
n
meoda wirtualna może zostać odziedziczona przez klasę
pochodną
może zostać przedefiniowana, w jej ciele możemy wywołać
metodę wirtualną klasy bazowej.
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
n
inne metody (o innych parametrach) są zwykłymi
metodami/operatorami.
np. metoda nie wirtualna:
void punkt::pokaz(char * opis){...};
20
Zaprzyjaźnianie a wirtualność
n
Wirtualność jest niezależna od zaprzyjaźniania.
n
n
n
Zaprzyjaźnianie nie jest przechodnie,
Zaprzyjaźnianie dotyczy tylko tej metody
(klasa::metoda) która została zaprzyjaźniona,
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
n
zatem decyduje typ wskaźnika/referencji.
np., przyjmijmy, że wszystkie składowe klasy okrąg
sa prywatne:
okrag o;
punkt &rp=o;
rp.pokaz(); // OK. – dlaczego?
21
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
n
np. klasa abstrakcyjna „figura” „liczba”
Dziedziczenie klas abstrakcyjnych
Metoda czysto wirtualna
n
W C++ klasa abstrakcyjna to taka, która
zawiera przynajmniej jedną metodę czysto
wirtualną – tj. taką która jest wirtualna, i nie
ma zdefiniowanego ciała.
void virtual figura::rysuj()=0;
22
Koniec
23