
Destruktor daje obiektowi ostatnie namaszczenie.
Destruktory są używane do zwalniania zasobów alokowanych wcześniej przez obiekt. Np. hipotetyczna klasa Lock mogłaby zamykać semafor [obiekt sterujący dostępem do pewnych danych lub fragmentu kodu - przyp. tłum.], zaś jej destruktor - otwierać. Najczęściej jest tak, że konstruktory tworzą składowe obiektu przy użyciu operatora new, natomiast destruktory niszczą je operatorem delete.
Destruktory są metodami typu "przygotuj się na śmierć". W języku angielskim, zamiast słowa "destruktor" często stosuje się skrót "dtor".
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
W odwrotnej, niż były konstruowane: pierwszy utworzony = ostatni zniszczony.
W poniższym przykładzie, najpierw zostanie wywołany destruktor obiektu b, a następnie destruktor obiektu a.
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
W odwrotnej niż były konstruowane: pierwszy utworzony = ostatni zniszczony.
W poniższym przykładzie destruktory zostaną wywołane kolejno dla obiektów: a[9], a[8], ..., a[1], a[0]:
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
Nie.
Możesz utworzyć tylko jeden destruktor dla klasy Fred. Musi się on nazywać Fred::~Fred(). Nie może on pobierać żadnych parametrów, ani też zwracać jakiejkolwiek wartości.
Nie możesz przekazać destruktorowi żadnych parametrów, gdyż nigdy nie powinno się jawnie wywoływać destruktora (właściwie to prawie nigdy).
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
Nie!
Destruktor zostanie wtedy ponownie wywołany pod koniec bloku (znak "}"), w którym zmienna lokalna została utworzona. Gwarantuje to specyfikacja języka; "przepisowa" destrukcja następuje automagicznie i nie można jej w żaden sposób powstrzymać. Jeżeli wywołasz destruktor po raz drugi na tym samym obiekcie, możesz odczuć naprawdę poważne tego skutki. Bum! Nie żyjesz!
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]

