it-swarm.it

Linee guida generali per evitare perdite di memoria in C ++

Quali sono alcuni suggerimenti generali per assicurarsi di non perdere la memoria nei programmi C++? Come faccio a capire chi dovrebbe liberare memoria che è stata allocata in modo dinamico?

125
dulipishi

Invece di gestire la memoria manualmente, prova a utilizzare i puntatori intelligenti ove applicabile.
Dai un'occhiata a Boost lib , TR1 e puntatori intelligenti .
Anche i puntatori intelligenti fanno ora parte dello standard C++ chiamato C++ 11 .

38
Andri Möll

Approvo a fondo tutti i consigli su RAII e gli smart pointer, ma vorrei anche aggiungere un suggerimento di livello leggermente superiore: la memoria più semplice da gestire è la memoria che non hai mai allocato. A differenza di linguaggi come C # e Java, dove praticamente tutto è un riferimento, in C++ dovresti mettere gli oggetti nello stack ogni volta che puoi. Come ho visto diverse persone (incluso il dottor Stroustrup), il motivo principale per cui la raccolta dei rifiuti non è mai stata popolare in C++ è che il C++ ben scritto non produce molta spazzatura in primo luogo.

Non scrivere

Object* x = new Object;

o anche

shared_ptr<Object> x(new Object);

quando puoi semplicemente scrivere

Object x;
196
Ross Smith

