2012-02-07 12 views
17
Obj *op = new Obj; 
Obj *op2 = op; 
delete op; 
delete op2; // What happens here? 

Qual è la cosa peggiore che può accadere quando si accidentalmente doppio eliminato? Importa? Il compilatore sta per lanciare un errore?Cosa succede in una doppia eliminazione?

+5

Sì, può essere importante. No, il compilatore non sta per "lanciare un errore". Semplicemente non farlo :) – paulsm4

+13

Penso che in pratica il peggio che * plausibilmente * possa accadere è che qualche tempo dopo, l'allocatore di memoria si comporta male.Questo (a) ti costringe a impiegare molto tempo a tentare il debug, poiché i sintomi non sono affatto vicini alla causa e inoltre (b) crea una seria vulnerabilità di sicurezza nel software, che il tuo datore di lavoro viene citato in giudizio, perde, fallisce e il tuo contratto finisce per essere di proprietà di un ragazzo di nome Clive che effettua permessi domestici a un prezzo fisso per camion. Ti costringe a lavorare con i tuoi giochi di ruolo sul porno tentacolo giapponese che ti riguardano. –

+0

Non farlo e non farlo mai. È un comportamento indefinito. L'implementazione di C++ può fare qualsiasi cosa in questi casi. Potrebbe davvero fare cose cattive come corrompere il tuo heap, apportare modifiche arbitrarie e bizzarre ad altri oggetti nell'heap e fare anche cose peggiori. Se si usano puntatori grezzi è meglio impostarlo come puntatore nullo dopo la prima cancellazione perché l'eliminazione del puntatore nullo è sicura, altrimenti utilizzare puntatori intelligenti nel C++ moderno. – Destructor

risposta

19

Causa un comportamento non definito. Tutto può succedere. In pratica, un crash in fase di esecuzione è probabilmente quello che mi aspetterei.

+16

Anche un cucciolo casuale potrebbe morire. – Petr

+3

Si è verificato un caso in cui un monitor di persone si è incendiato a causa di un comportamento indefinito. –

16

È un comportamento indefinito, quindi il risultato effettiva varia a seconda dell'ambiente runtime compilatore &.

Nella maggior parte dei casi, il compilatore non noterà. In molti casi, se non nella maggior parte dei casi, la libreria di gestione della memoria di runtime si arresterà in modo anomalo.

Sotto il cofano, qualsiasi gestore di memoria deve conservare alcuni metadati su ciascun blocco di dati che assegna, in modo da consentirgli di cercare i metadati dal puntatore restituito da malloc/new. In genere questo assume la forma di una struttura a offset fisso prima del blocco assegnato. Questa struttura può contenere un "numero magico", una costante che è improbabile che si verifichi per puro caso. Se il gestore della memoria vede il numero magico nel luogo previsto, sa che il puntatore fornito per liberare/eliminare è molto probabilmente valido. Se non vede il numero magico, o se vede un numero diverso che significa "questo puntatore è stato liberato di recente", può ignorare in silenzio la richiesta libera, oppure può stampare un messaggio utile e abortire. O è legale sotto la specifica, e ci sono argomenti pro/contro per entrambi gli approcci.

Se il gestore della memoria non mantiene un numero magico nel blocco di metadati, o non abbia altrimenti controllare la sanità mentale dei metadati, quindi tutto può succedere. A seconda di come viene implementato il gestore della memoria, il risultato è molto probabilmente un arresto anomalo senza un messaggio utile, immediatamente nella logica del gestore della memoria, un po 'più tardi quando il gestore della memoria tenta di allocare o liberare memoria, o molto più tardi e molto lontano quando due diverse parti del programma pensano di avere la proprietà della stessa porzione di memoria.

Proviamo. Trasforma il tuo codice in un programma completo in so.cpp:

class Obj 
{ 
public: 
    int x; 
}; 

int main(int argc, char* argv[]) 
{ 
    Obj *op = new Obj; 
    Obj *op2 = op; 
    delete op; 
    delete op2; 

    return 0; 
} 

compilarlo (sto usando gcc 4.2.1 su OSX 10.6.8, ma YMMV):

