Stack, statico e heap in C ++


160

Ho cercato, ma non ho capito molto bene questi tre concetti. Quando devo utilizzare l'allocazione dinamica (nell'heap) e qual è il suo vero vantaggio? Quali sono i problemi di statico e stack? Potrei scrivere un'intera applicazione senza allocare variabili nell'heap?

Ho sentito che altre lingue incorporano un "garbage collector", quindi non devi preoccuparti della memoria. Cosa fa il garbage collector?

Cosa potresti fare manipolando la memoria da solo che non potresti fare usando questo garbage collector?

Una volta qualcuno mi ha detto che con questa dichiarazione:

int * asafe=new int;

Ho un "puntatore a un puntatore". Cosa significa? È diverso da:

asafe=new int;

?


Qualche tempo fa è stata posta una domanda molto simile: cosa e dove sono stack e heap? Ci sono alcune risposte davvero valide a questa domanda che dovrebbe far luce sulla tua.
Scott Saad,

Risposte:


223

È stata posta una domanda simile , ma non si trattava di statica.

Riepilogo di quali memorie statiche, heap e stack sono:

  • Una variabile statica è sostanzialmente una variabile globale, anche se non è possibile accedervi a livello globale. Di solito c'è un indirizzo per esso che si trova nell'eseguibile stesso. C'è solo una copia per l'intero programma. Non importa quante volte entri in una chiamata di funzione (o classe) (e in quanti thread!) La variabile si riferisce alla stessa posizione di memoria.

  • L'heap è un mucchio di memoria che può essere utilizzato in modo dinamico. Se vuoi 4kb per un oggetto, l'allocatore dinamico cercherà attraverso il suo elenco di spazio libero nell'heap, sceglierà un pezzo da 4kb e te lo darà. Generalmente, l'allocatore di memoria dinamica (malloc, new, ecc.) Inizia alla fine della memoria e funziona all'indietro.

  • Spiegare come uno stack cresce e si restringe è un po 'al di fuori dell'ambito di questa risposta, ma basti dire che aggiungi e rimuovi sempre solo dalla fine. Le pile di solito iniziano in alto e scendono a indirizzi inferiori. La memoria si esaurisce quando lo stack incontra l'allocatore dinamico da qualche parte nel mezzo (ma fare riferimento alla memoria fisica e virtuale e alla frammentazione). Più thread richiedono più stack (il processo in genere riserva una dimensione minima per lo stack).

