Tablice, sterta i stos oraz typy wartości


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

Czy w powyższym kodzie new int [100] generuje tablicę na stercie? Z tego, co przeczytałem o CLR za pomocą c #, odpowiedź brzmi: tak. Ale nie mogę zrozumieć, co dzieje się z rzeczywistymi int wewnątrz tablicy. Ponieważ są to typy wartości, myślę, że musiałyby być opakowane, ponieważ mogę na przykład przekazać moje liczby całkowite do innych części programu i zaśmiecałoby stos, gdyby były na nim cały czas . A może się mylę? Sądzę, że zostałyby po prostu zapakowane i żyłyby na stercie tak długo, jak istnieje tablica.

Odpowiedzi:


289

Twoja tablica jest alokowana na stercie, a wartości int nie są opakowane.

Przyczyną twojego zamieszania jest prawdopodobnie to, że ludzie powiedzieli, że typy odwołań są przydzielane na stercie, a typy wartości są przydzielane na stosie. To nie jest całkowicie dokładne przedstawienie.

Wszystkie lokalne zmienne i parametry są przydzielane na stosie. Obejmuje to zarówno typy wartości, jak i typy odwołań. Różnica między nimi polega tylko na tym, co jest przechowywane w zmiennej. Nic dziwnego, że w przypadku typu wartości wartość typu jest przechowywana bezpośrednio w zmiennej, a dla typu referencyjnego wartość typu jest przechowywana na stercie, a odwołanie do tej wartości jest przechowywane w zmiennej.

To samo dotyczy pól. Gdy pamięć jest przydzielana dla instancji typu zagregowanego (a classlub a struct), musi ona obejmować pamięć dla każdego z jej pól instancji. W przypadku pól typu referencyjnego ta pamięć przechowuje tylko odniesienie do wartości, która zostanie później przydzielona na stercie. W przypadku pól typu wartość ta pamięć przechowuje rzeczywistą wartość.

Tak więc, biorąc pod uwagę następujące typy:

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

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

Wartości każdego z tych typów wymagałyby 16 bajtów pamięci (przy założeniu 32-bitowego rozmiaru słowa). W Ikażdym przypadku pole zajmuje 4 bajty na przechowywanie swojej wartości, pole Szajmuje 4 bajty na przechowywanie odniesienia, a pole Lzajmuje 8 bajtów na przechowywanie wartości. Więc pamięć dla wartości obu RefTypei ValTypewygląda tak:

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

Teraz, jeśli miał trzy zmienne lokalne w funkcji, typów RefType, ValTypeoraz int[], jak to:

RefType refType;
ValType valType;
int[]   intArray;

Twój stos może wyglądać następująco:

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

Jeśli przypisałeś wartości do tych zmiennych lokalnych, na przykład:

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;

Wtedy twój stos może wyglądać mniej więcej tak:

 0 ┌───────────────────┐
   │ 0x4A963B68 │ - adres sterty „refType”
 4 ├───────────────────┤
   │ 200 │ - wartość „valType.I”
   │ 0x4A984C10 │ - adres sterty „valType.S”
   │ 0x44556677 │ - niskie 32 bity wartości „valType.L”
   │ 0x00112233 │ - wysokie 32 bity wartości „valType.L”
20 ├───────────────────┤
   │ 0x4AA4C288 │ - adres sterty „intArray”
24 └───────────────────┘

Pamięć pod adresem 0x4A963B68(wartość refType) wyglądałaby tak:

 0 ┌───────────────────┐
   │ 100 │ - wartość „refType.I”
 4 ├───────────────────┤
   │ 0x4A984D88 │ - adres sterty „refType.S”
 8 ├───────────────────┤
   │ 0x89ABCDEF │ - niskie 32 bity wartości „refType.L”
   │ 0x01234567 │ - wysokie 32 bity wartości „refType.L”
16 └───────────────────┘

Pamięć pod adresem 0x4AA4C288(wartość intArray) wyglądałaby tak:

 0 ┌───────────────────┐
   │ 4 │ - długość tablicy
 4 ├───────────────────┤
   │ 300 │ - `intArray [0]`
 8 ├───────────────────┤
   │ 301 │ - `intArray [1]`
