Стек и куча: где живут переменные

Вопрос, с которого начинается почти любое собеседование по C++: «Куда именно попадает переменная — в стек или в кучу?»

Стек — область памяти, где переменные создаются и удаляются автоматически, в строгом порядке «последним пришёл — первым ушёл». Куча — область памяти, которой управляет программист вручную через new и delete.

Что выведет этот код?

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

#include <iostream>

int* sozdatUkazatel() {
    int lokalnaya = 42;
    return &lokalnaya; // опасно!
}

int main() {
    int* p = sozdatUkazatel();
    std::cout << *p << std::endl;
    return 0;
}

Вывод:

непредсказуемое значение (может быть 42, может быть мусор, может быть краш)

Это ловушка. Переменная lokalnaya — самая обычная локальная переменная функции, а значит она живёт в стеке. Как только функция sozdatUkazatel заканчивает работу, память под lokalnaya автоматически освобождается — компилятор сам «сворачивает» стековый кадр этой функции. Указатель p в main после этого хранит адрес, по которому раньше лежало число 42, но гарантий, что там всё ещё 42, нет никаких. Это называется висячий указатель (dangling pointer) — он указывает на память, которая формально ему больше не принадлежит.

Как устроен стек

Представьте стопку тарелок в столовой. Вы можете положить тарелку только сверху и снять — тоже только сверху. Так же работает стек памяти: каждый вызов функции добавляет «тарелку» (стековый кадр) со всеми её локальными переменными, а когда функция завершается, эта тарелка снимается автоматически. Программисту не нужно ничего освобождать вручную — компилятор делает это сам, причём мгновенно, простым сдвигом указателя стека.

Именно поэтому стек такой быстрый: выделение памяти — это буквально одна операция вычитания из указателя, а не поиск свободного блока нужного размера. Но у скорости есть цена: размер стека ограничен (обычно несколько мегабайт), и он не переживает выход из функции. Всё, что должно «пережить» функцию, где оно создано, нельзя класть только в стек.

Как устроена куча

Куча — это большая общая область памяти, из которой можно попросить кусок в любой момент через new, и он останется вашим, пока вы явно не вернёте его через delete. Никакого автоматического порядка — куски можно выделять и освобождать в любой последовательности, отсюда и название на английском — heap, «куча», в смысле беспорядочная груда, а не стройная стопка.

#include <iostream>

int* sozdatVKuche() {
    int* p = new int(42); // память в куче
    return p; // безопасно возвращать!
}

int main() {
    int* p = sozdatVKuche();
    std::cout << *p << std::endl;
    delete p; // обязаны освободить сами
    return 0;
}

Вывод:

42

Здесь new int(42) выделяет память в куче, и эта память живёт до тех пор, пока кто-то явно не вызовет delete — выход из функции на неё никак не влияет. Поэтому возвращать такой указатель из функции безопасно. Но появляется новая ответственность: если забыть про delete, память останется занятой навсегда, пока не завершится программа — это называется утечкой памяти, и подробно про неё поговорим в следующем уроке раздела.

Сравнение в одной таблице

ПризнакСтекКуча
Кто выделяеткомпилятор автоматическипрограммист через new
Кто освобождаеткомпилятор автоматическипрограммист через delete
Скоростьочень быстраямедленнее (поиск свободного блока)
Время жизнипока выполняется функцияпока не вызван delete
Размерограничен (обычно единицы МБ)ограничен объёмом ОЗУ

Частые ошибки на собеседовании

  • Путают направление роста: думают, что куча «быстрее», хотя на самом деле быстрее именно стек — из-за отсутствия поиска свободного блока.
  • Не могут объяснить, почему возврат адреса локальной переменной — это баг, а возврат указателя на объект из кучи — нормальная практика.
  • Забывают, что даже если «повезло» и висячий указатель показал правильное значение (как в первом примере), это не делает код корректным — на других компиляторах или в других условиях он упадёт.

Итог

  • Стек — автоматическая память, живёт ровно до конца функции, освобождается сама.
  • Куча — ручная память, живёт до явного delete, может пережить функцию, где создана.
  • Возвращать адрес локальной (стековой) переменной из функции — классическая ошибка, ведущая к висячему указателю.
Проверьте себя
1. Почему нельзя безопасно возвращать из функции указатель на её обычную локальную переменную?
AПотому что локальные переменные хранятся в куче и требуют delete
BПотому что память локальной переменной в стеке освобождается автоматически при выходе из функции
CПотому что компилятор запрещает такой код и он не соберётся
DПотому что указатели в C++ нельзя возвращать из функций
2. Почему выделение памяти в стеке обычно быстрее, чем в куче?
AСтек выделяет память сдвигом указателя, а куча ищет подходящий свободный блок
BСтек использует более быструю физическую память ОЗУ
CВ куче память выделяется только через операционную систему напрямую
DНа самом деле разницы в скорости нет