Как работает delete c
Перейти к содержимому

Как работает delete c

  • автор:

Как устроены операторы delete и delete[]?

Сможет ли оператор delete корректно удалить объект через бестиповый указатель? То есть, использует ли оператор delete информацию, доступную во время компиляции?

Отслеживать
задан 17 апр 2020 в 13:05
4,118 1 1 золотой знак 7 7 серебряных знаков 22 22 бронзовых знака

1 ответ 1

Сортировка: Сброс на вариант по умолчанию

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

Если в delete приходит указатель «неверного типа», то будет вызван и «неверный деструктор». В некоторых случаях это может закончится плачевно. У void деструктор фактически отсутствует, значит вызывать нечего и соответственно ничего не вызовется. Если деструктор выполняет нетривиальную работу — будет не очень хорошо.

delete[] получив указатель, «магическим способом» получает размер массива (обычно, он хранится рядом с указателем, но это личное дело компилятора, поэтому я и написал «магическим способом»). А дальше просто в цикле вызываем delete . Ничего необычного. Поэтому, если попутать delete и delete[] могут быть разные непонятные баги ( delete[] будет искать размер массива там, где его нет, а потом пойдет в разнос).

Некоторые компиляторы могут добавлять немножко больше кода для проверки себя и удалять корректно. К примеру, так может сделать visual studio в дебажном режиме. Но я бы на это не закладывался.

Секреты оператора delete

Когда выполняется delete 0 , произойдет ли удаление 0 из системы выполнения? Очевидно, нет, истинная цель оператора — удалить ссылку на свойство объекта.

delete object.property
delete object['property']

Как правило, успешное удаление возвращает true , а неудачное — false , но есть некоторые исключения.

Собственные свойства

Оператор delete работает только с собственным свойством объекта. Если в цепочке прототипов окажется свойство с таким же именем, это свойство будет пропущено.

Object.prototype.name = 1;const object = name: 2,
>;
// Действует только в отношении собственных свойств.
console.log(delete object.name); // true
console.log(object.name); // 1
// Но мы можем напрямую манипулировать объектом-прототипом.
console.log(delete Object.getPrototypeOf(object).name); // true
console.log(object.name); // undefined

Несуществующее свойство

Если удаляемое свойство не существует, delete не будет иметь никакого эффекта, но все равно вернет true .

const object = <>;delete object.name; // true

Неконфигурируемое свойство

Неконфигурируемые свойства не могут быть удалены.

const object = <>;Reflect.defineProperty(object, 'name', configurable: false,
>);
// Встроенные свойства объекта
console.log(delete Math.PI); // false
// Неконфигурируемые свойства
console.log(delete object.name); // false

В нестрогом режиме удаление неконфигурируемого свойства из самого себя вернет false , но в строгом режиме оно вызовет TypeError .

'use strict';const object = <>;Reflect.defineProperty(object, 'name', configurable: false,
>);
// ❌ TypeError: Cannot delete property 'PI' of #
console.log(delete Math.PI);
// ❌ TypeError: Cannot delete property 'name' of #
console.log(delete object.name);

Свойства, объявленные с помощью var, let, const

В глобальной области видимости нельзя удалить ни свойства, объявленные с помощью var , ни функции, объявленные с помощью определения функций (нефункциональных выражений). Это связано с тем, что оба объявленных свойства закреплены за окном и не являются настраиваемыми, поэтому при удалении будет соблюдена логика предыдущего пункта.

Кроме того, свойства, объявленные с помощью var , let , const и этих функций, не могут быть удалены ни в глобальной области видимости, ни в области видимости функции.

 
let object = name: 1,
>;
function getName(obj) return obj.name;
>
console.log(delete object); // false
console.log(delete getName); // false
>

В нестрогом режиме возвращается false , но в строгом режиме, вместо TypeError , выбрасывается SyntaxError .

