Какой оператор необходимо вызвать для удаления массива
Перейти к содержимому

Какой оператор необходимо вызвать для удаления массива

  • автор:

ArrayRemove

Удаляет из массива указанное число элементов начиная с указанного индекса.

[in] Индекс, начиная с которого удаляются элементы массива.

[in] Количество удаляемых элементов. Значение WHOLE_ARRAY означает удаление всех элементов с указанного индекса до конца массива.

Возвращает true в случае успеха, иначе false. Чтобы получить информацию об ошибке, необходимо вызвать функцию GetLastError(). Возможные ошибки:

  • 5052 – ERR_SMALL_ARRAY (значение start слишкое большое),
  • 5056 – ERR_SERIES_ARRAY (массив не может быть изменен, индикаторный буфер),
  • 4003 – ERR_INVALID_PARAMETER (значение count слишком большое),
  • 4005 — ERR_STRUCT_WITHOBJECTS_ORCLASS (массив фиксированного размера, который содержит сложные объекты с деструктором),
  • 4006 — ERR_INVALID_ARRAY (массив фиксированного размера, который содержит объекты структур или классов с деструктором).

Если функция используется для массива фиксированного размера, то сам размер массива не меняется: при этом происходит физическое копирование оставшегося «хвоста» в позицию start . Для точного понимания работы функции смотрите пример ниже. «Физическое» копирование означает, что копируемые объекты не создаются с помощью вызова конструктора или оператора копирования, а просто происходит копирование бинарного представления объекта. Именно по этой причине запрещается применять функцию ArrayRemove() к массиву фиксированного размера, содержащего объекты с деструктором (взводится ошибка ERR_INVALID_ARRAY или ERR_STRUCT_WITHOBJECTS_ORCLASS). Так как при удалении такого объекта деструктор должен быть вызван дважды – для первоначального объекта и его копии.

Нельзя удалять элементы из динамических массивов, назначенных в качестве индикаторных буферов функцией SetIndexBuffer(), это приведет к появлению ошибки ERR_SERIES_ARRAY. Для индикаторных буферов все операции по изменению размера производит исполняющая подсистема терминала.

//+——————————————————————+
//| Script program start function |
//+——————————————————————+
void OnStart ()
<
//— объявим массив фиксированного размера и заполним значениями
int array[10];
for ( int i=0;i <10;i++)
<
array[i]=i;
>
//— покажем массив до удаления элементов
Print ( «До вызова ArrayRemove()» );
ArrayPrint (array);
//— удалим 2 элемента из массива и покажем новый состав
ArrayRemove (array,4,2);
Print ( «После вызова ArrayRemove()» );
ArrayPrint (array);
/*
Результат выполнения:
До вызова ArrayRemove()
0 1 2 3 4 5 6 7 8 9
После вызова ArrayRemove()
0 1 2 3 6 7 8 9 8 9
*/

Оператор delete: удаление массива

Если вы выделяли память при помощи new[] , вы обязаны освободить её с помощью delete[] . Так гласит стандарт.

Освобождение такой памяти как-то по-другому ( delete без [] или вообще free ) является Undefined Behaviour. Если в программе есть Undefined Behaviour, она имеет право вести себя как угодно: может ничего плохого не делать, может вылететь в любой точке, отформатировать ваш винчестер или выбросить из окна вашего кота.

You have been warned.

Отслеживать
ответ дан 12 июн 2013 в 16:48
206k 28 28 золотых знаков 291 291 серебряный знак 526 526 бронзовых знаков

я понимаю что это по стандарту, но вот смотрите, тут после выполнения первого варианта и после второго. joxi.ru/uploads/prod/20130612/4a2/824/…

12 июн 2013 в 16:51
ну теперь понятнее, спасибо 🙂
12 июн 2013 в 16:54
Почитайте про undefined behaviour, забавная и опасная штука.
12 июн 2013 в 16:55
читаю, благодарю)
12 июн 2013 в 17:07
Палец вверх за кота 🙂
12 июн 2013 в 20:23

Если Вы выделили массив элементов, то этот массив и нужно удалить. New делает malloc на sizeof(объект) и вызывает конструктор объекта, new[] делает malloc на sizeof(объект) * кол-во_объектов, вызывает конструкторы для каждого будущего объекта и записывает информацию о том, память для скольких объектов была выделена. Куда и как эта информация записывается вопрос отдельный (плюс бывает ситуации когда это информация не нужна).

Вызывая delete Вы говорите компилятору «удали этот один элемент по такому-то адресу». Оператор delete[] же читает сколько объектов расположено в выделенной памяти (как мы помним, оператор new[] сохранил это число в процессе своей работы), вызывает для каждого их них деструктор, а после вызывает free() , «отдавая память назад ОС». Именно поэтому для памяти, выделенной через new/new[] нужно вызывать delete/delete[] соответственно. Контроль за тем, что для оператора выделения должен быть вызван соответствующий оператор освобождения лежит на программисте.

Отслеживать
ответ дан 13 июн 2013 в 9:02
3,456 12 12 серебряных знаков 9 9 бронзовых знаков

Оператор new вовсе не обязан по стандарту использовать malloc (он может, например, запросить прямо у системы без посредничества malloc ).

Перегрузка в C++. Часть III. Перегрузка операторов new/delete

Продолжаем серию «C++, копаем вглубь». Цель этой серии — рассказать максимально подробно о разных особенностях языка, возможно довольно специальных. Эта статья посвящена перегрузке операторов new/delete . Это третья статья из серии, первая, посвященная перегрузке функций и шаблонов, находится здесь, вторая, посвященная перегрузке операторов, находится здесь. Статья завершает цикл из трех статей, посвященный перегрузке в C++.

