Domanda

Inizierò dicendo: usa i puntatori intelligenti e non dovrai mai preoccuparti di questo.

Quali sono i problemi con il seguente codice?

Foo * p = new Foo;
// (use p)
delete p;
p = NULL;

Ciò è stato innescato da una risposta e commenti ad un'altra domanda.Un commento da Neil Butterworth ha generato alcuni voti positivi:

L'impostazione dei puntatori su NULL dopo l'eliminazione non è una buona pratica universale in C++.Ci sono momenti in cui è una buona cosa da fare e momenti in cui è inutile e può nascondere errori.

Ci sono molte circostanze in cui non sarebbe d'aiuto.Ma secondo la mia esperienza, non può far male.Qualcuno mi illumini.

È stato utile?

Soluzione

Impostazione di un puntatore a 0 (che è "nullo" in serie C ++, il NULL definire da C è alquanto differente) evita crash su eliminazioni doppie.

Si consideri il seguente:

Foo* foo = 0; // Sets the pointer to 0 (C++ NULL)
delete foo; // Won't do anything

considerando quanto segue:

Foo* foo = new Foo();
delete foo; // Deletes the object
delete foo; // Undefined behavior 

In altre parole, se non si imposta puntatori cancellati a 0, si otterrà nei guai se si sta facendo doppie eliminazioni. Un argomento contro l'impostazione puntatori a 0 dopo eliminazione sarebbe che così facendo solo maschere cancellare doppia bug e li lascia non gestita.

E 'meglio non avere doppia eliminare i bug, ovviamente, ma a seconda della semantica di proprietà e cicli di vita degli oggetti, questo può essere difficile da raggiungere nella pratica. Io preferisco un mascherato doppia bug di eliminazione sopra UB.

Infine, una nota a margine per quanto riguarda la gestione di allocazione degli oggetti, vi consiglio di dare un'occhiata a std::unique_ptr di una rigorosa / singolare proprietà, std::shared_ptr per la proprietà condivisa o un'altra implementazione puntatore intelligente, a seconda delle esigenze.

Altri suggerimenti

Impostazione puntatori a NULL dopo hai eliminato ciò che indicò certamente non può far male, ma è spesso un po 'di un cerotto su un problema più fondamentale: Perché stai usando un puntatore, in primo luogo? Vedo due motivi tipici:

  • È sufficiente voleva qualcosa allocato sul mucchio. Nel qual caso avvolgendolo in un oggetto RAII sarebbe stato molto più sicuro e più pulito. Terminare la portata dell'oggetto Raii quando non è più necessario l'oggetto. Ecco come funziona std::vector, e risolve il problema di accidentalmente lasciando puntatori a deallocate memory intorno. Non ci sono puntatori.
  • O forse si voleva alcuni complessi semantica proprietà condivisa. Il puntatore restituito da new potrebbe non essere la stessa di quella che delete è chiamato. Più oggetti possono essere utilizzati simultaneamente l'oggetto nel frattempo. In tal caso, un puntatore condiviso o qualcosa di simile sarebbe stato preferibile.

La mia regola è che se si lascia puntatori in giro in codice utente, si sta facendo male. Il puntatore non dovrebbe essere lì per puntare a spazzatura, in primo luogo. Perché non c'è un oggetto assumendo la responsabilità di assicurare la sua validità? Perché non la sua fine, quando il campo di applicazione a punta-di opporsi fa?

Ho una ancora migliore best practice: Dove possibile, finire la portata della variabile

{
    Foo* pFoo = new Foo;
    // use pFoo
    delete pFoo;
}

Ho sempre impostato un puntatore a NULL (ora nullptr) dopo l'eliminazione dell'oggetto (s) a cui punta.

  1. Si può aiutare cattura molti riferimenti alla memoria liberata (supponendo che i vostri difetti piattaforma su un deref di un puntatore nullo).

  2. E non prenderà tutti i riferimenti a free'd memoria se, ad esempio, si dispone di copie del puntatore in giro. Ma alcuni è meglio di niente.

  3. Sarà mascherare una doppia eliminazione, ma trovo questi sono molto meno comune rispetto accessi già liberata la memoria.

  4. In molti casi il compilatore sta per ottimizzarlo via. Quindi l'argomento che è inutile non mi convincere.

  5. Se si sta già utilizzando Raii, quindi non ci sono molte deletes nel codice per cominciare, così l'argomento che l'assegnazione in più provoca disordine non mi persuadono.

  6. È spesso conveniente, quando debugging, per visualizzare il valore nullo anziché un puntatore stantio.

  7. Se ancora ti dà fastidio, utilizzare un puntatore intelligente o un riferimento, invece.

