Matrici, heap e stack e tipi di valore


134
int[] myIntegers;
myIntegers = new int[100];

Nel codice sopra, new int [100] sta generando l'array sull'heap? Da quello che ho letto su CLR tramite c #, la risposta è sì. Ma quello che non riesco a capire, è cosa succede agli int reali all'interno dell'array. Dato che sono tipi di valore, immagino che dovrebbero essere inscatolati, come posso, ad esempio, passare i miei numeri interi ad altre parti del programma e ingombrerebbe lo stack se fossero lasciati su di esso tutto il tempo . O mi sbaglio? Immagino che sarebbero solo inscatolati e vivrebbero nell'heap per tutto il tempo in cui l'array è esistito.

Risposte:


289

L'array è allocato sull'heap e gli ints non sono inscatolati.

La fonte della tua confusione è probabilmente perché le persone hanno detto che i tipi di riferimento sono allocati sullo heap e che i tipi di valore sono allocati sullo stack. Questa non è una rappresentazione del tutto accurata.

Tutte le variabili e i parametri locali sono allocati nello stack. Ciò include sia tipi di valore che tipi di riferimento. La differenza tra i due è solo ciò che è memorizzato nella variabile. Non sorprende che, per un tipo di valore, il valore del tipo sia memorizzato direttamente nella variabile e, per un tipo di riferimento, il valore del tipo è memorizzato nell'heap e un riferimento a questo valore è ciò che è memorizzato nella variabile.

Lo stesso vale per i campi. Quando la memoria viene allocata per un'istanza di un tipo aggregato (a classo a struct), deve includere l'archiviazione per ciascuno dei suoi campi di istanza. Per i campi del tipo di riferimento, questa memoria contiene solo un riferimento al valore, che verrà allocato sull'heap in un secondo momento. Per i campi di tipo valore, questa memoria contiene il valore effettivo.

Quindi, dati i seguenti tipi:

class RefType{
    public int    I;
    public string S;
    public long   L;
}

struct ValType{
    public int    I;
    public string S;
    public long   L;
}

I valori di ciascuno di questi tipi richiederebbero 16 byte di memoria (assumendo una dimensione di parola a 32 bit). Il campo Irichiede in ogni caso 4 byte per memorizzare il suo valore, il campo Srichiede 4 byte per memorizzare il suo riferimento e il campo Limpiega 8 byte per memorizzare il suo valore. Quindi la memoria per il valore di entrambi RefTypee ValTypeassomiglia a questo:

 0 ┌───────────────────┐
   │ I │
 4 ├───────────────────┤┤
   │ S │
 8 ├───────────────────┤┤
   │ L │
   │ │
16 └───────────────────┘┘

Ora, se tu avessi tre variabili locali in una funzione, di tipi RefType, ValTypee int[], in questo modo:

RefType refType;
ValType valType;
int[]   intArray;

quindi il tuo stack potrebbe apparire così:

 0 ┌───────────────────┐
   │ refType │
 4 ├───────────────────┤┤
   │ valType │
   │ │
   │ │
   │ │
20 ├───────────────────┤
   │ intArray │
24 └───────────────────┘

Se hai assegnato valori a queste variabili locali, in questo modo:

refType = new RefType();
refType.I = 100;
refType.S = "refType.S";
refType.L = 0x0123456789ABCDEF;

valType = new ValType();
valType.I = 200;
valType.S = "valType.S";
valType.L = 0x0011223344556677;

intArray = new int[4];
intArray[0] = 300;
intArray[1] = 301;
intArray[2] = 302;
intArray[3] = 303;

Quindi il tuo stack potrebbe assomigliare a questo:

 0 ┌───────────────────┐
   │ 0x4A963B68 │ - indirizzo heap di `refType`
 4 ├───────────────────┤┤
   │ 200 │ - valore di `valType.I`
   │ 0x4A984C10 │ - indirizzo heap di `valType.S`
   │ 0x44556677 │ - basso 32 bit di `valType.L`
   │ 0x00112233 │ - 32 bit alti di `valType.L`
