ПОРТАЛ ПРОГРАММИСТОВ
Как работают функции в C/C++ и стек?


Здесь мы поговорим о работе функций. Не секрет, что любая программа на С++ состоит из функций. И не только на С++, но и на других языках программирования программы составляются из функций, процедур, подпрограмм.

В общем-то, всем известно, как надо использовать функции. Вызывается функция, ей передаются параметры, потом выполняется тело функции, после того, как оно выполнилось, функция возвращает значение, которое мы уже используем по своему разумению. Всё красиво и понятно! Но, начинающие программеры - народ любопытный, всё им интересно, и задаются они, порой, такими вопросами: а как на самом деле это всё происходит? Как программа не заблудится, прыгая из тела одной функции в другую и обратно? Где хранятся переменные при передаче их в качестве аргументов? Что происходит с переменными, которые объявляются в теле функции? Как возвращаемое значение передаётся назад?

Итак, когда программа начинает работу, операционная система (будь то DOS, UNIX или Microsoft Windows, неважно) выделяет различные области памяти. Нам с вами довольно часто приходится иметь дело с пространством глобальных имён, свободной памятью, регистрами, памятью сегментов программы и стеками.

Глобальные переменные находятся в пространстве глобальных имён.

Регистры - это область памяти, находящаяся в ЦП. Именно в них размещаются данные в момент их обработки процессором. Но всё же рассмотрим набор регистров, ответственных за указание на следующую строку программы в любой момент времени. Назовём эти регистры (все вместе) указателями команд (instruction pointer). Именно указатель команды содержит информацию о том, какая строка программы должна выполняться следующей.

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

Стек - это специальная область оперативной памяти, выделенная для размещения данных программы, необходимых каждой вызываемой функции. Называется это добро стеком, потому что работает по принципу LIFO - последним пришёл - первым вышел (Last In - First Out). Замусоленных аналогий приводилось уже миллион, давайте возьмём самую популярную - стопка тарелок… В стопке тарелок, последняя положенная в стопку тарелка будет взята из неё первой. А чтобы достать тарелку из середины стопки, придётся сначала вытащить все тарелки, которые лежат выше неё (т.е. положены были позже). Сия повесть справедлива и для данных в стеке памяти. Это сравнение довольно наглядно, но в принципе неверно. Более точной моделью является этажёрка, состоящая из ряда полок, расположенных друг над другом. Вершиной стека будет служить любая полка, на которую в данный момент указывает указатель вершины стека (эту роль выполняет другой регистр).

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

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

Данные выше указателя вершины стека - это мусор, они случайны и полагаться на то, что они пусты, нельзя!

Когда мы удаляем значение из стека, просто изменяется адрес в указателе вершины стека.

Теперь некоторые способы размещения данных в памяти понятны, и можно разобраться во всём процессе работы функции. Итак, встретился в программе вызов функции, вот что происходит дальше:

Адрес в указателе команд увеличивается так, чтобы указывать на инструкцию, следующую после вызова функции. Затем этот адрес помещается в стек и будет служить адресом возврата по завершении выполнения функции.

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

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

Текущий адрес вершины стека сохраняется в специальном указателе, называемом записью активации (stack frame). Всё, добавленное в стек с этого момента и до завершения работы функции, будет рассматриваться как локальные данные функции.

Все аргументы функции помещаются в стек.

Выполняется команда, адрес которой находится в данный момент в указателе команд, т.е. первая команда функции.

Локальные переменные помещаются в стек в прядке их определения.

И пошло-поехало… Когда функция завершается, возвращаемое значение помещается в область стека, зарезервированную на этапе 2. Указатель из записи активации пересылается в указатель стека, таким образом стек полностью очищается вплоть до начала кода функции. Удаляются все локальные переменные и аргументы функции.

Затем возвращаемое значение извлекается из стека и присваивается переменной, вызвавшей функцию, а адрес команды, сохранённый в стеке на этапе 1, присваивается указателю команд. Таким образом, программа продолжает свою работу с команды, следующей после обращения к функции, уже обладая значением, возвращённым функцией.

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