• Nie Znaleziono Wyników

Programowanie obiektowe w C++

N/A
N/A
Protected

Academic year: 2021

Share "Programowanie obiektowe w C++"

Copied!
23
0
0

Pełen tekst

(1)

Programowanie obiektowe

w C++

Wykład 07 Temat wiodący:

Wzorce (szablony) klas, polimorfizm, funkcje wirtualne

Wzorce

(2)

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 };

(3)

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 *>

(4)

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 }

(5)

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 };

(6)

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];

//};

(7)

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();

(8)

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.

(9)

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;

(10)

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);

};

(11)

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);

};

(12)

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.

(13)

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ć?

(14)

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.

(15)

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,

(16)

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 !!!

(17)

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.

(18)

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.

(19)

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?).

(20)

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){...};

(21)

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?

(22)

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

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;

(23)

Koniec

Cytaty

Powiązane dokumenty

Składowe publiczne klasy bazowej są odziedziczone jako publiczne, a składowe chronione jako chronione.. Dziedziczenie chronione - składowe publiczne są dziedziczone jako

n Skojarzenie referencji do klasy bazowej z obiektem klasy potomnej jest dozwolone przy dziedziczeniu publicznym. n uwagi (konwersje wskaźników

n można go wykorzystać jeżeli mamy zwrócić wskaźnik bądź referencję do obiektu na rzecz którego wywoływana jest metoda.. n nie

n operator konwersji tworzy obiekt określonego typu lub klasy z obiektu na rzecz którego

Na końcu tej funkcji umieszczamy wiersze: system(&#34;pause&#34;); - polecenie to zatrzymuje wykonanie programu do momentu naciśnięcia jakiegoś klawisza (pozwala to zobaczyć

(4 pkt) W klasie Stos zdeniuj metody dost¦pu, wstawiania oraz usuwania elementu stosu  pa- mi¦taj, »e do stosu dost¦p jest tylko z jednej strony.. (4 pkt) W klasie Stos

DODATKOWE - na dodatkowe punkty lub wy»sz¡ ocen¦ (zadania 1-3 musz¡ by¢ wykonane!) Do realizacji podobnego jak wy»ej zadania i budowy klas wyj¡tków wykorzystaj bibliotek¦

Za pomocą klas programista stara się opisać obiekty, ich właściwości, zbudować konstrukcje, interfejs, dzięki któremu będzie można wydawać polecenia realizowane potem