Оглавление

Оглавление

1. Стандартные формы операторов new/delete

C++ поддерживает несколько вариантов операторов new/delete . Их можно разделить на основные стандартные, дополнительные стандартные и пользовательские. В этом разделе и разделе 2 рассматриваются стандартные формы, пользовательские формы будут рассмотрены в разделе 3.

1.1. Основные стандартные формы

Основные стандартные формы операторов new/delete , используемые при создании и удалении объекта и массива типа T следующие:

new T(/* аргументы конструктора */) new T[/* длина массива */] delete ptr; delete[] ptr;

Их работу можно описать следующим образом. При вызове оператора new сначала выделяется память для объекта. Если выделение прошло успешно, то вызывается конструктор. Если конструктор выбрасывает исключение, то выделенная память освобождается. При вызове оператора delete все происходит в обратном порядке: сначала вызывается деструктор, потом освобождается память. Деструктор не должен выбрасывать исключений.

Когда оператор new[] используется для создания массива объектов, то сначала выделяется память для всего массива. Если выделение прошло успешно, то вызывается конструктор по умолчанию (или другой конструктор, если есть инициализатор) для каждого элемента массива начиная с нулевого. Если какой-нибудь конструктор выбрасывает исключение, то для всех созданных элементов массива вызывается деструктор в порядке, обратном вызову конструктора, затем выделенная память освобождается. Для удаления массива надо вызвать оператор delete[] , при этом для всех элементов массива вызывается деструктор в порядке, обратном вызову конструктора, затем выделенная память освобождается.

Внимание! Необходимо вызывать правильную форму оператора delete в зависимости от того, удаляется одиночный объект или массив. Это правило надо соблюдать неукоснительно, иначе можно получить неопределенное поведение, то есть может случиться все, что угодно: утечки памяти, аварийное завершение и т.д. Подробнее см. [Meyers1].

В приведенном выше описании необходимо сделать одно уточнение. Для так называемых тривиальных типов (встроенные типы, структуры в стиле С), конструктор по умолчанию может не вызываться, а деструктор в любом случае ничего не делает.

Стандартные функции выделения памяти при невозможности удовлетворить запрос выбрасывают исключение типа std::bad_alloc . Но это исключение можно перехватить, для этого надо установить глобальный перехватчик с помощью вызова функции set_new_handler() , подробнее см. [Meyers1].

Любую форму оператора delete безопасно применять к нулевому указателю.

При создании массива оператором new[] размер может быть установлен нулевым.

Обе формы оператора new допускают использование инициализаторов в фигурных скобках.

new int new int[8]

1.2. Дополнительные стандартные формы

При подключении заголовочного файла становятся доступными еще 4 стандартные формы оператора new :

new(ptr) T(/* аргументы конструктора*/); new(ptr) T[/* длина массива */]; new(std::nothrow) T(/* аргументы конструктора */); new(std::nothrow) T[/* длина массива */];

Первые две из них называются размещающим оператором new (non-allocating placement new ). Аргумент ptr — это указатель на область памяти, размер которой достаточен для размещения экземпляра или массива. Также область памяти должна иметь соответствующее выравнивание. Этот вариант оператора new не выделяет памяти, он обеспечивает только вызов конструктора. Таким образом данный вариант позволяет разделить фазы выделения памяти и инициализации объектов. Эта возможность активно используется в стандартных контейнерах. Оператор delete для объектов, созданных таким способом, вызывать, конечно, нельзя. Для удаление объекта надо прямо вызвать деструктор, а затем освободить память способом, зависящим от способа выделения памяти.

Вторые два варианта называются не выбрасывающим исключений оператором new (nothrow new ) и отличаются тем, что при невозможности удовлетворить запрос возвращают nullptr , а не выбрасывают исключение типа std::bad_alloc . Удаление объекта происходит с помощью основного оператора delete . Эти варианты считаются устаревшими и не рекомендованы для использования.

1.3. Функции выделения и освобождения памяти

Стандартные формы операторов new/delete используют следующие функции выделения и освобождения памяти (allocation and deallocation functions):

void* operator new(std::size_t size); void operator delete(void* ptr); void* operator new[](std::size_t size); void operator delete[](void* ptr); void* operator new(std::size_t size, void* ptr); void* operator new[](std::size_t size, void* ptr); void* operator new(std::size_t size, const std::nothrow_t& nth); void* operator new[](std::size_t size, const std::nothrow_t& nth);

Эти функции определены в глобальном пространстве имен. Функции выделения памяти для размещающих операторов new ничего не делают и просто возвращают ptr .

C++17 поддержал дополнительные формы функций выделения и освобождения памяти, с указанием выравнивания. Вот некоторые из них:

void* operator new (std::size_t size, std::align_val_t al); void* operator new[](std::size_t size, std::align_val_t al);

Эти формы непосредственно пользователю недоступны, их использует компилятор для объектов у которых требования по выравниванию превосходят __STDCPP_DEFAULT_NEW_ALIGNMENT__ , поэтому главная проблема состоит в том, чтобы пользователь случайно их не скрыл (см. раздел 2.2.1). Напомним, что в C++11 появилась возможность явно задавать выравнивание пользовательских типов.

struct alignas(32) X < /* . */ >;

2. Перегрузка стандартных форм операторов new/delete

