Массивы, куча, стек и типы значений - PullRequest
127 голосов
/ 11 июля 2009
int[] myIntegers;
myIntegers = new int[100];

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

Ответы [ 8 ]

268 голосов
/ 11 июля 2009

Ваш массив размещен в куче, а целые числа не упакованы.

Источник вашей путаницы, вероятно, связан с тем, что люди говорили, что ссылочные типы размещаются в куче, а типы значений - в стеке. Это не совсем точное представление.

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

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

Итак, даны следующие типы:

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 ┌───────────────────┐
   │        I          │
 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     │ -- heap address of `refType`
 4 ├───────────────────┤
   │       200         │ -- value of `valType.I`
   │    0x4A984C10     │ -- heap address of `valType.S`
   │    0x44556677     │ -- low 32-bits of `valType.L`
   │    0x00112233     │ -- high 32-bits of `valType.L`
20 ├───────────────────┤
   │    0x4AA4C288     │ -- heap address of `intArray`
24 └───────────────────┘

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

 0 ┌───────────────────┐
   │       100         │ -- value of `refType.I`
 4 ├───────────────────┤
   │    0x4A984D88     │ -- heap address of `refType.S`
 8 ├───────────────────┤
   │    0x89ABCDEF     │ -- low 32-bits of `refType.L`
   │    0x01234567     │ -- high 32-bits of `refType.L`
16 └───────────────────┘

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

 0 ┌───────────────────┐
   │        4          │ -- length of array
 4 ├───────────────────┤
   │       300         │ -- `intArray[0]`
 8 ├───────────────────┤
   │       301         │ -- `intArray[1]`
12 ├───────────────────┤
   │       302         │ -- `intArray[2]`
16 ├───────────────────┤
   │       303         │ -- `intArray[3]`
20 └───────────────────┘

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

23 голосов
/ 11 июля 2009

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

Значения внутри массива не будут упакованы. Тот факт, что тип значения существует в куче, не обязательно означает, что он будет упакован. Упаковка будет происходить только тогда, когда тип значения, такой как int, назначен для ссылки на объект типа.

Например

Не упаковывается:

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

Ящики:

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

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

19 голосов
/ 11 июля 2009

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

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

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

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

9 голосов
/ 11 июля 2009

Я думаю, что в основе вашего вопроса лежит недопонимание относительно типов ссылок и значений. Вероятно, с этим боролся каждый разработчик .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;
1 голос
/ 06 декабря 2017

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

enter image description here

enter image description here

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

enter image description here

1 голос
/ 09 апреля 2013

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

Выдержка:

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

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

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

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

1 голос
/ 11 июля 2009

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

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

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


    class HasAnInt
    {
        int i;
    }

    HasAnInt h = new HasAnInt();

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

1 голос
/ 11 июля 2009

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

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

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

...