Arrays, Heap- und Stack- und Werttypen

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

Generiert new int [100] im obigen Code das Array auf dem Heap? Nach dem, was ich über c # über CLR gelesen habe, lautet die Antwort ja. Aber was ich nicht verstehen kann, ist, was mit den tatsächlichen Ints im Array passiert. Da es sich um Werttypen handelt, müssten sie vermutlich eingerahmt werden, da ich beispielsweise myIntegers an andere Teile des Programms weitergeben kann und es den Stapel überladen würde, wenn sie die ganze Zeit darauf belassen würden . Oder liege ich falsch? Ich würde vermuten, dass sie nur geboxt würden und so lange auf dem Haufen leben würden, wie das Array existierte.

verschlungenes Elysium
quelle

Antworten:

289

Ihr Array wird auf dem Heap zugewiesen, und die Ints werden nicht eingerahmt.

Die Ursache Ihrer Verwirrung ist wahrscheinlich, dass Leute gesagt haben, dass Referenztypen auf dem Heap und Werttypen auf dem Stapel zugewiesen werden. Dies ist keine ganz genaue Darstellung.

Alle lokalen Variablen und Parameter werden auf dem Stapel zugeordnet. Dies umfasst sowohl Werttypen als auch Referenztypen. Der Unterschied zwischen den beiden ist nur das, was in der Variablen gespeichert ist. Es ist nicht überraschend, dass für einen Werttyp der Wert des Typs direkt in der Variablen gespeichert wird, und für einen Referenztyp wird der Wert des Typs auf dem Heap gespeichert, und ein Verweis auf diesen Wert wird in der Variablen gespeichert.

Gleiches gilt für Felder. Wenn Speicher für eine Instanz eines Aggregattyps (a classoder a struct) zugewiesen wird , muss er Speicher für jedes seiner Instanzfelder enthalten. Bei Feldern vom Referenztyp enthält dieser Speicher nur einen Verweis auf den Wert, der später selbst auf dem Heap zugewiesen wird. Bei Feldern vom Werttyp enthält dieser Speicher den tatsächlichen Wert.

Also, angesichts der folgenden Typen:

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

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

Die Werte für jeden dieser Typen würden 16 Byte Speicher erfordern (unter der Annahme einer Wortgröße von 32 Bit). Das Feld benötigt Ijeweils 4 Bytes, um seinen Wert zu speichern, das Feld Sbenötigt 4 Bytes, um seine Referenz zu speichern, und das Feld Lbenötigt 8 Bytes, um seinen Wert zu speichern. Also der Speicher für den Wert von beiden RefTypeund ValTypesieht so aus:

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

Nun , wenn Sie hatte drei lokale Variablen in einer Funktion, von Typen RefType, ValTypeund int[], wie folgt aus :

RefType refType;
ValType valType;
int[]   intArray;

dann könnte Ihr Stapel folgendermaßen aussehen:

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

Wenn Sie diesen lokalen Variablen Werte zugewiesen haben, wie folgt:

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;

Dann könnte Ihr Stapel ungefähr so ​​aussehen:

 0 ┌────────────────────┐
   │ 0x4A963B68 │ - Heap-Adresse von `refType`
 4 ├────────────────────┤
   │ 200 │ - Wert von `valType.I`
   │ 0x4A984C10 │ - Heap-Adresse von `valType.S`
   │ 0x44556677 │ - niedrige 32-Bit von `valType.L`
   │ 0x00112233 │ - hohe 32-Bit von `valType.L`
20 ├────────────────────┤
   │ 0x4AA4C288 │ - Heap-Adresse von `intArray`
24 └────────────────────┘

Speicher an Adresse 0x4A963B68(Wert von refType) wäre so etwas wie:

 0 ┌────────────────────┐
   │ 100 │ - Wert von `refType.I`
 4 ├────────────────────┤
   │ 0x4A984D88 │ - Heap-Adresse von `refType.S`
 8 ├────────────────────┤
   │ 0x89ABCDEF │ - niedrige 32-Bit von `refType.L`
   │ 0x01234567 │ - hohe 32-Bit von `refType.L`
16 └────────────────────┘

Speicher an Adresse 0x4AA4C288(Wert von intArray) wäre so etwas wie:

 0 ┌────────────────────┐
   │ 4 │ - Länge des Arrays
 4 ├────────────────────┤
   │ 300 │ - `intArray [0]`
 8 ├────────────────────┤
   │ 301 │ - `intArray [1]`
