Jezyki i paradygmaty cz IV

background image

53

Modele etapów tworzenia oprogramowania

Już w latach sześćdziesiątych zauważono, że przebieg procesu
tworzenia oprogramowania informatycznego podlega takim
samym regułom, jak tworzenie dowolnego urządzenia np.
budowli. Wtedy właśnie wprowadzono pojęcie Inżynierii
Oprogramowania (Software Engineering).

Poniżej przedstawione zostaną trzy wybrane modele cyklu
życia systemu informatycznego:
- model kaskadowy,
- prototypowanie błyskawiczne,
- programowanie ekstremalne.

Model kaskadowy





przekazanie









Model KASKADOWY cyklu życia systemu

Ścisła

interpretacja

modelu

kaskadowego

traktuje

poszczególne

fazy

jako

niezależne

okresy

realizacji

przedsięwzięcia. Według tej interpretacji okresy te nie

Analiza

Projektowanie

Implementacja

Testowanie

Konserwacja

background image

54

nakładają się na siebie, zaś ich wykonanie przebiega
sekwencyjnie, bez procesów iteracyjnych.

W rzeczywistości proces ten musi mieć charakter iteracyjny
(w postaci powrotów do wcześniejszych faz modelu w
przypadku wykrycia błędów powstałych w tychże fazach) i
przyrostowy (w każdej fazie nawrotu następuje wzbogacenie
modelu).

Do zalet modelu kaskadowego należy zaliczyć:



Łatwość zarządzania przedsięwzięciem,



Łatwość harmonogramowania poszczególnych etapów,



Łatwość określenia kosztów całego przedsięwzięcia,



Łatwość tworzenia dokumentacji.


Wadami tego modelu są;



Wysoki koszt błędów popełnionych we wstępnych fazach
projektu (błędy z fazy analizy i projektowania mogą wyjść
na jaw dopiero w fazie testowania lub konserwacji)



Długa przerwa w kontaktach z klientem (od określenia
wymagań – do przekazania).



Prototypowanie błyskawiczne

przekazanie





Prototypowanie błyskawiczne

Prototyp

1

Prototyp

2

Prototyp

N

Prototyp

N-1

background image

55

Model prototypowy cyklu życia systemów powstał jako
antidotum na wymienione wyżej wady modelu kaskadowego,
z zwłaszcza duże koszty błędów popełnionych w fazie analizy
wymagań, co ma miejsce zwłaszcza w przypadku
nowatorskich i złożonych systemów. Model ten jest
przeciwieństwem modelu kaskadowego.

W modelu prototypowym wyróżnia się następujące fazy:



Ogólne określenie wymagań,



Budowa prototypu pierwszego,



Weryfikacja prototypu przez klienta, (!!!)



Budowa kolejnego prototypu,



. . . . . . . . .



Przekazanie systemu klientowi,



Dalsze doskonalenie systemu,



. . . . . . . . .


Głównym celem budowy prototypów jest lepsze określenie
wymagań, realizowane poprzez:



Wykrycie nieporozumień pomiędzy klientem a twórcami
systemu,



Wykrycie brakujących i trudnych usług.


Prócz tego pojawiają się dodatkowe zalety budowy
prototypów:



Możliwość

szybkiej

demonstracji

pracującej

wersji

systemu,



Możliwość szkoleń, zanim zbudowany zostanie pełen
system.



background image

56

Model ten posiada oczywiście swoje wady. Zaliczyć do nich
należy:



Dodatkowo ponoszony i trudny do określenia koszt budowy
prototypów,



Trudny do określenia moment zakończenia całego
przedsięwzięcia (bardzo często proces ten nigdy się nie
kończy),



Pewne zaskoczenie klienta, który musi długo czekać na
odbiór systemu, którego „prawie całkowite” wykonanie
(demonstrowany prototyp) zajęło tak mało czasu.


