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

Динамическая память в 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 << "dtorn"; } };
    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++ на курсе «С Нуля до Гуру» с практикой и обратной связью.

    Источник

    НЕТ КОММЕНТАРИЕВ

    Оставить комментарий