Динамическая память (heap) выделяется во время выполнения программы, когда размер данных заранее неизвестен или слишком велик для стека. В C++ для этого используются операторы new и delete (а также их варианты для массивов).

Динамическая память в 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.

Что дальше изучать