Глобальные и статические переменные
Глобальные и статические переменные в этом случае наименее интересны. Они существуют в памяти до тех пор, пока программа работает, поэтому их размещение выполняется компилятором в сегменте данных (для настольных ПК) или просто в ОЗУ (для микроконтроллеров). Каждая переменная получает постоянный адрес выделенного ей блока памяти соответствующего размера. Состояния «глобальный», «внешний», «статический» для процессора не важны. Они необходимы компилятору, чтобы различать доступ к переменным, и программисту, поскольку они отражают семантику видимости данных.
Локальные переменные
А вот с локальными переменными все интереснее. Во-первых, такие переменные не всегда существуют, а это значит, что они должны каким-то образом создаваться и уничтожаться во время выполнения программы. Во-вторых, если мы создаем вложенный блок в программе, переменные этого блока, которые имеют те же имена, что и блок более высокого уровня, скрывают ранее определенные переменные, которые продолжают существовать.
То есть локальные переменные должны не только создаваться и уничтожаться во время работы программы, но также должна поддерживаться их иерархия. Локальные переменные в языке C, если не указано иное, получают модификатор auto, но для нас это пока не важно. Местоположение локальных переменных по умолчанию — в стеке.
Что, если в архитектуре процессора, например микроконтроллера, нет стека данных? Таким образом, микроконтроллеры Microchip PIC среднего класса имеют аппаратный стек всего из 8 ячеек, и он не доступен в программном обеспечении и автоматически используется только в командах вызова процедуры, возврата из процедуры и для прерываний. В этом случае компилятор будет использовать для стека обычную область памяти, так называемый программно управляемый стек.
Возьмем небольшой пример. При этом не будем обращать внимание на то, что главное — это процедура.
Напомним, что стек позиционируется в сторону уменьшения адресов. Это не очень важно, но обычно бывает так. Сначала в стек помещается переменная var1, затем var2, затем структура var3. Вот как это будет выглядеть.
Предположим, что int имеет длину два байта. Следовательно, адрес переменной var3 будет равен содержимому регистра SP (указателя стека). Переменная var2 будет в SP + 4, а переменная var1 — в SP + 6. Пока ничего сложного. Обратите внимание на «начало кадра». Набор переменных процедуры (или блок, как вы увидите ниже), хранящийся в стеке, называется кадром стека. Это не просто термин, это удобная абстракция. Что нужно, посмотрим. Указатель на начало кадра на рисунке показан некорректно, но об этом позже.
Теперь давайте немного усложним наш пример, добавив вложенные блоки.
Теперь наш стек будет выглядеть так
Обратите внимание, что у нас есть еще один кадр стека. Но он появится не сразу в начале выполнения программы, а только тогда, когда выполнение достигнет оператора for, в котором объявлены две другие переменные. Оператор for создает новый вложенный блок внутри родительского блока, который будет блоком верхнего уровня для блока for. Когда выполнение достигает закрывающей фигурной скобки оператора for, frame2 будет удален.
Но теперь проблема стала очевидной: внутри оператора for нам нужно получить доступ к переменным var2 и var3 внешнего блока, и указатель стека изменился, и теперь SP указывает на переменную tmp, а не на var3. Здесь становится понятна еще одна функция фрейма стека. Переменные в кадрах стека адресуются не относительно SP, а относительно начала кадра. Если вам нужно получить доступ к переменной из одного из внешних блоков, вам нужно добавить смещение к началу соответствующего кадра.
Это похоже на доступ к элементам массива, который мы обсуждали ранее в другой статье. Для нашего рисунка адрес переменной var3 будет равен «начало кадра 1» минус 6 независимо от того, появились новые кадры в стеке или нет. В процессорах x86 есть регистр BP (EBP), который принимает имя «базового указателя» и который, в отличие от регистров SI, DI и BX (косвенная адресация), формирует только адрес относительно сегмента SS, то есть куча.
Этот регистр используется для хранения адреса начала кадра стека, а регистр SP, как обычно, используется для указания вершины стека. В процессорах другой архитектуры регистр типа BP может не существовать или его функцию может выполнять другой регистр, суть остается прежней.
Примечание. Локальные переменные могут быть не только явно объявлены программистом. Компилятор может создавать скрытые переменные для хранения возвращаемых значений функций. Точно так же скрытые временные переменные могут быть созданы оптимизатором кода компилятора в процессе обработки сложных математических выражений.
Регистровые переменные
Переменные реестра доступны только на языке C (модификатор реестра). По крайней мере, с общих языков. Обычно они не используются в обычных программах. Более того, компилятор имеет полное право переопределить модификатор реестра. Объявляя переменную как регистр, компилятору рекомендуется размещать переменную не в ОЗУ, а в одном из регистров процессора. Доступ к регистрам обычно намного быстрее, чем к основной памяти, что может быть важно для критических по времени разделов программ. Регистровые переменные не имеют адреса и поэтому недоступны по ссылке.
В то же время компилятор может поместить переменную в память (скорее всего, в стек), но если к ней часто обращаются в каком-либо фрагменте программы, поместите ее в регистр, а затем скопируйте обратно в память. Переменная реестра не может быть глобальной. Кроме того, для процессора мощности может быть зарегистрирована только одна естественная переменная. То есть структуру, матрицу, объединение нельзя зарегистрировать.
Динамические переменные
Точнее, переменные с динамическим распределением памяти. Если для позиционирования переменной требуется много памяти, например для большого массива, размещение ее в стеке становится проблематичным. В конце концов, размер стопки часто ограничен. В этом случае объявляется переменная-указатель (другое имя — это ссылочная переменная), которая содержит адрес блока памяти, выделенного операционной системой (или средой выполнения компилятора).
Конечно, запрос на выделение блока памяти должен быть сделан программистом, поскольку выделение переменной-указателя не подразумевает автоматического выделения памяти для самих данных. Точно так же выделенный системой блок памяти должен быть возвращен системе программистом перед уничтожением переменной-указателя. В противном случае возникнут ошибки доступа к памяти или утечки памяти. В C ++ есть концепции конструктора и деструктора.
Операции запроса блока кучи памяти и возврата памяти системе могут быть помещены в эти процедуры, в классе, экземпляром которого является переменная.
Читайте также: Github — turicas