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

Чем стек отличается от кучи, почему большой локальный массив и глубокая рекурсия приводят к падению, что такое утечка памяти и висячий указатель. Адреса настоящих переменных, сообщения санитайзера и правило, которое избавляет от большинства ошибок: vector и unique_ptr вместо new.

Содержание

Программа объявляет массив на десять миллионов чисел и падает, не выполнив ни одной строки. Функция возвращает указатель, а по нему лежит мусор. Программа работает сутки и занимает всё больше памяти. У всех трёх историй общий корень: переменные в C++ живут в разных областях памяти, и у каждой области свои правила.

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

Посмотрим на адреса

C++
#include <iostream>
#include <vector>

int global_counter = 0;

int main() {
    int local = 1;
    int other = 2;
    int* heap_value = new int(3);
    std::vector<int> numbers(1000000, 7);

    std::cout << "глобальная:       " << &global_counter << '\n';
    std::cout << "локальная:        " << &local << '\n';
    std::cout << "ещё локальная:    " << &other << '\n';
    std::cout << "в куче:           " << heap_value << '\n';
    std::cout << "сам вектор:       " << &numbers << '\n';
    std::cout << "элементы вектора: " << numbers.data() << '\n';
    std::cout << "sizeof(vector) = " << sizeof(numbers) << '\n';

    delete heap_value;
    return 0;
}

Вывод на нашем компьютере (Linux, g++ 14.2); у Вас числа будут другими, но картина — той же:

TXT
глобальная:       0xaaaace490094
локальная:        0xffffdd3277b0
ещё локальная:    0xffffdd3277ac
в куче:           0xaaaafe41e2b0
сам вектор:       0xffffdd327790
элементы вектора: 0xffff98800010
sizeof(vector) = 24

Что здесь видно.

  • Две локальные переменные лежат вплотную: адреса отличаются на 4 байта, размер одного int. Это стек.
  • Глобальная переменная и число, созданное через new, находятся совсем в другом месте.
  • Сам вектор — локальная переменная, он рядом с остальными и занимает всего 24 байта. А миллион его элементов лежит отдельно: в 24 байта они не поместились бы.

Стек

Стек — область памяти, которой программа пользуется как стопкой тарелок. Вызвали функцию — сверху положили «тарелку» с её параметрами и локальными переменными. Функция закончилась — тарелку сняли.

Отсюда свойства стека.

  • Память выделяется и освобождается сама. Вы ничего для этого не пишете.
  • Это очень быстро. Выделить место — значит сдвинуть одну границу.
  • Переменная живёт до конца блока, в котором объявлена. Вышли за закрывающую скобку — её больше нет.
  • Размер стека мал и задан заранее. В Linux обычно 8 мегабайт, в Windows — 1 мегабайт.

Последнее свойство — причина двух известных падений.

Большой локальный массив

C++
int main() {
    int big[10000000];          // 40 мегабайт на стеке
    big[0] = 1;
    // ...
}

Десять миллионов чисел по 4 байта — это 40 мегабайт. В стек размером 8 мегабайт они не помещаются, и программа завершается с ошибкой Segmentation fault, едва начав работу.

Слишком глубокая рекурсия

C++
long long depth(long long n) {
    return n == 0 ? 0 : 1 + depth(n - 1);
}

Вызов depth(10000) работает. Вызов depth(100000000) падает: каждый вложенный вызов кладёт на стек свою «тарелку», и на сто миллионов вызовов места нет. Это и есть переполнение стека — stack overflow.

Куча

Куча — большая область памяти, из которой программа берёт куски нужного размера по запросу. Размер кучи ограничен только памятью компьютера.

C++
int* scores = new int[10000000];    // 40 мегабайт в куче — без проблем
scores[0] = 1;
delete[] scores;                     // вернуть память

Свойства кучи противоположны свойствам стека.

  • Память выделяется по запросу — оператором new или контейнером вроде std::vector.
  • Это медленнее: нужно найти свободный кусок подходящего размера.
  • Данные живут, пока их явно не освободят. Конец функции на них не влияет.
  • Места много.
СтекКуча
Кто выделяет памятькомпилятор, автоматическипрограмма, по запросу
Кто освобождаеткомпилятор, в конце блокапрограммист или контейнер
Скоростьочень быстромедленнее
Размернесколько мегабайтпочти вся память компьютера
Что хранитьпеременные, небольшие массивыбольшие и долгоживущие данные