12 ├────────────────────┤
   │ 302 │ - `intArray [2]`
16 ├────────────────────┤
   │ 303 │ - `intArray [3]`
20 └────────────────────┘

Wenn Sie nun an intArrayeine andere Funktion übergeben, lautet der auf den Stapel übertragene Wert 0x4AA4C288die Adresse des Arrays und keine Kopie des Arrays.

P Papa
quelle
52
Ich stelle fest, dass die Aussage, dass alle lokalen Variablen auf dem Stapel gespeichert sind, ungenau ist. Lokale Variablen, die äußere Variablen einer anonymen Funktion sind, werden auf dem Heap gespeichert. Lokale Variablen von Iteratorblöcken werden auf dem Heap gespeichert. Lokale Variablen von asynchronen Blöcken werden auf dem Heap gespeichert. Lokale Variablen, die registriert sind, werden weder auf dem Stapel noch auf dem Heap gespeichert. Elidierte lokale Variablen werden weder auf dem Stapel noch auf dem Heap gespeichert.
Eric Lippert
5
LOL, immer der Trottel, Mr. Lippert. :) Ich fühle mich gezwungen darauf hinzuweisen, dass mit Ausnahme Ihrer beiden letztgenannten Fälle die sogenannten "Einheimischen" zur Kompilierungszeit keine Einheimischen mehr sind. Die Implementierung erhöht sie in den Status von Klassenmitgliedern. Dies ist der einzige Grund, warum sie auf dem Heap gespeichert werden. Es handelt sich also lediglich um ein Implementierungsdetail (Snicker). Natürlich ist der Registerspeicher ein noch untergeordnetes Implementierungsdetail, und Elision zählt nicht.
P Daddy
3
Natürlich besteht mein gesamter Beitrag aus Implementierungsdetails, aber wie Sie sicher feststellen, wurde alles versucht, die Konzepte von Variablen und Werten zu trennen . Eine Variable (nennen wir sie lokal, ein Feld, einen Parameter usw.) kann auf dem Stapel, dem Heap oder einem anderen von der Implementierung definierten Ort gespeichert werden, aber das ist nicht wirklich wichtig. Wichtig ist, ob diese Variable den Wert, den sie darstellt, direkt speichert oder einfach einen Verweis auf diesen Wert, der an anderer Stelle gespeichert ist. Dies ist wichtig, da es die Kopiersemantik beeinflusst: Ob beim Kopieren dieser Variablen ihr Wert oder ihre Adresse kopiert wird.
P Daddy
16
Anscheinend haben Sie eine andere Vorstellung davon, was es bedeutet, eine "lokale Variable" zu sein als ich. Sie scheinen zu glauben, dass eine "lokale Variable" durch ihre Implementierungsdetails gekennzeichnet ist . Dieser Glaube ist nicht durch irgendetwas gerechtfertigt, das mir in der C # -Spezifikation bekannt ist. Eine lokale Variable ist tatsächlich eine Variable in einem Block , deren erklärter Namen ist in ihrem Umfang nur im gesamten Deklarationsraum mit dem Block verbunden ist . Ich versichere Ihnen, dass lokale Variablen, die als Implementierungsdetail in Felder einer Abschlussklasse gehisst werden, weiterhin lokale Variablen gemäß den Regeln von C # sind.
Eric Lippert
15
Das heißt, natürlich ist Ihre Antwort im Allgemeinen ausgezeichnet; Der Punkt, dass sich Werte konzeptionell von Variablen unterscheiden , muss so oft und so laut wie möglich gemacht werden, da er von grundlegender Bedeutung ist. Und doch glauben sehr viele Menschen an die seltsamsten Mythen über sie! So gut auf dich, dass du den guten Kampf kämpfst.
Eric Lippert
23

Ja, das Array befindet sich auf dem Heap.

Die Ints innerhalb des Arrays werden nicht eingerahmt. Nur weil ein Werttyp auf dem Heap vorhanden ist, bedeutet dies nicht unbedingt, dass er eingerahmt wird. Boxing tritt nur auf, wenn ein Werttyp wie int einer Referenz vom Typ Objekt zugewiesen ist.

Beispielsweise

Boxt nicht:

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

Boxen:

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

Vielleicht möchten Sie auch Erics Beitrag zu diesem Thema lesen:

