Distruttori (C++)

Un distruttore è una funzione membro richiamata automaticamente quando l'oggetto esce dall'ambito o viene eliminato in modo esplicito da una chiamata a delete o delete[]. Un distruttore ha lo stesso nome della classe ed è preceduto da una tilde (~). Ad esempio, il distruttore per la classe String viene dichiarato con ~String().

Se non si definisce un distruttore, il compilatore ne fornisce uno predefinito e per alcune classi è sufficiente. È necessario definire un distruttore personalizzato quando la classe gestisce risorse che devono essere rilasciate in modo esplicito, ad esempio handle per le risorse di sistema o i puntatori alla memoria che devono essere rilasciati quando un'istanza della classe viene eliminata definitivamente.

Si consideri la dichiarazione seguente di una classe 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++");
}

Nell'esempio precedente, il distruttore String::~String usa l'operatore delete[] per deallocare lo spazio allocato dinamicamente per l'archiviazione del testo.

Dichiarazione di distruttori

I distruttori sono funzioni con lo stesso nome della classe ma preceduti da un carattere tilde (~).

Diverse regole controllano la dichiarazione di distruttori. I distruttori:

  • Non accettare argomenti.
  • Non restituire un valore (o void).
  • Non è possibile dichiarare come const, volatileo static. Tuttavia, possono essere richiamati per la distruzione di oggetti dichiarati come const, volatileo static.
  • Può essere dichiarato come virtual. Usando i distruttori virtuali, è possibile eliminare definitivamente gli oggetti senza conoscerne il tipo. Il distruttore corretto per l'oggetto viene richiamato usando il meccanismo di funzione virtuale. I distruttori possono anche essere dichiarati come funzioni virtuali pure per le classi astratte.

Uso di distruttori

I distruttori vengono chiamati quando si verifica uno degli eventi seguenti:

  • Un oggetto (automatico) locale con ambito del blocco diventa esterno all'ambito.
  • Usare delete per deallocare un oggetto allocato usando new. L'uso delete[] di restituisce un comportamento non definito.
  • Usare delete[] per deallocare un oggetto allocato usando new[]. L'uso delete di restituisce un comportamento non definito.
  • La durata di un oggetto temporaneo termina.
  • Un programma termina e gli oggetti globali o statici sono presenti.
  • Il distruttore viene chiamato in modo esplicito utilizzando il nome completo della funzione distruttore.

I distruttori possono chiamare liberamente le funzioni membro di classe e accedere ai dati membro di classe.

Esistono due restrizioni per l'uso di distruttori:

  • Non puoi prenderne l'indirizzo.

  • Le classi derivate non ereditano il distruttore della relativa classe di base.

Ordine di distruzione

Quando un oggetto esce dall'ambito o viene eliminato, la sequenza di eventi nella relativa distruzione completa è la seguente:

  1. Il distruttore della classe viene chiamato e il corpo della funzione distruttore viene eseguito.

  2. I distruttori per gli oggetti membri non statici vengono chiamati in ordine inverso in cui appaiono nella dichiarazione di classe. L'elenco facoltativo di inizializzazione dei membri utilizzato per la costruzione di questi membri non influisce sull'ordine di costruzione o distruzione.

  3. I distruttori per le classi di base non virtuali vengono chiamati nell'ordine inverso della dichiarazione.

  4. I distruttori per le classi base virtuali vengono chiamati in ordine inverso di dichiarazione.

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

Classi di base virtuali

I distruttori delle classi base virtuali vengono chiamati in ordine inverso, rispetto a come appaiono in un grafico aciclico diretto (prima quelli che si trovano in profondità, da sinistra a destra, attraversamento post-ordine). La figura seguente rappresenta un grafico di ereditarietà.

Inheritance graph that shows virtual base classes.

Cinque classi, etichettate da A a E, sono disposte in un grafico di ereditarietà. La classe E è la classe base di B, C e D. Le classi C e D sono la classe base di A e B.

Di seguito sono elencate le definizioni di classe per le classi illustrate nella figura:

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

