Programowanie obiektowe, w6, 9


9. Szablony (templates).

9.1. Szablony funkcji

Szablony funkcji służą do wykonania tych samych czynności na różnych typach obiektów. Umożliwiają one wypisywanie treści funkcji jednokrotnie zamiast pisania funkcji przeciążonych.

Rozpatrzmy program:

# include <iostream.h>

template <class burykot>

burykot wieksza(burykot a, burykot b)

{

return (a>b)?a:b;

}

main()

{

int a = 44, b = 88;

double x = 12.6, y = 67.8;

unsigned long la = 987654L, lb = 456;

cout << "wiekszy int: " << wieksza(a,b) << endl;

cout << "wiekszy double: " << wieksza(x,y) << endl;

cout << "wiekszy long: " << wieksza(la,lb) << endl;

cout << "wiekszy znak (kod ASCII): " << wieksza('A','Z') << endl;

}

Po wykonaniu programu, na ekranie zobaczymy:

wiekszy int: 88

wiekszy double: 67.8

wiekszy long: 987654

wiekszy znak (kod ASCII): Z

Zauważmy:

  1. Szablon funkcji ma nazwę globalną.

  2. Parametr szablonu występuje w ostrych nawiasach <>

  3. Parametrem szablonu może być jedynie nazwa typu.

  4. Parametr szablonu zawsze deklarujemy jako class.

  5. Wywołania szablonu są takie same jak wywołania zwykłych funkcji.

Oczywiście, wszystkie wyrażenia w szablonie muszą mieć sens dla wszystkich typów argumentów, z którymi ewentualnie będziemy wykonywać naszą funkcję szablonową. Na przykład w funkcji wieksza(), jeśli pod parametr burykot podstawimy jakiś typ użytkownika, dla obiektów a i b tego typu musi mieć sens wyrażenie a>b.

Szablon w razie konieczności (dość rzadkiej) może być również tylko zadeklarowany:

template <class tt>

tt wieksza(tt a, tt b);

Szablon może mieć kilka parametrów - muszą to być różne typy.

Można stworzyć dwa szablony o tej samej nazwie (i różnych parametrach). Trzeba jednak przy tym uważać, żeby jeden szablon nie był szczególnym przypadkiem drugiego. Na przykład gdybyśmy prócz szablonu

template <class tt>

tt wieksza(tt a, tt b);

zadeklarowali drugi szablon

template <class tt1, class tt2>

tt wieksza(tt1 a, tt2 b);

kompilator po wywołaniu wieksza(1,2), nie będzie wiedział, którego szablonu użyć.

Funkcjom szablonowym możemy przypisywać wskaźniki, tak, jak zwykłym funkcjom.

Jeśli argument funkcji szablonowej jest typu pochodnego od parametru szablonu, dodatkowy parametr szablonu nie jest wymagany.

Przykład

template <class typ>

void fun(typ obj, typ* wsk);

Funkcje specjalizowane.

Niekiedy chcemy, by funkcja o tym samym typie i parametrach, co funkcje produkowane przez szablon, zachowywała się jednak inaczej. Piszemy wtedy funkcję specjalizowaną - po prostu konkretną funkcję dla argumentu określonego typu. Kompilator najpierw sprawdza, czy istnieją już funkcje o danym typie, a dopiero później tworzy funkcje z szablonu. Innym sposobem jest mechanizm RTTI.

Mechanizm identyfikacji typów czasu wykonania (Run Time Type Identification - RTTI).

Istnieje możliwość ustalenia typu obiektu podczas działania programu. Trzeba w tym celu dołączyć plik typeinfo.h i zastosować słowo kluczowe typeid. Ogólna postać takiego odwołania jest następująca:

typeid(obiekt)

Wartością wyrażenia typeid jest referencja do obiektu typu type_info, który opisuje typ zmiennej obiekt. W klasie typeinfo mamy następujące obiekty publiczne:

bool operator==(const typeinfo &ob.)const;

bool operator!=(const typeinfo &ob.)const;