20 ├───────────────────┤
   │ 0x4AA4C288 │ - indirizzo heap di `intArray`
24 └───────────────────┘

La memoria all'indirizzo 0x4A963B68(valore di refType) sarebbe simile a:

 0 ┌───────────────────┐
   │ 100 │ - valore di `refType.I`
 4 ├───────────────────┤┤
   │ 0x4A984D88 │ - indirizzo heap di `refType.S`
 8 ├───────────────────┤┤
   │ 0x89ABCDEF │ - basso 32 bit di `refType.L`
   │ 0x01234567 │ - 32 bit alti di `refType.L`
16 └───────────────────┘┘

La memoria all'indirizzo 0x4AA4C288(valore di intArray) sarebbe simile a:

 0 ┌───────────────────┐
   │ 4 │ - lunghezza dell'array
 4 ├───────────────────┤┤
   │ 300 │ - `intArray [0]`
 8 ├───────────────────┤┤
   │ 301 │ - `intArray [1]`
12 ├───────────────────┤┤
   │ 302 │ - `intArray [2]`
16 ├───────────────────┤┤
   │ 303 │ - `intArray [3]`
20 └───────────────────┘

Ora, se si passasse intArraya un'altra funzione, il valore inserito nello stack sarebbe 0x4AA4C288, l'indirizzo dell'array, non una copia dell'array.


52
Prendo atto che l'affermazione secondo cui tutte le variabili locali sono archiviate nello stack non è precisa. Le variabili locali che sono variabili esterne di una funzione anonima vengono archiviate nell'heap. Le variabili locali dei blocchi iteratori sono memorizzate nell'heap. Le variabili locali di blocchi asincroni sono archiviate nell'heap. Le variabili locali registrate non vengono memorizzate né sullo stack né sull'heap. Le variabili locali elise non vengono memorizzate né sullo stack né sull'heap.
Eric Lippert,

5
LOL, sempre il pignolo, signor Lippert. :) Mi sento in dovere di sottolineare che, ad eccezione di questi ultimi due casi, i cosiddetti "locali" cessano di essere locali al momento della compilazione. L'implementazione li porta allo stato dei membri della classe, che è l'unica ragione per cui vengono archiviati nell'heap. Quindi è solo un dettaglio di implementazione (snicker). Naturalmente, l'archiviazione dei registri è un dettaglio dell'implementazione di livello ancora inferiore e l'elisione non conta.
P Daddy,

3
Ovviamente, il mio intero post riguarda i dettagli dell'implementazione, ma, come sono sicuro che realizzi, è stato tutto nel tentativo di separare i concetti di variabili e valori . Una variabile (chiamatela locale, un campo, un parametro, qualunque cosa) può essere memorizzata nello stack, nell'heap o in un altro luogo definito dall'implementazione, ma non è proprio quello che conta. Ciò che è importante, è se quella variabile memorizza direttamente il valore che rappresenta, o semplicemente un riferimento a quel valore, memorizzato altrove. È importante perché influisce sulla semantica della copia: se la copia di quella variabile ne copia il valore o l'indirizzo.
P Daddy,

16
Apparentemente hai un'idea diversa di cosa significhi essere una "variabile locale". Sembra che tu creda che una "variabile locale" sia caratterizzata dai suoi dettagli di implementazione . Questa convinzione non è giustificata da nulla di cui sono a conoscenza nella specifica C #. Una variabile locale è in realtà una variabile dichiarata all'interno di un blocco il cui nome è nell'ambito solo nello spazio di dichiarazione associato al blocco. Vi assicuro che le variabili locali che sono, come dettaglio di implementazione, issate nei campi di una classe di chiusura, sono ancora variabili locali secondo le regole di C #.
Eric Lippert,

15
Detto questo, ovviamente la tua risposta è generalmente eccellente; il punto che i valori sono concettualmente diversi dalle variabili è uno che deve essere fatto il più spesso e ad alta voce possibile, poiché è fondamentale. Eppure moltissime persone credono nei miti più strani su di loro! Così buono con te per aver combattuto la buona battaglia.
Eric Lippert,

