Quando viene chiamato un distruttore C ++?


118

Domanda di base: quando un programma chiama il metodo distruttore di una classe in C ++? Mi è stato detto che viene chiamato ogni volta che un oggetto esce dall'ambito o è soggetto a un filedelete

Domande più specifiche:

1) Se l'oggetto viene creato tramite un puntatore e quel puntatore viene successivamente cancellato o gli viene assegnato un nuovo indirizzo a cui puntare, l'oggetto a cui puntava chiama il suo distruttore (assumendo che nient'altro lo stia puntando)?

2) Seguendo la domanda 1, cosa definisce quando un oggetto esce dallo scope (non riguardo a quando un oggetto lascia un dato {blocco}). Quindi, in altre parole, quando viene chiamato un distruttore su un oggetto in un elenco collegato?

3) Vorresti mai chiamare manualmente un distruttore?


3
Anche le tue domande specifiche sono troppo generiche. "Quel puntatore viene successivamente cancellato" e "gli viene assegnato un nuovo indirizzo a cui puntare" sono molto diversi. Cerca di più (ad alcune di queste è stata data risposta), quindi fai domande separate per le parti che non sei riuscito a trovare.
Matthew Flaschen

Risposte:


74

1) Se l'oggetto viene creato tramite un puntatore e quel puntatore viene successivamente cancellato o gli viene assegnato un nuovo indirizzo a cui puntare, l'oggetto a cui puntava chiama il suo distruttore (assumendo che nient'altro lo stia puntando)?

Dipende dal tipo di puntatori. Ad esempio, i puntatori intelligenti spesso eliminano i propri oggetti quando vengono eliminati. I puntatori ordinari non lo fanno. Lo stesso vale quando si fa in modo che un puntatore punti a un oggetto diverso. Alcuni puntatori intelligenti distruggeranno il vecchio oggetto o lo distruggeranno se non ha più riferimenti. I puntatori ordinari non hanno tali intelligenze. Contengono solo un indirizzo e consentono di eseguire operazioni sugli oggetti a cui puntano in modo specifico.

2) Seguendo la domanda 1, cosa definisce quando un oggetto esce dallo scope (non riguardo a quando un oggetto lascia un dato {blocco}). Quindi, in altre parole, quando viene chiamato un distruttore su un oggetto in un elenco collegato?

Dipende dall'implementazione dell'elenco collegato. Le raccolte tipiche distruggono tutti gli oggetti contenuti quando vengono distrutti.

Quindi, un elenco collegato di puntatori normalmente distruggerebbe i puntatori ma non gli oggetti a cui puntano. (Il che potrebbe essere corretto. Potrebbero essere riferimenti da altri puntatori.) Un elenco collegato specificamente progettato per contenere puntatori, tuttavia, potrebbe eliminare gli oggetti da solo.

Un elenco collegato di puntatori intelligenti potrebbe eliminare automaticamente gli oggetti quando i puntatori vengono eliminati o farlo se non avevano più riferimenti. Sta a te scegliere i pezzi che fanno quello che vuoi.

3) Vorresti mai chiamare manualmente un distruttore?

Sicuro. Un esempio potrebbe essere se si desidera sostituire un oggetto con un altro oggetto dello stesso tipo ma non si desidera liberare memoria solo per allocare di nuovo. Puoi distruggere il vecchio oggetto sul posto e costruirne uno nuovo sul posto. (Tuttavia, generalmente questa è una cattiva idea.)

// pointer is destroyed because it goes out of scope,
// but not the object it pointed to. memory leak
if (1) {
 Foo *myfoo = new Foo("foo");
}


// pointer is destroyed because it goes out of scope,
// object it points to is deleted. no memory leak
if(1) {
 Foo *myfoo = new Foo("foo");
 delete myfoo;
}

// no memory leak, object goes out of scope
if(1) {
 Foo myfoo("foo");
}

2
Pensavo che l'ultimo dei tuoi esempi dichiarasse una funzione? È un esempio della "analisi più irritante". (L'altro punto più banale è che immagino tu intendessi new Foo()con la 'F' maiuscola.)
Stuart Golodetz

1
Penso che Foo myfoo("foo")non sia Most Vexing Parse, ma lo char * foo = "foo"; Foo myfoo(foo);è.
Cosine