bool before(const typeinfo &ob.)const;

const char* name(const);

Operatory == i != umożliwiają porównywanie typów. Funkcja before() zwraca true, jeśli obiekt, z którego funkcja jest wywoływana znajduje się przed obiektem - argumentem tej funkcji. Funkcja name() zwraca wskaźnik do szukanej nazwy typu. (Czyli do napisu z nazwą).

Przykład:

# include <iostream.h>

#include <typeinfo.h>

template <class burykot>

burykot wieksza(burykot a, burykot b)

{

return (a>b)?a:b;

}

main()

{

int a = 44, b = 88;

double x = 12.6, y = 67.8;

unsigned long la = 987654L, lb = 456;

cout << "wiekszy int: " << wieksza(a,b) << endl;

cout << "wiekszy double: " << wieksza(x,y) << endl;

cout << "wiekszy long: " << wieksza(la,lb) << endl;

cout <<"informacja o typie la: " <<

typeid(la).name() << endl;

if (typeid(a)!=typeid(la))cout << "typy sa rozne"

<<endl;

cout << "wiekszy znak (kod ASCII): " <<

wieksza('A','Z') << endl;

}

Wynikiem powyższego programu jest:

wiekszy int: 88

wiekszy double: 67.8

wiekszy long: 987654

informacja o typie la: unsigned long

typy sa rozne

wiekszy znak (kod ASCII): Z

Jeśli instrukcję typeid użyto ze wskaźnikiem do bazowej klasy polimorficznej, to zwraca ona typ obiektu rzeczywiście wskazywanego.

Na koniec dwie uwagi.

1. Jeśli argumenty, z którymi funkcja jest wywołana nie pasują do żadnego szablonu, ani do żadnej funkcji zadeklarowanej explicite, kompilator będzie się starał te argumenty dopasować. Często otrzymany rezultat takiej działalności jest inny, niż się spodziewamy. trzeba z tym uważać.

2. Są pewne kłopoty przy dołączaniu jednego nagłówka z szablonami do kilku plików. W Borlandzie C++, trzeba wybrać następujące opcje:

Options - Project - C++options - Templates

I jako Instance generation wybrać "Smart".

9.2. Szablony klas.

Szablony klas omówimy na przykładzie stosu. Stos to struktura danych, do której wprowadzamy dane w pewnej kolejności, a wyprowadzamy je w kolejności odwrotnej - "ostatnia wpisana, pierwsza pobrana". Działanie stosu jest niezależne od typu danych w nim umieszczonych. W rzeczywistości jednak, w programie typ danych przechowywanych na stosie musi być określony. Daje to podstawę do zastosowania wzorców, który będzie nam generował klasy stosu dla różnych typów danych - "double", "int", "Pracownik" itd.

Przyjrzyjmy się programowi.

// Wzorzec klasy Stos

#include <iostream>

template<class T>

class Stos

{

public:

Stos(int = 10); //domyslny konstruktor (wielkosc stosu 10)

~Stos() //destruktor

{delete[] stosPtr;}

bool umiesc(const T&); //umiesc element na stosie

bool pobierz(T&); //pobierz element ze stosu

private:

int wielk; //liczba elementow na stosie

int szczyt; //polozenie elementow na szczycie

T *stosPtr; //wskaznik do stosu

bool czyPusty() const //funkcje pomocnicze

{return szczyt == -1;}

bool czyPelny() const

{return szczyt ==wielk - 1;}

};

template <class T> //Konstruktor z domyslna wielkościa 10

Stos< T >::Stos(int s)

{

wielk = s > 0 ? s : 10;

szczyt = -1; //Konstruktor tworzy pusty stos

stosPtr = new T[wielk]; //przydzial pamieci dla elementow

}

template <class T> //umieszczanie na stosie; true - jesli element umieszczono

bool Stos < T >::umiesc(const T &umiescWartosc)

{

if (!czyPelny())

{

stosPtr[++szczyt] = umiescWartosc; //umieszczanie

return true; //udalo sie!

}

return false; //Niestety nie udalo sie

}

