Cosa succede ai rifiuti in C ++?

51

Java ha un GC automatico che una volta ogni tanto ferma il mondo, ma si occupa di garbage su un heap. Ora le applicazioni C / C ++ non hanno questi blocchi STW, anche il loro utilizzo della memoria non cresce all'infinito. Come si ottiene questo comportamento? Come vengono presi in considerazione gli oggetti morti?

    
posta Ju Shua 16.06.2016 - 16:26
fonte

8 risposte

101

Il programmatore ha la responsabilità di garantire che gli oggetti creati tramite new vengano eliminati tramite delete . Se un oggetto viene creato, ma non distrutto prima che l'ultimo puntatore o riferimento ad esso vada fuori ambito, cade attraverso le fessure e diventa un Perdita di memoria .

Sfortunatamente per C, C ++ e altri linguaggi che non includono un GC, questo si accumula semplicemente nel tempo. Può far sì che un'applicazione o il sistema esauriscano la memoria e non siano in grado di allocare nuovi blocchi di memoria. A questo punto, l'utente deve ricorrere alla conclusione dell'applicazione in modo che il Sistema operativo possa recuperare quella memoria utilizzata.

Per mitigare questo problema, ci sono molte cose che rendono la vita di un programmatore molto più facile. Questi sono principalmente supportati dalla natura di scope .

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 cancellati. In questo caso, variableThatIsAPointer , come suggerisce il nome, è un puntatore a un oggetto in memoria. Quando esce dal campo di applicazione, il puntatore viene eliminato, ma l'oggetto a cui punta rimane. Qui, abbiamo delete questo oggetto prima che vada fuori ambito per garantire che non ci siano perdite di memoria. Tuttavia avremmo potuto anche passare questo puntatore altrove e ci aspettavamo che venisse cancellato 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 bar quando Foo viene cancellato. Tuttavia per otherBar , viene eliminato solo il puntatore. Se otherBar è l'unico puntatore valido a qualsiasi oggetto a cui punta, dovremmo probabilmente delete nel distruttore di Foo . Questo è il concetto guida di RAII

resource allocation (acquisition) is done during object creation (specifically initialization), by the constructor, while resource deallocation (release) is done during object destruction (specifically finalization), by the destructor. Thus the resource is guaranteed to be held between when initialization finishes and finalization starts (holding the resources is a class invariant), and to be held only when the object is alive. Thus if there are no object leaks, there are no resource leaks.

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

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

    
risposta data 28.06.2016 - 23:04
fonte
84

C ++ non ha garbage collection.

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

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

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

    
risposta data 16.06.2016 - 16:29
fonte
43

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

La struttura più semplice è archiviazione automatica . Molte volte, la lingua stessa assicura che gli oggetti siano 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 reclamare la memoria ad essi associata.

Quando si utilizza archiviazione dinamica , in C, la memoria viene tradizionalmente allocata con malloc e recuperata con free . In C ++, la memoria viene tradizionalmente allocata con new e recuperata con delete .

C non è cambiato molto nel corso degli anni, tuttavia il C ++ moderno esenta new e delete completamente e fa affidamento invece su strutture di libreria (che usano new e delete appropriatamente):

  • i puntatori intelligenti sono i più famosi: std::unique_ptr e std::shared_ptr
  • ma i contenitori sono molto più diffusi in realtà: std::string , std::vector , std::map , ... tutto gestisce internamente la memoria allocata dinamicamente in modo trasparente

Parlando di shared_ptr , c'è un rischio: se un ciclo di riferimenti è formato e non rotto, allora la perdita di memoria può esserci. Spetta allo sviluppatore evitare questa situazione, il modo più semplice è di evitare shared_ptr del tutto e il secondo più semplice di evitare cicli a livello di testo.

Di conseguenza perdite di memoria non sono un problema in C ++ , anche per i nuovi utenti, a patto che si astengano dall'utilizzare new , delete o std::shared_ptr . Questo è diverso da C in cui è necessaria una disciplina convinta e generalmente insufficiente.

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