Prototypowanie błyskawiczne jest często łączone z nasilonym
korzystaniem z gotowych komponentów.

Przedstawione

dwie

metodyki

(model

kaskadowy

i

prototypowanie błyskawiczne), stoją jak gdyby na dwóch
przeciwstawnych sobie biegunach koncepcji tworzenia
oprogramowania i w rzeczywistości żadne z nich w tak czystej
postaci nie jest stosowane. Dodatkowo ostatnio rozwijane jest
odbiegające jeszcze dalej od prototypowania błyskawicznego
podejście, zwane programowanie ekstremalnym.

Programowanie ekstremalne (eXtreme Programming - XP)

Ogólnie, podejście to, które pojawiło się w 1999 roku,
charakteryzuje:

• Stawianie programisty, a nie analityka systemowego i

projektanta, w centrum zainteresowania,

• Korzystanie z tzw. wzorców projektowych (nazwa jest

myląca, są to bowiem wzorce na poziomie kodu, a nie
projektu), przykłady wzorców projektowych: wzorzec
fasady, fabryka abstrakcyjna

• Uznanie kodu za dokumentację projektu,

background image

57

• Ścisła

współpraca

programistów

z

przyszłym

użytkownikiem.


Podejście to ma wiele cech prototypowania błyskawicznego,
zatarła

się

jednak

granica

między

poszczególnymi

prototypami. Było to możliwe dzięki:

• Rozwojowi paradygmatu obiektowego, co wyraziło się

opracowaniem wielu bibliotek wzorców projektowych,

• Pojawieniu się nowych metodyk w zakresie architektury

oprogramowania, jak: refaktoryzacja, czy transformacje
architektury oprogramowania,

• Rozwój i wzrost znaczenia metodyk testowania, na

przykład

Test

-

Driven

Development(TDD)

programowanie sterowane testami,


Stosuje się tu, mniej lub bardziej świadomie, czteroetapowy
model sukcesu:

1. Określ cel, 2. Wykonaj działanie,
3. Odbierz informację zwrotną,
4. Skoryguj działanie tak, aby kolejny efekt był bliższy
sukcesowi.


Fasadę dla XP tworzą, również cztery, główne wartości:

1. Komunikacja w zespole (do stałej praktyki należy

programowanie w parach) i komunikacja z klientem,

2. Prostota (stałe utrzymywanie przejrzystości i spójności

projektu),

3. Informacja zwrotna (informacje te programiści uzyskują

zarówno od klienta, jak i na podstawie wyników
przeprowadzanych testów),

4. Odwaga w podejmowaniu i wdrażaniu kluczowych

decyzji, wynikająca z wysokiego profesjonalizmu, przy

background image

58

pełnej świadomości odpowiedzialności za podjęte
decyzje.



Związki między klasami

Główne związki między klasami, widziane przez pryzmat
struktury programu (implementację):
1. dziedziczenie,
2. zawieranie
3. należenie, albo posiadanie
4. używanie,
5. związki zaprogramowane.


Związek zawierania

class X
{. . . . .
public:
X(int);
. . . . . };


class C1
{ X a; // klasyczne zawieranie
public:
C1(int i): a(i) { }

// niejawne wywołanie konstruktora klasy X
};



C1

X

background image

59



Związek posiadania

obiekt C2

class C2
{ X *p;
// wskaźnik do obiektu posiadanego

obiekt typu X

public:
C2(int i): p( new X(i)) { }
// ten konstruktor kreuje i inicjuje obiekt posiadany
C2(X *q): p(q) { }
// ... a ten „dopina się” do obiektów istniejących,
// może się też dopinać do obiektów klas potomnych
~C2( ) {delete p;} // utrata obiektu posiadanego
X *udostępnij( ) { return p; }
X *zamien( X *q) { X *t = p; p = q; return t; }
};

Niech

class XX: public X { };
class XXX: public X { };

wtedy

