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 dinamicamente?
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 dinamicamente?
Risposte:
Invece di gestire la memoria manualmente, prova a utilizzare i puntatori intelligenti ove applicabile.
Dai un'occhiata a Boost lib , TR1 e ai puntatori intelligenti .
Anche i puntatori intelligenti fanno ora parte dello standard C ++ chiamato C ++ 11 .
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) sottolineare, 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;
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 da auto_ptr (ma spesso abbastanza efficiente) (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.
}
Per riassumere (dopo il commento di Ogre Psalm33 ), RAII si basa su tre concetti:
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 mirato (ad esempio 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 fanno un uso intensivo di try
/ finally
per quel caso) ...
"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 ha ragione: ci sono modi eccezionali per imbrogliare RAII, il che porta a un brusco arresto del processo.
Questi sono modi eccezionali perché il codice C ++ non è disseminato di terminare, uscire, ecc., O nel caso delle eccezioni, vogliamo un'eccezione non gestita per arrestare il processo e scaricare la memoria come core, 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 preoccuparsene sullo stack di dati allocati o sull'avere decisioni "interessanti" sul non tornare mai dal loro ciclo principale ... non commenterò al riguardo) .
Ti consigliamo di guardare i puntatori intelligenti, come i 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 di creazione è finito. 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 ...
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 ad 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:
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.
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 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 rigoroso albero di oggetti ben definito 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 non invoca la distruzione di alcuna 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ì!
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 il 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 in questo articolo
Non iniziare a scrivere da solo un allocatore inutile ... No prima DOCUMENTO.
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.
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:
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.)
valgrind è un ottimo 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.
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 di digitare qualsiasi codice nell'ambito
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 poteva o meno supporre 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.
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 sofferto il debug dei miei puntatori intelligenti, sicuramente non prenderei lo stesso percorso di nuovo, ma poi di nuovo in questo momento non posso aggiungere spinta 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 delle perdite per la tua piattaforma preferita come l'intestazione "vld" di Visual Leak Detection
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à.
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.
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, durante il debug con VS2003 o superiore, ti verrà comunicato di eventuali perdite quando il tuo programma esce (tiene traccia di nuovo / elimina). È di base, ma mi ha aiutato in passato.
valgrind (disponibile solo per piattaforme * nix) è un ottimo controller di memoria
Se hai intenzione di gestire la tua memoria manualmente, hai due casi:
Se devi infrangere una di queste regole, ti preghiamo di documentarlo.
Si tratta della proprietà del puntatore.
È 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 a tutte le applicazioni).
Può anche essere eseguito in fase di compilazione sostituendo gli operatori con nuove funzioni di allocazione della memoria e 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.
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 new (" 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 vantaggi 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.
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.
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!
Gestisci la memoria nello stesso modo in cui gestisci altre risorse (handle, file, connessioni db, socket ...). GC non ti aiuterebbe neanche con loro.
Esattamente un ritorno da qualsiasi funzione. In questo modo puoi fare deallocazione lì e non perdere mai.
Altrimenti è troppo facile fare un errore:
new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.