23

Sì, l'array verrà posizionato nell'heap.

Gli ints all'interno dell'array non verranno boxati. Solo perché esiste un tipo di valore nell'heap, non significa necessariamente che verrà inscatolato. La boxe si verificherà solo quando un tipo di valore, come int, viene assegnato a un riferimento di tipo oggetto.

Per esempio

Non box:

int i = 42;
myIntegers[0] = 42;

scatole:

object i = 42;
object[] arr = new object[10];  // no boxing here 
arr[0] = 42;

Potresti anche voler controllare il post di Eric su questo argomento:


1
Ma non capisco. I tipi di valore non dovrebbero essere allocati nello stack? Oppure entrambi i tipi di valore e di riferimento possono essere allocati sia su heap che su stack ed è solo che di solito sono memorizzati in un posto o nell'altro?
divorò l'elisio l'

4
@Jorge, un tipo di valore senza wrapper / contenitore di tipo di riferimento vivrà nello stack. Tuttavia, una volta utilizzato all'interno di un contenitore del tipo di riferimento, vivrà nell'heap. Un array è un tipo di riferimento e quindi la memoria per int deve essere nell'heap.
JaredPar,

2
@Jorge: i tipi di riferimento vivono solo nell'heap, mai nello stack. Al contrario, è impossibile (in codice verificabile) memorizzare un puntatore in una posizione dello stack in un oggetto di un tipo di riferimento.
Anton Tykhyy,

1
Penso che volevi assegnare i ad arr [0]. L'assegnazione costante causerà comunque la boxe di "42", ma tu l'hai creata, quindi puoi anche usarla ;-)
Marcus Griep,

@AntonTykhyy: Non sono a conoscenza di regole per dire che un CLR non può fare analisi di fuga. Se rileva che un oggetto non verrà mai referenziato oltre la durata della funzione che lo ha creato, è del tutto legittimo - e persino preferibile - costruire l'oggetto nello stack, che si tratti di un tipo di valore o meno. "Tipo di valore" e "tipo di riferimento" descrivono fondamentalmente cosa c'è nella memoria occupata dalla variabile, non una regola rigida e veloce su dove vive l'oggetto.
cHao,

21

Per capire cosa sta succedendo, ecco alcuni fatti:

  • Gli oggetti sono sempre allocati sull'heap.
  • L'heap contiene solo oggetti.
  • I tipi di valore vengono allocati nello stack o parte di un oggetto nell'heap.
  • Un array è un oggetto.
  • Un array può contenere solo tipi di valore.
  • Un riferimento a un oggetto è un tipo di valore.

Pertanto, se si dispone di un array di numeri interi, l'array viene allocato sull'heap e gli interi che contiene fanno parte dell'oggetto array sull'heap. I numeri interi risiedono all'interno dell'oggetto array sull'heap, non come oggetti separati, quindi non sono inscatolati.

Se hai una matrice di stringhe, è davvero una matrice di riferimenti di stringa. Poiché i riferimenti sono tipi di valore, faranno parte dell'oggetto array sull'heap. Se si inserisce un oggetto stringa nell'array, si inserisce effettivamente il riferimento all'oggetto stringa nell'array e la stringa è un oggetto separato nell'heap.


Sì, i riferimenti si comportano esattamente come i tipi di valore, ma ho notato che di solito non vengono chiamati in questo modo o inclusi nei tipi di valore. Vedi ad esempio (ma ce ne sono molti altri simili) msdn.microsoft.com/en-us/library/s1ax56ch.aspx
Henk Holterman,

@Henk: Sì, hai ragione nel dire che i riferimenti non sono elencati tra le variabili del tipo di valore, ma quando si tratta di come viene allocata la memoria per loro sono in tutti i tipi di valore, ed è molto utile rendersene conto per capire come l'allocazione della memoria tutto si adatta insieme. :)
Guffa,

Dubito che il quinto punto, "Un array può contenere solo tipi di valore". Che dire dell'array di stringhe? string [] stringhe = nuova stringa [4];
Sunil Purushothaman,