void f( )
{
C2 *p1 = new C2( new X );
//obiekt dynamiczny klasy C2 „posiada” obiekt klasy X
C2 *p2 = new C2( new XX );
//obiekt dynamiczny klasy C2 „posiada” obiekt klasy XX
C2 *p3 = new C2( new XXX );
//obiekt dynamiczny klasy C2 „posiada” obiekt klasy XXX
}

p

background image

60


Użycie referencji tworzy klasy operujące na obiektach klas X
bezpośrednio, bez konieczności używania wskaźników.

class C3
{
X &r; obiekt C3
public:
C3( X &q): r(q) { }; obiekt o nazwie q klasy X
. . . . . . . . . . . . . . . .
};

o

Związki posiadania z użyciem wskaźników i referencji
tworzą swoistą hierarchię obiektów a nie klas.

Związki używania

Cytaty: [ B. Stroustrup ]

1/

„Wiedza o tym, jakich klas używa dana klasa i w jaki sposób,

jest często decydująca do wyrażenia i zrozumienia
projektu.”

Związek, zwany na poziomie implementacji związkiem
użycia, jest na poziomie analizy obiektowej zwany związkiem
zależności.

RozkładZajęć

dodaj(p : Przedmiot)
usuń(p : Przedmiot)

Przedmiot

Iterator

<<friend>>

r

Zależność

Zależność

background image

61

W tym przykładzie klasa RozkładZajęć używa klasy
Przedmiot

. Zmiany dokonane w specyfikacji klasy Przedmiot

mogą mieć wpływ na definicję klasy RozkładZajęć, ale nie na
odwrót.

Klasyfikacja sposobów, w jakich jedna klasa X może używać
innej klasy Y:
1. X używa nazwy klasy Y (jak w przykładzie powyżej),
2. X używa Y, ponieważ:

2.1. X czyta składową Y,
2.2. X zapisuje składową Y,
2.3. X wywołuje funkcje składową Y,

3. X tworzy obiekt klasy Y, tj. X przydziela pamięć dla

statycznego lub automatycznego obiektu klasy Y, lub
tworzy dynamiczny obiekt Y za pomocą operatora new,

4. X pobiera rozmiar Y

Związki zaprogramowane (inaczej ukryte)

Załóżmy,

że

w

projekcie

tworzonego

systemu

wyspecyfikowano, że każda operacja, która nie może być
obsłużona przez klasę A, powinna być obsłużona przez klasę
B, posiadaną przez klasę A.

class B
{ . . . . . .
void f( ); void g( ); void h( ); }

class A
{ B *p;
. . . . . .
void f( ); void ff( );
void g( ) { p→g( ); } // delegowanie g( )
void h( ) { p→h( ); } // delegowanie h( )
};

background image

62

Związki zaprogramowane są głęboko ukryte w implementacji,
przez to mało widoczne a ich skutki są trudne do
przewidzenia.


Interfejsy i implementacje






Komponent jest fizyczną, wymienną, częścią systemu
informatycznego. Komponent, obok swojej implementacji,
wykorzystuje i realizuje pewien zbiór własnych interfejsów.
Interfejs komponentu jest zestawem operacji, zamkniętych w
klasie (lub klasach) interfejsowych, które to operacje
wyznaczają usługi oferowane przez komponent. Taki zestaw
usług określa tzw. szwy systemu.

Idealny interfejs:

• udostępnia wszystkie obowiązki komponentu „reszcie

świata” w sposób pełny i logicznie spójny,

• nie ujawnia użytkownikowi szczegółów implementacyj-

nych,

• jest wyrażony za pomocą typów z poziomu użytkownika,

• w ograniczony i dobrze zdefiniowany sposób zależy od

innych interfejsów.

// przykład interfejsu w złym stylu
class X {
Y a;
public:
void f( const char *, . . . );
// funkcja interfejsowa ze zmienną liczbą parametrów

implementacja

interfejsy

komponent

background image

63

void ustaw_a( Y& );
// funkcja interfejsowa z parametrem w postaci referencji do
nieznanej klasy
Y& wez_a( );
// typ wyniku funkcji interfejsowej w postaci referencji do
nieznanej klasy
};