12 ├───────────────────┤
   │ 302 │ - `intArray [2]`
16 ├───────────────────┤
   │ 303 │ - `intArray [3]`
20 └───────────────────┘

Teraz, jeśli przekazałeś intArraydo innej funkcji, wartość umieszczona na stosie byłaby 0x4AA4C288adresem tablicy, a nie kopią tablicy.


52
Zwracam uwagę, że stwierdzenie, że wszystkie zmienne lokalne są przechowywane na stosie, jest niedokładne. Zmienne lokalne, które są zmiennymi zewnętrznymi funkcji anonimowej, są przechowywane na stercie. Lokalne zmienne bloków iteratora są przechowywane na stercie. Lokalne zmienne bloków asynchronicznych są przechowywane na stercie. Zarejestrowane zmienne lokalne nie są przechowywane ani na stosie, ani na stercie. Zmienne lokalne, które są usuwane, nie są przechowywane ani na stosie, ani na stercie.
Eric Lippert

5
LOL, zawsze zbieracz nitek, panie Lippert. :) Czuję się zmuszony wskazać, że z wyjątkiem twoich ostatnich dwóch przypadków, tak zwani „lokalni” przestają być lokalnymi w czasie kompilacji. Implementacja podnosi je do statusu członków klasy, co jest jedynym powodem, dla którego są przechowywane na stercie. Więc to tylko szczegół implementacji (chichot). Oczywiście przechowywanie rejestrów jest szczegółem implementacji jeszcze niższego poziomu, a elision się nie liczy.
P Daddy

3
Oczywiście cały mój post dotyczy szczegółów implementacji, ale, jak jestem pewien, zdajesz sobie sprawę, wszystko to było próbą oddzielenia pojęć zmiennych i wartości . Zmienna (nazwij ją lokalną, polem, parametrem, czymkolwiek) może być przechowywana na stosie, stercie lub innym miejscu zdefiniowanym przez implementację, ale to nie jest naprawdę ważne. Ważne jest, czy ta zmienna bezpośrednio przechowuje wartość, którą reprezentuje, czy po prostu odniesienie do tej wartości, przechowywane w innym miejscu. Jest to ważne, ponieważ wpływa na semantykę kopiowania: czy skopiowanie tej zmiennej powoduje skopiowanie jej wartości lub adresu.
P Daddy

16
Najwyraźniej masz inne pojęcie o tym, co to znaczy być „zmienną lokalną” niż ja. Wydaje się, że sądzisz, iż „zmienna lokalna” charakteryzuje się szczegółami implementacji . To przekonanie nie znajduje uzasadnienia w żadnej ze znanych mi specyfikacji C #. Zmienna lokalna jest w rzeczywistości zmienną zadeklarowaną wewnątrz bloku, której nazwa jest objęta zakresem tylko w przestrzeni deklaracji skojarzonej z blokiem. Zapewniam, że zmienne lokalne, które jako szczegół implementacji są przenoszone do pól klasy zamknięcia, są nadal zmiennymi lokalnymi zgodnie z regułami języka C #.
Eric Lippert

15
To powiedziawszy, oczywiście twoja odpowiedź jest ogólnie doskonała; kwestia, że wartości są koncepcyjnie różne od zmiennych, to kwestia, którą należy przedstawiać tak często i tak głośno, jak to tylko możliwe, ponieważ jest to fundamentalne. A jednak bardzo wielu ludzi wierzy w najdziwniejsze o nich mity! Tak dobrze, że walczyłeś w dobrej walce.
Eric Lippert

23

Tak, tablica zostanie umieszczona na stercie.

Wartości int wewnątrz tablicy nie będą otoczone ramką. Tylko dlatego, że typ wartości istnieje na stercie, niekoniecznie oznacza, że ​​zostanie on zapakowany. Boksowanie występuje tylko wtedy, gdy typ wartości, taki jak int, jest przypisany do odwołania do obiektu typu.

Na przykład