9

Penso che al centro della tua domanda risieda un malinteso su riferimento e tipi di valore. Questo è qualcosa che probabilmente tutti gli sviluppatori .NET e Java hanno dovuto affrontare.

Un array è solo un elenco di valori. Se si tratta di un array di un tipo di riferimento (diciamo a string[]), allora l'array è un elenco di riferimenti a vari stringoggetti sull'heap, poiché un riferimento è il valore di un tipo di riferimento. Internamente, questi riferimenti sono implementati come puntatori a un indirizzo in memoria. Se si desidera visualizzarlo, un tale array sarebbe simile a questo in memoria (sull'heap):

[ 00000000, 00000000, 00000000, F8AB56AA ]

Questa è una matrice stringche contiene 4 riferimenti a stringoggetti nell'heap (i numeri qui sono esadecimali). Attualmente, solo l'ultimo in stringrealtà punta a qualcosa (la memoria è inizializzata su tutti gli zero quando allocata), questo array sarebbe sostanzialmente il risultato di questo codice in C #:

string[] strings = new string[4];
strings[3] = "something"; // the string was allocated at 0xF8AB56AA by the CLR

L'array sopra sarebbe in un programma a 32 bit. In un programma a 64 bit, i riferimenti sarebbero due volte più grandi ( F8AB56AAsarebbero 00000000F8AB56AA).

Se hai una matrice di tipi di valore (diciamo an int[]), allora la matrice è un elenco di numeri interi, poiché il valore di un tipo di valore è il valore stesso (da cui il nome). La visualizzazione di un tale array sarebbe questa:

[ 00000000, 45FF32BB, 00000000, 00000000 ]

Questo è un array di 4 numeri interi, in cui solo al secondo int viene assegnato un valore (a 1174352571, che è la rappresentazione decimale di quel numero esadecimale) e il resto degli interi sarebbe 0 (come ho detto, la memoria è inizializzata su zero e 00000000 in esadecimale è 0 in decimale). Il codice che ha prodotto questo array sarebbe:

 int[] integers = new int[4];
 integers[1] = 1174352571; // integers[1] = 0x45FF32BB would be valid too

Questo int[]array verrebbe anche archiviato nell'heap.

Come altro esempio, la memoria di un short[4]array sarebbe simile a questa:

[ 0000, 0000, 0000, 0000 ]

Poiché il valore di a shortè un numero di 2 byte.

Dove viene archiviato un tipo di valore, è solo un dettaglio dell'implementazione come spiega Eric Lippert molto bene qui , non inerente alle differenze tra valore e tipi di riferimento (che è la differenza nel comportamento).

Quando passi qualcosa a un metodo (sia che si tratti di un tipo di riferimento o di un tipo di valore), una copia del valore del tipo viene effettivamente passata al metodo. Nel caso di un tipo di riferimento, il valore è un riferimento (pensate a questo come un puntatore a un pezzo di memoria, sebbene si tratti anche di un dettaglio di implementazione) e nel caso di un tipo di valore, il valore è la cosa stessa.

// Calling this method creates a copy of the *reference* to the string
// and a copy of the int itself, so copies of the *values*
void SomeMethod(string s, int i){}

La boxe si verifica solo se si converte un tipo di valore in un tipo di riferimento. Questo codice contiene:

object o = 5;

Credo che "un dettaglio di implementazione" dovrebbe essere una dimensione del carattere: 50px. ;)
sisve l'

2

Queste sono illustrazioni raffiguranti la risposta sopra di @P Daddy

inserisci qui la descrizione dell'immagine

inserisci qui la descrizione dell'immagine

E ho illustrato i contenuti corrispondenti nel mio stile.

inserisci qui la descrizione dell'immagine


@P Papà, ho fatto delle illustrazioni. Si prega di verificare se c'è parte sbagliata. E ho alcune domande aggiuntive. 1. Quando creo un array di tipo int di lunghezza 4, anche le informazioni sulla lunghezza (4) vengono sempre memorizzate?
YoungMin Park,