[email protected] ~: g++ so.cpp 

eseguirlo:

[email protected] ~: ./a.out 
a.out(1965) malloc: *** error for object 0x100100080: pointer being freed was not allocated 
*** set a breakpoint in malloc_error_break to debug 
Abort trap 

Lookie lì, il runtime gcc rileva che in realtà si trattava di un doppio eliminare ed è abbastanza utile prima che si schianti.

+0

Anche nel caso in cui ci sia la possibilità che si verifichi una "doppia cancellazione" - un puntatore intelligente di solito è la soluzione migliore. – paul23

+0

Buona risposta, ma sarei più felice se facessi un grosso affare con il comportamento indefinito. –

+0

Sono abbastanza sicuro che altre risposte abbiano un "comportamento indefinito" ben coperto. –

21

Comportamento non definito. Non ci sono garanzie di sorta rispetto allo standard. Probabilmente il tuo sistema operativo offre alcune garanzie, come "non corromperesti un altro processo", ma questo non aiuta molto il tuo programma.

Il programma potrebbe bloccarsi. I tuoi dati potrebbero essere corrotti Il deposito diretto del tuo prossimo stipendio potrebbe invece prendere 5 milioni di dollari dal tuo account.

+24

Oppure potrebbe ordinare la pizza con la tua carta di credito. –

+0

Può riparare la mia auto? – Goldname

3

Il compilatore può dare un avvertimento o qualcosa del genere, soprattutto in modo ovvio (come nell'esempio) ma non è possibile che rilevi sempre. (È possibile utilizzare qualcosa come valgrind, che in fase di esecuzione in grado di rilevarlo però). Per quanto riguarda il comportamento, può essere qualsiasi cosa. Alcune librerie sicure potrebbero controllare, e gestirle bene - ma altri runtime (per velocità) faranno supporre che tu chiami corretto (che non è) e poi crash o peggio.Il runtime può presupporre che non stai eliminando due volte (anche se la doppia eliminazione farebbe qualcosa di male, ad esempio l'arresto anomalo del tuo computer)

0

No, non è sicuro cancellare lo stesso puntatore due volte. È un comportamento non definito secondo lo standard C++.

Dalla FAQ C++: visita this link

E 'sicuro per eliminare lo stesso puntatore due volte?
No! (. Dando per scontato che non ha ottenuto che il puntatore di ritorno da nuovo in mezzo)

Ad esempio, il seguente è un disastro:

class Foo { /*...*/ }; 
void yourCode() 
{ 
    Foo* p = new Foo(); 
    delete p; 
    delete p; // DISASTER! 
    // ... 
} 

Questo secondo cancellare linea p potrebbe fare alcune cose davvero male a voi. Potrebbe, a seconda della fase lunare, corrompere il tuo heap, far crashare il tuo programma, apportare modifiche arbitrarie e bizzarre a oggetti che sono già presenti nell'heap, ecc. Purtroppo questi sintomi possono apparire e scomparire casualmente. Secondo la legge di Murphy, sarai colpito nel modo più duro possibile nel momento peggiore (quando il cliente sta cercando, quando una transazione di alto valore sta tentando di pubblicare, ecc.). Nota: alcuni sistemi di runtime ti proteggeranno da alcuni casi molto semplici di doppia eliminazione. A seconda dei dettagli, potresti stare bene se ti trovi in ​​esecuzione su uno di questi sistemi e se nessuno distribuisce mai il tuo codice su un altro sistema che gestisce le cose in modo diverso e se stai eliminando qualcosa che non ha un distruttore e se non fai nulla di significativo tra le due eliminazioni e se nessuno cambia mai il tuo codice per fare qualcosa di significativo tra le due eliminazioni e se il tuo scheduler di thread (su cui probabilmente non hai alcun controllo!) non capita di scambiare i thread tra le due eliminazioni e se, e se e se. Quindi torna a Murphy: dato che può andare storto, lo farà, e andrà male nel momento peggiore possibile. Un non-crash non prova l'assenza di un bug; semplicemente non riesce a dimostrare la presenza di un bug. Fidati di me: double-delete è cattivo, cattivo, cattivo. Dì semplicemente di no.