Nie box:

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

Pudła:

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

Możesz również sprawdzić post Erica na ten temat:


1
Ale nie rozumiem. Czy typy wartości nie powinny być przydzielane na stosie? A może zarówno typy wartości, jak i typy referencyjne mogą być przydzielone zarówno na stercie, jak i na stosie i po prostu są one zwykle przechowywane w jednym lub innym miejscu?
pożarł elizjum

4
@Jorge, typ wartości bez opakowania / kontenera typu referencyjnego będzie znajdować się na stosie. Jednak gdy zostanie użyty w kontenerze typu referencyjnego, będzie żył w stercie. Tablica jest typem referencyjnym i dlatego pamięć dla int musi znajdować się na stercie.
JaredPar,

2
@Jorge: typy referencyjne żyją tylko na stercie, nigdy na stosie. W przeciwieństwie do tego, niemożliwe jest (w kodzie weryfikowalnym) przechowywanie wskaźnika do lokalizacji stosu w obiekcie typu referencyjnego.
Anton Tykhyy,

1
Myślę, że chciałeś przypisać i do arr [0]. Ciągłe przypisanie nadal będzie powodowało boksowanie "42", ale stworzyłeś i, więc równie dobrze możesz go użyć ;-)
Marcus Griep

@AntonTykhyy: Nie ma reguły, którą znam, mówiąc, że CLR nie może uciec od analizy. Jeśli wykryje, że obiekt nigdy nie będzie przywoływany poza okresem istnienia funkcji, która go utworzyła, jest całkowicie uzasadnione - a nawet preferowane - aby zbudować obiekt na stosie, niezależnie od tego, czy jest to typ wartości, czy nie. „Typ wartości” i „typ odniesienia” zasadniczo opisują, co jest w pamięci zajmowanej przez zmienną, a nie sztywną i szybką regułę określającą, gdzie żyje obiekt.
cHao

21

Aby zrozumieć, co się dzieje, oto kilka faktów:

  • Obiekty są zawsze przydzielane na stercie.
  • Sterta zawiera tylko obiekty.
  • Typy wartości są przydzielane na stosie lub jako część obiektu na stercie.
  • Tablica to obiekt.
  • Tablica może zawierać tylko typy wartości.
  • Odniesienie do obiektu jest typem wartości.

Tak więc, jeśli masz tablicę liczb całkowitych, tablica jest alokowana na stercie, a liczby całkowite, które zawiera, są częścią obiektu tablicy na stercie. Liczby całkowite znajdują się wewnątrz obiektu tablicy na stercie, a nie jako oddzielne obiekty, więc nie są opakowane.

Jeśli masz tablicę ciągów, jest to w rzeczywistości tablica odniesień do ciągów. Ponieważ odwołania są typami wartości, będą one częścią obiektu tablicy na stercie. Jeśli umieścisz obiekt typu string w tablicy, w rzeczywistości umieścisz odniesienie do obiektu ciągu w tablicy, a łańcuch jest oddzielnym obiektem na stercie.


Tak, odwołania zachowują się dokładnie tak samo, jak typy wartości, ale zauważyłem, że zwykle nie są tak nazywane ani zawarte w typach wartości. Zobacz na przykład (ale takich jest znacznie więcej) msdn.microsoft.com/en-us/library/s1ax56ch.aspx
Henk Holterman

@Henk: Tak, masz rację, że odniesienia nie są wymienione wśród zmiennych typu wartości, ale jeśli chodzi o sposób przydzielania im pamięci, są one pod każdym względem typami wartości i bardzo przydatne jest uświadomienie sobie tego, aby zrozumieć, jak alokacja pamięci wszystko do siebie pasuje. :)
Guffa,

Wątpię w piąty punkt: „Tablica może zawierać tylko typy wartości”. A co z tablicą łańcuchową? ciąg [] ciągi = nowy ciąg [4];
Sunil Purushothaman

9

Myślę, że u podstaw twojego pytania leży niezrozumienie dotyczące typów referencyjnych i wartości. To jest coś, z czym prawdopodobnie borykał się każdy programista .NET i Java.