Potrebbe essere una domanda stupida, ma non dovrebbe delete myFooessere chiamata prima Foo *myFoo = new Foo("foo");? Oppure dovresti eliminare l'oggetto appena creato, no?
Matheus Rocha

Non c'è myFooprima della Foo *myFoo = new Foo("foo");linea. Quella linea crea una nuova variabile chiamata myFoo, che ombreggia qualsiasi esistente. Anche se in questo caso non ce n'è uno esistente poiché quanto myFoosopra è nell'ambito di if, che è terminato.
David Schwartz,

1
@galactikuh Un "puntatore intelligente" è qualcosa che agisce come un puntatore a un oggetto ma che ha anche caratteristiche che rendono più facile gestire la durata di quell'oggetto.
David Schwartz

20

Altri hanno già affrontato gli altri problemi, quindi guarderò solo a un punto: vuoi mai eliminare manualmente un oggetto.

La risposta è si. @DavidSchwartz ha fornito un esempio, ma è abbastanza insolito. Darò un esempio che è sotto il cofano di ciò che molti programmatori C ++ usano tutto il tempo: std::vector(e std::deque, sebbene non sia usato così tanto).

Come la maggior parte delle persone sa, std::vectorassegnerà un blocco di memoria più grande quando / se si aggiungono più elementi di quelli che la sua allocazione corrente può contenere. Quando lo fa, tuttavia, ha un blocco di memoria che è in grado di contenere più oggetti di quanti ne siano attualmente nel vettore.

Per gestirlo, ciò che vectorfa sotto le coperte è allocare memoria grezza tramite l' Allocatoroggetto (che, a meno che non specifichi diversamente, significa che utilizza ::operator new). Quindi, quando si utilizza (ad esempio) push_backper aggiungere un elemento a vector, internamente il vettore utilizza a placement newper creare un elemento nella parte (precedentemente) inutilizzata del suo spazio di memoria.

Ora, cosa succede quando / se eraseun elemento dal vettore? Non può semplicemente usare delete- questo libererebbe il suo intero blocco di memoria; deve distruggere un oggetto in quella memoria senza distruggerne gli altri, o liberare nessuno dei blocchi di memoria che controlla (ad esempio, se si erase5 elementi da un vettore, quindi immediatamente push_backaltri 5 elementi, è garantito che il vettore non si riallocherà memoria quando lo fai.

Per fare ciò, il vettore distrugge direttamente gli oggetti nella memoria chiamando esplicitamente il distruttore, non usando delete.

Se, forse, qualcun altro dovesse scrivere un contenitore usando l'archiviazione contigua all'incirca come vectorfa un (o una sua variante, come std::dequefa in realtà), quasi certamente vorrai usare la stessa tecnica.

Solo per esempio, consideriamo come potresti scrivere codice per un ring-buffer circolare.

#ifndef CBUFFER_H_INC
#define CBUFFER_H_INC

template <class T>
class circular_buffer {
    T *data;
    unsigned read_pos;
    unsigned write_pos;
    unsigned in_use;
    const unsigned capacity;
public:
    circular_buffer(unsigned size) :
        data((T *)operator new(size * sizeof(T))),
        read_pos(0),
        write_pos(0),
        in_use(0),
        capacity(size)
    {}

    void push(T const &t) {
        // ensure there's room in buffer:
        if (in_use == capacity) 
            pop();

        // construct copy of object in-place into buffer
        new(&data[write_pos++]) T(t);
        // keep pointer in bounds.
        write_pos %= capacity;
        ++in_use;
    }

    // return oldest object in queue:
    T front() {
        return data[read_pos];
    }

    // remove oldest object from queue:
    void pop() { 
        // destroy the object:
        data[read_pos++].~T();

        // keep pointer in bounds.
        read_pos %= capacity;
        --in_use;
    }
  
~circular_buffer() {
    // first destroy any content
    while (in_use != 0)
        pop();

    // then release the buffer.
    operator delete(data); 
}

};

#endif

A differenza dei contenitori standard, questo utilizza operator newe operator deletedirettamente. Per un uso reale, probabilmente vorrai usare una classe allocatore, ma per il momento farebbe più distrarre che contribuire (IMO, comunque).


9
  1. Quando crei un oggetto con new, sei responsabile della chiamata delete. Quando crei un oggetto con make_shared, il risultato shared_ptrè responsabile del conteggio e della chiamata deletequando il conteggio dell'uso va a zero.
  2. Uscire dall'ambito significa lasciare un blocco. Questo è quando viene chiamato il distruttore, assumendo che l'oggetto non sia stato allocato con new(cioè è un oggetto stack).
  3. L'unico momento in cui è necessario chiamare esplicitamente un distruttore è quando si alloca l'oggetto con un posizionamentonew .

1
C'è il conteggio dei riferimenti (shared_ptr), anche se ovviamente non per semplici puntatori.
Pubby

1
@ Pubby: buon punto, promuoviamo le buone pratiche. Risposta modificata.
MSalters

6

1) Gli oggetti non vengono creati "tramite puntatori". C'è un puntatore che viene assegnato a qualsiasi oggetto "nuovo". Supponendo che questo sia ciò che intendi, se chiami "cancella" sul puntatore, in realtà cancellerà (e chiamerà il distruttore) l'oggetto che il puntatore dereferenzia. Se si assegna il puntatore a un altro oggetto si verificherà una perdita di memoria; niente in C ++ raccoglierà la tua spazzatura per te.

