Стек и куча: где живут переменные
Вопрос, с которого начинается почти любое собеседование по 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, может пережить функцию, где создана. - Возвращать адрес локальной (стековой) переменной из функции — классическая ошибка, ведущая к висячему указателю.