Sdílet prostřednictvím


Destruktory (C++)

Destruktor je členová funkce, která se vyvolá automaticky, když objekt zmizí z oboru nebo je explicitně zničen voláním delete nebo delete[]. Destruktor má stejný název jako třída a předchází tilda (~). Například destruktor třídy String je deklarován jako: ~String().

Pokud destruktor nedefinujete, kompilátor poskytne výchozí hodnotu a pro některé třídy to stačí. Je třeba definovat vlastní destruktor, když třída udržuje prostředky, které musí být explicitně uvolněny, jako jsou popisovače systémových prostředků nebo ukazatele na paměť, které by se měly uvolnit při zničení instance třídy.

Zvažte následující deklaraci třídy String:

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

V předchozím příkladu používá destruktor String::~Stringdelete[] operátor k uvolnění prostoru dynamicky přiděleného pro textové úložiště.

Deklarace destruktorů

Destruktory jsou funkce se stejným názvem jako třída, ale předchází tilda (~)

Deklarace destruktorů se řídí několika pravidly. Destruktory:

  • Nepřijímají argumenty.
  • Nevrací hodnotu (nebo void).
  • Nelze deklarovat jako const, volatilenebo static. Mohou však být vyvolány za zničení objektů deklarovaných jako const, volatilenebo static.
  • Lze deklarovat jako virtual. Pomocí virtuálních destruktorů můžete zničit objekty bez znalosti jejich typu – správný destruktor objektu se vyvolá pomocí mechanismu virtuální funkce. Destruktory lze také deklarovat jako čistě virtuální funkce pro abstraktní třídy.

Použití destruktorů

Destruktory jsou zavolány, pokud dojde k jedné z následujících událostí:

  • Místní (automatický) objekt s rozsahem bloku se dostane mimo rozsah.
  • Slouží delete k uvolnění objektu přiděleného pomocí new. Použití delete[] výsledků nedefinovaného chování.
  • Slouží delete[] k uvolnění objektu přiděleného pomocí new[]. Použití delete výsledků nedefinovaného chování.
  • Doba života dočasného objektu skončí.
  • Program se ukončí a globální nebo statické objekty existují.
  • Destruktor je explicitně zavolán pomocí plně kvalifikovaného názvu funkce destruktoru.

Destruktory mohou volně volat členské funkce třídy a přistupovat k datům člena třídy.

Použití destruktorů má dvě omezení:

  • Nemůžete vzít jeho adresu.

  • Odvozené třídy nedědí destruktor jejich základní třídy.

Pořadí zničení

Když objekt zmizí z rozsahu nebo je odstraněn, posloupnost událostí v jeho úplné zničení je následující:

  1. Je volána destruktor třídy a tělo funkce destruktoru je spuštěno.

  2. Destruktory pro nestatické členské objekty jsou volány v obráceném pořadí, ve kterém se zobrazují v deklaraci třídy. Volitelný seznam inicializace členů použitý při konstrukci těchto členů nemá vliv na pořadí konstrukce nebo zničení.

  3. Destruktory pro jiné než virtuální základní třídy se volají v obráceném pořadí deklarace.

  4. Destruktory virtuálních základních tříd se volají v obráceném pořadí deklarace.

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

Virtuální základní třídy

Destruktory virtuálních základních tříd se volají v obráceném pořadí jejich vzhledu v řízeném acyklickém grafu (hloubkově první, zleva doprava, procházení po pořadí). Následující obrázek znázorňuje graf dědičnosti.

Inheritance graph that shows virtual base classes.

Pět tříd označených jako A až E jsou uspořádány v grafu dědičnosti. Třída E je základní třídou B, C a D. Třídy C a D jsou základní třídou A a B.

Následující seznam uvádí definice tříd pro třídy zobrazené na obrázku:

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

