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

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адресом массива, а не его копией.

P папа
источник
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;

Вы также можете проверить сообщение Эрика на эту тему:

JaredPar
источник
1
Но я не понимаю. Разве типы значений не должны быть размещены в стеке? Или как значения, так и ссылочные типы могут быть размещены как в куче, так и в стеке, и просто они обычно хранятся в одном месте или в другом?
пожрал Элизиум
4
@Jorge, тип значения без ссылочного типа-оболочки / контейнера будет жить в стеке. Однако, как только он используется в контейнере ссылочного типа, он будет жить в куче. Массив является ссылочным типом, и, следовательно, память для int должна находиться в куче.
JaredPar
2
@Jorge: ссылочные типы живут только в куче, а не в стеке. Наоборот, невозможно (в проверяемом коде) сохранить указатель на местоположение стека в объект ссылочного типа.
Антон Тихий
1
Я думаю, что вы хотели присвоить i arr [0]. Постоянное назначение все равно будет вызывать бокс «42», но вы создали i, так что вы можете также использовать его ;-)
Маркус Грип
@AntonTykhyy: я не знаю правила, что CLR не может выполнить анализ побега. Если он обнаруживает, что на объект никогда не будут ссылаться по истечении времени жизни функции, которая его создала, вполне законно - и даже предпочтительнее - построить объект в стеке, независимо от того, является ли он типом значения или нет. «Тип значения» и «ссылочный тип» в основном описывают то, что находится в памяти, занятой переменной, а не жесткое и быстрое правило о том, где находится объект.
cHao
21

Чтобы понять, что происходит, вот несколько фактов:

  • Объект всегда размещается в куче.
  • Куча содержит только объекты.
  • Типы значений либо размещаются в стеке, либо являются частью объекта в куче.
  • Массив - это объект.
  • Массив может содержать только типы значений.
  • Ссылка на объект является типом значения.

Таким образом, если у вас есть массив целых чисел, массив размещается в куче, а целые числа, которые он содержит, являются частью объекта массива в куче. Целые числа находятся внутри объекта массива в куче, а не как отдельные объекты, поэтому они не упакованы.

Если у вас есть массив строк, это действительно массив строковых ссылок. Поскольку ссылки являются типами значений, они будут частью объекта массива в куче. Если вы помещаете строковый объект в массив, вы фактически помещаете ссылку на строковый объект в массиве, и строка является отдельным объектом в куче.

Guffa
источник
Да, ссылки ведут себя точно так же, как типы значений, но я заметил, что они обычно не вызываются таким образом или не включаются в типы значений. Смотрите, например (но их гораздо больше) 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;
JulianR
источник
Я считаю, что «деталь реализации» должна иметь размер шрифта: 50 пикселей. ;)
sisve
2

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

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

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

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

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

YoungMin Park
источник
@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 некоторым функциям, вы только передаете ссылку на место, где размещается реальная группа целых чисел.

Dykam
источник
1

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

Типы значений могут жить в куче, как в массиве целых чисел. Массив размещается в куче и хранит целые числа, которые являются типами значений. Содержимое массива инициализируется по умолчанию (int), которое оказывается равным нулю.

Рассмотрим класс, который содержит тип значения:


    class HasAnInt
    {
        int i;
    }

    HasAnInt h = new HasAnInt();

Переменная h относится к экземпляру HasAnInt, который живет в куче. Так получилось, что он содержит тип значения. Это совершенно нормально, «я» просто живу в куче, как это содержится в классе. В этом примере также нет бокса.

Курт Николс
источник
1

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

Выдержка:

  1. Каждая локальная переменная (т. Е. Объявленная в методе) хранится в стеке. Это включает переменные ссылочного типа - сама переменная находится в стеке, но помните, что значение переменной ссылочного типа является только ссылкой (или нулем), а не самим объектом. Параметры метода также считаются локальными переменными, но если они объявлены с модификатором ref, они не получают свой собственный слот, а делят его с переменной, используемой в вызывающем коде. Смотрите мою статью о передаче параметров для более подробной информации.

  2. Переменные экземпляра для ссылочного типа всегда находятся в куче. Вот где сам объект «живет».

  3. Переменные экземпляра для типа значения хранятся в том же контексте, что и переменная, которая объявляет тип значения. Слот памяти для экземпляра эффективно содержит слоты для каждого поля в экземпляре. Это означает (учитывая две предыдущие точки), что переменная структуры, объявленная в методе, всегда будет в стеке, тогда как переменная структуры, которая является полем экземпляра класса, будет в куче.

  4. Каждая статическая переменная хранится в куче, независимо от того, объявлена ​​ли она в ссылочном типе или типе значения. Всего есть только один слот, независимо от того, сколько экземпляров создано. (Однако для того, чтобы существовал один слот, не нужно создавать никаких экземпляров.) Детали того, в какой именно куче находятся переменные, сложны, но подробно объясняются в статье MSDN на эту тему.

gmaran23
источник