'use strict'; 
let object = name: 1,
>;
function getName(obj) return obj.name;
>
// ❌ SyntaxError: Delete of an unqualified identifier in strict mode.
console.log(delete object);
// ❌ SyntaxError: Delete of an unqualified identifier in strict mode.
console.log(delete getName);
>

Свойство массива

Во-первых, свойство length (длины) массива не является настраиваемым, поэтому его удаление в строгом режиме вызовет TypeError .

'use strict';const arr = [1, 2, 3, 4, 5];console.log(Reflect.getOwnPropertyDescriptor(arr, 'length'));
// ❌ TypeError: Cannot delete property 'length' of [object Array]
console.log(delete arr.length);

Во-вторых, при удалении элемента массива удаляемый элемент будет пустым.

Чтобы изменить исходный массив, можно использовать Array.prototype.splice() .

Заключение

Истинное назначение оператора delete в JavaScript — это удаление ссылки на свойство объекта. В некоторых особых случаях он может вести себя странно, поэтому лучше использовать его только для удаления настраиваемых свойств, присущих самому объекту.

Обратите также внимание на то, что оператор delete не имеет ничего общего с освобождением памяти напрямую (в отличие от delete в C++). Это связано с тем, что среда выполнения JavaScript управляет памятью автоматически.

  • Javascript и функциональное программирование. Часть 4: каррирование
  • Зачем разбивать код на столбцы
  • JavaScript Essentials: Типы и структура данных

delete, new[] в C++ и городские легенды об их сочетании

Если в коде на C++ был создан массив объектов с помощью «new[]», удалять этот массив нужно с помощью «delete[]» и ни в коем случае не с помощью «delete» (без скобок). Разумный вопрос: а не то что?

На этот вопрос можно получить широчайший спектр неразумных ответов. Например, «будет удален только первый объект, остальные утекут» или «будет вызван деструктор только первого объекта». Следующие за этим «объяснения» не выдерживают обычно никакой серьезной критики.

В соответствии со Стандартом C++, в этой ситуации поведение не определено. Все предположения – не более чем популярные городские легенды. Разберем подробно, почему.

Нам понадобится хитрый план с примером, который бы ставил в тупик сторонников городских легенд. Вот такой безобидный будет ок:

class Class < public: ~Class() < printf( "Class::~Class()" ); >>; int main()

Здесь объект в массиве всего один. Если верить любой из двух легенд выше, «все будет хорошо» – утекать нечему и некуда, деструкторов будет вызвано ровно сколько нужно.

Идем на codepad.org, вставляем код в форму, получаем выдачу:

memory clobbered before allocated block Exited: ExitFailure 127 42 75 67 20 61 73 73 61 73 73 69 6E 20 77 61 6E 74 65 64 20 2D 20 77 77 77 2E 61 62 62 79 79 2E 72 75 2F 76 61 63 61 6E 63 79

MEMORY WHAT. Что это было?

int main()
No errors or program output.

Здесь хотя бы с виду все хорошо. Что происходит? Почему так происходит? Почему поведение с виду разное?

Причина в том, что происходит внутри.

Когда в коде встречается «new Type[count]», программа обязана выделить память объема, достаточного для хранения указанного числа объектов. Для этого она использует функцию «operator new[]()». Эта функция выделяет память – обычно внутри просто вызов malloc() и проверка возвращаемого значения (при необходимости – вызов new_handler() и выброс исключения). Затем в выделенной памяти конструируются объекты – вызывается нужное число конструкторов. Результатом «new Type[count]» является адрес первого элемента массива.

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

Важный момент: в конструкции «new Type[count]» число элементов было указано явно, а «delete[]» получает только адрес первого элемента.

Откуда программа узнает число элементов? Раз у нее есть только адрес первого элемента, она должна вычислить длину массива на основании одного этого адреса. Как это делается, зависит от реализации, обычно используется следующий способ.

