Jezyki i paradygmaty cz V

background image

77


Obsługa błędów i wyjątków

Terminy błąd i wyjątek mogą być używane zamiennie,
oznaczają one zawsze sytuację wyjątkową, która powinna
być obsłużona w sposób szczególny.

Znane mechanizmy obsługi błędów i wyjątków to:

1. propagacja błędu wstecz .
2. skorzystanie z predefiniowanych zmiennych globalnych

(np. erno ) przechowujących numery błędów.


Ad.1: Metoda ta polega na wbudowaniu w kod instrukcji
wykrywania błędów. Po wykryciu błędu, przerywana jest
dalsza realizacja algorytmu, a funkcja zamiast wyniku
przekazuje do miejsca wywołania sygnał błędu. Tam z kolei
zamiast obliczeń zachodzi dalsza propagacja błędu wstecz aż
do miejsca jego wizualizacji użytkownikowi.
Programując musimy jednak wystąpienie każdej takiej
sytuacji wyjątkowej przewidzieć i "ręcznie" zaprojektować
obsługę każdego błędu. Metoda sprawdza się bardzo dobrze w
programach o bardzo zwartych, nie zmieniających się kodach
(układy sterowania, kompilatory), natomiast jest nie do
przyjęcia w dużych, otwartych systemach.

Ad.2: Metoda ta ma wartość jedynie historyczną. Polega na
zastawieniu „pułapki” w miejscu narażonym na wystąpienie
określonego błędu, poprzez śledzenie wartości zmiennych
typu erno. Jeśli błąd wystąpi, obsługuje się go tylko w miejscu
wystąpienia. Na ogół bardzo trudno (a w dużych, otwartych
systemach wręcz niemożliwym jest) przewidzenie i obsłużenie
wszystkich skutków błędu, lub sytuacji wyjątkowej.

background image

78

Metoda try – throw - catch

Jest to najczęściej dziś używana metoda obsługi błędów i
wyjątków, chociaż trudno jej przypisać pełną doskonałość.

Zasada ogólna omawianej metody:
• Funkcja napotykająca problem, z którym nie może sobie

poradzić automatycznie zgłasza ( throw ) go do miejsca,
skąd została wywołana.

• Funkcja obsługująca problemy sygnalizuje chęć wyłapania

wyjątków ( catch ).


Poniżej przykład klasy z obsługą błędu przekroczenia zakresu
tablicy:

