Cosa succede alla spazzatura in C ++?


51

Java ha un GC automatico che ogni tanto ferma il mondo, ma si occupa della spazzatura su un mucchio. Ora le applicazioni C / C ++ non hanno questi blocchi STW, anche il loro utilizzo della memoria non cresce all'infinito. Come viene raggiunto questo comportamento? Come vengono curati gli oggetti morti?


38
Nota: stop-the-world è una scelta di implementazione di alcuni garbage collector, ma certamente non tutti. Esistono GC simultanei, ad esempio, che vengono eseguiti contemporaneamente al mutatore (questo è ciò che gli sviluppatori GC chiamano il programma reale). Credo che tu possa acquistare una versione commerciale dell'open source JVM J9 di IBM che ha un collezionista senza pause simultaneo. Azul Zing ha un raccoglitore "senza pausa" che in realtà non è senza pausa ma estremamente veloce in modo che non ci siano pause evidenti (le sue pause GC sono nello stesso ordine di un interruttore di contesto del thread del sistema operativo, che di solito non viene visto come una pausa) .
Jörg W Mittag,

14
La maggior parte dei (a lungo in esecuzione) programmi C ++ che uso fare avere l'utilizzo della memoria che cresce tempo su unboundedly. È possibile che tu non abbia l'abitudine di lasciare i programmi aperti per più di qualche giorno alla volta?
Jonathan Cast

12
Tieni presente che con il moderno C ++ e i suoi costrutti non è più necessario eliminare manualmente la memoria (a meno che tu non stia cercando una speciale ottimizzazione), perché puoi gestire la memoria dinamica tramite puntatori intelligenti. Ovviamente, aggiunge un certo sovraccarico allo sviluppo del C ++ e devi stare un po 'più attento, ma non è una cosa completamente diversa, devi solo ricordare di usare il costrutto del puntatore intelligente invece di chiamare semplicemente il manuale new.
Andy,

