11. Obsługa wyjątków.
Wyjątki - błędy programu. Na ogół są to dość rzadko występujące sytuacje, które często prowadzą do zakończenia programu. Oto typowe sytuacje: niedostateczna pamięć do spełnienia żądania new, wyjście poza zakres indeksów tablicy, otrzymanie zbyt dużej liczby w działaniach arytmetycznych, dzielenie przez zero, błąd w argumentach funkcji.
Często w takich sytuacjach lepiej nie przerywać pracy programu - może nastąpić tzw. "wyciek zasobu" - przerwany program pozostawi zasoby w stanie, w którym inne programy nie będą mogły z nich korzystać.
11.1. Słowa kluczowe: try, throw i catch.
Programista umieszcza w bloku try kod mogący generować kod, który spowoduje zgłoszenie (throw) wyjątku. Po bloku try może występować jeden lub kilka bloków catch, które wychwytują i obsługują odpowiednie typy wyjątków. Jeśli w żadnym z bloków catch nie zostanie znaleziona procedura obsługi danego wyjątku, zostaje wywołana funkcja terminate, która z kolei domyślnie wywołuje funkcję abort.
Rozpatrzmy przykładowy program:
// wyjątki - dzielenie przez zero.
# include <iostream>
class WyjDzielZer
{
const char* komunikat;
public:
WyjDzielZer(): komunikat("proba dzielenia przez zero"){}
const char *jaki() const{return komunikat;}
};
double iloraz(int dzielna, int dzielnik)
{
if(dzielnik==0)
throw WyjDzielZer();
return static_cast<double>(dzielna)/dzielnik;
}
int main()
{
int liczba1, liczba2;
double wynik;
cout <<"Wprowadź dwie liczby calkowite (EOF, by zakonczyc): ";
while( cin>>liczba1>>liczba2)
{
try
{
wynik=iloraz(liczba1,liczba2);
cout << "Iloraz wynosi: " <<wynik <<endl;
}
catch(WyjDzielZer wyj)
{
cout << "Wystapil wyjatek: " << wyj.jaki() << '\n';
}
cout <<"\nWprowadź dwie liczby calkowite (EOF, by zakonczyc): ";
}
cout << endl;
return 0;
}
Program ten daje następujące wyniki:
Wprowadź dwie liczby calkowite (EOF, by zakonczyc): 100 7
Iloraz wynosi: 14.2857
Wprowadź dwie liczby calkowite (EOF, by zakonczyc): 100 0
Wystapil wyjatek: proba dzielenia przez zero
Wprowadź dwie liczby calkowite (EOF, by zakonczyc): 33 9
Iloraz wynosi: 3.66667
Wprowadź dwie liczby calkowite (EOF, by zakonczyc):
Wyjątek nie został rzucony bezpośrednio z bloku try, ale z funkcji iloraz. Błędy mogą wypływać nawet z funkcji głęboko zagnieżdżonych w bloku try.
Blok catch występuje bezpośrednio po bloku try. Ten catch znajduje obiekty typu WyjDzielZer. Odpowiada to typowi obiektu zgłaszanego przez funkcję iloraz. Jeśli blok try nie rzuci wyjątku, wszystkie bloki catch zostaną pominięte.
11.2. Zgłaszanie (rzucanie) wyjątków.
Słowo kluczowe throw na ogół przesyłane jest z jednym argumentem (czasami może argumentów nie mieć). Argument ten może być dowolnego typu, również typu określonego przez użytkownika (wtedy zgłaszany obiekt nazywany "obiektem wyjatku" (exception object).
Przykłady:
throw (float) 1.0;
throw 1;
throw 'd';
throw kot sjam;
W ostatnim przypadku mamy obiekt sjam typu kot.
Wyjątek wychwytuje blok catch nastawiony na przyjęcie odpowiedniego typu i najbliższy do bloku try.
Jeżeli trzeba przekazać informację o błędzie, powinien on się znajdować w zgłaszanym obiekcie. Procedura obsługi catch powinna wtedy zawierać nazwę argumentu, przez którą można się odnieść do tej informacji.
Wyjątki powinny "przechodzić" przez blok try. Jeśli są zgłaszane poza tym blokiem, spowoduje to wywołanie funkcji terminate.
11.3. Wychwytywanie wyjątku.
Każdy blok catch rozpoczyna się od słowa kluczowego catch, po nim typ w nawiasie i opcjonalna nazwa argumentu. Jeśli istnieje argument - można go przetworzyć w bloku catch. Jeśli argumentu nie ma - przekazywane jest tylko sterowanie
Blok catch z wielokropkiem:
catch(...)
oznacza wychwytywanie wszystkich wyjątków. Blok ten należy umieścić na końcu wszystkich bloków catch.
Uwaga! Blok catch wychwytujący obiekty klasy podstawowej, będzie wychwytywał wszystkie wyjątki dla klas pochodnych - więc musi być na końcu. Procedury catch również mogą zgłosić wyjątek.
11.4. Powtórne zgłoszenie wyjątku.
Jeśli procedura obsługi, która wychwyciła wyjątek nie jest w stanie go obsłużyć, albo po prostu chce zwolnić zasoby i obsłużyć coś jeszcze, to może wyjątek odrzucić instrukcją:
throw;
To zgłoszenie bez argumentów zgłasza wyjątek powtórnie. Jeśli wyjątek nie został zgłoszony wcześniej zostaje wywołana funkcja terminate.
Taki powtórnie zgłoszony wyjątek jest przetwarzany przez następny odpowiedni blok catch.
Uwaga często korzystnie jest umieścić jako ostatni w programie blok catch(...), który łapie wszystkie wyjątki i porządkuje program.
Rozpatrzmy przykład
// wyjatek powtornie.
#include <iostream>
#include <stdexcept>
using namespace std;
void zglosWyj() throw(exception)
{
try
{
cout << "Funkcja zglosWyj\n";
throw exception();
}
catch (exception e)
{
cout <<"Wyjatek obsluzony w funkcji zglosWyj\n";
throw;
}
cout << "To także nie powinno zostać wydrukowane\n";
}
int main()
{
try
{
zglosWyj();
cout << "To nie powinno zostac wydrukowane\n";
}
catch (exception e)
{
cout << "Wyjatek obsluzony w main\n";
}
cout << "Program jest kontynuowany w main po bloku catch " <<endl;
return 0;
}
Program powyższy daje następujące wyniki:
Funkcja zglosWyj
Wyjatek obsluzony w funkcji zglosWyj
Wyjatek obsluzony w main
Program jest kontynuowany w main po bloku catch
Program powyższy ilustruje powtórne zgłoszenie wyjątku. W funkcji main, w bloku try wywoływana jest funkcja zglosWyj. W funkcji tej instrukcja throw zgłasza realizację wyjątku exception ze standardowej klasy bibliotecznej (z pliku stdexcept.h). Wyjątek ten jest wychwytywany w bloku catch w tej samej funkcji. Blok drukuje komunikat o błędzie i powtórnie zgłasza wyjątek. Powoduje to zakończenie funkcji zglosWyj i przełączenie sterowania do bloku catch w funkcji main, który powtórnie wychwytuje wyjątek i zgłasza komunikat o błędzie.
11.5 Specyfikacja wyjątków.
Zwróćmy uwagę na nagłówek funkcji zglosWyj z ostatniego programu:
void zglosWyj() throw(exception)
{
}
Wyrażenie w nawiasach po słowie throw to lista wyjątków, które mogą być zwracane przez funkcję. Ogólnie w nawiasie za słowem throw może występować kilka wyjątków, oddzielonych przecinkami. Np.:
int fun(float x) throw a,b,c)
{
// ciało funkcji
}
Jeśli nie ma listy wyjątków, funkcja może zgłosić dowolny wyjątek. Jeśli funkcja zgłasza wyjątek, którego nie ma na liście (ani nie jest pochodnym typu z listy), wówczas wywoływana jest funkcja unexpected.
Funkcja bez specyfikacji wyjątków może zgłosić każdy wyjątek.
11.6. Nieoczekiwane wyjątki.
Domyślnie funkcja unexpected wywołuje funkcję terminate, która z kolei wywołuje funkcje abort.
Funkcja terminate może być wywołana jawnie, jeśli wyjątek nie został wychwycony, lub jeśli podczas obsługi wyjątku został zniszczony stos. Również próba zgłoszenia wyjątku przez destruktor powoduje wywołanie funkcji terminate.
Zamiast abort funkcja terminate może wywołać jakąś inna funkcję, określoną funkcją _ set terminate. podobnie funkcja set_unexpected powoduje, że wywoływana w funkcji unexpected będzie nie funkcja terminate, a jakaś inna.
Prototypy funkcji set_terminate i set_unexpected znajduja się w plikach terminate.h i unexpected.h.
Funkcje set_terminate i set_unexpected przyjmują jako argumenty wskaźniki do funkcji. Każdy z tych wskaźników musi wskazywać na funkcje bez argumentów i zwracające typ void.
11.7 Skracanie (odwikłanie) stosu.
Jeśli zostanie zgłoszony wyjątek, a nie będzie wychwycony w określonym zasięgu, skracany jest stos wywołań funkcji i wyjątek próbuje przechwycić następny blok catch. Skracanie stosu wywołań funkcji oznacza, że funkcja ta kończy się, niszczone są wszystkie zmienne lokalne i sterowanie przechodzi do miejsca, z którego funkcja została wywołana.
Rozpatrzmy przykład:
// Skracanie ("odwiklanie") stosu
#include <iostream>
#include <stdexcept>
using namespace std;
void fun3() throw(runtime_error)
{
throw runtime_error("blad wykonania programu w fun3");
}
void fun2() throw(runtime_error)
{
fun3();
}
void fun1() throw(runtime_error)
{
fun2();
}
int main()
{
try
{
fun1();
}
catch(runtime_error e)
{
cout <<"Wystapil wyjatek: " <<e.what() <<endl;
}
return 0;
}
W powyższym programie, w bloku main wywoływana jest funkcja fun1, która wywołuje fun2, a ta z kolei - fun3. Ostatnia funkcja zwraca wyjątek. Ponieważ throw nie znajduje się w bloku try, funkcja fun3 jest likwidowana. Sterowanie przejmuje fun2, która zostanie zlikwidowana z tych samych powodów. Wyjątek będzie obsłużony w bloku main, gdyż wywołanie fun1 znajduje się w bloku try.
11.7 Wyjątki i dziedziczenie
Różne klasy wyjątków mogą być klasami pochodnymi wspólnej klasy podstawowej.. Jeśli catch wychwytuje wskaźnik lub referencję do obiektu typu klasy podstawowej może to zrobic również dla wskaźnika i referencji wszystkich klas pochodnych. Pozwala to na polimorficzne przetwarzanie powiązanych błędów.
Biblioteka STL posiada całą hierarchie standardowych wyjątków. Podstawową klasą jest tam exception, pochodnymi - klasy runtime_error i logic_error itd.
klasa exception znajduje się w pliku nagłówkowym exception.h
11.8. Po co to wszystko.
Oczywiście, efekty obsługi wyjątków można osiągnąć innymi środkami. Jednak procedury obsługi wyjątków sa elegantsze. Po pierwsze wygodniej jest, jeśli wyjątek zwracają funkcje biblioteczne (autor biblioteki nie wie z góry, co chciał użytkownik biblioteki). Z tych samych przyczyn wyjątki mają zastosowanie przy zespołowym pisaniu programów.
Po drugie aparat obsługi wyjątków oddziela obsługę błędów od głównych zadań programu, co zwiększa czytelność programu.
Po trzecie, łatwiej jest operować wyjątkami niż statusami błędów w takich funkcjach jak konstruktory, czy przeładowane operatory - które właściwie mają bardzo ograniczony zakres wartości zwracanych.
Po czwarte, możliwość zwracania całych obiektów, pozwala nam na dostarczenie pełniejszej informacji o błędach, które wystąpiły.
I wreszcie po piąte - mechanizm wyjątków pozwala nam przekazywać sterowanie na dużą odległość (taka ulepszona instrukcja goto). Nie jest to rozwiązanie eleganckie, ale wyobraźmy sobie, że trzeba wyjść z wielokrotnie zagnieżdżonej pętli, w której tkwią inne funkcje z pętlami.
9
Piotr Staniewski
C++ Studia Zaoczne. Wykład 8