template <class T> //pobranie ze stosu

bool Stos < T >::pobierz(T &pobierzWartosc)

{

if(!czyPusty())

{

pobierzWartosc = stosPtr[szczyt--]; //usun element

return true; //udalo sie!

}

return false; //Niestety

}

//program sprawdzajacy wzorzec

int main()

{

Stos<double> doubleStos(5);

double f = 1.1;

cout << "\nUmieszczanie elementow na stosie doubleStos" << endl;

while(doubleStos.umiesc(f)) //Udana operacja zwraca true

{

cout << f << ' ';

f+=1.1;

}

cout << "\nStos jest pelny. Nie mozna umiescic " << f

<< "\n\nPobieranie elementow ze stosu doubleStos" << endl;

while(doubleStos.pobierz(f)) //Udana operacja zwraca true

cout << f << ' ';

cout << "\nStos jest pusty. Nie mozna nic pobrac." <<endl;

Stos<int> intStos;

int i = 1;

cout << "Umieszczanie elementow na stosie intStos" << endl;

while(intStos.umiesc(i)) //Udana operacja zwraca true

{

cout << i << ' ';

++i;

}

cout << "\nStos jest pelny. Nie mozna umiescic " << i

<< "\n\nPobieranie elementow ze stosu intStos" << endl;

while(intStos.pobierz(i)) //Udana operacja zwraca true

cout << i << ' ';

cout << "\nStos jest pusty. Nie mozna nic pobrac." <<endl;

return 0;

}

Zwrócmy uwagę na nagłówek klasy stos. Od zwykłej definicji klasy różni ją tylko nagłówek:

template<class T>

Typ elementu przechowywanego na stosie jest określony jako T w nagłówku klasy i funkcjach składowych. W programie typ T został powiązany z typem int oraz typem double.

Linijka

template<class T>

wystepuje również przed nagłówkami funkcji składowych, zdefiniowanych poza obrębem klasy.

W programie głównym obiekty deklarujemy, zaznaczając parametr klasy.

Program powyższy wypisywał na ekranie komputera następujący tekst:

Umieszczanie elementow na stosie doubleStos

1.1 2.2 3.3 4.4 5.5

Stos jest pelny. Nie mozna umiescic 6.6

Pobieranie elementow ze stosu doubleStos

5.5 4.4 3.3 2.2 1.1

Stos jest pusty. Nie mozna nic pobrac.

Umieszczanie elementow na stosie intStos

1 2 3 4 5 6 7 8 9 10

Stos jest pelny. Nie mozna umiescic 11

Pobieranie elementow ze stosu intStos

10 9 8 7 6 5 4 3 2 1

Stos jest pusty. Nie mozna nic pobrac.

Zauważmy, że kod funkcji main() w powyższym programie składa się z dwóch bardzo podobnych części. Możemy wobec tego większa część funkcji main() zrealizować również w postaci funkcji szablonowej. Oto funkcja szablonowa testStos i zmodyfikowana funkcja main():

template <class T>

void testStos(Stos<T> &tenStos, T wartosc, T przyrost, const char*nazwaStosu)

{

cout << "\nUmieszczanie elementow na stosie " << nazwaStosu << endl;

while(tenStos.umiesc(wartosc)) //Udana operacja zwraca true

{

cout << wartosc << ' ';

wartosc+=przyrost;

}

cout << "\nStos jest pelny. Nie mozna umiescic " << wartosc

<< "\n\nPobieranie elementow ze stosu " << nazwaStosu << endl;

while(tenStos.pobierz(wartosc)) //Udana operacja zwraca true

cout << wartosc << ' ';

cout << "\nStos jest pusty. Nie mozna nic pobrac." <<endl;

}

int main()

{

Stos<double> doubleStos(5);

Stos<int> intStos;

testStos(doubleStos, 1.1, 1.1, "doubleStos");

testStos (intStos,1,1, "intStos");

return 0;

}