Tablica to po prostu lista wartości. Jeśli jest to tablica typu referencyjnego (powiedzmy a string[]), to tablica jest listą odniesień do różnych stringobiektów na stercie, ponieważ referencja jest wartością typu referencyjnego. Wewnętrznie te odwołania są implementowane jako wskaźniki do adresu w pamięci. Jeśli chcesz to zwizualizować, taka tablica wyglądałaby tak w pamięci (na stercie):

[ 00000000, 00000000, 00000000, F8AB56AA ]

To jest tablica stringzawierająca 4 odniesienia do stringobiektów na stercie (liczby tutaj są szesnastkowe). Obecnie tylko ostatnia stringfaktycznie wskazuje na cokolwiek (pamięć jest inicjowana do wszystkich zer po przydzieleniu), ta tablica byłaby w zasadzie wynikiem tego kodu w C #:

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

Powyższa tablica byłaby w programie 32-bitowym. W programie 64-bitowym odwołania byłyby dwa razy większe ( F8AB56AAbyłyby 00000000F8AB56AA).

Jeśli masz tablicę typów wartości (powiedzmy int[]), to tablica jest lista liczb całkowitych, gdy wartość typu wartości jest sama wartość (stąd nazwa). Wizualizacja takiej tablicy wyglądałaby następująco:

[ 00000000, 45FF32BB, 00000000, 00000000 ]

Jest to tablica 4 liczb całkowitych, w której tylko drugiej int ma przypisaną wartość (do 1174352571, która jest dziesiętną reprezentacją tej liczby szesnastkowej), a reszta liczb całkowitych będzie równa 0 (jak powiedziałem, pamięć jest inicjalizowana do zera a 00000000 szesnastkowo to 0 dziesiętnie). Kod, który utworzył tę tablicę, wyglądałby tak:

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

Ta int[]tablica byłaby również przechowywana na stercie.

Jako inny przykład, pamięć short[4]tablicy wyglądałaby następująco:

[ 0000, 0000, 0000, 0000 ]

Ponieważ wartość a shortjest liczbą 2-bajtową.

Gdzie przechowywany jest typ wartości, to tylko szczegół realizacja jak wyjaśnia Eric Lippert bardzo dobrze tutaj , nie wpisują się do różnic między wartością i typów referencyjnych (co jest różnica w zachowaniu).

Kiedy coś przekazać do metody (być, że typ odniesienia lub typ wartości) następnie kopia z wartości typu jest rzeczywiście przekazany do metody. W przypadku typu referencyjnego wartość jest referencją (pomyśl o tym jako wskaźniku do fragmentu pamięci, chociaż jest to również szczegół implementacyjny), aw przypadku typu wartości, wartość jest samą rzeczą.

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

Boksowanie występuje tylko wtedy, gdy konwertujesz typ wartości na typ referencyjny. Te skrzynki kodowe:

object o = 5;

Uważam, że „szczegół implementacji” powinien mieć rozmiar czcionki: 50 pikseli. ;)
sisve

2

To są ilustracje przedstawiające powyższą odpowiedź autorstwa @P Daddy

wprowadź opis obrazu tutaj

wprowadź opis obrazu tutaj

Odpowiednie treści zilustrowałem w swoim stylu.

wprowadź opis obrazu tutaj


@P Tato Zrobiłem ilustracje. Sprawdź, czy jest niewłaściwa część. Mam kilka dodatkowych pytań. 1. Kiedy tworzę tablicę typu int o 4 długościach, informacja o długości (4) jest również zawsze przechowywana w pamięci?
YoungMin Park

2. Na drugiej ilustracji skopiowany adres tablicy jest przechowywany gdzie? Czy jest to ten sam obszar stosu, w którym przechowywany jest adres intArray? Czy to inny stos, ale ten sam rodzaj stosu? Czy to inny rodzaj stosu? 3. Co oznacza niski 32-bity / wysoki 32-bity? 4. Jaka jest wartość zwracana, gdy przydzielam typ wartości (w tym przykładzie strukturę) na stosie za pomocą słowa kluczowego new? Czy to także adres? Kiedy sprawdzałem tę instrukcję Console.WriteLine (valType), pokazywałaby w pełni kwalifikowaną nazwę, taką jak obiekt ConsoleApp.ValType.
YoungMin Park