9
Si noti che è ancora possibile avere perdite di memoria in una lingua raccolta rifiuti. Non ho familiarità con Java, ma purtroppo le perdite di memoria sono abbastanza comuni nel mondo GC gestito di .NET. Gli oggetti a cui fa riferimento indirettamente un campo statico non vengono raccolti automaticamente, i gestori di eventi sono una fonte molto comune di perdite e la natura non deterministica della garbage collection rende impossibile eliminare completamente la necessità di liberare manualmente le risorse (portando all'IDisposable modello). Detto questo, il modello di gestione della memoria C ++ usato correttamente è di gran lunga superiore alla garbage collection.
Cody Grey,

26
What happens to garbage in C++? Di solito non è compilato in un eseguibile?
BJ Myers,

Risposte:


100

Il programmatore è responsabile di assicurare che gli oggetti che hanno creato newvengano eliminati tramite delete. Se un oggetto viene creato, ma non distrutto prima che l'ultimo puntatore o riferimento ad esso esca dall'ambito, cade attraverso le fessure e diventa una perdita di memoria .

Sfortunatamente per C, C ++ e altri linguaggi che non includono un GC, questo si accumula semplicemente nel tempo. Può causare l'esaurimento della memoria di un'applicazione o del sistema e l'impossibilità di allocare nuovi blocchi di memoria. A questo punto, l'utente deve ricorrere alla chiusura dell'applicazione in modo che il sistema operativo possa recuperare quella memoria utilizzata.

Per quanto riguarda l'attenuazione di questo problema, ci sono diverse cose che rendono la vita di un programmatore molto più semplice. Questi sono principalmente supportati dalla natura dell'ambito .

int main()
{
    int* variableThatIsAPointer = new int;
    int variableInt = 0;

    delete variableThatIsAPointer;
}

Qui, abbiamo creato due variabili. Esistono in Block Scope , come definito dalle {}parentesi graffe. Quando l'esecuzione esce da questo ambito, questi oggetti verranno automaticamente eliminati. In questo caso, variableThatIsAPointercome suggerisce il nome, è un puntatore a un oggetto in memoria. Quando esce dall'ambito, il puntatore viene eliminato, ma l'oggetto a cui punta rimane. Qui, deletequesto oggetto prima che esca dall'ambito per garantire che non vi siano perdite di memoria. Tuttavia, avremmo potuto passare questo puntatore altrove e aspettarci che venga eliminato in seguito.

Questa natura dell'ambito si estende alle classi:

class Foo
{
public:
    int bar; // Will be deleted when Foo is deleted
    int* otherBar; // Still need to call delete
}

Qui, si applica lo stesso principio. Non dobbiamo preoccuparci di barquando Fooviene eliminato. Tuttavia per otherBar, viene eliminato solo il puntatore. Se otherBarè l'unico puntatore valido a qualunque oggetto a cui punta, probabilmente dovremmo deletefarlo nel Foodistruttore. Questo è il concetto alla base di RAII

l'allocazione delle risorse (acquisizione) viene eseguita durante la creazione dell'oggetto (in particolare l'inizializzazione), dal costruttore, mentre la deallocazione delle risorse (rilascio) viene eseguita durante la distruzione dell'oggetto (in particolare la finalizzazione), dal distruttore. Pertanto, si garantisce che la risorsa sia trattenuta tra il termine dell'inizializzazione e l'inizio della finalizzazione (il mantenimento delle risorse è un invariante di classe) e deve essere trattenuta solo quando l'oggetto è vivo. Quindi se non ci sono perdite di oggetti, non ci sono perdite di risorse.

RAII è anche la tipica forza motrice dietro Smart Pointers . Nella libreria standard C ++, questi sono std::shared_ptr, std::unique_ptre std::weak_ptr; anche se ho visto e usato altre shared_ptr/ weak_ptrimplementazioni che seguono gli stessi concetti. Per questi, un contatore di riferimento tiene traccia di quanti puntatori ci sono per un determinato oggetto e automaticamente deletes l'oggetto quando non ci sono più riferimenti ad esso.

Oltre a ciò, tutto si riduce alle pratiche e alla disciplina adeguate per un programmatore per garantire che il loro codice gestisca correttamente gli oggetti.


4
cancellato via delete- questo è quello che stavo cercando. Eccezionale.
Ju Shua,

3
Potresti voler aggiungere dei meccanismi di scoping forniti in c ++ che consentono a gran parte del nuovo e dell'eliminazione di essere resi per lo più automatici.
whatsisname

9
@whatsisname non è che il nuovo ed elimina siano resi automatici, è che in molti casi non si verificano affatto
Caleth

10
La deletesi chiama automaticamente per voi da puntatori intelligenti , se li si usa così si dovrebbe considerare l'utilizzo di loro ogni momento in cui un magazzino automatico non può essere utilizzato.
Marian Spanik,

11
@JuShua Nota che quando scrivi C ++ moderno, non dovresti mai aver bisogno di avere deletenel tuo codice applicazione (e da C ++ 14 in poi, lo stesso con new), ma usa invece i puntatori intelligenti e RAII per cancellare gli oggetti heap. std::unique_ptrtipo e std::make_uniquefunzione sono la sostituzione diretta e più semplice di newe deleteal livello del codice dell'applicazione.
hyde,

82

C ++ non ha Garbage Collection.

Le applicazioni C ++ sono necessarie per smaltire i propri rifiuti.

I programmatori di applicazioni C ++ sono tenuti a comprenderlo.

Quando dimenticano, il risultato è chiamato "perdita di memoria".


22
Sicuramente ti sei assicurato che la tua risposta non contenga nemmeno immondizia, né
scaldabagno

15
@leftaroundabout: grazie. Lo considero un complimento.
John R. Strohm,

1
OK, questa risposta senza immondizia ha una parola chiave da cercare: perdita di memoria. Sarebbe anche bello in qualche modo citare newe delete.
Ruslan,

4
@Ruslan Lo stesso vale anche per malloce free, o new[]e delete[], o di qualsiasi altro ripartitori (come Windows di GlobalAlloc, LocalAlloc, SHAlloc, CoTaskMemAlloc, VirtualAlloc, HeapAlloc, ...), e la memoria allocata per voi (ad esempio via fopen).
user253751

43

In C, C ++ e altri sistemi senza Garbage Collector, allo sviluppatore vengono offerte funzionalità dal linguaggio e dalle sue librerie per indicare quando è possibile recuperare la memoria.

La funzione più semplice è la memorizzazione automatica . Molte volte, la lingua stessa assicura che gli articoli vengano eliminati:

int global = 0; // automatic storage

int foo(int a, int b) {
    static int local = 1; // automatic storage

    int c = a + b; // automatic storage

    return c;
}

In questi casi, il compilatore ha il compito di sapere quando quei valori non sono utilizzati e di recuperare la memoria ad essi associata.

Quando si utilizza l' archiviazione dinamica , in C, la memoria viene tradizionalmente allocata malloce recuperata con free. In C ++, la memoria viene tradizionalmente allocata newe recuperata con delete.

C non è cambiato molto nel corso degli anni, tuttavia moderno C ++ rifugge newe deletecompletamente e si basa invece sulla funzionalità di libreria (che siano essi stessi utenti newe deletein modo appropriato):

  • i puntatori intelligenti sono i più famosi: std::unique_ptrestd::shared_ptr
  • ma contenitori sono molto più diffuse realtà: std::string, std::vector, std::map, ... gestire tutte internamente memoria allocata dinamicamente trasparente

A proposito shared_ptr, c'è un rischio: se si forma un ciclo di riferimenti e non si interrompe, allora può esserci una perdita di memoria. Spetta allo sviluppatore evitare questa situazione, il modo più semplice è quello di evitare del shared_ptrtutto e il secondo è il più semplice evitare cicli a livello di tipo.

Di conseguenza, le perdite di memoria non sono un problema in C ++ , anche per i nuovi utenti, purché si astengano dall'uso new, deleteo std::shared_ptr. Questo è diverso da C dove è necessaria una disciplina rigorosa e generalmente insufficiente.


Tuttavia, questa risposta non sarebbe completa senza menzionare la sorella gemella delle perdite di memoria: puntatori penzolanti .

Un puntatore penzolante (o riferimento penzolante) è un pericolo creato mantenendo un puntatore o un riferimento a un oggetto che è morto. Per esempio:

int main() {
    std::vector<int> vec;
    vec.push_back(1);     // vec: [1]

    int& a = vec.back();

    vec.pop_back();       // vec: [], "a" is now dangling

    std::cout << a << "\n";
}

L'uso di un puntatore pendente o di un riferimento è Comportamento indefinito . In generale, per fortuna, si tratta di un arresto immediato; abbastanza spesso, sfortunatamente, questo causa prima la corruzione della memoria ... e di tanto in tanto si manifestano comportamenti strani perché il compilatore emette codice davvero strano.

Undefined Behavior è il problema più grande con C e C ++ fino ad oggi, in termini di sicurezza / correttezza dei programmi. Potresti voler dare un'occhiata a Rust per una lingua senza Garbage Collector e senza un comportamento indefinito.


17
Ri: "L'uso di un puntatore pendente o di un riferimento è un comportamento indefinito . In generale, per fortuna, si tratta di un arresto immediato": Davvero? Ciò non corrisponde affatto alla mia esperienza; al contrario, la mia esperienza è che l'uso di un puntatore penzolante non provoca quasi mai un arresto immediato. . .
Ruakh,

9
Sì, dato che per essere "penzoloni" un puntatore deve aver preso di mira la memoria allocata in precedenza in un punto, e è improbabile che quella memoria sia stata completamente decompressa dal processo in modo tale da non essere più accessibile, perché sarà un buon candidato per un riutilizzo immediato ... in pratica, i puntatori penzolanti non causano arresti anomali, causano caos.
Leushenko,

2
"Di conseguenza, le perdite di memoria non sono un problema in C ++" Certo che lo sono, ci sono sempre collegamenti C alle librerie da rovinare, così come ricorsivi shared_ptrs o persino ricorsivi unique_ptrs e altre situazioni.
Mooing Duck,

3
"Non è un problema in C ++, anche per i nuovi utenti" - lo qualificherei per "nuovi utenti che non provengono da un linguaggio simile a Java o C ".
leftaroundabout

3
@leftaroundabout: è qualificato "nella misura in cui astenersi dall'utilizzare new, deletee shared_ptr"; senza newe shared_ptrhai la proprietà diretta, quindi nessuna perdita. Certo, è probabile che tu abbia puntatori penzolanti, ecc ... ma temo che tu debba lasciare C ++ per sbarazzartene.
Matthieu M.,

27

C ++ ha questa cosa chiamata RAII . Fondamentalmente significa che la spazzatura viene ripulita man mano che procedi piuttosto che lasciarla in pila e lasciare che il pulitore riordini dopo di te. (immaginami nella mia stanza mentre guardo il calcio - mentre bevo lattine di birra e ne ho bisogno di nuove, il modo C ++ è di portare la lattina vuota nel cestino sulla strada per il frigo, il modo C # è di buttarla sul pavimento e aspetta che la cameriera li raccolga quando viene a fare le pulizie).

Ora è possibile perdere la memoria in C ++, ma per farlo è necessario abbandonare i soliti costrutti e tornare al modo C di fare le cose - allocare un blocco di memoria e tenere traccia di dove si trova quel blocco senza alcuna assistenza linguistica. Alcune persone dimenticano questo puntatore e quindi non possono rimuovere il blocco.


9
I puntatori condivisi (che utilizzano RAII) forniscono un modo moderno per creare perdite. Supponiamo che gli oggetti A e B facciano riferimento l'un l'altro tramite puntatori condivisi e nient'altro che faccia riferimento all'oggetto A o all'oggetto B. Il risultato è una perdita. Questo riferimento reciproco non è un problema nelle lingue con Garbage Collection.
David Hammen,

@DavidHammen certo, ma a costo di attraversare quasi ogni oggetto per esserne sicuro. Il tuo esempio di puntatori intelligenti ignora il fatto che il puntatore intelligente stesso andrà fuori dall'ambito e quindi gli oggetti verranno liberati. Supponi che un puntatore intelligente sia come un puntatore, non lo è, è un oggetto che viene passato nello stack come la maggior parte dei parametri. Questo non è molto diverso dalle perdite di memoria causate nei linguaggi GC. ad es. quello famoso in cui la rimozione di un gestore eventi da una classe UI lo lascia silenziosamente referenziato e quindi perde.
gbjbaanb,

1
@gbjbaanb nell'esempio con i puntatori intelligenti, nessuno dei due puntatori intelligenti esce mai dal campo di applicazione, ecco perché c'è una perdita. Poiché entrambi gli oggetti del puntatore intelligente sono allocati in un ambito dinamico , non lessicale, ognuno cerca di attendere l'altro prima di distruggerlo. Il fatto che i puntatori intelligenti siano oggetti reali in C ++ e non solo i puntatori è esattamente ciò che causa la perdita qui: gli oggetti puntatore intelligente aggiuntivi negli ambiti dello stack che puntavano anche agli oggetti contenitore non possono dislocarli quando si distruggono perché il refcount è non zero.
Leushenko,

2
Il modo .NET non è quello di buttarlo sul pavimento. Lo mantiene solo dov'era fino a quando la cameriera non viene in giro. E a causa del modo in cui .NET alloca la memoria in pratica (non contrattuale), l'heap è più simile a uno stack ad accesso casuale. È un po 'come avere una pila di contratti e documenti e passarci di tanto in tanto per scartare quelli che non sono più validi. E per renderlo più semplice, quelli che sopravvivono a ogni scarto vengono promossi in uno stack diverso, in modo da poter evitare di attraversare tutte le pile per la maggior parte del tempo - a meno che il primo stack non diventi abbastanza grande, la cameriera non tocchi le altre.
Luaan,

@Luaan era un'analogia ... Immagino che saresti più felice se dicessi che lascia le lattine sdraiate sul tavolo fino a quando la cameriera viene a ripulire.
gbjbaanb,

26

Va notato che è, nel caso del C ++, un malinteso comune che "è necessario eseguire la gestione manuale della memoria". In effetti, di solito non si esegue alcuna gestione della memoria nel codice.

Oggetti di dimensioni fisse (con durata dell'ambito)

Nella stragrande maggioranza dei casi, quando è necessario un oggetto, l'oggetto avrà una durata definita nel programma e verrà creato nello stack. Funziona con tutti i tipi di dati primitivi integrati, ma anche con istanze di classi e strutture:

class MyObject {
    public: int x;
};

int objTest()
{
    MyObject obj;
    obj.x = 5;
    return obj.x;
}

Gli oggetti dello stack vengono rimossi automaticamente al termine della funzione. In Java, gli oggetti vengono sempre creati sull'heap e quindi devono essere rimossi da alcuni meccanismi come la garbage collection. Questo non è un problema per gli oggetti dello stack.

Oggetti che gestiscono dati dinamici (con durata dell'ambito)

L'uso dello spazio nello stack funziona per oggetti di dimensioni fisse. Quando è necessaria una quantità variabile di spazio, ad esempio un array, viene utilizzato un altro approccio: l'elenco viene incapsulato in un oggetto di dimensioni fisse che gestisce la memoria dinamica per l'utente. Questo funziona perché gli oggetti possono avere una funzione di pulizia speciale, il distruttore. È garantito che venga chiamato quando l'oggetto esce dall'ambito e fa l'opposto del costruttore:

class MyList {        
public:
    // a fixed-size pointer to the actual memory.
    int* listOfInts; 
    // constructor: get memory
    MyList(size_t numElements) { listOfInts = new int[numElements]; }
    // destructor: free memory
    ~MyList() { delete[] listOfInts; }
};

int listTest()
{
    MyList list(1024);
    list.listOfInts[200] = 5;
    return list.listOfInts[200];
    // When MyList goes off stack here, its destructor is called and frees the memory.
}

Non esiste alcuna gestione della memoria nel codice in cui viene utilizzata la memoria. L'unica cosa che dobbiamo accertarci è che l'oggetto che abbiamo scritto abbia un distruttore adatto. Indipendentemente da come lasciamo il campo di applicazione listTest, che si tratti di un'eccezione o semplicemente di ritorno da esso, il distruttore ~MyList()verrà chiamato e non è necessario gestire alcuna memoria.

(Penso che sia una decisione di progettazione divertente usare l' operatore binario NOT~ , per indicare il distruttore. Quando usato sui numeri, inverte i bit; in analogia, qui indica che ciò che il costruttore ha fatto è invertito.)

Fondamentalmente tutti gli oggetti C ++ che richiedono memoria dinamica usano questo incapsulamento. È stato chiamato RAII ("acquisizione di risorse è inizializzazione"), che è un modo abbastanza strano di esprimere la semplice idea che gli oggetti si preoccupino del proprio contenuto; quello che acquisiscono è il loro da ripulire.

Oggetti polimorfici e durata oltre lo scopo

Ora, entrambi questi casi riguardavano una memoria che ha una durata chiaramente definita: la durata è la stessa dell'ambito. Se non vogliamo che un oggetto scada quando lasciamo l'ambito, esiste un terzo meccanismo che può gestire la memoria per noi: un puntatore intelligente. I puntatori intelligenti vengono utilizzati anche quando si hanno istanze di oggetti il ​​cui tipo varia in fase di esecuzione, ma che hanno un'interfaccia o una classe base comuni:

class MyDerivedObject : public MyObject {
    public: int y;
};
std::unique_ptr<MyObject> createObject()
{
    // actually creates an object of a derived class,
    // but the user doesn't need to know this.
    return std::make_unique<MyDerivedObject>();
}

int dynamicObjTest()
{
    std::unique_ptr<MyObject> obj = createObject();
    obj->x = 5;
    return obj->x;
    // At scope end, the unique_ptr automatically removes the object it contains,
    // calling its destructor if it has one.
}

Esiste un altro tipo di puntatore intelligente std::shared_ptr, per la condivisione di oggetti tra più client. Eliminano il loro oggetto contenuto solo quando l'ultimo client esce dall'ambito, quindi possono essere utilizzati in situazioni in cui è completamente sconosciuto quanti client ci saranno e per quanto tempo useranno l'oggetto.

In sintesi, vediamo che in realtà non si esegue alcuna gestione manuale della memoria. Tutto è incapsulato e quindi curato attraverso una gestione della memoria completamente automatica e basata sull'ambito. Nei casi in cui ciò non è sufficiente, vengono utilizzati i puntatori intelligenti che incapsulano la memoria non elaborata.

È considerata una cattiva pratica utilizzare puntatori non elaborati come proprietari di risorse in qualsiasi punto del codice C ++, allocazioni non elaborate all'esterno dei costruttori e deletechiamate non elaborate all'esterno dei distruttori, poiché sono quasi impossibili da gestire quando si verificano eccezioni e generalmente difficili da utilizzare in modo sicuro.

Il meglio: funziona con tutti i tipi di risorse

Uno dei maggiori vantaggi di RAII è che non si limita alla memoria. In realtà fornisce un modo molto naturale per gestire risorse come file e socket (apertura / chiusura) e meccanismi di sincronizzazione come mutex (blocco / sblocco). Fondamentalmente, ogni risorsa che può essere acquisita e deve essere rilasciata è gestita esattamente allo stesso modo in C ++ e nessuna di questa gestione è lasciata all'utente. È tutto incapsulato in classi che acquisiscono nel costruttore e rilasciano nel distruttore.

Ad esempio, una funzione che blocca un mutex viene solitamente scritta in questo modo in C ++:

void criticalSection() {
    std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
    doSynchronizedStuff();
} // myMutex is released here automatically

Altre lingue lo rendono molto più complicato, richiedendoti di farlo manualmente (ad esempio in una finallyclausola) o generando meccanismi specializzati che risolvono questo problema, ma non in un modo particolarmente elegante (di solito più tardi nella loro vita, quando abbastanza persone hanno ha sofferto per la mancanza). Tali meccanismi sono try-with-resources in Java e l' istruzione using in C #, entrambi i quali sono approssimazioni della RAII di C ++.

Quindi, per riassumere, tutto questo era un resoconto molto superficiale di RAII in C ++, ma spero che aiuti i lettori a capire che la gestione della memoria e persino delle risorse in C ++ non è solitamente "manuale", ma in realtà per lo più automatica.


7
Questa è l'unica risposta che non disinforma le persone né dipinge il C ++ più difficile o pericoloso di quanto non sia in realtà.
Alexander Revo,

6
A proposito, è considerato solo una cattiva pratica utilizzare il puntatore non elaborato come proprietari di risorse. Non c'è niente di sbagliato nell'usarli se indicano qualcosa che è garantito per sopravvivere al puntatore stesso.
Alexander Revo,

8
Io secondo Alexander. Sono sconcertato nel vedere che "C ++ non ha una gestione automatizzata della memoria, dimentica un deletee sei morto" risponde alle stelle sopra i 30 punti e viene accettato, mentre questo ha cinque. Qualcuno usa effettivamente C ++ qui?
Quentin,

8

Rispetto a C in particolare, il linguaggio non offre strumenti per gestire la memoria allocata dinamicamente. Sei assolutamente responsabile di assicurarti che ognuno *allocabbia un corrispondente freeda qualche parte.

Dove le cose si fanno davvero brutte è quando un'allocazione delle risorse fallisce a metà; riprovi, esegui il rollback e ricomincia dall'inizio, esegui il rollback e esci con un errore, esegui il cauzione e lasci che il sistema operativo lo gestisca?

Ad esempio, ecco una funzione per allocare un array 2D non contiguo. Il comportamento qui è che se si verifica un errore di allocazione a metà del processo, eseguiamo il rollback di tutto e restituiamo un'indicazione di errore utilizzando un puntatore NULL:

/**
 * Allocate space for an array of arrays; returns NULL
 * on error.
 */
int **newArr( size_t rows, size_t cols )
{
  int **arr = malloc( sizeof *arr * rows );
  size_t i;

  if ( arr ) // malloc returns NULL on failure
  {
    for ( i = 0; i < rows; i++ )
    {
      arr[i] = malloc( sizeof *arr[i] * cols );
      if ( !arr[i] )
      {
        /**
         * Whoopsie; we can't allocate any more memory for some reason.
         * We can't just return NULL at this point since we'll lose access
         * to the previously allocated memory, so we branch to some cleanup
         * code to undo the allocations made so far.  
         */
        goto cleanup;
      }
    }
  }
  goto done;

/**
 * We encountered a failure midway through memory allocation,
 * so we roll back all previous allocations and return NULL.
 */
cleanup:
  while ( i )         // this is why we didn't limit the scope of i to the for loop
    free( arr[--i] ); // delete previously allocated rows
  free( arr );        // delete arr object
  arr = NULL;

done:
  return arr;
}

Questo codice è brutto con quelli goto, ma, in assenza di qualsiasi meccanismo strutturato di gestione delle eccezioni, questo è praticamente l'unico modo per affrontare il problema senza salvare completamente, soprattutto se il codice di allocazione delle risorse è nidificato di più di un loop in profondità. Questa è una delle pochissime volte in cui gotoè effettivamente un'opzione attraente; altrimenti stai usando un mucchio di bandiere e ifdichiarazioni extra .

Puoi semplificarti la vita scrivendo funzioni allocatore / deallocatore dedicate per ogni risorsa, qualcosa del genere

Foo *newFoo( void )
{
  Foo *foo = malloc( sizeof *foo );
  if ( foo )
  {
    foo->bar = newBar();
    if ( !foo->bar ) goto cleanupBar;
    foo->bletch = newBletch(); 
    if ( !foo->bletch ) goto cleanupBletch;
    ...
  }
  goto done;

cleanupBletch:
  deleteBar( foo->bar );
  // fall through to clean up the rest

cleanupBar:
  free( foo );
  foo = NULL;

done:
  return foo;
}

void deleteFoo( Foo *f )
{
  deleteBar( f->bar );
  deleteBletch( f->bletch );
  free( f );
}

1
Questa è una buona risposta, anche con le gotodichiarazioni. Questa è una pratica raccomandata in alcune aree. È uno schema comunemente usato per proteggere dall'equivalente delle eccezioni in C. Dai un'occhiata al codice del kernel Linux, che è pieno zeppo di gotoaffermazioni e che non perde.
David Hammen,

"senza limitarsi a salvare completamente" -> in tutta onestà, se vuoi parlare di C, questa è probabilmente una buona pratica. C è un linguaggio usato al meglio per gestire blocchi di memoria che venivano da qualche altra parte, o per dividere piccoli blocchi di memoria in altre procedure, ma preferibilmente non fare entrambe le cose contemporaneamente in modo intercalato. Se stai usando "oggetti" classici in C, probabilmente non stai usando il linguaggio per i suoi punti di forza.
Leushenko,

Il secondo gotoè estraneo. Sarebbe più leggibile se cambiassi goto done;in return arr;e arr=NULL;done:return arr;in return NULL;. Sebbene in casi più complicati potrebbero esserci effettivamente più messaggi goto, iniziando a srotolarsi a diversi livelli di prontezza (cosa sarebbe fatto dallo stack di eccezioni che si svolgeva in C ++).
Ruslan,

2

Ho imparato a classificare i problemi di memoria in diverse categorie.

  • Una volta gocciola. Supponiamo che un programma perda 100 byte all'avvio, ma non perda mai più. Inseguendo ed eliminando quelle perdite di una volta è bello (mi piace avere un rapporto pulito da una capacità di rilevamento delle perdite) ma non è essenziale. A volte ci sono problemi più grandi che devono essere attaccati.

  • Perdite ripetute Una funzione che viene chiamata ripetutamente nel corso di una durata del programma che perde regolarmente memoria un grosso problema. Queste gocce tortureranno a morte il programma, e possibilmente il sistema operativo.

  • Riferimenti reciproci. Se gli oggetti A e B fanno riferimento l'un l'altro tramite puntatori condivisi, devi fare qualcosa di speciale, sia nella progettazione di quelle classi sia nel codice che implementa / usa quelle classi per interrompere la circolarità. (Questo non è un problema per le lingue raccolte con immondizia.)

  • Ricordando troppo. Questo è il cugino malvagio delle perdite di immondizia / memoria. RAII non aiuterà qui, né la raccolta dei rifiuti. Questo è un problema in qualsiasi lingua. Se una variabile attiva ha un percorso che la collega a un pezzo di memoria casuale, quel pezzo di memoria casuale non è spazzatura. Fare in modo che un programma diventi smemorato in modo che possa funzionare per diversi giorni è difficile. Realizzare un programma che può essere eseguito per diversi mesi (ad es. Fino a quando il disco non si guasta) è molto, molto complicato.

Non ho avuto seri problemi con le perdite da molto, molto tempo. L'uso di RAII in C ++ aiuta moltissimo a far fronte a gocciolamenti e perdite. (Bisogna però fare attenzione con i puntatori condivisi.) Ancora più importante, ho avuto problemi con le applicazioni il cui uso della memoria continua a crescere e crescere e crescere a causa di connessioni non osservate alla memoria che non servono più.


-6

Spetta al programmatore C ++ implementare la propria forma di garbage collection dove necessario. In caso contrario, si otterrà quella che viene chiamata "perdita di memoria". È abbastanza comune che i linguaggi "di alto livello" (come Java) abbiano incorporato la garbage collection, ma i linguaggi "di basso livello" come C e C ++ no.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.