Динамическая память: новые, удаленные и их опасности
Узнайте, как динамическое распределение работает с новым и удалять, понять опасности утечек памяти, двойной-бесплатно, и использовать-после-бесплатно, и узнать, почему современный C++ должен содержать ноль сырой новый / удалить.
Динамическое распределение с новым и удалением
stack быстрый, но ограниченный по размеру и объему. Когда вам нужны объекты, которые переживают свой объем создания, или размер которых неизвестен во время компиляции, вы выделяете на heap (бесплатный магазин).
new T(args) выделяет память для одного T, вызывает его конструктор и возвращает T*. delete p вызывает деструктор и освобождает память. Для массивов: new T[n] и delete[] p. Несоответствие new с delete[] или new[] с delete является неопределенным поведением — компилятор не поймает это.
Новые, удаленные и их массивные вариации
Вот API распределения сырья. Изучите его так, чтобы вы узнали его в унаследованном коде, но никогда не пишите это в новом коде - вместо этого используйте умные указатели.
#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 не должен использоваться на объектах размещения.
#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 — они выполняют распределение и конструкцию как единую, безопасную операцию.
Custom Allocators Обзор
new/delete по умолчанию использует глобальный распределитель (обычно malloc/free), который является универсальным, но не оптимальным для всех моделей. Таможенные распределители предоставляют:
Арена/линейные распределители — нажимают указатель, освобождают все сразу. Отлично подходит для игровых данных.
- Бассейн распределителей - предварительно распределить блоки фиксированного размера. O(1) alloc/free, без фрагментации.
Стек аллокаторы — распределение ЛИФО, чрезвычайно быстрое.
Контейнеры C++ принимают параметры шаблона распределителя: std::vector. Пространство имен std::pmr (ресурсы полиморфной памяти) в C++17 обеспечивает структуру полиморфного распределителя времени выполнения, которая позволяет избежать проблемы параметров шаблона.
В современных C++ (C++14 и более поздних) хорошо написанный код приложения должен содержать ноль сырых new и delete:
- Однопользовательская собственность: std::make_unique
Совместное владение: использование std::make_shared
Динамические массивы: std::vector
- Струны: используйте std::string
- Факультативные объекты: используйте std::optional
Сырые new/delete могут появляться в библиотечном коде (распорядители, пользовательские контейнеры), но прикладной код никогда не должен в них нуждаться. Если вы видите new в обзоре кода, спросите почему — почти всегда есть более безопасная альтернатива.
- несоответствие
new/delete[]илиnew[]/deleteНеопределенное поведение - Утечки памяти, двойные и неиспользованные — три смертельных ошибки памяти
f(new A(), new B())может просачиваться при броске конструктора (до C++17 даже с обертками share ptr)- Размещение новых конструкций по заданному адресу — уничтожайте вручную, никогда не используйте
delete - Современный код приложения C++ должен иметь нулевой исходный
new/delete— используйте умные указатели и контейнеры
Тест — проверьте свои знания
(15 XP)1. Что произойдет, если использовать `delete` в памяти, выделенной `new[]`?
2. Почему `f(new A(), new B())` может утечка памяти?
3. Как уничтожить объект, созданный с размещением нового?