Quando vuoi usare ognuno di essi:

  • Statica / globuli sono utili per la memoria che sai che avrai sempre bisogno e sai che non vorrai mai deallocare. (A proposito, si può pensare che gli ambienti incorporati abbiano solo memoria statica ... lo stack e l'heap fanno parte di uno spazio di indirizzi noto condiviso da un terzo tipo di memoria: il codice del programma. I programmi eseguono spesso un'allocazione dinamica dal loro memoria statica quando hanno bisogno di cose come elenchi collegati, ma a prescindere, la memoria statica stessa (il buffer) non è essa stessa "allocata", ma piuttosto altri oggetti sono allocati dalla memoria mantenuta dal buffer per questo scopo. anche nei giochi non incorporati, e i giochi per console spesso eviteranno i meccanismi di memoria dinamica incorporati a favore del controllo rigoroso del processo di allocazione utilizzando buffer di dimensioni predefinite per tutte le allocazioni.)

  • Le variabili dello stack sono utili quando sai che finché la funzione è nell'ambito (nello stack da qualche parte), vorrai che le variabili rimangano. Gli stack sono utili per le variabili necessarie per il codice in cui si trovano, ma che non sono necessarie al di fuori di quel codice. Sono anche molto utili quando si accede a una risorsa, come un file, e si desidera che la risorsa scompaia automaticamente quando si lascia quel codice.

  • Le allocazioni di heap (memoria allocata dinamicamente) sono utili quando si desidera essere più flessibili di quanto sopra. Spesso, una funzione viene chiamata per rispondere a un evento (l'utente fa clic sul pulsante "Crea casella"). La risposta corretta potrebbe richiedere l'allocazione di un nuovo oggetto (un nuovo oggetto Box) che dovrebbe rimanere in sospeso per molto tempo dopo l'uscita dalla funzione, quindi non può essere nello stack. Ma non sai quante caselle vuoi all'inizio del programma, quindi non può essere statico.

Raccolta dei rifiuti

Ultimamente ho sentito parlare di quanto siano grandi i Garbage Collector, quindi forse un po 'di voce dissenziente sarebbe utile.

Garbage Collection è un meccanismo meraviglioso per quando le prestazioni non sono un grosso problema. Ho sentito che i GC stanno migliorando e diventando più sofisticati, ma il fatto è che potresti essere costretto ad accettare una penalità per le prestazioni (a seconda del caso d'uso). E se sei pigro, potrebbe ancora non funzionare correttamente. Nel migliore dei casi, i Garbage Collector si rendono conto che la tua memoria scompare quando si rende conto che non ci sono più riferimenti ad essa (vedi conteggio dei riferimenti). Tuttavia, se si dispone di un oggetto che fa riferimento a se stesso (eventualmente facendo riferimento a un altro oggetto che fa riferimento indietro), il conteggio dei riferimenti da solo non indicherà che la memoria può essere eliminata. In questo caso, il GC deve esaminare l'intera zuppa di riferimento e capire se ci sono isole a cui si fa riferimento solo da sole. Direttamente, immagino che si tratti di un'operazione O (n ^ 2), ma qualunque cosa sia, può andare male se si è interessati alle prestazioni. (Modifica: Martin B sottolinea che è O (n) per algoritmi ragionevolmente efficienti. Questo è ancora O (n) troppo se si è interessati alle prestazioni e si può deallocare in tempo costante senza raccolta dei rifiuti.)

Personalmente, quando sento dire che il C ++ non ha Garbage Collection, la mia mente lo contrassegna come una caratteristica del C ++, ma probabilmente sono in minoranza. Probabilmente la cosa più difficile da imparare per la programmazione in C e C ++ sono i puntatori e come gestire correttamente le allocazioni di memoria dinamica. Alcune altre lingue, come Python, sarebbero orribili senza GC, quindi penso che dipenda da ciò che vuoi da una lingua. Se vuoi prestazioni affidabili, allora C ++ senza garbage collection è l'unica cosa che mi viene in mente da questo lato di Fortran. Se vuoi facilità d'uso e ruote di addestramento (per salvarti dagli arresti anomali senza richiedere l'apprendimento di una "corretta" gestione della memoria), scegli qualcosa con un GC. Anche se sai come gestire bene la memoria, ti farà risparmiare tempo che puoi dedicare all'ottimizzazione di altro codice. In realtà non c'è più una penalità per le prestazioni, ma se hai davvero bisogno di prestazioni affidabili (e della capacità di sapere esattamente cosa sta succedendo, quando, sotto le coperte), mi atterrei al C ++. C'è una ragione per cui ogni motore di gioco principale di cui io abbia mai sentito parlare è in C ++ (se non in C o assembly). Python e altri vanno bene per gli script, ma non il motore di gioco principale.


Non è davvero rilevante per la domanda originale (o per niente, in realtà), ma hai le posizioni dello stack e heap all'indietro. In genere , lo stack cresce e l'heap cresce (anche se un heap in realtà non "cresce", quindi questa è un'enorme semplificazione eccessiva) ...
P Daddy,

non penso che questa domanda sia simile o addirittura duplicata dell'altra domanda. questo riguarda specificamente il C ++ e ciò che intendeva dire è quasi certamente le tre durate di archiviazione esistenti nel C ++. È possibile disporre di un oggetto dinamico allocato nella memoria statica, ad esempio un sovraccarico su nuovo.
Johannes Schaub -

7
Il tuo trattamento dispregiativo della raccolta dei rifiuti è stato un po 'meno utile.
P Daddy,

9
Spesso la garbage collection è oggi meglio della memoria di liberazione manuale perché accade quando c'è poco lavoro da fare, al contrario di liberare memoria che può avvenire proprio quando le prestazioni potrebbero essere utilizzate diversamente.
Georg Schölly,

3
Solo un piccolo commento: la garbage collection non ha complessità O (n ^ 2) (che, in effetti, sarebbe disastrosa per le prestazioni). Il tempo impiegato per un ciclo di raccolta dei rifiuti è proporzionale alla dimensione dell'heap: consultare hpl.hp.com/personal/Hans_Boehm/gc/complexity.html .
Martin B,

54

Ciò che segue ovviamente non è del tutto preciso. Prendilo con un granello di sale quando lo leggi :)

Bene, le tre cose a cui ti riferisci sono la durata di archiviazione automatica, statica e dinamica , che ha a che fare con la durata della vita degli oggetti e quando iniziano la vita.


Durata di conservazione automatica

Si utilizza la durata di archiviazione automatica per dati di breve durata e piccoli , necessari solo localmente all'interno di un blocco:

if(some condition) {
    int a[3]; // array a has automatic storage duration
    fill_it(a);
    print_it(a);
}

La durata termina non appena usciamo dal blocco e inizia non appena viene definito l'oggetto. Sono il tipo di durata di archiviazione più semplice e sono molto più veloci rispetto alla durata di archiviazione dinamica particolare.


Durata di conservazione statica

Si utilizza la durata dell'archiviazione statica per variabili libere, a cui è possibile accedere in qualsiasi momento da qualsiasi codice, se il loro ambito consente tale utilizzo (ambito dello spazio dei nomi) e per le variabili locali che devono prolungare la loro durata attraverso l'uscita dal loro ambito (ambito locale), e per le variabili membro che devono essere condivise da tutti gli oggetti della loro classe (ambito delle classi). La loro vita dipende l'ambito cui si trovano. Essi possono avere portata namespace e ambito locale e ambito di classe . Ciò che è vero in entrambi è che, una volta iniziata la loro vita, la vita finisce alla fine del programma . Ecco due esempi:

// static storage duration. in global namespace scope
string globalA; 
int main() {
    foo();
    foo();
}

void foo() {
    // static storage duration. in local scope
    static string localA;
    localA += "ab"
    cout << localA;
}

Il programma stampa ababab, perché localAnon viene distrutto all'uscita del suo blocco. Si può dire che gli oggetti con ambito locale iniziano la vita quando il controllo raggiunge la loro definizione . Per localA, succede quando viene inserito il corpo della funzione. Per gli oggetti nell'ambito dello spazio dei nomi, la durata inizia all'avvio del programma . Lo stesso vale per gli oggetti statici di ambito di classe:

class A {
    static string classScopeA;
};

string A::classScopeA;

A a, b; &a.classScopeA == &b.classScopeA == &A::classScopeA;

Come vedi, classScopeAnon è legato a oggetti particolari della sua classe, ma alla classe stessa. L'indirizzo di tutti e tre i nomi sopra è lo stesso e tutti indicano lo stesso oggetto. Esistono regole speciali su quando e come inizializzare gli oggetti statici, ma non preoccupiamoci di questo ora. Questo si intende con il termine fiasco dell'ordine di inizializzazione statica .


Durata della memoria dinamica

L'ultima durata della memoria è dinamica. Lo usi se vuoi che gli oggetti vivano su un'altra isola e vuoi mettere dei puntatori attorno a quel riferimento. Li usi anche se i tuoi oggetti sono grandi e se vuoi creare matrici di dimensioni conosciute solo in fase di esecuzione . A causa di questa flessibilità, gli oggetti con durata di archiviazione dinamica sono complicati e lenti da gestire. Gli oggetti con tale durata dinamica iniziano la vita quando si verifica una nuova chiamata dell'operatore appropriata :

int main() {
    // the object that s points to has dynamic storage 
    // duration
    string *s = new string;
    // pass a pointer pointing to the object around. 
    // the object itself isn't touched
    foo(s);
    delete s;
}

void foo(string *s) {
    cout << s->size();
}

La sua durata termina solo quando si chiama Elimina per loro. Se lo dimentichi, quegli oggetti non finiscono mai la vita. E agli oggetti di classe che definiscono un costruttore dichiarato dall'utente non verranno chiamati i loro distruttori. Gli oggetti con durata di archiviazione dinamica richiedono la gestione manuale della loro durata e delle risorse di memoria associate. Esistono librerie per facilitarne l'uso. La garbage collection esplicita per oggetti particolari può essere stabilita utilizzando un puntatore intelligente:

int main() {
    shared_ptr<string> s(new string);
    foo(s);
}

void foo(shared_ptr<string> s) {
    cout << s->size();
}

Non devi preoccuparti di chiamare delete: il ptr condiviso lo fa per te, se l'ultimo puntatore che fa riferimento all'oggetto non rientra nell'ambito. Lo stesso ptr condiviso ha una durata di memorizzazione automatica. Pertanto, la sua durata viene gestita automaticamente, consentendogli di verificare se deve eliminare l'oggetto puntato a dinamico nel suo distruttore. Per riferimento shared_ptr, consultare i documenti boost: http://www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm


39

È stato detto in modo elaborato, proprio come "la risposta breve":


  • durata variabile variabile (classe) = runtime programma (1)
    visibilità = determinato dai modificatori di accesso (privato / protetto / pubblico)

  • variabile statica (portata globale)
    durata = runtime programma (1)
    visibilità = unità di compilazione in cui è istanziata (2)

  • heap variabile
    vita = definito da te (nuovo da eliminare)
    visibilità = definito da te (qualunque cosa tu assegni il puntatore a)

  • impilare la
    visibilità variabile = dalla dichiarazione fino all'uscita dall'ambito della
    vita = dalla dichiarazione fino all'uscita dall'ambito della dichiarazione


(1) più esattamente: dall'inizializzazione fino alla deinizializzazione dell'unità di compilazione (cioè file C / C ++). L'ordine di inizializzazione delle unità di compilazione non è definito dallo standard.

(2) Attenzione: se si crea un'istanza di una variabile statica in un'intestazione, ogni unità di compilazione ottiene la propria copia.


5

Sono sicuro che uno dei pedanti troverà una risposta migliore a breve, ma la differenza principale è la velocità e le dimensioni.

Pila

Drammaticamente più veloce da allocare. Viene eseguito in O (1) poiché è allocato durante l'impostazione del frame dello stack, quindi è essenzialmente libero. Lo svantaggio è che se si esaurisce lo spazio di stack si viene disossati. Puoi regolare le dimensioni dello stack, ma IIRC hai ~ 2 MB con cui giocare. Inoltre, non appena si esce dalla funzione, tutto nello stack viene cancellato. Quindi può essere problematico fare riferimento in un secondo momento. (I puntatori per impilare oggetti allocati portano a bug.)

Mucchio

Drammaticamente più lento da allocare. Ma hai GB con cui giocare e puntare.

Netturbino

Il garbage collector è un codice che viene eseguito in background e libera la memoria. Quando si alloca memoria sull'heap, è molto facile dimenticare di liberarlo, che è noto come perdita di memoria. Nel tempo, la memoria consumata dall'applicazione cresce e cresce fino a quando non si blocca. Avere un garbage collector periodicamente libera la memoria che non ti serve più aiuta a eliminare questa classe di bug. Naturalmente questo ha un prezzo, poiché il cestino della spazzatura rallenta le cose.


3

Quali sono i problemi di statico e stack?

Il problema con l'allocazione "statica" è che l'allocazione viene effettuata in fase di compilazione: non è possibile utilizzarla per allocare un numero variabile di dati, il cui numero non è noto fino al runtime.

Il problema con l'allocazione sullo "stack" è che l'allocazione viene distrutta non appena la subroutine che esegue l'allocazione ritorna.

Potrei scrivere un'intera applicazione senza allocare variabili nell'heap?

Forse ma non un'applicazione banale, normale, grande (ma i cosiddetti programmi "incorporati" potrebbero essere scritti senza heap, usando un sottoinsieme di C ++).

Cosa fa il garbage collector?

Continua a guardare i tuoi dati ("mark and sweep") per rilevare quando l'applicazione non fa più riferimento a questi. Questo è utile per l'applicazione, perché l'applicazione non ha bisogno di deallocare i dati ... ma il garbage collector potrebbe essere costoso dal punto di vista computazionale.

I Garbage Collector non sono una caratteristica abituale della programmazione C ++.

Cosa potresti fare manipolando la memoria da solo che non potresti fare usando questo garbage collector?

Scopri i meccanismi C ++ per la deallocazione della memoria deterministica:

  • 'statico': mai deallocato
  • 'stack': non appena la variabile "esce dall'ambito"
  • 'heap': quando il puntatore viene eliminato (eliminato esplicitamente dall'applicazione o implicitamente eliminato all'interno di una subroutine in qualche modo)

1

L'allocazione della memoria dello stack (variabili di funzione, variabili locali) può essere problematica quando lo stack è troppo "profondo" e si trabocca la memoria disponibile per le allocazioni dello stack. L'heap è per gli oggetti a cui è necessario accedere da più thread o durante il ciclo di vita del programma. Puoi scrivere un intero programma senza usare l'heap.

Puoi perdere la memoria abbastanza facilmente senza un Garbage Collector, ma puoi anche dettare quando vengono liberati oggetti e memoria. Ho riscontrato problemi con Java durante l'esecuzione del GC e ho un processo in tempo reale, perché il GC è un thread esclusivo (nient'altro può essere eseguito). Quindi, se le prestazioni sono fondamentali e puoi garantire che non ci siano oggetti trapelati, non usare un GC è molto utile. Altrimenti ti fa odiare la vita quando l'applicazione consuma memoria e devi rintracciare l'origine di una perdita.


1

Che cosa succede se il tuo programma non conosce in anticipo la quantità di memoria da allocare (quindi non puoi utilizzare le variabili dello stack). Di 'elenchi collegati, gli elenchi possono crescere senza sapere in anticipo quali sono le sue dimensioni. Quindi l'allocazione su un heap ha senso per un elenco collegato quando non si è consapevoli di quanti elementi verrebbero inseriti in esso.


0

Un vantaggio di GC in alcune situazioni è un fastidio in altre; fare affidamento su GC incoraggia a non pensarci molto. In teoria, aspetta fino al periodo di inattività o fino a quando non è assolutamente necessario, quando ruberà la larghezza di banda e causerà la latenza di risposta nella tua app.

Ma non devi "non pensarci". Proprio come con qualsiasi altra cosa nelle app multithread, quando puoi cedere, puoi cedere. Ad esempio, in .Net, è possibile richiedere un GC; in questo modo, invece di utilizzare GC più lunghi e meno frequenti, è possibile avere GC più brevi e più frequenti e distribuire la latenza associata a questo sovraccarico.

Ma questo sconfigge l'attrazione principale di GC che sembra essere "incoraggiato a non doverci pensare molto perché è auto mat."

Se sei stato esposto per la prima volta alla programmazione prima che GC diventasse prevalente e ti sentissi a tuo agio con malloc / free e new / delete, allora potrebbe anche accadere che trovi GC un po 'fastidioso e / o diffidente (come si potrebbe non fidarsi di " ottimizzazione ", che ha avuto una cronologia a scacchi.) Molte app tollerano la latenza casuale. Ma per le app che non lo fanno, dove la latenza casuale è meno accettabile, una reazione comune è quella di evitare gli ambienti GC e spostarsi nella direzione di codice puramente non gestito (o dio non voglia, un'arte che muore a lungo, un linguaggio assembly).

Ho avuto uno studente estivo qui qualche tempo fa, uno stagista, un ragazzo intelligente, che era svezzato con GC; era così innamorato della superiorità di GC che anche durante la programmazione in C / C ++ non gestito si rifiutava di seguire il modello malloc / free new / delete perché, citando, "non dovresti farlo in un linguaggio di programmazione moderno". E tu sai? Per app di piccole dimensioni e di breve durata, puoi davvero cavartela, ma non per app di lunga durata.


0

Lo stack è una memoria allocata dal compilatore, ogni volta che compiliamo il programma, nel compilatore predefinito alloca un po 'di memoria dal sistema operativo (possiamo modificare le impostazioni dalle impostazioni del compilatore nel tuo IDE) e il sistema operativo è quello che ti dà la memoria, dipende su molte memorie disponibili sul sistema e molte altre cose, e arrivando a impilare la memoria viene allocato quando dichiariamo una variabile che copiano (riferimento come formali), quelle variabili che vengono spinte per impilare seguono alcune convenzioni di denominazione di default il suo CDECL in Visual Studios es: notazione infissa: c = a + b; la spinta dello stack viene eseguita da PUSHING da destra a sinistra, b per impilare, operatore, a per impilare e il risultato di quelli i, ec per impilare. Nella notazione pre-correzione: = + cab Qui tutte le variabili vengono spinte nello stack 1 ° (da destra a sinistra) e quindi vengono eseguite le operazioni. Questa memoria allocata dal compilatore è fissa. Supponiamo quindi che 1 MB di memoria sia allocato alla nostra applicazione, diciamo che le variabili hanno utilizzato 700 kb di memoria (tutte le variabili locali sono inviate allo stack a meno che non siano allocate dinamicamente), quindi la memoria 324 kb rimanente è allocata all'heap. E questo stack ha meno tempo di vita, quando termina l'ambito della funzione queste pile vengono cancellate.

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.