Wyniki są takie same jak w poprzednim przypadku, jednak w main() skorzystalismy z funkcji szablonowej.

Tworzenie klas na podstawie szablonu.

Kompilator tworzy konkretne klasy w konkretnych wypadkach.

a) Przy pierwszym zadeklarowaniu obiektu klasy szablonowej:

Stos<int> intStos;

b) Przy definicji wskaźnika na obiekty klasy szablonowej

Stos<int> *wsk;

c) Przy deklaracjifunkcji zawierajacej nazwę klasy szablonowej;

void testStos(Stos<T> &tenStos, T wartosc, T przyrost, const char*nazwaStosu)

d) Jeśli kompilator zobaczy klasę szablonową na liście pochodzenia klasy pochodnej.

class lepszystos : Stos<int>

{

}

Parametry szablonu.

W przypadku szablonów funkcji parametrami szablonu mogły być tylko typy. W przypadku szablonu klas możliwości jest więcej. Parametrem szablonu może być nie tylko nazwa typu, ale inne wyrażenia. Mianowicie:

a) stała dosłowna typu całkowitego.

template <class typ, int rozmiar>

class koty

{typ tablica[rozmiar]

.....

}

Obiekty tej klasy tworzymy deklarując:

koty(int, 27> sjamy;

koty<double, 1000> bure;

b) adres jakiejś konkretnej komórki w pamięci.

Wyrażony explicite. Czasami znamy takie adresy np. z instrukcji obsługi komputera.

c) adresem konkretnego obiektu globalnego.

d) adresem funkcji globalnej.

e) adresem statycznego składnika klasy.

Parametrami szablonu nie może być:

a) napis.

b) adres elementu tablicy

c) adres niestatycznego składnika klasy.

d) typ zdefiniowany lokalnie (np. klasa zagnieżdżona w środku jakiejś funkcji).

Specjalizacja

Jeśli dla jakiegoś typu, klasa produkowana przez szablon nam nie odpowiada, możemy zadeklarować specjalizowane klasy szablonowe.

Jeśli mamy na przykład:

template <class jakisTyp>

class koty

{

....

};

to możemy utworzyć specjalizowaną klasę szablonową:

class koty<char*>

{

.....

}

Analogicznie, jeśli nie podoba nam się w konkretnym szablonie klas jakaś funkcja składowa - chcemy, by dla pewnego parametru była ona "nieszablonowa", to możemy dla tego parametru napisac jej wersje specjalizowaną:

template <class jakisTyp>

class koty

{

jakisTyp miauk;

....

};

template <class jakisTyp>

void koty<jakisTyp>::glos(jakisTyp a)

{

Miauk+=a;

}

void koty<char*>::glos(char* a)

{

Miauk=strcat(a, "Mrrrrr");

}

Szablony i przyjaciele.

Funkcje.

Jeśli wewnątrz szablonu danej klasy umieszczona jest deklaracja funkcji zaprzyjaźnionej, oznacza to, ze funkcja jest zaprzyjaźniona ze wszystkimi klasami produkowanymi z tego szablonu.

template <class typ>

class kot

{

...

friend void miauk();

};

void miauk()

{

...

}

Jeśli natomiast wewnątrz szablonu danej klasy umieścimy funkcję, następująco;

template <class typ>

class kot

{

...

friend void mruk(kot<typ> obj);

};

template <class typ>

void mruk(kot<typ> obj)

{

...

}

Wtedy każda klasa wyprodukowana z szablonu kot będzie miała swoją "osobistą" funkcje zaprzyjaźnioną.

Klasy.

Z klasami jest podobnie jak z funkcjami. Jeśli we wzorcu klasy mamy deklarację konkretnej, zwykłej klasy zaprzyjaźnionej, wtedy klasa ta jest zaprzyjaźniona ze wszystkimi klasami wzorca. jeśli natomiast jeśli wewnątrz wzorca klasy X określonej jako

template <class T>

class X

mamy zadeklarowaną drugą klasę Z

friend class Z <T>