Zawarte w klasie interfejsowej powyższego przykładu metody
realizują interfejs na bardzo niskim poziomie abstrakcji,
ujawniając szczegóły implementacji. Nie są samoopisujące
się.

Zasady praktyczne projektowania

Motto:
„Nie

ma

jednej

„właściwej”

metody

projektowania.

Projektowanie

wymaga

wyczucia,

doświadczenia

i

inteligencji.”

1. Użyj publicznego dziedziczenia do reprezentowania relacji

bycia.

Jeśli class Y: public X { ... }; to klasa Y jest swego
rodzaju klasą X.
2. Użyj wskaźników, lub referencji, do reprezentowania relacji

posiadania.

3. Upewnij się, że zależności używania są zrozumiałe,

minimalne i niecykliczne.

4. Wyrażaj interfejsy w kategoriach typów z dziedziny

zastosowań.






background image

64

Rozkład zobowiązań w modelowanym systemie

Ilościowy rozkład zobowiązań dla poszczególnych klas
powinien w modelowanym systemie być taki, aby
poszczególne klasy obejmowały porównywalne względem
siebie ilości atrybutów i usług. Poniższy rysunek przedstawia
poglądowo jako niedopuszczalne modele 1 i 3.







MODEL 1 – każde dana jest klasą
( klasy są jednopolowe )

MODEL 2 – model opcjonalny (ze zrównoważonym
rozkładem zobowiązań)

MODEL 3 – model z jedną tylko klasą
= model wg. paradygmatu strukturalnego

Modele 1 i 3 są nie do przyjęcia.

Aktualne

podejście

do

tworzenia

oprogramowania

charakteryzuje:

- zacieranie

podziałów

między

etapami

analizy,

projektowania i implementacji,

- rozdział

w

metodyce

tworzenia

systemów

informatycznych

korporacyjnych

(opartych

na

platformach typu: Windows, Unix, OS )i systemów
WEB-owych (opartych o Internet),

MODEL 1

MODEL 2

MODEL3

background image

65

- ciągle wzrastający udział i specjalizacja gotowych

komponentów w procesie tworzenia oprogramowania,

- stosowanie wzorców projektowych.


Funkcje operatorowe
( czyli przeciążanie operatorów)

Dlaczego zmuszeni jesteśmy pisać funkcje operatorowe ?

• Aby rozszerzać liczbę operatorów działających na znanych

typach. Np. typowi int towarzyszą obsługujące go operatory
+ - * /. Każdy z tych operatorów możemy jednak
przeciążyć, nadając im nowe, dodatkowe znaczenia.

• Aby definiować nowe operatory dla nowych klas, takich

jak: liczby zespolone, obiekty algebry wektorów, macierzy i
wyznaczników, napisy, sygnały, itd.

Zadeklarujmy w klasie COMPLEX zaprzyjaźnienie z nią
dwóch funkcji operatorowych, implementujących wybrane
operacje na liczbach zespolonych.

class COMPLEX
{ double re, im;
public:
COMPLEX( double r, double i ): re(r), im(i) {}
friend COMPLEX operator+( COMPLEX, COMPLEX);
friend COMPLEX operator*( COMPLEX, COMPLEX);
};

Przykłady użycia:
COMPLEX a(1,2), b(0,1), c(0,0);
a = b + c;
// użyto operatora + dla typu COMPLEX, wymagana będzie
jednak definicja operatora przypisania =

background image

66

