Масиви, купи та тип стека та значення


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

У наведеному вище коді новий int [100] генерує масив на купі? З того, що я прочитав у CLR через c #, відповідь - так. Але те, що я не можу зрозуміти, - це те, що відбувається з фактичним int всередині масиву. Оскільки вони є типовими значеннями, я б здогадувався, що їх доведеться покласти в ящик, як я можу, наприклад, передати мій Integers іншим частинам програми, і це буде захаращувати стек, якби вони залишалися на ньому весь час . Або я помиляюся? Я б здогадувався, що вони просто потрапили в бокс і проживуть на купі до тих пір, поки масив існував.

Відповіді:


289

Ваш масив розміщений на купі, а вкладені елементи не позначені коробкою.

Джерело вашої плутанини, ймовірно, тому, що люди сказали, що типи посилань виділяються на купі, а типи значень виділяються на стеку. Це не зовсім точне уявлення.

Всі локальні змінні та параметри виділяються на стеку. Сюди входять і типи значень, і типи посилань. Різниця між ними полягає лише в тому, що зберігається у змінній. Не дивно, що для типу значень значення типу зберігається безпосередньо у змінній, а для еталонного типу значення типу зберігається у купі, а посилання на це значення - це те, що зберігається у змінній.

Те саме стосується полів. Коли пам'ять виділяється для екземпляра сукупного типу (a classабо a struct), вона повинна містити сховище для кожного свого поля екземпляра. Для полів типу посилання цей сховище містить лише посилання на значення, яке було б виділено пізніше. Для полів типу значень це сховище містить фактичне значення.

Отже, враховуючи такі типи:

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

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

Значення кожного з цих типів потребували б 16 байтів пам'яті (якщо припустити розмір 32-бітного слова). Поле Iв кожному випадку займає 4 байти, щоб зберігати його значення, поле Sзаймає 4 байти, щоб зберігати його посилання, а поле Lзаймає 8 байт, щоб зберігати його значення. Тож пам'ять про значення обох RefTypeі ValTypeвиглядає приблизно так:

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

Тепер , якщо у вас три локальні змінні в функції, типів RefType, ValTypeі int[], як це:

RefType refType;
ValType valType;
int[]   intArray;

тоді ваш стек може виглядати приблизно так:

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

Якщо ви присвоїли значення цим локальним змінним, наприклад:

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;

Тоді ваш стек може виглядати приблизно так:

 0 ┌────────────────────┐
   │ 0x4A963B68 │ - купа купи `refType`
 4 ├────────────────────┤
   │ 200 │ - значення `valType.I`
   │ 0x4A984C10 │ - купа купи `valType.S`
   │ 0x44556677 │ - низький 32-бітний бік `valType.L`
   │ 0x00112233 │ - високий 32-бітний бік `valType.L`
20 ├────────────────────┤
   │ 0x4AA4C288 │ - купа адреси `intArray`
24 └────────────────────┘

Пам'ять за адресою 0x4A963B68(значення refType) буде приблизно таким:

 0 ┌────────────────────┐
   │ 100 │ - значення `refType.I`
 4 ├────────────────────┤
   │ 0x4A984D88 │ - купа адреси `refType.S`
 8 ├────────────────────┤
   │ 0x89ABCDEF │ - низький 32-бітний бік `refType.L`
   │ 0x01234567 │ - високий 32-бітний бік `refType.L`
16 └────────────────────┘

Пам'ять за адресою 0x4AA4C288(значення intArray) буде приблизно таким:

 0 ┌────────────────────┐
   │ 4 │ - довжина масиву
 4 ├────────────────────┤
   │ 300 │ - `intArray [0]`
 8 ├────────────────────┤
   │ 301 │ - `intArray [1]`
12 ├────────────────────┤
   │ 302 │ - `intArray [2]`
16 ├────────────────────┤
   │ 303 │ - `intArray [3]`
20 └────────────────────┘

Тепер, якщо ви перейшли intArrayдо іншої функції, значенням, висунутим на стек, була б 0x4AA4C288адреса масиву, а не копія масиву.