Перегрузка стандартных форм операторов new/delete заключается в определении пользовательских функций выделения и освобождения памяти, сигнатуры которых совпадают со стандартными. Эти функции можно определить в глобальном пространстве имен или в классе, но не в пространстве имен, отличном от глобального. Функцию выделения памяти для стандартного размещающего оператора new нельзя определить в глобальном пространстве имен. После такого определения соответствующие операторы new/delete будут использовать их, а не стандартные.

2.1. Перегрузка в глобальном пространстве имен

Пусть, например, в некотором модуле в глобальном пространстве имен определены пользовательские функции:

void* operator new(std::size_t size) < // . >void operator delete(void* ptr) < // . >

В этом случае произойдет фактически подмена (replacement) стандартных функций выделения и освобождения памяти для всех вызовов операторов new/delete для любых классов (в том числе и стандартных) во всем модуле. Это может привести к полному хаосу. Отметим, что описанный механизм подмены — это особый механизм, реализованный только для этого случая, а не какой-то общий механизм C++. В этом случае при реализации пользовательских функций выделения и освобождения памяти становится невозможным вызов соответствующих стандартных функций, они полностью скрыты (оператор :: не помогает) и при попытке их вызвать возникает рекурсивный вызов пользовательской функции.

Определенная в глобальном пространстве имен функция

void* operator new(std::size_t size, const std::nothrow_t& nth) < // . >

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

Такая же ситуация с функциями для массивов.

Перегрузка операторов new/delete в глобальном пространстве имен настоятельно не рекомендуется.

2.2. Перегрузка в классе

Перегрузка операторов new/delete в классе лишена описанных выше недостатков. Перегрузка действует только при создании и удалении экземпляров соответствующего класса независимо от контекста вызова операторов new/delete . При реализации пользовательских функций выделения и освобождения памяти с помощью оператора :: можно получить доступ к соответствующим стандартным функциям. Рассмотрим пример.

class X < // . public: void* operator new(std::size_t size) < std::cout void operator delete(void* ptr) < std::cout void* operator new[](std::size_t size) < std::cout void operator delete[](void* ptr) < std::cout >;

В этом примере к стандартным операциям просто добавляется трассировка. Теперь в выражениях new X() и new X[N] будут использоваться эти функции для выделения и освобождения памяти.

Эти функции формально являются статическими, и их можно объявлять как static . Но по существу они являются экземплярными, с вызова функции operator new() начинается создание экземпляра, а вызов функции operator delete() завершает его удаление. Эти функции никогда не вызываются для других задач. Более того, как будет показано ниже, функция operator delete() по существу является виртуальной. Так что правильнее объявлять их без static .

2.2.1. Доступ к стандартным формам операторов new/delete

Операторы new/delete можно использовать с дополнительным оператором разрешения области видимости, например ::new(p) X() . В этом случае функция operator new() , определенная в классе, будет игнорироваться, а будет использована соответствующая стандартная. Таким же способом можно использовать и оператор delete .

2.2.2. Сокрытие других форм операторов new/delete

Если теперь для класса X мы попробуем использовать размещающий или не выбрасывающий исключений new , то получим ошибку. Дело в том что, функция operator new(std::size_t size) будет скрывать (hide) другие формы operator new() . Проблему можно решить двумя способами. В первом надо добавить соответствующие варианты в класс (эти варианты должны просто делегировать операцию стандартной функции). Во втором надо использовать оператор new с оператором разрешения области видимости, например ::new(p) X() .

2.2.3. Стандартные контейнеры

Если мы попробуем разместить экземпляры X в каком-нибудь стандартном контейнера, например std::vector , то увидим, что наши функции для выделения и освобождения памяти не используются. Дело в том, что все стандартные контейнеры имеют собственный механизм выделения и освобождения памяти (специальный класс-аллокатор, являющийся шаблонным параметром контейнера), а для инициализации элементов используют размещающий оператор new .

2.2.4. Наследование

Функции для выделения и освобождения памяти наследуются. Если эти функции определены в базовом классе, а в производном нет, то для производного класса также будет перегружены операторы new/delete , и будут использованы функции для выделения и освобождения памяти, определенные в базовом классе.

Рассмотрим теперь полиморфную иерархию классов, где каждый класс перегружает операторы new/delete . Пусть теперь экземпляр производного класса удаляется с помощью оператора delete через указатель на базовый класс. Если деструктор базового класса виртуальный, то стандарт гарантирует вызов деструктора этого производного класса. В этом случае также гарантируется вызов функции operator delete() , определенной для этого производного класса. Таким образом функция operator delete() фактически является виртуальной.

2.2.5. Альтернативная форма функции operator delete()

В классе (особенно, когда используется наследование) иногда удобно применить альтернативную форму функции освобождения памяти:

void operator delete(void* p, std::size_t size); void operator delete[](void* p, std::size_t size);

Параметр size задает размер элемента (даже в варианте для массива). Такая форма позволяет использовать разные функции для выделения и освобождения памяти в зависимости от конкретного производного класса.

3. Пользовательские операторы new/delete

C++ может поддержать пользовательские формы оператора new следующего вида:

new(/* аргументы */) T(/* аргументы конструктора */) new(/* аргументы */) T[/* длина массива */]

Для того, чтобы эти формы были поддержаны, необходимо определить соответствующие функции выделения и освобождения памяти:

void* operator new(std::size_t size, /* доп. параметры */); void* operator new[](std::size_t size, /* доп. параметры */); void operator delete(void* p, /* доп. параметры */); void operator delete[](void* p, /* доп. параметры */);

