Udostępnij za pośrednictwem


Destruktory (C++)

Destruktor jest funkcją składową, która jest wywoływana automatycznie, gdy obiekt wykracza poza zakres lub jest jawnie niszczony przez wywołanie metody delete lub delete[]. Destruktor nosi tę samą nazwę co klasa i jest poprzedzony tyldą (~). Na przykład destruktor klasy String jest zadeklarowany: ~String().

Jeśli nie zdefiniujesz destruktora, kompilator udostępnia domyślną, a dla niektórych klas jest to wystarczające. Należy zdefiniować niestandardowy destruktor, gdy klasa zarządza zasobami, które muszą zostać jawnie zwolnione, takimi jak uchwyty do zasobów systemowych lub wskaźniki do pamięci, która powinna zostać zwolniona po zniszczeniu instancji klasy.

Rozważ następującą deklarację String klasy:

// spec1_destructors.cpp
#include <string> // strlen()

class String
{
    public:
        String(const char* ch);  // Declare the constructor
        ~String();               // Declare the destructor
    private:
        char* _text{nullptr};
};

// Define the constructor
String::String(const char* ch)
{
    size_t sizeOfText = strlen(ch) + 1; // +1 to account for trailing NULL

    // Dynamically allocate the correct amount of memory.
    _text = new char[sizeOfText];

    // If the allocation succeeds, copy the initialization string.
    if (_text)
    {
        strcpy_s(_text, sizeOfText, ch);
    }
}

// Define the destructor.
String::~String()
{
    // Deallocate the memory that was previously reserved for the string.
    delete[] _text;
}

int main()
{
    String str("We love C++");
}

W poprzednim przykładzie destruktor String::~String używa operatora delete[] do dealokacji miejsca dynamicznie przydzielonego na przechowywanie tekstu.

Deklarowanie destruktorów

Destruktory są funkcjami o tej samej nazwie co klasa, ale poprzedzane tyldą (~)

Kilka zasad rządzi deklaracją destruktorów. Destruktory:

  • Nie akceptuj argumentów.
  • Nie zwracaj wartości (lub void).
  • Nie można zadeklarować jako const, volatilelub static. Można je jednak wywołać w celu zniszczenia obiektów zadeklarowanych jako const, volatilelub static.
  • Można zadeklarować jako virtual. Za pomocą destruktorów wirtualnych można zniszczyć obiekty bez znajomości ich typu — poprawny destruktor obiektu jest wywoływany przy użyciu mechanizmu funkcji wirtualnej. Destruktory można również zadeklarować jako czyste funkcje wirtualne dla klas abstrakcyjnych.

Używanie destruktorów

Destruktory są wywoływane, gdy wystąpi jedno z następujących zdarzeń:

  • Lokalny (automatyczny) obiekt z zakresu bloku wykracza poza zakres.
  • Użyj delete aby zwolnić obiekt przydzielony przy użyciu new. Użycie delete[] prowadzi do niezdefiniowanego zachowania.
  • Użyj delete[] do zwolnienia obiektu przydzielonego przy użyciu new[]. Użycie delete prowadzi do niezdefiniowanego zachowania.
  • Kończy się okres istnienia obiektów tymczasowych.
  • Program się kończy, a obiekty globalne lub statyczne istnieją.
  • Destruktor jest jawnie wywoływany przy użyciu jego w pełni kwalifikowanej nazwy.

Destruktory mogą swobodnie wywoływać funkcje składowe klasy i uzyskać dostęp do danych składowych klasy.

Istnieją dwa ograniczenia dotyczące używania destruktorów:

  • Nie można zdobyć jego adresu.

  • Klasy pochodne nie dziedziczą destruktora klasy bazowej.

Kolejność zniszczenia