Статическая память

Третья область — для глобальных переменных и переменных со словом static. Они создаются до начала main и живут до конца программы. Большой массив, объявленный вне функций, на стек не попадает, поэтому в олимпиадных решениях массивы часто делают глобальными:

C++
int big[10000000];              // вне функций: статическая память, не стек

int main() {
    big[0] = 1;
}

Ошибки, которые отсюда происходят

Указатель на локальную переменную

C++
int* make_number() {
    int value = 42;
    return &value;              // адрес локальной переменной
}

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

TXT
warning: address of local variable 'value' returned [-Wreturn-local-addr]

У нас программа, которая попыталась прочитать число по этому указателю, упала. Возвращайте значение, а не адрес: int make_number() { return 42; }.

Утечка памяти

C++
for (int i = 0; i < 3; ++i) {
    int* scores = new int[1000];
    scores[0] = i;
    // delete[] scores; забыли
}

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

TXT
ERROR: LeakSanitizer: detected memory leaks
Direct leak of 12000 byte(s) in 3 object(s) allocated from:
    #1 main main.cpp:5
SUMMARY: AddressSanitizer: 12000 byte(s) leaked in 3 allocation(s).

В учебной программе утечка незаметна: когда программа завершается, операционная система забирает всю память. В программе, которая работает неделями, — сервере, игре — память постепенно заканчивается.

Обращение после освобождения

C++
int* value = new int(5);
delete value;
std::cout << *value << '\n';    // памяти уже нет

Собранная обычным способом, эта программа вывела у нас −1431368166: память уже отдана и переиспользована. С санитайзером она останавливается с объяснением:

TXT
ERROR: AddressSanitizer: heap-use-after-free
READ of size 4 at 0x502000000010 thread T0
    #0 main main.cpp:6

Санитайзеры включаются флагом -fsanitize=address; подробнее о них и о поиске таких ошибок — в статьях «Segmentation fault в C++» и «Мусор в переменных и неопределённое поведение в C++».

Как писать, чтобы этих ошибок не было

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

C++
#include <iostream>
#include <memory>
#include <string>
#include <vector>

struct Player {
    std::string name;
    int level;
};

int main() {
    std::vector<int> scores(1000);               // массив в куче, освободится сам
    auto player = std::make_unique<Player>();    // один объект в куче, освободится сам

    player->name = "Анна";
    player->level = 3;
    scores[0] = player->level;
    std::cout << player->name << ' ' << scores[0] << '\n';
    return 0;
}

std::vector и std::unique_ptr — это небольшие объекты на стеке, которые владеют памятью в куче. Когда такой объект выходит из области видимости, он сам освобождает то, чем владел. Получается лучшее от обоих миров: места — сколько угодно, а следит за ним компилятор.

Отсюда простые правила.

  • Нужен массив — берите std::vector. Особенно если он большой или его размер неизвестен заранее.
  • Нужна строка — берите std::string.
  • Нужен один объект в куче — std::make_unique.
  • Не пишите new и delete, пока не изучаете, как устроены сами контейнеры.
  • Не возвращайте адреса и ссылки на локальные переменные.
  • Рекурсию глубже нескольких десятков тысяч вызовов заменяйте циклом.

Коротко

  • Локальные переменные живут на стеке: быстро и автоматически, но места мало, а жизнь короткая.
  • Куча большая, но память в ней нужно освобождать.
  • Большой локальный массив и глубокая рекурсия переполняют стек.
  • Забытый delete — утечка, лишний — обращение к освобождённой памяти.
  • std::vector, std::string и std::unique_ptr хранят данные в куче и освобождают их сами.

Что такое оперативная память, адрес и ячейка на уровне железа, объясняет урок учебника «Регистры и оперативная память: ячейки и адреса», а что делает с программами операционная система — урок «Операционная система: программа, которая управляет программами».

Источники

  1. Storage duration — cppreference.com [Электронный ресурс]. — URL: https://en.cppreference.com/w/cpp/language/storage_duration (дата обращения: 10.10.2026).
  2. std::unique_ptr — cppreference.com [Электронный ресурс]. — URL: https://en.cppreference.com/w/cpp/memory/unique_ptr (дата обращения: 10.10.2026).
  3. Program Instrumentation Options — GCC Manual [Электронный ресурс]. — URL: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html (дата обращения: 10.10.2026).
C++Память