Nie! [Dla wyjaśnienia czemu, proszę przeczytać poprzedniego faqa].
Przypuśćmy, że ubocznym (ale jednocześnie celowym) efektem destrukcji obiektu klasy "Plik" jest zamknięcie pliku obsługiwanego przez ten obiekt. Następnie załóżmy, że masz obiekt p klasy "Plik" i chcesz zamknąć "Plik" p przed końcem bloku, w którym zdefiniowano ten obiekt (przed znakiem }):
Istnieje proste rozwiązanie tego problemu. Ale zanim je poznasz, zapamiętaj: nie wywołuj destruktora jawnie!
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
[Dla wyjaśnienia o co chodzi, proszę przeczytać poprzedniego faqa].
Po prostu utwórz nowy, "sztuczny" blok {...} i w nim zdefiniuj obiekt:
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
W większości przypadków możesz ograniczyć "czas życia" (ang. lifetime) lokalnego obiektu umieszczając go w sztucznym bloku ({...}). Jeśli z jakiegoś powodu nie możesz tego zrobić, zdefiniuj metodę, której działanie będzie podobne do destruktora. Ale nie wywołuj destruktora bezpośrednio!
Przykładowo, do klasy "Plik" możnaby dodać metodę zamknij() (tak jak w poniżej przedstawionym kodzie). W większości przypadków destruktor po prostu wywoływałby tę metodę. Zauważ, że metoda zamknij() musi w jakiś sposób zaznaczać, że "Plik" został zamknięty, tak aby późniejsze wywołania tej metody nie próbowały go ponownie zamknąć. Na przykład, metoda ta, po pomyślnym zamknięciu pliku, mogłaby ustawiać pole deskryptor_ na jakąś nonsensowną wartość, powiedzmy -1, a przed próbą zamknięcia pliku sprawdzałaby, czy deskryptor_ został już wcześniej ustawiony na -1.
Zwróć uwagę, że niektóre metody klasy "Plik" być może będą potrzebowały sprawdzać, czy deskryptor_ jest równy -1 (tzn., sprawdzać czy "Plik" jest otwarty).
Ponadto wszystkie konstruktory, których działanie nie doprowadziłoby do otwarcia pliku [np. na skutek błędu - przyp. tłum.], muszą ustawić deskryptor_ na -1.
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
Prawdopodobie nie.
Jeżeli obiekt nie został alokowany przy użyciu placement new, to powinno się go usuwać raczej poprzez operator delete niż jawne wywołanie destruktora. Na przykład, załóżmy że alokowałeś(-aś) pewien obiekt przy użyciu "zwyczajnego new":
W takim przypadku destruktor Fred::~Fred() zostanie wywołany automagicznie podczas usuwania obiektu operatorem delete:
W powyższym przykładzie nie powinno się jawnie wywoływać destruktora, gdyż nie spowoduje to zwolnienia obszaru pamięci zajmowanego przez obiekt klasy Fred. Zapamiętaj: delete p wykonuje dwie czynności: wywołuje destruktor obiektu p i zwalnia pamięć zajmowaną przez ten obiekt.
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
Istnieje wiele zastosowań "placement new". Najprostszym z nich jest umieszczenie obiektu w ściśle określonym miejscu w pamięci. W tym celu podaje się wskaźnik do tego miejsca jako parametr dla operatora new:
W wierszu #1 zostaje utworzona tablica mająca rozmiar sizeof(Fred) bajtów, czyli wystarczająco duża na zmieszczenie w niej obiektu klasy Fred. W wierszu #2 tworzony jest wskaźnik wskazujący na początek tej tablicy (doświadczeni programiści C zauważą z pewnością, że ten krok jest w zupełności zbędny; ma on tylko uczynić kod bardziej oczywistym). W wierszu #3 nastąpi po prostu wywołanie konstruktora Fred::Fred() [operator new nie będzie alokował żadnego obszaru w pamięci, ponieważ podano mu już miejsce, w którym ma zostać utworzony obiekt - przyp. tłum.]. Wskaźnik this w konstruktorze Freda będzie równy place. Stąd wskaźnik f również będzie równy place.
PORADA: Nie używaj "placement new" chyba że musisz. Stosuj je tylko wtedy, gdy naprawdę zależy Ci na tym, aby obiekt znajdował się w konkretnym miejscu w pamięci. Przykładem może być sytuacja, gdy programowany przez Ciebie sprzęt posiada wbudowany układ czasowy, który jest mapowany w pewnym miejscu w pamięci (ang. memory-mapped I/O), a ty chcesz w tym miejscu umieścić obiekt klasy Zegar [który np. odczytywałby aktualną godzinę i datę z tego układu czasowego - przyp. tłum.].
[Przypis tłumacza - Mapowanie urządzeń wejścia/wyjścia w pamięci polega na tym, że urządzenia te widziane są jako fragment pamięci operacyjnej. Typowym przykładem w architekturze PC może być karta graficzna - jej pamięć zawierająca wyświetlany obraz (tzw. "bufor ramki") widziana jest przez procesor jako fragment pamięci RAM. Podczas przesyłania danych do "bufora ramki" procesorowi "wydaje się", że nawiązuje komunikację z pamięcią RAM, ale w rzeczywistości dane są przesyłane między nim a kartą graficzną. W ten sposób sterowanie urządzeniem wygląda jak dostęp do "zwykłego" obiektu w pamięci.]
UWAGA: Bierzesz całkowitą odpowiedzialność za to, że wskaźnik będący parametrem operatora new wskazuje na obszar pamięci odpowiednio wyrównany (ang. aligned) i wystarczający na zmieszczenie nowego obiektu. Ani kompilator ani biblioteka czasu pracy programu (ang. run-time system) nie będą sprawdzać, czy wskaźnik spełnia te wymagania. Jeżeli klasa Fred wymagałaby wyrównania do 4-bajtowego bloku, a dostarczony wskaźńik wskazywałby na obszar pamięci niewyrównany w ten sposób, mogłoby to narazić Twój program na poważną katastrofę (jeżeli nie wiesz co oznacza "wyrównanie miejsca w pamięci", proszę nie stosuj "placement new"). Zostałeś(aś) ostrzeżony(a)!
Ponadto bierzesz całkowitą odpowiedzialność za destrukcję obiektu utworzonego przy użyciu "placement new". Przeprowadza się ją przez jawne wywołanie destruktora:
Jest to prawdopodobnie jedyna sytuacja, w której będziesz potrzebował(a) jawnie wywołać destruktor.
Notka: istnieje wygodniejsza, ale bardziej skomplikowana metoda przeprowadzania destrukcji obiektów.
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
Nie. Nigdy nie ma potrzeby jawnego wywoływania destruktorów (z wyjątkiem placement new).
Destruktor klasy automagicznie wywołuje destruktory wszystkich pól składowych. Pola są niszczone w odwrotnej kolejności niż występują w deklaracji klasy.
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
Nie. Nigdy nie ma potrzeby jawnego wywoływania destruktorów (z wyjątkiem placement new).
Destruktor klasy potomnej (nieważne czy go jawnie zdefiniujesz czy też nie) automagicznie wywołuje destruktory dla każdej klasy bazowej. Najpierw niszczone są pola należące do klasy potomnej, a następnie niszczone są kolejne klasy bazowe. Jeżeli klasa posiada kilka bezpośrednich klas bazowych (dziedziczenie wielokrotne) to są one niszczone w odwrotnej kolejności, niż zostały one zapisane w liście dziedziczenia klasy.
Notka: Kolejność w przypadku dziedziczenia wirtualnego jest bardziej zagmatwana. Jeżeli Twój kod ma polegać na kolejności inicjalizacji/deinicjalizacji w dziedziczeniu wirtualnym, będziesz potrzebował(a) znacznie więcej informacji niż jest to zawarte w tym faqu.
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
Wystrzegaj się!!! Wyjaśnienia znajdziesz tutaj.
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
Tak. Dobrą wiadomością będzie z pewnością fakt, że tego rodzaju "pule pamięci" (ang. "memory pool") mają wiele zastosowań. Zła wiadomość jest natomiast taka, że zanim omówimy te wszystkie zastosowania, będziemy musieli przebyć grzęzawisko wiedzy na temat "jak to działa?". Jednak z drugiej strony, jeśli teraz nic nie wiesz na temat "pul pamięci", to przebrnięcie przez tego faqa może się okazać warte zachodu być może nauczysz się czegoś użytecznego!
Na początku, przypomnijmy sobie do czego służy alokator pamięci - ma on za zadanie zwrócić niezainicjowany osbzar w pamięci; nie tworzy on nowych obiektów [tylko po prostu przygotowuje dla nich miejsce w pamięci - przyp. tłum.]. W szczególności alokator nie ustawia wskaźnika do tablicy wirtualnej (ang. virtual pointer) ani jakiegokolwiek pola obiektu, gdyż jest to zadaniem konstruktora wywoływanego tuż po alokatorze. Mając prostą funkcję spełniającą rolę alokatora, np. allocate(), możnaby użyć placement new do skonstruowania obiektu w obszarze przygotowanym przez alokator. Innymi słowy, poniższy kod jest równoważny instrukcji "new Foo()":
OK, zakładając, że uzyłeś(-aś) placement new i przetrwałeś(-aś) powyższe dwie linijki kodu, możemy przejść do kolejnego kroku - zamiany funkcji-alokatora na obiekt. Tego rodzaju obiekt nazywa się "pulą pamięci" lub "areną pamięci" (ang. memory arena). Rozwiązanie to pozwala użytkownikowi na posiadanie kilku pul (lub "aren", jak kto woli), z których może on alokować pamięć. Każdy z tych obiektów-aren, przy użyciu jakiegoś wywołania systemowego, najpierw alokuje duży obszar pamięci (np. z pamięci współdzielonej, z pamięci programu, ze stosu itd.; zobacz niżej) a następnie udostępnia go programowi w formie mniejszych fragmentów, w zależności od potrzeb. Twoja klasa obsługująca pulę pamięci mogłaby wyglądać mniej więcej tak:
Teraz jeden z użytkowników Twojej klasy mógłby mieć pulę o nazwie pool, z której alokowałby obiekty w ten sposób:
Lub prościej:
Zdefiniowanie puli pamięci jako klasy jest o tyle dobrym rozwiązaniem, że pozwala ono użytkownikom na utworzenie N różnych pul pamięci zamiast jednej dużej współdzielonej przez wszystkich użytkowników. Dzięki temu użytkownicy mogą robić różne, dziwne rzeczy. Na przykład, jeśli fragment programu alokowałby jak dziki mnóstwo różnych obiektów, to mógłby je później wszystkie usunąć za jednym zamachem, usuwając po prostu pulę w której te obiekty zostały alokowane (zamiast usuwać każdy z nich osobno używając operatora delete). Innym przykładem jest założenie puli w obszarze "pamięci współdzielonej" (tzn. pamięci wspólnej dla kilku procesów) i pobieranie pamięci z obszaru współdzielonego zamiast z "lokalnej pamięci procesu". Jeszcze inny przykład: wiele kompilatorów posiada dodatkową funkcję alloca(), która alokuje blok pamięci ze stosu a nie ze sterty. Naturalnie blok ten jest automatycznie dealokowany po opuszczeniu funkcji, co eliminuje konieczność jawnego kasowania poszczególnych obiektów operatorem delete. Ktoś może zatem użyć funkcji alloca(), aby ta "dostarczyła" dla puli odpowiednio dużą ilość pamięci. Następnie wszystkie obiekty alokowane przy użyciu tej puli będą się zachowywać jak zmienne lokalne: automatycznie "znikną" po opuszczeniu funkcji. Oczywiście w ten sposób nie zostaną wywołane destruktory tych obiektów; jeśli destruktory te miały wykonywać jakieś ważne czynności, nie będziesz mógł (mogła) zastosować tej techniki, ale w niektórych przypadkach, gdy destruktory jedynie dealokują pamięć, technika "pul pamięci" może się okazać przydatna.
OK, zakładając, że przetrwałeś(-aś) od 6 do 8 linii kodu potrzebnych do zamiany Twojej funkcji-alokatora w metodę klasy Pool, przystępujemy do następnego kroku - zmiany składni służącej do alokowania obiektów. Celem jest przejście z raczej niewygodnego zapisu new(pool.alloc(sizeof(Foo))) Foo() do prostszej składni new(pool) Foo(). Aby urzeczywistnić ten plan, potrzebujemy dodać jeszcze dwie linijki kodu tuż pod definicją Twojej klasy Pool:
Teraz kompilator, za każdym razem gdy "zobaczy" new(pool) Foo(), wstawi w to miejsce powyższą funkcję i przekaże jej jako parametry sizeof(Foo) i pool. Jedynym miejscem, w którym będzie występował śmieszny zapis pool.alloc(nbytes), jest Twój nowy operator new.
Przejdźmy teraz do kwestii niszczenia/dealokacji obiektów Foo. Przypomnijmy sobie na czym polega "metoda siłowa" stosowana czasami w "placement new" - mianowicie na jawnym wywołaniu destruktora a następnie na jawnej dealokacji pamięci zajmowanej przez obiekt:
Metoda ta stwarza kilka problemów, ale wszystkie z nich da się rozwiązać:
Rozwiążemy te problemy po kolei:
Problem #1: zapobieganie wyciekom pamięci. Kiedy używasz "zwykłego" operatora new, np. Foo* p = new Foo(), kompilator wstawia w to miejsce specjalny kod obsługujący sytuacje, gdy konstruktor wyrzuci wyjątek. Kod generowany przez kompilator funkcjonalnie przypomina poniższy:
Wniosek jest taki, że kompilator dealokuje obszar przeznaczony na obiekt, jeśli konstruktor zgłosi wyjątek. Ale w przypadku "new z parametrem" (często nazywanego "placement new"), kompilator nie będzie wiedział co zrobić, jeśli zostanie zgłoszony wyjątek, tak więc w tej sytuacji nie zrobi on nic.
Wobec tego trzeba zmusić kompilator, aby ten zrobił coś podobnego jak w przypadku globalnej wersji operatora new. Na szczęście jest to proste: kiedy kompilator trafia na instrukcję new(pool) Foo(), rozpoczyna poszukiwania odpowiadającej jej wersji operatora delete. Jeżeli znajdzie taką, w miejsce instrukcji z operatoreem new wstawia kod z konstruktorem umieszczonym w bloku try, analogicznie jak w podanym wyżej przykładzie ze "zwykłym new". Zatem wystarczy zdefiniować operator delete według poniższego kodu (ostrożnie; jeśli drugi parametr operatora delete będzie miał inny typ niż w operator new(size_t, Pool&), kompilator nie będzie z tego powodu narzekał; po prostu nie wstawi on bloku try gdy użytkownik napisze new(pool) Foo()):
Teraz kompilator będzie automatycznie umieszczał wywołanie konstruktora w bloku try w miejscu instrukcji z operatorem new:
Innymi słowy, zdefiniowanie jednoliniowej funkcji operator delete(void* p, Pool& pool) zmusiło kompilator do automagicznego zatykania wycieku pamięci. Oczywiście funkcja ta może, choć nie musi, być inline.
Problemy #2 ("niewygodna, więc przyciągająca błędy składnia") i #3 ("użytkownicy muszą ręcznie wiązać obiekty ze wskaźnikami do pól, w których były one alokowane, co może stać się przyczyną powstawania błędów w programie") można rozwiązać jednocześnie wstawiając dodatkowe 10-20 linijek kodu w jednym miejscu. Innymi słowy, umieścimy 10-20 linii kodu w jednym miejscu (w pliku nagłówkowym z deklaracją obiektu Pool) i dzięki temu uprościmy kod w wielu innych miejscach (w każdym kawałku kodu korzystającym z Twojej klasy Pool).
Pomysł polega na ukrytym powiązaniu wskaźnika Pool* z każdym alokowanym obszarem. Wskaźnikiem Pool* powiązanym z globalnym alokatorem [nie wykorzystującym puli - przyp. tłum.] mógłby być NULL; chodzi o to żeby przynajmniej koncepcyjnie każda alokacja była powiązana z jakąś pulą poprzez wskaźnik Pool*. Wtedy możesz zastąpić globalny operator delete funkcją, która sprawdzałaby z którą pulą Pool* jest powiązany usuwany obszar - jeśli pulą tą nie byłby NULL to obszar dealokowany by był metodą klasy Pool. Przykładowo, jeśli(!) "normalny" dealokator używałby funkcji free(), nasz nowy operator delete mógłby wyglądać mniej więcej tak:
Jeżeli nie masz pewności czy normalnym dealokatorem jest free(), najprostszym rozwiązaniem jest wymiana operatora new na funkcję, która do alokacji używałaby malloc(). Funkcja ta mogłaby wyglądać tak jak poniżej (uwaga: ta definicja nie uwzględnia kilku szczegółów takich jak pętla new_handler i throw std::bad_alloc() w przypadku braku pamięci):
Jedynym problemem, jaki w tym momencie pozostał, jest powiązanie wskaźnika Pool* z alokowanym obszarem. Jednym z możliwych rozwiązań, zastosowanym w conajmniej jednej komercyjnej aplikacji, jest użycie std::map<void*,Pool*>. Innymi słowy, chodzi o utworzenie tablicy przeglądowej (ang. "look-up table"), której kluczami byłyby wskaźniki do alokowanych obszarów zaś elementami wskaźniki Pool*. Z powodów które omówię za chwilę, para klucz/element nie powinna być dodawana do listy wewnątrz globalnego operatora new (np., nie wolno Ci napisać poolMap[p] = NULL w definicji globalnego operatora new). Powód jest następujący: niezastosowanie się do tej rady doprowadziłoby do powstania nieskończonej pętli std::map prawdopodobnie wykorzystuje globalny operator new, stąd dodanie nowego elementu do listy spowodowałoby dodanie kolejnego elementu do listy itd., prowadząc do nieskończonej rekursji bum, nie żyjesz.
Nawet uwzględniając fakt, że technika ta wymaga przeglądnięcia tablicy std::map przy każdej dealokacji, jej wydajność okazuje się akceptowalna, przynajmniej w wielu przypadkach.
Innym rozwiązaniem - szybszym ale mogącym zużywać więcej pamięci, a poza tym nieco skomplikowanym - jest umieszczenie wskaźnika Pool* tuż przed każdym alokowanym obszarem. Na przykład, jeśli nbytes byłoby równe 24, co oznaczałoby że użytkownik prosi o alokowanie 24 bajtów, moglibyśmy alokować 28 (lub 32, jeśli Twój komputer wymaga 8-bajtowego wyrównania dla danych typu double lub long long), umieścić wskaźnik do puli Pool* w pierwszych czterech bajtach alokowanego obszaru a następnie zwrócić adres o 4 (lub 8) bajtów większy od adresu tego co alokowaliśmy. Z kolei operator delete cofałby się o 4 (lub 8) bajtów, odczytywał wskaźnik Pool*, i jeśli byłby on równy NULL to dealokowałby obszar funkcją free(); natomiast gdyby wskaźnik nie był równy NULL to obszar dealokowany by był metodą pool->dealloc(). Parametrem przekazywanym do funkcji free() lub metody pool->dealloc() byłby wskaźnik powiększony o 4 (lub 8) bajtów w stosunku do oryginalnego parametru p. Jeśli(!) zdecydował(a)byś się na 4-bajtowe wyrównanie, Twój kod mógłby wyglądać mniej więcej tak (podobnie jak poprzednio pominięto obsługę braku pamięci):
Naturalnie ostatnie akapity tego faqa mają zastosowanie tylko jeżeli masz możliwość zmiany definicji globalnych operatorów new i delete. Nawet jeśli nie masz takiej możliwości, pierwsze trzy czwarte tego faqa wciąż pozostają aktualne.
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
E-mail the author
[ C++ FAQ Lite
| Spis treści
| Skorowidz
| O autorze
| ©
| Pobierz swoją własną kopię ]
Ostatnia aktualizacja Jun 17, 2002
Wersja polska: 0.1i Jul 12, 2004