b = b + c * a; // użyto operatorów „+” i „* ”
// zapis jest równoważny b = b + (c * a);
// bowiem przeciążone operatory zachowują swoją
// składnię ( priorytety i wiązania )
c = a * b + COMPLEX(10,20);
// w wyrażeniu wystąpił obiekt tymczasowy
// (część pogrubiona wyrażenia)
COMPLEX d = operator+( a ,b );
// jawne użycie funkcji operatorowej
// zapis jest równoważny COMPLEX d = a + b ;

Ogólne zasady przeciążania operatorów:
1. można przeciążać wszystkie operatory, za wyjątkiem

. .* :: ? :

2. nie można zmieniać składni operatorów, w szczególności:

priorytetów i wiązań, zamieniać operator unarny na
binarny, i odwrotnie, np. definiować binarny operator !

3. nie można wprowadzać nowych symboli operatorów,

np. operatora ** dla potęgowania (bo brak w C++ składni
dla takich operatorów).


Ogólnie funkcja operatora może być:
1. funkcja składową klasy, na obiektach której ma działać

operator,

2. funkcją

nie

będącą

składową

klasy

(funkcją

zaprzyjaźnioną).

Przeciążanie operatorów jednoargumentowych (unarnych)

Operator unarny można zdefiniować jako:
1. bezparametrową funkcję składową klasy,
2. funkcję o jednym argumencie, zaprzyjaźnioną z klasą.

background image

67

Na przykład użycie ++ x będzie oznaczać wywołanie:

dla przypadku 1. - x . operator++( );

dla przypadku 2. - operator++( x );


Przeciążanie operatorów dwuargumentowych (binarnych)

Można je definiować jako:
1. jednoparametrowe, funkcje składowe klasy (podlegają

dziedziczeniu !), wówczas x @ y (gdzie @ jest symbolem
operatora) odpowiada wywołaniu funkcji operatorowej
x . operator@( y )

2. dwuparametrowe funkcje zaprzyjaźnioną z klasą, wówczas

x @ y

odpowiada

wywołaniu

funkcji

operatorowej

operator@( x, y )


Przykłady definicji funkcji operatorów unarnych:

• jako funkcji składowych klasy

COMPLEX COMPLEX:: operator−( )
{ re = − re; im = − im;
return * this;
}

• jako funkcji zaprzyjaźnionej z klasą

