Как происходит инициализация переменных в C# и где они "лежат" перед созданием?

Есть несколько переменных:

var a = 1;
var b = "string";
var c = false;
var d = new MyClass();

Можете объяснить, где лежат значения этих переменных перед их инициализацией?

Правильно ли я понимаю, что на этапе компиляции значения помещаются в кучу, а в рантайме из кучи берутся значения?


Ответы (1 шт):

Автор решения: Швеев Алексей

Глобально память можно поделить на 2 области: стек и куча.

Размер стека всегда известен и фиксирован ещё на этапе компиляции внутри конкретного блока кода. В нём хранятся переменные локальной области видимости

В куче же хранятся данные, размер которых (на этапе компиляции) не известен.

Я немного всё упрощу для понимания, но основные концепции понять можно

Давайте не основе примера разберём, как именно выделяется память. Допустим у нас есть блок кода:

{
  int x;
  string str = "Hello world!";
  List<int> intList = new() {1, 2, 3};
  MyClass anyClass = new();
  MyStruct anyStruct = new();
}

Представим стек как массив байт: [...]. Когда мы открываем новый блок кода мы грубо говоря объявляем, что сейчас в стеке будет использовано ещё N байтов. Для этого нам заранее (ещё на этапе компиляции) должен быть известен размер данного блока кода.

Однако не все типы фиксированного размера. Типы, размер которых неизвестен на этапе компиляции лежат в куче. Таким образом в C# появляется разделение на ссылочные типы и типы значений

Размер типов значений нам известен на этапе компиляции. Для int это например 4 байта.

Ссылочные же типы превращаются в значение адреса в памяти самого объекта, т. е. в nint (тип, размер которого зависит от разрядности системы). Сам объект же лежит в куче, и при обращении к нему мы его достаём из него.

P. S.: Для упрощения представим, что тип строки представляет из себя ссылочный тип, так как там свои тонкости.

В связи с этой информацией перепишем код выше на идентичный, но более близкий к компилятору:

{
  int x;
  nint strPointer;
  nint listPoiner;
  nint classPointer;
  MyStruct anyStruct; // Структуры являются типом значений
}

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

int32 (4 байта) + 
int64(8 байтов) +
int64(8 байтов) +
int64(8 байтов) + 
MyStruct (сумма размеров всех полей, допустим 4 * 3 байта) =
40 байтов

Теперь на стеке в том же порядке мы выделяем эти 40 байтов под наши типы:

[..., int32_0, int32_1, int32_2, int32_3, int64_0, int64_1, ...]

Что бы получить доступ к переменной x нам надо получить sizeof(int) (4) байтов со сдвигом 0 относительно текущего блока кода.

Что бы получить доступ к ссылке на строку нам надо получить sizeof(int64) (8) байтов со сдвигом суммы всех предыдущих размеров (sizeof(int) байтов, так как перед ним стоит только int)

Что бы получить доступ к данным anyStruct переменной, нам надо получить sizeof(MyStruct) (12) байтов со сдвигом sizeof(int) + sizeof(int64) + sizeof(int64) + sizeof(int64) (28) байтов

Т. е. грубо говоря для данного блока кода относительно его начала внутри стека стек всегда будет выглядеть так: [..., int, int64, int64, int64, MyStruct, ...]

Отвечая на главный вопрос:

Перед инициализацией они лежат в стеке, и C# по умолчанию присваивает им значения по умолчанию.

Если же говорить про объекты, то до инициализации в куче они могут вообще не существовать (null значит нулевая ссылка, т. е. на ничто)

→ Ссылка