Podstrony
|
[ Pobierz całość w formacie PDF ]
na koniec zwalniamy zasób, jeżeli nie jest już nam potrzebny (czyli korzystamy z delete w przypadku pamięci) Najbardziej znany nam zasob, czyli pamięć opercyjna, jest przez nas wykorzystywany choćby tak: CFoo* pFoo = new CFoo; // alokacja (utworzenie) obiektu-zasobu // (robimy coś...) Wyjątki 487 delete pFoo; // zwolnienie obiektu-zasobu Między stworzeniem a zniszczeniem obiektu może jednak zajść sporo zdarzeń. W szczególności: możliwe jest rzucenie wyjątku. Co się wtedy stanie?& Wydawać by się mogło, że obiekt zostanie zniszczony, bo przecież tak było zawsze& Błąd! Obiekt, na który wskazuje pFoo nie zostanie zwolniony z prostego powodu: nie jest on obiektem lokalnym, rezydującym na stosie, lecz tworzonym dynamicznie na stercie. Sami wydajemy polecenie jego utworzenia (new), więc również sami musimy go potem usunąć (poprzez delete). Zostanie natomiast zniszczony wskaznik na niego (zmienna pFoo), bo jest to zmienna lokalna - co aczkolwiek nie jest dla nas żadną korzyścią. Możesz zapytać: A w czym problem? Skoro pamięć należy zwolnić, to zróbmy to przed rzuceniem wyjątku - o tak: try { CFoo* pFoo = new CFoo; // ... if (warunek_rzucenia_wyjątku) { delete pFoo; throw wyjątek; } // ... delete pFoo; } catch (typ obiekt) { // ... } To powinno rozwiązać problem. Taki sposób to jednak oznaka skrajnego i niestety nieuzasadnionego optymizmu. Bo kto nam zagwarantuje, że wyjątki, które mogą nam przeszkadzać, będą rzucane wyłącznie przez nas?& Możemy przecież wywołać jakąś zewnętrzną funkcję, która sama będzie wyrzucała wyjątki - nie pytając nas o zgodę i nie bacząc na naszą zaalokowaną pamięć, o której przecież nic nie wie! To też nie katastrofa , odpowiesz, Możemy przecież wykryć rzucenie wyjątku i w odpowiedzi zwolnić pamięć: try { CFoo* pFoo = new CFoo; // ... try { // wywołanie funkcji potencjalnie rzucającej wyjątki FunkcjaKtoraMozeWyrzucicWyjatek(); } catch (...) { // niszczymy obiekt delete pFoo; // rzucamy dalej otrzymany wyjątek Zaawansowane C++ 488 throw; } // ... delete pFoo; } catch (typ obiekt) { // ... } Blok catch(...) złapie nam wszystkie wyjątki, a my w jego wnętrzu zwolnimy pamięć i rzucimy je dalej poprzez throw;. Wszystko proste, czyż nie? Brawo, twoja pomysłowość jest całkiem duża. Już widzę te dziesiątki wywołań funkcji bibliotecznych, zamkniętych w ich własne bloki try-catch(...), które dbają o zwalnianie pamięci& Jak sądzisz, na ile eleganckie, efektywne (zarówno pod względem czasu wykonania jak i zakodowania) i łatwe w konserwacji jest takie rozwiązanie?& Jeżeli zastanowisz się nad tym choć trochę dłuższą chwilę, to zauważysz, że to bardzo złe wyjście. Jego stosowanie (podobnie zresztą jak delete przed throw) jest świadectwem koszmarnego stylu programowania. Pomyślmy tylko, że wymaga to wielokrotnego napisania instrukcji delete - powoduje to, że kod staje się bardzo nieczytelny: na pierwszy rzut oka można pomyśleć, że kilka(naście) razy usuwany jest obiekt, który tworzymy tylko raz. Poza tym obecność tego samego kodu w wielu miejscach znakomicie utrudnia jego zmianę. Być może teraz pomyślałeś o preprocesorze i jego makrach& Jeśli naprawdę chciałbyś go zastosować, to bardzo proszę. Potem jednak nie narzekaj, że wyprodukowałeś kod, który stanowi zagadkę dla jasnowidza. Teraz możesz się oburzyć: No to co należy zrobić?! Przecież nie możemy dopuścić do powstawania wycieków pamięci czy niezamykania plików! Może należy po prostu zrezygnować z tak nieprzyjaznego narzędzia, jak wyjątki? Cóż, możemy nie lubić wyjątków (szczególnie w tej chwili), ale nigdy od nich nie uciekniemy. Jeżeli sami nie będziemy ich stosować, to użyje ich ktoś inny, którego kodu my będziemy potrzebowali. Na wyjątki nie powinniśmy się więc obrażać, lecz spróbować je zrozumieć. Rozwiązanie problemu zasobów, które zaproponowaliśmy wyżej, jest złe, ponieważ próbuje wtrącić się w automatyczny proces odwijania stosu ze swoim ręcznym zwalnianiem zasobów (tutaj pamięci). Nie tędy droga; należy raczej zastosować taką metodę, która pozwoli nam czerpać korzyści z automatyki wyjątków. Teraz poznamy właściwy sposób dokonania tego. Problem z niezwolnionymi zasobami występuje we wszystkich językach, w których funkcjonują wyjątki. Trzeba jednak przyznać, że w większości z nich poradzono sobie z nim znacznie lepiej niż w C++. Przykładowo, Java i Object Pascal posiadają możliwość zdefiniowania dodatkowego (obok catch) bloku finally ( nareszcie ). W nim zostaje umieszczany kod wykonywany zawsze - niezależnie od tego, czy wyjątek w try wystąpił, czy też nie. Jest to więc idealne miejsce na instrukcje zwalniające zasoby, pozyskane w bloku try. Mamy bowiem gwarancję, iż zostaną one poprawnie oddane niezależnie od okoliczności. Opakowywanie Pomysł jest dość prosty. Jak wiemy, podczas odwijania stosu niszczone są wszystkie obiekty lokalne. W przypadku, gdy są to obiekty naszych własnych klas, do pracy ruszają wtedy destruktory tych klas. Właśnie we wnętrzu tych destruktorów możemy umieścić kod zwalniający przydzieloną pamięć czy jakikolwiek inny zasób. Wyjątki 489 Wydaje się to podobne do ręcznego zwalniania zasobów przed rzuceniem wyjątku lub w blokach catch(...). Jest jednak jedna bardzo ważna różnica: nie musimy tutaj wiedzieć, w którym dokładnie miejscu wystąpi wyjątek. Kompilator bowiem i tak wywoła
[ Pobierz całość w formacie PDF ]
zanotowane.pldoc.pisz.plpdf.pisz.plkskarol.keep.pl
|