Ho anche impostare altri tipi di risorsa gestisce al valore no-risorsa quando la risorsa è free'd (che è in genere solo nel distruttore di un involucro RAII scritta per incapsulare la risorsa).

ho lavorato su un grande (9 milioni di dichiarazioni) prodotto commerciale (principalmente in C). A un certo punto, abbiamo usato la magia macro a null il puntatore quando la memoria è stata liberata. Questo immediatamente esposto un sacco di bug in agguato che sono stati prontamente risolti. Per quanto posso ricordare, non abbiamo mai avuto un bug doppio gratuito.

Aggiornamento: Microsoft ritiene che si tratta di una buona pratica per la sicurezza e raccomanda la pratica nelle loro politiche SDL. A quanto pare MSVC ++ 11 sarà calpestare il puntatore cancellato automaticamente (in molti casi) se si compila con l'opzione / SDL.

In primo luogo, ci sono un sacco di domande esistenti su questo e argomenti strettamente correlati, ad esempio Perché non elimina impostato il puntatore a NULL? .

Nel codice, la questione cosa succede in (uso p). Ad esempio, se da qualche parte si dispone di codice in questo modo:

Foo * p2 = p;

quindi impostando p a NULL compie molto poco, come lei ha ancora la p2 puntatore di cui preoccuparsi.

Questo non vuol dire che l'impostazione di un puntatore a NULL è sempre inutile. Ad esempio, se p fosse una punta variabile membro a una risorsa che è vita non era esattamente la stessa della classe contenente p, quindi impostando p NULL potrebbe essere un modo utile di indicare la presenza o l'assenza della risorsa.

Se non v'è più il codice dopo la delete, sì. Quando il puntatore viene eliminato in un costruttore o alla fine del metodo o funzione, n

Il punto di questa parabola è quello di ricordare il programmatore, durante la fase di esecuzione, che l'oggetto è già stato cancellato.

Una pratica ancora migliore è quello di utilizzare Smart Pointers (condiviso o ambito), che automagically cancellare i suoi oggetti di destinazione.

Come altri hanno detto, delete ptr; ptr = 0; non sta per causare demoni per volare fuori del vostro naso. Tuttavia, non incoraggiare l'uso di ptr come una bandiera di sorta. Il codice viene disseminato di delete e impostando il puntatore NULL. Il passo successivo è quello di spargere if (arg == NULL) return; attraverso il codice per la protezione contro l'uso accidentale di un puntatore NULL. Il problema si verifica una volta che i controlli contro NULL diventano tuoi principale mezzo di controllo per lo stato di un oggetto o di un programma.

Sono sicuro che c'è un codice odore sull'utilizzo di un puntatore come una bandiera da qualche parte, ma non ho trovato uno.

cambierò la tua domanda un po ':

  

Vuoi utilizzare un non inizializzata   puntatore? Sai, uno che non hai fatto   set NULL o allocare la memoria che   punta a?

Ci sono due scenari in cui l'impostazione del puntatore a NULL può essere saltata:

  • la variabile puntatore esce dallo scope immediatamente
  • si hanno sovraccaricato la semantica del puntatore e si utilizza il suo valore non solo come un puntatore di memoria, ma anche come valore chiave o crudo. questo approccio tuttavia soffre di altri problemi.

Nel frattempo, sostenendo che l'impostazione del puntatore a NULL potrebbe nascondere gli errori a me suona come sostenendo che non si deve correggere un bug, perché la correzione potrebbe nascondere un altro bug. Gli unici insetti che potrebbero mostrare se il puntatore non è impostato su NULL sarebbero quelli che cercano di utilizzare il puntatore. Ma l'impostazione a NULL sarebbe effettivamente causare esattamente lo stesso bug come mostrerebbe se lo si utilizza con memoria liberata, non è vero?

Se non avete altro vincolo che ti costringe a uno impostare o non si imposta il puntatore a NULL dopo averlo eliminato (un tale vincolo è stata menzionata da Neil Butterworth ), quindi la mia preferenza personale è quello di lasciare che sia.

Per quanto mi riguarda, la questione non è "questa è una buona idea?" ma "quale comportamento dovrei impedire o permettere di riuscire in questo modo?" Ad esempio, se questo permette altro codice di vedere che il puntatore non è più disponibile, il motivo per cui è altro codice, anche il tentativo di guardare ai puntatori liberati dopo che sono stati liberati? Di solito, si tratta di un bug.

Si fa anche più lavoro del necessario, così come ostacolare il debugging post mortem. Il meno si tocca memoria dopo non ne hai bisogno, più facile è quello di capire perché qualcosa si è bloccato. Molte volte mi hanno fatto affidamento sul fatto che la memoria è in uno stato simile a quando un particolare bug si è verificato per diagnosticare e risolvere bug, ha detto.

