Destruktor
Ten artykuł od 2012-06 zawiera treści, przy których brakuje odnośników do źródeł. |
Destruktor – specjalna metoda, wywoływana przez program przed usunięciem obiektu i niemal nigdy nie wywoływana wprost w kodzie używającym obiektu. Pod względem funkcjonalnym jest to przeciwieństwo konstruktora.
Destruktor ma za zadanie wykonać czynności składające się na jego „zniszczenie”, inne niż zwolnienie pamięci zajmowanej przez sam obiekt, a przygotowujące obiekt do fizycznego usunięcia. Po jego wykonaniu obiekt znajduje się w stanie osobliwym i zazwyczaj nie można już z tym obiektem zrobić nic poza fizycznym usunięciem. Destruktor zwykle wykonuje takie czynności, jak zamknięcie połączenia z plikiem/gniazdem/potokiem, wyrejestrowanie się z innych obiektów, czasem również zanotowanie faktu usunięcia, a także usunięcie obiektów podległych, które obiekt utworzył lub zostały mu przydzielone jako podległe (jeśli jest ich jedynym właścicielem), lub wyrejestrowanie się z ich użytkowania (jeśli są to obiekty przezeń współdzielone).
W wielu językach programowania (np. Object Pascal) destruktor jest dziedziczony jak każda inna metoda.
Wiele obiektów nie musi mieć wcale destruktora, jeżeli nie wymagają innych czynności poza zwolnieniem zajmowanej przez siebie pamięci; takie obiekty nazywamy trywialnie-destruowalnymi (ang. trivially-destructible). W takiej sytuacji wykorzystywany jest destruktor domyślny, tworzony automatycznie przez kompilator języka.
Istnienie destruktora i jego konstrukcja zależy od użytego języka programowania. Choć w każdym języku obiekt musi być zniszczony pod koniec swojego istnienia, nie zawsze jest to oczywiste lub widoczne dla programisty: w niektórych językach istnieje mechanizm rozpoznawania, czy obiekt jest używany, a program automatycznie usuwa obiekty, których nie używa żaden inny obiekt. Tak nie jest np. w C++, ze względu na konieczność „ręcznego” zarządzania pamięcią, większość nietrywialnych klas musi mieć jawne destruktory. Dla kontrastu, w Pythonie, gdzie normalnie nigdy nie ma miejsca jawny przydział zasobów, destruktory są rzadkością; można stworzyć nawet bardzo rozbudowane hierarchie klas bez napisania jednego destruktora. W praktyce destruktory wymagane są niemal wyłącznie w kodzie znajdującym się na styku Pythona i innego systemu lub języka, w którym wymagane jest jawne zarządzanie zasobami.
Oznaczanie destruktora
Destruktor:
- w C++, C# i innych językach których składnia wzorowana jest na C, destruktor ma taką samą nazwę jak klasa, poprzedzoną tyldą [~] (dla odróżnienia od konstruktora)
- w Pascalu destruktor jest metodą oznaczoną słowem kluczowym destructor[1]
- w PHP destruktor ma nazwę __destruct
Przykładowy destruktor
W składni C++:
class Samochod{ public: string marka; //... (pewne dane i metody) ~Samochod() { std::cout << "Samochod " << marka << " zostal usuniety.\n"; } };
Tu destruktorem jest ~Samochod()
. W chwili usuwania obiektu, dokładnie przed zwolnieniem pamięci przeznaczonej dla obiektu jest wywoływany destruktor – stąd też podczas usuwania danego samochodu zostanie wyświetlona linijka tekstu informująca o tym. Warto zwrócić uwagę, że powyższy przykład jest nieco sztuczny – destruktor takiej postaci nie jest błędny, ale w rzeczywistych programach (poza momentem testowania powoływania i niszczenia obiektów) nie stosuje się destruktorów do drukowania komunikatów na ekranie.
Destruktor a wyciek pamięci
Ważnym zadaniem destruktora jest usuwanie podległych obiektów dynamicznych, które są mu przydzielone. Jeżeli nie zostaną one zwolnione w destruktorze, to dojdzie do tzw. wycieku pamięci. Poniżej przedstawiony został przykład klasy w C++, gdzie brak destruktora spowodowałby wyciek:
class MojaKlasa { public: MojaKlasa() : liczba(new int) { *liczba = 0; } ~MojaKlasa() { delete liczba; } private: int *liczba; };
Brak destruktora w tym przypadku spowoduje, że zostanie zwolniona pamięć przeznaczona na obiekt, w którym jest między innymi wskaźnik liczba
, ale nie zostanie zwolniona pamięć na którą wskazuje liczba
, ponieważ ten adres w pamięci nie zawiera się obszarze pamięci zajmowanej przez obiekt klasy MojaKlasa
. Domyślna implementacja destruktura nie jest w przypadku klasy MojaKlasa
wystarczająca do prawidłowego usunięcia zaalokowanej pamięci.
Powyższy przykład nie jest kompletną klasą, która działałaby prawidłowo w języku C++, gdyż brak jest konstruktora kopiującego i operatora przypisania. Pominięto je ze względu na czytelność przykładu.
Wirtualny destruktor
W większości języków destruktor, tak jak każda metoda może być wirtualny. W wielu sytuacjach destruktor musi być wirtualny aby zapewnić prawidłowe wykorzystanie klasy, np. w C++ klasa powinna mieć destruktor wirtualny kiedy zachodzi możliwość, że inna klasa będzie po niej dziedziczyła i będzie używany polimorfizm[2]. Jest to możliwość dziedziczenia, nawet kiedy w danej chwili klasa nie ma żadnych potomków; w przeciwnym razie zachodzi konieczność zmiany deklaracji destruktora w momencie dodania klasy pochodnej, a to spowodowałoby zerwanie kompatybilności z już istniejącym kodem wykorzystującym tę klasę.
Przykład w C++
#include <iostream>
class Osoba
{
public:
virtual ~Osoba()
{
std::cout << "~Osoba(): uruchomiono" << std::endl;
}
};
class Pracownik : public Osoba
{
public:
~Pracownik()
{
std::cout << "~Pracownik(): uruchomiono" << std::endl;
}
};
int main()
{
Osoba *wsk = new Pracownik;
delete wsk;
return 0;
}
W powyższym przykładzie zostały zdefiniowane dwie klasy – klasa podstawowa Osoba
oraz klasa pochodna Pracownik
. W sekcji publicznej klasy Osoba
został zdefiniowany wirtualny destruktor. Ma to istotne znaczenie w przypadku wykorzystywania polimorfizmu.
W funkcji głównej main()
zostaje zdefiniowany wskaźnik typu Osoba
, któremu zostaje przypisany adres obiektu typu Pracownik
zdefiniowanego dynamicznie. Poprzez wykorzystanie operatora delete
, który usuwa z pamięci wcześniej utworzony obiekt typu Pracownik
, zostaje wywołany niejawnie destruktor klasy Pracownik
.
Wynikiem działania programu będzie wypisanie na ekranie:
~Pracownik(): uruchomiono ~Osoba(): uruchomiono
W przypadku pominięcia słowa virtual
przed definicją destruktora w klasie Osoba
, destruktor ~Pracownik()
nie zostałby wywołany, ponieważ kompilator uznałby, że pokazuje na obiekt typu Osoba
(nie zostałby sprawdzony typ obiektu, na który pokazuje wskaźnik, w trakcie działania programu).
Pominięcie wywołania destruktora klasy pochodnej może doprowadzić do poważnych błędów w działaniu programu np. do wycieku pamięci. Oczywiście w tym prostym przykładzie nie doprowadzi to do żadnych poważnych konsekwencji. W przypadku niekorzystania z polimorfizmu, nie ma to znaczenia.
Destruktor klas dla których tworzy się klasy potomne muszą mieć destruktor wirtualny. Każda kolejna klasa, która dziedziczy od klasy potomnej, której rodzic ma destruktor wirtualny, również będzie posiadała destruktor wirtualny, nawet gdy nie użyjemy virtual
przy deklaracji czy definicji destruktora w klasie.
Oznacza to, że gdyby dopisać do powyższego przykładu:
(...)
class Elektryk : public Pracownik
{
public:
~Elektryk()
{
std::cout << "~Elektryk(): uruchomiono" << std::endl;
}
};
int main()
{
Osoba *wsk = new Elektryk;
delete wsk;
return 0;
}
to wynikiem działania programu będzie wypisanie na ekranie:
~Elektryk(): uruchomiono ~Pracownik(): uruchomiono ~Osoba(): uruchomiono
natomiast w przypadku pominięcia virtual
w klasie podstawowej, wywołanie wszystkich destruktorów (poza destruktorem klasy Osoba
) zostanie pominięte i na ekranie będzie wypisane tylko:
~Osoba(): uruchomiono
Finalizator
W niektórych językach z wbudowanym odśmiecaczem (np. Java i C#) dostępna jest składnia finalizatora – specjalnej metody wywoływanej, gdy obiekt jest usuwany przy odśmiecaniu. W przeciwieństwie do destruktora nie wiadomo, w którym dokładnie momencie działania programu to nastąpi.
Przypisy
- ↑ Static class methods, www.freepascal.org [dostęp 2017-11-22] .
- ↑ 4.5 Class Hierarchies. W: Bjarne Stroustrup: A Tour of C++. Wyd. 4. Addison-Wesley, lipiec 2015, seria: C++ In-Depth Series. ISBN 978-0-321-958310. [dostęp 2015-09-26]. (ang.)