При выполнении «new Type[count]» программа выделяет памяти столько, чтобы в нее поместились не только объекты, но и беззнаковое целое (обычно типа size_t), обозначающее число объектов. В начало выделенной области пишется это число, дальше размещаются объекты. Компилятор при компиляции «new Type[count]» вставляет в программу код, который реализует эти свистелки.

Итак, при выполнении «new Type[count]» программа выделяет чуть больше памяти, записывает число элементов в начало выделенного блока памяти, вызывает конструкторы и возвращает вызывающему коду адрес первого элемента. Адрес первого элемента будет отличаться от адреса, который возвратила функция выделения памяти «operator new[]()».

При выполнении «delete[]» программа берет адрес первого элемента, переданный в «delete[]», определяет адрес начала блока (вычитая ровно столько же, сколько было прибавлено при выполнении «new[]»), читает число элементов из начала блока, вызывает нужное число деструкторов, затем – вызывает функцию «operator delete[]()», передав ей адрес начала блока.

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

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

Что должно произойти? В этой реализации программа аварийно завершается. Поскольку Стандарт говорит, что поведение не определено, это допустимо.

Для сравнения, программа на Visual C++ 9 по умолчанию исходит сообщениями об ошибках в отладочной версии, но вроде бы нормально отрабатывает (по крайней мере, функция _heapchk() возвращает код _HEAP_OK, _CrtDumpMemoryLeaks() не выдает никаких сообщений). Это тоже допустимо.

Почему во втором примере поведение другое? Скорее всего, компилятор учел, что у типа char тривиальный деструктор, т.е. не нужно ничего делать для разрушения объектов, а достаточно просто освободить память, поэтому и число элементов хранить не нужно, а значит, можно сразу вернуть вызывающему коду тот же адрес, который вернула функция «operator new[]()». Никаких смещений адреса – точно так же, как и при вызове «new» (без скобок). Такое поведение компилятора полностью соответствует Стандарту.

Чего-то не хватает…

Вы уже заметили, что выше по тексту встречаются функции выделения и освобождения памяти то с квадратными скобками, то без? Это не опечатки – это две разные пары функций, они могут быть реализованы совершенно по-разному. Даже когда компилятор пытается сэкономить, он всегда вызывает функцию «operator new[]()», когда видит в коде «new Type[count]», и всегда вызывает функцию «operator new()», когда видит в коде «new Type».

Обычно реализации функций «operator new()» и «operator new[]()» одинаковы (обе вызывают malloc()), но их можно заменить – определить свои, причем можно заменить как одну пару, так и обе, также можно заменять эти функции по отдельности для любого выбранного класса. Стандарт позволяет это делать сколько угодно (естественно, нужно адекватно заменить парную функцию освобождения памяти).

Это дает богатые возможности для неопределенного поведения. Если ваш код приводит к тому, что память освобождается «не той» функцией, это может приводить к любым последствиям, в частности, к повреждению кучи, порче памяти или немедленному аварийному завершению программы. В первом примере реализация функции «operator delete()» не смогла распорядиться переданным ей адресом и программа аварийно завершилась.

Самая приятная часть этого рассказа – вы никогда не сможете утверждать, что использование «delete» вместо «delete[]» (и наоборот – тоже) приводит к какому-то конкретному результату. Стандарт говорит, что поведение не определено. Даже полностью соответствующий Стандарту компилятор не обязан выдать вам программу с каким-либо адекватным поведением. Поведение программы, на которое вы будете ссылаться в комментариях и спорах, является только наблюдаемым – внутри может происходить все что угодно. Вы только констатируете наблюдаемое вами поведение.

Во втором примере с виду все хорошо… на этой реализации. На другой реализации функции «operator new()» и «operator new[]()» могут быть, например, реализованы на разных кучах (Windows позволяет создавать более одной кучи на процесс). Что произойдет при попытке возвратить блок «не в ту» кучу?

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

Как быть? Смириться, не путать «delete» и «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 не будет опубликован. Обязательные поля помечены *