Per determinare l'ordine di distruzione delle classi base virtuali di un oggetto di tipo E, il compilatore genera un elenco applicando l'algoritmo seguente:

  1. Attraversare il grafico a sinistra, a partire dal punto più profondo nel grafico, in questo caso, E.
  2. Eseguire attraversamenti verso sinistra, fino a che non siano stati visti tutti i nodi. Annotare il nome del nodo corrente.
  3. Rivedere il nodo precedente (in basso a destra) per determinare se il nodo memorizzato è una classe base virtuale.
  4. Se il nodo memorizzato è una classe base virtuale, analizzare l'elenco per verificare se sia già stata inserita. Se non è una classe di base virtuale, ignorarla.
  5. Se il nodo memorizzato non è ancora presente nell'elenco, aggiungerlo alla fine dell'elenco.
  6. Attraversare il grafico verso l'alto e lungo il successivo percorso a destra.
  7. Andare al passaggio 2.
  8. Quando viene esaurito l'ultimo percorso verso l'alto, annotare il nome del nodo corrente.
  9. Andare al passaggio 3.
  10. Continuare questo processo finché l'ultimo nodo non sia nuovamente il nodo corrente.

Di conseguenza, per la classe E, l'ordine di distruzione è:

  1. Classe base Enon virtuale .
  2. Classe base Dnon virtuale .
  3. Classe base Cnon virtuale .
  4. Classe base virtuale B.
  5. Classe base virtuale A.

Questo processo genera un elenco ordinato di voci univoche. Nessun nome di classe viene visualizzato due volte. Una volta costruito l'elenco, viene camminato in ordine inverso e viene chiamato il distruttore per ognuna delle classi nell'elenco dall'ultimo al primo.

L'ordine di costruzione o distruzione è importante soprattutto quando i costruttori o i distruttori in una classe si basano sull'altro componente creato per primo o persistente, ad esempio se il distruttore per A (nella figura illustrata in precedenza) si basava sulla B presenza del codice quando il codice viene eseguito o viceversa.

Tali interdipendenze tra le classi in un grafico di ereditarietà sono di per sé rischiose, perché le classi derivate in seguito possono modificare la nozione di percorso più a sinistra, alterando, in questo modo, l'ordine di costruzione e distruzione.

Classi di base non virtuali

I distruttori per le classi di base non virtuali vengono chiamati nell'ordine inverso in cui vengono dichiarati i nomi delle classi di base. Si consideri la seguente dichiarazione di classe:

class MultInherit : public Base1, public Base2
...

Nell'esempio precedente, il distruttore di Base2 viene chiamato prima del distruttore di Base1.

Chiamate del distruttore esplicite

La chiamata di un distruttore in modo esplicito è raramente necessaria. Tuttavia, può essere utile eseguire la pulizia di oggetti inseriti in corrispondenza di indirizzi assoluti. Questi oggetti vengono comunemente allocati usando un operatore definito dall'utente new che accetta un argomento di posizionamento. L'operatore delete non può deallocare questa memoria perché non viene allocata dall'archivio gratuito . Per altre informazioni, vedere Operatori nuovi ed eliminati. Una chiamata al distruttore è tuttavia in grado di eseguire una pulizia appropriata. Per chiamare in modo esplicito il distruttore di un oggetto, s, di classe String, utilizzare una delle seguenti istruzioni:

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

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

La notazione per le chiamate esplicite ai distruttori, illustrata nel passaggio precedente, può essere usata indipendentemente dal fatto che il tipo definisca un distruttore. Ciò consente di effettuare tali chiamate esplicite senza sapere se per tale tipo venga definito un distruttore. Una chiamata esplicita a un distruttore in cui non viene definito alcun distruttore non produce alcun effetto.

Programmazione efficiente

Una classe richiede un distruttore se acquisisce una risorsa e per gestire la risorsa in modo sicuro, probabilmente deve implementare un costruttore di copia e un'assegnazione di copia.

Se queste funzioni speciali non sono definite dall'utente, vengono definite in modo implicito dal compilatore. I costruttori generati in modo implicito e gli operatori di assegnazione eseguono una copia superficiale e membro per membro, che è quasi certamente errata se un oggetto gestisce una risorsa.

Nell'esempio successivo il costruttore di copia generato in modo implicito renderà i puntatori str1.text e str2.text farà riferimento alla stessa memoria e, quando si restituisce da copy_strings(), tale memoria verrà eliminata due volte, ovvero un comportamento non definito:

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

La definizione esplicita di un distruttore, un costruttore di copia o un operatore di assegnazione di copia impedisce la definizione implicita del costruttore di spostamento e dell'operatore di assegnazione di spostamento. In questo caso, l'errore di fornire operazioni di spostamento è in genere, se la copia è costosa, un'opportunità di ottimizzazione non riuscita.

Vedi anche

Costruttori di copia e operatori di assegnazione copia
Costruttori di spostamento e operatori di assegnazione spostamento