52
Зауважу, що твердження про те, що всі локальні змінні зберігаються у стеку, є неточним. Локальні змінні, що є зовнішніми змінними анонімної функції, зберігаються в купі. Локальні змінні блоків ітераторів зберігаються у купі. Локальні змінні блоків асинхронізації зберігаються у купі. Зареєстровані локальні змінні не зберігаються ні в стеці, ні в купі. Локальні змінні, які видаляються, не зберігаються ні в стеці, ні в купі.
Ерік Ліпперт

5
LOL, завжди збирач азоту, містер Ліпперт. :) Я змушений зазначити, що за винятком двох останніх ваших випадків, так звані "місцеві жителі" перестають бути місцевими жителями на час збирання. Реалізація піднімає їх до статусу членів класу, що є єдиною причиною їх зберігання в купі. Тож це лише деталь реалізації (снайпер). Звичайно, зберігання в регістрі - це ще більш детальна інформація про реалізацію, і elision не враховується.
P тато

3
Звичайно, весь мій пост - це деталі реалізації, але, як я впевнений, ви усвідомлювали, що все це намагалось відокремити поняття змінних та значень . Змінна (називайте її локальною, полем, параметром, будь-яким іншим) може зберігатися у стеці, купі чи іншому місці, визначеному реалізацією, але це не дуже важливо. Що важливо, чи ця змінна безпосередньо зберігає значення, яке вона представляє, або просто посилання на це значення, яке зберігається в іншому місці. Це важливо, оскільки воно впливає на семантику копіювання: чи копіює цю змінну, копіює її значення чи адресу.
П тато

16
Мабуть, ви маєте інше уявлення про те, що означає бути "локальною змінною", ніж я. Ви, здається, вважаєте, що "локальна змінна" характеризується деталями її впровадження . Ця віра не виправдана тим, що мені відомо в специфікації C #. Локальна змінна насправді є змінною, оголошеною всередині блоку, ім'я якої знаходиться в області дії лише у всьому просторі декларації, пов'язаному з блоком. Я запевняю вас, що локальні змінні, які, як деталь реалізації, піднімаються до полів класу закриття, все ще є локальними змінними згідно з правилами C #.
Ерік Ліпперт

15
Однак, звичайно, ваша відповідь загалом відмінна; Справа в тому, що значення концептуально відрізняються від змінних - це те, що потрібно робити якомога частіше і голосніше, оскільки воно є основним. І все ж дуже багато людей вірять у них найдивніші міфи! Так добре вам за те, що ви боролися з хорошою боротьбою.
Ерік Ліпперт

23

Так, масив буде розташований на купі.

Вкладиші всередині масиву не будуть полями. Тільки тому, що тип значень існує на купі, не обов'язково означає, що він буде розміщений у полі. Бокс відбуватиметься лише тоді, коли тип значення, наприклад int, присвоєно посиланням на об'єкт типу.

Наприклад

Не вказує:

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

Коробки:

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

Ви також можете перевірити публікацію Еріка на цю тему:


1
Але я цього не розумію. Чи не слід виділяти типи значень у стеці? Або обидва типи значень та еталон можна виділити як на купі, так і на стеку, і це просто те, що вони зазвичай просто зберігаються в одному чи іншому місці?
пожирав елізіум

4
@Jorge, тип значення без упаковки / контейнера без опорного типу буде жити на стеку. Однак як тільки він використовується в контейнері базового типу, він буде жити в купі. Масив є еталонним типом, а значить, пам'ять для int повинна знаходитися в купі.
JaredPar

2
@Jorge: типи посилань живуть лише в купі, ніколи в стеці. Навпаки, неможливо (у контрольованому коді) зберігати вказівник на місце стека в об’єкт еталонного типу.
Антон Тихий

1
Я думаю, що ви мали намір призначити i arr [0]. Постійне присвоєння все ще спричиняє бокс на "42", але ви створили i, тож ви також можете використовувати його ;-)
Маркус Гріп,

@AntonTykhyy: Немає жодного правила, який я знаю, сказавши, що CLR не може зробити аналіз втечі. Якщо він виявить, що на об'єкт ніколи не буде посилатися протягом життя функції, яка його створила, цілком законно - і навіть бажано - сконструювати об’єкт на стеці, будь то тип значення чи ні. "Тип значення" і "тип посилання" в основному описують те, що знаходиться в пам'яті, зайнятої змінною, а не жорстке і швидке правило про те, де живе об'єкт.
cHao

21

