A destruktor to kolejny specjalny rodzaj funkcji składowej klasy, która jest wykonywana, gdy obiekt tej klasy zostaje zniszczony. Podczas gdy konstruktory są przeznaczone do inicjowania klasy, destruktory mają za zadanie pomóc w czyszczeniu.
Kiedy obiekt normalnie wykracza poza zakres lub dynamicznie przydzielany obiekt jest jawnie usuwany przy użyciu słowa kluczowego usuwania, destruktor klasy jest automatycznie wywoływany (jeśli istnieje) w celu przeprowadzenia niezbędnego czyszczenia przed usunięciem obiektu z pamięci. W przypadku prostych klas (tych, które po prostu inicjują wartości normalnych zmiennych składowych) destruktor nie jest potrzebny, ponieważ C++ automatycznie czyści pamięć za Ciebie.
Jeśli jednak obiekt klasy przechowuje jakiekolwiek zasoby (np. pamięć dynamiczną, uchwyt pliku lub bazy danych) lub jeśli musisz przeprowadzić jakąkolwiek konserwację przed zniszczeniem obiektu, destruktor jest do tego idealnym miejscem, ponieważ zazwyczaj jest to ostatnia rzecz, która ma miejsce przed zniszczeniem obiektu zniszczone.
Nazewnictwo destruktorów
Podobnie jak konstruktory, destruktory mają określone zasady nazewnictwa:
- Destruktor musi mieć taką samą nazwę jak klasa, poprzedzoną tyldą (~).
- Destruktor nie może przyjmować argumentów.
- Destruktor nie ma typu zwracanego.
Klasa może mieć tylko jeden destruktor.
Generalnie nie powinieneś jawnie wywoływać destruktora (ponieważ zostanie on wywołany automatycznie, gdy obiekt zostanie zniszczony), ponieważ rzadko zdarzają się przypadki, w których chciałbyś wyczyścić obiekt więcej niż raz. Jednakże destruktory mogą bezpiecznie wywoływać inne funkcje składowe, ponieważ obiekt nie jest niszczony do czasu wykonania destruktora.
Przykład destruktora
Przyjrzyjmy się prostej klasie korzystającej z destruktora:
#include <iostream>
#include <cassert>
#include <cstddef>
class IntArray
{
private:
int* m_array{};
int m_length{};
public:
IntArray(int length) // constructor
{
assert(length > 0);
m_array = new int[static_cast<std::size_t>(length)]{};
m_length = length;
}
~IntArray() // destructor
{
// Dynamically delete the array we allocated earlier
delete[] m_array;
}
void setValue(int index, int value) { m_array[index] = value; }
int getValue(int index) { return m_array[index]; }
int getLength() { return m_length; }
};
int main()
{
IntArray ar ( 10 ); // allocate 10 integers
for (int count{ 0 }; count < ar.getLength(); ++count)
ar.setValue(count, count+1);
std::cout << "The value of element 5 is: " << ar.getValue(5) << '\n';
return 0;
} // ar is destroyed here, so the ~IntArray() destructor function is called hereWskazówka
Jeśli skompilujesz powyższy przykład i otrzymasz następujący błąd:
error: 'class IntArray' has pointer data members [-Werror=effc++]| error: but does not override 'IntArray(const IntArray&)' [-Werror=effc++]| error: or 'operator=(const IntArray&)' [-Werror=effc++]|
Następnie możesz albo usunąć flagę „-Weffc++” ze swoich ustawień kompilacji w tym przykładzie lub możesz dodać do klasy następujące dwa wiersze:
IntArray(const IntArray&) = delete;
IntArray& operator=(const IntArray&) = delete;Omawiamy =delete dla elementów na lekcji 14.14 -- Wprowadzenie do konstruktora kopiującego
Ten program generuje wynik:
The value of element 5 is: 6
W pierwszym wierszu funkcji main() tworzymy instancję nowego obiektu klasy IntArray o nazwie ar i przekazujemy długość 10. Wywołuje to konstruktor, który dynamicznie przydziela pamięć członowi tablicy. Musimy tutaj zastosować alokację dynamiczną, ponieważ w czasie kompilacji nie wiemy, jaka jest długość tablicy (decyduje o tym osoba wywołująca).
Na końcu funkcji main() ar wychodzi poza zakres. Powoduje to wywołanie destruktora ~IntArray(), który usuwa tablicę, którą zaalokowaliśmy w konstruktorze!
Przypomnienie
W lekcji 16.2 — Wprowadzenie do std::vector i konstruktorów list, zauważamy, że inicjalizacja oparta na nawiasach powinna być używana podczas inicjowania klasy tablica/kontener/lista o długości (a nie listy elementów). Z tego powodu inicjujemy IntArray za pomocą IntArray ar ( 10 );.
Konstruktora i destruktora taktowania
Jak wspomniano wcześniej, konstruktor jest wywoływany, gdy obiekt jest tworzony, a destruktor jest wywoływany, gdy obiekt zostaje zniszczony. W poniższym przykładzie używamy instrukcji cout wewnątrz konstruktora i destruktora, aby to pokazać:
#include <iostream>
class Simple
{
private:
int m_nID{};
public:
Simple(int nID)
: m_nID{ nID }
{
std::cout << "Constructing Simple " << nID << '\n';
}
~Simple()
{
std::cout << "Destructing Simple" << m_nID << '\n';
}
int getID() { return m_nID; }
};
int main()
{
// Allocate a Simple on the stack
Simple simple{ 1 };
std::cout << simple.getID() << '\n';
// Allocate a Simple dynamically
Simple* pSimple{ new Simple{ 2 } };
std::cout << pSimple->getID() << '\n';
// We allocated pSimple dynamically, so we have to delete it.
delete pSimple;
return 0;
} // simple goes out of scope hereTen program daje następujący wynik:
Constructing Simple 1 1 Constructing Simple 2 2 Destructing Simple 2 Destructing Simple 1
Zauważ, że „Simple 1” jest niszczone po „Simple 2”, ponieważ usunęliśmy pSimple przed końcem funkcji, podczas gdy proste nie zostało zniszczone aż do końca main().
Zmienne globalne są konstruowane przed funkcją main() i niszczone po main().
RAII
RAII (Resource Acquisition Is Inicjalizacja) to technika programowania, w której wykorzystanie zasobów jest powiązane z czasem życia obiektów o automatycznym czasie trwania (np. obiektów niealokowanych dynamicznie). W C++ RAII jest implementowany poprzez klasy z konstruktorami i destruktorami. Zasób (taki jak pamięć, uchwyt pliku lub bazy danych itp.) jest zwykle pobierany w konstruktorze obiektu (chociaż można go uzyskać po utworzeniu obiektu, jeśli ma to sens). Zasobu tego można następnie używać, gdy obiekt żyje. Zasób jest zwalniany w destruktorze, gdy obiekt ulega zniszczeniu. Podstawową zaletą RAII jest to, że pomaga zapobiegać wyciekom zasobów (np. brakowi zwolnienia pamięci), ponieważ wszystkie obiekty przechowujące zasoby są czyszczone automatycznie.
Klasa IntArray na początku tej lekcji jest przykładem klasy, która implementuje RAII — alokacja w konstruktorze, zwolnienie alokacji w destruktorze. std::string i std::vector to przykłady klas w bibliotece standardowej zgodnych z RAII - pamięć dynamiczna jest pobierana podczas inicjalizacji i automatycznie czyszczona po zniszczeniu.
Ostrzeżenie dotyczące funkcji std::exit()
Zauważ, że jeśli użyjesz funkcji std::exit(), program zakończy działanie i nie zostaną wywołane żadne destruktory. Zachowaj ostrożność, jeśli polegasz na destruktorach w celu wykonania niezbędnych prac porządkowych (np. zapisania czegoś do pliku dziennika lub bazy danych przed wyjściem).
Streszczenie
Jak widzisz, gdy konstruktory i destruktory są używane razem, twoje klasy mogą inicjować i sprzątać po sobie bez konieczności wykonywania przez programistę żadnej specjalnej pracy! Zmniejsza to prawdopodobieństwo popełnienia błędu i ułatwia korzystanie z klas.

