2009-10-23 7 views
9

In C++, supponendo che non ci sia ottimizzazione, i seguenti due programmi finiscono con lo stesso codice macchina di allocazione di memoria?In che modo l'allocazione automatica della memoria funziona effettivamente in C++?

int main() 
{  
    int i; 
    int *p; 
} 

int main() 
{ 
    int *p = new int; 
    delete p; 
} 
+9

Come possono avere la stessa allocazione di memoria? Il primo alloca una dimensione per un int (in pila) e un puntatore (in pila). Il secondo alloca un puntatore sullo stack e lo spazio per un int sull'heap. – bobbyalex

+1

Fondamentalmente la mia domanda è "Al momento dell'esecuzione, qual è la differenza tra stack e heap?" – Tarquila

+2

Prova qui: http://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap – Alex

risposta

17

No, senza ottimizzazione ...

int main() 
{  
    int i; 
    int *p; 
} 

non fa quasi nulla - solo un paio di istruzioni per regolare lo stack pointer, ma

int main() 
{ 
    int *p = new int; 
    delete p; 
} 

alloca un blocco di memoria heap e poi lo libera, questo è un sacco di lavoro (sono serio qui - l'allocazione dell'heap non è un'operazione banale).

+4

+1, l'allocazione dell'heap richiama molte informazioni sulla conservazione dei libri, solo per parlare dell'overhead della memoria. –

2

Sembra che tu non sappia lo stack e l'heap. Il tuo primo esempio consiste nell'assegnare un po 'di memoria allo stack che verrà eliminata non appena sarà fuori portata. La memoria nell'heap che si ottiene usando malloc/new rimarrà in attesa fino a quando non la cancellerai usando free/delete.

+1

Quando viene "cancellato appena esce dall'oscilloscopio" - il compilatore inserisce il codice per farlo? – Tarquila

+0

Il compilatore gestisce lo stack e i frame di stack richiesti per le regole di scoping, quindi in un certo senso sì ... ma "eliminare" qualcosa nello stack è banale in quanto implica solo la decrementazione di un puntatore allo stack e non è come l'allocazione dell'heap e deallocazione. – workmad3

+0

Sì, sposta il puntatore dello stack all'indietro. La pila ha una dimensione fissa (o almeno puoi fingere che sia). – gnud

2

Nel primo programma le variabili si trovano tutte in pila. Non stai allocando alcuna memoria dinamica. 'p' è solo in pila e se ti denigrerai otterrai spazzatura. Nel secondo programma stai creando un valore intero sull'heap. "p" indica effettivamente una memoria valida in questo caso. Si potrebbe effettivamente dereference p e impostarlo a qualcosa di significativo in tutta sicurezza:

*p = 5; 

Questo è valido il secondo programma (prima della cancellazione), non il primo. Spero possa aiutare.

6
int i; 
    int *p; 

^Assegnazione di un intero e una lancetta intero sulla pila

int *p = new int; 
delete p; 

^Assegnazione di un puntatore intero sulla pila e blocco della dimensione di intero sul mucchio

EDIT:

Differenza tra segmento Stack e segmento Heap

alt text http://www.maxi-pedia.com/web_files/images/HeapAndStack.png

void another_function(){ 
    int var1_in_other_function; /* Stack- main-y-sr-another_function-var1_in_other_function */ 
    int var2_in_other_function;/* Stack- main-y-sr-another_function-var1_in_other_function-var2_in_other_function */ 
} 
int main() {      /* Stack- main */ 
    int y;      /* Stack- main-y */ 
    char str;      /* Stack- main-y-sr */ 
    another_function();   /*Stack- main-y-sr-another_function*/ 
    return 1 ;     /* Stack- main-y-sr */ //stack will be empty after this statement       
} 

Ogni volta che qualsiasi programma inizia l'esecuzione memorizza tutte le sue variabili in particolare locazione di memoria memoy chiamato segmento di stack. Ad esempio, nel caso di C/C++, la prima funzione chiamata è main. quindi sarà messo in pila prima. Qualsiasi variabile all'interno del main verrà messa in pila mentre il programma viene eseguito. Ora come principale è la prima funzione chiamata sarà l'ultima funzione a restituire qualsiasi valore (o verrà estratto dallo stack).

Ora quando si assegna dinamicamente memoria utilizzando new viene utilizzata un'altra posizione di memoria speciale chiamata Segmento di heap. Anche se i dati reali sono presenti sul puntatore dell'heap si trova sullo stack.

+0

Questo è utile. Nel tuo diagramma, hai disegnato il mucchio con le frecce - suppongo tu intenda che ci può essere una qualsiasi quantità di distanza (la memoria che non appartiene al programma) tra lo stack e l'heap? – bobobobo

+0

A ogni processo sono allocati 4 GB di memoria virtuale, in modo che la memoria venga distribuita tra quei segmenti – Xinus

23

Per capire meglio cosa sta succedendo, immaginiamo di avere solo un sistema operativo molto primitivo in esecuzione su un processore a 16 bit in grado di eseguire un solo processo alla volta. Vale a dire: solo un programma può essere eseguito contemporaneamente. Inoltre, facciamo finta che tutti gli interrupt siano disabilitati.

C'è un costrutto nel nostro processore chiamato stack. Lo stack è un costrutto logico imposto sulla memoria fisica. Diciamo che la nostra RAM esiste negli indirizzi da E000 a FFFF. Ciò significa che il nostro programma in esecuzione può utilizzare questa memoria come preferiamo. Immaginiamo che il nostro sistema operativo dica che E000 a EFFF è lo stack e che F000 a FFFF è l'heap.

Lo stack è gestito da hardware e da istruzioni macchina. Non c'è davvero molto che dobbiamo fare per mantenerlo. Tutto quello che dobbiamo fare (o il nostro sistema operativo) è assicurarci di impostare un indirizzo corretto per l'inizio dello stack. Il puntatore dello stack è un'entità fisica che risiede nell'hardware (processore) ed è gestito dalle istruzioni del processore. In questo caso, il nostro puntatore stack verrebbe impostato su EFFF (supponendo che lo stack aumenti INDIETRO, che è piuttosto comune, -). Con un linguaggio compilato come C, quando si chiama una funzione, si spinge qualsiasi argomento passato alla funzione nello stack. Ogni argomento ha una certa dimensione. int è in genere 16 o 32 bit, il char è in genere 8 bit, ecc. Supponiamo che sul nostro sistema, int e int * siano 16 bit. Per ogni argomento, il puntatore dello stack è DECREMENTED (-) per sizeof (argomento) e l'argomento viene copiato nello stack. Quindi, tutte le variabili che hai dichiarato nell'ambito sono spinte nello stack nello stesso modo, ma i loro valori non sono inizializzati.

Diamo riconsiderare due esempi simili ai vostri due esempi.

int hello(int eeep) 
{ 
    int i; 
    int *p; 
} 

Cosa accade qui sul nostro sistema a 16 bit è il seguente: 1) EEEP spinta nello stack. Ciò significa che decrementiamo il puntatore dello stack su EFFD (poiché sizeof (int) è 2) e quindi effettivamente copiamo eeep per indirizzare EFFE (il valore corrente del nostro puntatore dello stack, meno 1 perché il nostro puntatore dello stack punta al primo spot disponibile dopo l'assegnazione). A volte ci sono istruzioni che possono fare entrambe le cose in un colpo solo (supponendo che tu stia copiando i dati che si inseriscono in un registro, altrimenti dovresti copiare manualmente ogni elemento di un tipo di dati nella giusta posizione nello stack - ordine!).

2) creare spazio per i. Questo probabilmente significa solo decrementare il puntatore dello stack su EFFB.

