Почему синтаксис delete [] существует в C++?

Каждый раз, когда кто-то задает здесь вопрос delete[], всегда есть довольно общий delete[] ответ «как это делает C++, использовать ». Исходя из ванильного фона C, я не понимаю, почему вообще нужен другой вызов.

С malloc() / free() ваши варианты - получить указатель на непрерывный блок памяти и освободить блок непрерывной памяти. Что-то в области реализации приходит и знает, какой размер выделенного вами блока был основан на базовом адресе, когда вам нужно его освободить.

Нет функции free_array() . Я видел несколько сумасшедших теорий по другим вопросам, косвенно связанным с этим, например, когда вызов delete ptr освобождает только верхнюю часть массива, а не весь массив. Или, вернее, не определяется реализацией. И конечно ... если бы это была первая версия C++, и вы сделали странный выбор дизайна, который имеет смысл. Но почему со $PRESENT_YEAR стандартом C++ он не перегружен ???

Мне кажется, что единственный дополнительный бит, который добавляет C++, проходит через массив и вызывает деструкторы, и я думаю, что, возможно, в этом его суть, и он буквально использует отдельную функцию, чтобы сэкономить нам один поиск длины времени выполнения или nullptr в конце списка в обмен на мучения каждого нового программиста на C++ или программиста, у которого был нечеткий день и который забыл, что есть другое зарезервированное слово.

Может кто-нибудь прояснить раз и навсегда, есть ли причина, кроме «так сказано в стандарте, и никто не ставит под сомнение»?

Ответов (6)

Решение

Объекты в C++ часто имеют деструкторы, которые необходимо запустить в конце своего жизненного цикла. delete[] обеспечивает вызов деструкторов каждого элемента массива. Но при этом накладные расходы не указаны , а delete их нет. Один для массивов, который оплачивает накладные расходы, и один для отдельных объектов, который этого не делает.

Чтобы иметь только одну версию, реализации потребуется механизм для отслеживания дополнительной информации о каждом указателе. Но один из основополагающих принципов C++ заключается в том, что пользователя не следует заставлять оплачивать затраты, которые ему совершенно не нужны.

Всегда delete то, что ты new и всегда delete[] то, что ты new[] . Но в современном C++ new и new[] вообще больше не используются. Используйте std::make_unique, std::make_shared, std::vectorили другие более выразительными и более безопасные альтернативы.

История крышки является то , что delete требуется из - за отношения С ++ с C .

new Оператор может сделать динамически выделяемый объект практически любого типа объекта.

Но из-за наследия C указатель на тип объекта неоднозначен между двумя абстракциями:

  • расположение одного объекта, и
  • являясь основой динамического массива.

Из этого просто следует delete противоположная delete[] ситуация.

Однако это не похоже на правду, потому что, несмотря на то, что вышеприведенные наблюдения верны, можно использовать один delete оператор. Из этого логически не следует, что требуются два оператора.

Вот неофициальное доказательство. new T Оператор вызова (один объект случай) может неявно вести себя так , как будто это было new T[1] . То есть каждый new всегда может выделить массив. Когда не упоминается синтаксис массива, может подразумеваться, что массив [1] будет выделен. Тогда просто должен был бы существовать сингл, delete который ведет себя как сегодня delete[] .

Почему не соблюдается этот дизайн?

Я думаю, что все сводится к обычному: это коза, принесенная в жертву богам эффективности. Когда вы выделяете массив с помощью new [], дополнительное хранилище выделяется для метаданных, чтобы отслеживать количество элементов, чтобы delete [] можно было узнать, сколько элементов необходимо повторить для уничтожения. Когда вы выделяете один объект с помощью new, такие метаданные не требуются. Объект может быть построен непосредственно в памяти, которая поступает из нижележащего распределителя без какого-либо дополнительного заголовка.

Это часть «не платите за то, что не используете» с точки зрения затрат времени выполнения. Если вы выделяете отдельные объекты, вам не нужно «платить» за любые накладные расходы на представление в этих объектах, чтобы иметь дело с возможностью того, что любой динамический объект, на который ссылается указатель, может быть массивом. Однако на вас возложена ответственность за кодирование этой информации так, как вы выделяете объект в массив new и впоследствии удаляете его.

Что-то не упомянутое в других (вполне хороших) ответах заключается в том, что основная причина этого заключается в том, что массивы, унаследованные от C, никогда не были "первоклассной" вещью в C++.

Они имеют примитивную семантику C и не имеют семантики C++, и поэтому компилятор C++ и поддержка времени выполнения позволяют вам или системам времени выполнения компилятора делать полезные вещи с указателями на них.

Фактически, они настолько не поддерживаются C++, что указатель на массив вещей выглядит так же, как указатель на одну вещь. В частности, этого не произошло бы, если бы массивы были собственными частями языка - даже как часть библиотеки, например, строки или вектора.

