programowanieProgramista Backend C++

Opisz różnice między stack unwinding a destrukcją obiektów podczas obsługi wyjątków w C++. Jak zapewnić prawidłowe zwalnianie zasobów w przypadku wystąpienia wyjątku?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

W C++ obsługa wyjątków wiąże się z mechanizmem stack unwinding — automatycznym destruktorowaniem lokalnych obiektów na stosie wywołań, które zostały utworzone przed wystąpieniem wyjątku. To zjawisko jest ważne dla prawidłowego zwalniania zasobów.

Historia pytania

Początkowo, podczas projektowania C++, brakowało wbudowanego garbage collectora, więc zadanie zwalniania zasobów (pamięci, plików, gniazd) spadło na programistę. Obsługa wyjątków miała zapewnić poprawne zwalnianie w przypadku wycofywania kodu z powodu błędu.

Problem

Bez mechanizmu automatycznej destrukcji obiektów w przypadku wyjątków występuje wyciek zasobów. Jeśli nie zwalniać zasobów ręcznie w każdym bloku catch, kod staje się skomplikowany i niepewny.

Rozwiązanie

C++ implementuje stack unwinding, gdy przy rzucaniu wyjątku wszystkie obiekty umieszczone na stosie w zakresie widoczności przed throw są niszczone w odwrotnej kolejności do ich utworzenia. Ich destruktory są wywoływane automatycznie.

Klasycznym sposobem na ciągłe zwalnianie zasobów jest stosowanie wzorca RAII (Resource Acquisition Is Initialization): wszystkie zasoby są "owinięte" w obiekty, które przy destrukcji zwalniają posiadany zasób.

Przykład kodu:

#include <iostream> #include <stdexcept> struct FileHandle { FILE* file; FileHandle(const char* path) { file = fopen(path, "r"); if (!file) throw std::runtime_error("Nie można otworzyć pliku"); } ~FileHandle() { if (file) fclose(file); } }; void processFile(const char* path) { FileHandle fh(path); // Wycofuje, jeśli wystąpi throw // ... operacje na pliku throw std::runtime_error("Jakiś błąd"); // fclose wywoła się automatycznie }

Kluczowe cechy:

  • Stack unwinding automatycznie wywołuje destruktory wszystkich obiektów na stosie.
  • RAII zapewnia zwalnianie zasobów niezależnie od wystąpienia wyjątku.
  • Ręczne zwalnianie zasobów w catch rzadko jest potrzebne: destruktory robią to lepiej.

Pytania z podstępem.

Dlaczego nie można po prostu użyć try-catch i ręcznie wywołać delete lub fclose w bloku catch, aby uniknąć wycieków?

Odpowiedź:

Jest to niewygodne i niepewne: łatwo zapomnieć zamknąć zasób, szczególnie przy wielu punktach wyjścia lub zagnieżdżonych zasobach. Destruktory są wywoływane nawet, gdy wyjątek "przelatuje" przez funkcję, catch do tego nie jest potrzebny.

Czy obiekty w stercie (heap) zostaną zniszczone, jeśli nie są "owinięte" w obiekty na stosie podczas stack unwinding?

Odpowiedź:

Nie. Stack unwinding wywołuje destruktory tylko dla obiektów umieszczonych na stosie. Aby obiekty heap były poprawnie niszczone, muszą być zarządzane przez obiekt na stosie (na przykład przez wskaźniki inteligentne).

Co się stanie, jeśli destruktor podczas stack unwinding sam rzuca wyjątek?

Odpowiedź:

Jeśli w trakcie stack unwinding podczas niszczenia jakiegoś obiektu zostanie rzucony drugi wyjątek, program zakończy się wywołaniem std::terminate(). Nigdy nie rzucaj wyjątków z destruktorów!

Typowe błędy i antywzorce

  • Nie zwalniać zasobów w destruktorach, polegając tylko na ręcznym zarządzaniu w catch.
  • Rzucać wyjątki z destruktorów.
  • Nie używać inteligentnych wskaźników do zarządzania zasobami heap.
  • Zapominać o zagnieżdżonych zasobach (na przykład plik w strukturze).

Przykład z życia

Negatywny przypadek

Programista ręcznie zamyka pliki i zwalnia pamięć przez bloki catch, nie stosując RAII.

Zalety:

  • Jawnie zarządza zasobami.

Wady:

  • W przypadku wyjścia przez throw przed catch nie da się uniknąć wycieków.
  • Trudno utrzymywać i modyfikować kod, łatwo zapomnieć o zwalnianiu.

Pozytywny przypadek

Pliki i zasoby są owijane w klasy-owoły RAII. W rezultacie: zwalnianie odbywa się w destruktorach, niezależnie od throw/catch.

Zalety:

  • Wiarygodność zwalniania zasobów.
  • Mniej kodu, łatwe w utrzymaniu.

Wady:

  • Należy umieć pisać owijki RAII.
  • Dodanie nowych rodzajów zasobów wymaga nowych klas.