Menu

Динамическая память в C++: new и delete простыми словами

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

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Зачем запрашивать память во время выполнения

До сих пор каждая создаваемая вами переменная жила в стеке: её размер известен на этапе компиляции, и она автоматически уничтожается при завершении своей области видимости. Это быстро и безопасно, но не подходит для случая, когда вы не знаете, сколько памяти понадобится, пока программа не запущена, — буфер, размер которого задаётся вводом пользователя, структура, которая должна пережить создавшую её функцию, или граф, форма которого заранее неизвестна.

Для таких случаев C++ позволяет выделять память из кучи (также называемой free store) с помощью new и возвращать её через delete. Выражение new резервирует блок, запускает конструктор и возвращает указатель на него, опираясь напрямую на указатели, которые вы видели на предыдущей странице.

Сама переменная p живёт в стеке — это просто указатель. А int, на который она указывает, живёт в куче и остаётся в живых, пока вы не вызовете delete, сколько бы областей видимости ни сменилось.

Стек против кучи

Это различие — сама причина существования new, поэтому стоит сделать его конкретным.

void demo() {
    int a = 10;            // в стеке - исчезает, когда demo() возвращает управление
    int* b = new int(10);  // 'b' в стеке, int, на который он указывает, в куче
}                          // 'a' уничтожен; int в куче УТЕКАЕТ - никогда не удаляется

Ключевые различия:

  • Стек — автоматическое время жизни, очень быстрый, ограниченный размер (обычно несколько МБ), освобождается за вас при выходе из области видимости.
  • Куча — ручное время жизни, чуть медленнее, большая, и освобождается только при вызове delete.

Это обмен гибкости на ответственность: память в куче живёт ровно столько, сколько вы захотите, но именно вы становитесь тем, кто обязан не забыть её освободить.

Выделение массивов с помощью new[]

Когда вам нужен блок, длина которого определяется во время выполнения, используйте массивную форму new T[n]. Она возвращает указатель на первый элемент, и вы освобождаете его соответствующим delete[].

Правило строгое, и его легко нарушить: память от new освобождается через delete, а память от new[] — через delete[]. Смешивать их — delete arr для того, что выделено через new[], — это неопределённое поведение, даже если на вашей машине это вроде бы работает.

Три классические ошибки

Ручное управление памятью имеет небольшой набор ошибок, на которые приходится большинство багов с кучей. Научитесь распознавать все три.

1. Утечка памяти — вы никогда не вызываете delete. Блок остаётся занятым навсегда. Безвредно один раз, фатально в цикле.

void leaky() {
    int* p = new int(5);
    // ... нет delete ...
}   // p исчез; int в куче теперь и недостижим, И не освобождён

2. Висячий указатель — вы используете память после её освобождения. Указатель всё ещё хранит старый адрес, но эта память больше не ваша.

3. Двойное освобождение — вы делаете delete для одного и того же блока дважды. Это портит внутренний учёт кучи и обычно приводит к падению.

int* p = new int(1);
delete p;
delete p;   // двойное освобождение - неопределённое поведение, часто падение

Присвоение указателю nullptr после удаления обезвреживает и висячее использование, и двойное освобождение: разыменование nullptr падает немедленно (легко отлаживать), а delete nullptr явно является безопасной операцией, которая ничего не делает.

Реалистичный цикл «выделить — использовать — освободить»

Собирая всё вместе, вот как выглядит корректное ручное управление: выделить, использовать, освободить ровно один раз и больше не трогать указатель.

Обратите внимание, что для типа класса delete u делает две вещи: сначала запускает деструктор объекта, а затем освобождает сырую память. Этот порядок важен, как только ваши объекты начинают владеть собственными ресурсами.

Тонкая ловушка: если между new и delete бросается исключение, delete никогда не выполняется, и вы получаете утечку. Оборачивать каждое выделение в try/catch, чтобы это обработать, утомительно и чревато ошибками — а это как раз та проблема, которую решает следующая страница.

Далее: Умные указатели

Теперь вы увидели полную цену управления памятью вручную: каждый new — это обещание позже вызвать delete, а единственное пропущенное, удвоенное или преждевременное освобождение — неопределённое поведение. Современный C++ почти никогда не даёт это обещание вручную. Следующая страница знакомит с умными указателямиstd::unique_ptr и std::shared_ptr — объектами, которые владеют выделенной в куче памятью и автоматически вызывают delete за вас, когда выходят из области видимости, превращая все три классические ошибки в то, с чем за вас справляются компилятор и RAII.

Часто задаваемые вопросы

В чём разница между new и delete в C++?

new выделяет память в куче во время выполнения и возвращает указатель на неё; delete освобождает память, выделенную с помощью new. Каждый new должен сопровождаться ровно одним delete, иначе вы получите утечку памяти. Для массивов используйте new[] вместе с delete[].

Что произойдёт, если забыть вызвать delete в C++?

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

Стоит ли использовать new и delete напрямую в современном C++?

Редко. Предпочитайте контейнеры вроде std::vector или умные указатели (std::unique_ptr, std::shared_ptr), которые освобождают память автоматически. Понимать сырые new/delete полезно, потому что умные указатели оборачивают их, но в повседневном коде это источник утечек и висячих указателей.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