2) Queste sono due domande separate. Una variabile esce dall'ambito quando lo stack frame in cui è dichiarata viene estratto dallo stack. Di solito questo è quando lasci un blocco. Gli oggetti in un heap non escono mai dall'ambito, anche se i loro puntatori nello stack possono. Niente in particolare garantisce che verrà chiamato un distruttore di un oggetto in un elenco collegato.

3) Non proprio. Potrebbe esserci Deep Magic che suggerirebbe il contrario, ma in genere si desidera abbinare le "nuove" parole chiave con le parole chiave "elimina" e inserire tutto ciò che è necessario nel distruttore per assicurarsi che si pulisca correttamente da solo. Se non lo fai, assicurati di commentare il distruttore con istruzioni specifiche a chiunque utilizzi la classe su come dovrebbe ripulire manualmente le risorse di quell'oggetto.


3

Per dare una risposta dettagliata alla domanda 3: sì, ci sono (rare) occasioni in cui potresti chiamare esplicitamente il distruttore, in particolare come controparte di un nuovo posizionamento, come osserva dasblinkenlight.

Per dare un esempio concreto di questo:

#include <iostream>
#include <new>

struct Foo
{
    Foo(int i_) : i(i_) {}
    int i;
};

int main()
{
    // Allocate a chunk of memory large enough to hold 5 Foo objects.
    int n = 5;
    char *chunk = static_cast<char*>(::operator new(sizeof(Foo) * n));

    // Use placement new to construct Foo instances at the right places in the chunk.
    for(int i=0; i<n; ++i)
    {
        new (chunk + i*sizeof(Foo)) Foo(i);
    }

    // Output the contents of each Foo instance and use an explicit destructor call to destroy it.
    for(int i=0; i<n; ++i)
    {
        Foo *foo = reinterpret_cast<Foo*>(chunk + i*sizeof(Foo));
        std::cout << foo->i << '\n';
        foo->~Foo();
    }

    // Deallocate the original chunk of memory.
    ::operator delete(chunk);

    return 0;
}

Lo scopo di questo genere di cose è separare l'allocazione della memoria dalla costruzione dell'oggetto.


2
  1. Puntatori : i puntatori normali non supportano RAII. Senza un esplicito delete, ci sarà spazzatura. Fortunatamente C ++ ha puntatori automatici che gestiscono questo per te!

  2. Ambito : pensa a quando una variabile diventa invisibile al tuo programma. Di solito questo è alla fine {block}, come fai notare.

  3. Distruzione manuale : non tentare mai di farlo. Lascia che scope e RAII facciano la magia per te.


Una nota: auto_ptr è deprecato, come cita il tuo link.
tnecniv

std::auto_ptrè deprecato in C ++ 11, sì. Se l'OP ha effettivamente C ++ 11, dovrebbe usarlo std::unique_ptrper singoli proprietari o std::shared_ptrper più proprietari contati per riferimento.
chrisaycock