Эта бородавка на языке C++ произошла из-за наследия C. И он остается частью языка - даже несмотря на то, что теперь у нас есть std::array массивы фиксированной длины и (всегда были) std::vector массивы переменной длины - в основном для целей совместимости: Возможность обращаться с C++ к API операционной системы и к библиотекам, написанным на других языках, с использованием взаимодействия с языком C.

И ... потому что существует масса книг, веб-сайтов и классов, обучающих массивам очень рано в своей педагогике на C++, из-за а) возможности на раннем этапе писать полезные / интересные примеры, которые фактически вызывают API ОС, и, конечно же, из-за потрясающей силы b) «мы всегда так поступали».

Пример может помочь. Когда вы выделяете массив объектов в стиле C, эти объекты могут иметь собственный деструктор, который необходимо вызвать. delete Оператор не делает этого. Он работает с объектами-контейнерами, но не с массивами в стиле C. Вы delete[] для них нуждаетесь .

Вот пример:

#include <iostream>
#include <stdlib.h>
#include <string>

using std::cerr;
using std::cout;
using std::endl;

class silly_string : private std::string {
  public:
    silly_string(const char* const s) :
      std::string(s) {}
    ~silly_string() {
      cout.flush();
      cerr << "Deleting \"" << *this << "\"."
           << endl;
      // The destructor of the base class is now implicitly invoked.
    }

  friend std::ostream& operator<< ( std::ostream&, const silly_string& );
};

std::ostream& operator<< ( std::ostream& out, const silly_string& s )
{
  return out << static_cast<const std::string>(s);
}

int main()
{
  constexpr size_t nwords = 2;
  silly_string *const words = new silly_string[nwords]{
    "hello,",
    "world!" };

  cout << words[0] << ' '
       << words[1] << '\n';

  delete[] words;

  return EXIT_SUCCESS;
}

Эта тестовая программа явно инструментирует вызовы деструктора. Очевидно, это надуманный пример. Во-первых, программе не нужно освобождать память непосредственно перед завершением работы и освобождением всех своих ресурсов. Но он демонстрирует, что происходит и в каком порядке.

Некоторые компиляторы, такие как clang++, достаточно умны, чтобы предупредить вас, если вы не укажете [] in delete[] words;, но если вы все равно заставите его скомпилировать ошибочный код, вы получите повреждение кучи.

Обычно компиляторы C++ и связанные с ними среды выполнения строятся поверх среды выполнения C платформы. В частности, в этом случае диспетчер памяти C.

Диспетчер памяти C позволяет вам освобождать блок памяти, не зная его размера, но стандартного способа получить размер блока из среды выполнения не существует, и нет гарантии, что фактически выделенный блок имеет именно тот размер, который вы просил. Вполне может быть больше.

Таким образом, размер блока, хранящийся в диспетчере памяти C, нельзя использовать для обеспечения функциональности более высокого уровня. Если функциональным возможностям более высокого уровня требуется информация о размере распределения, они должны хранить ее сами. (И C++ delete[] это необходимо для типов с деструкторами, чтобы запускать их для каждого элемента.)

C++ также придерживается позиции «вы платите только за то, что используете», хранение поля дополнительной длины для каждого выделения (отдельно от бухгалтерии базового распределителя) не соответствовало бы этой позиции.

Поскольку обычным способом представления массива неизвестного (во время компиляции) размера в C и C++ является указатель на его первый элемент, компилятор не может отличить выделение одного объекта от выделения массива на основе типа. система. Так что возможность различать остается на усмотрение программиста.

В основном malloc и free выделяют память, new и delete создают и уничтожают объекты. Итак, вы должны знать, что это за объекты.

Чтобы подробнее рассказать о неуказанных накладных расходах, упомянутых в ответе Франсуа Андрие, вы можете увидеть мой ответ на этот вопрос, в котором я изучил, что делает конкретная реализация (Visual C++ 2013, 32-разрядная версия). Другие реализации могут делать то же самое, а могут и не делать.

В случае, если new[] он использовался с массивом объектов с нетривиальным деструктором, он выделял на 4 байта больше и возвращал указатель, сдвинутый на 4 байта вперед, поэтому, когда он delete[] хочет узнать, сколько там объектов, он берет указатель , сдвигает его на 4 байта раньше и берет число по этому адресу и обрабатывает его как количество сохраненных там объектов. Затем он вызывает деструктор для каждого объекта (размер объекта известен из типа переданного указателя). Затем, чтобы освободить точный адрес, он передает адрес, который был на 4 байта раньше переданного адреса.

В этой реализации передача массива, выделенного с new[] помощью обычного, delete приводит к вызову единственного деструктора первого элемента с последующей передачей неправильного адреса в функцию освобождения, что приводит к повреждению кучи. Не делай этого!