2. Sulla seconda illustrazione, l'indirizzo dell'array copiato è memorizzato dove? È la stessa area dello stack in cui è memorizzato l'indirizzo intArray? È un altro stack ma lo stesso tipo di stack? È diverso il tipo di pila? 3. Cosa significano basso 32 bit / alto 32 bit? 4. Qual è il valore restituito quando allocare il tipo di valore (in questo esempio, la struttura) nello stack utilizzando una nuova parola chiave? È anche l'indirizzo? Quando stavo controllando questa affermazione Console.WriteLine (valType), mostrava il nome completo come oggetto come ConsoleApp.ValType.
YoungMin Park,

5. valType.I = 200; Questa affermazione significa che ottengo l'indirizzo di valType, con questo indirizzo accedo all'I e proprio lì ne conservo 200 ma "nello stack".
YoungMin Park,

1

Una matrice di numeri interi è allocata sull'heap, niente di più, niente di meno. myIntegers fa riferimento all'inizio della sezione in cui sono allocati gli ints. Tale riferimento si trova nello stack.

Se si dispone di una matrice di oggetti del tipo di riferimento, come il Tipo di oggetto, myObjects [], situato nello stack, farebbe riferimento al gruppo di valori che fanno riferimento agli oggetti stessi.

Per riassumere, se si passa myIntegers ad alcune funzioni, si passa solo il riferimento al luogo in cui è allocato il gruppo reale di numeri interi.


1

Non c'è boxe nel tuo codice di esempio.

I tipi di valore possono vivere nell'heap come fanno nell'array di ints. L'array è allocato sull'heap e memorizza ints, che sono tipi di valore. Il contenuto dell'array viene inizializzato sul valore predefinito (int), che risulta essere zero.

Considera una classe che contiene un tipo di valore:


    class HasAnInt
    {
        int i;
    }

    HasAnInt h = new HasAnInt();

La variabile h si riferisce a un'istanza di HasAnInt che vive nell'heap. Capita solo di contenere un tipo di valore. Va perfettamente bene, "io" capita solo di vivere nell'heap poiché è contenuto in una classe. In questo esempio non esiste boxe.


1

È stato detto abbastanza da tutti, ma se qualcuno è alla ricerca di un campione chiaro (ma non ufficiale) e di documentazione su heap, stack, variabili locali e variabili statiche, consultare l'articolo completo di Jon Skeet sulla memoria in .NET - che cosa succede dove

Estratto:

  1. Ogni variabile locale (ovvero una dichiarata in un metodo) è memorizzata nello stack. Ciò include le variabili del tipo di riferimento: la variabile stessa è nello stack, ma ricorda che il valore di una variabile del tipo di riferimento è solo un riferimento (o null), non l'oggetto stesso. I parametri del metodo contano anche come variabili locali, ma se vengono dichiarati con il modificatore ref, non ottengono il proprio slot, ma condividono uno slot con la variabile utilizzata nel codice chiamante. Vedi il mio articolo sul passaggio dei parametri per maggiori dettagli.

  2. Le variabili di istanza per un tipo di riferimento sono sempre nell'heap. Ecco dove l'oggetto stesso "vive".

  3. Le variabili di istanza per un tipo di valore vengono archiviate nello stesso contesto della variabile che dichiara il tipo di valore. Lo slot di memoria per l'istanza contiene effettivamente gli slot per ciascun campo all'interno dell'istanza. Ciò significa (dati i due punti precedenti) che una variabile struct dichiarata all'interno di un metodo sarà sempre nello stack, mentre una variabile struct che è un campo di istanza di una classe sarà nell'heap.

  4. Ogni variabile statica viene archiviata nell'heap, indipendentemente dal fatto che sia dichiarata in un tipo di riferimento o in un tipo di valore. C'è un solo slot in totale, indipendentemente dal numero di istanze create. (Tuttavia, non è necessario creare istanze per l'esistenza di quello slot.) I dettagli di quale heap su cui vivono le variabili sono complicati, ma spiegati in dettaglio in un articolo MSDN sull'argomento.

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.