Список дополнительных параметров функций выделения памяти должен быть не пуст и не состоять из одного void* или const std::nothrow_t& , то есть их сигнатура не должна совпадать с одной из стандартных. Списки дополнительных параметров в operator new() и operator delete() должны совпадать. Аргументы, передаваемые в оператор new , должны соответствовать дополнительным параметрам функций выделения памяти. Пользовательская функция operator delete() также может быть в форме с дополнительным параметром размера.

Эти функции можно определить в глобальном пространстве имен или в классе, но не в пространстве имен, отличном от глобального. Если они определены в глобальном пространстве имен, то они не подменяют, а перегружают стандартные функции выделения и освобождения памяти, поэтому их использование предсказуемо и безопасно, а стандартные функции всегда доступны. Если они определены в классе, то скрывают стандартные формы, но доступ к стандартным формам можно получить с помощью оператора :: , это подробно описано в разделе 2.2.

Пользовательские формы оператора new называют пользовательским размещающим оператором new (user-defined placement new ). Их не надо путать со стандартным (non-allocating) размещающим оператором new , описанным в разделе 1.2.

Соответствующей формы оператора delete не существует. Удалять объект, созданный с помощью пользовательского оператора new , можно двумя способами. Если пользовательская функция operator new() делегирует операцию выделения памяти стандартные функции выделения памяти, то можно применять стандартный оператор delete . Если нет, то придется явно вызвать деструктор, а потом пользовательскую функцию operator delete() . Компилятор вызывает пользовательскую функцию operator delete() только в одном случае: когда в процессе работы пользовательского оператора new конструктор выбрасывает исключение.

Вот пример (в глобальной области видимости).

void* operator new(std::size_t size, int a, const char* b) < std::cout void operator delete(void* p, int a, const char* b) < std::cout class X ; X* p = new(42, "meow") X(); // вывод: new 42 + meow delete p; // вызов стандартной ::operator delete()

4. Определение функций выделения памяти

В приведенных примерах пользовательские функции operator new() и operator delete() делегировали операцию соответствующей стандартной функции. Иногда и такой вариант полезен, но главная цель перегрузки new/delete является создание нового механизма выделения/освобождения памяти. Задача это не простая, и прежде, чем браться за нее, надо тщательно все продумать. Скотт Мейерс [Meyers1] обсуждает возможные мотивы для принятия подобного решения (конечно, главные из них — это эффективность). Также он обсуждает основные технические проблемы связанные с правильной реализацией пользовательских функций выделения и освобождения памяти (использование функции set_new_handler() , многопоточная синхронизация, выравнивание). В [Guntheroth] приведен пример реализации относительно простых пользовательских функций выделения и освобождения памяти. Прежде, чем создавать свой вариант, следует поискать готовые решения, в качестве примера можно привести библиотеку Pool из проекта Boost.

5. Классы-аллокаторы стандартных контейнеров

Как уже упоминалось выше, стандартные контейнеры используют специальные классы-аллокаторы для задач выделения и освобождения памяти. Эти классы являются шаблонными параметрами контейнеров и пользователь может определить свою версию такого класса. Мотивы для такого решения примерно те же, что и для перегрузки операторов new/delete . В [Guntheroth] описано, как создавать подобные классы.

Список литературы

[Guntheroth]
Гантерот, Курт. Оптимизация программ на C++. Проверенные методы для повышения производительности.: Пер. с англ. — СПб.: ООО «Альфа-книга», 2017.
[Meyers1]
Мэйерс, Скотт. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2014.

  • C++
  • перегрузка операторов new/delete

Алёна C++

Смысл существования delete[] мне всегда был не до конца ясен. Нет, понятно, что он нужен для освобождения массивов памяти, выделенных посредством new[], но почему бы все удаление не интегрировать в простой delete?

А так программисту приходится всегда помнить, что именно вызывать для освобождения памяти: delete или delete[]. Зачем — не очень понятно. Информация о том, сколько именно элементов в массиве все равно где-то да хранится. Почему бы компилятору вообще не освободить программиста от каких-либо запоминаний?

Все, что мне удалось раскопать по этому поводу — старое обсуждение в comp.lang.c++
Why does delete[] exist?

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

Но экономия получается уж очень несущественная. Ну разве что для мобильных устройств она может оказаться значимой. И многие с большим удовольствием отказались бы от этой экономии, ради того чтобы не ловить таинственные глюки и утечки памяти долгими часами из-за каких-то пропущенных скобочек. Простого delete’а вполне достаточно. Более того, по словам участников вышеупомянутой дискуссии, именно так работали ранние Борландовские компиляторы.

52 комментария:

все гораздо страшнее, когда один программер выделяет память malloc а другой освобождает ее с помощью delete :)) Ответить Удалить

я уже давным давно в коде не использую ни delete, ни delete[] 🙂 Ответить Удалить

Я начинал где-то во времена borland c++ 3.1. И там точно был delete [].

А можно подробнее про скорость? На что в приниципе влияют эти скобочки (это ведь только этап компиляции)? Ответить Удалить

Покажите пальцем — кто сейчас пишут руками delete или delete[]? Давно изобретены умные указатели. Соответственно, new/delete или new[]/delete[] встречается только внутри классов типа SomeAllocator размером в несколько [десятков] строк. На таком коротком участке кода забыть что использовать — со скобочками или без — очень сложно. Ответить Удалить

2black zorro:
Я начинал где-то во времена borland c++ 3.1. И там точно был delete [].

delete[] обязательно должен быть, иначе компилятор бы не соответствовал Стандарту. Интересно как там отрабатывал delete на массивах.

А можно подробнее про скорость?

