Sytuacja wyjątkowa może zdarzyć się podczas wykonywania konstruktora lub destruktora. Taka sytuacja jest szczególnie trudna do właściwej obsługi. Powiedzmy zatem o kilku sprawach, o których trzeba wtedy pamiętać.
Jeśli wyjątek został zgłoszony podczas konstrukcji obiektu, to
obiekt ten nie powstanie, jego destruktor nie zostanie wywołany, a
wszystkie do tej pory utworzone składowe zostaną usunięte. Mogą
to być już utworzone składowe obiektowe: dla nich destruktory
zostaną wywołane. Oczywiście powstanie kłopot, jeśli są
w klasie składowe wskaźnikowe, a same obiekty, na które one
wskazują, zostały w konstruktorze zaalokowane na stercie lub odnoszą
się do zasobów systemowych, jak np. plików. Tego typu obiekty są
zwykle usuwane (zwalniane) w destruktorze, ale on nie zadziała. W ten
sposób, w razie wystąpienia sytuacji wyjątkowej, nieudany obiekt
zostanie co prawda usunięty, ale zasoby (pamięć, otwarte pliki) nie
zostaną zwolnione. Można temu zaradzić „opakowując” tego rodzaju
składowe wskaźnikowe tak, aby uczynić z nich obiekty, dla których
w razie niepowodzenia wywołany zostanie destruktor zwalniający zasoby.
Rozpatrzmy przykład:
1. #include <iostream> 2. #include <cstring> 3. #include <cstdio> // FILE, fopen, fclose 4. using namespace std; 5. 6. class A { 7. struct nazw { 8. char* n; 9. nazw(const char* n) 10. : n(strcpy(new char[strlen(n)+1],n)) 11. { } 12. ~nazw() { 13. cerr << "dtor nazw: " << n << endl; 14. delete [] n; 15. } 16. }; 17. 18. nazw Nazwisko; 19. FILE* plik; 20. public: 21. A(const char* n, const char* p) 22. : Nazwisko(n) 23. { 24. plik = fopen(p,"r"); 25. // ... 26. // throw 1; 27. // ... 28. } 29. 30. // inne pola i metody 31. 32. ~A() { 33. cerr << "dtor A" << endl; 34. if (plik) fclose(plik); 35. } 36. }; 37. 38. int main() { 39. try { 40. A a("Kowalski","zasob.cpp"); 41. } catch(...) { 42. cerr << "Nie udalo sie skonstruowac obiektu\n"; 43. } 44. }
Klasa A zawiera składową opisującą nazwisko w postaci C-napisu. Sam napis alokowany jest dynamicznie, ale wskaźnik do niego nie jest bezpośrednio składową klasy A. Zamiast tego składową tej klasy jest obiekt pomocniczej, „opakowującej” struktury nazw, który dopiero zawiera, jako swoją składową n, wskaźnik do napisu. Ta pomocnicza struktura definiuje destruktor usuwający napis ze sterty.
Prócz nazwiska, klasa A zawiera pole wskaźnikowe wskazujące obiekt typu FILE (jest to standardowy typ w czystym C opisujący pliki).
Załóżmy, że linia 26 (throw 1) jest wykomentowana. Konstruktor klasy A inicjuje składowe opisujące nazwisko i kończy się prawidłowo. Żaden wyjątek nie został zgłoszony. Po wyjściu sterowania z ciała bloku try obiekt klasy A, jako obiekt lokalny dla tego bloku, jest usuwany i wywoływany jest jego destruktor zamykający plik. Następnie usuwane są obiekty składowe i wywoływane są ich destruktory, a więc w naszym przypadku usunięty będzie obiekt Nazwisko, a w jego destruktorze zwolniona zostanie pamięć na nazwisko. Wydruk programu
dtor A dtor nazw: Kowalskiświadczy o tym, że obiekt a został prawidłowo usunięty.
Spróbujmy teraz uaktywnić linię 26, która powoduje powstanie sytuacji wyjątkowej w trakcie wykonywania konstruktora. Teraz wydruk z programu to
dtor nazw: Kowalski Nie udalo sie skonstruowac obiektuPo powstaniu wyjątku destruktor klasy A dla powstającego obiektu nie został wywołany. Tak więc plik, choć już otwarty, nie został zamknięty — przepadł tylko wskaźnik do niego. Natomiast napis zawierający nazwisko został prawidłowo usunięty! Stało się tak, bo powstanie wyjątku spowodowało wywołanie destruktorów dla już utworzonych składowych obiektowych, a więc dla składowej Nazwisko.
W przykładzie powyższym nie wyłapywaliśmy wyjątku powstającego podczas konstruowania obiektu w samym konstruktorze, ale pozwoliliśmy mu wyjść poza konstruktor, gdzie był przechwytywany w funkcji main. Inna jest sytuacja z wyjątkami, jakie mogą powstać w trakcie wykonania destruktora. Problem polega na tym, że destruktor, jak mówiliśmy, może zostać wywołany podczas zwijania stosu w poszukiwaniu procedury obsługi innego wyjątku. Powstanie dodatkowego nieobsłużonego wyjątku w destruktorze powodowałoby „podwójne” zwijanie stosu. Taka sytuacja nie jest w C++ możliwa; jeśli powstanie, program jest natychmiast kończony za pomocą funkcji terminate. Tak więc, jeśli jakikolwiek wyjątek może być zgłoszony podczas wykonywania destruktora, to należy go obsłużyć — przechwycić odpowiednią frazą catch — wewnątrz tego destruktora, nie dopuszczając do jego „ucieczki”.
T.R. Werner, 23 lutego 2022; 19:40