Obsługa wyjątków
Sposoby reagowania na błędne sytuacje:
komunikat i przerwanie działania programu,
kod błędu jako wynik,
globalna zmienna z kodem błędu,
parametr - funkcja obsługi błędów.
Bardzo często zdarza się, że pisząc jakąś operację (funkcję), zauważamy, że ta operacja nie zawsze musi się dać poprawnie wykonać. Nasza funkcja powinna jakoś zareagować w takiej sytuacji, kłopot polega na tym, że nie wiemy jak. Może:
Wypisać komunikat i przerwać działanie całego programu.
Bardzo brutalne.
Przekazać wartość oznaczającą błąd.
Nie zawsze jest wykonalne (może nie być wartości, która nie może być poprawną wartością funkcji). Poza tym zwykle jest bardzo
niewygodne, bo wymaga sprawdzania wartości funkcji po każdym jej wywołaniu. Program konsekwentnie wykonujący takie sprawdzenia
staje zupełnie nieczytelny, jeśli zaś sprawdzanie nie jest konsekwentnie stosowane to jest warte tyle samo, co gdyby go w ogóle
nie było.
Przekazać jakąś poprawną wartość, natomiast ustawić jakąś zmienną (zmienne) w programie sygnalizujące zaistnienie błędnej sytuacji.
To już jest zupełnie złe rozwiązanie: tak samo jak poprzednie wymaga ciągłego sprawdzania czy nie nastąpił błąd, jest też bardzo
prawdopodobne, że używający takiej operacji w ogóle nie będzie świadom tego, że błędy w ogóle są sygnalizowane.
Wywołać funkcję dostarczoną przez użytkownika (np. jako parametr), obsługującą błędne sytuacje.
Najlepsze z dotąd przedstawionych rozwiązanie. Jego wadą jest to, że każde wywołanie funkcji trzeba obciążyć dodatkowym parametrem.
Celem jest przezwyciężenie problemów z wcześniejszych rozwiązań.
Wyjątek rozumiany jako błąd.
Funkcja zgłasza wyjątek.
Kod obsługi wyjątku może być w zupełnie innym miejscu programu.
Szukanie obsługi wyjątku z ”paleniem mostów”.
Nie ma mechanizmu powrotu z obsługi wyjątku do miejsca jego zgłoszenia.
W celu rozwiązania takich problemów włączono do języka C++ mechanizm obsługi wyjątków. W C++ wyjątek oznacza błąd, zaś obsługa wyjątków oznacza reakcję programu na błędy wykryte podczas działania programu. Idea obsługi wyjątków polega na tym, że funkcja, która napotkała problem, z którym nie potrafi sobie poradzić zgłasza wyjątek. Wyjątek jest przesyłany do miejsca wywołania funkcji. Tam może być wyłapany i obsłużony lub może być przesłany dalej (wyżej). Podczas tego przechodzenia, przy wychodzeniu z funkcji i bloków następuje automatyczne usuwanie automatycznych obiektów stworzonych w tych funkcjach i blokach (to bardzo ważne). W C++ nie możliwości powrotu z obsługi wyjątku, do miejsca jego wystąpienia, w celu ponownego wykonania akcji, która spowodowała błąd.
Uwaga: Mechanizm obsługi wyjątków w innych językach może być zrealizowany zupełnie inaczej (np. wyjątek nie musi być utożsamiany z błędem, może być możliwe wznowienie wykonywania programu w miejscu wystąpienia wyjątku itp.). W szczególności nie ma ogólnej zgody czym powinien być wyjątek.
Składnia instrukcji związanych z obsługą wyjątków:
try{ <instrukcje> } catch (<parametr 1>){ <obsługa wyjątku 1> } // ... catch (<parametr n>){ <obsługa wyjątku n> }
Zgłoszenie wyjątku:
throw <wyrażenie>;
Zgłoszenie wyjątku rozpoczyna wyszukiwanie obsługi
Klauzule catch
są przeglądane w kolejności ich deklaracji
Trzy możliwości
instrukcje nie zgłosiły wyjątku
klauzula wyjątku znaleziona - być może w innym bloku - i wykonana
brak pasującej klauzuli
Semantyka:
Jeśli jakaś z <instrukcji>
zgłosiła wyjątek, to przerywamy wykonywanie tego ciągu instrukcji i szukamy instrukcji catch z odpowiednim parametrem.
Jeśli znajdziemy, to wykonujemy obsługę tego wyjątku.
Klauzule catch
są przeglądane w kolejności ich deklaracji.
Po zakończeniu obsługi wyjątku (o ile obsługa wyjątku jawnie nie spowodowała przejścia do innej części programu) wykonuje się pierwszą
instrukcję stojącą po instrukcjach catch.
Jeśli zaś nie znaleziono obsługi stosownego wyjątku, to następuje przejście do wywołującej funkcji,
połączone z usunięciem obiektów automatycznych i tam znów rozpoczyna się poszukiwanie obsługi wyjątku.
Jeśli takie poszukiwanie nie zakończy się sukcesem, to wykonywanie programu zostanie przerwane.
Przykład:
class Wektor{ int *p; int rozm; public: class Zakres{}; // Wyjątek: wyjście poza zakres class Rozmiar{}; // Wyjątek: zły rozmiar wektora Wektor(int r); int& operator[](int i); // ... }; Wektor::Wektor(int r){ if (r<=0) throw Rozmiar(); // ... } int& Wektor::operator[](int i){ if (0<=i && i < rozm) return p[i]; else throw Zakres(); // Na razie nie przekazujemy wartości błędnego indeksu }
void f(){ try{ // używanie wektorów } catch (Wektor::Zakres){ // obsługa błędu przekroczenia zakresu } catch (Wektor::Rozmiar){ // obsługa błędu polegającego na błędnym podaniu rozmiaru } // Sterowanie do chodzi tutaj, gdy: // a) nie było zgłoszenia wyjątku, lub // b) zgłoszono wyjątek Zakres lub Rozmiar i obsługa tego // wyjątku nie zawierała instrukcji powodujących wyjście // z funkcji f }
Obsługa wyjątków może być podzielona między wiele funkcji:
void f1(){ try{ f2(w); } catch (Wektor::Rozmiar) { /* ... */ } } void f2(Wektor& w){ try{ /* używanie wektora w */ } catch (Wektor::Zakres) { /* ... */ } }
Obsługa wyjątku może zgłosić kolejny.
Wyjątek jest uważany za obsłużony z momentem wejścia do klauzuli obsługującej go.
Można zagnieżdżać wyjątki.
W instrukcjach obsługujących wyjątek może się pojawić instrukcja throw
.
W szczególności może się też pojawić instrukcja throw
zgłaszająca taki sam wyjątek, jak ten właśnie wywoływany.
Nie spowoduje to zapętlenia ani nie będzie błędem.
Z punktu widzenia języka C++ wyjątek jest obsłużony z chwilą wejścia do procedury obsługi wyjątku, zaś wyjątki zgłaszane w procedurach
obsługi są obsługiwane przez funkcje wywołujące blok try
.
Można także zagnieżdżać bloki try
-catch
w instrukcjach catch
(nie wydaje się to jednak celowe).
Wyjątek jest obiektem.
Obiekt może mieć swój stan.
Instrukcja zgłaszająca wyjątek (throw
), zgłasza obiekt.
Taki obiekt może posiadać składowe i dzięki nim przenosić informację z miejsca zgłoszenia do miejsca obsługi.
class Wektor{ // ... public: class Zakres{ public: int indeks; Zakres(int i): indeks(i) {} }; int& operator[] (int i); // ... };
int& Wektor::operator[](int i){ if (0<=i && i<rozm) return p[i]; throw Zakres(i); } // ... void f(Wektor& w){ // ... try{ /* używanie w */ } catch (Wektor::Zakres z){ cerr << "Zły indeks" << z.indeks << "\n"; // ... } }
Klasy wyjątków można łączyć w hierarchie.
Pozwala to specjalizować obsługę wyjątków.
Ponieważ wyjątki są obiektami klas, możemy tworzyć hierarchie klas wyjątków. Co to daje? Możemy, w zależności od sytuacji, pisać wyspecjalizowane procedury obsługi wyjątków, lub jedną obsługującą wszystkie wyspecjalizowane wyjątki:
class BłądMatemat {}; class Nadmiar: public BłądMatemat {}; class Niedomiar: public BłądMatemat {}; class DzielPrzezZero: public BłądMatemat {}; // ... try { /* ... */ } catch (Nadmiar) { /* Obsługa nadmiaru */ } catch (BłądMatemat) { /* Obsługa pozostałych bł. mat. */ }
Wznawianie wyjątku throw
Obsługa dowolnego wyjątku
W procedurze obsługi wyjątku można ponownie zgłosić ten sam wyjątek pisząc throw
bez argumentu (jak już poprzednio zaznaczyliśmy, nie spowoduje to zapętlenia).
Obsługa dowolnego wyjątku
catch (….)
oznacza wyłapywanie dowolnego wyjątku. Można je zastosować razem z throw
:
void f(){ try { /* ... */ } catch (...) { /* Instrukcje, które muszą się zawsze wykonać na koniec procedury f, jeśli nastąpił błąd. */ throw; // Ponowne zgłoszenie złapanego wyjątku }
Częstym problemem związanym z obsługą wyjątków, jest zwalnianie zasobów, które funkcja zdążyła już sobie przydzielić zanim nastąpił błąd. Dzięki wyjątkom możemy bardzo prosto rozwiązać ten problem, obudowując zasoby obiektami (pamiętajmy, że procesowi szukania procedury obsługi błędu towarzyszy usuwanie obiektów lokalnych).
Problem - zwalnianie zasobów.
Pomysł:
opakowanie zasobów obiektami
wykorzystanie mechanizmu wyjątków.
Rozwiązanie 1 (tradycyjne - niewygodne):
void używanie_pliku(const char* np){ FILE* p = fopen(np, "w"); try { /* coś z plikiem p */ } catch (...){ fclose(p); throw; } fclose(p); }
Rozwiązanie 2 (eleganckie i ogólne):
class Wsk_do_pliku{ FILE* p; public: Wsk_do_pliku(const char* n, const char * a) {p = fopen(n,a); } ~Wsk_do_pliku() {fclose(p);} operator FILE*() {return p;} // Jeśli będę potrzebował wskaźnika do struktury FILE }; void używanie_pliku(const char* np){ Wsk_do_pliku p(np, "w"); /* coś z plikiem p */ }
Teraz nasza funkcja daje się już ładnie zapisać, nie ma w niej ani jednej dodatkowej instrukcji, nie ma nawet operacji fclose
!
Zdobywanie zasobów jest inicjacją (RAII - Resource Acquisition Is Initialization)
Bardzo skuteczna technika w C++
W innych językach:
Java: finilize
C#: instrukcja using
Tę technikę nazywa się zwykle ”zdobywanie zasobów jest inicjacją”.
Zastanówmy się teraz nad następującym problemem. Obiekt uważa się za skonstruowany, dopiero po zakończeniu wykonywania jego konstruktora. Dopiero wtedy porządki wykonywane wraz z szukaniem procedury obsługi błędu usuną obiekt z pamięci (i zwolnią zajmowane przez niego zasoby). Pojawia się więc naturalny problem: co ma robić konstruktor, gdy wykryje błąd? Powinien zwrócić te zasoby, które już sobie przydzielił. Stosując powyższą technikę jesteśmy w stanie bardzo łatwo to zagwarantować. Oto przykład, konstruktor przydzielający dwa zasoby: plik i pamięć:
class X{ Wsk_do_pliku p; Wsk_do_pamięci<int> pam; // ... X(const char*x, int y): p(x, "w"), pam(r) { /* inicjacja */ } // ... }; class Za_mało_pamięci{}; template<class T> class Wsk_do_pamięci{ public: T* p; Wsk_do_pamięci(size_t); ~Wsk_do_pamięci() {delete[] p;} operator T*() {return p;} }; template<class T> Wsk_do_pamięci::Wsk_do_pamięci(size_t r){ p = new T[t]; }
Teraz to już implementacja dba o to, by wywołać destruktory dla tych obiektów, które zostały skonstruowane (i tylko dla nich).
Wyjątki są istotną cechą specyfikacji funkcji.
C++ nie wymusza ich specyfikowania.
Jeśli są wyspecyfikowane ich sprawdzenie odbywa się podczas wykonania programu.
Specyfikowanie braku wyjątków i ich niezgłaszania.
Jeśli mamy wyjątki, to interfejs funkcji staje się o nie bogatszy. Język C++ pozwala (nie zmusza) do ich wyspecyfikowania:
void f() throw (x1, x2, x3) { /* treść f() */ }
Jest to równoważne napisaniu:
void f(){ try { /* treść f() */ } catch (x1) { throw; } catch (x2) { throw; } catch (x3) { throw; } catch (...) { unexpected(); } }
Czyli f() może zgłosić wyjątki x1, x2 i x3 oraz pochodne. Domyślnym znaczeniem unexpected() jest zakończenie działania programu. Zwróćmy uwagę, że sprawdzanie, czy zgłoszony wyjątek jest we specyfikacji, następuje dopiero podczas wykonywania programu (a nie podczas kompilacji). Funkcja dla której nie wyspecyfikowano żadnego wyjątku, może zgłosić każdy wyjątek. Jeśli funkcja ma nie zgłaszać żadnych wyjątków, to piszemy:
void g() throw();
Treść automatycznie generowana z plików źródłowych LaTeXa za pomocą oprogramowania wykorzystującego LaTeXML.
strona główna | webmaster | o portalu | pomoc
© Wydział Matematyki, Informatyki i Mechaniki UW, 2009-2010. Niniejsze materiały są udostępnione bezpłatnie na licencji Creative Commons Uznanie autorstwa-Użycie niekomercyjne-Bez utworów zależnych 3.0 Polska.
Projekt współfinansowany przez Unię Europejską w ramach Europejskiego Funduszu Społecznego.
Projekt współfinansowany przez Ministerstwo Nauki i Szkolnictwa Wyższego i przez Uniwersytet Warszawski.