JaredPar
quelle
1
Aber ich verstehe es nicht. Sollten keine Werttypen auf dem Stapel zugewiesen werden? Oder können sowohl Wert- als auch Referenztypen sowohl auf dem Heap als auch auf dem Stack zugewiesen werden und es ist nur so, dass sie normalerweise nur an dem einen oder anderen Ort gespeichert werden?
verschlang Elysium
4
@Jorge, ein Werttyp ohne Referenztyp Wrapper / Container wird auf dem Stapel leben. Sobald es jedoch in einem Referenztyp-Container verwendet wird, befindet es sich im Heap. Ein Array ist ein Referenztyp und daher muss sich der Speicher für das int im Heap befinden.
JaredPar
2
@Jorge: Referenztypen leben nur im Heap, niemals auf dem Stapel. Im Gegensatz dazu ist es unmöglich (in überprüfbarem Code), einen Zeiger auf eine Stapelposition in einem Objekt eines Referenztyps zu speichern.
Anton Tykhyy
1
Ich denke, Sie wollten i arr [0] zuweisen. Die konstante Zuweisung führt immer noch zum Boxen von "42", aber Sie haben i erstellt, also können Sie es auch verwenden ;-)
Marcus Griep
@AntonTykhyy: Es gibt keine Regel, von der ich weiß, dass eine CLR keine Fluchtanalyse durchführen kann. Wenn festgestellt wird, dass ein Objekt nach Ablauf der Lebensdauer der Funktion, mit der es erstellt wurde, niemals referenziert wird, ist es völlig legitim - und sogar vorzuziehen -, das Objekt auf dem Stapel zu erstellen, unabhängig davon, ob es sich um einen Werttyp handelt oder nicht. "Werttyp" und "Referenztyp" beschreiben im Wesentlichen, was sich im Speicher befindet, der von der Variablen belegt wird, und keine feste Regel, wo sich das Objekt befindet.
CHao
21

Um zu verstehen, was passiert, sind hier einige Fakten:

  • Objekte werden immer auf dem Heap zugeordnet.
  • Der Heap enthält nur Objekte.
  • Werttypen werden entweder auf dem Stapel oder als Teil eines Objekts auf dem Heap zugewiesen.
  • Ein Array ist ein Objekt.
  • Ein Array kann nur Werttypen enthalten.
  • Eine Objektreferenz ist ein Werttyp.

Wenn Sie also ein Array von Ganzzahlen haben, wird das Array auf dem Heap zugewiesen und die darin enthaltenen Ganzzahlen sind Teil des Array-Objekts auf dem Heap. Die Ganzzahlen befinden sich innerhalb des Array-Objekts auf dem Heap und nicht als separate Objekte, sodass sie nicht in Kästchen eingeschlossen sind.

Wenn Sie ein Array von Zeichenfolgen haben, ist es wirklich ein Array von Zeichenfolgenreferenzen. Da Referenzen Werttypen sind, sind sie Teil des Array-Objekts auf dem Heap. Wenn Sie ein Zeichenfolgenobjekt in das Array einfügen, fügen Sie tatsächlich den Verweis auf das Zeichenfolgenobjekt in das Array ein, und die Zeichenfolge ist ein separates Objekt auf dem Heap.

Guffa
quelle
Ja, Referenzen verhalten sich genau wie Werttypen, aber ich habe festgestellt, dass sie normalerweise nicht so genannt oder in den Werttypen enthalten sind. Siehe zum Beispiel (aber es gibt noch viel mehr davon) msdn.microsoft.com/en-us/library/s1ax56ch.aspx
Henk Holterman
@Henk: Ja, Sie haben Recht, dass Referenzen nicht unter Werttypvariablen aufgeführt sind, aber wenn es darum geht, wie Speicher für sie zugewiesen wird, handelt es sich in jeder Hinsicht um Werttypen, und es ist sehr nützlich, dies zu erkennen, um zu verstehen, wie die Speicherzuweisung erfolgt alles passt zusammen :)
Guffa
Ich bezweifle den 5. Punkt: "Ein Array kann nur Werttypen enthalten." Was ist mit String-Array? string [] strings = neuer string [4];
Sunil Purushothaman
9

Ich denke, im Kern Ihrer Frage liegt ein Missverständnis über Referenz- und Werttypen. Dies ist etwas, mit dem wahrscheinlich jeder .NET- und Java-Entwickler zu kämpfen hat.