Un puntatore pendente (o riferimento penzolante) è un pericolo creato mantenendo un puntatore o un riferimento a un oggetto che è morto. Ad 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 riferimento, è Comportamento indefinito . In generale, per fortuna, questo è un incidente immediato; molto spesso, sfortunatamente, questo causa prima la corruzione della memoria ... e di quando in quando si verifica un comportamento strano perché il compilatore emette un codice davvero strano.

Comportamento indefinito è il più grande problema con C e C ++ fino ad oggi, in termini di sicurezza / correttezza dei programmi. Potresti voler controllare Rust per una lingua senza Garbage Collector e nessun comportamento non definito.

    
risposta data 16.06.2016 - 18:04
fonte
27

C ++ ha questa cosa chiamata RAII . Fondamentalmente significa che la spazzatura viene ripulita mentre vai piuttosto che lasciarla in un mucchio e lasciare che il pulitore riordini dopo di te. (immaginami nella mia stanza a guardare 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 frigorifero, il modo C # è di buttarlo sul pavimento e aspetta che la cameriera li raccolga quando viene a fare le pulizie).

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

    
risposta data 16.06.2016 - 17:07
fonte
26

Va notato che è, nel caso del C ++, un malinteso comune che "è necessario fare la gestione manuale della memoria". Di fatto, di solito non esegui alcuna gestione della memoria nel tuo codice.

Oggetti a dimensione fissa (con durata dell'ambito)

Nella maggior parte dei casi quando hai bisogno di un oggetto, l'oggetto avrà una durata definita nel tuo programma e verrà creato nello stack. Funziona per tutti i tipi di dati primitivi incorporati, ma anche per le istanze di classi e strutture:

class MyObject {
    public: int x;
};

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

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

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

L'uso dello spazio sulla pila funziona per oggetti di dimensioni fisse. Quando hai bisogno di una quantità variabile di spazio, come un array, viene utilizzato un altro approccio: la lista è incapsulata in un oggetto a dimensione fissa che gestisce la memoria dinamica per te. 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 dal modo in cui lasciamo l'ambito di listTest , sia tramite un'eccezione o semplicemente ritornando da esso, verrà chiamato il distruttore ~MyList() e non è necessario gestire alcuna memoria.

(Penso che sia una decisione divertente progettare di usare l'operatore 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 necessitano di memoria dinamica usano questo incapsulamento. È stato chiamato RAII ("acquisizione delle risorse è inizializzazione"), che è piuttosto un modo strano per esprimere la semplice idea che gli oggetti si preoccupino dei propri contenuti; quello che acquistano è il loro da pulire.

Oggetti polimorfici e durata oltre l'ambito

Ora, entrambi questi casi erano per la memoria che ha una durata ben definita: la durata è la stessa dell'oscilloscopio. 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 anche utilizzati quando si hanno istanze di oggetti il cui tipo varia in fase di esecuzione, ma che hanno un'interfaccia o una classe base comune:

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.
}

C'è un altro tipo di puntatore intelligente, std::shared_ptr , per condividere oggetti tra diversi client. Eliminano solo il loro oggetto contenuto 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 breve, vediamo che non esegui alcuna gestione manuale della memoria. Tutto è incapsulato e viene poi gestito con una gestione della memoria completamente automatica e basata su un ambito. Nei casi in cui questo non è sufficiente, vengono utilizzati puntatori intelligenti che incapsulano la memoria non elaborata.

È considerata una pratica estremamente scorretta utilizzare i puntatori grezzi come proprietari di risorse ovunque nel codice C ++, allocazioni non elaborate al di fuori dei costruttori e chiamate delete al di fuori dei distruttori, poiché sono quasi impossibili da gestire quando si verificano eccezioni e in generale difficile da usare in sicurezza.

La migliore: funziona per 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 (locking / unlocking). Fondamentalmente, ogni risorsa che può essere acquisita e deve essere rilasciata viene gestita esattamente nello stesso modo in C ++ e nessuna di queste viene lasciata all'utente. È tutto incapsulato in classi che acquisiscono nel costruttore e rilasciano nel distruttore.

Ad esempio, una funzione che blocca un mutex è 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, richiedendo che tu lo faccia manualmente (ad esempio in una clausola finally ) o generano meccanismi specializzati che risolvono questo problema, ma non in un modo particolarmente elegante (di solito più tardi nella loro vita, quando abbastanza persone hanno sofferto della mancanza). Tali meccanismi sono try-with-resources in Java e l'istruzione using in C #, entrambi sono approssimazioni del 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 memoria e persino la gestione delle risorse in C ++ non sono di solito "manuali", ma in realtà per lo più automatico.

    
risposta data 17.06.2016 - 02:17
fonte
9

Rispetto a C in particolare, la lingua non fornisce strumenti per gestire la memoria allocata dinamicamente. Sei assolutamente responsabile di assicurarti che ogni *alloc abbia un free corrispondente da qualche parte.

Dove le cose vanno veramente male quando l'assegnazione di una risorsa fallisce a metà strada; si riprova, si esegue il rollback e si ricomincia dall'inizio, si esegue il rollback e si esce con un errore, si esegue una semplice cauzione e si lascia 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 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 è butt-ugly con quei goto s, ma, in assenza di qualsiasi tipo di meccanismo di gestione delle eccezioni strutturato, questo è praticamente l'unico modo per affrontare il problema senza salvataggio completo, specialmente se il codice di allocazione delle risorse è nidificato più di un loop in profondità. Questa è una delle pochissime volte in cui goto è in realtà un'opzione interessante; in caso contrario, stai utilizzando un gruppo di flag e dichiarazioni extra if .

Puoi semplificarti la vita scrivendo funzioni dedicate di allocatore / deallocator per ogni risorsa, ad esempio

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 );
}
    
