Содержание
Программа объявляет массив на десять миллионов чисел и падает, не выполнив ни одной строки. Функция возвращает указатель, а по нему лежит мусор. Программа работает сутки и занимает всё больше памяти. У всех трёх историй общий корень: переменные в 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); у Вас числа будут другими, но картина — той же:
глобальная: 0xaaaace490094
локальная: 0xffffdd3277b0
ещё локальная: 0xffffdd3277ac
в куче: 0xaaaafe41e2b0
сам вектор: 0xffffdd327790
элементы вектора: 0xffff98800010
sizeof(vector) = 24
Что здесь видно.
- Две локальные переменные лежат вплотную: адреса отличаются на 4 байта, размер одного
int. Это стек. - Глобальная переменная и число, созданное через
new, находятся совсем в другом месте. - Сам вектор — локальная переменная, он рядом с остальными и занимает всего 24 байта. А миллион его элементов лежит отдельно: в 24 байта они не поместились бы.
Стек
Стек — область памяти, которой программа пользуется как стопкой тарелок. Вызвали функцию — сверху положили «тарелку» с её параметрами и локальными переменными. Функция закончилась — тарелку сняли.
Отсюда свойства стека.
- Память выделяется и освобождается сама. Вы ничего для этого не пишете.
- Это очень быстро. Выделить место — значит сдвинуть одну границу.
- Переменная живёт до конца блока, в котором объявлена. Вышли за закрывающую скобку — её больше нет.
- Размер стека мал и задан заранее. В Linux обычно 8 мегабайт, в Windows — 1 мегабайт.
Последнее свойство — причина двух известных падений.
Большой локальный массив
int main() {
int big[10000000]; // 40 мегабайт на стеке
big[0] = 1;
// ...
}
Десять миллионов чисел по 4 байта — это 40 мегабайт. В стек размером 8 мегабайт они не помещаются, и программа завершается с ошибкой Segmentation fault, едва начав работу.
Слишком глубокая рекурсия
long long depth(long long n) {
return n == 0 ? 0 : 1 + depth(n - 1);
}
Вызов depth(10000) работает. Вызов depth(100000000) падает: каждый вложенный вызов кладёт на стек свою «тарелку», и на сто миллионов вызовов места нет. Это и есть переполнение стека — stack overflow.
Куча
Куча — большая область памяти, из которой программа берёт куски нужного размера по запросу. Размер кучи ограничен только памятью компьютера.
int* scores = new int[10000000]; // 40 мегабайт в куче — без проблем
scores[0] = 1;
delete[] scores; // вернуть память
Свойства кучи противоположны свойствам стека.
- Память выделяется по запросу — оператором
newили контейнером вродеstd::vector. - Это медленнее: нужно найти свободный кусок подходящего размера.
- Данные живут, пока их явно не освободят. Конец функции на них не влияет.
- Места много.
| Стек | Куча | |
|---|---|---|
| Кто выделяет память | компилятор, автоматически | программа, по запросу |
| Кто освобождает | компилятор, в конце блока | программист или контейнер |
| Скорость | очень быстро | медленнее |
| Размер | несколько мегабайт | почти вся память компьютера |
| Что хранить | переменные, небольшие массивы | большие и долгоживущие данные |
Статическая память
Третья область — для глобальных переменных и переменных со словом static. Они создаются до начала main и живут до конца программы. Большой массив, объявленный вне функций, на стек не попадает, поэтому в олимпиадных решениях массивы часто делают глобальными:
int big[10000000]; // вне функций: статическая память, не стек
int main() {
big[0] = 1;
}
Ошибки, которые отсюда происходят
Указатель на локальную переменную
int* make_number() {
int value = 42;
return &value; // адрес локальной переменной
}
Переменная value исчезает в момент выхода из функции, а её адрес возвращается наружу. Такой указатель называют висячим. Компилятор предупреждает:
warning: address of local variable 'value' returned [-Wreturn-local-addr]
У нас программа, которая попыталась прочитать число по этому указателю, упала. Возвращайте значение, а не адрес: int make_number() { return 42; }.
Утечка памяти
for (int i = 0; i < 3; ++i) {
int* scores = new int[1000];
scores[0] = i;
// delete[] scores; забыли
}
На каждом шаге цикла выделяется новый массив, а указатель на старый пропадает. Память занята, но добраться до неё и освободить уже нельзя. Санитайзер находит это сразу:
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).
В учебной программе утечка незаметна: когда программа завершается, операционная система забирает всю память. В программе, которая работает неделями, — сервере, игре — память постепенно заканчивается.
Обращение после освобождения
int* value = new int(5);
delete value;
std::cout << *value << '\n'; // памяти уже нет
Собранная обычным способом, эта программа вывела у нас −1431368166: память уже отдана и переиспользована. С санитайзером она останавливается с объяснением:
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++ этого почти никогда не нужно делать самому.
#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хранят данные в куче и освобождают их сами.
Что такое оперативная память, адрес и ячейка на уровне железа, объясняет урок учебника «Регистры и оперативная память: ячейки и адреса», а что делает с программами операционная система — урок «Операционная система: программа, которая управляет программами».
Источники
- Storage duration — cppreference.com [Электронный ресурс]. — URL: https://en.cppreference.com/w/cpp/language/storage_duration (дата обращения: 10.10.2026).
- std::unique_ptr — cppreference.com [Электронный ресурс]. — URL: https://en.cppreference.com/w/cpp/memory/unique_ptr (дата обращения: 10.10.2026).
- Program Instrumentation Options — GCC Manual [Электронный ресурс]. — URL: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html (дата обращения: 10.10.2026).



