Domanda

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

È stato utile?

Soluzione

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

Altri suggerimenti

Approvo pienamente tutti i consigli su RAII e puntatori intelligenti, ma vorrei anche aggiungere un suggerimento di livello leggermente superiore:la memoria più semplice da gestire è quella 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 sottolineare da diverse persone (incluso il dottor Stroustrup), il motivo principale per cui la garbage collection 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;

Utilizzo RAII

  • Dimentica la raccolta dei rifiuti (Utilizzare invece RAII).Tieni presente che anche il Garbage Collector può presentare delle perdite (se dimentichi di "annullare" alcuni riferimenti in Java/C#) e che il Garbage Collector non ti aiuterà a smaltire le risorse (se hai 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 non si utilizza il modello "dispose" in C#).
  • Dimentica la regola "un reso per funzione"..Questo è un buon consiglio in C per evitare fughe di informazioni, ma è obsoleto in C++ a causa dell'uso delle eccezioni (usa invece RAII).
  • E mentre il "modello sandwich" è un buon consiglio in C, esso è obsoleto in C++ a causa del suo uso di eccezioni (usa invece RAII).

Questo post sembra ripetitivo, ma in C++ il modello più elementare da conoscere è 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 sia della sicurezza delle eccezioni che dell'eliminazione delle risorse in C++ e nessun altro modello (sandwich, ecc.) ti fornirà entrambi (e la maggior parte delle volte non te ne 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<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.
}

Di RAII