In modo esplicito che annulla i dopo eliminare fortemente suggerisce a un lettore che il puntatore rappresenta qualcosa che è concettualmente opzionale . Se ho visto che sta facendo, mi piacerebbe iniziare a preoccuparsi che ovunque nel sorgente il puntatore si abitua che dovrebbe essere prima testato contro NULL.

Se questo è ciò che realmente dire, è meglio fare che esplicita nella fonte usando qualcosa come boost :: opzionale

optional<Foo*> p (new Foo);
// (use p.get(), but must test p for truth first!...)
delete p.get();
p = optional<Foo*>();

Ma se voi persone veramente voluto sapere il puntatore è "andato a male", io la passo in 100% accordo con chi dice che la cosa migliore da fare è farlo andare fuori del campo di applicazione. Poi si sta utilizzando il compilatore per prevenire la possibilità di cattivi dereferenziazioni in fase di esecuzione.

Questo è il bambino in tutta l'acqua sporca C ++, non dovrebbe buttare fuori. :)

In un programma ben strutturato con opportuno controllo degli errori, non v'è alcuna ragione non per assegnare nullo. 0 si trova solo come un valore non valido universalmente riconosciuto in questo contesto. Fail duro e riuscire al più presto.

Molti degli argomenti contro l'assegnazione 0 suggeriscono che potrebbero nascondere un bug o complicare flusso di controllo. Fondamentalmente, che è sia un errore a monte (non è colpa tua (scusate il gioco di parole)) o un altro errore per conto del programmatore -. Forse anche l'indicazione che il flusso del programma è cresciuta troppo complesso

Se il programmatore vuole introdurre l'uso di un puntatore che può essere nullo come un valore speciale e scrivere tutte le necessarie schivare intorno a quella, che è una complicazione che hanno deliberatamente introdotto. La migliore è la quarantena, la prima si trova i casi di uso improprio, e tanto meno sono in grado di diffondersi in altri programmi.

programmi ben strutturati possono essere progettati utilizzando funzioni C ++ per evitare questi casi. È possibile utilizzare i riferimenti, oppure si può semplicemente dire "passaggio / usando argomenti nulli o non validi è un errore" - un approccio che è ugualmente applicabile a contenitori, come ad esempio i puntatori intelligenti. Aumentare comportamento coerente e corretto vieta questi bug di ottenere lontano.

Da lì, si hanno solo una portata molto limitata e il contesto in cui un puntatore nullo può esistere (o è consentito).

Lo stesso può essere applicato ai puntatori che non sono const. Seguendo il valore di un puntatore è banale perché la sua portata è così piccolo, e l'uso improprio viene controllato e ben definita. Se il set di strumenti e gli ingegneri non può seguire il programma a seguito di una rapida lettura o v'è inadeguato controllo degli errori o il flusso del programma incoerente / indulgente, avete altri, problemi più grandi.

Infine, il compilatore e l'ambiente ha probabilmente alcune guardie per i momenti in cui si vorrebbe introdurre errori (scarabocchiare), di rilevare gli accessi alla memoria liberata, e prendere altri UB correlate. È anche possibile introdurre la diagnostica simili nei vostri programmi, spesso senza influenzare i programmi esistenti.

Permettimi di espandere ciò che hai già inserito nella tua domanda.

Ecco cosa hai inserito nella tua domanda, sotto forma di punti elenco:


L'impostazione dei puntatori su NULL dopo l'eliminazione non è una buona pratica universale in C++.Ci sono momenti in cui:

  • è una buona cosa da fare
  • e momenti in cui è inutile e può nascondere errori.

Tuttavia, c'è senza volte quando questo sarà Cattivo!Desideri non introduci più bug annullandolo esplicitamente, non lo farai perdere memoria, non lo farai causare un comportamento indefinito accadere.

Quindi, in caso di dubbio, annullalo.

Detto questo, se ritieni di dover annullare esplicitamente alcuni puntatori, allora a me sembra che tu non abbia suddiviso abbastanza un metodo e dovresti esaminare l'approccio di refactoring chiamato "Metodo Estrai" per suddividere il metodo in parti separate.

Sì.

L'unico "danno" si può fare è quello di introdurre inefficienza (un'operazione di salvataggio non necessaria) nel vostro programma - ma questo overhead sarà insignificante rispetto al costo di allocare e liberare il blocco di memoria nella maggior parte dei casi

Se non lo fai, è avere qualche brutto bug puntatore derefernce un giorno.