Usa RAII

  • Dimentica Garbage Collection (usa invece RAII). Si noti che anche Garbage Collector può perdere anche (se si dimentica di "null" alcuni riferimenti in Java/C #) e che Garbage Collector non ti aiuterà a smaltire le risorse (se si dispone di un oggetto che ha acquisito un handle per un file, il file non verrà liberato automaticamente quando l'oggetto uscirà dall'ambito se non lo si fa manualmente in Java, o si utilizza il modello "dispose" in C #).
  • Dimentica la regola "un ritorno per funzione" . Questo è un buon consiglio C per evitare perdite, ma è obsoleto in C++ a causa del suo uso di eccezioni (usare RAII invece).
  • E mentre il "Sandwich Pattern" è un buon consiglio in C, è obsoleto in C++ a causa del suo uso di eccezioni (utilizzare RAII invece).

Questo post sembra essere ripetitivo, ma in C++, il modello più semplice da sapere è RAII .

Impara a usare i puntatori intelligenti, sia da boost, TR1 o anche dal basso (ma spesso abbastanza efficiente) auto_ptr (ma devi conoscerne i limiti).

RAII è la base della sicurezza delle eccezioni e dello smaltimento delle risorse in C++, e nessun altro modello (sandwich, ecc.) Ti darà entrambi (e il più delle volte, non ti darà nessuno).

Vedi sotto un confronto di codice RAII e non RAII:

void doSandwich()
{
   T * p = new T() ;
   // do something with p
   delete p ; // leak if the p processing throws or return
}

void doRAIIDynamic()
{
   std::auto_ptr<T> p(new T()) ; // you can use other smart pointers, too
   // do something with p
   // WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}

void doRAIIStatic()
{
   T p ;
   // do something with p
   // WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}

Informazioni su RAII

Riassumendo (dopo il commento di Salmo Ogre33), RAII si basa su tre concetti:

  • Una volta che l'oggetto è costruito, funziona e basta! Acquisisci risorse nel costruttore.
  • La distruzione dell'oggetto è sufficiente! Fai risorse gratuite nel distruttore.
  • Si tratta di ambiti! Gli oggetti con ambito (vedi l'esempio doRAIIStatic sopra) saranno costruiti alla loro dichiarazione e saranno distrutti nel momento in cui l'esecuzione esce dall'ambito, indipendentemente da come l'uscita (ritorno, interruzione, eccezione, ecc.).

Ciò significa che nel corretto codice C++, la maggior parte degli oggetti non verrà costruita con new e verrà invece dichiarata nello stack. E per quelli costruiti usando new, tutto sarà in qualche modo con ambito (ad es. collegato a un puntatore intelligente).

Come sviluppatore, questo è davvero molto potente in quanto non dovrai preoccuparti della gestione manuale delle risorse (come fatto in C, o per alcuni oggetti in Java che fa un uso intensivo di try/finally per quel caso) ...

Modifica (2012-02-12)

"Gli oggetti con ambito ... verranno distrutti ... indipendentemente dall'uscita" non è del tutto vero. ci sono modi per imbrogliare RAII. qualsiasi sapore di terminate () ignorerà la pulizia. exit (EXIT_SUCCESS) è un ossimoro in questo senso.

- wilhelmtell

Wilhelmtell ha ragione: ci sono eccezionale modi per imbrogliare RAII, portando tutti alla brusca interruzione del processo.

Questi sono modi eccezionali perché il codice C++ non è disseminato di terminate, exit, ecc., O nel caso con eccezioni, vogliamo un eccezione non gestita per arrestare in modo anomalo il processo e il core scarica l'immagine di memoria così com'è e non dopo la pulizia.

Ma dobbiamo ancora conoscere questi casi perché, anche se raramente accadono, possono ancora accadere.

(chi chiama terminate o exit nel codice C++ casuale? ... Ricordo di aver dovuto affrontare quel problema quando giocavo con GLUT : questa libreria è molto orientata al C, arrivando al punto di progettarla attivamente per rendere le cose difficili per gli sviluppatori C++ come non preoccuparsi di impilare i dati allocati , o avere decisioni "interessanti" su - non tornare mai dal loro ciclo principale ... Non commenterò al riguardo).

101
paercebal

Ti consigliamo di guardare i puntatori intelligenti, come puntatori intelligenti di boost .

Invece di

int main()
{ 
    Object* obj = new Object();
    //...
    delete obj;
}

boost :: shared_ptr verrà automaticamente eliminato quando il conteggio dei riferimenti è zero:

int main()
{
    boost::shared_ptr<Object> obj(new Object());
    //...
    // destructor destroys when reference count is zero
}

Nota la mia ultima nota, "quando il conteggio dei riferimenti è zero, che è la parte più interessante. Quindi, se hai più utenti del tuo oggetto, non dovrai tenere traccia del fatto che l'oggetto sia ancora in uso. Una volta che nessuno si riferisce al tuo puntatore condiviso, viene distrutto.

Questa non è una panacea, tuttavia. Sebbene sia possibile accedere al puntatore di base, non si vorrebbe passarlo a un'API di terze parti a meno che non si fosse sicuri di ciò che stava facendo. Molte volte, le tue "pubblicazioni" su qualche altra discussione per il lavoro da fare DOPO che l'ambito della creazione è terminata. Questo è comune con PostThreadMessage in Win32:

void foo()
{
   boost::shared_ptr<Object> obj(new Object()); 

   // Simplified here
   PostThreadMessage(...., (LPARAM)ob.get());
   // Destructor destroys! pointer sent to PostThreadMessage is invalid! Zohnoes!
}

Come sempre, usa il tuo cappello pensante con qualsiasi strumento ...

25
Doug T.

Leggi su RAII e assicurati di capirlo.

12
Hank

La maggior parte delle perdite di memoria sono il risultato di non essere chiari sulla proprietà e sulla durata degli oggetti.

La prima cosa da fare è allocare in pila ogni volta che puoi. Questo riguarda la maggior parte dei casi in cui è necessario allocare un singolo oggetto per qualche scopo.

Se hai bisogno di "nuovo" un oggetto, la maggior parte delle volte avrà un unico proprietario ovvio per il resto della sua vita. Per questa situazione, tendo a usare un sacco di modelli di raccolte che sono progettati per "possedere" oggetti memorizzati in essi tramite puntatore. Sono implementati con i contenitori di vettore e mappa STL ma presentano alcune differenze:

  • Queste raccolte non possono essere copiate o assegnate a. (una volta che contengono oggetti.)
  • I puntatori agli oggetti vengono inseriti in essi.
  • Quando la raccolta viene eliminata, il distruttore viene prima chiamato su tutti gli oggetti nella raccolta. (Ho un'altra versione in cui si afferma se distrutta e non vuota.)
  • Poiché memorizzano i puntatori, è anche possibile archiviare oggetti ereditati in questi contenitori.

Il mio problema con STL è che è così focalizzato sugli oggetti Value mentre nella maggior parte delle applicazioni gli oggetti sono entità uniche che non hanno una semantica di copia significativa richiesta per l'uso in quei contenitori.

11
Jeroen Dirks

Bah, voi ragazzi e i vostri nuovi raccoglitori di immondizia ...

Regole molto rigide sulla "proprietà": quale oggetto o parte del software ha il diritto di eliminare l'oggetto. Cancella commenti e saggi nomi di variabili per renderlo ovvio se un puntatore "possiede" o è "guarda, non toccare". Per aiutare a decidere chi possiede cosa, segui il più possibile il modello "sandwich" all'interno di ogni subroutine o metodo.

create a thing
use that thing
destroy that thing

A volte è necessario creare e distruggere in luoghi molto diversi; penso intensamente per evitarlo.

In qualsiasi programma che richieda strutture dati complesse, creo un albero di oggetti rigorosamente chiaro contenente altri oggetti, usando i puntatori "proprietario". Questo albero modella la gerarchia di base dei concetti del dominio dell'applicazione. Esempio una scena 3D possiede oggetti, luci, trame. Alla fine del rendering quando il programma si chiude, c'è un modo chiaro per distruggere tutto.

Molti altri puntatori sono definiti come necessari ogni volta che un'entità ha bisogno di accedervi, per scansionare arays o altro; questi sono i "solo guardando". Per l'esempio della scena 3D - un oggetto usa una trama ma non possiede; altri oggetti possono usare la stessa trama. La distruzione di un oggetto no invoca la distruzione di qualsiasi trama.

Sì, richiede tempo, ma è quello che faccio. Raramente ho perdite di memoria o altri problemi. Ma poi lavoro nell'arena limitata del software scientifico, di acquisizione dati e grafica ad alte prestazioni. Spesso non gestisco transazioni come nel settore bancario ed e-commerce, GUI guidate da eventi o caos asincrono ad alta rete. Forse i nuovi modi hanno un vantaggio lì!

10
DarenW

Ottima domanda!

se stai usando c ++ e stai sviluppando un'applicazione boud in tempo reale su CPU e memoria (come i giochi) devi scrivere il tuo Memory Manager.

Penso che il meglio che puoi fare sia unire alcune opere interessanti di vari autori, posso darti un suggerimento:

  • L'allocatore a dimensione fissa è ampiamente discusso, ovunque nella rete

  • Small Object Allocation è stata introdotta da Alexandrescu nel 2001 nel suo libro perfetto "Modern c ++ design"

  • Un grande progresso (con codice sorgente distribuito) può essere trovato in un fantastico articolo in Game Programming Gem 7 (2008) chiamato "High Performance Heap allocator" scritto da Dimitar Lazarov

  • Un grande elenco di risorse è disponibile nell'articolo questo

Non iniziare a scrivere da solo un allocatore inutile ... No prima DOCUMENTO.

8
ugasoft

Una tecnica che è diventata popolare con la gestione della memoria in C++ è RAII . Fondamentalmente usi costruttori/distruttori per gestire l'allocazione delle risorse. Naturalmente ci sono altri dettagli odiosi in C++ a causa della sicurezza delle eccezioni, ma l'idea di base è piuttosto semplice.

Il problema generalmente si riduce a quello di proprietà. Consiglio vivamente di leggere la serie C++ Effective di Scott Meyers e Modern C++ Design di Andrei Alexandrescu.

5
Jason Dagit

C'è già molto su come non perdere, ma se hai bisogno di uno strumento per aiutarti a tenere traccia delle perdite dai un'occhiata a:

5
fabiopedrosa

Puntatori intelligenti per l'utente ovunque tu sia! Intere classi di perdite di memoria scompaiono.

4
DougN

Condividi e conosci le regole di proprietà della memoria nel tuo progetto. L'uso delle regole COM garantisce la migliore coerenza (i parametri [in] sono di proprietà del chiamante, il destinatario deve copiare; i parametri [out] sono di proprietà del chiamante, il destinatario deve effettuare una copia se si mantiene un riferimento; ecc.)

4
Seth Morris

valgrind è un buon strumento per controllare anche le perdite di memoria dei programmi in fase di esecuzione.

È disponibile sulla maggior parte delle versioni di Linux (incluso Android) e su Darwin.

Se usi per scrivere unit test per i tuoi programmi, dovresti prendere l'abitudine di eseguire sistematicamente valgrind sui test. Potrà evitare molte perdite di memoria in una fase iniziale. Di solito è anche più facile individuarli in semplici test che in un software completo.

Naturalmente questo consiglio rimane valido per qualsiasi altro strumento di controllo della memoria.

4
Joseph

Inoltre, non utilizzare la memoria allocata manualmente se esiste una classe di libreria std (ad es. Vettore). Assicurati di violare quella regola di avere un distruttore virtuale.

3
Joseph

Se non puoi/non utilizzare un puntatore intelligente per qualcosa (anche se dovrebbe essere un'enorme bandiera rossa), digita il codice con:

allocate
if allocation succeeded:
{ //scope)
     deallocate()
}

Questo è ovvio, ma assicurati di digitarlo prima digiti qualsiasi codice nell'ambito

2
Seth Morris

Suggerimenti in ordine di importanza:

-Tip # 1 Ricorda sempre di dichiarare i tuoi distruttori "virtuali".

-Tip # 2 Usa RAII

-Tip # 3 Usa gli smartpointer boost

-Tipo n. 4 Non scrivere i tuoi smartpointer con errori, usa boost (su un progetto in questo momento non riesco a usare boost, e ho avuto il debug di dover eseguire il debug dei miei puntatori intelligenti, sicuramente non prenderei lo stesso percorso di nuovo, ma poi di nuovo in questo momento non posso aggiungere boost alle nostre dipendenze)

-Tip # 5 Se funziona in modo casual/non performante (come nei giochi con migliaia di oggetti), guarda il contenitore puntatore boost di Thorsten Ottosen

-Tip # 6 Trova un'intestazione di rilevamento perdite per la tua piattaforma preferita come l'intestazione "vld" di Visual Leak Detection

2
Robert Gould

Una fonte frequente di questi bug è quando si dispone di un metodo che accetta un riferimento o un puntatore a un oggetto ma lascia la proprietà poco chiara. Convenzioni di stile e commenti possono renderlo meno probabile.

Lascia che il caso in cui la funzione diventi proprietaria dell'oggetto sia il caso speciale. In tutte le situazioni in cui ciò accade, assicurarsi di scrivere un commento accanto alla funzione nel file di intestazione indicando questo. Dovresti cercare di assicurarti che nella maggior parte dei casi anche il modulo o la classe che alloca un oggetto sia responsabile della deallocazione.

L'uso di const può aiutare molto in alcuni casi. Se una funzione non modifica un oggetto e non memorizza un riferimento a esso persistente dopo la sua restituzione, accetta un riferimento const. Dalla lettura del codice del chiamante sarà ovvio che la tua funzione non ha accettato la proprietà dell'oggetto. Avresti potuto fare in modo che la stessa funzione accettasse un puntatore non const e il chiamante avrebbe potuto o meno presumere che la chiamata accettasse la proprietà, ma con un riferimento const non c'è dubbio.

Non utilizzare riferimenti non costanti negli elenchi di argomenti. Non è molto chiaro quando si legge il codice del chiamante che la chiamata potrebbe aver mantenuto un riferimento al parametro.

Non sono d'accordo con i commenti che raccomandano puntatori contati di riferimento. Questo di solito funziona bene, ma quando hai un bug e non funziona, specialmente se il tuo distruttore fa qualcosa di non banale, come in un programma multithread. Sicuramente prova a modificare il tuo design per non aver bisogno del conteggio dei riferimenti se non è troppo difficile.

2
Jonathan

Se puoi, usa boost shared_ptr e standard C++ auto_ptr. Questi trasmettono la semantica della proprietà.

Quando si restituisce un auto_ptr, si sta dicendo al chiamante che si sta dando loro la proprietà della memoria.

Quando restituisci un shared_ptr, stai dicendo al chiamante che hai un riferimento ad esso e che prendono parte della proprietà, ma non è solo una loro responsabilità.

Questa semantica si applica anche ai parametri. Se il chiamante ti passa un auto_ptr, ti stanno dando la proprietà.

1
Justin Rudd
  • Cerca di evitare l'allocazione dinamica degli oggetti. Finché le classi hanno costruttori e distruttori appropriati, usa una variabile del tipo di classe, non un puntatore ad essa, ed eviti allocazione dinamica e deallocazione perché il compilatore lo farà per te.
    In realtà questo è anche il meccanismo usato da "puntatori intelligenti" e indicato come RAII da alcuni degli altri scrittori ;-).
  • Quando si passano oggetti ad altre funzioni, preferire i parametri di riferimento ai puntatori. Questo evita alcuni possibili errori.
  • Dichiarare i parametri const, ove possibile, in particolare i puntatori agli oggetti. In questo modo gli oggetti non possono essere liberati "accidentalmente" (tranne se si lancia via la const ;-))).
  • Ridurre al minimo il numero di posizioni nel programma in cui si esegue l'allocazione e la deallocazione della memoria. Per esempio. se assegnate o liberate più volte lo stesso tipo, scrivete una funzione (o un metodo di fabbrica ;-)).
    In questo modo è possibile creare facilmente output di debug (quali indirizzi sono allocati e deallocati, ...) facilmente, se necessario.
  • Utilizzare una funzione di fabbrica per allocare oggetti di diverse classi correlate da una singola funzione.
  • Se le tue classi hanno una classe base comune con un distruttore virtuale, puoi liberarle tutte usando la stessa funzione (o metodo statico).
  • Controlla il tuo programma con strumenti come purify (purtroppo molti $/€/...).
1
mh.

Se hai intenzione di gestire la tua memoria manualmente, hai due casi:

  1. Ho creato l'oggetto (forse indirettamente, chiamando una funzione che alloca un nuovo oggetto), lo uso (o una funzione che chiamo lo usa), quindi lo libero.
  2. Qualcuno mi ha dato il riferimento, quindi non dovrei liberarlo.

Se devi infrangere una di queste regole, ti preghiamo di documentarlo.

Si tratta della proprietà del puntatore.

1
Null303

valgrind (disponibile solo per piattaforme * nix) è un ottimo controller di memoria

1
Ronny Brendel

Altri hanno menzionato i modi per evitare perdite di memoria in primo luogo (come i puntatori intelligenti). Ma uno strumento di profilazione e analisi della memoria è spesso l'unico modo per rintracciare i problemi di memoria una volta che li hai.

Valgrind memcheck è un eccellente gratuito.

1
eli

Solo per MSVC, aggiungi quanto segue all'inizio di ogni file .cpp:

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

Quindi, durante il debug con VS2003 o superiore, ti verrà comunicato di eventuali perdite all'uscita del programma (tiene traccia di nuovo/elimina). È di base, ma mi ha aiutato in passato.

1
Rob

C++ è progettato per RAII. Penso che non esista davvero un modo migliore per gestire la memoria in C++. Ma fai attenzione a non allocare blocchi molto grandi (come oggetti buffer) su ambito locale. Può causare overflow dello stack e, se si verifica un difetto nel controllo dei limiti durante l'utilizzo di quel blocco, è possibile sovrascrivere altre variabili o restituire gli indirizzi, il che porta a tutti i tipi di buchi di sicurezza.

0
artificialidiot

Uno dei pochi esempi sull'allocazione e la distruzione in luoghi diversi è la creazione di thread (il parametro che si passa). Ma anche in questo caso è facile. Ecco la funzione/metodo che crea un thread:

struct myparams {
int x;
std::vector<double> z;
}

std::auto_ptr<myparams> param(new myparams(x, ...));
// Release the ownership in case thread creation is successfull
if (0 == pthread_create(&th, NULL, th_func, param.get()) param.release();
...

Qui invece la funzione thread

extern "C" void* th_func(void* p) {
   try {
       std::auto_ptr<myparams> param((myparams*)p);
       ...
   } catch(...) {
   }
   return 0;
}

Abbastanza facile, vero? Nel caso in cui la creazione del thread non riesca, la risorsa verrà liberata (eliminata) da auto_ptr, altrimenti la proprietà verrà passata al thread. Cosa succede se il thread è così veloce che dopo la creazione rilascia la risorsa prima di

param.release();

viene chiamato nella funzione/metodo principale? Niente! Perché "diremo" a auto_ptr di ignorare la deallocazione. La gestione della memoria C++ è semplice, vero? Saluti,

Ema!

0
Emanuele Oriani

Gestisci la memoria nello stesso modo in cui gestisci altre risorse (handle, file, connessioni db, socket ...). GC non ti aiuterebbe neanche con loro.

0

È possibile intercettare le funzioni di allocazione della memoria e vedere se ci sono alcune zone di memoria non liberate all'uscita dal programma (anche se non è adatto per tutte le applicazioni).

Può anche essere fatto in fase di compilazione sostituendo gli operatori con nuove funzioni di allocazione della memoria ed eliminazione e altre.

Ad esempio, controlla in questo sito [Debugging allocazione della memoria in C++] Nota: esiste un trucco per eliminare l'operatore anche qualcosa del genere:

#define DEBUG_DELETE PrepareDelete(__LINE__,__FILE__); delete
#define delete DEBUG_DELETE

È possibile memorizzare in alcune variabili il nome del file e quando l'operatore di eliminazione sovraccarico saprà da quale posizione è stato chiamato. In questo modo puoi avere la traccia di ogni cancellazione e malloc dal tuo programma. Alla fine della sequenza di controllo della memoria dovresti essere in grado di segnalare quale blocco allocato di memoria non è stato "cancellato" identificandolo con il nome del file e il numero di riga, suppongo che tu voglia.

Puoi anche provare qualcosa come BoundsChecker in Visual Studio che è piuttosto interessante e facile da usare.

0
INS

Avvolgiamo tutte le nostre funzioni di allocazione con un livello che aggiunge una breve stringa nella parte anteriore e un flag di sentinella alla fine. Quindi, ad esempio, avresti una chiamata a "myalloc (pszSomeString, iSize, iAlignment); o nuovo (" description ", iSize) MyObject (); che alloca internamente la dimensione specificata più spazio sufficiente per la tua intestazione e sentinella. , non dimenticare di commentarlo per build senza debug! Ci vuole un po 'più di memoria per farlo, ma i benefici superano di gran lunga i costi.

Ciò ha tre vantaggi: in primo luogo ti consente di tracciare facilmente e rapidamente quale codice perde, facendo ricerche rapide per il codice allocato in determinate "zone" ma non ripulito quando quelle zone avrebbero dovuto essere liberate. Può anche essere utile rilevare quando un limite è stato sovrascritto controllando che tutte le sentinelle siano intatte. Questo ci ha salvato numerose volte nel tentativo di trovare quei crash ben nascosti o passi falsi dell'array. Il terzo vantaggio è nel tracciare l'uso della memoria per vedere chi sono i grandi giocatori: una raccolta di alcune descrizioni in un MemDump ti dice quando il "suono" occupa molto più spazio di quanto ti aspettassi, per esempio.

0
screenglow