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


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

В приведенном выше коде новый int [100] генерирует массив в куче? Из того, что я прочитал на CLR через c #, ответ - да. Но то, что я не могу понять, - это то, что происходит с действительными значениями int внутри массива. Так как они являются типами значений, я бы предположил, что они должны быть упакованы, как я могу, например, передать myIntegers в другие части программы, и это будет загромождать стек, если они будут все время оставаться на нем. , Или я не прав? Я предполагаю, что они будут просто упакованы и будут жить в куче столько, сколько существует массив.

Ответы:


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 ├───────────────────┤
   Ar 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 Daddy

3
Конечно, весь мой пост посвящен деталям реализации, но, как я уверен, вы понимаете, это была попытка отделить понятия переменных и значений . Переменная (назовите ее локальная, поле, параметр, что угодно) может храниться в стеке, куче или каком-то другом месте, определяемом реализацией, но это не так важно. Важно то, хранит ли эта переменная непосредственно значение, которое она представляет, или просто ссылку на это значение, хранящееся в другом месте. Это важно, потому что это влияет на семантику копирования: копирует ли эта переменная копирует ее значение или адрес.
P Daddy

16
Очевидно, у вас есть другое представление о том, что значит быть «локальной переменной», чем я. Кажется, вы верите, что «локальная переменная» характеризуется деталями ее реализации . Это убеждение не оправдано ничем, что я знаю в спецификации C #. Локальная переменная фактически является переменной, объявленной внутри блока, имя которого находится в области видимости только во всем пространстве объявления, связанном с блоком. Уверяю вас, локальные переменные, которые, как деталь реализации, добавляются к полям класса замыкания, по-прежнему являются локальными переменными в соответствии с правилами C #.
Эрик Липперт

15
Тем не менее, конечно, ваш ответ в целом отлично; Дело в том, что значения концептуально отличаются от переменных - это то, что нужно делать как можно чаще и громче, поскольку это фундаментально. И все же очень многие люди верят в самые странные мифы о них! Так хорошо, что ты сражаешься в хорошей борьбе.
Эрик Липперт

23

Да, массив будет расположен в куче.

Ints внутри массива не будет упакован. Тот факт, что тип значения существует в куче, не обязательно означает, что он будет упакован. Упаковка будет происходить только тогда, когда тип значения, такой как 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: Да, вы правы, что ссылки не перечислены среди переменных типа значения, но когда дело доходит до того, как для них выделяется память, они имеют значение во всех отношениях, и очень полезно понять это, чтобы понять, как распределяется память. все сходится. :)
Гуффа

Я сомневаюсь в 5-м пункте: «Массив может содержать только типы значений». Что насчет строкового массива? строка [] строки = новая строка [4];
Сунил Пурушотаман

9

Я думаю, что в основе вашего вопроса лежит недоразумение о ссылочных типах и типах значений. Наверное, с этим боролся каждый разработчик .NET и Java.

Массив - это просто список значений. Если это массив ссылочного типа (скажем, a 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).

Если у вас есть массив типов значений (скажем, an int[]), то массив представляет собой список целых чисел, поскольку значением типа значения является само значение (отсюда и имя). Визуализация такого массива будет такой:

[ 00000000, 45FF32BB, 00000000, 00000000 ]

Это массив из 4 целых чисел, где только второму целому присваивается значение (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 ]

В качестве значения используется short2-байтовое число.

Где хранится тип значения, это просто деталь реализации, как очень хорошо объясняет здесь Эрик Липперт , не свойственный различиям между типом значения и ссылочным типом (что является различием в поведении).

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

// 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;

Я считаю, что «деталь реализации» должна иметь размер шрифта: 50 пикселей. ;)
sisve

2

Это иллюстрации, изображающие приведенный выше ответ @P Daddy

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

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

И я проиллюстрировал соответствующее содержание в своем стиле.

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


@P Папа, я сделал иллюстрации. Пожалуйста, проверьте, если есть неправильная часть. И у меня есть несколько дополнительных вопросов. 1. Когда я создаю массив типа int длиной 4, информация о длине (4) также всегда сохраняется в памяти?
YoungMin Park

2. На втором рисунке адрес скопированного массива хранится где? Это та же самая область стека, в которой хранится адрес intArray? Это другой стек, но такой же? Это другой вид стека? 3. Что означает младший 32-битный / старший 32-битный? 4. Что такое возвращаемое значение, когда я выделяю тип значения (в этом примере структуру) в стеке с помощью ключевого слова new? Это тоже адрес? Когда я проверял этим оператором Console.WriteLine (valType), он показывал бы полное имя, подобное объекту, например, ConsoleApp.ValType.
YoungMin Park

5. valType.I = 200; Означает ли это утверждение, что я получаю адрес valType, по этому адресу я получаю доступ к I и тут же храню 200, но «в стеке».
YoungMin Park

1

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

Если у вас есть массив объектов ссылочного типа, таких как тип объекта, myObjects [], расположенный в стеке, будет ссылаться на группу значений, которые сами ссылаются на объекты.

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


1

В вашем примере кода нет бокса.

Типы значений могут жить в куче, как в массиве целых чисел. Массив размещается в куче и хранит целые числа, которые являются типами значений. Содержимое массива инициализируется по умолчанию (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.