Delen via


Destructors (C++)

Een destructor is een lidfunctie die automatisch wordt aangeroepen wanneer het object buiten het bereik valt of expliciet wordt vernietigd door een aanroep naar delete of delete[]. Een destructor heeft dezelfde naam als de klasse en wordt voorafgegaan door een tilde (~). De destructor voor klasse String wordt bijvoorbeeld gedeclareerd: ~String().

Als u geen destructor definieert, biedt de compiler een standaardwaarde en voor sommige klassen is dit voldoende. U moet een aangepaste destructor definiëren wanneer de klasse resources onderhoudt die expliciet moeten worden vrijgegeven, zoals ingangen naar systeemresources of aanwijzers naar geheugen die moeten worden vrijgegeven wanneer een exemplaar van de klasse wordt vernietigd.

Houd rekening met de volgende declaratie van een String klasse:

// 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++");
}

In het voorgaande voorbeeld gebruikt de destructor String::~String de delete[] operator om de ruimte die dynamisch aan tekstopslag is toegewezen vrij te maken.

Destructors declareren

Destructors zijn functies met dezelfde naam als de klasse, maar voorafgegaan door een tilde (~)

Verschillende regels zijn van toepassing op de declaratie van destructors. Destructors:

  • Accepteer geen argumenten.
  • Geef geen waarde terug (of void).
  • Kan niet worden gedeclareerd als const, volatileof static. Ze kunnen echter worden aangeroepen voor de vernietiging van objecten die zijn aangegeven als const, volatileof static.
  • Kan worden gedeclareerd als virtual. Met behulp van virtuele destructors kunt u objecten vernietigen zonder hun type te kennen. De juiste destructor voor het object wordt aangeroepen met behulp van het mechanisme van de virtuele functie. Destructors kunnen ook worden gedeclareerd als pure virtuele functies voor abstracte klassen.

Destructors gebruiken

Destructors worden aangeroepen wanneer een van de volgende gebeurtenissen plaatsvindt:

  • Een lokaal (automatisch) object met blokbereik verlaat zijn scope.
  • Gebruik delete om een object vrij te geven dat gealloceerd is met new. Het gebruik van delete[] leidt tot ongedefinieerd gedrag.
  • Gebruik delete[] om een object vrij te geven dat gealloceerd is met new[]. Het gebruik van delete leidt tot ongedefinieerd gedrag.
  • De levensduur van een tijdelijk object eindigt.
  • Een programma eindigt en er bestaan globale of statische objecten.
  • De destructor wordt expliciet aangeroepen met behulp van de volledig gekwalificeerde naam van de destructorfunctie.

Destructors kunnen klasselidfuncties vrijelijk aanroepen en toegang krijgen tot klasselidgegevens.

Er gelden twee beperkingen voor het gebruik van destructors:

  • Je kunt het adres niet aannemen.

  • Afgeleide klassen nemen de destructor van hun basisklasse niet over.

Vernietigingsvolgorde

Wanneer een object buiten het bereik valt of wordt verwijderd, is de volgorde van gebeurtenissen in de volledige vernietiging als volgt:

  1. De destructor van de klasse wordt aangeroepen en de inhoud van de destructorfunctie wordt uitgevoerd.

  2. Destructors voor niet-statische lidobjecten worden aangeroepen in de omgekeerde volgorde waarin ze worden weergegeven in de klassedeclaratie. De optionele initialisatielijst die wordt gebruikt bij de constructie van deze leden heeft geen invloed op de volgorde van constructie of vernietiging.

  3. Destructors voor niet-virtuele basisklassen worden aangeroepen in de omgekeerde volgorde van de declaratie.

  4. Destructors voor virtuele basisklassen worden aangeroepen in de omgekeerde volgorde van de declaratie.

// 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

Virtuele basisklassen

Destructors voor virtuele basisklassen worden aangeroepen in de omgekeerde volgorde van hun uiterlijk in een gerichte acyclische grafiek (diepte-eerst, van links naar rechts, doorkruising van postorder). in de volgende afbeelding ziet u een overnamegrafiek.

Overnamegrafiek met virtuele basisklassen.

Vijf klassen, met het label A tot en met E, worden gerangschikt in een overnamegrafiek. Klasse E is de basisklasse B, C en D. Klassen C en D zijn de basisklasse A en B.

Hieronder ziet u de klassedefinities voor de klassen die worden weergegeven in de afbeelding:

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 {};

