Основные принципы программирования: стек и куча

Обложка: Основные принципы программирования: стек и куча
Кратко о главном:
- Стек (stack) работает по принципу LIFO и управляется процессором — быстрый, но ограниченный по размеру
- Куча (heap) позволяет динамически выделять память и создавать глобальные переменные
- Стек используется для локальных переменных и вызовов функций, куча — для динамических структур данных
- В языках без сборщика мусора (C, C++) за освобождение памяти в куче отвечает программист
- Некорректное управление кучей приводит к утечкам и фрагментации памяти

Рассказывает Аарон Краус

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

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

Стек

Стек — это область оперативной памяти, которая создаётся для каждого потока. Он работает в порядке LIFO (Last In, First Out), то есть последний добавленный в стек кусок памяти будет первым в очереди на вывод из стека. Каждый раз, когда функция объявляет новую переменную, она добавляется в стек, а когда эта переменная пропадает из области видимости (например, когда функция заканчивается), она автоматически удаляется из стека. Когда стековая переменная освобождается, эта область памяти становится доступной для других стековых переменных.

Из-за такой природы стека управление памятью оказывается весьма логичным и простым для выполнения на ЦП; это приводит к высокой скорости, в особенности потому, что время цикла обновления байта стека очень мало, т.е. этот байт скорее всего привязан к кэшу процессора. Тем не менее, у такой строгой формы управления есть и недостатки. Размер стека — это фиксированная величина, и превышение лимита выделенной на стеке памяти приведёт к переполнению стека. Размер задаётся при создании потока, и у каждой переменной есть максимальный размер, зависящий от типа данных. Это позволяет ограничивать размер некоторых переменных (например, целочисленных), и вынуждает заранее объявлять размер более сложных типов данных (например, массивов), поскольку стек не позволит им изменить его. Кроме того, переменные, расположенные на стеке, всегда являются локальными.

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

Куча

Куча — это хранилище памяти, также расположенное в ОЗУ, которое допускает динамическое выделение памяти и не работает по принципу стека: это просто склад для ваших переменных. Когда вы выделяете в куче участок памяти для хранения переменной, к ней можно обратиться не только в потоке, но и во всем приложении. Именно так определяются глобальные переменные. По завершении приложения все выделенные участки памяти освобождаются. Размер кучи задаётся при запуске приложения, но, в отличие от стека, он ограничен лишь физически, и это позволяет создавать динамические переменные.

Вы взаимодействуете с кучей посредством ссылок, обычно называемых указателями — это переменные, чьи значения являются адресами других переменных. Создавая указатель, вы указываете на местоположение памяти в куче, что задаёт начальное значение переменной и говорит программе, где получить доступ к этому значению. Из-за динамической природы кучи ЦП не принимает участия в контроле над ней; в языках без сборщика мусора (C, C++) разработчику нужно вручную освобождать участки памяти, которые больше не нужны. Если этого не делать, могут возникнуть утечки и фрагментация памяти, что существенно замедлит работу кучи.

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

Сравнение стека и кучи

Параметр | Стек (Stack) | Куча (Heap)
Принцип работы | LIFO (последний пришёл — первый ушёл) | Произвольный доступ через указатели
Скорость | Очень высокая (управляется ЦП) | Ниже (управляется программой или GC)
Размер | Фиксированный, задаётся при создании потока | Ограничен только физической памятью
Область видимости | Локальные переменные | Глобальные и динамические переменные
Управление | Автоматическое (ЦП) | Ручное (программист) или сборщик мусора
Риски | Переполнение стека (stack overflow) | Утечки и фрагментация памяти
Применение | Вызовы функций, локальные переменные | Динамические структуры, объекты, массивы

Заключение

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

Часто задаваемые вопросы

Что произойдёт, если стек переполнится?

Возникнет ошибка переполнения стека (stack overflow). Чаще всего это случается при бесконечной рекурсии — когда функция вызывает саму себя без условия выхода, каждый вызов добавляет новый фрейм в стек, пока память не закончится.

Зачем нужна куча, если стек быстрее?

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

Как сборщик мусора связан с кучей?

Сборщик мусора (garbage collector) автоматически отслеживает объекты в куче и освобождает память, которая больше не используется. Он есть в языках вроде Java, Python, Go и C#. В языках без сборщика мусора (C, C++) программист обязан вручную вызывать free() или delete для освобождения памяти.

В каких языках программист должен управлять памятью вручную?

В C и C++ программист сам отвечает за выделение и освобождение памяти в куче (malloc/free, new/delete). Rust использует уникальный подход — систему владения (ownership), которая позволяет управлять памятью без сборщика мусора и без ручного освобождения.