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, 

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 

są 

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 

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, 

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 

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