Смысл существования delete[] мне всегда был не до конца ясен. Нет, понятно, что он нужен для освобождения массивов памяти, выделенных посредством new[], но почему бы все удаление не интегрировать в простой delete?
А так программисту приходится всегда помнить, что именно вызывать для освобождения памяти: delete или delete[]. Зачем - не очень понятно. Информация о том, сколько именно элементов в массиве все равно где-то да хранится. Почему бы компилятору вообще не освободить программиста от каких-либо запоминаний?
Все, что мне удалось раскопать по этому поводу - старое обсуждение в comp.lang.c++
Why does delete[] exist?
Там же приводится цитата из Страуструпа, где говорится, что вообще в delete[] нет никакой логической необходимости, а нужен он для того, чтобы сэкономить место и время.
Но экономия получается уж очень несущественная. Ну разве что для мобильных устройств она может оказаться значимой. И многие с большим удовольствием отказались бы от этой экономии, ради того чтобы не ловить таинственные глюки и утечки памяти долгими часами из-за каких-то пропущенных скобочек. Простого delete'а вполне достаточно. Более того, по словам участников вышеупомянутой дискуссии, именно так работали ранние Борландовские компиляторы.
Ссылки по теме:
Что делает выражение вида delete p
My Nutty, Extremist Beliefs
12 часов назад
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 - нету. Там много чего вообще нет. Так что нечему тут удивляться ;-)
2ilya:
опять же, из за невозможности это сделать в рантайме.
Предположим, функция вернула некоторый указатель. причем возвращает она указатель на массив, или на один элемент - пусть это рантайм-поведение.
Как узнать, на что указывает - только проверить; проверка же нетривиальна и требует затрат (как и все связанное с управлением памятью). Поэтому все остается на совести программиста. В угоду производительности.
насколько я помню отличие 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 выяснить, что это за объект, обратившись к менеджеру памяти. А лишний код - дополнительное место, и, самое главное, _время_.
2Prokrust:
В первом случае delete для массива и для одного объекта должен быть разным по определению, если конечно не считать один объект массивом из одного элемента (+4 байта при этом).
Угу, ну да, плюс сколько-то там байт будет.
2zorgg:
И это пишет человек, не понаслышке знающий о программировании игр =)
Я знаю людей, которые в статьях 2006-го года (т.е., не 86-го) пишут следующее:
<skip>
И, в общем-то, абсолютно правы.
Сурово.
Все, конечно, от задачи зависит... Если, к примеру, у тебя память течет по мегабайту в час, то сэкономленные несколько байт вряд ли тебя обрадуют.
2sse:
В run-time он не может знать, потому что его удел - это время compile-time. Он только организует код в compile-time так, что может потом в run-time выяснить, что это за объект, обратившись к менеджеру памяти.
Угу, ок.
А лишний код - дополнительное место, и, самое главное, _время_.
Это да, безусловно.
Перегрузка delete[] может быть полезной для отслеживания выделения\освобождения памяти. В этом случае нужно иметь пару для new[].
Не поленилась скачать Borland C++ 3.1, чтобы проверить, не удалит ли там delete массив. Не удалил.
Честное удаление массива из трех элементов через delete[]
CBase::CBase
CBase::CBase
CBase::CBase
CBase::~CBase
CBase::~CBase
CBase::~CBase
Нечестное удаление массива из трех элементов через 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[]. Зачем - не очень понятно. Информация о том, сколько именно элементов в массиве все равно где-то да хранится. Почему бы компилятору вообще не освободить программиста от каких-либо запоминаний?
Очень странный вопрос для опытного программиста на С++ :) Его задают на собеседовании, что бы отсеять новичков.
Что бы понять разницу предлагаю прогнать пару раз такую программу:
#include <stdio.h>
class Test
{
public:
~Test()
{
puts("~");
}
};
int main(int, char**)
{
Test *a = new Test[2];
// пробуем два варианта
// delete a;
delete[] a;
return(0);
}
Хм.. мне тоже странно. Почему бы компилятору не привести 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, и .т.п.
2Анонимный:
Т.е. создал что-то c [] незабудь,
это что-то [] освободить.
Люди будут забывать. Можно этому удивляться, можно над этим потешаться, но если у нас есть что-то с пометкой "не забыть", то это - потенциальный источник неприятностей. Причем наступать на эти грабли будут и новички, и опытные программисты. И выясняться это будет в самый неподходящий момент.
По-моему в одной из публикаций Страуструпа, было написано, что оператор освобождения памяти для массивов должен был выглядеть так:
delete[n] ptr; // ptr указывает на массив из n элементов
В этом случае компилятору не нужно было сохранять информацию о кол-ве элементов. А уже потом его решили сократить до:
delete[] prt;
Для блока памяти известен его размер.
При удалении массива компилятор знает тип его элемента.
Поэтому количество элементов он очень просто вычислит.
Стало быть, 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.
Alex:
Наконец, вы меня успокоили, что можно писать delete везде без сомнительных скобок
Нет, такого я не писала. delete[] нужно использовать при работе с массивами.
Я не конкретно про вас Алена. Я про Интернет. Он меня успокоил, в т.ч. и этот блог.
Наконец, вы меня успокоили, что можно писать delete везде без сомнительных скобок
Нужно не "успокаиваться интернетом" а разбираться с непонятными вопросами.
Тогда вместо глупости типа "delete везде без сомнительных скобок" просто будешь правильно расставлять скобки. :)
Ну то, что нельзя массив удалять delet'ом, как бы выяснили. Возникает другой вопрос: а можно ли всегда и во всем использовать delete []? Т.е. чего ждать если какой нибудь
int *p=new int(5);
//удалить так
delete [] p; //?
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[].
int *p_var = new int;
int *p_array = new int[50];
delete[] p_array;
delete p_var;
Отправить комментарий