Соблюдайте парность операторов new/delete
Всегда соблюдайте соответствие форм new и delete: одиночный объект освобождается через delete, массив — строго через delete[].
Использование неправильной формы ведёт к неопределённому поведению (чаще всего — утечкам или падению), потому что для массивов компилятор выделяет память иначе, сохраняя скрытую служебную информацию о количестве элементов.
Неправильно:
// Выделен массив, но вызывается одиночный delete
int* arr = new int[100];
delete arr; // Неопределённое поведение
Правильно:
// Форма освобождения строго соответствует форме выделения
int* obj = new int(42);
delete obj;
int* arr = new int[100];
delete[] arr;
Детали:
При вызове new происходит два действи:
- выделение памяти
- вызов конструкторов (для нетривиальных типов)
Для тривиальных типов (например, int, double, struct Point { float x,y; };) оператор new просто выделяет размер памяти, равный sizeof(T)*n, где n - это количество элементов массива или =1 для обычного указателя.
Для массива из элементов нетривиального типа размер запрашиваемой памяти будет равен sizeof(T)*n + sizeof(size_t). То есть перед массивом сохраняется счетчик элементов размером sizeof(size_t).
Проверка необходимости создания дополнительного места для счетчика может выглядеть так (зависит от реализации компилятора):
if (!std::is_trivially_destructible_v<T> && n > 1) {
len += sizeof(size_t);
_cockie = true;
}
// allocation
return _cockie ? p + sizeof(size_t) : p;
При вызове delete выполняются обратные действия:
- вызов деструкторов (для нетривиальных типов)
- освобождение памяти
Опять же для массива нетривиальных типов считывается значение счетчика перед указателем и в цикле вызываются деструкторы.