Wykład 10.

Transkrypt

Wykład 10.
FUNKCJE
WZORCOWE
Programowanie Obiektowe
(język C++)
Wykład 10.
-1-
Funkcje wzorcowe – wprowadzenie (1)
-2-
Funkcje wzorcowe – wprowadzenie (2)
int max ( int a, int b )
{ return a>b ? a : b; }
W obu językach C i C++ moŜemy się posłuŜyć w tej sytuacji, jako pewnego
rodzaju alternatywą, tzw. makrodefinicją:
Aby mieć analogiczną funkcję działającą na danych typu double
w jęz. C musimy wprowadzić dla niej odrębny identyfikator:
#define MAX(a,b) (((a)>(b)) ? (a) : (b))
co daje nam moŜliwość posługiwania się wyraŜeniami przypominającymi
wywołania 2-argumentowej funkcji o tej samej nazwie dla argumentów
róŜnych typów:
double d_max ( double a, double b )
{ return a>b ? a : b; }
W jęz. C++ mamy moŜliwość przeciąŜania identyfikatorów funkcji,
więc moŜemy uŜyć tej samej nazwy:
int i, j, k;
double A, B, C;
……………………….
k = MAX(2*i+5, j+i);
C = MAX(3.4+A, 7*B);
double max ( double a, double b )
{ return a>b ? a : b; }
Ale wiąŜe się to z szeregiem niedogodności i pułapek.
-3-
-4-
Funkcje wzorcowe (1)
Funkcje wzorcowe (2)
W języku C++ moŜemy się posłuŜyć konstrukcją wzorca (szablonu) funkcji:
template < class TYPE >
TYPE max (TYPE a, TYPE b )
{ return a>b ? a : b; }
1. Wzorzec funkcji moŜe mieć więcej parametrów (ale nie mniej niŜ jeden!).
2. KaŜdy opis parametru wzorca składa się ze słowa kluczowego class
( lub typename ) oraz wybranego identyfikatora ( nazwy parametru ).
gdzie TYPE moŜe być typem wbudowanym lub definiowanym w programie.
<class TYPE> nazywa się tu opisem parametru wzorca.
Mając tak zdefiniowany szablon, moŜe go wykorzystać do konkretyzacji funkcji
przyjmujących argumenty potrzebnych typów, n.p.:
int main ( )
{
int i, j, k;
3. Na liście parametrów wzorca kaŜdy identyfikator moŜe wystąpić tylko raz.
4. Parametr wzorca staje się specyfikatorem typu, którego moŜna uŜywać
w pozostałej części definicji funkcji wzorcowej ( n.p. w deklaracjach zmiennych
lokalnych, operacjach rzutowania e.t.c. )
double A, B, C;
k = max( i, j + 5 );
C = max( A, B + 0.5 );
A = max ( B, 10 );
5. KaŜdy parametr wzorca musi wystąpić co najmniej jeden raz w sygnaturze
funkcji wzorcowej.
// skonkretyzuje int max ( int, int );
// skonkretyzuje double max ( double, double );
// BŁĄD! – konieczna ścisła zgodność typów
-5-
Funkcje wzorcowe (3)
-6-
Funkcje wzorcowe (4)
Wychodząc od definicji funkcji:
Wychodząc od definicji tej samej funkcji:
double Summa ( double tab[ ], int size )
{
double sum = 0;
for ( int i = 0; i < size; i++ ) sum += tab[ i ];
return sum;
}
double Summa ( double tab[ ], int size )
{
double sum = 0;
for ( int i = 0; i < size; i++ ) sum += tab[ i ];
return sum;
}
MoŜemy łatwo utworzyć jednoparametrowy wzorzec:
MoŜemy równie łatwo utworzyć wzorzec dwuparametrowy:
template < class T >
T Summa ( T tab[ ], int size )
{
T sum = 0;
for ( int i = 0; i < size; i++ ) sum += tab[ i ];
return sum;
}
template < class T, class S >
T Summa ( T tab[ ], S size )
{
T sum = 0;
for ( S i = 0; i < size; i++ ) sum += tab[ i ];
return sum;
}
-7-
-8-
Funkcje wzorcowe (5)
Funkcje wzorcowe (6)
Rozpatrzmy przykład:
RozróŜnianie przeciąŜonych funkcji wzorca i innych funkcji
o tej samej nazwie jest realizowane wg schematu:
template < class T >
T max ( T a, T b )
{ return a>b ? a : b; }
1. JeŜeli istnieje funkcja o deskryptorze dokładnie pasującym do wywołania,
to ją wywołaj.
void test ( )
{
int a, b;
char c, d;
2. JeŜeli istnieje wzorzec pozwalający zrealizować (skonkretyzować)
funkcję o dokładnie pasującym deskryptorze, to uŜyj tego wzorca.
3. JeŜeli istnieje funkcja przeciąŜona, którą moŜna dopasować wg zwykłych
reguł (tzn. z zastosowaniem konwersji), to jej uŜyj.
//int A = max( a, c ); // BŁĄD! – nie moŜna wygenerować int max(int,char);
int B = max( a, b ); // int max(int, int); - niejawne utworzenie egzemplarza
char C = max( c, d ); // char max(char,char); niejawne utworzenie egz.
int D = max( c, d ); // char max(char, char); typ zwracanej wartości nie
// nie naleŜy do deskryptora
4. JeŜeli Ŝadnego z punktów 1., 2., 3. nie dało się zastosować, to wywołanie
zostanie uznane za błędne.
-9-
Funkcje wzorcowe (7)
-10-
Funkcje wzorcowe (8)
Ale:
Inne moŜliwości:
template < class T >
T max ( T a, T b )
{ return a>b ? a : b; }
int max ( int, int );
template < class T >
T max ( T a, T b )
{ return a>b ? a : b; }
// deklaracja funkcji (być moŜe zewnętrznej)
template int max ( int, int );
void test ( )
{
int a, b;
char c, d;
int
A = max( a, c );
// wymuszone (jawne) utworzenie egzemplarza
// (tylko w zasięgu definicji szablonu)
template< >
char max ( char a, char b )
{ return a>b ? a : 0; }
// O.K.! – uŜyte będzie int max(int,int);
// zgodnie z regułą 3.
// dostarczenie szczególnej definicji egzemplarza!
// (tylko w zasięgu definicji szablonu)
-11-
-12-
Klasy wzorcowe - wprowadzenie (1)
W język C++ moŜemy się posłuŜyć równieŜ konstrukcją wzorca (szablonu)
klasy. Podobnie jak w przypadku wzorców funkcji najprościej jest przyjąć za
punkt wyjścia jakąś konkretną klasę (dobrze wcześniej sprawdzoną w praktyce).
#define CSTACKSIZE 100
class CharStack
{
char tab [ CSTACKSIZE ];
int size, top;
public:
CharStack ( ) { size = CSTACKSIZE; top = 0; }
void Push ( char e ) { tab [ top++ ] = e; }
char Pop ( ) { return tab [ --top ]; }
char Top ( ) { return tab [ top - 1 ]; }
int Size ( ) const { return size; }
int Used ( ) const { return top; }
int Place ( ) const { return size - top; }
void Display ( ) const
{ cout << endl; for ( int i = 0; i < top; ++i ) cout << tab [ i ] << " "; }
};
WZORCE KLAS
( SZABLONY KLAS )
-13-
Przy okazji … (1)
-14-
Przy okazji … (2)
ZauwaŜmy, Ŝe dotychczas w definicji klasy podawaliśmy zwykle jedynie
deklaracje metod. Ich definicje umieszczaliśmy na zewnątrz, najczęściej
w tzw. pliku implementacyjnym.
Tym razem definicje metod zostały podane od razu w definicji klasy.
Jaka róŜnica?
1. Metoda definiowana w ciele definicji klasy otrzymuje domyślnie
atrybut inline. Metody (i zwykłe funcje) z takim atrybutem nie mają
jednokrotnie wygenerowanego kodu o określonym adresie, który
jest uruchamiany przy kaŜdym odwołaniu do metody (funkcji).
Zamiast tego kompilator moŜe (ale nie musi!) generować kod metody
(funkcji) w kaŜdym miejscu jej wywołania. MoŜe to dać zysk na czasie
wykonania programu, chociaŜ zwykle zwiększa jego objętość.
2. Atrybut inline moŜe być podany jawnie a treść metody na zewnątrz
definicji klasy, ale wtedy naleŜy ją podać w pliku header'owym (.h)
klasy, a nie w pliku implementacyjnym.
Wynika to z faktu, Ŝe treść takiej metody potrzebna jest kompilatorowi
w kaŜdym miejscu jej wywołania.
3. UŜycie odrębnego (niezaleŜnie kompilowanego) pliku zawierającego
definicje metod ( posiadających atrybut extern ) pozwala ukryć
szczegóły implementacyjne. UŜytkownik naszej klasy będzie korzystał
tylko z pliku nagłówkowego w postaci źródłowej (.h) i skompilowanego
pliku zawierającego kod metod (.obj). A więc np. wcale nie musi
wiedzieć, jaki algorytm zastosowaliśmy do realizacji konkretnych
obliczeń numerycznych.
UWAGA: Funkcja z atrybutem inline nazywa się teŜ
… funkcją rozwijalną …
albo
… funkcją otwartą …
-15-
-16-
Przy okazji … (3)
Klasy wzorcowe - wprowadzenie (2)
// charstack.h
……………………………
class CharStack
{
……………………………
void Push ( char e ) { tab [ top++ ] = e; }
inline char Pop ( );
char Top ( ) const;
……………………………
};
……………………………
inline char CharStack :: Pop ( ) { return tab [ --top ]; }
……………………………..…………………………………..….… end of charstack.h
Poszukajmy 'kandydatów' na parametry.
Zaznaczyłem je na kolorowo.
Oczywiście nie wszystkie moŜliwości musimy wykorzystać.
#define CSTACKSIZE 100
class CharStack
{
char tab [ CSTACKSIZE ];
int size, top;
public:
CharStack ( ) { size = CSTACKSIZE; top = 0; }
void Push ( char e ) { tab [ top++ ] = e; }
char Pop ( ) { return tab [ --top ]; }
char Top ( ) { return tab [ top - 1 ]; }
int Size ( ) const { return size; }
int Used ( ) const { return top; }
int Place ( ) const { return size – top; }
void Display ( ) const
{ cout << endl; for ( int i = 0; i < top; ++i ) cout << tab [ i ] << " "; }
};
// charstack.cpp
……………………………
char CharStack :: Top ( ) const { return tab [ top - 1 ]; }
……………………………
W powyŜszym przykładzie metody:
Push i Pop mają atybut inline ( Push – domyślnie, Pop – jawnie )
metoda
Top ma atrybut extern.
-17-
-18-
Klasy wzorcowe (1)
Klasy wzorcowe (2)
A tak moŜe wyglądać nasz szablon:
I funkcja main:
#define STACKSIZE 100
int main ( )
{
const int k = 10;
Stack<> S0;
Stack<int> S1, S2; Stack<int,10> S3;
Stack<double> S4;
Stack<double,55> S5;
Stack<double,k+5> S6; Stack<double,k+45> S7;
template < class T = char, int S = STACKSIZE >
class Stack
{
T tab [ S ];
int size, top;
public:
Stack ( ) { size = S; top = 0; }
void Push ( T e ) { tab [ top++ ] = e; }
T Pop ( ) { return tab [ --top ]; }
T Top ( ) { return tab [ top - 1 ]; }
int Size ( ) const { return size; }
int Used ( ) const { return top; }
int Place ( ) const { return size - top; }
void Display ( ) const
{ cout << endl; for ( int i = 0; i < top; ++i ) cout << tab [ i ] << " "; }
};
#undef STACKSIZE
-19-
cout << endl << S0.Size() << endl << S1.Size() << endl << S3.Size();
cout << endl << S4.Size() << endl << S5.Size() << endl << S6.Size();
S1.Push(10); S1.Push(11); S1.Push(12); S1.Push(13); S1.Push(14);
S1.Display();
S2 = S1;
S1.Pop(); S1.Pop();
S1.Display(); S2.Display();
//S3 = S1;
// BŁĄD!
//S4 = S1;
// BŁĄD! ale S7 = S5; O.K.
while ( S1.Place() && S2.Used() ) S1.Push( S2.Pop() );
S1.Display();
}
-20-
Klasy wzorcowe (3)
Klasy wzorcowe (4)
CharStack St;
// obiekt St jest typu CharStack
Program wyświetli (po zakomentowaniu wierszy z błędami):
Stack<> S0;
// obiekt S0 jest typu Stack<char, 100>
100
100
10
100
55
15
10
10
10
10
Stack<int> S1, S2;
// obiekty S1 i S2 są typu Stack<int, 100>
Stack<int,10> S3;
// obiekt S3 jest typu Stack<int, 10>
11
11
11
11
12
12
12
12
13
14
13
14
14
13
Stack<double> S4;
// obiekt S4 jest typu Stack<double, 100>
12
11
Stack<double,k+5> S6;
// obiekt S6 jest typu Stack<double, 15>
10
Stack<CMPLX> SC;
// obiekt SC jest typu Stack<CMPLX, 100>
-21-
Klasy wzorcowe (1a)
-22-
Klasy wzorcowe (5)
Kusząca (niebezpieczna) alternatywa:
template < class T = char, int S = STACKSIZE >
class Stack
{
T tab [ S ];
T *p;
int size;
public:
Stack ( ) { p = tab; size = S; }
void Push ( T e ) { *p++ = e; }
T Pop ( ) { return *--p; }
T Top ( ) { return *(p – 1); }
int Size ( ) const { return size; }
int Used ( ) const { return p – tab; }
int Place ( ) const { return size – (p – tab); }
void Display ( ) const { T *r = tab; while ( r < p ) cout << *r++ << " "; }
};
Na czym polega niebezpieczeństwo
i jak mu zaradzić?
-23-
1. Wzorzec klasy moŜe mieć więcej parametrów (ale nie mniej niŜ jeden!).
2. Opis parametru wzorca składa się ze słowa kluczowego class
(ew. typename) lub nazwy typu wbudowanego oraz wybranego
identyfikatora (nazwy parametru). Dla parametrów moŜna określać
wartości domyślne.
3. Na liście parametrów wzorca kaŜdy parametr moŜe wystąpić tylko raz.
4. Parametr wzorca poprzedzony słowem kluczowym class (ew. typename)
staje się specyfikatorem typu, którego moŜna uŜywać w pozostałej części
definicji klasy wzorcowej (n.p. w deklaracjach pól, specyfikacjach parametrów
metod et c.).
5. Parametr wzorca poprzedzony nazwą typu wbudowanego staje się stałą,
której moŜna uŜywać np. do zapisu rozmiarów tablic, inicjowania wartości
zmiennych et c.
-24-
Klasy wzorcowe (6)
Klasy wzorcowe (7)
Zobaczmy inny wariant zapisu szablonu.
W dalszym ciągu pliku stack.h podajemy szablony metod:
...................................................
// stack.h
template < class T, int S >
Stack< T, S > :: Stack ( ) { p = q = tab; size = S; }
#include <iostream>
using namespace std;
template < class T, int S >
Stack< T, S >& Stack< T, S > :: operator= ( const Stack& rhs )
{ p = q; for ( T* r = rhs.q; r < rhs.p; *p++ = *r++ ); return *this; }
#define STACKSIZE 1000
template < class T = char, int S = STACKSIZE >
class Stack
{
T tab [ S ]; T *p, *q;
int size;
public:
Stack ( );
Stack& operator= ( const Stack& );
void Push ( T );
T Pop ( );
T Top ( ) const;
int Size ( ) const;
int Used ( ) const;
int Place ( ) const;
void Display ( ) const;
};
...................................................
template < class T, int S >
void Stack< T, S > :: Push ( T e ) { *p++ = e; }
template < class T, int S >
T Stack< T ,S > :: Pop ( ) { return *--p; }
template < class T, int S >
T Stack< T, S > :: Top ( ) const { return *(p - 1); }
template < class T, int S >
int Stack< T, S > :: Size ( ) const { return size; }
...................................................
-25-
-26-
Klasy wzorcowe (8)
i dalej w pliku stack.h:
...................................................
template < class T, int S >
int Stack< T, S > :: Used ( ) const { return p - q; }
funkcje zaprzyjaźnione
we wzorcach klas
template < class T, int S >
int Stack< T, S > :: Place ( ) const { return size - (p - q); }
template < class T, int S >
void Stack< T, S > :: Display ( ) const
{ T* r = q; while ( r < p ) cout << *r++ << " "; }
#undef STACKSIZE
UWAGA! UWAGA!
To wszystko powinno być podane w pliku stack.h.
Dla klas wzorcowych nie tworzymy odrębnych plików implementacyjnych *.cpp.
-27-
-28-
Klasy wzorcowe (6)
Wzorce i friend (1)
W rozpatrywanym wcześniej wzorcu klasy
...................................................
Próba zrealizowania tego zadania w następujący sposób:
...................................................
template < class T = char, int S = STACKSIZE >
class Stack
{
T tab [ S ]; T *p, *q;
int size;
public:
Stack ( );
Stack& operator= ( const Stack& );
void Push ( T );
T Pop ( );
T Top ( ) const;
int Size ( ) const;
int Used ( ) const;
int Place ( ) const;
void Display ( ) const;
};
...................................................
template < class T = char, int S = STACKSIZE >
class Stack
{
T tab [ S ]; T *p, *q;
int size;
public:
Stack ( );
Stack& operator= ( const Stack& );
void Push ( T );
T Pop ( );
T Top ( ) const;
int Size ( ) const;
int Used ( ) const;
int Place ( ) const;
friend ostream& operator << (ostream&, const Stack<T,S>&);
};
...................................................
template < class T, int S>
ostream& operator << (ostream& out, const Stack<T,S>& st)
{ T* r = st.q; while ( r < st.p ) cout << *r++ << " ";
return out;
}
chcemy zastąpić metodę Display zaprzyjaźnionym operatorem << .
nie da oczekiwanego efektu. Operator << nie zostanie wygenerowany.
-29-
Wzorce i friend (2)
-30-
Wzorce i friend (3)
Poprawny efekt otrzymamy pisząc:
...................................................
a nawet:
...................................................
template < class T = char, int S = STACKSIZE >
class Stack
{
T tab [ S ]; T *p, *q;
int size;
public:
Stack ( );
Stack& operator= ( const Stack& );
void Push ( T );
T Pop ( );
T Top ( ) const;
int Size ( ) const;
int Used ( ) const;
int Place ( ) const;
template < class Q, int R>
friend ostream& operator << (ostream&, const Stack<Q,R>&);
};
...................................................
template < class T, int S>
ostream& operator << (ostream& out, const Stack<T,S>& st)
{ T* r = st.q; while ( r < st.p ) out << *r++ << " ";
return out;
}
template < class T = char, int S = STACKSIZE >
class Stack
{
T tab [ S ]; T *p, *q;
int size;
public:
Stack ( );
Stack& operator= ( const Stack& );
void Push ( T );
T Pop ( );
T Top ( ) const;
int Size ( ) const;
int Used ( ) const;
int Place ( ) const;
template < class Q, int R>
friend ostream& operator << (ostream&, const Stack&);
};
...................................................
template < class T, int S>
ostream& operator << (ostream& out, const Stack<T,S>& st)
{ T* r = st.q; while ( r < st.p ) out << *r++ << " ";
return out;
}
-31-
-32-
Wzorce i friend (4)
Jeszcze innym rozwiązaniem jest:
...................................................
template < class T = char, int S = STACKSIZE >
class Stack
{
T tab [ S ]; T *p, *q;
int size;
public:
Stack ( );
Stack& operator= ( const Stack& );
void Push ( T );
T Pop ( );
T Top ( ) const;
int Size ( ) const;
int Used ( ) const;
int Place ( ) const;
friend ostream& operator << (ostream& out, const Stack& st)
{ T* r = st.q; while ( r < st.p ) out << *r++ << " ";
return out;
}
};
...................................................
Koniec wykładu 10.
-33-
-34-

Podobne dokumenty