Io uso sempre una macro per cancellare:

#define SAFEDELETE(ptr) { delete(ptr); ptr = NULL; }

(e simili per una matrice, libero (), rilasciando maniglie)

Si può anche scrivere "auto delete" metodi che accettano un riferimento al puntatore del codice chiamante, in modo da forzare il puntatore del codice chiamante a NULL. Ad esempio, per eliminare una sottostruttura di molti oggetti:

static void TreeItem::DeleteSubtree(TreeItem *&rootObject)
{
    if (rootObject == NULL)
        return;

    rootObject->UnlinkFromParent();

    for (int i = 0; i < numChildren)
       DeleteSubtree(rootObject->child[i]);

    delete rootObject;
    rootObject = NULL;
}

modifica

Sì, queste tecniche non violano alcune regole circa l'uso di macro (e sì, in questi giorni probabilmente si potrebbe ottenere lo stesso risultato con i modelli) - ma utilizzando per molti anni I mai accedere morti memoria - uno dei peggiori e più difficile e richiede più tempo per eseguire il debug di problemi si possono affrontare. In pratica nel corso degli anni hanno effettivamente eliminato una classe whjole di bug da ogni squadra li ho introdotto il.

Ci sono anche molti modi si potrebbe implementare quanto sopra - Sto solo cercando di illustrare l'idea di costringere la gente a NULL un puntatore se eliminare un oggetto, piuttosto che fornire un mezzo per loro di rilasciare la memoria che non lo fa NULL puntatore del chiamante.

Naturalmente, l'esempio di cui sopra è solo un passo verso un auto-puntatore. Che non ho suggerisco perché il PO è stato specificamente chiesto in merito al caso di non utilizzare un puntatore automatico.

"Ci sono momenti in cui è una buona cosa da fare, e momenti in cui è inutile e può nascondere gli errori"

vedo due problemi: Questo semplice codice:

delete myObj;
myobj = 0

diventa un per-liner in ambienti multithread:

lock(myObjMutex); 
delete myObj;
myobj = 0
unlock(myObjMutex);

Il "best practice" di Don Neufeld non si applicano sempre. Per esempio. in un progetto automobilistico abbiamo dovuto impostare puntatori a 0 anche in distruttori. Posso immaginare nel software safety-critical tali norme non sono infrequenti. E 'più facile (e saggio) di seguirli che cercare di convincere la squadra / code-checker per ogni uso puntatore nel codice, che una linea azzeramento questo cursore è ridondante.

Un altro pericolo si basa su questa tecnica in eccezioni-utilizzando il codice:

try{  
   delete myObj; //exception in destructor
   myObj=0
}
catch
{
   //myObj=0; <- possibly resource-leak
}

if (myObj)
  // use myObj <--undefined behaviour

In tale codice o si producono delle risorse di perdita e di rinviare il problema o si blocca il processo.

Quindi, questi due problemi che vanno spontaneamente attraverso la mia testa (Herb Sutter avrebbero di sicuro dire di più) fare per me tutte le domande del tipo "Come evitare l'uso di smart-puntatori e fare il lavoro in modo sicuro con i puntatori normali" come obsoleti .

C'è sempre Dangling Puntatori di cui preoccuparsi.

Se avete intenzione di riallocare il puntatore prima di usarlo di nuovo (dereferenziazione esso, passandolo a una funzione, ecc), rendendo il NULL pointer è solo un'operazione supplementare. Tuttavia, se non siete sicuri se sarà riassegnato o meno prima di essere riutilizzato, impostandolo a NULL è una buona idea.

Come molti hanno detto, è ovviamente molto più facile usare solo i puntatori intelligenti.

Edit: Come ha detto Thomas Matthews in questa risposta in precedenza , se un puntatore viene eliminato in un distruttore, non c'è alcuna necessità di assegnare NULL ad esso in quanto non verrà utilizzato di nuovo perché l'oggetto viene distrutto già.

Posso immaginare l'impostazione di un puntatore a NULL dopo l'eliminazione di esso che è utile nei rari casi in cui v'è un legittima scenario di riutilizzare in una singola funzione (o di un oggetto). Altrimenti non ha senso - ha bisogno di un puntatore per puntare a qualcosa di significativo a patto che essa esiste -. Periodo

Se il codice non appartiene alla parte più prestazioni-critical della vostra applicazione, fare cose semplici e utilizzare uno shared_ptr:

shared_ptr<Foo> p(new Foo);
//No more need to call delete

Si esegue il conteggio di riferimento ed è thread-safe. Lo si può trovare nella TR1 (namespace std :: TR1, #include ) o se il compilatore non prevede che, a farla da spinta.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top