0

Tutti ti hanno già detto che non dovresti fare questo e che causerà un comportamento indefinito. Questo è ampiamente noto, quindi approfondiamolo su un livello più basso e vediamo cosa succede realmente.

La risposta universale standard è che tutto può accadere, che non è completamente vero. Ad esempio, il computer non tenterà di ucciderti per farlo (a meno che tu non stia programmando l'intelligenza artificiale per un robot) :)

Il motivo per cui non può esserci alcuna risposta universale è che, poiché questo non è definito, potrebbe differiscono dal compilatore al compilatore e anche attraverso diverse versioni dello stesso compilatore.

Ma questo è quello "grosso" avviene nella maggior parte dei casi:

delete composte da 2 operazioni principali:

  • si chiama il distruttore se è definito
  • che libera in qualche modo la memoria allocata l'oggetto

Quindi, se il distruttore contiene un codice che accede a qualsiasi dato di classe che è già stato eliminato, potrebbe essere segfau ORA (molto probabilmente) leggerete alcuni dati senza senso. Se questi dati cancellati sono puntatori, molto probabilmente segfault, perché tenterai di accedere alla memoria che contiene qualcos'altro o che non ti appartiene.

Se il costruttore non tocca alcun dato o non è presente (non consideriamo i distruttori virtuali qui per semplicità), potrebbe non essere un motivo di arresto anomalo nella maggior parte delle implementazioni del compilatore. Tuttavia, chiamare un distruttore non è l'unica operazione che sta per accadere qui.

La memoria deve essere liberata. Il modo in cui è fatto dipende dall'implementazione nel compilatore, ma può anche eseguire alcune funzioni simili a free, dandogli il puntatore e la dimensione del tuo oggetto. Chiamare free sulla memoria che è stata già eliminata potrebbe bloccarsi, perché la memoria potrebbe non appartenere più a te. Se appartiene a te, potrebbe non bloccarsi immediatamente, ma potrebbe sovrascrivere la memoria già allocata per qualche oggetto diverso del tuo programma.

Ciò significa che una o più delle tue strutture di memoria sono appena state danneggiate e il tuo programma probabilmente si bloccherà prima o poi o potrebbe comportarsi in modo incredibilmente strano. Le ragioni non saranno ovvie nel tuo debugger e potresti passare settimane a capire che diavolo è appena successo.

Quindi, come altri hanno già detto, è in genere una cattiva idea, ma suppongo che tu lo sappia già. Non ti preoccupare, il micio innocente molto probabilmente non morirà se elimini un oggetto due volte.

Ecco esempio di codice che è sbagliato, ma può funzionare bene così (funziona bene con GCC su linux):

class a {}; 

int main() 
{ 
    a *test = new a(); 
    delete test; 
    a *test2 = new a(); 
    delete test; 
    return 0; 
} 

Se non mi crea un'istanza intermedia di quella classe tra eliminazioni, 2 chiamate a liberare sulla stessa memoria avviene come previsto:

*** Error in `./a.out': double free or corruption (fasttop): 0x000000000111a010 *** 

per rispondere alle vostre domande direttamente:

Qual è la cosa peggiore che può accadere:

In teoria, il programma causa qualcosa di fatale. Si può anche tentare in modo casuale di cancellare il disco rigido in alcuni casi estremi. Le probabilità dipendono da cosa sia effettivamente il tuo programma (driver del kernel? Programma di spazio utente?).

In pratica, molto probabilmente si bloccherebbe con il segfault. Ma potrebbe accadere qualcosa di peggio.

Il compilatore sta per inviare un errore?

Non dovrebbe.

0

Anche se questo non è definito:

int* a = new int; 
delete a; 
delete a; // same as your code 

questo è ben definito:

int* a = new int; 
delete a; 
a = nullptr; // or just NULL or 0 if your compiler doesn't support c++11 
delete a; // nothing happens! 

pensato che avrei dovuto pubblicarlo dato che nessun altro è stato citarlo.

Problemi correlati