class WEKTOR
{ int * p; int rozm;
public:
class ZAKRES ; // deklaracja klasy wyjątku
int & operator [ ] ( int i ); // operator indeksowania
// . . .};

Poniżej definicja operatora [ ] ze zgłoszeniem obsługi wyjątku

int & WEKTOR:: operator[ ] ( int i )
{ if( 0 <= i < rozm ) return p[ i ] ;
throw ZAKRES ( ) ;
}

Poniżej przykład użycia opisywanego mechanizmu:

int main( )
{ // . . .

try { . . . rob_cos( w ); . . . }

catch ( WEKTOR:: ZAKRES )

{ // tutaj kod obsługi błędu zakresu }

background image

79


Działanie:
Jeżeli wywołanie jakiejkolwiek funkcji wewnątrz try{ }, np.
rob_cos(w) (lub też innej przez nią wywołanej) spowoduje że
wywołanie operatora [ ] nastąpi z niewłaściwym indeksem, to
procedura catch wyłapie wyjątek i zostanie wykonany kod
obsługi błędu.
Konstrukcji catch( ) można używać tylko natychmiast po
bloku poprzedzonym słowem kluczowym try. W nawiasach
specyfikujemy typ obiektu klasy wyjątku, z którym wchodzi
się do procedury obsługi błędu catch( ). Obiekt taki tworzony
jest automatycznie.

Poniżej definicje przykładowych funkcji zewnętrznych,
prowadzące do wystąpienia błędu zakresu:

void rob_cos( WEKTOR & w )
{ // . . .
krach( w );
// . . . }

void krach( WEKTOR & w )
{ w[ w.jaki_rozm( )+10 ]; }

Bardziej szczegółowy opis mechanizmu funkcjonowania
obsługi wyjątków:

Jeżeli funkcja ( np. operator [ ] ) zgłasza wyjątek, następuje,
od punktu zgłoszenia, przeglądanie łańcucha wywołań w
poszukiwaniu procedury obsługi wyjątku. Jednocześnie
zwijany jest stos wywołań aż do zrębu stosu funkcji
wyłapującej ( w naszym przykładzie jest to funkcja main). Po
drodze wywoływane są destruktory i niszczone wszystkie
obiekty lokalne.

background image

80

Po wyłapaniu wyjątku, inne procedury obsługi, jeżeli nawet
istnieją dla tego wyjątku, już go nie będą dotyczyć.

Przykład:

int ff( WEKTOR & w ) {
try{ f(w); }
catch( WEKTOR:: ZAKRES ) // ( 1 )
{ /* . . . */ }
}

Ponieważ funkcja f sama obsługuje wyjątki, obsługa
wyjątków ( 1 ) zapisana w funkcji ff nigdy nie będzie wołana.

 Jeżeli w procesie zwijania łańcucha wywołań nie

napotkana zostanie odpowiednia procedura obsługi
wyjątku, wykonanie programu zostanie przerwane z
błędem.

 Powyższy mechanizm obsługuje tylko zdarzenia

synchroniczne.

Zdarzenia

asynchroniczne

(np.

przerwania od klawiatury) nie mogą być obsługiwane w
ten sposób.


Zalety omawianego mechanizmu obsługi błędów i wyjątków:

 duża czytelność, wynikająca z oddzielenia kodu obsługi

od "zwykłego" kodu,

 większa efektywność,

 bardziej regularny styl programowania, chociaż nie jest

on jeszcze dostatecznie strukturalny.




background image

81

Paradygmat programowania uogólnionego

Paradygmat programowania uogólnionego zakłada skupienie
się na ogólnych mechanizmach manipulowania strukturami
danych bez skupiania się na różnego rodzaju szczegółach
implementacyjnych, przede wszystkim uzależnienia kodu
programu od użytych typów danych.

Pierwszym, niesłychanie istotnym krokiem w tym kierunku
było powstanie (omówionych wcześniej) szablonów klas
(typów sparametryzowanych). Pozwalają one na budowanie
klas opisujących byty ogólne (stosy, kolejki, tablice
asocjacyjne, itd.) bez określania typów obiektów w tych
strukturach przechowywanych.

Dalszym krokiem było zbudowanie uogólnionych iteratorów
i algorytmów. Doprowadziło to do zbudowania ( i
zatwierdzenia w 1998 roku) standardowej biblioteki
szablonów STL (Standard Template Library). Biblioteka ta
zawiera trzy rodzaje bytów ogólnych:

 uogólnione kolekcje danych (inaczej: kontenery)

 uogólnione iteratory,

 algorytmy uogólnione.

Kolekcje (Kontenery)

Kontener jest strukturą danych zawierającą obiekty tego
samego typu, ułożone w jednolity dla danego typu kontenera
sposób. Kontener zapewnia narzędzia dostępu. W zależności
od przyjętej organizacji, poszczególne kontenery różnią się
wydajnością poszczególnych operacji.
Bardziej

złożone

kontenery

charakteryzować

może

specyficzna organizacja przechowywanych danych lub
istnienie

dodatkowych

operacji

do

manipulowania

zawartością.

background image

82

Kontenery nakładają czasem pewne ograniczenia na dane,
które mogą być w nich przechowywane. Ograniczenia te
wynikają zazwyczaj z przyjętej struktury przechowywania
danych, zależności pomiędzy danymi itp.

Biblioteka STL zawiera następujące kontenery:

• deque

- kolejki dwustronne

• list

- listy liniowe

• map i multimap

- tablice asocjacyjne

• set i multiset

- zbiory i multizbiory

• stack

- stosy

• queue i priority_queue - kolejki i kolejki priorytetowe

• vector

- tablice ”dynamiczne”

• Kontenery w STL zaimplementowano jako szablony klas,

zaopatrzone w metody.

• Większość metod jest wspólnych dla wszystkich

kontenerów,

choć

mogą

one

być

różnie

zaimplementowane dla poszczególnych kontenerów.


Operatory wspólne dla wszystkich kolekcji

Następujące operatory są wspólne dla wszystkich kolekcji (w

tym dla klasy string):

 operatory == oraz != przekazują wartość true lub false,

 operator przypisania = kopiuje jedną kolekcje do drugiej

 empty ( ) przekazuje wartość true gdy w kolekcji nie ma

przechowywanych elementów,

 size( ) przekazuje liczbę elementów bieżąco przecho-

wywanych w kolekcji,

 clear( ) usuwa wszystkie elementy kolekcji,

background image

83

 begin( ) przekazuje iterator (wskaźnik) na pierwszy

element kolekcji uporządkowanej,

 end( ) przekazuje iterator (wskaźnik) na pierwszy adres

za ostatnim elementem kolekcji uporządkowanej,

 insert( ) dodaje jeden element (lub pewien zbiór

elementów) do kolekcji,

 erase( ) usuwa jeden element (lub pewien zbiór

elementów) z danej kolekcji.

Zachowanie dwóch ostatnich operacji zależy od tego, czy

kolekcja jest uporządkowana, czy asocjacyjna.

Iteratory

Iterator to obiekt klasy iteratora używany do wskazywania
obiektu z kontenera. Iterator to swego rodzaju uogólnienie
wskaźnika.

Można zdefiniować własny iterator dla określonej kolekcji
używając zdefiniowanej w bibliotece STL klasy

iterator. Na

przykład:

vector <string> svec;
// powyżej deklaracja obiektu svec klasy vector do

// przechowywania napisów (obiektów klasy string)

vector<string>::iterator iter=svec.begin;

Iterator iter został tu zdefiniowany jako iterator dla wektora
napisów i zainicjowany tak, aby wskazywał na pierwszy
element tego wektora. Można go używać, tak jak wskaźnika
wbudowanego,

a

więc

dopuszczalne

będzie

użycie

operatorów: ++ * = = != ->.


background image

84

Przykład użycia tak zdefiniowanego iteratora:

for( ; iter!=svec.end( ); iter++)
cout<<iter->size( )<<”: „<<*iter<<endl;

Algorytmy uogólnione

Algorytmy

uogólnione

uogólnionymi

funkcjami,

używanymi

do

rozwiązywania

najróżniejszych

często

spotykanych, problemów. Typowe algorytmy, to: znalezienie
elementu w kontenerze, wstawienie elementu do ciągu
elementów w kontenerze, usunięcie elementu z ciągu,
modyfikacja elementu, porównywanie elementów, sortowanie
ciągu, itd.
Nieomalże wszystkie algorytmy do wskazywania zakresu
elementów, na których działają, używają iteratorów. Pierwszy
iterator wskazuje pierwszy element zakresu, drugi iterator –
pierwszy element poza zakresem. Zakłada się, że zawsze
można dojść do iteratora drugiego poprzez inkrementację
iteratora pierwszego.
Algorytmów uogólnionych można używać zarówno do
kolekcji z biblioteki STL, jak i tzw. kolekcji wbudowanych, tj.
zdefiniowanych przez programistę.

Przykłady użycia omówionych bytów ogólnych:

const int asize=8;
int ia[asize]={1,2,3,4,5,6,7,8};

vector<int>ivec(ia, ia+asize);
list <int> ilist(ia, ia+asize);
//użyto konstruktorów klas vector i list z inicjacją obu

//obiektów wszystkimi elementami tablicy ia


background image

85

int *pia = find( ia, ia+asize, 13 );
if( pia != ia+asize ) //znaleziono
//powyżej wywołanie algorytmu uogólnionego find z użyciem

//wskaźników wbudowanych do tablicy wbudowanej

vector <int>:: iterator it;
it = find( ivec.begin( ), ivec.end( ), 13 );
if( it != ivec.end( ) ) //znaleziono
//wywołanie funkcji find dla kolekcji w postaci wektora ivec z

//użyciem iteratora it

list <int>:: iterator iter;
it = find( ilist.begin( ), ilist.end( ), 13 );
if( iter != ilist.end( ) ) //znaleziono
//wywołanie funkcji find dla kolekcji w postaci listy ilist z

//użyciem iteratora iter

W implementacji funkcji find użyto operatora przyrównania

dla typu elementu int. Jeżeli dla innego typu nie można użyć
operatora równości, lub jeżeli użytkownik chciałby inaczej
zdefiniować to przyrównanie, to trzeba szukać rozwiązania
bardziej ogólnego.

Są dwie możliwości rozwiązania tego problemu:
- użycie funkcji przekazywanej jako wskaźnik do funkcji,
- użycie obiektu funkcyjnego (funktora).

Obok algorytmu uogólnionego find( ) istnieje jeszcze
algorytm uogólniony find_if( ), który zapewnia pożądaną
przez nas elastyczność przez przekazywanie wskaźnika do
funkcji, lub obiektu funkcyjnego, zamiast używania operatora
przyrównania dla podstawowego typu elementu.


background image

86


Funktory
wykorzystujące przeciążanie operatora wywołania funkcji ( )

Każdy obiekt klasy, zawierającej definicję operatora
wywołania funkcji, nazywamy funktorem. Przypomnijmy, że
operator wywołania funkcji ( ) jest dwuargumentowym
operatorem, dla którego lewym argumentem jest nazwa
funkcji, a prawym – lista argumentów funkcji. Może być
przeciążany tak jak każdy inny operator.

Ponadto:

• Może zwracać daną dowolnego typu i mieć dowolną

liczbę parametrów.

• Podobnie jak operator przypisania, może być przeciążany

tylko jako metoda klasy.

• Funktor jest obiektem zachowującym się jakby był

funkcją (sic!!!).

• Kiedy funktor jest używany, jego parametry stają się

parametrami operatora wywołania funkcji.

Obiekty funkcyjne

Przypomnijmy, że obiekt funkcyjny jest obiektem specjalnej

klasy iteratora, zaprzyjaźnionej z klasą posiadającą strukturę
typu tablica, lista. Taka klasa iteratora dostarcza przeciążonej
wersji operatora wywołania funkcji (patrz str. 74 w części IV
wykładu).
W bibliotece standardowej STL znajduje się:

- sześć

arytmetycznych

obiektów

funkcyjnych,

implementujących podstawowe operacje arytmetyczne, np.
plus<typ>, gdzie typ jest typem standardowym, lub typem
klasy wbudowanej,

background image

87

- sześć

relacyjnych

obiektów

funkcyjnych,

implementujących

wszystkie

operacje

relacji,

np.

greater<typ>,

- trzy logiczne obiekty funkcyjne, np. logical_not<typ>.

Aby użyć pierwotnie zbudowanych obiektów funkcyjnych

musimy dołączyć do programu następujący plik nagłówkowy

#include<functional>

Przykłady użycia:

Algorytm sort będzie porządkował elementy kolekcji w

kolejności malejącej, jeśli przekażemy mu jako trzeci
parametr obiekt funkcyjny greater tak jak to przykładowo
pokazano poniżej

sort( ivec.begin( ), ivec.end( ), greater<int>( ));

Ostatni argument wywołania powoduje utworzenie i

przekazanie do algorytmu sort( ) nie nazwanego obiektu
funkcyjnego klasy greater dla typu całkowitego. Wywołanie
algorytmy sort( ) bez trzeciego argumentu powoduje
domyślne użycie obiektu funkcyjnego less (mniejszy) i
posortowanie w kolejności rosnącej.

Mapy i multimapy

Na str.72 w części IV wykładu została omówiona klasa

tablicy asocjacyjnej z przeciążonym operatorem indeksowania
[ ]. Przedstawiono tam szczegółowo implementację tego
przeciążonego dla tablicy asocjacyjnej operatora.

Wzorzec tablicy asocjacyjnej (często zwany słownikiem)

został zaimplementowany w bibliotece STL jako tzw. mapa.
Przy czym odgrywający tutaj zasadniczą rolę operator [ ] ma
implementację zgodną z przestawioną w IV części wykładu.

background image

88

Rozważmy jeszcze raz przykład, analizowanego przy okazji

omawiania tablicy asocjacyjnej, problemu zliczania słów w
tekście. Tym razem skorzystamy z kolekcji map.

Użyte w tablicy asocjacyjnej pary składać się będą z klucza

do indeksowania typu string oraz wartości typu int:

#include <map>
#include <string>
map<string,int> words;
// utworzenie słownika o nazwie words

Dla naszego problemu zliczania ilości wystąpień słów w

tekście zapiszemy:

string tword;
while( cin>>tword ) (1)
words[tword]++;

Zauważmy, że jest to zapis o tej samej funkcjonalności co

kod na str. 73 dla tablicy asocjacyjnej wbudowanej (ale
znacznie prostszy).

Wyrażenie words[tword] wyszukuje w słowniku words typu

map wartości związanej z napisem znajdującym się w
obiekcie tword. Wartość ta jest następnie inkrementowana o
jeden. Jeśli wczytany napis nie znajduje się w słowniku,
zostanie on do kolekcji words wprowadzony z wartością 0, po
czym

wartość

ta

zostanie

w

naszym

programie

zinkrementowana do 1.

Odbywać się też będzie automatycznie, w miarę potrzeb,

zwiększanie obszaru pamięci zajmowanej przez kolekcję.

background image

89

Zapiszmy teraz polecenie drukowania zawartości słownika

words z użyciem predefiniowanych w klasie map składowych
first oraz second oraz odpowiedniego iteratora:

map<string,int>::iterator it=words.begin( );
for( ; it!=words.end( ); ++ it )
cout<<”klucz: ” <<it->first
<<” wartość: ” <<it->second<<endl;

Mamy dwie możliwości wyszukiwania w słowniku według

pierwszego obiektu pary, tj. klucza.

Pierwszym sposobem będzie wykorzystanie przeciążonego

operatora indeksowania [ ]:

int count=0;
if( !(count=words[”napis”]) ) //”napis” nie występuje

Po wykonaniu operacji zmienna count otrzyma wartość

drugiej części pary (jeśli ”napis” występuje w słowniku), lub
wartość 0 (jeśli nie występuje). Niestety, efektem ubocznym
będzie wstawienie poszukiwanego napisu do słownika (z
wartością drugiej części pary równą 0 ). Ogólnie – wartość
domyślna drugiej części pary, przy wstawianiu nowej pary,
zależy od typów elementów pary. Dla pary typów <string,int>
wynosi ona 0.

Drugą możliwością będzie użycie specjalnie zaprojektowanej

dla kolekcji map metody count( ), która zwraca liczbę
wystąpień przekazanego jej elementu w słowniku:

int count=0;
string search_word(”napis”);
if( words.count(search_word) )// OK - występuje

count=words[search_word];

background image

90

W słowniku map może znajdować się tylko pojedynczy

egzemplarz klucza pierwszej części pary. Jeżeli chcemy
przechowywać wiele par z tym samym egzemplarzem klucza,
to musimy użyć kolekcji multimap (wielosłownika).

Kolekcje typu

set

(zbiory)

Kontener set jest po prostu kolekcją kluczy. Najczęściej jest

używany, gdy chcemy wiedzieć, czy występują pewne obiekty
z większego zbioru obiektów. Na przykład, w algorytmie
przeglądania

grafu,

kolekcji

set

możemy

użyć

do

przechowywania węzłów już odwiedzonych.

W rozważanym przykładzie zliczania wystąpień wyrazów

wprowadźmy zbiór wyrazów często odwiedzanych w celu
wykluczenia ich z wyszukiwania w tablicy asocjacyjnej: :

#include<set>
#include<string>
set<string>word_exlusion;


Teraz pętla while (1) ze strony 88 będzie miała postać:

string tword;
while( cin>>tword )
{
if( word_exlusion.count( tword ) )
continue;
words[tword]++;
}

Jeśli wczytany wyraz znajduje się w zbiorze word_exlusion

metoda count( ) klasy set zwróci wartość różną od 0 i wykona
się polecenie continue przenoszące sterowanie na koniec
bloku stanowiącego instrukcję wewnętrzną pętli while. W

background image

91

przeciwnym przypadku wykona się polecenie inkrementacji
drugiej części pary dla klucza tword.

W zbiorze set znajduje się zawsze tylko pojedynczy

egzemplarz każdej wartości klucza. Aby móc przechowywać
wiele egzemplarzy kluczy musimy użyć wielozbioru multiset.
Elementy w zbiorze są domyślnie uporządkowane przy użyciu
operatora mniejszy niż. W podanym niżej przykładzie:

int ival;
int ia[10]={1,3,5,8,5,3,1,5,8,1};
vector<int> vec(ia, ia+10);
set<int> iset(vec.begin( ), vec.end( ) );

elementami znajdującymi się w zbiorze iset są {1,3,5,8}.

Poszczególne elementy dodajemy do zbioru za pomocą

metody insert( ), np.: iset.insert(ival); W celu wypisania
elementów zbioru użyjemy iteratora:

int ival;
set<int>::iterator it= iset.begin( );
for( ; it!=iset.end( ); ++it) cout << *it <” ”;
cout << endl;

Krótkie

omówienie

pozostałych

paradygmatów

programowania

1. Programowanie zdarzeniowe
(zwane też programowaniem wizualno-zdarzeniowym)

Istotą tego podejścia do programowania jest sterowanie
przebiegiem programu za pomocą zdarzeń pochodzących
z zewnątrz. W najprostszym przypadku są to zdarzenia
pochodzące z urządzeń zewnętrznych typu: klawiatura,
mysz, joystick. Tworzenie oprogramowania opartego na

background image

92

silnym powiązaniu wyświetlanych wyników działania
programu ze zdarzeniami pochodzącymi z tego typu
urządzeń jest zgodne z paradygmatem programowania
wizualno-zdarzeniowego.
Przykładem

języka

wspierającego

programowanie

wizualno-zdarzeniowe jest Visual C++.

2. Programowanie imperatywne

Jest to najbardziej pierwotny sposób programowania, w
którym program postrzegany jest jako ciąg poleceń dla
komputera, zgodnie z przebiegiem algorytmu, który
realizuje.
Ten sposób patrzenia na programy związany jest ściśle z
klasyczną budową jednoprocesorowego komputera o
architekturze

von

Neumanna,

w

której

rozkazy

wykonywane są sekwencyjnie. Większość języków
wysokiego poziomu, mimo iż posługuje się różnego rodzaju
abstrakcjami, w podstawowej warstwie wykonawczej wciąż
działa

zgodnie

z

paradygmatem

programowania

imperatywnego.

3. Programowanie w logice (logiczne, albo deklaratywne)

Programowanie sprowadza się do kompletowania zbioru
zależności (przesłanek) w celu wykazania prawdziwości
pewnego stwierdzenia (celu). Obliczenia wykonywane są
niejako „przy okazji” dowodzenia celu.

4. Programowanie funkcyjne

Nie ma tutaj imperatywnych z natury, zmiennych, ani też
tradycyjnie rozumianych pętli (bo te wymagają zmiennych
do sterowania ich przebiegiem). Nie ma też żadnych
efektów ubocznych.

background image

93

Konstruowanie programów to składanie funkcji, zazwyczaj
z istotnym wykorzystaniem rekurencji. Charakterystyczne
jest również definiowanie funkcji wyższego rzędu, czyli
takich, dla których argumentami i których wynikami mogą
być funkcje (a nie tylko „proste” dane jak liczby lub
napisy). Podobnie jak w programowaniu funkcyjnym, nie
„wydajemy rozkazów”, a jedynie opisujemy, co wiemy i co
chcemy uzyskać.

K o n i e c w y k ł a d u


Wyszukiwarka

Podobne podstrony:
Jezyki i paradygmaty cz IV
Tresci kursu, studia Polibuda Informatyka, III semestr, języki paradygmaty programowania (jipp)
Kol W2, studia Polibuda Informatyka, III semestr, języki paradygmaty programowania (jipp), kolos 2
przyklad Zad2, studia Polibuda Informatyka, III semestr, języki paradygmaty programowania (jipp)
Kol W1, studia Polibuda Informatyka, III semestr, języki paradygmaty programowania (jipp), kolos 2
po proj, studia Polibuda Informatyka, III semestr, języki paradygmaty programowania (jipp)
przyklad kol1, studia Polibuda Informatyka, III semestr, języki paradygmaty programowania (jipp)
Tresci kursu, studia Polibuda Informatyka, III semestr, języki paradygmaty programowania (jipp)
Szymański Języki i paradygmaty programowania, test
Języki i paradygmaty programowania
bd cz 2 jezyki zapytan do baz danych
cz 3 Języki zapytań do baz danych
cz 2 Języki zapytań do baz danych
bd cz 2 jezyki zapytan do baz danych
Jezyki Atari XL XE cz 2

więcej podobnych podstron