Ein Array ist nur eine Liste von Werten. Wenn es sich um ein Array eines Referenztyps handelt (z. B. a string[]), ist das Array eine Liste von Referenzen auf verschiedene stringObjekte auf dem Heap, da eine Referenz der Wert eines Referenztyps ist. Intern werden diese Referenzen als Zeiger auf eine Adresse im Speicher implementiert. Wenn Sie dies visualisieren möchten, würde ein solches Array im Speicher (auf dem Heap) folgendermaßen aussehen:

[ 00000000, 00000000, 00000000, F8AB56AA ]

Dies ist ein Array string, das 4 Verweise auf stringObjekte auf dem Heap enthält (die Zahlen hier sind hexadezimal). Derzeit zeigt nur der letzte stringtatsächlich auf etwas (der Speicher wird bei der Zuweisung auf alle Nullen initialisiert). Dieses Array wäre im Grunde das Ergebnis dieses Codes in C #:

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

Das obige Array würde sich in einem 32-Bit-Programm befinden. In einem 64-Bit-Programm wären die Referenzen doppelt so groß ( F8AB56AAwären 00000000F8AB56AA).

Wenn Sie einen Array von Werttypen haben (sagt eine int[]) , dann das Array ist eine Liste von ganzen Zahlen, wie der Wert eines Werttypen ist der Wert selbst (daher der Name). Die Visualisierung eines solchen Arrays wäre folgende:

[ 00000000, 45FF32BB, 00000000, 00000000 ]

Dies ist ein Array von 4 Ganzzahlen, wobei nur dem zweiten int ein Wert zugewiesen wird (bis 1174352571, was die Dezimaldarstellung dieser Hexadezimalzahl ist) und der Rest der Ganzzahlen 0 wäre (wie gesagt, der Speicher wird auf Null initialisiert und 00000000 in hexadezimal ist 0 in dezimal). Der Code, der dieses Array erzeugt hat, wäre:

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

Dieses int[]Array würde auch auf dem Heap gespeichert.

Als weiteres Beispiel würde der Speicher eines short[4]Arrays folgendermaßen aussehen:

[ 0000, 0000, 0000, 0000 ]

Da der Wert von a shorteine 2-Byte-Zahl ist.

Wo ein Werttyp gespeichert ist, ist nur ein Implementierungsdetail, wie Eric Lippert hier sehr gut erklärt , und nicht den Unterschieden zwischen Wert- und Referenztypen (was ein Unterschied im Verhalten ist) inhärent.

Wenn Sie etwas an eine Methode übergeben (sei es ein Referenztyp oder ein Werttyp), wird tatsächlich eine Kopie des Werts des Typs an die Methode übergeben. Im Fall eines Referenztyps ist der Wert eine Referenz (stellen Sie sich dies als Zeiger auf ein Stück Speicher vor, obwohl dies auch ein Implementierungsdetail ist), und im Fall eines Werttyps ist der Wert das Ding selbst.

// 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){}

Boxing tritt nur auf, wenn Sie einen Werttyp in einen Referenztyp konvertieren . Diese Codefelder:

object o = 5;
JulianR
quelle
Ich glaube, "ein Implementierungsdetail" sollte eine Schriftgröße haben: 50px. ;)
Schwester
2

Dies sind Abbildungen, die die obige Antwort von @P Daddy zeigen

Geben Sie hier die Bildbeschreibung ein

Geben Sie hier die Bildbeschreibung ein

Und ich habe die entsprechenden Inhalte in meinem Stil illustriert.

Geben Sie hier die Bildbeschreibung ein

YoungMin Park
quelle
@P Daddy Ich habe Illustrationen gemacht. Bitte überprüfen Sie, ob ein Teil falsch ist. Und ich habe einige zusätzliche Fragen. 1. Wenn ich ein Array vom Typ int mit 4 Längen erstelle, werden die Längeninformationen (4) auch immer im Speicher gespeichert.
YoungMin Park
2. In der zweiten Abbildung wird die kopierte Array-Adresse wo gespeichert. Ist es derselbe Stapelbereich, in dem die intArray-Adresse gespeichert ist? Ist es ein anderer Stapel, aber dieselbe Art von Stapel? Ist es eine andere Art von Stapel? 3. Was bedeutet niedrige 32-Bit / hohe 32-Bit? 4. Was ist der Rückgabewert, wenn ich dem Wert mithilfe des neuen Schlüsselworts einen Werttyp (in diesem Beispiel Struktur) zuordne? Ist es auch die Adresse? Wenn ich anhand dieser Anweisung Console.WriteLine (valType) überprüfe, wird der vollständig qualifizierte Name wie ein Objekt wie ConsoleApp.ValType angezeigt.
YoungMin Park
5. valType.I = 200; Bedeutet diese Anweisung, dass ich die Adresse von valType erhalte, über diese Adresse greife ich auf das I zu und speichere genau dort 200, aber "auf dem Stapel".
YoungMin Park
1