"Distruzione manuale - Non tentare mai di farlo". Molto spesso metto in coda i puntatori di oggetti a un thread diverso utilizzando una chiamata di sistema che il compilatore non comprende. "Affidarsi" a puntatori scope / auto / smart causerebbe il fallimento catastrofico delle mie app poiché gli oggetti venivano eliminati dal thread chiamante prima che potessero essere gestiti dal thread consumer. Questo problema interessa gli oggetti e le interfacce con ambito limitato e refCounted. Solo i puntatori e l'eliminazione esplicita funzioneranno.
Martin James

@MartinJames Puoi pubblicare un esempio di una chiamata di sistema che il compilatore non comprende? E come stai implementando la coda? Non std::queue<std::shared_ptr>?ho scoperto che pipe()tra un thread produttore e consumatore renda la concorrenza molto più semplice, se la copia non è troppo costosa.
chrisaycock

myObject = new myClass (); PostMessage (aHandle, WM_APP, 0, LPPARAM (myObject));
Martin James

1

Ogni volta che usi "new", cioè alleghi un indirizzo a un puntatore, o per dire, rivendichi spazio sull'heap, devi "cancellarlo".
1.sì, quando elimini qualcosa, viene chiamato il distruttore.
2.Quando viene chiamato il distruttore dell'elenco collegato, viene chiamato il distruttore degli oggetti. Ma se sono puntatori, è necessario eliminarli manualmente. 3.quando lo spazio è rivendicato da "nuovo".


0

Sì, un distruttore (noto anche come dtor) viene chiamato quando un oggetto esce dall'ambito se è in pila o quando si chiama delete un puntatore a un oggetto.

  1. Se il puntatore viene cancellato tramite delete verrà chiamato il dtor. Se riassegni il puntatore senza deleteprima chiamare , otterrai una perdita di memoria perché l'oggetto esiste ancora in memoria da qualche parte. In quest'ultimo caso, il dtor non viene chiamato.

  2. Una buona implementazione dell'elenco collegato chiamerà il dtor di tutti gli oggetti nell'elenco quando l'elenco viene distrutto (perché o hai chiamato un metodo per eliminarlo o è uscito dallo scope stesso). Questo dipende dall'implementazione.

  3. Ne dubito, ma non sarei sorpreso se ci fosse qualche strana circostanza là fuori.


1
"Se riassegni il puntatore senza chiamare prima delete, otterrai una perdita di memoria perché l'oggetto esiste ancora in memoria da qualche parte.". Non necessariamente. Potrebbe essere stato cancellato tramite un altro puntatore.
Matthew Flaschen

0

Se l'oggetto viene creato non tramite un puntatore (ad esempio, A a1 = A ();), il distruttore viene chiamato quando l'oggetto viene distrutto, sempre quando la funzione in cui si trova l'oggetto è terminata. Per esempio:

void func()
{
...
A a1 = A();
...
}//finish


il distruttore viene chiamato quando il codice viene eseguito sulla riga "finish".

Se l'oggetto viene creato tramite un puntatore (ad esempio, A * a2 = new A ();), il distruttore viene chiamato quando il puntatore viene cancellato (cancella a2;). Se il punto non viene cancellato dall'utente esplicitamente o nuovo indirizzo prima di eliminarlo, si è verificata la perdita di memoria. Questo è un bug.

In una lista collegata, se usiamo std :: list <>, non dobbiamo preoccuparci del descrittore o della perdita di memoria perché std :: list <> ha terminato tutto questo per noi. In una lista collegata scritta da noi stessi, dovremmo scrivere il descrittore ed eliminare il puntatore in modo esplicito, altrimenti causerà una perdita di memoria.

Raramente chiamiamo manualmente un distruttore. È una funzione che provvede al sistema.

Scusa per il mio pessimo inglese!


Non è vero che non puoi chiamare un distruttore manualmente, puoi (vedi il codice nella mia risposta, per esempio). Ciò che è vero è che la stragrande maggioranza delle volte non dovresti :)
Stuart Golodetz

0

Ricorda che il Costruttore di un oggetto viene chiamato immediatamente dopo che la memoria è stata allocata per quell'oggetto e mentre il distruttore viene chiamato appena prima di rilasciare la memoria di quell'oggetto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.