Gdy obiekt wykracza poza zakres lub jest usuwany, sekwencja zdarzeń w jego całkowitym zniszczeniu jest następująca:

  1. Destruktor klasy jest wywoływany, a treść funkcji destruktora jest wykonywana.

  2. Destruktory elementów członkowskich niestatycznych są wywoływane w odwrotnej kolejności, w której są zadeklarowane w deklaracji klasy. Opcjonalna lista inicjalizacji składowych używana przy konstruowaniu tych składowych nie ma wpływu na kolejność konstrukcji ani destrukcji.

  3. Destruktory klas bazowych niewirtualnych są wywoływane w kolejności odwrotnej do ich deklaracji.

  4. Destruktory dla wirtualnych klas bazowych są wywoływane w odwrotnej kolejności deklaracji.

// order_of_destruction.cpp
#include <cstdio>

struct A1      { virtual ~A1() { printf("A1 dtor\n"); } };
struct A2 : A1 { virtual ~A2() { printf("A2 dtor\n"); } };
struct A3 : A2 { virtual ~A3() { printf("A3 dtor\n"); } };

struct B1      { ~B1() { printf("B1 dtor\n"); } };
struct B2 : B1 { ~B2() { printf("B2 dtor\n"); } };
struct B3 : B2 { ~B3() { printf("B3 dtor\n"); } };

int main() {
   A1 * a = new A3;
   delete a;
   printf("\n");

   B1 * b = new B3;
   delete b;
   printf("\n");

   B3 * b2 = new B3;
   delete b2;
}
A3 dtor
A2 dtor
A1 dtor

B1 dtor

B3 dtor
B2 dtor
B1 dtor

Wirtualne klasy bazowe

Destruktory dla wirtualnych klas bazowych są wywoływane w odwrotnej kolejności ich wystąpień w skierowanym grafie acyklicznym (przechodzenie w głąb, od lewej do prawej, w porządku postorder). na poniższej ilustracji przedstawiono wykres dziedziczenia.

Wykres dziedziczenia przedstawiający wirtualne klasy bazowe.

Pięć klas, oznaczonych etykietą A przez E, jest rozmieszczonych na wykresie dziedziczenia. Klasa E jest klasą bazową B, C i D. Klasy C i D są klasą bazową A i B.

Poniżej wymieniono definicje klas dla klas pokazanych na rysunku:

class A {};
class B {};
class C : virtual public A, virtual public B {};
class D : virtual public A, virtual public B {};
class E : public C, public D, virtual public B {};

Aby określić kolejność zniszczenia wirtualnych klas bazowych obiektu typu E, kompilator tworzy listę, stosując następujący algorytm:

  1. Przejdź na lewo przez graf, zaczynając od najgłębszego punktu w grafie (w tym przypadku E).
  2. Wykonaj przechodzenie po lewej stronie, dopóki wszystkie węzły nie zostały odwiedzone. Zanotuj nazwę bieżącego węzła.
  3. Ponownie przejdź do poprzedniego węzła (w dół i po prawej stronie), aby dowiedzieć się, czy zapamiętany węzeł jest wirtualną klasą bazową.
  4. Jeśli zapamiętany węzeł jest wirtualną klasą bazową, przeskanuj listę, aby sprawdzić, czy został już wprowadzony. Jeśli nie jest to wirtualna klasa bazowa, zignoruj ją.
  5. Jeśli zapamiętany węzeł nie znajduje się jeszcze na liście, dodaj go do dołu listy.
  6. Przejdź po grafie do góry i na prawo, wzdłuż następnej ścieżki.
  7. Przejdź do kroku 2.
  8. Po wyczerpaniu ostatniej ścieżki w górę zanotuj nazwę bieżącego węzła.
  9. Przejdź do kroku 3.
  10. Kontynuuj ten proces, aż węzeł dolny zostanie ponownie węzłem bieżącym.

W związku z tym dla klasy Ekolejność zniszczenia to:

  1. Klasa bazowa niewirtualna E.
  2. Klasa bazowa niewirtualna D.
  3. Nie-wirtualna klasa bazowa C.
  4. Wirtualna klasa Bbazowa .
  5. Wirtualna klasa Abazowa .

Ten proces tworzy uporządkowaną listę unikatowych wpisów. Nazwa klasy nie jest wyświetlana dwa razy. Po skonstruowaniu listy jest ona przeglądana w odwrotnej kolejności i destruktor dla każdej z klas na liście, od ostatniego do pierwszego, jest wywoływany.

Kolejność tworzenia lub niszczenia jest przede wszystkim ważna, gdy konstruktory lub destruktory w jednej klasie polegają na tym, że drugi składnik musi być stworzony pierwszy lub trwać dłużej — na przykład, jeśli destruktor dla A (na wcześniej pokazanym rysunku) polegał na obecności B, gdy jego kod jest wykonywany, lub odwrotnie.

Takie współzależności między klasami w grafie dziedziczenia są z natury niebezpieczne, ponieważ klasy pochodne utworzone później mogą zmienić najbardziej lewostronną ścieżkę, zmieniając w ten sposób kolejność tworzenia i niszczenia.

Klasy bazowe inne niż wirtualne

Destruktory dla klas bazowych innych niż wirtualne są wywoływane w odwrotnej kolejności, w której są deklarowane nazwy klas bazowych. Rozważmy następującą deklarację klasy:

class MultInherit : public Base1, public Base2
...

W poprzednim przykładzie destruktor dla Base2 jest wywoływany przed destruktorem dla Base1.

Jawne wywołania destruktora

Jawne wywoływanie destruktora jest rzadko konieczne. Jednak może być przydatne czyszczenie obiektów umieszczonych w adresach bezwzględnych. Te obiekty są często przydzielane przy użyciu operatora zdefiniowanego przez użytkownika new, który przyjmuje argument lokalizacji. Operator delete nie może cofnąć przydziału tej pamięci, ponieważ nie jest przydzielony z bezpłatnego magazynu (aby uzyskać więcej informacji, zobacz Nowe i usuń operatory). Wywołanie destruktora może jednak wykonać odpowiednie czyszczenie. Aby jawnie wywołać destruktor dla obiektu , sklasy String, użyj jednej z następujących instrukcji:

s.String::~String();     // non-virtual call
ps->String::~String();   // non-virtual call

s.~String();       // Virtual call
ps->~String();     // Virtual call

Notacja jawnych wywołań destruktorów, pokazanych w poprzednim, może być używana niezależnie od tego, czy typ definiuje destruktora. Umożliwia to wykonywanie takich jawnych wywołań bez znajomości, czy destruktor jest zdefiniowany dla typu. Explicitne wywołanie destruktora, gdzie żaden nie jest zdefiniowany, nie ma wpływu.

Niezawodne programowanie

Klasa wymaga destruktora, jeśli uzyskuje zasób i aby bezpiecznie zarządzać zasobem, prawdopodobnie musi zaimplementować konstruktor kopiujący i przypisanie kopii.

Jeśli te specjalne funkcje nie są zdefiniowane przez użytkownika, są niejawnie zdefiniowane przez kompilator. Niejawnie generowane konstruktory i operatory przypisania wykonują płytkie kopiowanie członkowskie, co jest niemal na pewno błędne, jeśli obiekt zarządza zasobem.

W następnym przykładzie niejawnie wygenerowany konstruktor kopiujący sprawi, że wskaźniki str1.text i str2.text będą wskazywać na tę samą pamięć, a gdy wrócimy z copy_strings(), ta pamięć zostanie usunięta dwukrotnie, co prowadzi do zachowania niezdefiniowanego.

void copy_strings()
{
   String str1("I have a sense of impending disaster...");
   String str2 = str1; // str1.text and str2.text now refer to the same object
} // delete[] _text; deallocates the same memory twice
  // undefined behavior

Jawne definiowanie destruktora, konstruktora kopii lub operatora przypisania kopiowania uniemożliwia domyślną definicję konstruktora przenoszenia i operatora przypisania przenoszenia. W takim przypadku, brak zapewnienia operacji przenoszenia zazwyczaj oznacza straconą możliwość optymalizacji, zwłaszcza jeśli kopiowanie jest kosztowne.

Zobacz też

Konstruktory kopiujące i operatory przypisania
Konstruktory przenoszące i operatory przypisania przenoszącego