3) creare spazio per p. Questo probabilmente significa solo decrementare il puntatore dello stack su EFF9.

Allora il nostro programma viene eseguito, ricordare dove vivono le nostre variabili (EEEP inizia alle EFFE, ho a EFFC, e p a EFFA). La cosa importante da ricordare è che anche se lo stack conta INDIETRO, le variabili continuano a funzionare IN AVANTI (questo dipende in realtà da endianness, ma il punto è che & eeep == EFFE, non EFFF).

Quando la funzione chiude, semplicemente incremento (++) il puntatore stack 6, (perché 3 "oggetti", non C++ genere, di misura 2 sono state spinte in pila.

Ora, il secondo scenario è molto più difficile da spiegare, perché ci sono tanti metodi per realizzare è che è quasi impossibile da spiegare su internet.

int hello(int eeep) 
{ 
    int *p = malloc(sizeof(int));//C's pseudo-equivalent of new 
    free(p);//C's pseudo-equivalent of delete 
} 

EEEP e P sono ancora spinti e allocati sullo stack come nel precedente esempio, in questo caso, inizializziamo p al risultato di una chiamata di funzione: che malloc (o new, ma new fa di più in C++. trotori quando appropriato, e tutto il resto.) fa è andare a questa scatola nera chiamata HEAP e ottiene un indirizzo di memoria libera. Il nostro sistema operativo gestirà l'heap per noi, ma dobbiamo informarlo quando vogliamo la memoria e quando abbiamo finito.

Nell'esempio, quando chiamiamo malloc(), il sistema operativo restituirà un blocco di 2 byte (sizeof (int) sul nostro sistema è 2) fornendoci l'indirizzo iniziale di questi byte. Diciamo che la prima chiamata ci ha dato l'indirizzo F000. Il sistema operativo tiene quindi traccia degli indirizzi F000 e F001 attualmente in uso. Quando chiamiamo libero (p), il sistema operativo trova il blocco di memoria a cui punta p e segna 2 byte come non usato (perché sizeof (stella p) è 2). Se invece allociamo più memoria, l'indirizzo F002 verrà probabilmente restituito come blocco iniziale della nuova memoria. Si noti che malloc() stesso è una funzione. Quando p viene messo in pila per la chiamata di malloc(), il p viene copiato nuovamente nello stack al primo indirizzo aperto che ha abbastanza spazio sullo stack per adattarsi alla dimensione di p (probabilmente EFFB, perché abbiamo solo premuto 2 le cose nello stack questa volta di dimensione 2, e sizeof (p) è 2), e il puntatore dello stack è nuovamente decrementato su EFF9, e malloc() metterà le sue variabili locali nello stack a partire da questa posizione. Quando termina Malloc, esso espelle tutti gli elementi dallo stack e imposta il puntatore dello stack su ciò che era prima che venisse chiamato. Il valore di ritorno di malloc(), una stella vuota, sarà probabilmente inserito in un registro (di solito l'accumulatore su molti sistemi) per il nostro uso.

In fase di implementazione, entrambi gli esempi REALMENTE non sono così semplici. Quando assegni la memoria dello stack, per una nuova chiamata di funzione, devi assicurarti di salvare il tuo stato (salva tutti i registri) in modo che la nuova funzione non cancelli i valori in modo permanente. Questo di solito comporta anche spingerli in pila. Allo stesso modo, in genere si salva il registro del programma in modo da poter tornare al punto corretto dopo il ritorno della subroutine. I gestori della memoria usano la memoria per "ricordare" quale memoria è stata distribuita e cosa no. La memoria virtuale e la segmentazione della memoria complicano ulteriormente questo processo, e gli algoritmi di gestione della memoria devono continuamente spostare i blocchi (e proteggerli anche) al fine di prevenire la frammentazione della memoria (un argomento a sé stante), e questo si lega alla memoria virtuale anche. Il secondo esempio è davvero un grande contenitore di vermi rispetto al primo esempio. Inoltre, l'esecuzione di più processi rende tutto ciò molto più complicato, poiché ogni processo ha il proprio stack e l'heap può essere utilizzato da più processi (il che significa che deve proteggersi). Inoltre, ogni architettura del processore è diversa. Alcune architetture si aspettano che tu imposti il ​​puntatore dello stack sul primo indirizzo libero nello stack, altri si aspettano che tu lo punti al primo punto non libero.

Spero che questo abbia aiutato. Per favore mi faccia sapere.

avviso, tutti gli esempi precedenti sono per una macchina fittizia che è eccessivamente semplificata. Su hardware reale, questo diventa un po 'più peloso.

modifica: gli asterischi non vengono visualizzati. Li ho sostituiti con la parola "stella"


Per quello che vale, se usiamo (soprattutto) lo stesso codice negli esempi, in sostituzione di "ciao" con "example1" e "esempio2", rispettivamente, abbiamo ottenere il seguente output di assembly per Intel su Windows.

 
    .file "test1.c" 
    .text 
.globl _example1 
    .def _example1; .scl 2; .type 32; .endef 
_example1: 
    pushl %ebp 
    movl %esp, %ebp 
    subl $8, %esp 
    leave 
    ret 
.globl _example2 
    .def _example2; .scl 2; .type 32; .endef 
_example2: 
    pushl %ebp 
    movl %esp, %ebp 
    subl $8, %esp 
    movl $4, (%esp) 
    call _malloc 
    movl %eax, -4(%ebp) 
    movl -4(%ebp), %eax 
    movl %eax, (%esp) 
    call _free 
    leave 
    ret 
    .def _free; .scl 3; .type 32; .endef 
    .def _malloc; .scl 3; .type 32; .endef 
+0

come ci si può aspettare con qualcosa di noioso, continuo a trovare piccoli errori. per favore commentateli e aggiornerò se penso che sia un buon punto. –

+0

FF002 => F002. Entrata molto bella! Penso che potresti renderlo più chiaro con alcuni diagrammi ascii dei layout di memoria di cui parli, ma quello sarebbe molto più lavoro. :) – Bill

+0

questo è ora un wiki della comunità, quindi se qualcuno vorrebbe aggiungere i diagrammi (buon suggerimento!), Per favore sentiti libero di farlo. –

Problemi correlati