Per riassumere (dopo il commento di Salmo dell'Orco33), RAII si basa su tre concetti:

  • Una volta costruito l'oggetto, funziona e basta! Acquisisci risorse nel costruttore.
  • La distruzione degli oggetti è sufficiente! Libera risorse nel distruttore.
  • È tutta una questione di ambiti! Gli oggetti con ambito (vedere l'esempio doRAIIStatic sopra) verranno costruiti alla loro dichiarazione e verranno distrutti nel momento in cui l'esecuzione esce dall'ambito, indipendentemente dal modo in cui l'uscita (return, break, eccezione, ecc.).

Ciò significa che nel codice C++ corretto la maggior parte degli oggetti non verrà costruita con new, e verrà invece dichiarato nello stack.E per quelli costruiti utilizzando new, tutto sarà in qualche modo ambito (per 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 in tal caso)...

Modifica (12-02-2012)

"oggetti con ambito...verrà distrutto...non importa l'uscita", non è del tutto vero.ci sono modi per imbrogliare RAII.qualsiasi versione di terminate() ignorerà la pulizia.exit(EXIT_SUCCESS) è un ossimoro a questo proposito.

WilhelmTell

WilhelmTell ha perfettamente ragione:Ci sono eccezionale modi per ingannare RAII, che portano tutti al brusco arresto del processo.

Quelli sono eccezionale modi perché il codice C++ non è pieno di termina, exit, ecc. o, nel caso di eccezioni, vogliamo un eccezione non gestita per arrestare in modo anomalo il processo e eseguire il core dump della sua immagine di memoria così com'è e non dopo la pulizia.

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

(chi chiama terminate O exit nel codice C++ casuale?...Ricordo di aver dovuto affrontare quel problema mentre giocavo GLUT:Questa libreria è molto orientata al C, arrivando al punto di progettarla attivamente per rendere le cose difficili agli sviluppatori C++ come non preoccuparsi di impilare i dati allocati, o prendere decisioni "interessanti" in merito senza mai tornare dal loro ciclo principale...non commenterò questo).

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

Invece di

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

boost::shared_ptr verrà eliminato automaticamente una volta che il conteggio dei riferimenti sarà pari a 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.Pertanto, se hai più utenti del tuo oggetto, non dovrai tenere traccia se l'oggetto è ancora in uso.Una volta che nessuno fa riferimento al tuo puntatore condiviso, viene distrutto.

Questa però non è una panacea.Sebbene tu possa accedere al puntatore di base, non vorrai passarlo a un'API di terze parti a meno che tu non sia sicuro di ciò che sta facendo.Molte volte, il tuo materiale "pubblicato" su qualche altro thread per il lavoro da svolgere DOPO che l'ambito di creazione è terminato.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...

Leggere su RAII e assicurati di capirlo.

La maggior parte delle perdite di memoria sono il risultato della mancata chiarezza sulla proprietà e sulla durata degli oggetti.

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

Se hai bisogno di "nuovo" un oggetto, nella maggior parte dei casi avrà un unico proprietario ovvio per il resto della sua vita.Per questa situazione tendo a utilizzare una serie di modelli di raccolte progettati per "possedere" oggetti memorizzati in essi tramite puntatore.Sono implementati con i contenitori vettoriali e mappe STL ma presentano alcune differenze:

  • Queste raccolte non possono essere copiate o assegnate.(una volta che contengono oggetti.)
  • Al loro interno vengono inseriti puntatori agli oggetti.
  • Quando la raccolta viene eliminata, il distruttore viene prima chiamato su tutti gli oggetti nella raccolta.(Ho un'altra versione in cui afferma se distrutto e non vuoto.)
  • Poiché memorizzano i puntatori, puoi anche memorizzare oggetti ereditati in questi contenitori.

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

Bah, voi ragazzini e i vostri nuovi spazzini...

Regole molto rigide sulla "proprietà": quale oggetto o parte del software ha il diritto di eliminare l'oggetto.Commenti chiari e nomi saggi delle variabili per rendere ovvio se un puntatore "possiede" o è "basta guardare, non toccare".Per aiutare a decidere chi possiede cosa, seguire il più possibile lo schema "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 qualsiasi programma che richiede strutture dati complesse, creo un albero rigoroso e chiaro di oggetti contenenti altri oggetti, utilizzando i puntatori "proprietario".Questo albero modella la gerarchia di base dei concetti del dominio 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 accedere a un'altra, per scansionare array o altro;questi sono i "solo guardando".Per l'esempio della scena 3D: un oggetto utilizza una texture ma non la possiede;altri oggetti possono utilizzare la stessa trama.La distruzione di un oggetto sì non invocare la distruzione di qualsiasi texture.

Sì, richiede tempo, ma è quello che faccio.Raramente ho perdite di memoria o altri problemi.Ma poi lavoro nell'arena limitata dei software scientifici, di acquisizione dati e di grafica ad alte prestazioni.Non mi occupo spesso di 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 applicazioni di CPU e memoria in tempo reale (come i giochi) devi scrivere il tuo Memory Manager.

Penso che la cosa migliore che puoi fare sia unire alcuni lavori interessanti di vari autori, posso darti qualche suggerimento:

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

  • L'allocazione di piccoli oggetti è 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 uno straordinario articolo in Game Programming Gem 7 (2008) chiamato "High Performance Heap allocator" scritto da Dimitar Lazarov

  • È possibile trovare un ottimo elenco di risorse in Questo articolo

Non iniziare da solo a scrivere un allocatore inutile...DOCUMENTATI prima.

Una tecnica 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++ dovuti alla sicurezza delle eccezioni, ma l'idea di base è piuttosto semplice.

La questione generalmente si riduce a una questione 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 evitare perdite, ma se hai bisogno di uno strumento che ti aiuti a tenere traccia delle perdite, dai un'occhiata a:

Utilizza puntatori intelligenti ovunque tu possa!Intere classi di perdite di memoria scompaiono.

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

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

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

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

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

Inoltre, non utilizzare la memoria allocata manualmente se è presente una classe di libreria std (ad es.vettore).Assicurati di violare questa regola di avere un distruttore virtuale.

Se non puoi/non usi un puntatore intelligente per qualcosa (anche se dovrebbe essere un enorme campanello d'allarme), digita il tuo codice con:

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

È ovvio, ma assicurati di digitarlo Prima si digita 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 ne lascia la proprietà poco chiara.Le convenzioni di stile e di commento possono rendere questo meno probabile.

Lasciamo che il caso in cui la funzione acquisisca la proprietà dell'oggetto sia il caso speciale.In tutte le situazioni in cui ciò accade, assicurati di scrivere un commento accanto alla funzione nel file di intestazione che lo indica.Dovreste sforzarvi di assicurarvi che nella maggior parte dei casi il modulo o la classe che alloca un oggetto sia anche responsabile della sua deallocazione.

L'uso di const può aiutare molto in alcuni casi.Se una funzione non modificherà un oggetto 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'oggetto.Avresti potuto fare in modo che la stessa funzione accettasse un puntatore non const e il chiamante avrebbe potuto o meno presumere che il chiamato accettasse la proprietà, ma con un riferimento const non ci sono dubbi.

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

Non sono d'accordo con i commenti che raccomandano i puntatori contati di riferimento.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 ad adattare il tuo progetto in modo che non sia necessario il conteggio dei riferimenti se non è troppo difficile.

Suggerimenti in ordine di importanza:

-Suggerimento#1 Ricordatevi sempre di dichiarare i vostri distruttori "virtuali".

-Suggerimento n. 2 Usa RAII

-Suggerimento n. 3 Utilizza gli smartpointer di boost

-Suggerimento n. 4 Non scrivere i tuoi Smartpointer difettosi, usa boost (su un progetto a cui sto lavorando in questo momento non posso usare boost e ho sofferto nel dover eseguire il debug dei miei puntatori intelligenti, sicuramente non lo prenderei di nuovo lo stesso percorso, ma in questo momento non posso aggiungere potenziamento alle nostre dipendenze)

-Suggerimento n. 5 Se si tratta di un lavoro critico casuale/non prestazionale (come nei giochi con migliaia di oggetti), guarda il contenitore del puntatore di potenziamento di Thorsten Ottosen

-Suggerimento n. 6 Trova un'intestazione di rilevamento delle perdite per la tua piattaforma preferita, ad esempio l'intestazione "vld" di Visual Leak Detection

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

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

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

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

Altri hanno menzionato innanzitutto i modi per evitare perdite di memoria (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 si presentano.

Controllo della memoria di Valgrind è un eccellente 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 versione successiva, ti verranno comunicate eventuali perdite quando il programma termina (tiene traccia delle operazioni nuove/eliminate).È basilare, ma mi ha aiutato in passato.

valgrind (disponibile solo per piattaforme *nix) è un controllo della memoria molto carino

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

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

Se devi infrangere una qualsiasi di queste regole, documentalo.

È tutta una questione di proprietà del puntatore.

  • Cerca di evitare di allocare gli oggetti in modo dinamico.Finché le classi hanno costruttori e distruttori appropriati, usa una variabile del tipo di classe, non un puntatore ad essa, ed eviterai l'allocazione e la deallocazione dinamica perché il compilatore lo farà per te.
    In realtà questo è anche il meccanismo utilizzato dai "puntatori intelligenti" e chiamato RAII da alcuni degli altri scrittori ;-).
  • Quando passi oggetti ad altre funzioni, preferisci i parametri di riferimento ai puntatori.Ciò 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 il const ;-))).
  • Ridurre al minimo il numero di posizioni nel programma in cui si esegue l'allocazione e la deallocazione della memoria.E.G.se allochi o liberi lo stesso tipo più volte, scrivi una funzione per esso (o un metodo factory ;-)).
    In questo modo è possibile creare facilmente l'output di debug (quali indirizzi sono allocati e deallocati, ...), se necessario.
  • Utilizzare una funzione factory 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 utilizzando la stessa funzione (o metodo statico).
  • Controlla il tuo programma con strumenti come purificare (purtroppo molti $/€/...).

Puoi intercettare le funzioni di allocazione della memoria e vedere se ci sono delle zone di memoria non liberate all'uscita del programma (anche se non è adatto per Tutto le applicazioni).

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

Ad esempio, controlla questo luogo Allocazione della memoria di debug 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

È possibile memorizzare in alcune variabili il nome del file e quando l'operatore delete sovraccaricato saprà da quale luogo è stato chiamato.In questo modo puoi avere traccia di ogni eliminazione e malloc dal tuo programma.Alla fine della sequenza di controllo della memoria dovresti essere in grado di segnalare quale blocco di memoria allocato non è stato "eliminato" identificandolo con il nome del file e il numero di riga che immagino sia quello che vuoi.

Potresti anche provare qualcosa del genere Controllo dei limiti sotto Visual Studio che è piuttosto interessante e facile da usare.

Avvolgiamo tutte le nostre funzioni di allocazione con uno strato che aggiunge una breve stringa all'inizio e una bandiera sentinella alla fine.Quindi, ad esempio, dovresti chiamare "myalloc( pszSomeString, iSize, iAlignment );o new( "descrizione", iSize) MioOggetto();che alloca internamente la dimensione specificata più spazio sufficiente per l'intestazione e la sentinella.Naturalmente, non dimenticare di commentarlo per le build non di debug!Per farlo è necessaria un po' più di memoria, ma i vantaggi superano di gran lunga i costi.

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

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

Uno dei pochi esempi di allocazione e distruzione in luoghi diversi è la creazione del thread (il parametro che passi).Ma anche in questo caso è facile.Ecco la funzione/metodo per creare 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, non è vero?Nel caso in cui la creazione del thread fallisca, 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 del file

param.release();

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

Ema!

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

Esattamente un ritorno da qualsiasi funzione.In questo modo puoi effettuare la deallocazione lì e non perderla mai.

E' troppo facile sbagliare altrimenti:

new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top