Если обойтись одним delete’ом, то придется делать проверку что удаляем — массив или не массив, это будет занимать какое-то время.

На что в приниципе влияют эти скобочки (это ведь только этап компиляции)?

Про это я уже как-то писала тут. Ответить Удалить

2Raider:
Покажите пальцем — кто сейчас пишут руками delete или delete[]?

Я периодически встречаю такой код. Это же не обязательно прямо сейчас написанный код должен быть. Есть много старого кода, оставшегося с лохматых времен. Есть всякие менеджеры работы с памятью, которые стараются поближе к железу держаться и управлять всем самостоятельно.

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

На таком коротком участке кода забыть что использовать — со скобочками или без — очень сложно.

Человек очень ненадежен в этом смысле. Один раз не забудет, второй не забудет, на десятый раз забудет. Ответить Удалить

Информация о том, сколько именно элементов в массиве все равно где-то да хранится.
А где? Я бы реализуя оператор new для массивов помещал бы количество элементов перед первым элементом массива. В этом случае реализация delete должна знать был выделен массив или элемент чтобы правильно удалить. И экономия в 4 байта при выделении памяти для одного объекта вполне существенна.
ЗЫ. Вообще то не вижу проблемы, так как в режиме отладки эту ошибку легко отловить. Вывод — пишите тесты! Ответить Удалить

Возможно, стоит предложить обдумать тему комиссии разработки нового стандарта С++ 09? Ответить Удалить

Заранее прошу из-за изгороди камнями не кидаццо )

Во-первых, перед с++ было поставлено требование поддерживать совместимость с с (си)(sic!). Вопрос — при чем здесь delete[]? при том, что указатели тоже должны отвечать требованиям совместимости. а это sizeof(ptrdiff_t), и точка. Так что при настоящем положении вещей никаких тегов перед указателем ввести не удастся. Более того, с учетом допустимости конструкции

int m[];
int * p = m; // or int * p = &m[0];

в рантайме невозможно в принципе узнать, какой тег следует приписать указателю — массив или нет. Эта проблема проистекает из того факта, что в с++ массивы не есть сущности «первого рода», этапа выполнения, а скорее этакая вещь в себе, представленная метаинформацией, понятной компилятору (но не разработчику, даже если он понимает, что делает :))

Вот поэтому придется пока мириться. Или пересматривать стандарт, что, к сожалению, без breaking changes не обойдется. Ответить Удалить

Меня больше волновал другой вопрос. При использовании delete[] очевидно, что компилятор использует некий механизм, который знает сколько элементов нужно удалить, Следовательно так или иначе в рантайме всегда есть информация о том, сколько элементов содержится в массиве по данному указателю. Почему бы не ввести в язык возможность узнавать эту длину? Вместо такого удобства приходится постоянно передавать массивы вместе с указанием кол-ва элементов в них. Ответить Удалить

delete[] обязательно должен быть, иначе компилятор бы не соответствовал Стандарту. Интересно как там отрабатывал delete на массивах.
delete[] там был, но компилятор не соответствовал стандарту так как в те времена еще стандарта не было (1992 год). namespace, std, string — нету. Там много чего вообще нет. Так что нечему тут удивляться 😉 Ответить Удалить

опять же, из за невозможности это сделать в рантайме.

Предположим, функция вернула некоторый указатель. причем возвращает она указатель на массив, или на один элемент — пусть это рантайм-поведение.

Как узнать, на что указывает — только проверить; проверка же нетривиальна и требует затрат (как и все связанное с управлением памятью). Поэтому все остается на совести программиста. В угоду производительности. Ответить Удалить

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

2Prokrust:
Информация о том, сколько именно элементов в массиве все равно где-то да хранится.

В C++ FAQ Lite есть описание двух алгоритмов: over-allocation и associative array

2Евгений Железников:
Возможно, стоит предложить обдумать тему комиссии разработки нового стандарта С++ 09?

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

2sse:
Заранее прошу из-за изгороди камнями не кидаццо )

Ни в коем случае.

Во-первых, перед с++ было поставлено требование поддерживать совместимость с с (си)(sic!). Вопрос — при чем здесь delete[]?

Есть работа с памятью в стиле С. Это у нас malloc и free. Вот и совместимость с С.
Есть в стиле С++. Это new и delete. И смешивать их нельзя.

в рантайме невозможно в принципе узнать, какой тег следует приписать указателю — массив или нет

Я чуть выше дала ссылки на реализации динамических массивов в С++. К любому их них можно добавить информацию массив/не массив. Информация о числе элементов уже хранится, можно хранить и что-то еще, значит. Ответить Удалить

2ilya:
При использовании delete[] очевидно, что компилятор использует некий механизм, который знает сколько элементов нужно удалить, Следовательно так или иначе в рантайме всегда есть информация о том, сколько элементов содержится в массиве по данному указателю. Почему бы не ввести в язык возможность узнавать эту длину? Вместо такого удобства приходится постоянно передавать массивы вместе с указанием кол-ва элементов в них.

Теоретически получается, что да, это возможно. Но меня этот момент волнует меньше, потому что это чисто сишная работа с массивами получается. Можно использовать std::vector или что там больше для данной задачи подходит и с размером не мучаться.

2sse:
опять же, из за невозможности это сделать в рантайме.

Ээээ, ну почему же, компилятор-то как раз знает размер массива, причем именно в рантайме.

Поэтому все остается на совести программиста. В угоду производительности.

Ну вот скорее по этой причине. Ответить Удалить

2Анонимный:
насколько я помню отличие delete[] от delete в вызове пачки деструкторов вместо одного, а для случая стандартных типов данных, где нет деструкторов, никакой разницы между delete и delete[] нету — никаких утечек небудет.