Auf dem Heap wird ein Array von Ganzzahlen zugewiesen, nicht mehr und nicht weniger. myIntegers verweist auf den Anfang des Abschnitts, in dem die Ints zugewiesen sind. Diese Referenz befindet sich auf dem Stapel.

Wenn Sie ein Array von Objekten vom Referenztyp haben, wie z. B. den Objekttyp, verweist myObjects [] auf dem Stapel auf die Werte, die auf die Objekte selbst verweisen.

Zusammenfassend lässt sich sagen, dass Sie, wenn Sie myIntegers an einige Funktionen übergeben, den Verweis nur an die Stelle übergeben, an der die tatsächliche Anzahl von Ganzzahlen zugewiesen ist.

Dykam
quelle
1

Ihr Beispielcode enthält kein Boxen.

Werttypen können auf dem Heap wie in Ihrem Ints-Array gespeichert werden. Das Array wird auf dem Heap zugewiesen und speichert Ints, bei denen es sich zufällig um Werttypen handelt. Der Inhalt des Arrays wird auf den Standardwert (int) initialisiert, der zufällig Null ist.

Stellen Sie sich eine Klasse vor, die einen Werttyp enthält:


    class HasAnInt
    {
        int i;
    }

    HasAnInt h = new HasAnInt();

Die Variable h bezieht sich auf eine Instanz von HasAnInt, die auf dem Heap lebt. Es enthält nur einen Werttyp. Das ist vollkommen in Ordnung. Ich lebe zufällig auf dem Haufen, da er in einer Klasse enthalten ist. Auch in diesem Beispiel gibt es kein Boxen.

Curt Nichols
quelle
1

Alle haben genug gesagt, aber wenn jemand nach einem klaren (aber nicht offiziellen) Beispiel und einer Dokumentation zu Heap, Stack, lokalen Variablen und statischen Variablen sucht, lesen Sie den vollständigen Artikel von Jon Skeet über Memory in .NET - was geht wo

Auszug:

  1. Jede lokale Variable (dh eine in einer Methode deklarierte) wird auf dem Stapel gespeichert. Dazu gehören Referenztypvariablen - die Variable selbst befindet sich auf dem Stapel. Beachten Sie jedoch, dass der Wert einer Referenztypvariablen nur eine Referenz (oder Null) ist, nicht das Objekt selbst. Methodenparameter gelten ebenfalls als lokale Variablen. Wenn sie jedoch mit dem Modifikator ref deklariert werden, erhalten sie keinen eigenen Slot, sondern teilen sich einen Slot mit der im aufrufenden Code verwendeten Variablen. Weitere Informationen finden Sie in meinem Artikel zur Parameterübergabe.

  2. Instanzvariablen für einen Referenztyp befinden sich immer auf dem Heap. Dort "lebt" das Objekt selbst.

  3. Instanzvariablen für einen Werttyp werden im selben Kontext gespeichert wie die Variable, die den Werttyp deklariert. Der Speichersteckplatz für die Instanz enthält effektiv die Steckplätze für jedes Feld innerhalb der Instanz. Dies bedeutet (unter Berücksichtigung der beiden vorherigen Punkte), dass sich eine innerhalb einer Methode deklarierte Strukturvariable immer auf dem Stapel befindet, während sich eine Strukturvariable, die ein Instanzfeld einer Klasse ist, auf dem Heap befindet.

  4. Jede statische Variable wird auf dem Heap gespeichert, unabhängig davon, ob sie in einem Referenztyp oder einem Werttyp deklariert ist. Insgesamt gibt es nur einen Steckplatz, unabhängig davon, wie viele Instanzen erstellt wurden. (Es müssen jedoch keine Instanzen erstellt werden, damit dieser eine Slot vorhanden ist.) Die Details, auf welchem ​​Heap die Variablen leben, sind kompliziert, werden jedoch in einem MSDN-Artikel zu diesem Thema ausführlich erläutert.

gmaran23
quelle