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

Quali sono alcuni suggerimenti generali per assicurarmi di non perdere memoria nei programmi C ++? Come faccio a capire chi deve liberare memoria che è stata allocata dynamicmente?

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

Approvo a fondo tutti i consigli su RAII e puntatori intelligenti, ma mi piacerebbe anche aggiungere un suggerimento di livello leggermente superiore: la memoria più semplice da gestire è la memoria che non hai mai assegnato. A differenza delle lingue come C # e Java, dove praticamente tutto è un riferimento, in C ++ dovresti mettere gli oggetti nello stack ogni volta che puoi. Come ho visto molte persone (incluso il Dr Stroustrup) sottolineano, il motivo principale per cui la raccolta dei rifiuti non è mai stata popolare in C ++ è che il C ++ ben scritto non produce molti rifiuti in primo luogo.

Non scrivere

Object* x = new Object; 

o anche

 shared_ptr x(new Object); 

quando puoi scrivere

 Object x; 

Usa RAII

  • Elimina la raccolta dati inutili (usa RAII invece). Si noti che anche il Garbage Collector può perdere (se si dimentica di “null” alcuni riferimenti in Java / C #), e che Garbage Collector non ti aiuterà a disporre delle risorse (se hai un object che ha acquisito un handle per un file, il file non verrà liberato automaticamente quando l’object uscirà dall’ambito se non lo si fa manualmente in Java o si utilizza il pattern “Dispose” in C #).
  • Dimentica la regola “un ritorno per funzione” . Questo è un buon consiglio per evitare perdite, ma è obsoleto in C ++ a causa del suo uso di eccezioni (usa RAII invece).
  • E mentre il “Sandwich Pattern” è un buon consiglio C, è obsoleto in C ++ a causa del suo uso di eccezioni (usa RAII invece).

Questo post sembra essere ripetitivo, ma in C ++, lo schema più semplice da conoscere è RAII .

Impara a usare puntatori intelligenti, sia da boost, TR1 o anche low_ptr (ma spesso abbastanza efficiente) auto_ptr (ma devi conoscere i suoi 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 la maggior parte delle volte non ti darà nessuno).

Vedi sotto un confronto tra 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 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

Per riassumere (dopo il commento di Ogre Psalm33 ), RAII si basa su tre concetti:

  • Una volta che l’object è costruito, funziona! Acquisisci risorse nel costruttore.
  • La distruzione dell’object è sufficiente! Risorse gratuite nel distruttore.
  • È tutto sugli ambiti! Gli oggetti con scope (vedi esempio doRAIIStatic sopra) saranno costruiti alla loro dichiarazione e saranno distrutti nel momento in cui l’esecuzione esce dall’ambito, indipendentemente dall’uscita (ritorno, interruzione, eccezione, ecc.).

Ciò significa che nel codice C ++ corretto, la maggior parte degli oggetti non sarà costruita con new , e sarà invece dichiarata in pila. E per quelli costruiti usando new , tutti saranno in qualche modo mirati (ad es. Collegati 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 di alcuni oggetti in Java che fanno un uso intensivo di try / finally per quel caso) …

Modifica (2012-02-12)

“gli oggetti con scope … saranno distrutti … non importa l’uscita” non è completamente vero. ci sono modi per ingannare RAII. qualsiasi sapore di terminate () ignorerà la pulizia. exit (EXIT_SUCCESS) è un ossimoro a questo proposito.

– Wilhelmtell

wilhelmtell ha ragione su questo: ci sono modi eccezionali per ingannare RAII, il che porta tutti alla fermata brusca del processo.

Questi sono modi eccezionali perché il codice C ++ non è pieno di terminazione, uscita, ecc. O, nel caso di eccezioni, vogliamo un’eccezione non gestita per arrestare il processo e core core l’immagine di memoria così com’è, e non dopo la pulizia.

Ma dobbiamo ancora sapere su questi casi perché, mentre raramente accadono, possono ancora accadere.

(chi chiama terminate o exit con un codice C ++ casuale? … Ricordo di aver avuto a che fare con questo problema quando giocavo con GLUT : questa libreria è molto orientata al C, andando fino a progettarla triggersmente per rendere le cose difficili agli sviluppatori C ++ come non preoccuparsi dei dati allocati nello stack o di prendere decisioni “interessanti” sul non tornare mai dal loro ciclo principale … Non voglio commentare a riguardo) .

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

Invece di

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

boost :: shared_ptr verrà automaticamente eliminato una volta che il conteggio dei riferimenti è zero:

 int main() { boost::shared_ptr 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 object, non dovrai tenere traccia di se l’object è ancora in uso. puntatore condiviso, viene distrutto.

Questa non è una panacea, comunque. Sebbene tu possa accedere al puntatore di base, non vorrai passarlo a un’API di terze parti a meno che tu non fossi sicuro di ciò che stava facendo. Un sacco di volte, i tuoi “postare” roba su qualche altro thread per il lavoro da fare DOPO che l’ambito di creazione è finito. Questo è comune con PostThreadMessage in Win32:

 void foo() { boost::shared_ptr obj(new Object()); // Simplified here PostThreadMessage(...., (LPARAM)ob.get()); // Destructor destroys! pointer sent to PostThreadMessage is invalid! Zohnoes! } 

Come sempre, usa il tuo cappuccio pensante con qualsiasi strumento …

Leggi su RAII e assicurati di aver capito.

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

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

Se hai bisogno di “nuovo” un object allora la maggior parte delle volte avrà un unico proprietario ovvio per il resto della sua vita. Per questa situazione tendo ad usare un sacco di modelli di collezioni che sono progettati per ‘possedere’ oggetti memorizzati in essi da un puntatore. Sono implementati con il vettore STL e con i contenitori di mappe ma presentano alcune differenze:

  • Queste raccolte non possono essere copiate o assegnate a. (una volta contengono oggetti).
  • Puntatori agli oggetti sono 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 destructed e non vuoto.)
  • Poiché memorizzano i puntatori, puoi anche memorizzare oggetti ereditati in questi contenitori.

Il mio beaf con STL è che è così focalizzato sugli oggetti Value mentre nella maggior parte degli oggetti applicazioni sono entity framework uniche che non hanno una semantica di copia significativa richiesta per l’uso in quei contenitori.

Bah, voi ragazzi e i vostri nuovi spazzini della spazzatura …

Regole molto forti sulla “proprietà” – quale object o parte del software ha il diritto di cancellare l’object. Commenti chiari e nomi di variabili saggi per rendere evidente se un puntatore “possiede” o “guarda, non toccare”. Per aiutare a decidere chi possiede cosa, seguire il più ansible 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 che sia difficile evitarlo.

In ogni programma che richiede strutture dati complesse, creo un albero preciso e definito di oggetti contenenti 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’ quadro ha bisogno di accedere ad un’altra, per eseguire la scansione su arabi o altro; questi sono i “semplici occhi”. Per l’esempio di scena 3D: un object usa una trama ma non possiede; altri oggetti potrebbero usare quella stessa trama. La distruzione di un object non richiama la distruzione di trame.

Sì, richiede molto tempo, ma è quello che faccio. Raramente ho perdite di memoria o altri problemi. Ma poi lavoro nell’arena limitata del software di acquisizione, acquisizione dati e grafica ad alte prestazioni. Non mi occupo spesso di transazioni come nel settore bancario ed e-commerce, GUI basate su eventi o caos asincrono in rete. Forse i modi nuovi di zecca hanno un vantaggio lì!

Ottima domanda!

se si utilizza c ++ e si sta sviluppando un’applicazione boud CPU-e-memory in tempo reale (come i giochi) è necessario scrivere il proprio Memory Manager.

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

  • L’allocatore di dimensioni fisse è molto discusso, ovunque nella rete

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

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

  • Un grande elenco di risorse può essere trovato in questo articolo

Non iniziare a scrivere da solo un allocatore inutile di noob … prima fai da te.

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

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

C’è già molto su come non perdere, ma se hai bisogno di uno strumento che ti aiuti a tenere traccia delle perdite dai un’occhiata a:

Puntatori intelligenti dell’utente ovunque tu possa! Intere classi di perdite di memoria vanno via.

Condividi e conosci le regole di proprietà della memoria nel tuo progetto. L’uso delle regole COM rende la consistenza ottimale (i parametri [in] sono di proprietà del chiamante, il callee deve copiare; [out] i parametri sono di proprietà del chiamante, il callee deve fare una copia se conserva un riferimento, ecc.)

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

È disponibile sulla maggior parte dei gusti di Linux (incluso Android) e su Darwin.

Se si usano i test unitari per i propri programmi, si dovrebbe prendere l’abitudine di eseguire sistematicamente valgrind sui test. Eviterà potenzialmente molte perdite di memoria in una fase iniziale. Di solito è anche più facile individuarli in test semplici che in un software completo.

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

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

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

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

È ovvio, ma assicurati di scriverlo prima di digitare qualsiasi codice nell’ambito

Una fonte frequente di questi bug è quando si ha un metodo che accetta un riferimento o un puntatore a un object ma lascia la proprietà poco chiara. Lo stile e le convenzioni sui commenti possono renderlo meno probabile.

Lascia che sia il caso in cui la funzione assume la proprietà dell’object. In tutte le situazioni in cui ciò accade, assicurati di scrivere un commento accanto alla funzione nel file di intestazione indicandolo. Dovresti cercare di assicurarti che nella maggior parte dei casi il modulo o la class che assegna un object sia anche responsabile della deallocazione.

L’uso di const può essere di grande aiuto in alcuni casi. Se una funzione non modificherà un object e non memorizzerà un riferimento ad esso che persiste dopo il suo ritorno, accetta un riferimento const. Dalla lettura del codice del chiamante sarà ovvio che la tua funzione non ha accettato la proprietà dell’object. Si potrebbe avere la stessa funzione accettare un puntatore non-const e il chiamante può o non aver assunto che il callee abbia accettato la proprietà, ma con un riferimento const non c’è dubbio.

Non utilizzare riferimenti non const negli elenchi di argomenti. Non è chiaro quando si legge il codice del chiamante che il callee può aver mantenuto un riferimento al parametro.

Non sono d’accordo con i commenti che raccomandano il riferimento contato puntatori. Questo di solito funziona bene, ma quando si ha un bug e non funziona, specialmente se il distruttore fa qualcosa di non banale, come in un programma multithread. Cercherò sicuramente di adattare il tuo design per non aver bisogno del conteggio dei riferimenti, se non è troppo difficile.

Suggerimenti in ordine di importanza:

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

-Tip # 2 Usa RAII

-Tip # 3 Usa gli smartpoint di boost

-Tip # 4 Non scrivere il tuo buggy Smartpointers, usa boost (su un progetto al quale sto lavorando in questo momento non posso usare boost, e ho sofferto di dover eseguire il debug dei miei puntatori intelligenti, sicuramente non prendere lo stesso percorso di nuovo, ma poi di nuovo in questo momento non posso aggiungere ulteriore spinta alle nostre dipendenze)

-Tip # 5 Se alcune critiche casuali / non performanti (come nei giochi con migliaia di oggetti) funzionano guardando il contenitore del 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

Se puoi, usa boost_ap_ptr e standard C ++ auto_ptr. Quelli trasmettono la semantica della proprietà.

Quando restituisci un auto_ptr, stai dicendo al chiamante che stai dando loro la proprietà della memoria.

Quando ritorni a shared_ptr, stai dicendo al chiamante che hai un riferimento e ne prendono parte, ma non è solo una loro responsabilità.

Queste semantiche si applicano anche ai parametri. Se il chiamante ti passa un auto_ptr, ti stanno dando la proprietà.

Altri hanno menzionato i modi per evitare perdite di memoria in primo luogo (come 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.

Il memcheck di Valgrind è eccellente e gratuito.

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

 #ifdef _DEBUG #define new DEBUG_NEW #endif 

Quindi, quando esegui il debugging con VS2003 o versioni successive, ti verrà detto di eventuali perdite quando il tuo programma si chiude (tiene traccia di nuovo / cancella). È fondamentale, ma mi ha aiutato in passato.

valgrind (disponibile solo per piattaforms * nix) è un controller di memoria molto bello

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

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

Se hai bisogno di infrangere una di queste regole, documentala.

Si tratta di proprietà del puntatore.

È ansible intercettare le funzioni di allocazione della memoria e vedere se ci sono alcune zone di memoria non liberate all’uscita del programma (sebbene non sia adatta a tutte le applicazioni).

Può anche essere fatto in fase di compilazione sostituendo gli operatori new e delete e altre funzioni di allocazione della memoria.

Ad esempio, controlla in questo sito [Debugging memory allocation in C ++] Nota: c’è un trucco per l’operatore delete anche qualcosa del genere:

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

È ansible memorizzare in alcune variabili il nome del file e quando l’operatore di overload sovraccarico saprà qual è il luogo da cui è stato chiamato. In questo modo puoi avere la traccia di ogni eliminazione e malloc dal tuo programma. Alla fine della sequenza di controllo della memoria dovresti essere in grado di riportare quale blocco di memoria assegnato non è stato “cancellato” identificandolo per nome di file e numero di riga, che credo sia quello che vuoi.

Si potrebbe anche provare qualcosa come BoundsChecker sotto Visual Studio che è piuttosto interessante e facile da usare.

Comprimiamo tutte le nostre funzioni di allocazione con un livello che aggiunge una breve stringa nella parte anteriore e un contrassegno sentinella alla fine. Ad esempio, avresti una chiamata a “myalloc (pszSomeString, iSize, iAlignment) o new (” description “, iSize) MyObject (), che alloca internamente la dimensione specificata più spazio sufficiente per l’intestazione e la sentinella. , non dimenticare di commentare questo per le build non di debug! Ci vuole un po ‘di memoria in più per fare questo, ma i benefici superano di gran lunga i costi.

Questo ha tre vantaggi: in primo luogo consente di rintracciare facilmente e rapidamente il codice che perde, effettuando ricerche rapide per il codice assegnato in determinate “zone” ma non ripulito quando tali zone dovrebbero essere liberate. Può anche essere utile rilevare quando un confine è stato sovrascritto controllando che tutte le sentinelle siano intatte. Questo ci ha salvato molte volte quando cercavo di trovare quegli arresti ben nascosti o passi falsi. 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 “suono” sta prendendo più spazio di quanto previsto, per esempio.

Il C ++ è progettato pensando a RAII. Non c’è davvero modo migliore per gestire la memoria in C ++, penso. Ma attenzione a non allocare pezzi molto grandi (come gli oggetti buffer) sull’ambito locale. Può causare overflow dello stack e, se vi è un difetto nei controlli durante l’utilizzo di quel blocco, è ansible sovrascrivere altre variabili o restituire indirizzi, il che porta a tutti i tipi di falle di sicurezza.

Uno degli unici esempi di allocazione e distruzione in diversi luoghi è la creazione di thread (il parametro che si passa). Ma anche in questo caso è facile. Ecco la funzione / metodo per creare un thread:

 struct myparams { int x; std::vector z; } std::auto_ptr 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 param((myparams*)p); ... } catch(...) { } return 0; } 

Pretty easyn non è vero? Nel caso in cui la creazione del thread fallisca, la risorsa verrà liberata (cancellata) 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 del

 param.release(); 

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

Ema!

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

  • Cerca di evitare di allocare oggetti in modo dinamico. Finché le classi hanno costruttori e distruttori appropriati, usa una variabile del tipo di class, non un puntatore ad esso, ed eviti allocazione dynamic e deallocazione perché il compilatore lo farà per te.
    In realtà questo è anche il meccanismo utilizzato da “puntatori intelligenti” e indicato come RAII da alcuni degli altri scrittori ;-).
  • Quando passi oggetti ad altre funzioni, preferisci i parametri di riferimento sopra i puntatori. Questo evita alcuni possibili errori.
  • Dichiarare i parametri const, laddove ansible, in particolare i puntatori agli oggetti. In questo modo gli oggetti non possono essere liberati “accidentalmente” (eccetto se lanci il const via ;-))).
  • Riduci al minimo il numero di posizioni nel programma in cui si eseguono allocazioni e deallocazioni di memoria. Per esempio. se si assegnano o si liberano più volte lo stesso tipo, scrivere una funzione (o un metodo di fabbrica ;-)).
    In questo modo è ansible creare facilmente l’output di debug (che gli indirizzi sono allocati e deallocati, …), se necessario.
  • Utilizzare una funzione di fabbrica per allocare oggetti di più classi correlate da una singola funzione.
  • Se le tue classi hanno una class 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 $ / € / …).

Esattamente un ritorno da qualsiasi funzione. In questo modo puoi fare deallocation lì e non mancarlo mai.

È troppo facile fare un errore altrimenti:

 new a() if (Bad()) {delete a; return;} new b() if (Bad()) {delete a; delete b; return;} ... // etc.