Нет, различие не только в этом. Массив в начале может содержать некоторую дополнительную информацию, и если его начать удалять обычным delete’ом, то получится удаление не пойми какого адреса. По Стандарту — udefined behavior.
Подробнее здесь: Can I drop the [] when deleteing array of some built-in type (char, int, etc)? Ответить Удалить

2Yuriy Volkov:
delete[] там был, но компилятор не соответствовал стандарту так как в те времена еще стандарта не было (1992 год). namespace, std, string — нету. Там много чего вообще нет. Так что нечему тут удивляться 😉

Ну как же так, должны же были быть хотя бы какие-нибудь соглашения, назидания Страуструпа.
Ну хоть что-нибудь.
🙂 Ответить Удалить

В C++ FAQ Lite есть описание двух алгоритмов: over-allocation и associative array
В первом случае delete для массива и для одного объекта должен быть разным по определению, если конечно не считать один объект массивом из одного элемента (+4 байта при этом).
Во втором случае можно сделать delete одинаковыми, но зато этот способ намного медленнее.
ЗЫ. Страуструп делая С++ руководствовался скоростью выполнения. Все что могло замедлить выполнение было отброшено. И конечно язык строился так чтобы компактнее хранить данные. Ответить Удалить

А я просто запускаю valgrind и он говорит где я что забыл. Ответить Удалить

Но экономия получается уж очень несущественная. Ну разве что для мобильных устройств она может оказаться значимой.

И это пишет человек, не понаслышке знающий о программировании игр =)

Я знаю людей, которые в статьях 2006-го года (т.е., не 86-го) пишут следующее:

[. ]As this sums up to 12 bytes in total, all nodes are aligned on four-byte boundaries. This allows one to use the lower two bits of the children-pointer to indicate the axis (00: x, 01: y, 10: z) or a leaf (case 11).

И, в общем-то, абсолютно правы. Ответить Удалить

Анонимный пишет.
А я просто запускаю valgrind и он говорит где я что забыл.

Это типа, лучше, чем использовать идиомы языка, которые все делают автоматически и никогда ничего «не забывают» =)? А вот у меня пара проектов с валгриндом часов пять будет работать. =)

У нас на работе вообще никто про смартпойнтеры не слышал. И обычные массивы были в почете до недавнего времени.

2 Zorgg
Я знаю людей, которые в статьях 2006-го года (т.е., не 86-го) пишут следующее:
[. ]As this sums up to 12 bytes in total, all nodes are aligned on four-byte boundaries. This allows one to use the lower two bits of the children-pointer to indicate the axis (00: x, 01: y, 10: z) or a leaf (case 11).

>А вот у меня пара проектов с валгриндом
>часов пять будет работать. =)

а зачем целый проект, обычно delete[] парный new не покидает пределов модуля,
достаточно автоматизировать запуск unit тестов под valgrind.

Хотя должен признаться, что проблема именно delete[] меня давно не беспокоила,
хотя занимаюсь встроенными решениями. Ответить Удалить

2Алёна:
Ну как же так, должны же были быть хотя бы какие-нибудь соглашения, назидания Страуструпа.
Ну хоть что-нибудь.
🙂

да, в 1986 году вышло первое издание книги Страуструпа, в 1991 — второе. Еще был CFront (компилятор С++, который написал Страуструп). Стандартной библиотеки шаблонов для С++ тогда еще попросту не существовало. Хотя cin/cout компилятором поддерживались, но они изначально не были частью STL для C++. Чуть подробнее про историю STL можно почитать здесь.
В силу этих обстоятельств каждый производитель компилятора мог по своему толковать книги и рекомендации. В частности то, как работает delete. Я рад, что этот период давно пройден и глубоко сожалею, что в некоторый университетах до сих пор для обучения используют BC++ 3.1 Ответить Удалить

2Алёна
>>Ээээ, ну почему же, компилятор-то
>> как раз знает размер массива,
>> причем именно в рантайме.

В run-time он не может знать, потому что его удел — это время compile-time. Он только организует код в compile-time так, что может потом в run-time выяснить, что это за объект, обратившись к менеджеру памяти. А лишний код — дополнительное место, и, самое главное, _время_. Ответить Удалить

В первом случае delete для массива и для одного объекта должен быть разным по определению, если конечно не считать один объект массивом из одного элемента (+4 байта при этом).

Угу, ну да, плюс сколько-то там байт будет.

2zorgg:
И это пишет человек, не понаслышке знающий о программировании игр =)

Я знаю людей, которые в статьях 2006-го года (т.е., не 86-го) пишут следующее:

И, в общем-то, абсолютно правы.

Сурово.
Все, конечно, от задачи зависит. Если, к примеру, у тебя память течет по мегабайту в час, то сэкономленные несколько байт вряд ли тебя обрадуют.

2sse:
В run-time он не может знать, потому что его удел — это время compile-time. Он только организует код в compile-time так, что может потом в run-time выяснить, что это за объект, обратившись к менеджеру памяти.

А лишний код — дополнительное место, и, самое главное, _время_.

Перегрузка delete[] может быть полезной для отслеживания выделения\освобождения памяти. В этом случае нужно иметь пару для new[]. Ответить Удалить

Не поленилась скачать Borland C++ 3.1, чтобы проверить, не удалит ли там delete массив. Не удалил.

Честное удаление массива из трех элементов через delete[]

Нечестное удаление массива из трех элементов через delete

