kierownik tej organizacji tworzy plik zawierający informacje o załat-wianych w tym dniu adopcjach; 'Iwoim zadaniem jest napisanie pro-gramu, który czyta te pliki i odpowiednio przetwarza każdą informację o adopcji
Scott Meyers : “JÄ™zyk C++ bardziej efektywny", WNT 1998
Punkt 9: Używaj destruktorów, aby nie dopuścić do luk w zasobach
Pożegnaj się ze wskaźnikami. Przyznaj to: nigdy nie wzbudzały Twojej szczególnej sympatii.
W porzÄ…dku, nie musisz mówić “Å»egnaj!" wszystkim wskaźnikom, ale tym, które sÅ‚użą do manipulowania zasobami lokalnymi trzeba powiedzieć “Adieu!". Przypuśćmy, na przykÅ‚ad, że piszesz oprogramowanie dla Przytuliska Uwielbianych Zwierzaczków, organizacji zajmujÄ…cej siÄ™ wyszukiwaniem domów dla szczeniaków i kociaków. Codziennie kierownik tej organizacji tworzy plik zawierajÄ…cy informacje o zaÅ‚atwianych w tym dniu adopcjach; Twoim zadaniem jest napisanie programu, który czyta te pliki i odpowiednio przetwarza każdÄ… informacjÄ™ o adopcji.
Rozsądne podejście do tego zadania polega na zdefiniowaniu abstrakcyjnej klasy podstawowej, UZ (tzn. Uwielbiany Zwierzaczek), oraz jej konkretnych klas pochodnych, utworzonych odpowiednio dla piesków
i kotków.
UZ
Piesek Kotek
Funkcja wirtualna dokonajAdopcji obsługuje niezbędne przetwarzanie obiektów tych klas.
class UZ {
public:virtual void dokonajAdopcji() = 0;
...
};class Piesek: public UZ {
public:
virtual void dokonajAdopcji();
...
};
class Kotek: public UZ {
public:
virtual void dokonajAdopcji();
...
};
Potrzebna nam funkcja, która czyta informacje zawarte w pliku i w zależności od nich tworzy obiekt albo klasy Piesek, albo klasy Kotek. To doskonałe zadanie dla konstruktora wirtualnego, funkcji opisanej w punkcie 25. Dla naszych celów wystarczy taka deklaracja funkcji:
// czytaj ze strumienia s informacje o zwierzaku, następnie przekaż wskaźnik do nowo przydzielonej pamięci
// dla obiektu odpowiedniego typuUZ
* czytajUZ(istream& s);
Sercem programu będzie zapewne funkcja, która wygląda tak jak przytoczona poniżej:
void dokonajWszystkichAdopcji(istream& daneWej)
{while (daneWej) { // dopóki są jeszcze dane,
UZ *wz = czytajUZ(daneWej); // pobierz następne zwierzę
wz->dokonajAdopcji(); // dokonaj adopcji
delete wz; // usuń obiekt przekazany przez czytajUZ
}
}
Funkcja ta w pętli pobiera i przetwarza kolejno wszystkie informacje ze strumienia daneWej. Trzeba tylko zastosować tu drobną sztuczkę - skasować wskaźnik wz na końcu każdej iteracji. Jest to konieczne, ponieważ funkcja czytajUZ przy każdym jej wywołaniu tworzy nowy obiekt w stercie. Jeżeli zapomnimy użyć operatora delete, to powstanie luka w zasobach.
Zastanówmy się teraz, co by się zdarzyło, gdyby podczas wykonania instrukcji wz->dokonajAdopcji zgłoszono sytuację wyjątkową. Funkcja dokonajWszystkichAdopcji nie wyłapuje sytuacji wyjątkowych, więc zgłoszona sytuacja wyjątkowa zostałaby powielona na tę funkcję, która wywołała funkcję dokonajWszystkichAdopcji. Wówczas w funkcji dokonajWszystkichAdopcji byłby wykonany przeskok poprzez wszystkie instrukcje po instrukcji wz->dokonajAdopcji, to zaś oznacza, że nie skasowano by wskaźnika wz. W konsekwencji - powstaje luka w zasobach za każdym razem, gdy instrukcja wz->dokonajAdopcji zgłasza sytuację wyjątkową.
Taką lukę można dosyć łatwo zatkać:
void dokonajWszystkichAdopcji(istream& daneWej)
{while (daneWej)UZ *wz = czytajUZ(daneWej);
try {wz->dokonajAdopcji();
} catch (...) { // wyłapuj wszystkie sytuacje wyjątkowe ; unikaj luk w zasobach, gdy
delete wz; // są zgłaszane sytuacje wyjątkowe powielaj sytuację wyjątkową
throw; // na funkcję wywołującą
} delete wz; // unikaj luk w zasobach wtedy, gdy nie ma zgłoszenia sytuacji wyjątkowej
}
Musimy jednak w tym celu zaśmiecać program wstawiając do niego bloki try oraz catch. Jeszcze ważniejsze jest to, że jesteśmy zmuszeni powtarzać kod porządkujący, który jest wspólny zarówno dla normalnych, jak i dla wyjątkowych przebiegów sterowania. W tym wypadku trzeba powtórnie użyć operatora delete. Jak każde powtarzanie jest to nużące i utrudnia utrzymywanie programu, a przy tym jeszcze robi złe wrażenie. Skoro musimy skasować wskaźnik wz niezależnie od tego, czy opuszczamy funkcję dokonajWszystkichAdopcji poprzez zwykły powrót z wywołania funkcji czy też w wyniku zgłoszenia sytuacji wyjątkowej, to dlaczego trzeba zapisać tę operację w więcej niż jednym miejscu w programie?
Nie musimy tego robić, jeżeli kod porządkujący, który musi być zawsze wykonany, potrafimy jakoś przesunąć do destruktora obiektu lokalnego dla funkcji dokonajWszystkichAdopcji. Wynika to stąd, że obiekty lokalne są zawsze usuwane przy wychodzeniu z funkcji, niezależnie od sposobu zakończenia jej wykonywania. (Jedyny wyjątek od tej zasady dotyczy wywołania funkcji standardowej longjmp i właśnie to niedociągnięcie było głównym powodem wprowadzenia obsługi sytuacji wyjątkowych do języka C++). Tak więc jesteśmy tym zainteresowani, aby w naszym przykładzie przesunąć operator delete do destruktora obiektu lokalnego dla funkcji dokonajWszystkichAdopcji. Rozwiązanie polega na zastąpieniu wskaźnika wz przez obiekt, który działa tak jak wskaźnik. W ten sposób, kiedy obiekt wskaźnikopodobny jest (automatycznie) usuwany, wtedy wywołanie operatora delete może być wykonane za pomocą destruktora tego obiektu. Obiekty, które działają jak wskaźniki, ale robią jeszcze coś więcej, są nazywane inteligentnymi wskaźnikami (ang. smart pointers). Wskaźnikopodobne obiekty można uczynić rzeczywiście bardzo inteligentnymi wskaźnikami (zob. punkt 28). W naszym przykładzie nie potrzebujemy specjalnie zmyślnego wskaźnika, chcemy mieć wskaźnikopodobny obiekt, który wie tylko tyle, aby wtedy, gdy kończy się jego zasięg, móc usunąć to, na co on wskazuje.
Nietrudno napisać klasę takich obiektów, ale nie musimy tego robić. Biblioteka standardowa C++ zawiera wzorzec klasy, zwanej auto_ptr, która robi właśnie to, co chcemy. W każdej klasie auto_ptr jej konstruktor pobiera wskaźnik do obiektu w stercie i jej destruktor usuwa ten obiekt. Zawężając klasę auto_ptr do jej zasadniczych zadań, można ją przedstawić tak jak poniżej:
template<class T> class auto_ptr {
public: auto_ptr(T *w = 0): wsk(w) // zachowaj wskaźnik do obiektu
{}~auto_ptr() { delete wsk; } // skasuj wskaźnik wskprivate:
T *wsk; // zwyczajny wskaźnik do obiektu
};
Wersja standardowa klasy auto_ptr jest znacznie wymyślniejsza, a przedstawiona powyżej jej ogołocona implementacja nie nadaje się do rzeczywistego użytku (musimy dodać przynajmniej konstruktor kopiujący, operator przypisania oraz funkcje imitujące działania na wskaźnikach, omawiane w punkcie 28), ale sama koncepcja powinna być jasna: zastosuj obiekty klasy auto_ptr zamiast zwyczajnych wskaźników, aby nawet wówczas, gdy były zgłaszane sytuacje wyjątkowe, nie trzeba było martwić się o te obiekty w stercie, których nie usunięto. (Ponieważ destruktor klasy auto_ptr korzysta z operatora delete w postaci przeznaczonej do usuwania pojedynczych obiektów, więc auto_ptr nie może być klasą wskaźników do tablic obiektów. Jeżeli chcesz podobną klasę zastosować do tablic, to musisz napisać sobie własny wzorzec klasy) .
Gdy zastosujemy obiekty klasy auto_ptr zamiast zwyczajnych wskaźników, wówczas funkcja dokonajWszystkichAdopcji będzie wyglądać tak jak poniżej :
void dokonajWszystkichAdopcji(istream& daneWej)
{while (daneWej) { auto_ptr<UZ> wz(czytajUZ(daneWej));
wz->dokonajAdopcji();
}
}
Ta wersja funkcji dokonajWszystkichAdopcji różni się od pierwotnej tylko pod dwoma względami. Po pierwsze, deklaruje się, że wz jest obiektem klasy auto_ptr<UZ>, nie zaś zwyczajnym wskaźnikiem do obiektu klasy UZ. Po drugie, na końcu pętli nie ma instrukcji delete. I to wszystko. Cała reszta jest identyczna, ponieważ - oprócz usuwania wskaźników - obiekty klasy auto_ptr zachowują się tak samo jak normalne wskaźniki. Prawda, że to łatwe?
Przedstawiona powyżej koncepcja polegająca na tym, że używa się obiektu w celu przydzielenia zasobów, które trzeba automatycznie uwolnić, a do ich uwolnienia stosuje się destruktor tego obiektu, ma więcej zastosowań oprócz zastępowania wskaźników obiektami klasy auto_ptr. Rozważmy funkcję występującą w programie użytkowym korzystającym z graficznego interfejsu użytkownika (ang. GUI), która musi utworzyć okno w celu wyświetlania pewnych informacji.
// ta funkcja może zostawiać luki w zasobach,
// jeżeli będzie zgłoszona sytuacja wyjątkowa
void informuj(const Informacje& inf){
UCHWYT OKNA o(utwórzOkno());
przedstaw informacje inf w oknie odpowiadajÄ…cym zmiennej o;
skasuj0kno(o);
}
Wiele systemów tworzenia okien ma interfejsy programowe przeznaczone dla języka C, które korzystają z takich funkcji jak utwórz0kno oraz skasujOkno w celu pozyskiwania i uwalniania zasobów dla okna. Jeżeli podczas wyświetlania informacji inf w oknie o będzie zgłoszona sytuacja wyjątkowa, to obsługiwane okno będzie stracone tak samo, jak byłyby stracone dowolne zasoby przydzielane dynamicznie. Rozwiązanie jest takie samo jak w poprzednim przykładzie. Utwórzmy klasę, której konstruktor i destruktor - odpowiednio - pozyskuje i uwalnia zasoby.
// klasa służąca do pozyskiwania i uwalniania uchwytu okna
class Uchwyt0kna {public:
UchwytOkna(UCHWYT OKNA uchwyt) : o(uchwyt) {}
~UchwytOkna() { skasujOkno(o); }operator UCHWYT OKNA() { return o; } // patrz niżej
private:
UCHWYT OKNA o; // Poniższe funkcje zadeklarowano jako prywatne, aby
// zapobiec powstawaniu wielu kopii obiektu UCHWYT OKNA.
// Omówienie elastyczniejszego podejścia podano w punkcie 28
UchwytOkna(const UchwytOkna&);UchwytOkna& operator=(const Uchwyt0kna&);
};
Wygląda to tak samo jak wzorzec klasy auto_ptr, oprócz tego, że jawnie zabroniono przypisania i kopiowania oraz występuje operator niejawnego przekształcenia typu, którego można użyć , aby obiekt klasy UchwytOkna przekształcić w obiekt typu UCHWYT_OKNA. Ta możliwość jest ważna w praktycznych zastosowaniach obiektu klasy UchwytOkna, ponieważ oznacza, że można używać tej klasy właściwie zawsze tam, gdzie bezpośrednio użyłoby się typu UCHWYT_OKNA. (Powinno się jednak zachować ostrożność przy korzystaniu z operatorów niejawnego przekształcenia typu - zob. punkt 5).
Mając klasę Uchwyt0kna, możemy teraz napisać funkcję informuj tak jak poniżej .
// ta funkcja unika pozostawiania luk w zasobach w wypadku zgłoszenia sytuacji wyjątkowej
void informuj(const Informacje& inf)
{UchwytOkna o(utwórzOkno());
przedstaw informacje inf w oknie odpowiadajÄ…cym zmiennej o;
}
Okno utworzone przez funkcję utwórzOkno będzie usunięte nawet wówczas, gdy podczas wykonywania funkcji informuj będzie zgłoszona sytuacja wyjątkowa.
Można zazwyczaj uniknąć pozostawiania luk w zasobach w związku z występowaniem sytuacji wyjątkowych, jeśli przestrzega się zasady, że zasoby powinny być ukrywane wewnątrz obiektów. Ale co się zdarzy wtedy, kiedy sytuacja wyjątkowa będzie zgłoszona podczas operacji pozyskiwania zasobów, np. podczas działania konstruktora klasy pobierającej zasoby? Co będzie, jeżeli sytuację wyjątkową zgłoszono podczas automatycznego uwalniania takich zasobów? Czy konstruktory i destruktory nie wymagają stosowania specjalnych technik? Tak, wymagają - możesz o tym przeczytać w punktach 10 i 11.
Wyszukiwarka
Podobne podstrony:
punkt02punkt06punkt01punkt08punkt04więcej podobnych podstron