Menu

Distruttori in C++: ~ClassName, RAII e pulizia

Un distruttore viene eseguito automaticamente quando un oggetto viene distrutto. Scopri la sintassi ~ClassName(), quando scatta, perché libera le risorse e la Regola del Tre/Cinque.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Cos'è un distruttore

Nella pagina precedente hai visto i costruttori, funzioni speciali che vengono eseguite quando un oggetto nasce per impostarne lo stato iniziale. Un distruttore è la sua immagine speculare: una funzione speciale che viene eseguita quando un oggetto muore, per fare pulizia.

Lo dichiari con il nome della classe preceduto da una tilde (~). Non riceve parametri, non restituisce nulla e una classe può averne esattamente uno. Non lo chiami quasi mai a mano: è il C++ a chiamarlo per te al momento giusto.

Nota che il messaggio del distruttore viene stampato dopo che main ha finito il suo corpo, ma prima che il programma termini. Quando log esce dallo scope alla graffa di chiusura, il C++ esegue ~Logger() per te.

Quando vengono eseguiti i distruttori

Il momento esatto dipende da dove vive l'oggetto:

  • Gli oggetti sullo stack (locali) vengono distrutti quando escono dallo scope, alla } di chiusura del blocco.
  • Gli oggetti sull'heap (creati con new) vengono distrutti quando chiami delete. Se dimentichi il delete, il distruttore non viene mai eseguito e hai un memory leak.

Questo esempio rende visibile la differenza:

Gli oggetti vengono distrutti in ordine inverso rispetto alla costruzione. a è stato costruito per primo, quindi muore per ultimo. Questo ordine LIFO (last-in, first-out) conta quando gli oggetti dipendono l'uno dall'altro.

Perché i distruttori contano: RAII

La vera forza dei distruttori è che rendono la pulizia automatica e sicura rispetto alle eccezioni. Invece di ricordarti di rilasciare una risorsa in ogni percorso del codice, metti il rilascio in un distruttore e lasci che sia il linguaggio a garantirne l'esecuzione. Questo schema si chiama RAII, Resource Acquisition Is Initialization, ed è la spina dorsale del C++ moderno.

Qui una classe possiede un buffer sull'heap: lo alloca nel costruttore e lo libera nel distruttore, così chi la usa non tocca mai new/delete direttamente.

Il punto chiave: anche se venisse lanciata un'eccezione dopo la creazione di squares, lo stack verrebbe srotolato e ~IntArray() verrebbe comunque eseguito. Questa garanzia è il motivo per cui RAII è così affidabile, e per cui nel buon codice C++ raramente scrivi un delete diretto.

La Regola del Tre (e del Cinque)

Una classe con un distruttore personalizzato quasi sempre possiede una risorsa grezza, e questo crea un pericolo nascosto. Il costruttore di copia e l'assegnazione per copia generati dal compilatore fanno una copia superficiale: copiano il puntatore, non il buffer a cui punta. Ora due oggetti hanno lo stesso puntatore, e entrambi i distruttori faranno delete su di esso, causando un crash da double free.

IntArray a(5);
IntArray b = a;   // copia superficiale: a.data e b.data sono lo STESSO puntatore
// alla fine dello scope: il distruttore di b libera il buffer,
// poi il distruttore di a lo libera DI NUOVO -> comportamento indefinito (double free)

Da qui nasce la Regola del Tre: se scrivi uno qualsiasi tra distruttore, costruttore di copia o operatore di assegnazione per copia, quasi sicuramente ti servono tutti e tre. In C++11 e successivi si estende alla Regola del Cinque, che aggiunge il costruttore di spostamento e l'assegnazione per spostamento.

C'è però una regola ancora migliore, la Regola dello Zero: progetta le classi in modo da non gestire affatto risorse grezze. Usa invece un std::vector, una std::string o uno smart pointer, e il distruttore generato dal compilatore farà la cosa giusta gratis.

Parti dalla Regola dello Zero come impostazione predefinita. Scrivi un distruttore personalizzato solo quando possiedi davvero una risorsa grezza che nessun tipo standard gestisce per te.

Distruttori virtuali

Quando elimini un oggetto tramite un puntatore alla classe base, il distruttore deve essere virtual, altrimenti viene distrutta solo la parte base e la parte derivata resta in memoria. È uno dei bug più comuni nel codice polimorfico, e di default il compilatore non ti avvisa.

Senza virtual su ~Base, delete p chiamerebbe solo ~Base(): comportamento indefinito, e la parte Derived dell'oggetto non verrebbe mai ripulita. Regola pratica: qualsiasi classe con funzioni virtuali (una classe base polimorfica) ha bisogno di un distruttore virtuale. Vedrai esattamente perché conta non appena inizierai a derivare classi.

Errori comuni e insidie

Alcune trappole mettono in difficoltà quasi tutti:

new/delete non corrispondenti. Se allochi con new[], libera con delete[]. Mescolare new[] con un semplice delete (o viceversa) è comportamento indefinito.

Dimenticare virtual sul distruttore della base. Come visto sopra, eliminare un oggetto derivato tramite un puntatore alla base senza distruttore virtuale lascia in memoria la parte derivata. Se scrivi una classe pensata per essere ereditata, rendi virtuale il distruttore.

Lasciare che le eccezioni escano da un distruttore. Un distruttore che lancia un'eccezione durante lo srotolamento dello stack termina il programma. Nel C++ moderno i distruttori sono implicitamente noexcept: fai in modo che il codice di pulizia non lanci eccezioni, oppure intercettale dentro il distruttore.

Scrivere un distruttore che non serve. Se i tuoi membri fanno già pulizia da soli, un ~ClassName() {} vuoto aggiunge rumore e può disattivare in silenzio le operazioni di spostamento. Quando non c'è nulla da ripulire, non scrivere alcun distruttore.

Prossimo: Ereditarietà

Hai visto l'intero ciclo di vita di un oggetto: i costruttori lo fanno nascere, i distruttori lo ripuliscono e i distruttori virtual mantengono corretta quella pulizia quando una classe si basa su un'altra. Quest'ultimo punto anticipa la prossima grande idea, l'ereditarietà, in cui una classe riusa ed estende i dati e il comportamento di un'altra. La prossima pagina mostra come derivare una classe da un'altra, come costruzione e distruzione si concatenano lungo la gerarchia e come si incastrano i pezzi che hai appena imparato.

Domande frequenti

Cos'è un distruttore in C++?

Un distruttore è una funzione membro speciale chiamata ~ClassName() che viene eseguita automaticamente quando un oggetto viene distrutto, cioè quando esce dallo scope o quando lo elimini con delete. Il suo compito è fare pulizia: liberare memoria, chiudere file o rilasciare qualsiasi risorsa posseduta dall'oggetto. Non riceve parametri, non ha tipo di ritorno e una classe può averne uno solo.

Quando viene eseguito un distruttore in C++?

Per un oggetto locale (sullo stack), il distruttore viene eseguito quando esce dallo scope, alla } di chiusura. Per un oggetto sull'heap creato con new, viene eseguito quando chiami delete. Membri e classi base vengono distrutti automaticamente subito dopo, in ordine inverso rispetto alla costruzione.

Devo sempre scrivere un distruttore in C++?

No. Se la tua classe contiene solo membri che fanno pulizia da soli (come std::string, std::vector o gli smart pointer), il distruttore generato dal compilatore basta: non scriverne uno. Ti serve un distruttore personalizzato solo quando la classe possiede una risorsa grezza, come memoria ottenuta con new o un file aperto.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA