Перейти к содержимому

Динамическая память: новые, удаленные и их опасности

Узнайте, как динамическое распределение работает с новым и удалять, понять опасности утечек памяти, двойной-бесплатно, и использовать-после-бесплатно, и узнать, почему современный C++ должен содержать ноль сырой новый / удалить.

Динамическое распределение с новым и удалением

stack быстрый, но ограниченный по размеру и объему. Когда вам нужны объекты, которые переживают свой объем создания, или размер которых неизвестен во время компиляции, вы выделяете на heap (бесплатный магазин).

new T(args) выделяет память для одного T, вызывает его конструктор и возвращает T*. delete p вызывает деструктор и освобождает память. Для массивов: new T[n] и delete[] p. Несоответствие new с delete[] или new[] с delete является неопределенным поведением — компилятор не поймает это.

Новые, удаленные и их массивные вариации

Вот API распределения сырья. Изучите его так, чтобы вы узнали его в унаследованном коде, но никогда не пишите это в новом коде - вместо этого используйте умные указатели.

new_delete.cpp
#include <iostream>
#include <cstring>

struct Widget {
    int id;
    Widget(int i) : id(i) { std::cout << "Widget " << id << " created\n"; }
    ~Widget() { std::cout << "Widget " << id << " destroyed\n"; }
};

int main() {
    // Single object
    Widget* w = new Widget(1);
    std::cout << "Using widget " << w->id << '\n';
    delete w;  // destructor called, memory freed

    // Array
    Widget* arr = new Widget[3]{{10}, {20}, {30}};
    for (int i = 0; i < 3; ++i)
        std::cout << arr[i].id << ' ';
    std::cout << '\n';
    delete[] arr;  // MUST use delete[] for new[]

    // UNDEFINED BEHAVIOR — don't do these:
    // delete arr;      // wrong: used delete instead of delete[]
    // delete[] w;      // wrong: used delete[] instead of delete
}

Размещение новых

Новое размещение конструирует объект по определенному адресу памяти без выделения. Он используется распределителями, пулами памяти и контейнерами, такими как std::vector. Вы должны называть деструктор явно — delete не должен использоваться на объектах размещения.

placement_new.cpp
#include <iostream>
#include <new>       // required for placement new
#include <cstddef>   // std::byte
#include <memory>    // std::align

struct Sensor {
    int id;
    double reading;
    Sensor(int i, double r) : id(i), reading(r) {
        std::cout << "Sensor " << id << " init\n";
    }
    ~Sensor() { std::cout << "Sensor " << id << " shutdown\n"; }
};

int main() {
    // Pre-allocate raw storage (aligned)
    alignas(Sensor) std::byte buffer[sizeof(Sensor)];

    // Construct in pre-allocated memory
    Sensor* s = new (buffer) Sensor(42, 3.14);
    std::cout << "Reading: " << s->reading << '\n';

    // Must destroy explicitly — do NOT use delete
    s->~Sensor();  // explicit destructor call
    // delete s;    // WRONG — memory was not allocated by new
}

Три смертельных жука памяти

Raw new/delete подвергает вас трем категориям ошибок:

1. Memory Leak — выделение new, но не вызов delete. Память никогда не возвращается в ОС (до выхода процесса). В долговременном сервере это в конечном итоге потребляет всю доступную оперативную память.

2. Double Free — вызов delete по одному и тому же указателю дважды. Второй delete портит внутренние структуры данных распределителя кучи, часто приводя к сбоям гораздо позже в несвязанном коде. Это чрезвычайно трудно отладить.

3. Use-After-Free — доступ к памяти после delete. Память может быть повторно использована для другого распределения, поэтому вы можете читать или записывать данные другого объекта. Это уязвимость безопасности (баг класса CVE).

Подводный камень

Взять хотя бы f(new A(), new B()). Компилятор может оценивать аргументы в любом порядке и может их переплетать:

1.Выделить память на A
2. Выделить память на B
3. Конструкция А
4. Конструкция Б

Если на шаге 4 броски, A просачивается — никто не вызывает delete на нем. Даже f(std::shared_ptr(new A()), std::shared_ptr(new B())) имел эту проблему до C++17 (что гарантирует, что каждый аргумент будет полностью оценен до следующего). Всегда используйте std::make_unique или std::make_shared — они выполняют распределение и конструкцию как единую, безопасную операцию.