Щоб зрозуміти, що відбувається, ось деякі факти:

  • Об'єкт завжди виділяється на купі.
  • Купа містить лише предмети.
  • Типи значень або виділяються на стеку, або частина об'єкта в купі.
  • Масив - це об'єкт.
  • Масив може містити лише типи значень.
  • Посилання на об'єкт - тип значення.

Отже, якщо у вас є масив цілих чисел, масив виділяється на купу, а цілі числа, які він містить, є частиною об’єкта масиву на купі. Цілі числа знаходяться всередині об’єкта масиву на купі, а не як окремі об'єкти, тому вони не розміщуються в коробці.

Якщо у вас є масив рядків, це дійсно масив посилань на рядок. Оскільки посилання - це типи значень, вони будуть частиною об’єкта масиву на купі. Якщо ви розмістите об’єкт рядка в масиві, ви фактично помістите посилання на об'єкт рядка в масиві, а рядок - це окремий об'єкт у купі.


Так, посилання поводяться точно як типи значень, але я помітив, що їх зазвичай не називають таким чином або не включають у типи значень. Дивіться, наприклад, (але є набагато більше подібного) msdn.microsoft.com/en-us/library/s1ax56ch.aspx
Хенк Холтерман

@Henk: Так, ви праві, що посилання не вказані серед змінних типів значень, але коли мова йде про те, як розподіляється пам'ять для них, вони є в усіх відношеннях типовими значеннями, і це дуже корисно зрозуміти, щоб зрозуміти, як розподіляти пам'ять все підходить разом. :)
Guffa

Сумніваюсь у 5-й точці: "Масив може містити лише типи значень". А як з масивом рядків? string [] рядки = новий рядок [4];
Суніл Пурушотаман

9

Я думаю, що в основі вашого питання лежить непорозуміння щодо еталонних та ціннісних типів. З цим, мабуть, боролися всі розробники .NET та Java.

Масив - це лише список значень. Якщо це масив еталонного типу (скажімо, а string[]), то масив - це список посилань на різні stringоб'єкти в купі, так як посилання є значенням еталонного типу. Внутрішньо ці посилання реалізуються як вказівники на адресу в пам'яті. Якщо ви хочете візуалізувати це, такий масив виглядатиме так у пам'яті (на купі):

[ 00000000, 00000000, 00000000, F8AB56AA ]

Це масив, stringякий містить 4 посилання на stringоб'єкти на купі (номери тут шістнадцяткові). В даний час лише останнє stringнасправді вказує на що-небудь (пам'ять ініціалізується на всі нулі при виділенні), цей масив в основному буде результатом цього коду в C #:

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

Вищенаведений масив був би в 32-бітній програмі. У 64-бітній програмі посилання були б удвічі більшими ( F8AB56AAбули б 00000000F8AB56AA).

Якщо у вас є масив типів значень (скажімо int[]) , то масив являє собою список цілих чисел, так як значення типу значення є саме значення (звідси і назва). Візуалізація такого масиву була б такою:

[ 00000000, 45FF32BB, 00000000, 00000000 ]