risposta data 16.06.2016 - 21:28
fonte
2

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

  • Una volta gocciola. Supponiamo che un programma rilasci 100 byte al momento dell'avvio, solo che non ci siano più perdite. Inseguire ed eliminare quelle perdite una tantum è 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 della durata di un programma che perde regolarmente la memoria un grosso problema. Questi gocciolamenti porteranno a morte il programma, e forse il sistema operativo, fino alla morte.

  • riferimenti reciproci. Se gli oggetti A e B si fanno riferimento l'un l'altro tramite i puntatori condivisi, devi fare qualcosa di speciale, sia nel design di quelle classi che nel codice che implementa / usa quelle classi per rompere la circolarità. (Questo non è un problema per i linguaggi raccolti con garbage.)

  • Ricordando troppo. Questo è il cugino cattivo di immondizia / perdite di memoria. RAII non aiuterà qui, né sarà garbage collection. Questo è un problema in qualsiasi lingua. Se alcune variabili attive hanno un percorso che la collega a un blocco casuale di memoria, quel pezzo casuale di memoria non è spazzatura. Fare in modo che un programma diventi smemorato per poter funzionare per diversi giorni è complicato. Realizzare un programma che può essere eseguito per diversi mesi (ad es. Fino a quando il disco non va a buon fine) è molto, molto difficile.

Non ho avuto un problema serio con le perdite per molto, molto tempo. L'utilizzo di RAII in C ++ aiuta molto a risolvere le perdite e le perdite. (Uno però deve fare attenzione con i puntatori condivisi.) Molto 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 mirate alla memoria che non è più di alcun uso.

    
risposta data 16.06.2016 - 22:29
fonte
-6

Spetta al programmatore C ++ implementare la propria forma di garbage collection laddove necessario. In caso contrario, si verificherà una cosiddetta "perdita di memoria". È piuttosto comune che linguaggi 'di alto livello' (come Java) abbiano incorporato la garbage collection, ma i linguaggi 'di basso livello' come C e C ++ non lo fanno.

    
risposta data 16.06.2016 - 19:36
fonte

Leggi altre domande sui tag