10. Obsuga wyjtk�w
W j�zyku potocznym przyj�o si� m�wi�, �e “wyj�tek potwierdza regu��”. O ile maksyma ta raczej nie ma odniesienia do j�zyka programowania, o tyle mo�e si� odnosi� do os�b pisz�cych programy oraz do ograniczonych zasob�w systemu.
W og�lnoœci wyj�tkiem (ang. exception) nazywamy zdarzenie, spowodowane przez anormaln� sytuacj�, wymagaj�c� przywr�cenia normalnego stanu. Dobrze zaprojektowany system obs�ugi wyj�tk�w powoduje w�wczas zawieszenie normalnego wykonywania programu i przekazanie sterowania do odpowiedniej procedury obs�ugi wyj�tku (ang. exception handler).
Wyj�tki w œrodowisku programowym mog� pochodzi� z r�nych Ÿr�de� i wyst�powa� na r�nych poziomach: sprz�towym, programu i systemu.
Na najni�szym poziomie, nazwijmy go sprz�towym, mog� wyst�powa� r�ne tego typu zdarzenia, w tym:
b��dy parzystoœci pami�ci, generuj�ce niemaskowalne przerwanie niskiego poziomu;
niesprawnoœ� urz�dzenia zewn�trznego, np. wy��czenie drukarki lub brak papieru, otwarte drzwiczki nap�du dysk�w, brak dyskietki w nap�dzie;
uszkodzenie urz�dzenia zewn�trznego.
B��dy te s� asynchroniczne wzgl�dem programu i nie maj� zwi�zku z tym, co akurat wykonuje program, a wi�c nie b�d� przechwytywane przez mechanizm obs�ugi wyj�tk�w.
Zdarzenia wymagaj�ce reakcji na poziomie programu. Wyst�puj�ce tutaj b��dy mog� mie� r�ne przyczyny: b��dny format danych wprowadzanych przez u�ytkownika, pr�ba usuni�cia nieistniej�cego pliku, pr�ba obliczenia logarytmu lub pierwiastka z liczby ujemnej, pr�ba dzielenia przez zero, pr�ba pobrania elementu z pustego stosu, pr�ba wywo�ania nieistniej�cej funkcji wirtualnej, etc.
Na poziomie systemowym do najcz�œciej wyst�puj�cych b��d�w mo�emy zaliczy� brak pami�ci przy pr�bie utworzenia nowego obiektu, albo brak miejsca na dysku.
Przy tradycyjnym podejœciu do programowania reakcje na b��dy mo�na pogrupowa� w nast�puj�ce kategorie.
Zaniechanie wykonania programu i wys�anie komunikatu o b��dzie.
Przekazanie do programu wartoœci reprezentuj�cej b��d.
Zignorowanie b��du.
Przekazanie do programu poprawnej wartoœci i przes�anie informacji o b��dzie przez specjaln� zmienn�.
Wywo�anie procedury obs�ugi b��du, napisanej przez programist�.
Ka�de z tych rozwi�za� ma trudne do zaakceptowania wady.
Pierwsze z nich mo�e by� stosowalne w takich programach jak edytory, kompilatory, gry, etc. Z regu�y wystarcza wtedy wyczyszczenie pami�ci, zwolnienie wykorzystywanych zasob�w systemu (np. zamkni�cie plik�w), wydruk komunikatu i wyjœcie z programu. Jednak jest ono nie do przyj�cia w takich programach interakcyjnych, w kt�rych program reaguj�cy na najmniejsze potkni�cie operatora zako�czeniem dzia�ania m�g�by go pozbawi� wynik�w d�ugotrwa�ej pracy.
Drugie rozwi�zanie jest na og� nie�atwe w implementacji, poniewa� w wielu przypadkach trudno jest odr�ni� wartoœ� poprawn� od b�Ÿdnej. �atwym przypadkiem jest odr��nienie b�Ÿdnego wskaŸnika. Np. funkcja czytaj�ca dane z pliku mo�e albo zwr�ci� wskaŸnik do nast�pnej pozycji, albo wskaŸnik zerowy; podobnie funkcja typu char* mo�e zwraca� pusty �a�cuch dla sygnalizacji niepowodzenia. Nie ma natomiast sposobu okreœlenia b��dnego kodu dla funkcji typu int, poniewa� ka�da wartoœ� zwracana mo�e by� uwa�ana za poprawn�. Zreszt� nawet w przypadkach, gdy takie rozwi�zanie jest dopuszczalne, mo�e si� okaza� nieop�acalne, poniewa� sprawdzanie poprawnoœci wyniku przy ka�dym wywo�aniu funkcji poci�ga za sob� du�e narzuty czasowe i pami�ciowe.
Trzecie rozwi�zanie jest trudne w implementacji i na og� niebezpieczne. Nie jest �atwym brak reakcji na b��d, szczeg�lnie w odniesieniu do funkcji, kt�ra zwraca wartoœ� inn� ni� void, czy void*. Zdarzaj� si� tak�e sytuacje, w kt�rych brak reakcji jest niedopuszczalny, np. gdy konstruktor kopiuj�cy ustali, �e nie ma doœ� pami�ci dla utworzenia kopii obiektu (w tym przypadku nie b�dzie to, rzecz jasna, b��d u�ytkownika).
Rozwi�zanie czwarte jest stosowane w standardowych bibliotekach j�zyka C. Wiele funkcji z tych bibliotek (np. funkcje matematyczne) sygnalizuje b��d, ustawiaj�c wartoœ� EDOM (b��d dziedziny) lub ERANGE (b��d zakresu) w zmiennej errno, np.
double sqrt(double x)
{ if(x < 0) { errno = EDOM; return 0; } //... }
Jest to mechanizm niezbyt pomocny dla u�ytkownika. Komunikat o b��dzie zawiera w tym przypadku jedynie typ b��du, a nie nazw� b��dnie wywo�anej funkcji. Co wi�cej, jeœli zdarz� si� dwa kolejne b��dy, to drugi mo�e przys�oni� komunikat o pierwszym. Oczywiœcie takim sytuacjom mo�na zapobiec, ale znowu kosztem sporego narzutu pami�ciowego i czasowego.
Rozwi�zanie pi�te spotyka si� w dw�ch wariantach. U�ywane w programie klasy mo�na wyposa�y� w domyœlne funkcje obs�ugi b��d�w. Zwykle s� to funkcje, kt�re drukuj� komunikat o b��dzie i powoduj� zako�czenie programu, tak jak to czyniliœmy w wielu przyk�adowych programach. Mo�liwe jest tak�e zadeklarowanie funkcji, kt�ra np. zapisuje komunikat o b��dzie do pliku i pozwala na kontynuacj� wykonywania programu. Jednak takie rozwi�zanie nie b�dzie mie� cech og�lnoœci, poniewa� w zasadzie ka�d� klas� nale�a�oby wyposa�y� w jej w�asny mechanizm obs�ugi b��d�w.
10.1. Model obsugi wyjtk�w w jzyku C++
Przeprowadzona wy�ej krytyka tradycyjnych sposob�w reakcji na wyj�tki sugeruje, �e optymalnym rozwi�zaniem by�oby rozszerzenie sk�adni j�zyka o nast�puj�ce konstrukcje:
Przekazanie od procedury obs�ugi wyj�tku do funkcji, wywo�uj�cej t� procedur�, informacji o rodzaju b��du oraz ewentualnych dodatkowych informacji.
Generalizacj� wyj�tk�w w postaci hierachii klas (np. wyj�tki “b��d dziedziny” i “b��d zakresu” s� szczeg�lnymi przypadkami wyj�tku “b��d matematyczny”).
Do��czenie do funkcji wywo�uj�cej niezale�nego kodu obs�ugi dla poszczeg�lnych b��d�w.
Automatyczne przekazanie sterowania do odpowiedniego fragmentu kodu obs�ugi b��du w przypadku zg�oszenia wyj�tku.
Zastosowany w j�zyku C++ mechanizm obs�ugi wyj�tk�w spe�nia powy�sze postulaty. Zaakceptowany przez komitety ANSI X3J16/ISO WG-21 w roku 1990 sta� si� po raz pierwszy dost�pny we wzorcowym kompilatorze AT&T wersji 3.0 we wrzeœniu 1991, a pierwsze implementacje przemys�owe firm DEC i IBM wesz�y na rynek na pocz�tku 1992. Dane te przytaczamy nie bez powodu: j�zyk, kt�ry zapewnia skuteczn� obs�ug� wyj�tk�w, mo�e s�u�y� do budowy system�w odpornych na b��dy (ang. fault-tolerant systems), a wi�c ma szans� sta� si� standardem przemys�owym.
W j�zyku C++ dla obs�ugi wyj�tk�w zastosowano model z terminacj�. Oznacza to, �e procesy obs�ugi wyj�tk�w przebiegaj� w sekwencji: zg�oszenie wyj�tku przez funkcj� - wyjœcie z jej bloku - przechwycenie przez procedur� obs�ugi - obs�uga - zako�czenie programu (lub przejœcie do nast�pnej instrukcji w bloku funkcji zawieraj�cej procedur� obs�ugi).
Nie jest to jedyne mo�liwe rozwi�zanie: wielokrotnie w innych j�zykach pr�bowano zastosowa� bardziej og�lny model ze wznowieniem. Jest to bardzo atrakcyjna alternatywa: zak�ada ona, �e procedura obs�ugi wyj�tku powinna by� tak zaprojektowana, aby mog�a ��da� wznowienia programu od punktu, w kt�rym zosta� zg�oszony wyj�tek. Model taki m�g�by by� szczeg�lnie obiecuj�cy dla unifikacji obs�ugi wyj�tk�w na poziomie programu z obs�ug� wyj�tk�w na poziomie systemu (wyczerpanie zasob�w). Jednak wieloletnia praktyka pokaza�a, �e model z terminacj� jest prostszy, bardziej przejrzysty oraz ta�szy prowadzi do �atwiejszego zarz�dzania systemami.
10.1.1. Deklaracje wyjtk�w
Mechanizm obs�ugi wyj�tk�w j�zyka C++ wprowadza trzy nowe s�owa kluczowe. Pierwsze z nich, try oznacza blok kodu, w kt�rym mog� wyst�pi� sytuacje wyj�tkowe. Ich zg�oszenie nast�puje za pomoc� instrukcji throw, a s� one obs�ugiwane w blokach poprzedzonych s�owem kluczowym catch. Bloki catch, kt�re musz� wyst�powa� bezpoœrednio za blokiem try, mog� wyst�powa� wielokrotnie. Ci�g blok�w catch, wyst�puj�cych bezpoœrednio za blokiem try, zawiera procedury obs�ugi wyj�tk�w. Konstrukcj� t� zapisuje si� w postaci:
try{ }catch() { } ... catch() { }
Wyj�tki mog� by� zg�aszane wy��cznie wewn�trz bloku try, kt�ry, podobnie jak bloki instrukcji z�o�onych lub funkcji, mo�e zawiera� deklaracje, definicje i instrukcje.
Najprostsza sk�adniowo instrukcja throw ma posta�:
throw;
i oznacza ponowne zg�oszenie wyj�tku aktualnie obs�ugiwanego w bloku catch. Wywo�anie throw bez parametru w chwili gdy �aden wyj�tek nie jest obs�ugiwany powoduje (domyœlnie) zako�czenie programu.
Instrukcja throw najcz�œciej wyst�puje z parametrem:
throw wrn;
gdzie wrn mo�e by� dowolnym wyra�eniem traktowanym przez kompilator tak, jak wyra�enia b�d�ce argumentem wywo�ania funkcji lub instrukcji return, np. throw 10; throw "abc"; throw obiekt; przy czym obiekt jest wyst�pieniem wczeœniej zdefiniowanej klasy.
Typ obiektu b�d�cego wynikiem obliczenia wyra�enia wrn okreœla rodzaj wyj�tku, zaœ sam obiekt jest przekazywany do tego bloku catch, kt�ry wyst�puje za ostatnio napotkanym blokiem try. Je�eli zg�oszony wyj�tek nie jest obs�ugiwany przez dan� procedur� w bloku catch, to jest on przekazywany do nast�pnej. Je�eli dla danego wyj�tku nie zosta�a znaleziona procedura jego obs�ugi, to wykonanie programu zostanie zako�czone. W procesie zako�czenia programu wywo�ywana jest w�wczas funkcja terminate(), kt�ra z kolei wywo�uje funkcj� abort().
Przyk�ad 10.1.
#include <iostream.h>
#include <excpt.h>
int main() {
char znak = *\0*;
while (znak != ***) {
try
{
cout << *Znak ***- koniec. *
<< *Podaj dowolny znak: *;
cin >> znak;
switch (znak) {
case *a*: throw 1;
case *b*: throw *tekst*;
case *c*: throw 2.0;
default : throw *x*;
} //Koniec switch
} // Koniec try
catch(int) { cout << *Przypadek 1\n*; }
catch(char*) { cout << *Przypadek 2\n*; }
catch(double) { cout << *Przypadek 3\n*; }
catch(...) {
cout << *Wymagana kolejna procedura catch! *;
return 1;
} // Koniec catch(...)
} // Koniec while
return 0;
}
Przyk�adowy wydruk z programu ma posta�:
Znak '*' - koniec. Podaj dowolny znak: a
Przypadek 1
Znak '*' - koniec. Podaj dowolny znak: c
Przypadek 3
Znak '*' - koniec. Podaj dowolny znak: *
Wymagana kolejna procedura catch!
Dyskusja. W powy�szym programie wyj�tki s� zg�aszane w bloku try, umieszczonym w funkcji main(). Plik nag��wkowy <excpt.h> zawiera niezb�dne deklaracje, pozwalaj�ce na dost�p do mechanizmu obs�ugi wyj�tk�w. Procedury obs�ugi przechwytuj� wyj�tki typu int, char* i double. Blok oznaczony catch(...) {} obs�uguje wszystkie nieobs�u�one wyj�tki dowolnego typu. W sekwencji
try{ }catch() { } ... catch() { }
blok catch(...) { }, je�eli wyst�puje, musi by� umieszczony jako ostatni.
•
Sekwencj� try-catch mo�na przenieœ� do oddzielnej funkcji, wywo�ywanej nast�pnie z bloku funkcji main(), jak pokazano w kolejnym przyk�adzie.
Przyk�ad 10.2.
#include <iostream.h>
#include <excpt.h>
void fun() {
char znak = \0;
while (znak != *) {
try {
cout << Znak *- koniec. ;
cout << Podaj dowolny znak: ;
cin >> znak;
switch (znak) {
case a: throw 1;
case b: throw tekst;
case c: throw 2.0;
default : throw x;
} //Koniec switch
} // Koniec try
catch(int) { cout << Przypadek 1\n; }
catch(char*) { cout << Przypadek 2\n; }
catch(double) { cout << Przypadek 3\n; }
catch(...)
{ cout << Wymagana kolejna procedura catch!\n; }
} // Koniec while
}// Koniec fun
int main() {
fun();
return 0;
}
Je�eli wprowadzimy t� sam� sekwencj� znak�w co poprzednio, to otrzymamy identyczny obraz interakcji u�ytkownika z programem.
10.2. Wyjtek jako obiekt
W praktyce programy mog� zawiera� wiele mo�liwych b��d�w w fazie wykonania. B��dy takie mog� by� odwzorowane na wyj�tki o rozr�nialnych nazwach. Ponadto wskazane jest, aby w przypadku wyst�pienia b��du zg�oszony wyj�tek zawiera� maksimum informacji o przyczynie b��du. Je�eli typ zg�aszanego instrukcj� throw wyj�tku jest typem wbudowanym, to mo�liwoœci s� stosunkowo niewielkie. Rozwi�zaniem jest zdefiniowanie klasy wyj�tk�w z odpowiednim publicznym interfejsem i traktowanie wyj�tku jako obiektu. Ilustruje to poni�szy przyk�ad.
Przyk�ad 10.3.
#include <iostream.h>
class Liczba {
public:
class Zakres { };
Liczba(int);
};
Liczba::Liczba(int i)
{ if (i > 10) throw Zakres(); }
int main() {
int x;
char znak;
try {
cout << Podaj liczbe typu int: ;
cin >> x;
Liczba num(x);
} //Koniec try
catch (Liczba::Zakres)
{
cout << endl << Przechwycony wyjatek!\n;
}; // Koniec catch
cout << Kontynuacja programu.\n
<< Wcisnij klawisz litery lub cyfry: ;
cin >> znak;
return 0;
}
Przyk�adowa interakcja z u�ytkownikiem:
Podaj liczbe typu int: 19
Przechwycony wyjatek!
Kontynuacja programu.
Wcisnij klawisz litery lub cyfry: a
Mo�liwoœ� traktowania wyj�tku jako obiektu typu zdefiniowanego przez u�ytkownika prowadzi do koncepcji hierarchii wyj�tk�w, w kt�rej pewne wyj�tki mog� by� typami pochodnymi od wyj�tk�w og�lniejszych. Np. dla biblioteki matematycznej mo�na zdefiniowa� klas� bazow� B��dMat i klasy od niej pochodne Nadmiar, Niedomiar, DzielZero. W takich przypadkach istotna jest kolejnoœ�, w jakiej wyst�puj� bloki catch. Wiadomo, �e pr�by przechwycenia wyj�tk�w zg�aszanych z bloku try odbywaj� si� w takiej kolejnoœci, w jakiej wyst�puj� kolejne bloki catch. Zatem procedur� obs�ugi dla klasy bazowej nale�y umieszcza� jako ostatni� (albo przedostatni�, je�eli wyst�puje catch(...){}); w przeciwnym przypadku procedura dla klasy pochodnej nie zosta�aby nigdy wywo�ana. Zwr��my jeszcze uwag� na nast�puj�cy moment. Je�eli weŸmiemy ci�g deklaracji:
class WyjOg�lny {
public:
virtual void ff() { /* instrukcje */ }
};
class WyjSzczeg�lny: public WyjOgolny {
public:
void ff() { /* instrukcje */ }
};
void funkcja()
{
try
{
// Wywo�anie funkcji, kt�ra zg�asza
// wyj�tek typu WyjSzczeg�lny
}
catch(WyjOg�lny wo) { wo.ff(); }
}
to w tym przypadku zostanie wykonana funkcja WyjOg�lny::ff(), pomimo �e zg�oszony wyj�tek by� typu WyjSzczeg�lny, a funkcja ff() jest funkcj� wirtualn�. Wynika to st�d, �e obiekt typu WyjSzczeg�lny jest przekazywany przez wartoœ� (za pomoc� konstruktora kopiuj�cego klasy WyjSzczeg�lny) jako parametr aktualny procedury catch(). Poniewa� parametrem formalnym jest obiekt typu WyjOg�lny, to obiekt przes�any z bloku try zostanie “obci�ty na wymiar wo”. W obiekcie wo b�dzie wi�c dost�pny jedynie wskaŸnik do funkcji wirtualnej ff() klasy WyjOg�lny. Mo�na temu zapobiec, stosuj�c wskaŸniki lub referencje, np.
catch(WyjOg�lny& wo) { wo.ff(); }
10.3. Sygnalizacja wyjtk�w w deklaracji funkcji
Konstrukcje throw-try-catch zwykle wyst�puj� w bloku oddzielnej funkcji, wywo�ywanej w ciele innej funkcji. Interakcj� takiej funkcji z innymi funkcjami mo�na uczyni� bardziej czyteln�, podaj�c jawnie w jej nag��wku mo�liwe do zg�oszenia wyj�tki, np.
void ff(int i) throw(A, B);
Powy�sza deklaracja m�wi, �e funkcja ff mo�e zg�osi� wyj�tki tylko dw�ch podanych typ�w. Taki spos�b deklarowania stosuje si� r�wnie�, gdy podajemy definicj� funkcji, a nie tylko jej prototyp. W obu przypadkach w bloku funkcji mog� (ale nie musz�) wyst�pi� odpowiednie instrukcje throw lub wywo�ania funkcji generuj�cych wyj�tki z bloku try.
Gdyby z bloku tej funkcji zosta� zg�oszony wyj�tek r�ny od A lub B, to funkcja nie b�dzie w stanie obs�u�y� wyj�tku samodzielnie, ani te� przekaza� go do funkcji wo�aj�cej. Po wyst�pieniu takiego nieoczekiwanego wyj�tku zostanie automatycznie wywo�ana funkcja void unexpected(). Funkcja ta wywo�uje opisan� uprzednio funkcj� terminate(), kt�ra z kolei wywo�uje abort() i ko�czy program. Jednak wywo�aniem domyœlnym dla funkcji unexpected() jest wywo�anie funkcji, zdefiniowanej przez u�ytkownika, a “rejestrowanej” jako argument funkcji set_unexpected(). Funkcja ta jest wprowadzona w pliku nag��wkowym <except.h> deklaracjami:
typedef void (*PFV)();
PFV set_unexpected(PFV);
Stwarza ona u�ytkownikowi pewn� mo�liwoœ� wp�ywania na obs�ug� wyj�tku nieoczekiwanego. Jak wida� z deklaracji, wskaŸnik PFV do bezargumentowej funkcji typu void jest typem zwracanym przez funkcj� set_unexpected(), tj. typem funkcji, kt�ra by�a parametrem aktualnym w ostatnim wywo�aniu funkcji set_unexpected().
W deklaracji (definicji) funkcji mo�na jej “zakaza�” zg�aszania wyj�tk�w, dodaj�c w jej nag��wku throw(), np.
void ff(int i) throw();
Je�eli, mimo zakazu, powy�sza funkcja zg�osi wyj�tek, to musi on zosta� przechwycony i obs�u�ony w jej bloku. W przeciwnym przypadku zostanie wywo�ana funkcja unexpected() i dalszy bieg zdarze� b�dzie analogiczny, jak w poprzednim przypadku.
Przyk�ad 10.4.
#include <iostream.h>
#include <excpt.h>
class Nowa { };
Nowa obiekt;
void f3(void) throw (Nowa)
{
cout << Wywolana f3() << endl;
throw(obiekt);
}
void f2(void) throw()
{
try {
cout << Wywolana f2() << endl;
f3();
}
catch ( ... )
{
cout << Przechwycony wyjatek w f2()! << endl;
}
}
int main() {
try {
f2();
return 0;
}
catch ( ... ) {
cout << Potrzebna kolejna procedura catch! ;
return 1;
}
}
Wydruk z programu b�dzie mia� posta�:
Wywolana f2()
Wywolana f3()
Przechwycony wyjatek w f2()!
Dyskusja. W przyk�adzie pokazano wp�yw specyfikacji wyj�tk�w w nag��wku funkcji na dzia�anie programu. Zdefiniowano w nim klas� wyj�tk�w Nowa i jej wyst�pienie o nazwie obiekt. Prototyp funkcji f3()
void f3(void) throw (Nowa);
m�wi, �e jedynymi wyj�tkami, kt�re mo�e ona zg�asza�, s� obiekty klasy Nowa. Natomiast funkcja f2(), co wynika z postaci jej prototypu
void f2(void) throw();
nie powinna zg�asza� �adnych wyj�tk�w. Jednak z jej bloku jest wywo�ywana funkcja f3(), kt�ra mo�e i zg�asza wyj�tek. Tak wi�c wykonanie programu po wywo�aniu f2() z bloku main() nie ko�czy si� wykonaniem instrukcji return 0; lecz return 1; po przechwyceniu wyj�tku zg�oszonego z bloku funkcji f3().
10.4. Propagacja wyjtk�w
Funkcje, wywo�ywane w bloku try, mog� r�wnie� zawiera� bloki try; pozwala to tworzy� hierarchie obs�ugi wyj�tk�w.
Je�eli funkcja zg�aszaj�ca wyj�tek jest wywo�ywana z bloku innej, nadrz�dnej funkcji, to proces obs�ugi wyj�tku mo�e przebiega� w spos�b, zilustrowany rysunkiem 10-1. Schemat wywo�a� jest tutaj nast�puj�cy: z bloku funkcji A zosta�a wywo�ana funkcja B, z jej bloku zosta�a wywo�ana funkcja C, a z jej bloku funkcja D, kt�ra zg�osi�a wyj�tek.
Rys. 10-1 Obs�uga wyj�tku przy zagnie�d�onych wywo�aniach funkcji
W chwili zg�oszenia wyj�tku zamykany jest blok funkcji D, to znaczy usuwany jest ze stosu jego rekord aktywacyjny i usuwane s� wszystkie zmienne lokalne (automatyczne) utworzone w tym bloku. Je�eli w bloku D istnieje odpowiednia procedura obs�ugi zg�oszonego wyj�tku, to sterowanie zostanie przekazane do tej procedury. Za��my, �e tak nie jest, i �e odpowiedni blok catch znajduje si� w funkcji nadrz�dnej B. Wobec tego, po zako�czeniu bloku D, zostan� zako�czone w taki sam spos�b bloki C i B, po czym sterowanie zostanie przekazane do procedury obs�ugi wyj�tku z bloku B. Po zako�czeniu obs�ugi zostanie wznowione wykonanie bloku funkcji A od nast�pnej po wywo�aniu funkcji B instrukcji.
Gdyby w �adnym bloku z �a�cucha wywo�a� nie zosta� znaleziony odpowiedni blok catch, to zosta�yby zako�czone wszystkie bloki i sterowanie zosta�oby przekazane do funkcji terminate(). Standardowo funkcja ta powoduje zako�czenie programu. Dok�adniej m�wi�c, funkcja void terminate() wykonuje ostatni� funkcj�, przekazan� jako parametr aktualny (wskaŸnik) do funkcji set_terminate(), wprowadzonej deklaracjami:
typedef void (*PFV) ();
PFV set_terminate(PFV);
Jak wida� z deklaracji, wskaŸnik PFV do bezargumentowej funkcji typu void jest i argumentem, i typem zwracanym przez funkcj� set_terminate().
•
Podany ni�ej przyk�ad ilustruje opisany mechanizm.
Przyk�ad 10.5.
//Propagacja wyjatkow
#include <iostream.h>
class Nowa { };//Deklaracja wyjatku
Nowa obiekt;
void B() throw();
void C() throw(Nowa); void D() throw (Nowa);
void A() throw() {
try { cout << Blok try funkcji A()\n;
B(); }
catch(...) { cout << catch() w A(); }
cout << Kontynuacja A()\n;
}
void B() throw() {
try { cout << Blok try funkcji B()\n;
C();
}
catch(Nowa)
{ cout << Przechwycony wyjatek z D()!\n; }
cout << Zamykany blok B()\n;
}
void C() throw(Nowa) {
try {
cout << Blok try funkcji C()\n;
D();
}
catch(int) { cout << catch w C()\n; throw; }
cout << Kontynuacja C()\n;
}
void D() throw (Nowa) {
try {
cout << Blok try funkcji D()\n;
throw(obiekt);
}
catch(int) { cout << catch w D()\n; }
cout << Kontynuacja D()\n;
}
int main() {
try {
cout << Wywolana A()\n;
A();
cout << Po A()\n;
return 0;
}
catch(...) { cout<<Potrzebny kolejny blok catch\n; }
cout << Kontynuacja main()\n;
return 0;
}
Wydruk z programu ma posta�:
Blok try funkcji A()
Blok try funkcji B()
Blok try funkcji C()
Blok try funkcji D()
Przechwycony wyjatek z D()!
Zamykany blok B()
Kontynuacja A()
Po A()
10.5. Wyjtki i zasoby systemowe
Mo�liwoœ� wyst�pienia wyj�tk�w wymaga starannej uwagi programisty, poniewa� burzy ona liniowy przebieg wykonania programu. Je�eli np. funkcja rezerwuje pewne zasoby (otwiera plik, przydziela pami�� z kopca, itp.), to powinna je w odpowiedni spos�b zwolni�, gdy� w przeciwnym przypadku mo�e to spowodowa� nieoczekiwany przebieg wykonania programu. Zazwyczaj funkcja zwalnia zasoby przy wyjœciu ze swojego bloku, tu� przed przekazaniem sterowania do funkcji wo�aj�cej. Jednak�e odnosi si� to jedynie do zmiennych lokalnych; je�eli funkcja operuje na zmiennych globalnych, to nie mamy takiej gwarancji. WeŸmy dla przyk�adu sekwencj� instrukcji:
ifs.open(we.doc);
fun(ifs);
ifs.close();
Je�eli ifs jest zmienn� globaln� (obiektem) klasy ifstream, to funkcja fun (lub funkcja przez ni� wywo�ywana) mo�e zg�osi� wyj�tek i instrukcja ifs.close(); nie zostanie nigdy wykonana!
Opanowanie takiej sytuacji jest technicznie mo�liwe przez przechwycenie dowolnego z mo�liwych wyj�tk�w, zamkni�cie pliku i ponowne zg�oszenie przechwyconego wyj�tku:
ifs.open(we.doc);
try {
fun(ifs); }
catch(...) {
ifs.close();
throw; }
ifs.close();
Jednak stosowanie takiej strategii by�oby nadzwyczaj k�opotliwe. Zamiast takiego podejœcia nale�y wykorzysta� fakt, �e w C++ przy wyjœciu z funkcji nast�puje automatyczne wywo�anie destruktor�w dla wszystkich obiekt�w lokalnych. Dotyczy to r�wnie� tych obiekt�w, dla kt�rych zosta�y zarezerwowane zasoby w funkcjach, wywo�ywanych przez funkcj� fun. W pokazanym wy�ej przypadku wystarczy zadeklarowa� ifs jako obiekt lokalny klasy ifstream:
ifstream ifs(we.doc);
fun(ifs);
poniewa� destruktor klasy bibliotecznej ifstream automatycznie zamknie plik we.doc w momencie, gdy wykonanie dojdzie do ko�ca otaczaj�cego bloku, lub gdy wyj�tek jest obs�ugiwany w bloku zewn�trznym.
Powy�sze uwagi odnosz� si� w r�wnym stopniu do alokacji pami�ci.
Przyk�ad 10.6.
#include <iostream.h>
#include <fstream.h>
class GetMemory {
public:
int* wskmem;
GetMemory(int m) { wskmem = new int[m];}
~GetMemory() { delete[] wskmem;}
};
class MojaKlasa {
public:
class Rozmiar { };
MojaKlasa(const char* filename, int sizemem);
};
MojaKlasa::MojaKlasa(const char* filename, int sizemem)
{
ofstream os(filename);
if (sizemem < 0 || 30 < sizemem) throw Rozmiar();
GetMemory obiekt1(sizemem);
cout << Przydzielona zadana pamiec
<< sizemem << bajtow
<< i otwarty plik\n;
}
int main() {
int x;
try
{
cout << Podaj rozmiar pamieci w bajtach: ;
cin >> x;
MojaKlasa obiekt2(zasob.txt,x);
}
catch ( MojaKlasa::Rozmiar )
{
cout << Niepoprawny rozmiar zadanej pamieci \n;
}
return 0;
}
Dyskusja. Dwukrotne uruchomienie programu mo�e da� nast�puj�ce wydruki:
Podaj rozmiar pamieci w bajtach: 25
Przydzielona pamiec 25 bajtow i otwarty plik
Podaj rozmiar pamieci w bajtach: -100
Niepoprawny rozmiar zadanej pamieci
W przyk�adzie pokazano przechwytywanie b��d�w powstaj�cych w konstruktorze obiektu klasy MojaKlasa. Je�eli z klawiatury podamy rozmiar alokowanej pami�ci w granicach od 0 do 30, to wykonanie programu przebiega liniowo: program przydzieli zadan� pami�� i utworzy (otworzy i zamknie) plik zasob.txt o zerowej zawartoœci. W drugim przypadku mamy nast�puj�c� sekwencj� czynnoœci:
utworzenie obiektu os i otwarcie pliku zasob.txt
zg�oszenie wyj�tku typu MojaKlasa::Rozmiar
zamkni�cie pliku zasob.txt przez niejawnie wywo�any destruktor klasy ofstream (destrukcja obiektu os)
przechwycenie wyj�tku przez blok catch
obs�ug� wyj�tku
zako�czenie programu.
10.6. Naduywanie wyjtk�w
Wyj�tki powinny by� wyj�tkowe. To oczywiste stwierdzenie nie zawsze jest respektowane: programiœci doœ� cz�sto ulegaj� pokusie wykorzystania mechanizmu wyj�tk�w do przekazywania sterowania z jednego punktu programu do innego. Takie post�powanie mo�e w najlepszym razie œwiadczy� o z�ym stylu programowania. Wyj�tki powinny by� zarezerwowane dla takich przypadk�w, kt�re nie mog� si� zdarzy� w normalnym przebiegu oblicze� i kt�rych wyst�pienie tworzy sytuacj�, z kt�rej nie ma wyjœcia w aktualnym zasi�gu. Dobrym przyk�adem koniecznoœci u�ycia mechanizmu wyj�tk�w jest wyczerpanie si� pami�ci. Nikt nie mo�e z g�ry przewidzie� kiedy zabraknie pami�ci, a w punkcie detekcji tego faktu rzadko jest mo�liwe zrobi� coœ wi�cej.
Przeciwie�stwem dla tego przypadku mo�e by� wykrycie ko�ca pliku, z kt�rego w�aœnie czytamy dane. Wiadomo, �e w ka�dym pliku dojdziemy do jego ko�ca, a zatem kod dla czytania z pliku musi by� na to przygotowany. To samo dotyczy pobierania danych z kolejki: przed ka�d� pr�b� usuni�cia elementu nale�y si� upewni�, czy kolejka nie jest pusta.
Podany ni�ej przyk�ad ilustruje nadu�ywanie mechanizmu wyj�tk�w do przkazywania sterowania w sytuacjach, w kt�rych ca�kowicie wystarczaj�ce by�oby warunkowe wywo�ywanie funkcji.
Przyk�ad 10.7.
#include <iostream.h>
#include <string.h>
void wykonanie1()
{ cout << Wykonanie a\n; }
void wykonanie2()
{ cout << Wykonanie b\n; }
int main() {
char rozkaz[80];
cout << Napisz znak lub sekwencje znakow.\n;
cout << Zacznij od znakow a i b: ;
while(cin >> rozkaz) {
try
{
if (strcmp(rozkaz, a) == 0) wykonanie1();
else if(strcmp(rozkaz, b) == 0) wykonanie2();
else cout << Nieznany rozkaz:
<< rozkaz << endl;
} // Koniec try
catch (char* komunikat)
{
cout << komunikat << endl;
} // Koniec catch
catch(...) { cout << Koniec\n; }
} // Koniec while
return 0;
}
Wydruk dla przyk�adowego wykonania programu:
Napisz znak lub sekwencje znakow.
Zacznij od znakow 'a' i 'b': a
Wykonanie a
b
Wykonanie b
abc
Nieznany rozkaz: abc