Це масив із 4 цілих чисел, де лише другому int присвоюється значення (1174352571, що є десятковою поданням цього шістнадцяткового числа), а решта цілих чисел буде 0 (як я вже сказав, пам'ять ініціалізується до нуля а 00000000 у шістнадцятковій цифрі дорівнює 0 у десятковій частині) Код, який створив цей масив, буде:

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

Цей int[]масив також буде зберігатися у купі.

Як інший приклад, пам'ять short[4]масиву виглядатиме так:

[ 0000, 0000, 0000, 0000 ]

Як значення аshort - це 2-байтне число.

Там, де зберігається тип значення, - це лише деталь реалізації, як тут дуже добре пояснює Ерік Ліпперт , а не притаманні різниці між значеннями та еталонними типами (що є різницею в поведінці).

Коли ви що - то передати в метод (будь то контрольний тип або тип значення) , то копія з значення типу фактично передається методу. У випадку еталонного типу значення - це посилання (подумайте про це як на вказівник на частину пам'яті, хоча це також є детальною частиною реалізації), а у випадку типу типу значення - сама річ.

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

Бокс відбувається лише в тому випадку, якщо ви перетворюєте тип значення в тип посилання. Це кодові поля:

object o = 5;

Я вважаю, що "деталь реалізації" має бути розміром шрифту: 50px. ;)
sisve

2

Це ілюстрації із зображенням вищевідповіді від @P Daddy

введіть тут опис зображення

введіть тут опис зображення

І я проілюстрував відповідний зміст у своєму стилі.

введіть тут опис зображення


@ P Daddy Я робив ілюстрації. Перевірте, чи є неправильна частина. І у мене є додаткові запитання. 1. Коли я створюю масив типу int 4 довжини, інформація про довжину (4) також завжди зберігається в пам'яті?
Парк ЯнгМін

2. На другій ілюстрації скопійована адреса масиву зберігається де? Це та сама область стека, в якій зберігається адреса intArray? Це інший стек, але такий же стек? Це різний вид стека? 3. Що означає низький 32 біт / високий 32 біт? 4. Яке значення повернення, коли я виділяю тип значення (у цьому прикладі структури) на стеку за допомогою нового ключового слова? Це також адреса? Коли я перевіряв цим твердженням Console.WriteLine (valType), воно відображало б повністю кваліфіковане ім’я, як об'єкт типу ConsoleApp.ValType.
Парк ЯнгМін

5. valType.I = 200; Чи означає це твердження, що я отримую адресу valType, за цією адресою я отримую доступ до I і прямо там я зберігаю 200, але "у стеці".
Парк ЯнгМін

1

Масив цілих чисел виділяється на купу, нічого більше, нічого менше. myIntegers посилаються на початок розділу, де розміщені вставки. Ця посилання знаходиться на стеці.

Якщо у вас є масив об'єктів опорного типу, як, наприклад, тип Object, myObjects [], розташований на стеці, буде посилатися на купу значень, на які посилаються самі об'єкти.

Підводячи підсумок, якщо ви передаєте myIntegers деяким функціям, ви передаєте лише посилання на місце, де виділено реальну купу цілих чисел.


1

У вашому прикладі коду немає боксу.

Типи значень можуть жити на купі, як це відбувається у вашому масиві ints. Масив розподіляється на купі, і він зберігає вкладиші, які є типовими значеннями. Вміст масиву ініціалізується до типового значення (int), який, як правило, дорівнює нулю.

Розглянемо клас, який містить тип значення:


    class HasAnInt
    {
        int i;
    }

    HasAnInt h = new HasAnInt();

Змінна h посилається на екземпляр HasAnInt, який живе на купі. Це просто буває, що містить тип значення. Це цілком нормально, "я" просто трапляється жити на купі, як це міститься в класі. У цьому прикладі також немає боксу.


1

Досить сказано всім, але якщо хтось шукає чіткий (але неофіційний) зразок та документацію про купу, стек, локальні та статичні змінні, перегляньте повну статтю Джона Скіта про Пам'ять у .NET - що йде де

Витяг:

  1. Кожна локальна змінна (тобто одна, оголошена методом) зберігається у стеці. Це включає змінні типу опорного типу - сама змінна знаходиться в стеці, але пам’ятайте, що значення змінної типу посилання є лише посиланням (або нулем), а не самим об'єктом. Параметри методу також зараховуються до локальних змінних, але якщо вони оголошені за допомогою модифікатора ref, вони не отримують власний слот, а поділяють слот зі змінною, що використовується в коді виклику. Дивіться мою статтю про проходження параметрів для отримання більш детальної інформації.

  2. Змінні екземпляри для еталонного типу завжди знаходяться в купі. Ось тут і живе сам об’єкт.

  3. Змінні екземпляра для типу значення зберігаються в тому ж контексті, що і змінна, яка оголошує тип значення. Слот пам'яті для екземпляра ефективно містить слоти для кожного поля в екземплярі. Це означає (з огляду на два попередні пункти), що змінна структури, оголошена в методі, завжди буде знаходитись у стеці, тоді як структурна змінна, яка є полем екземпляра класу, буде у купі.

  4. Кожна статична змінна зберігається в купі, незалежно від того, декларована вона в межах еталонного типу або типу значення. Всього лише один слот, незалежно від кількості створених екземплярів. (Для цього одного слоту не потрібно створювати жодних екземплярів.) Деталі того, на якому саме місці розміщуються змінні, складні, але детально пояснені в статті MSDN з цього питання.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.