Есть два типа разработчиков.
Первый пишет код, нажимает Run — и всё работает.
Второй пишет код, нажимает Run — и получает:
Segmentation fault (core dumped)
После этого начинается расследование уровня криминального сериала: кто обращался к памяти, куда исчез указатель и почему программа умерла именно сейчас, хотя пять минут назад всё было нормально. 😅
Добро пожаловать в мир памяти C++.
Если Python, JavaScript или Java стараются спрятать большую часть работы с памятью, то C++ вручает тебе ключи от всего здания и говорит:
Вот память. Управляй. Только потом не говори, что тебя не предупреждали.
Именно управление памятью делает C++ одновременно мощным, быстрым и немного пугающим. Но когда понимаешь основные принципы, большая часть «магии» исчезает.

🧠 Что вообще такое память программы?
Когда программа запускается, операционная система выделяет ей пространство в оперативной памяти. Там хранятся переменные, объекты, массивы, аргументы функций и другие данные, необходимые для работы.
Условно память программы можно представить как большое здание с разными помещениями. Для новичка особенно важны две зоны:
стек — Stack;
куча — Heap.
Названия звучат так, будто речь идёт либо о книгах, либо о мусорной свалке. И, что забавно, это довольно точное описание.
📚 Стек: быстро, аккуратно и автоматически
Стек используется для хранения локальных переменных, аргументов функций и служебной информации о вызовах функций.
void printAge(){
int age=25;
std::cout<<age;
}Переменная age создаётся, когда начинается выполнение функции, и автоматически удаляется, когда функция заканчивается.
Тебе не нужно вручную освобождать память. Компилятор и сама структура стека делают это за тебя.
Стек работает по принципу LIFO — Last In, First Out: последним пришёл, первым ушёл.
Представь стопку тарелок. Чтобы достать нижнюю тарелку, сначала придётся убрать все верхние. С вызовами функций происходит примерно то же самое.
Почему стек быстрый?
Для выделения памяти в стеке системе обычно достаточно сдвинуть специальный указатель. Не нужно искать свободное место среди множества блоков памяти.
Поэтому стек:
работает очень быстро;
автоматически очищается;
предсказуемо управляет временем жизни объектов;
обычно безопаснее ручного выделения памяти.
Но стек не бесконечный. Если положить туда слишком много данных, можно получить переполнение.
int main(){
int hugeArray[100000000];
}Такой массив может привести к ошибке Stack Overflow.
И нет, это не тот сайт, на котором ты копируешь решение ошибки. Это сама ошибка. 😄
📦 Куча: много места, но порядок наводишь сам
Куча — это область динамической памяти. Она нужна, когда размер данных становится известен только во время выполнения программы или объект должен жить дольше текущей функции.
int* number=new int(42);Оператор new выделяет память в куче и возвращает её адрес. Этот адрес сохраняется в указателе number.
Но здесь начинается самое интересное: выделенная память не освобождается автоматически после выхода из функции.
Её нужно освободить вручную:
delete number;Забыл написать delete? Поздравляем, ты создал утечку памяти.
💧 Что такое утечка памяти?
Утечка памяти возникает, когда программа выделила участок памяти, но больше не может его освободить или просто забыла это сделать.
void createNumber(){
int* value=new int(100);
}Функция завершилась, локальный указатель value исчез, а выделенная память осталась занятой.
Программа уже не знает адрес этого участка и не может вызвать для него delete.
Представь гостиницу, в которой приложение постоянно бронирует новые номера, но никогда не оформляет выезд. Сначала всё работает нормально. Потом свободных номеров становится всё меньше. Затем гостиница превращается в сервер, который нужно срочно перезапустить.
В маленькой программе одна утечка может быть незаметна. В игре, браузере, сервере или приложении, которое работает сутками, постоянные утечки приводят к росту потребления памяти, тормозам и падениям.
👻 Висячий указатель
После вызова delete память освобождается, но сам указатель автоматически не исчезает.
int* number=new int(10);
delete number;Переменная number всё ещё содержит старый адрес. Но объект по этому адресу уже уничтожен.
Если попробовать обратиться к нему:
std::cout<<*number;программа попадает в область неопределённого поведения.
Она может:
вывести старое значение;
вывести случайное число;
упасть;
сломать другие данные;
продолжить работу, чтобы упасть позже в совершенно другом месте.
Такой указатель называется висячим указателем, или dangling pointer.
После ручного освобождения памяти указатель часто обнуляют:
delete number;
number=nullptr;Это не исправляет все возможные проблемы, но снижает риск случайного повторного обращения к освобождённому объекту.
💥 Двойное освобождение памяти
Иногда разработчик настолько старается не допустить утечку, что освобождает память дважды.
int* number=new int(42);
delete number;
delete number;Первый delete корректно освобождает память. Второй пытается повторно освободить участок, который уже не принадлежит объекту.
Это снова неопределённое поведение.
Компьютер в такой момент примерно:
Ты уже удалил это. Что именно я должен удалить сейчас?
Поэтому ручное управление памятью требует строгой дисциплины. Один забытый или лишний delete способен создать ошибку, которую придётся искать несколько часов.
🔗 Указатели: не данные, а адреса
Указатель — это переменная, которая хранит адрес другой переменной или объекта.
int value=50;
int* pointer=&value;Здесь:
valueхранит число50;&valueполучает адрес переменной;pointerхранит этот адрес;*pointerполучает значение по адресу.
std::cout<<*pointer;Программа выведет 50.
Указатели нужны для динамической памяти, эффективной передачи объектов, создания структур данных и взаимодействия с низкоуровневыми API.
Но указатель сам по себе не знает:
существует ли объект по адресу;
сколько памяти там выделено;
можно ли её освобождать;
кто отвечает за время жизни объекта.
Указатель просто хранит адрес. Вся ответственность остаётся у разработчика.
🏗️ Когда нужна динамическая память?
Куча используется не потому, что разработчикам нравится писать delete. Она решает реальные задачи.
Динамическая память нужна, когда:
размер массива неизвестен заранее;
объект должен существовать после завершения функции;
создаются деревья, графы и связанные списки;
объекты создаются в зависимости от действий пользователя;
данные слишком большие для стека;
нужно управлять временем жизни объекта независимо от области видимости.
Например, количество игровых противников определяется уже во время игры. Создать заранее переменную под каждого возможного врага невозможно. Поэтому объекты создаются динамически.
🚀 Почему современный C++ не любит ручные new и delete?
Ручное управление памятью даёт полный контроль, но одновременно создаёт слишком много возможностей для ошибки.
Поэтому современный C++ использует принцип RAII — Resource Acquisition Is Initialization.
Смысл простой: ресурс должен быть привязан к объекту. Когда объект уничтожается, ресурс автоматически освобождается.
Вместо обычного указателя:
User* user=new User();
delete user;можно использовать умный указатель:
auto user=std::make_unique<User>();Когда user выйдет из области видимости, память освободится автоматически.
Не нужно вручную писать delete. Меньше кода, меньше ошибок, меньше ночных сообщений в рабочем чате с текстом «прод снова упал».
🧠 std::unique_ptr
std::unique_ptr означает, что объектом владеет только один указатель.
#include <memory>
auto player=std::make_unique<Player>();Такой указатель нельзя просто скопировать:
auto secondPlayer=player;Этот код не скомпилируется, потому что у объекта должен быть только один владелец.
Но владение можно передать:
auto secondPlayer=std::move(player);После этого объектом владеет secondPlayer, а исходный указатель становится пустым.
std::unique_ptr — хороший выбор по умолчанию, когда объекту нужен один понятный владелец.
🤝 std::shared_ptr
std::shared_ptr используется, когда одним объектом должны совместно владеть несколько частей программы.
auto user=std::make_shared<User>();
auto anotherReference=user;Умный указатель считает количество владельцев. Пока существует хотя бы один shared_ptr, объект остаётся в памяти.
Когда последний владелец исчезает, память освобождается автоматически.
Звучит идеально, но у shared_ptr есть цена: дополнительный счётчик ссылок и риск циклических зависимостей.
Например, объект A хранит shared_ptr на B, а B — shared_ptr на A. Оба объекта считают, что другой всё ещё ими владеет, поэтому никогда не уничтожаются.
Для решения этой проблемы существует std::weak_ptr.
🧹 Контейнеры тоже управляют памятью
Во многих случаях тебе вообще не нужно напрямую работать с динамической памятью.
Вместо динамического массива:
int* numbers=new int[size];
delete[] numbers;лучше использовать:
std::vector<int> numbers(size);std::vector сам выделяет память, расширяется и освобождает ресурсы.
Современный C++ предлагает готовые инструменты:
std::vector— динамический массив;std::string— строка;std::array— массив фиксированного размера;std::unique_ptr— уникальное владение;std::shared_ptr— совместное владение;std::optional— значение, которое может отсутствовать.
Чем чаще используешь стандартные контейнеры и RAII, тем реже приходится вручную вспоминать, где поставить delete.
🔍 Почему ошибки памяти так сложно искать?
Главная проблема ошибок памяти — они не всегда проявляются в момент появления.
Ты можешь случайно записать данные за пределами массива:
int numbers[3]={1,2,3};
numbers[10]=42;Программа может не упасть сразу. Она просто перезапишет чужой участок памяти.
Падение произойдёт позже — например, при выводе текста, создании объекта или закрытии программы.
Разработчик смотрит на строку, где произошёл сбой, и думает:
Почему программа падает на обычном std::cout?
А настоящая ошибка находится на 300 строк выше.
Для поиска таких проблем используют:
AddressSanitizer;
Valgrind;
отладчик;
статические анализаторы;
предупреждения компилятора.
Например, AddressSanitizer помогает обнаруживать выход за границы массива, обращение к освобождённой памяти и двойное освобождение.
⚙️ Базовые правила работы с памятью в C++
Старайся хранить объекты на стеке. Если динамическая память не нужна, не создавай её.
Используй стандартные контейнеры.
std::vectorпочти всегда лучше ручного динамического массива.Предпочитай std::unique_ptr. Он явно показывает владельца объекта.
Не используй new и delete без необходимости. Современный C++ позволяет обходиться без них в большинстве обычных задач.
Не обращайся к объекту после его уничтожения.
Не освобождай одну память дважды.
Включай предупреждения компилятора. Иногда компилятор замечает проблему раньше тебя.
Проверяй код санитайзерами. Особенно если программа ведёт себя случайным образом.
💙 Как изучать C++ без бесконечной теории?
Память невозможно нормально понять, просто прочитав определения стека, кучи и указателей.
Нужно самостоятельно создать динамический объект, забыть освободить его, получить ошибку, разобраться и переписать код правильно. Только после этого тема начинает действительно укладываться в голове.
В приложении Кодик обучение программированию построено вокруг практики. Ты изучаешь C++, пишешь код прямо во время уроков, решаешь упражнения и постепенно закрепляешь сложные темы: переменные, функции, массивы, указатели, классы и управление памятью.
Не просто читаешь, как работает std::vector, а используешь его. Не просто запоминаешь определение указателя, а работаешь с адресами и видишь результат выполнения программы.
А в Telegram-сообществе Кодика регулярно выходят полезные посты, небольшие разборы, шпаргалки, мемы и практические материалы для разработчиков. Это удобный способ повторять программирование, даже когда нет времени проходить полноценный урок.
