[ Pobierz całość w formacie PDF ]
Dalej nic go już nie obchodzi: kod z bloku catch jest traktowany jako normalne
instrukcje, bowiem sam kompilator uznaje już, że z chwilą rozpoczęcia ich wykonywania
jego praca została wykonana. Wyjątek został przyniesiony i to się liczy.
Tak więc:
Obsługa wyjątku dokonywana przez kompilator polega na jego dostarczeniu go
do odpowiedniego bloku catch przy jednoczesnym odwinięciu stosu.
Teraz już wiemy, na czym polega zastrzeżenie podane na początku. Nie możemy rzucić
następnego wyjątku w chwili, gdy kompilator zajmuje się jeszcze transportem
poprzedniego. Inaczej mówiąc, między wykonaniem instrukcji throw a obsługą wyjątku w
bloku catch nie może wystapić następna instrukcja throw.
Strefy bezwyjątkowe
No dobrze, ale właściwie co z tego? Przecież po rzuceniu jednego wyjątku wszystkim
zajmuje się już kompilator. Jak więc moglibyśmy rzucić kolejny wyjątek, zanim ten
pierwszy dotrze do bloku catch?&
Faktycznie, tak mogłoby się wydawać. W rzeczywistości istnieją aż dwa miejsca, z
których można rzucić drugi wyjątek.
Jeśli chodzi o pierwsze, to pewnie się go domyślasz, jeżeli uważnie czytałeś opis procesu
odwijania stosu i związanego z nim niszczenia obiektów lokalnych. Powiedziałem tam, że
przebiega ono w identyczny sposób, jak normalnie. Pamięć jest zawsze zwalniania, a w
przypadku obiektów klas wywoływane są destruktory.
Bingo! Destruktory są właśnie tymi procedurami, które są wywoływane podczas obsługi
wyjątku dokonywanej przez kompilator. A zatem nie możemy wyrzucać z nich żadnych
wyjątków, ponieważ może zdarzyć, że dany destruktor jest wywoływany podczas
odwijania stosu.
Nie rzucaj wyjątków z destruktorów.
Druga sytuacja jest bardziej specyficzna. Wiemy, że mechanizm wyjątków pozwala na
rzucanie obiektów dowolnego typu. Należą do nich także obiekty klas, które sami sobie
zdefiniujemy. Definiowanie takich specjalnych klas wyjątków to zresztą bardzo pożądana
i rozsądna praktyka. Pomówimy sobie jeszcze o niej.
Jednak niezależnie od tego, jakiego rodzaju obiekty rzucamy, kompilator z każdym
postępuje tak samo. Podczas transportu wyjątku do catch czyni on przynajmniej jedną
kopię obiektu rzucanego. W przypadku typów podstawowych nie jest to żaden problem,
ale dla klas wykorzystywane są normalne sposoby ich kopiowania. Znaczy to, że może
zostać użyty konstruktor kopiujący - nasz własny.
Mamy więc drugie (i na szczęście ostatnie) potencjalne miejsce, skąd można rzucić nowy
wyjątek w trakcie obsługi starego. Pamiętajmy więc o ostrzeżeniu:
Nie rzucajmy nowych wyjątków z konstruktorów kopiujących klas, których obiekty
rzucamy jako wyjątki.
Z tych dwóch miejsc (wszystkie destruktory i konstruktory kopiujące obiektów
rzucanych) nie powinniśmy rzucać żadnych wyjątków. W przeciwnym wypadku
kompilator uzna to za bardzo poważny błąd. Zaraz się przekonamy, jak poważny&
Zaawansowane C++
486
Biblioteka Standardowa udostępnia prostą funkcję uncaught_exception(). Zwraca ona
true, jeżeli kompilator jest w trakcie obsługi wyjątku. Można jej użyć, jeśli koniecznie
musimy rzucić wyjątek w destruktorze; oczywiście powinniśmy to zrobić tylko wtedy, gdy
funkcja zwróci false.
Prototyp tej funkcji znajduje się w pliku nagłówkowym exception w przestrzeni nazw std.
Skutki wypadku
Co się stanie, jeżeli zignorujemy któryś z zakazów podanych wyżej i rzucimy nowy
wyjątek w trakcie obsługi innego?&
Będzie to wtedy bardzo poważna sytuacja. Oznaczać ona będzie, że kompilator nie jest w
stanie poprawnie przeprowadzić obsługi wyjątku. Inaczej mówiąc, mechanizm
wyjątków zawiedzie - tyle że będzie to rzecz jasna nasza wina.
Co może wówczas zrobić kompilator? Niewiele. Jedyne, co wtedy czyni, to wywołanie
funkcji terminate(). Skutkiem jest więc nieprzewidziane zakończenie programu.
Naturalnie, zmiana funkcji terminate() (poprzez set_terminate()) sprawi, że zamiast
domyślnej będzie wywoływana nasza procedura. Pisząc ją powinniśmy pamiętać, że
funkcja terminate() jest wywoływana w dwóch sytuacjach:
gdy wyjątek nie został złapany przez żaden blok catch
gdy został rzucony nowy wyjątek w trakcie obsługi poprzedniego
Obie są sytuacjami krytycznymi. Zatem niezależnie od tego, jakie dodatkowe akcje
będziemy podejmować w naszej funkcji, zawsze musimy na koniec zamknąć nasz
program. W aplikacjach konsolowych można uczynić to poprzez exit().
Zarządzanie zasobami w obliczu wyjątków
Napisałem wcześniej, że transport rzuconego wyjątku do bloku catch powoduje
zniszczenie wszystkich obiektów lokalnych znajdujących się po drodze . Nie musimy się
o to martwić; zresztą, nie troszczyliśmy się o nie także i wtedy, gdy nie korzystaliśmy z
wyjątków.
Obiekty lokalne nie są jednak jedynymi z jakich korzystamy w C++. Wiemy też, że
możliwe jest dynamiczne tworzenie obiektów na stercie, czyli w rezerwuarze pamięci.
Dokonujemy tego poprzez new.
Pamięć jest z kolei jednym z tak zwanych zasobów (ang. resources), czyli zewnętrznych
bogactw naturalnych komputera. Możemy do nich zaliczyć nie tylko pamięć operacyjną,
ale np. otwarte pliki dyskowe, wyłączność na wykorzystanie pewnych urządzeń lub
aktywne połączenia internetowe. Właściwe korzystanie z takich zasobów jest jednym z
zadań każdego poważnego programu.
Zazwyczaj odbywa się ono według prostego schematu:
najpierw pozyskujemy żądany zasób w jakiś sposób (np. alokujemy pamięć
poprzez new)
[ Pobierz całość w formacie PDF ]