Chcete-li určit pořadí zničení virtuálních základních tříd objektu typu E, kompilátor sestaví seznam pomocí následujícího algoritmu:

  1. Procházení grafu doleva, počínaje nejsouhlším bodem grafu (v tomto případě E).
  2. Proveďte procházení vlevo, dokud nebudou navštíveny všechny uzly. Poznamenejte si název aktuálního uzlu.
  3. Znovu se podívejte na předchozí uzel (dolů a vpravo), abyste zjistili, jestli je zapamatován uzel virtuální základní třídou.
  4. Pokud je zapamatovaný uzel virtuální základní třídou, zkontrolujte v seznamu, jestli už byl zadaný. Pokud není virtuální základní třídou, ignorujte ji.
  5. Pokud se zapamatovaný uzel ještě v seznamu nenachází, přidejte ho do dolní části seznamu.
  6. Projdete graf nahoru a podél další cesty doprava.
  7. Přejděte ke kroku 2.
  8. Když se poslední směrem nahoru vyčerpá, poznamenejte si název aktuálního uzlu.
  9. Přejděte ke kroku 3.
  10. Pokračujte v tomto procesu, dokud nebude dolní uzel znovu aktuálním uzlem.

Proto pro třídu E, pořadí zničení je:

  1. Ne virtuální základní třída E.
  2. Ne virtuální základní třída D.
  3. Ne virtuální základní třída C.
  4. Virtuální základní třída B.
  5. Virtuální základní třída A.

Tento proces vytvoří uspořádaný seznam jedinečných položek. Dvakrát se nezobrazí žádný název třídy. Jakmile je seznam vytvořen, je procházený v obráceném pořadí a je volán destruktor pro každou třídu v seznamu od posledního po první.

Pořadí konstrukce nebo zničení je primárně důležité, když konstruktory nebo destruktory v jedné třídě spoléhají na to, že druhá komponenta je vytvořena jako první nebo trvale delší – například pokud destruktor pro A (na obrázku zobrazeném dříve) spoléhá na B to, že při spuštění jeho kódu nebo naopak.

Takové vzájemné závislosti mezi třídami v grafu dědičnosti jsou ze své podstaty nebezpečné, protože třídy odvozené později mohou změnit, což je levá cesta, čímž se mění pořadí konstrukce a zničení.

Jiné než virtuální základní třídy

Destruktory pro jiné než virtuální základní třídy jsou volána v obráceném pořadí, ve kterém jsou deklarovány názvy základních tříd. Zvažte následující deklaraci třídy:

class MultInherit : public Base1, public Base2
...

V předchozím příkladu je destruktor volána Base2 před destruktoru pro Base1.

Explicitní volání destruktoru

Explicitní volání destruktoru je zřídka nezbytné. Avšak může být užitečné pro vyčištění objektů umístěných na absolutních adresách. Tyto objekty se běžně přidělují pomocí uživatelem definovaného new operátoru, který přebírá argument umístění. Operátor delete nemůže uvolnit tuto paměť, protože není přidělený z bezplatného úložiště (další informace najdete v tématu Nové a odstraňovací operátory). Volání destruktoru však může provádět odpovídající vyčištění. Chcete-li explicitně zavolat destruktor objektu s třídy String, použijte jeden z následujících příkazů:

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

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

Zápis explicitního volání destruktorů, ukázaný v předchozím příkladu, lze použít bez ohledu na to, zda typ destruktor definuje. To umožňuje provést explicitní volání bez znalosti, zda je definován destruktor typu. Explicitní volání destruktoru, který není definován, nemá žádný vliv.

Robustní programování

Třída potřebuje destruktor, pokud získá prostředek, a ke správě prostředku bezpečně musí implementovat konstruktor kopírování a přiřazení kopie.

Pokud uživatel tyto speciální funkce nedefinuje, jsou implicitně definovány kompilátorem. Implicitně generované konstruktory a operátory přiřazení provádějí mělké členové kopie, což je téměř jistě chybné, pokud objekt spravuje prostředek.

V dalším příkladu implicitně vygenerovaný konstruktor kopírování vytvoří ukazatele str1.text a str2.text odkazuje na stejnou paměť, a když se vrátíme z copy_strings(), tato paměť se odstraní dvakrát, což je nedefinované chování:

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

Explicitní definování destruktoru, konstruktoru kopírování nebo operátoru přiřazení kopírování brání implicitní definici konstruktoru přesunutí a operátoru přiřazení přesunutí. V takovém případě je neúspěšné poskytování operací přesunutí obvykle, pokud kopírování je nákladné, zmeškanou příležitost optimalizace.

Viz také

Konstruktory a operátory přiřazení pro kopírování
Konstruktory a operátory přiřazení pro přesunutí