Wówczas każda klasa wyprodukowana z wzorca X będzie miała własną zaprzyjaźnioną klasę Z.

Szablony, a dziedziczenie.

W przypadku dziedziczenia możemy mieć wiele sytuacji.

Konkretna klasa nie może odziedziczyć całego szablonu.

Natomiast

a) zwykła klasa może odziedziczyć klasę szablonową.

b) szablon może dziedziczyć zwykłą klasę, inny szablon lub klasę szablonową.

c) klasa szablonowa specjalizowana może odziedziczyć zwykłą klasę lub klasę szablonową.

Wzmianka o pojemnikach (kontenerach) i bibliotekach szablonów.

W C++ zdefiniowano kilka bibliotek klas. Najpopularniejsza obecnie jest tzw. STL

(standard template library - standardowa biblioteka szablonów).

STL zawiera pojemniki. Pojemnikami nazywamy klasy tworzące obiekty do przechowywania innych obiektów. Podstawowe pojemniki to tablica dynamiczna albo wektor (vector), lista liniowa (list), lub kolejka dwustronna deque, u Grębosza - talia).

W bibliotece zawarte są również szablony algorytmów i iteratorów - w pewnym sensie uogólnienie wskaźników. Omówimy je na następnym wykładzie. Podam tu jedynie przykład zastosowania pojemnika vector. Oto on:

# include <iostream>

# include <vector>

using namespace std;

int main()

{

vector<int> v;

cout << "Rozmiar = " << v.size() << endl;

for (int i=0; i<10; i++)v.push_back(i);

cout<< "Obecny rozmiar = " <<v.size() <<endl;

for (int i=0; i<10; i++) cout << v[i] << " ";

cout << endl;

cout << "Pierwszy element = " << v.front() << endl;

cout << "Ostatni element = " << v.back() <<endl;

// Odczytywanie elementów przy użyciu iteratora

vector<int>::iterator p =v.begin();

while (p != v.end())

{

cout << *p << " ";

p++;

}

return 0;

}

Funkcja składowa push_back()dopisuje liczby na koniec tablicy, zwiększając w razie potrzeby jej rozmiar. Działanie pozostałych funkcji składowych jest oczywiste.

Program powyższy wyświetla następujące informacje:

Rozmiar = 0

Obecny rozmiar = 10

0 1 2 3 4 5 6 7 8 9

Pierwszy element = 0

Ostatni element = 9

0 1 2 3 4 5 6 7 8 9

Dostęp do współrzędnych wektora uzyskano za pomocą zwykłego indeksu oraz za pomocą iteratora.

14

Piotr Staniewski

C++ Studia Zaoczne. Wykład 6



Wyszukiwarka

Podobne podstrony:
Programowanie obiektowe(ćw) 1
Zadanie projekt przychodnia lekarska, Programowanie obiektowe
Programowanie obiektowe w PHP4 i PHP5 11 2005
Programowanie Obiektowe ZadTest Nieznany
Egzamin Programowanie Obiektowe Głowacki, Programowanie Obiektowe
Jezyk C Efektywne programowanie obiektowe cpefpo
Programowanie Obiektowe Ćwiczenia 5
Programowanie obiektowe(cw) 2 i Nieznany
programowanie obiektowe 05, c c++, c#
Intuicyjne podstawy programowania obiektowego0
Programowanie obiektowe, CPP program, 1
wyklad5.cpp, JAVA jest językiem programowania obiektowego
projekt01, wisisz, wydzial informatyki, studia zaoczne inzynierskie, programowanie obiektowe, projek
przeciazanie metod i operatorow, Programowanie obiektowe
projekt06, wisisz, wydzial informatyki, studia zaoczne inzynierskie, programowanie obiektowe, projek
projekt07, wisisz, wydzial informatyki, studia zaoczne inzynierskie, programowanie obiektowe, projek
Programowanie Obiektowe Cz2, Dziedziczenie proste
Programowanie obiektowe, w2, 2

więcej podobnych podstron