Динамическая память в C++: new и delete — практическое руководство с примерами
Что такое динамическая память и когда она нужна
Динамическая память (heap) выделяется во время выполнения программы, когда размер данных заранее неизвестен или слишком велик для стека. В C++ для этого используются операторы new и delete (а также их варианты для массивов).
Базовый синтаксис: new и delete
int* p = new int; // без инициализации (значение не определено)
int* q = new int(42); // прямая инициализация
int* r = new int(); // value-init: для int даёт 0
std::cout << *q << "\n"; // 42
delete p; // освобождаем память одного объекта
delete q;
delete r;
Важно: для встроенных типов new int оставляет значение неинициализированным; используйте new int(), если нужен ноль. Для пользовательских типов всегда вызывается конструктор.
Массивы: new[] и delete[]
int* a = new int[5]; // элементы неинициализированы
int* b = new int[5]{1,2,3,4,5}; // список инициализации
// ... работаем с a и b
delete[] a; // обязательно delete[] для массивов
delete[] b;
Нельзя смешивать new с delete[] и наоборот — это неопределённое поведение. Всегда освобождайте память тем же оператором, которым выделяли.
Конструкторы и деструкторы при динамическом выделении
#include <iostream>
struct Logger {
Logger(int id) : id(id) { std::cout << "ctor(" << id << ")\n"; }
~Logger() { std::cout << "dtor(" << id << ")\n"; }
int id;
};
int main() {
Logger* p = new Logger(1); // вызов конструктора
delete p; // вызов деструктора
Logger* arr = new Logger[3]{ {10}, {20}, {30} }; // для каждого элемента ctor
delete[] arr; // для каждого элемента dtor
}
При выделении массива объектов вызывается конструктор для каждого элемента, а при освобождении — соответствующие деструкторы.
Исключения и nothrow
Если памяти не хватает, new бросает std::bad_alloc. Это корректный и безопасный путь обработки ошибок выделения памяти.
#include <new>
#include <iostream>
try {
int* huge = new int[1'000'000'000];
delete[] huge;
} catch (const std::bad_alloc& e) {
std::cerr << "Не удалось выделить память: " << e.what() << "\n";
}
Вариант с nothrow не бросает исключение, а возвращает nullptr при ошибке. Его удобно использовать в местах, где нельзя бросать исключения:
#include <new>
int* data = new (std::nothrow) int[1000000000000ULL];
if (!data) {
// обработка ошибки: памяти нет
} else {
delete[] data;
}
Типичные ошибки и как их избежать
- Утечка памяти: забыли сделать delete или пути выхода сложные.
- Двойное удаление: вызвать delete дважды для одного указателя — UB.
- Висячий указатель (dangling pointer): продолжаете использовать указатель после delete.
- Несоответствие операторов: new с delete[], new[] с delete — UB.
- Смешивание new/delete и malloc/free: так делать нельзя.
int* p = new int(10);
int* alias = p; // второй указатель на тот же объект
delete p; // память освобождена
// *alias = 5; // ОПАСНО: alias висячий указатель (UB)
p = nullptr; // смягчает риск повторного delete, но alias всё ещё опасен
Хорошая привычка: после delete обнулять указатель, если он ещё будет виден. Но помните, что копии указателя в других местах останутся висячими — проектируйте владение аккуратно.
Не смешивайте new/delete и malloc/free
int* p = new int(5);
// free(p); // НЕЛЬЗЯ — повредите аллокатор/метаданные, UB
delete p; // корректно
int* q = (int*)std::malloc(sizeof(int));
// delete q; // НЕЛЬЗЯ — освобождать нужно free
std::free(q);
Причина: механизмы управления памятью инициализации/деинициализации у этих семейства функций разные. Для объектов C++ всегда используйте new/delete.
virtual деструктор и удаление через базовый указатель
Если удаляете объект наследника через указатель на базовый класс, деструктор базового класса должен быть виртуальным, иначе деструктор производного не вызовется (утечка/поломка логики).
struct Base { virtual ~Base() = default; };
struct Derived : Base { ~Derived() { /* освобождение ресурсов */ } };
Base* b = new Derived();
delete b; // вызовется ~Derived(), затем ~Base()
Placement new (коротко)
Placement new конструирует объект в уже выделенной области памяти. Удалять такую память оператором delete нельзя; нужно вручную вызвать деструктор и освободить буфер тем способом, которым он был получен.
#include <new>
#include <iostream>
struct S { ~S(){ std::cout << "dtor\n"; } };
alignas(S) unsigned char buf[sizeof(S)];
S* obj = new (buf) S(); // конструируем в buf
obj->~S(); // явно вызываем деструктор
// память buf освобождать не нужно (статический буфер здесь)
Используйте этот приём только при чётком понимании жизненного цикла и выравнивания памяти.
Лучшие практики
- По возможности используйте автоматические объекты (на стеке) и контейнеры стандартной библиотеки (
std::vector,std::string) вместо ручногоnew/delete. - Если нужен динамический объект с владением — в современном коде чаще выбирают умные указатели (
std::unique_ptr,std::shared_ptr) — это снижает риск утечек и упрощает код. - Для массивов предпочитайте
std::vector<T>— безопаснее и удобнее, чемnew T[]. - Обнуляйте указатель после
delete, избегайте дублирования владения одним и тем же сырым указателем. - Обрабатывайте ошибки выделения: исключения или
nothrow— в зависимости от политики проекта.
Мини‑практикум: динамический массив с инициализацией и проверкой ошибок
#include <iostream>
#include <new>
int* make_array(std::size_t n, int init) {
int* data = new (std::nothrow) int[n];
if (!data) return nullptr;
for (std::size_t i = 0; i < n; ++i) data[i] = init;
return data; // Владелец обязан вызвать delete[]
}
int main() {
std::size_t n = 5;
if (int* a = make_array(n, 7)) {
for (std::size_t i = 0; i < n; ++i) std::cout << a[i] << ' ';
std::cout << "\n";
delete[] a; // не забудьте! Иначе утечка
} else {
std::cerr << "Память не выделена\n";
}
}
Обратите внимание на договорённость о владении: кто получил указатель — тот и освобождает. В реальных проектах старайтесь инкапсулировать владение (RAII/умные указатели/контейнеры), чтобы не забывать про delete.
Чек‑лист по new/delete
- Выделил с
new— освободиdelete; выделил сnew[]— освободиdelete[]. - Не используй указатель после
delete; по возможности обнуляй его. - Не смешивай
new/deleteиmalloc/free. - При удалении через базовый указатель — виртуальный деструктор в базе.
- Обрабатывай ошибки выделения: исключения или
nothrow.
Что дальше изучать
Закрепить материал и перейти к более безопасным техникам управления ресурсами поможет качественный курс. Рекомендую посмотреть программу и попробовать практику из курса: Хочу быстро прокачать C++ на курсе «С Нуля до Гуру» с практикой и обратной связью.
-
Создано 27.05.2026 17:01:59
-
Михаил Русаков

Комментарии (0):
Для добавления комментариев надо войти в систему.
Если Вы ещё не зарегистрированы на сайте, то сначала зарегистрируйтесь.