CBase::CBase
CBase::CBase
CBase::CBase
CBase::~CBase
Null pointer assignment Ответить Удалить

Не убедительно, Алёна. Вам уже рассказали, что информацию, массив ли это или один объект, нужно где-то хранить. И платить за это! А Строустрап говорил: если не используете — не плАтите. Ответить Удалить

2pesec:
Не убедительно, Алёна. Вам уже рассказали, что информацию, массив ли это или один объект, нужно где-то хранить.

Так она уже есть.

И платить за это! А Строустрап говорил: если не используете — не плАтите.

Если нужна какая-нибудь информация, кроме той, которая есть — можно сделать какой-нибудь волшебный ключик. Как с RTTI. Ответить Удалить

Алёна, я не вдовался в подробности, но скорее всего причина использования delete[], в том что некоторые реализаций непонимают разницы между указателем на переменную и именем массива. Ответить Удалить

Есть стандарт там всё сказано.

char *a = new char;
char *b = new [10];

delete[] a; поведение не определено
delete b; поведение не определено

соотвественно как сказано выше если реализовать не так как в стандарте то будет не соответствие стандарту 🙂 кстати это же не один неопределенный случай. а про внутренности тут уже всё от автора компилятора зависит. так же как в стандарте не описана реализация внутренностей класса так и компиляторы делают её по своему например g++ и msvc++ ^) Ответить Удалить

Мне кажется, все эти непонятки с указателями происходят от того, что плюсы, вслед за С смешивают 2 понятия : указатель на структурированную область памяти, которая может быть размешена в куче, и итератор по массиву.
Соответственно, если эти понятия разнести — сделать для них разные типы, то проблемы сразу кончатся. 🙂
Компилятор будет знать, что удалять по итераторам нельзя — и не даст.
Ну а при удалении по указателю, тип его будет статически известен, и компилятор сможет сгенерить нужный код без каких-нибудь затрат во время выполнения.

Останутся проблемы несовместимости с С, где понятия смешаны.
С другой стороны, можно ввести новую типизацию параллельно старой — как сделано с приведениями, например.

P.S. Я на вскидку не смог придумать алгоритм, где бы эти типы нельзя бы было тривиально разделить. 🙂 Ответить Удалить

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

2Анонимный:
Хм. ведь известен размер выделенной области памяти и размер удаляемого типа данных, соответсвенно можно определить кол-во элементов в массиве. Получается, что в delete[] нет необходимости. Или я не прав?

А так программисту приходится всегда помнить, что именно вызывать для освобождения памяти: delete или delete[]. Зачем — не очень понятно. Информация о том, сколько именно элементов в массиве все равно где-то да хранится. Почему бы компилятору вообще не освободить программиста от каких-либо запоминаний?

Очень странный вопрос для опытного программиста на С++ 🙂 Его задают на собеседовании, что бы отсеять новичков.

Что бы понять разницу предлагаю прогнать пару раз такую программу:

int main(int, char**)
Test *a = new Test[2];

// пробуем два варианта
// delete a;
delete[] a;

Хм.. мне тоже странно. Почему бы компилятору не привести delete к delete[]? Ведь по сути там только надо добавить количество элементов в структуру, создаваемую new. А если это нельзя, то почему бы не выдавать предупреждение если используется delete вместо delete[]? Ответить Удалить

Программист я не достаточно опытный, конечно. Но всё же работая уже над своим вторым движком и менеджером памяти к нему, до сих пор ловлю себя на мысли с таким «дурацким» вопросом.

2Алёна: Спасибо за ведение такого замечательного блога — не первый раз обнаруживаю обсуждение интересных тем.=) Ответить Удалить

Случайно наткнулся на эту ветку. И очень удивился, что delete [] мешает кому-то жить. Скажу так, что во-первых delete [] — вносит ясность в код, что удаляем именно массив, а не что-то еще. Т.е. создал что-то c [] незабудь, это что-то [] освободить. Когда код будет читать кто-то еще запись без скобок логически может быть неясна!. Второе, вообще не понял зачем городить какие-то границы массива. Не забываем, что массив это указатель на область памяти и всё. Для всяческих приятных извратов есть vector, list и т.д. Т.е. я хочу сказать, что реализация массива в С++ это самый низкий уровень, и программисту предоставляется ВЫБОР: или оптимизация и колбасня с указателями или простота вектора, списка и т.п.

Ух,
Я за простой и понятный код!

P.S.
А есть еще тип char* . который окaнчивается /0 — и чтобы выбрать элементы нам достаточно знать только это, а не количество символов. (это к слову о границах массива)

А также можно написать:

char *a=new char [20];
strcpy (a,»Hello World»);
a[5]=’_’;

//Цена разницы синтаксиса
delete a; //Освободили только первый элемент — утечка памяти
delete [] a; //Освободили все

//Отношение к памяти должно быть бережное несмотря на ее количество.

//Тот кто считает, что освобождать память не нужно — не должен писать программы на С и С++. Для этого есть Basic, Java, и .т.п. Ответить Удалить

Т.е. создал что-то c [] незабудь,
это что-то [] освободить.

Люди будут забывать. Можно этому удивляться, можно над этим потешаться, но если у нас есть что-то с пометкой «не забыть», то это — потенциальный источник неприятностей. Причем наступать на эти грабли будут и новички, и опытные программисты. И выясняться это будет в самый неподходящий момент. Ответить Удалить

По-моему в одной из публикаций Страуструпа, было написано, что оператор освобождения памяти для массивов должен был выглядеть так:

delete[n] ptr; // ptr указывает на массив из n элементов

