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