COMPLEX operator−( COMPLEX &c )
{ return COMPLEX(−c.re, −c.im ; }




background image

68

Przykłady przeciążania operatorów binarnych:

class WEKTOR
{
double x, y;
public:
WEKTOR( ): x(0), y(0) { }
WEKTOR( double px, double py): x(px), y(py) { }
WEKTOR operator+ ( WEKTOR & );
WEKTOR operator+=( WEKTOR & );
friend WEKTOR operator−(WEKTOR &,WEKTOR &);
friend WEKTOR operator*(double, WEKTOR &);
friend ostream & operator<<( ostream &,WEKTOR &);
};

inline WEKTOR WEKTOR:: operator+
( WEKTOR & U );
{ return WEKTOR( this→x+U.x, this→y+U.y ); }

inline WEKTOR operator−( WEKTOR & U,
WEKTOR & V);
{ return WEKTOR( U.x−V.x, U.y−V.y ); }

inline WEKTOR operator*( double k, WEKTOR & U);
{ return WEKTOR( k * U.x, k * U.y ); }

inline ostream & operator<<( ostream & st,
WEKTOR & U);
{ st << ‘[‘ << U.x << ‘,’ << U.y << ‘]’ ; return st; }



background image

69

Przykłady użycia:

WEKTOR A( 1, 1 ), B( 5, 5 ), C( -3, 3 );
cout << ‘\n’ << A + B << ‘\n’ << A − B << ‘\n’ <<
2 * C + A;

Otrzymane wyniki:

[ 6, 6 ]
[ - 4, - 4 ]
[ - 5, 7 ]

Przeciążanie złożonych operatorów przypisania op=

Złożone operatory przypisania to operatory typu: +=, *=, <<=.
W języku C++ zdefiniowano 11 takich operatorów. Pozwalają
one zapisać skrótowo wyrażenia, w których zmienne stojące
po lewej stronie operatora przypisania, występują również w
wyrażeniach stojących po prawej stronie tego operatora. Na
przykład zamiast x = x + y możemy zapisać x += y.

Ograniczenia w przeciążaniu operatorów przypisania:

• można używać wyłącznie funkcji składowych,

• operatorów przypisania nie można dziedziczyć.


Poniżej

przykład

przeciążenia

złożonego

operatora

przypisania dla klasy WEKTOR za pomocą funkcji składowej:

WEKTOR & WEKTOR:: operator+=( WEKTOR & U )
{ this→x += U.x; this→y += U.y; return * this;
}

Jest to sytuacja, w której użycie zmiennej this jest istotnie
konieczne – aby przekazać wektor po wykonaniu operacji
polegającej na jego zwiększeniu o wektor U.

background image

70

Przykład użycia tak zdefiniowanego operatora C += A + B;

Zapis ten równoważny jest wywołaniu funkcji operatorowych
C.operator += ( A.operator+ (B) );



W wyniku działania operatora + z wyrażenia A + B
tworzony jest obiekt tymczasowy (użyty za chwilę jako
drugi argument operatora += po poddaniu go konwersji
trywialnej do WEKTOR &).



Obiekt tymczasowy jest po użyciu usuwany, chociaż można
tak zapisać funkcję operatorową, aby nie był usuwany.



Dzięki użyciu referencji w wyniku funkcji, pierwszy
argument operatora przypisania może być l-wartością i stać
po lewej stronie operatora przypisania.


Przeciążanie operatora indeksowania [ ]

Przystępując do studiowania tego rozdziału musimy mieć
świadomość, że wydobywając i-ty element tablicy t poprzez
zapis

t[i]

posługujemy

się

w

gruncie

rzeczy

dwuargumentowym operatorem indeksowania w sposób
następujący t[]i , gdzie lewym argumentem operatora
indeksowania jest nazwa tablicy, a prawym – położenie
interesującego nas jej elementu. Ponieważ wyrażenie t[i]
powinno być l-wyrażeniem (aby można było do niego
podstawiać wartości) deklaracja funkcji operatorowej dla
tablicy przechowującej liczby całkowite powinna mieć postać

int & operator[](int i);

Funkcje tę należy zgłosić w klasie, która obsługuje jakąś
kolekcję danych typu int, np. tablicę danych typu int.

W poniższym przykładzie rozważymy klasę STRING
posiadającą i obsługującą łańcuch znakowy

background image

71

class STRING
{
char *str;
public:
STRING( void ): str( NULL ) { }
// poniżej konstruktor kopiujący
STRING( char *s ): str( strdup( s )) { }
// poniżej sprzątający po sobie destruktor
∼STRING( void ) { delete str; }
// poniżej definicja przeciążonego operatora indeksowania
char & operator[ ]( int index )
{ return *( str + index ); }
// poniżej definicja przeciążonego
// operatora wyprowadzenia do strumienia
friend ostream & operator<<( ostream & st, STRING & s)
{ return (st << s.str) ; }

Przykłady użycia obu operatorów:

STRING s( ”Adam” );
s[1] = ‘l’ ; cout << “ “<< s[0];
// powyżej wykorzystano przeciążony operator
// indeksowania dla obiektu s klasy STRING
cout << “ “ << s ;
// powyżej wykorzystano przeciążony operator
// wyprowadzenia do strumienia obiektu s klasy STRING

Wynik: A Alam





background image

72

Tablica asocjacyjna
(czyli wykorzystanie operatora [ ] )

W poprzednim przykładzie do indeksowania tablicy użyto
danych typu int, ale można tu użyć w zasadzie dowolnego
typu ( sic ! ).

Tablica

asocjacyjna

(zwana

też

słownikiem

lub

odwzorowaniem) przechowuje pary wartości pozwalając
dotrzeć poprzez obiekt zwany kluczem do obiektu zwanego
wartością.

Przykład prostej tablicy asocjacyjnej:

#include <string>
using namespace std; klucz wartość

class TAB_ASOC 2
{

struct PARA 1
{
string klucz; 0
int wartosc;
} * tab;

int max;

int wolny;
public:
TAB_ASOC( int ); // konstruktor
int & operator[ ]( const string ); (1)
// operator indeksowania [ ] zwraca referencję do
// drugiej części pary
};

Wojtek

24

tab

wolny=1

max=3

background image

73


Zdefiniowana powyżej tablica asocjacyjna przechowuje pary,
w których (służące do indeksowania) pole klucz, jest
dowolnym napisem, a wartością jest dana typu int.

// definicja konstruktora
TAB_ASOC:: TAB_ASOC( int r )
{ max = (r < 16 ) ? 16 : r ; wolny = 0 ;
wektor = new PARA[ max ];
}


Definicje operatora indeksowania tablic asocjacyjnych są
zwykle rozbudowane i obdarzone „pewną inteligencją”.
Potrafią zwykle:
- utrzymywać niezbędny rozmiar wektora par,
- wyszukiwać i przekazywać referencję do wyszukanej

drugiej części pary,

- dodawać do wektora nową parę, jeśli nie została ona tam

jeszcze dotychczas umieszczona.



Przykład użycia:

TAB_ASOC * zlicz_slowa( void )
{ const MAX = 256; // maksymalna długość słowa
char buf [MAX] ; // bufor pojedynczego słowa
TAB_ASOC wek( 512 );
while( cin >> buf ) wek[ buf ] ++ ;
return wek ;
}


background image

74


Powyższa przykładowa funkcja zlicz_slowa( ) po wywołaniu:

1.

tworzy tablicę asocjacyjną o rozmiarze początkowym
512 par,

2. pobiera z wejścia do bufora słowa buf pojedyncze słowa

i wstawia je do tablicy asocjacyjnej wraz z wartością 1
(jeśli słowo nie zostało tam jeszcze umieszczone), lub
zwiększa o 1 drugą część pary (wartość) jeżeli słowo
zostało w tablicy asocjacyjnej umieszczone wcześniej,

3. zwiększa w miarę potrzeb rozmiar tablicy asocjacyjnej,
4. po dojściu do końca czytanego tekstu funkcja kończy

swoje działanie zwracając wskazanie do utworzonej
przez siebie tablicy asocjacyjnej.


Wykonywanie pk. 2 i 3 odbywa się w poleceniu wek[ buf ]++
i jest możliwe dzięki „zaszyciu” powyższej funkcjonalności w
funkcji operatorowej implementującej działanie operatora
indeksowania [ ].
Tego typu podejście jest już realizacją jednego z
paradygmatów programowania uogólnionego, które zakłada
jednolitą dla różnych struktur danych, metodę obsługi poleceń
(w tym przypadku polecenia wyszukiwania).


Iteratory

(czyli przeciążenie operatora wywołania funkcji ( )


Zadaniem iteratora jest zawsze dostarczanie obiektów w
określonym porządku. Aby iterator miał dostęp do
składowych struktury, którą iteruje, definiujemy go jako klasę
zaprzyjaźnioną z tą strukturą.

background image

75

Zdefiniujemy przykładowo klasę iteratora dla tablicy
asocjacyjnej:

class ITERATR_ASOC
{
const TAB_ASOC * ta ; // wskazanie iterowanej tabl. asocj.
int ind; // indeks bieżący w tablicy asocjacyjnej * ta
public:

ITERATOR_ASOC( const TAB_ASOC & s )

{ ta = & s; ind = 0 ; } // konstruktor
PARA * operator( ) ( void ) // funkcja iteratora
} ;

W definicji klasy TAB_ASOC należy umieścić deklarację
zaprzyjaźnienia w postaci:

friend class ITERATOR_ASOC ;

Operator wywołania funkcji ( ) jest zwykle używany w
sposób, który ilustruje wywołanie sin(x), gdzie sin jest nazwą
funkcji, a x jej argumentem. Nic nie stoi na przeszkodzie aby
powyższe wyrażenie zapisać sin( )x i spróbować przeciążać
ten operator tak, jak to zrobiono w klasie ITERATOR_ASOC.

W klasie ITERATOR_ASOC zdefiniowaliśmy funkcje
iteratora w postaci funkcji operatorowej, wywoływanej na
rzecz obiektu klasy ITERATOR_ASOC (czyli iteratora) z
pustą listą argumentów. Funkcja ta dostarcza wskazanie
aktualnego elementu tablicy asocjacyjnej (pary). Jest to
możliwe, ponieważ obiekt klasy iteratora posiada swoją
prywatną daną (ind ), która posiada to aktualne wskazanie w
iterowanym obiekcie klasy TAB_ASOC.

background image

76

Iterator ( tj. obiekt klasy iteratora ) inicjuje się w momencie
jego deklaracji. Natomiast każde użycie metody w postaci
funkcji operatorowej operator( ) zwraca zwykle wskazanie do
aktualnego

elementu

iterowanej

tablicy

asocjacyjnej,

(inkrementując jednocześnie indeks ind), lub 0 - po dojściu do
zajętego obszaru tablicy asocjacyjnej.

Przykłady użycia:
TAB_ASOC wek( 512 );
// deklaracja tablicy asocjacyjnej
ITERATOR_ASOC następny( wek );
// utworzenie obiektu klasy iteratora „przypiętego” do
// tablicy asocjacyjnej wek
TAB_ASOC:: PARA * p ;
// deklaracja wskazania pary jako zmiennej pomocniczej
while ( p = następny( ) )
cout << p → nazwa << ” : ” << p → wartosc << ‘\n’ ;
// następny jako nazwa obiektu klasy iteratora występuje
// w roli lewego argumentu operatora ( ),
// prawy argument nie występuje.

W

powyższym

przykładzie

każdorazowe

wywołanie

następny( ) zwraca wskazanie kolejnej pary w iterowanej
tablicy asocjacyjnej, co umożliwia wydrukowanie w pętli obu
składowych pary. Po dojściu do końca zajętego obszaru
tablicy asocjacyjnej zwrócona zostanie wartość 0 i działanie
pętli zakończy się.

o

Można jednocześnie aktywować wiele iteratorów tego
samego typu,

o

Można zdefiniować również inne iteratory, np.
pierwszy( ), ostatni( ), seek( parametr ). Muszą to być
jednak klasy iteratory a nie tylko funkcje.

Koniec części IV wykładu


Wyszukiwarka

Podobne podstrony:
Jezyki i paradygmaty cz V
MATERIALY DO WYKLADU CZ IV id Nieznany
debres II, Języki, JH-1, Espanol IV sem
Tresci kursu, studia Polibuda Informatyka, III semestr, języki paradygmaty programowania (jipp)
ETYKA ZAWODU.cz.IV
Choroby kolkowe koni cz IV
Pięcioksiąg cz. IV - Rdz (Kobieta w Księdze Rodzaju, Teologia(3)
Dziady cz IV
Głębia ostrości cz IV
FINANSE - pytania testowe cz. IV
Dziady cz IV
Kol W2, studia Polibuda Informatyka, III semestr, języki paradygmaty programowania (jipp), kolos 2
piesni cz IV 2012, LITURGICZNE
A Mickiewicz Dziady cz IV
cz+iv 4PNHU5G3PV35PS757BS6JX766CNVU4NF3CXZQSQ
piosenki wspólnota miłości ukrzyzowanej śpiewnik cz IV format A5
Instrukcja VisSim cz IV Kanaly

więcej podobnych podstron