Соблюдайте парность операторов 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 происходит два действи:

  • выделение памяти
  • вызов конструкторов (для нетривиальных типов)

Для тривиальных типов (например, intdoublestruct 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 выполняются обратные действия:

  • вызов деструкторов (для нетривиальных типов)
  • освобождение памяти

Опять же для массива нетривиальных типов считывается значение счетчика перед указателем и в цикле вызываются деструкторы.