Om de volgorde van vernietiging van de virtuele basisklassen van een object van het type Ete bepalen, bouwt de compiler een lijst door het volgende algoritme toe te passen:

  1. Doorkruis de grafiek links, beginnend bij het uiterste punt in de grafiek (in dit geval E).
  2. Voer linkse doorkruisingen uit totdat alle knooppunten zijn bezocht. Noteer de naam van het huidige knooppunt.
  3. Ga terug naar het vorige knooppunt (omlaag en rechts) om erachter te komen of het knooppunt dat wordt onthouden een virtuele basisklasse is.
  4. Als het onthouden knooppunt een virtuele basisklasse is, scant u de lijst om te zien of het al is ingevoerd. Als het geen virtuele basisklasse is, negeert u deze.
  5. Als het onthouden knooppunt nog niet in de lijst staat, voegt u het toe aan het einde van de lijst.
  6. Doorloop de grafiek omhoog en langs het volgende pad naar rechts.
  7. Ga naar stap 2.
  8. Wanneer het laatste pad naar boven is uitgeput, noteert u de naam van het huidige knooppunt.
  9. Ga naar stap 3.
  10. Ga door met dit proces totdat het onderste knooppunt opnieuw het huidige knooppunt is.

Daarom is voor klasse Ede orde van vernietiging:

  1. De niet-virtuele basisklasse E.
  2. De niet-virtuele basisklasse D.
  3. De niet-virtuele basisklasse C.
  4. De virtuele basisklasse B.
  5. De virtuele basisklasse A.

Dit proces produceert een geordende lijst met unieke vermeldingen. Geen klassenaam verschijnt twee keer. Zodra de lijst is samengesteld, wordt deze in omgekeerde volgorde doorlopen en wordt de destructor voor elk van de klassen in de lijst van de laatste tot de eerste aangeroepen.

De volgorde van constructie of destructie is vooral belangrijk wanneer constructors of destructors in de ene klasse afhankelijk zijn van het feit dat de andere component eerst wordt gemaakt of langer blijft bestaan. Bijvoorbeeld, als de destructor voor A (zoals eerder in de afbeelding getoond) afhankelijk zou zijn van de aanwezigheid van B op het moment dat de code wordt uitgevoerd, of omgekeerd.

Dergelijke onderlinge afhankelijkheden tussen klassen in een overnamegrafiek zijn inherent gevaarlijk, omdat klassen die later worden afgeleid, kunnen veranderen wat het meest linkse pad is, waardoor de volgorde van constructie en vernietiging wordt gewijzigd.

Niet-virtuele basisklassen

De destructors voor niet-virtuele basisklassen worden aangeroepen in de omgekeerde volgorde waarin de namen van de basisklassen worden gedeclareerd. Houd rekening met de volgende klassedeclaratie:

class MultInherit : public Base1, public Base2
...

In het voorgaande voorbeeld wordt de destructor voor Base2 aangeroepen vóór de destructor voor Base1.

Expliciete aanroepen van de destructor

Het aanroepen van een destructor is zelden nodig. Het kan echter handig zijn om objecten die op absolute adressen zijn geplaatst, op te ruimen. Deze objecten worden meestal toegewezen met behulp van een door de gebruiker gedefinieerde new operator die een plaatsingsargument gebruikt. De delete-operator kan dit geheugen niet vrijgeven omdat het niet is toegewezen vanuit de vrije opslagruimte (zie De new- en delete-operators voor meer informatie). Een aanroep naar de destructor kan echter de juiste opschoning uitvoeren. Als u de destructor voor een object, s, van klasse String expliciet wilt aanroepen, gebruikt u een van de volgende instructies:

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

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

De notatie voor expliciete aanroepen naar destructors, zoals in het voorgaande wordt weergegeven, kan worden gebruikt, ongeacht of het type een destructor definieert. Hierdoor kunt u dergelijke expliciete aanroepen doen zonder te weten of er een destructor is gedefinieerd voor het type. Een expliciete aanroep van een destructor waarbij er geen is gedefinieerd, heeft geen effect.

Robuuste programmering

Een klasse heeft een destructor nodig als deze een resource verkrijgt en om de resource veilig te beheren, moet deze waarschijnlijk een kopieerconstructor en een kopieertoewijzing implementeren.

Als deze speciale functies niet door de gebruiker worden gedefinieerd, worden ze impliciet gedefinieerd door de compiler. De impliciet gegenereerde constructors en toewijzingsoperatoren voeren ondiepe, lidgewijze kopie uit. Dit is bijna zeker onjuist als een object een resource beheert.

In het volgende voorbeeld zal de impliciet gegenereerde kopieerconstructor de aanwijzers str1.text en str2.text naar hetzelfde geheugen laten verwijzen. Wanneer we terugkeren van copy_strings(), wordt dat geheugen twee keer verwijderd, wat tot ongedefinieerd gedrag leidt.

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

Als u expliciet een destructor, kopieerconstructor of kopieertoewijzingsoperator definieert, voorkomt u impliciete definitie van de verplaatsingsconstructor en de operator voor de verplaatsingstoewijzing. In dit geval is het mislukken van verplaatsingsbewerkingen meestal, als kopiëren duur is, een gemiste optimalisatiekans.

Zie ook

Kopieconstructors en toewijzingsoperatoren
Constructors verplaatsen en toewijzingsoperatoren verplaatsen