В этом случае компилятору не нужно было сохранять информацию о кол-ве элементов. А уже потом его решили сократить до:

Для блока памяти известен его размер.
При удалении массива компилятор знает тип его элемента.
Поэтому количество элементов он очень просто вычислит.
Стало быть, delete[n] — излишен.

А вот различить на что указывает указатель — на массив или на один элемент компилятор не может — синтаксически ситуации не различимы.
Поэтому без деления на delete и delete[] не получается. Ответить Удалить

Наконец, вы меня успокоили, что можно писать delete везде без сомнительных скобок. Почитал в Интернете, что по этому поводу пишут. Как я выяснил вся разница в (возможно) производительности. Также есть такие несущественные детали как вызов конструкторов для отдельных объектов и (НЕПОНЯТНО. ) кому нужная перегрузка операторов new/new[] и delete/delete[].

Я предпочитаю никогда не писать код типа new Object [100]; Я всегда использую массивы указателей m = new *Object[100]. И потом for (. ) m[i] = new Object; Указатели не имеют деструкторов, а удалять объекты гораздо понятнее через for (. ) delete m[i]; delete m; где Object *m[100]; например.

delete[] очень сложно всегда помнить. Что касается векторов, то у меня в проектах они реализованы в наших библиотеках, а не в std, т.к. на 20 ОС, которые нужно поддерживать явно нельзя рассчитывать на отсутствие багов в той или иной библиотеке std. Ответить Удалить

Наконец, вы меня успокоили, что можно писать delete везде без сомнительных скобок

Нет, такого я не писала. delete[] нужно использовать при работе с массивами. Ответить Удалить

Я не конкретно про вас Алена. Я про Интернет. Он меня успокоил, в т.ч. и этот блог. Ответить Удалить

Наконец, вы меня успокоили, что можно писать delete везде без сомнительных скобок
Нужно не «успокаиваться интернетом» а разбираться с непонятными вопросами.
Тогда вместо глупости типа «delete везде без сомнительных скобок» просто будешь правильно расставлять скобки. 🙂 Ответить Удалить

Ну то, что нельзя массив удалять delet’ом, как бы выяснили. Возникает другой вопрос: а можно ли всегда и во всем использовать delete []? Т.е. чего ждать если какой нибудь

undefind behaviour ждать Ответить Удалить

а можно ли всегда и во всем использовать delete []?
При использовании delete[] для «всего» возникают 2 неоднозначности:
1. delete[] не работает с динамическими типами — всегда только статмические. Т. е. при попытке удалить экземпляр порождённого класса через указатель на базовый, вызовется деструктор базового, т. к. С++ не предоставляет полиморфных массивов.
2. Количество разрушаемых элементов нужно как-то узнавать. Но если использовать один и тот же оператор и для единичных объектов, и для массивов, то придётся делать одинаковый механизм для тех и других.
Пусть, например, мы решили сохранять количество элементов перед массивом (по отрицательному смещению) — тогда придётся так же поступать и для единичного объекта. Т. е. перед каждым объектом сохранять количество — 1. 🙂
Или другой вариант — можно узнать количество, если поделить нацело размер занимаемого блока на размер элемента. Но что делать если это экземпляр порожденного класса, который занимает совсем другой размер чем базовый?

В общем причина непоняток в том, что объект и массив объектов — это таки разные типы, но в языках С/С++ указатель на объект и указатель на массив неявно приводятся, и работая с динамическими массивами, мы вынуждены оперировать указателем на первый элемент.
Т. е. компилятор просто не можето отличить идёт ли работа с динамическим массивам или с отдельным элементом. А вынесение этого в рантайм неизбежно приведёт к пенальти по размеру или скорости при работе с динамической памятью. Ответить Удалить

ВНИМАНИЕ. delete [] вызывает деструкторы для каждого элемента массива, а delete вызывает лишь один деструктор, то есть:
T[5] a;
delete a;
Конструктор вызван 5 раз, а деструктор 1. Мне это сказал препод по проге, так что инфа 100% Ответить Удалить

На самом деле дела обстоят так.
delete и delete[] — это совсем разные вещи. delete вместе с new — это выделение памяти, грубо говоря — это контролируемый и перегружаемый аналог malloc/free / HeapAlloc/HeapFree и любой другой кучи (просто примеры, конечно).
А delete [] делает совсем другое. Он берёт кол-во элементов массива, хранящееся в предыдущих (!) 4 байтах (в общем случае, размер size_t) перед экземпляром первого класса и крутит цикл, в котором вызывает DTOR-ы объектов столько раз, сколько там указано. Ну а new[], собственно, перед вызовом конструкторов кладет туда это кол-во. И размер блока кучи тут совершенно ни при чем. Собственно, всё это можно проверить, скомпилировав такой код:

std::string a1;
std::string* a2 = &a1;
delete [] a2;

и посмотрев его в дизасме (std::string тут взят для примера, разглядеть всю эту логику намного проще с минимальной генерацией ‘поддерживающего’ кода — т.е. с отключенными /defaultlib и т.д.) Ответить Удалить

Кстати, delete[] и delete различаются только для типов, имеющих деструктор. К примеру, для встроеных типов никаких sizeof(size_t) байт перед первым экземпляром в блоке выделяться под хранение кол-во объектов не будет.
Это всё только наблюдения, аподиктически ничего не утверждается, к примеру, я чисто с потолка взял sizeof(size_t) как самое очевидное для описание размера в 4 байта на моей виндовой платформе. Мейби, в стандарте где-то и указаны такие детали поведения delete и delete[]. Ответить Удалить

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *