среда, января 17, 2007

Что делает выражение вида delete p

Листала C++ Faq Lite, очень хороший там раздел про освобождение памяти. Хотя, там вообще все разделы хорошие. Пара моментов оттуда.

Выражение delete p и оператор delete это разные вещи. Выражение вида delete p; делает нечто вроде


if (p != NULL) {
p->~Fred();
operator delete(p);
}

То есть сначала идет проверка на NULL, потом вызов деструктора, а потом отрабатывает оператор delete. Стандартный, не переопределенный оператор delete пометит память как свободную. Но его можно переопределить при желании.

Проверять на NULL отдельно не нужно и выражение вида

if (p != NULL)
delete p;

Смысла не имеет. Ибо по Стандарту при удалении указателя на NULL ничего страшного произойти не должно.

При удалении массива элементов после delete указываются квадратные скобочки.

Fred* p = new Fred[100];
...
delete[] p;

Очень неприятный момент будет, если квадратные скобочки забыть. А это очень частая ошибка. Существует устойчивое заблуждение, что будет просто удален только первый элемент и все. А вот и нет, будет undefined behavior. То есть может быть действительно будет удален первый элемент массива, а может быть и нет... Самое плохое, что в случае такой ошибки не будет выдано никаких предупреждений.

Число в квадратных скобочках указывать не надо, "оно само" знает сколько надо удалить элементов. В C++ FAQ Lite приведены объяснения двух алгоритмов реализации этого тайного знания: over-allocation и associative array.

20 коммент.:

GiNeR комментирует...

Огромное спасибо за этот пост!
Давно меня мучал этот вопрос.

Alena комментирует...

Огромное спасибо за этот пост!

Пожалуйста :-)

Анонимный комментирует...

Его примеры сбивают с толку: в C++ нулевым указателем принято считать 0, а не NULL (как объяснил Страуструп в параграфе 5.1.1 своего бессмертного труда).

Alena комментирует...

в C++ нулевым указателем принято считать 0, а не NULL (как объяснил Страуструп в параграфе 5.1.1 своего бессмертного труда).

Эээ... А какой именно труд имеется в виду?

В FAQ'е у себя на сайте он рассказывает, что ему лично удобнее использовать 0, а вообще это вопрос вкуса.

Анонимный комментирует...

БЕССМЕРТНЫЙ! :)))
The C+ + Programming Language
Third Edition
Bjarne Stroustrup

5.1.1 Zero [ptr.zero]
Zero (0 ) is an i n t . Because of standard conversions (§C.6.2.3), 0 can be used as a constant of any integral (§4.1.1), floatingpoint,
pointer, or pointertomember
type. The type of zero will be determined by context. Zero will typically (but not necessarily) be represented by the bit pattern allzeros of the appropriate size.
No object is allocated with the address 0 . Consequently, 0 acts as a pointer literal, indicating that a pointer doesn’t refer to an object.
In C, it has been popular to define a macro N U L L to represent the zero pointer. Because of C++’s tighter type checking, the use of plain 0 , rather than any suggested NULL macro, leads to fewer problems. If you feel you must define NULL, use
const int NULL = 0;
The const qualifier (§5.4) prevents accidental redefinition of NULL and ensures that NULL can be used where a constant is required.

Alena комментирует...

БЕССМЕРТНЫЙ! :)))
The C+ + Programming Language
Third Edition


Понятно, у меня издание другое и там вообще такого пункта нет.

Страуструп, похоже, поменял свое мнение с тех пор, судя по FAQ'у. А в C++0x нас вообще ждет новое ключевое слово nullptr.

oleg комментирует...

Совсем недавно откопал очередной баг с delete без []. Подленький такой баг - проявлялся крайне редко...

Анонимный комментирует...

Маленькая неточность c демонстрацией того, что делает operator delete. NULLевой p таки пойдёт в operator delete и operator delete должен даже в случае множественного наследования получить корректный указатель на начало объекта.

Причем это самое operator delete(p->достать адрес объмлющего подобъекта) - оно на практике прямо в виртуальном деструкторе, откуда уже и зовётся delete.

Так что скорее

#if деструктор виртуальный
if (p != 0)
p->~T'deleting destructor'();
else
delete p;
#else
if (p != 0)
p->~T();
delete p;
#endif



про "удалить только первый элемент" - непонятно что это ;)

Обычно если у объектов есть нетривиальный деструктор, то перед динамическим массивом кладётся его размер, что бы знать, сколько раз вызвать деструктор. И вызвать delete[] для такого массива - это значит что operator delete получит не тот адресс и всё упадёт внутри debug_free или молчаливо добавится невалидный блок в настоящем free. С последующим непонятным падением при malloc.

PS.
Your HTML cannot be accepted: Tag is not allowed: < pre > - что за... :(

pullover комментирует...

посмотрел реализацию operator delete[] в MSVC++ и ужаснулся:
void operator delete[]( void * p )
{
RTCCALLBACK(_RTC_Free_hook, (p, 0))

operator delete(p);
}

эта функция вызвалась сразу внутри
delete[] m_p;

Анонимный комментирует...

А в C++0x нас вообще ждет новое ключевое слово nullptr.

Об этом неплохо у Мэтью Уилсона написано, в "Imperfect C++". Для чего оно, и как реализовать средствами языка.

Alena комментирует...

Маленькая неточность c демонстрацией того, что делает operator delete. NULLевой p таки пойдёт в operator delete и operator delete должен даже в случае множественного наследования получить корректный указатель на начало объекта.

Что-то мне это кажется подозрительным. У тебя есть ссылки какие-нибудь на источник информации?
Я не раз читала в том же comp.lang.c++.moderated, что NULL в оператор delete не пойдет. Не очень поняла, связана ли передача NULL'а со второй частью фразы о множественном наследовании, но из нее вроде как следует что при множественном наследовании должен быть получен корректный указатель на начало объекта, даже если этот указатель NULL. Как?

#if деструктор виртуальный

Странный псевдокод. Там operator delete имелся в виду, нет?

про "удалить только первый элемент" - непонятно что это ;)

Это распространенное заблуждение.

Обычно если у объектов есть нетривиальный деструктор, то перед динамическим массивом кладётся его размер, что бы знать, сколько раз вызвать деструктор.

AFAIK, нетривиальный деструктор тут необязателен.

это значит что operator delete получит не тот адрес и всё упадёт внутри debug_free

Это зависит от реализации. Для динамических массивов я знаю две известные техники реализации, на которые дала ссылки. Ты рассказываешь об over-allocation, которая действительно так и работает и которая чаще используется, насколько мне известно. Но есть еще и associative array, который использовался в Cfront еще. Я думаю, оттуда и пошла легенда про удаление только первого элемента массива, потому что при такой реализации delete p; вместо delete[] p; приведет к удалению только первого элемента. Ну и никто не запрещает запоминать количество элементов еще каким-нибудь способом.

Ну в принципе Стандартом тут гарантировано undefined behavior, так что в любом случае ничего хорошего от такой ошибки произойти не может.

Your HTML cannot be accepted: Tag is not allowed:

Блоггер не все теги пропускает в комментариях, в том числе и pre, увы...

Alena комментирует...


А в C++0x нас вообще ждет новое ключевое слово nullptr.

Об этом неплохо у Мэтью Уилсона написано, в "Imperfect C++". Для чего оно, и как реализовать средствами языка.


Раз уж зашла об этом речь: документ по которому будет реализован nullptr.
A Name For the Null Pointer: nullptr (revision 2)

Анонимный комментирует...

Я немножко напутал, если деструктор виртуальный, то в operator delete NULL не придёт. Если невиртуальный - то придёт. Подветочка else в ветке #if - лишняя. Источник информации - достаточно просто в void ::operator delete(void* p) разместить printf("%p", p) и увидеть :) Как в VC++, так и в GCC.

про то, что operator delete должен вызываться не для p, а для чего-то вроде dynamic_cast<void*>(p) - это как бы очевидно, иначе туда придёт не то, что выделили по new. Реализовано может быть по разному, могу сказать точно что в VC++ - из виртуального деструктора.
class D: A, B {};
B *b = new D;
delete b;


Там operator delete имелся в виду, нет?
да, именно, я ещё плохо проснулся утром :]

AFAIK,
да, UB. Но просто когда отлаживаешь - необходимо знать определение UB на той платформе, где живёшь, иначе в C++ сложно жить :)

PS. Ещё блоггер не разрешает '_' '-' в URL на блог, поэтому я так странно подписался.

Анонимный комментирует...

Опаньки, ошибся опять :( в G++ не вызывается operator delete для NULL...

Alena комментирует...

Источник информации - достаточно просто в void ::operator delete(void* p) разместить printf("%p", p) и увидеть :) Как в VC++, так и в GCC.
...
Опаньки, ошибся опять :( в G++ не вызывается operator delete для NULL...

Хех, я и не думала что можно перекрывать глобальный operator delete...

Я попробовала погонять в компиляторах, что есть у меня, и вот что у меня вышло. Надо сказать, что виртуальность деструкторов на ситуацию не влияла никак. Сначала я переопределила operator delete для класса.
MSVC2003 не вызвает
MSVC6.0 не вызвает
g++3.4.4 - не вызывает

Потом я перекрыла ::operator delete

MSVC2003 вызвает, несколько раз, для других адресов, не для NULL
MSVC6.0 рапортует о попытке вызвать чисто виртуальную функцию (?)
g++3.4.4 - не вызывает

Но вообще это все не очень важно... Потому что Стандартом гарантировано, что delete NULL делать можно. А при переопределении в operator delete все равно нужно делать проверку на NULL, потому что его можно вызвать напрямую.

Анонимный комментирует...

Вот читаю это все и ужасаюсь... что бы было, если бы не было .NET или Java - ппц просто ! ...пойдет 7 бит в 137 ножку процессора или не пойдет... ппц... все таки пойдет... ан нет,я где то в faq'e видел что 137 это инвертная 145 нога, при чем на 8 прирывании ))) -значит пойдет но не всегда !!! Вообщем впечатления примерно такие )))
Как мне не нравились плюсы так и ща не понимаю этой каши, при чем многие считают что разбираться в этом край как нужно. Си - malloc() free() - железная логика, c#/java - это new и/или new [] для массивов, удаление происходит автоматически... С++ - это лажа, не получившийся C# из Си , столько противоречий нет ни в одном языке... ))

Анонимный комментирует...

а вот так будет работать:
char* с = new char[128];
delete c;

и заметьте скобочек нет :)

Unknown комментирует...

здравствуйте, Алена!
У меня есть такой пример:
{
char *x = new char [20];
char *part2, *part1;

strcpy(x, "ma gf gh ty");
part2 = &x[3];
x[2] = '\0';
part1 = x;
delete part1;
cout << part2;
}

В нем я разбиваю строку на 2 строки.
Вот кто-нибудь может объянить, почему при удалении part1 (казалось бы до нулевого символа),
Си удаляет весь массив x.
Если же удалить Part2, то вообще ошибка будет, то есть такое ощущение, что Си при удалении ищет не нулевой символ, а конец ранее выделенной памяти.

Alena комментирует...

2Анечка пишет:
здравствуйте, Алена!

Приветствую!

В нем я разбиваю строку на 2 строки.
Вот кто-нибудь может объянить, почему при удалении part1 (казалось бы до нулевого символа),


delete не будет удалять до нулевого символа. Результатом такой операции будет undefined behavior. Что не хорошо.

Си удаляет весь массив x.
Если же удалить Part2, то вообще ошибка будет, то есть такое ощущение, что Си при удалении ищет не нулевой символ, а конец ранее выделенной памяти.


delete[] удаляет весь массив выделенной памяти. Даже если он весь забит нулями. Даже если там нет нулевого символа вообще. Работа с delete на массиве непредсказуема.

А собственно, что нужно-то? Просто разбить строку на две?
Можно создать две новых строки, прокопировать все, что нужно туда, старую строку удалить.

Анонимный комментирует...

Java и .NET тоже выглядят не очень привлекательно. На C++ лучше писать, как умеешь и понимаешь, не шагу в сторону. Сегодня для меня стало новостью про освобождению памяти оператором delete[] выделенной под массив, конечно я это знал, но в один прекрасный момент забыл, т.к. использую именно массивы крайне редко. У С++ большие проблемы, и притекли они из Си и недокомпиляторов С++. Давно пора его изменить в ущерб обратной совместимости.