5. valType.I = 200; Czy to stwierdzenie oznacza, że ​​otrzymuję adres valType, przez ten adres mam dostęp do I i właśnie tam przechowuję 200 ale „na stosie”.
YoungMin Park

1

Tablica liczb całkowitych jest przydzielana na stercie, nic więcej, nic mniej. myIntegers odwołuje się do początku sekcji, w której są przydzielone wartości int. To odniesienie znajduje się na stosie.

Jeśli masz tablicę obiektów typu referencyjnego, takich jak typ Object, myObjects [], znajdujące się na stosie, odwoływałyby się do zbioru wartości, które odwołują się do samych obiektów.

Podsumowując, jeśli przekazujesz myIntegers do niektórych funkcji, przekazujesz tylko referencję do miejsca, w którym przydzielona jest prawdziwa paczka liczb całkowitych.


1

W Twoim przykładowym kodzie nie ma boksu.

Typy wartości mogą żyć na stercie, tak jak robią to w twojej tablicy int. Tablica jest alokowana na stercie i przechowuje wartości typu int, które są typami wartości. Zawartość tablicy jest inicjalizowana na wartość domyślną (int), która jest równa zero.

Rozważ klasę, która zawiera typ wartości:


    class HasAnInt
    {
        int i;
    }

    HasAnInt h = new HasAnInt();

Zmienna h odnosi się do instancji HasAnInt, która żyje na stercie. Tak się składa, że ​​zawiera typ wartości. To zupełnie w porządku, tak się składa, że ​​„ja” po prostu żyje na stercie, ponieważ jest zawarte w klasie. W tym przykładzie nie ma też boksu.


1

Wszyscy powiedzieli wystarczająco dużo, ale jeśli ktoś szuka jasnej (ale nieoficjalnej) próbki i dokumentacji dotyczącej sterty, stosu, zmiennych lokalnych i zmiennych statycznych, zapoznaj się z pełnym artykułem Jona Skeeta na temat pamięci w .NET - co się dzieje gdzie

Fragment:

  1. Każda zmienna lokalna (tj. Jedna zadeklarowana w metodzie) jest przechowywana na stosie. Obejmuje to zmienne typu referencyjnego - sama zmienna znajduje się na stosie, ale pamiętaj, że wartość zmiennej typu referencyjnego jest tylko referencją (lub wartością zerową), a nie samym obiektem. Parametry metod są również liczone jako zmienne lokalne, ale jeśli są zadeklarowane z modyfikatorem ref, nie otrzymują własnego gniazda, ale współdzielą gniazdo ze zmienną używaną w kodzie wywołującym. Więcej informacji znajdziesz w moim artykule na temat przekazywania parametrów.

  2. Zmienne instancji dla typu referencyjnego są zawsze na stercie. Tam „żyje” sam przedmiot.

  3. Zmienne instancji dla typu wartości są przechowywane w tym samym kontekście, co zmienna, która deklaruje typ wartości. Gniazdo pamięci dla instancji faktycznie zawiera szczeliny dla każdego pola w instancji. Oznacza to (biorąc pod uwagę poprzednie dwa punkty), że zmienna struct zadeklarowana w metodzie zawsze będzie na stosie, podczas gdy zmienna struct, która jest polem instancji klasy, będzie na stercie.

  4. Każda zmienna statyczna jest przechowywana na stercie, niezależnie od tego, czy została zadeklarowana w ramach typu referencyjnego, czy typu wartości. Bez względu na to, ile instancji utworzono, w sumie jest tylko jedno miejsce. (Nie trzeba jednak tworzyć żadnych instancji dla tego jednego miejsca). Szczegóły dotyczące dokładnie stosu, na którym znajdują się zmienne, są skomplikowane, ale wyjaśniono je szczegółowo w artykule MSDN na ten temat.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.