Будет ли этот код C++ вызывать утечку памяти (приведение нового массива)
Я работал над некоторым устаревшим кодом C++, который использует структуры переменной длины (TAPI), где размер структуры будет зависеть от строк переменной длины. Структуры выделяются литейным массивом new
таким образом:
STRUCT* pStruct = (STRUCT*)new BYTE [sizeof(STRUCT) + nPaddingSize];
Позже, однако, память освобождается с помощью delete
вызов:
delete pStruct;
Будет ли это сочетание массива new []
и не массив delete
вызвать утечку памяти или это будет зависеть от компилятора? Буду ли я лучше изменить этот код для использования malloc
а также free
вместо?
24 ответа
Технически я считаю, что это может вызвать проблемы с несовпадающими распределителями, хотя на практике я не знаю ни одного компилятора, который не справился бы с этим примером правильно.
Что более важно, если STRUCT
где иметь (или когда-либо получить) деструктор, тогда он вызовет деструктор, не вызвав соответствующий конструктор.
Конечно, если вы знаете, откуда появился pStruct, почему бы просто не привести его к удалению, чтобы соответствовать распределению:
delete [] (BYTE*) pStruct;
Я лично думаю, что вам лучше использовать std::vector
управлять вашей памятью, так что вам не нужно delete
,
std::vector<BYTE> backing(sizeof(STRUCT) + nPaddingSize);
STRUCT* pStruct = (STRUCT*)(&backing[0]);
После того, как поддержка покидает сферу, ваш pStruct
больше не действителен
Или вы можете использовать:
boost::scoped_array<BYTE> backing(new BYTE[sizeof(STRUCT) + nPaddingSize]);
STRUCT* pStruct = (STRUCT*)backing.get();
Или же boost::shared_array
если вам нужно переместить владение вокруг.
Поведение кода не определено. Вам может повезти (или нет), и это может работать с вашим компилятором, но на самом деле это не правильный код. Есть две проблемы с этим:
delete
должен быть массивdelete []
,delete
должен вызываться по указателю на тот же тип, что и выделенный тип.
Итак, чтобы быть полностью правильным, вы хотите сделать что-то вроде этого:
delete [] (BYTE*)(pStruct);
Да, это приведет к утечке памяти.
Смотрите это, за исключением C++ Gotchas: http://www.informit.com/articles/article.aspx?p=30642 чтобы узнать почему.
У Раймонда Чена есть объяснение того, как вектор new
а также delete
отличаются от скалярных версий под чехлами для компилятора Microsoft... Здесь: http://blogs.msdn.com/oldnewthing/archive/2004/02/03/66660.aspx
ИМХО, вы должны исправить удаление, чтобы:
delete [] pStruct;
вместо того, чтобы переключиться на malloc
/ free
хотя бы потому, что это проще сделать без ошибок;)
И, конечно, проще вносить изменения, которые я показываю выше, неправильно из-за приведения в исходном распределении, это должно быть
delete [] reinterpret_cast<BYTE *>(pStruct);
так что, наверное, так же легко переключиться на malloc
/ free
в конце концов;)
Стандарт C++ четко гласит:
delete-expression:
::opt delete cast-expression
::opt delete [ ] cast-expression
Первый вариант - для объектов, не являющихся массивами, а второй - для массивов. Операнд должен иметь тип указателя или тип класса, имеющий одну функцию преобразования (12.3.2) в тип указателя. Результат имеет тип void.
В первом варианте (объект удаления) значение операнда удаления должно быть указателем на объект, не являющийся массивом [...]. Если нет, поведение не определено.
Значение операнда в delete pStruct
это указатель на массив char
независимо от его статического типа (STRUCT*
). Поэтому любое обсуждение утечек памяти совершенно бессмысленно, потому что код некорректен, и компилятор C++ не требуется для создания разумного исполняемого файла в этом случае.
Это может привести к утечке памяти, не может или может привести к сбою системы. Действительно, реализация C++, с которой я тестировал ваш код, прерывает выполнение программы в точке выражения delete.
Как отмечено в других сообщениях:
1) Вызывает new/delete, выделяет память и может вызывать конструкторы / деструкторы (C++ '03 5.3.4 / 5.3.5)
2) Смешивание версий массивов / не массивов new
а также delete
является неопределенным поведением. (C++ '03 5.3.5/4)
Глядя на источник, кажется, что кто-то сделал поиск и замену malloc
а также free
и выше, это результат. C++ имеет прямую замену для этих функций, то есть для вызова функций выделения new
а также delete
непосредственно:
STRUCT* pStruct = (STRUCT*)::operator new (sizeof(STRUCT) + nPaddingSize);
// ...
pStruct->~STRUCT (); // Call STRUCT destructor
::operator delete (pStruct);
Если нужно вызвать конструктор для STRUCT, то вы можете рассмотреть вопрос о выделении памяти и затем использовать размещение new
:
BYTE * pByteData = new BYTE[sizeof(STRUCT) + nPaddingSize];
STRUCT * pStruct = new (pByteData) STRUCT ();
// ...
pStruct->~STRUCT ();
delete[] pByteData;
Различные возможные варианты использования ключевых слов new и delete создают значительную путаницу. В C++ всегда есть два этапа построения динамических объектов: выделение необработанной памяти и построение нового объекта в выделенной области памяти. На другой стороне времени существования объекта происходит разрушение объекта и освобождение места памяти, в котором находился объект.
Часто эти два шага выполняются одним оператором C++.
MyObject* ObjPtr = new MyObject;
//...
delete MyObject;
Вместо вышесказанного вы можете использовать функции выделения сырой памяти в C++ operator new
а также operator delete
и явная конструкция (через размещение new
) и уничтожение для выполнения эквивалентных шагов.
void* MemoryPtr = ::operator new( sizeof(MyObject) );
MyObject* ObjPtr = new (MemoryPtr) MyObject;
// ...
ObjPtr->~MyObject();
::operator delete( MemoryPtr );
Обратите внимание, что не происходит приведения типов, и в выделенной области памяти создается только один тип объекта. Используя что-то вроде new char[N]
как способ выделения необработанной памяти технически некорректен, так как, по логике char
объекты создаются во вновь выделенной памяти. Я не знаю ни одной ситуации, когда это не "просто работает", но стирает различие между необработанным выделением памяти и созданием объекта, поэтому я советую против этого.
В этом конкретном случае нет никакой выгоды, если разделить два этапа delete
но вам нужно вручную контролировать первоначальное распределение. Приведенный выше код работает в сценарии "все работает", но он утечет необработанную память в случае, когда конструктор MyObject
бросает исключение. Хотя это можно было бы уловить и решить с помощью обработчика исключений в точке выделения, вероятно, лучше предусмотреть новый пользовательский оператор, чтобы вся конструкция могла обрабатываться выражением размещения new.
class MyObject
{
void* operator new( std::size_t rqsize, std::size_t padding )
{
return ::operator new( rqsize + padding );
}
// Usual (non-placement) delete
// We need to define this as our placement operator delete
// function happens to have one of the allowed signatures for
// a non-placement operator delete
void operator delete( void* p )
{
::operator delete( p );
}
// Placement operator delete
void operator delete( void* p, std::size_t )
{
::operator delete( p );
}
};
Здесь есть пара тонких моментов. Мы определяем размещение класса новым, так что мы можем выделить достаточно памяти для экземпляра класса плюс некоторые пользовательские отступы. Поскольку мы делаем это, нам нужно обеспечить соответствующее удаление размещения, чтобы, если распределение памяти прошло успешно, но конструкция не удалась, выделенная память автоматически была освобождена. К сожалению, подпись для нашего удаления места размещения совпадает с одной из двух разрешенных подписей для удаления без размещения, поэтому нам нужно предоставить другую форму удаления без размещения, чтобы наше реальное удаление места размещения рассматривалось как удаление места размещения. (Мы могли бы обойти это, добавив дополнительный фиктивный параметр как к нашему новому месту размещения, так и к удалению места размещения, но это потребовало бы дополнительной работы на всех вызывающих сайтах.)
// Called in one step like so:
MyObject* ObjectPtr = new (padding) MyObject;
Используя одно новое выражение, мы теперь гарантируем, что память не будет вытекать, если какая-либо часть нового выражения выбрасывается.
На другом конце времени жизни объекта, поскольку мы определили оператор delete (даже если бы мы этого не сделали, память для объекта изначально была получена от глобального оператора new в любом случае), следующий способ является правильным способом уничтожения динамически созданного объекта,
delete ObjectPtr;
Резюме!
Смотри не бросает!
operator new
а такжеoperator delete
иметь дело с сырой памятью, размещение новых может создавать объекты в сырой памяти. Явный приведение отvoid*
указатель на объект обычно является признаком чего-то логически неправильного, даже если он "просто работает".Мы полностью проигнорировали new[] и delete[]. Эти объекты переменного размера не будут работать в массивах в любом случае.
Размещение new позволяет новому выражению не просачиваться, новое выражение по-прежнему оценивается как указатель на объект, который необходимо уничтожить, и память, которая требует освобождения. Использование некоторых типов интеллектуальных указателей может помочь предотвратить другие типы утечек. На положительной стороне мы дали равнине
delete
быть правильным способом сделать так, чтобы большинство стандартных умных указателей работало.
@eric - Спасибо за комментарии. Вы продолжаете говорить что-то, хотя, это сводит меня с ума:
Эти библиотеки времени выполнения обрабатывают вызовы управления памятью для ОС в независимом согласованном синтаксисе ОС, и эти библиотеки времени выполнения отвечают за то, чтобы malloc и new работали согласованно между операционными системами, такими как Linux, Windows, Solaris, AIX и т. Д....,
Это неправда. Например, разработчик компилятора обеспечивает реализацию библиотек std, и они абсолютно свободны для их реализации в зависимости от ОС. Например, они могут сделать один гигантский вызов malloc, а затем управлять памятью в блоке так, как им хочется.
Совместимость обеспечивается потому, что API std и т. Д. Одинаковы, а не потому, что все библиотеки времени выполнения вращаются и вызывают одни и те же вызовы ОС.
Если вы действительно должны делать такие вещи, вам, вероятно, следует позвонить оператору new
непосредственно:
STRUCT* pStruct = operator new(sizeof(STRUCT) + nPaddingSize);
Я полагаю, что вызов этого пути позволяет избежать вызова конструкторов / деструкторов.
В настоящее время я не могу голосовать, но ответ Slicedlime предпочтительнее , чем ответ Роба Уокера, поскольку проблема не имеет ничего общего с распределителями или с тем, имеет ли STRUCT деструктор.
Также обратите внимание, что пример кода не обязательно приводит к утечке памяти - это неопределенное поведение. Практически все могло случиться (от ничего плохого до крушения далеко-далеко).
Пример кода приводит к неопределенному поведению, простому и понятному. Ответ SlicedLime является прямым и точным (с оговоркой, что слово "вектор" должно быть заменено на "массив", поскольку векторы - это STL).
Этот вид материала довольно хорошо описан в FAQ C++ (разделы 16.12, 16.13 и 16.14):
Вы имеете в виду удаление массива ([]), а не удаление вектора. Вектор - это std::vector, и он заботится об удалении своих элементов.
В дополнение к превосходным ответам выше, я также хотел бы добавить:
Если ваш код работает на Linux или вы можете скомпилировать его на Linux, я бы предложил запустить его через Valgrind. Это отличный инструмент, среди множества полезных предупреждений, которые он выдает, он также сообщит вам, когда вы выделяете память в виде массива, а затем освобождаете ее как не массив (и наоборот).
Лен: проблема в том, что pStruct является STRUCT*, но выделенная память фактически является байтом [] некоторого неизвестного размера. Так что delete[] pStruct не будет выделять всю выделенную память.
Используйте оператор new и удалите:
struct STRUCT
{
void *operator new (size_t)
{
return new char [sizeof(STRUCT) + nPaddingSize];
}
void operator delete (void *memory)
{
delete [] reinterpret_cast <char *> (memory);
}
};
void main()
{
STRUCT *s = new STRUCT;
delete s;
}
Да, это возможно, так как вы размещаете с помощью new[], но освобождаете с помощью deletelte, да malloc/free здесь безопаснее, но в C++ вы не должны их использовать, поскольку они не будут обрабатывать (де) конструкторы.
Также ваш код будет вызывать деконструктор, но не конструктор. Для некоторых структур это может вызвать утечку памяти (если конструктор выделил дополнительную память, например, для строки)
Лучше было бы сделать это правильно, так как это также будет правильно вызывать любые конструкторы и деконструкторы
STRUCT* pStruct = new STRUCT;
...
delete pStruct;
Всегда лучше, чтобы приобретение / выпуск любого ресурса был как можно более сбалансированным. Хотя утечка или нет трудно сказать в этом случае. Это зависит от реализации компилятором выделения вектора (de).
BYTE * pBytes = new BYTE [sizeof(STRUCT) + nPaddingSize];
STRUCT* pStruct = reinterpret_cast< STRUCT* > ( pBytes ) ;
// do stuff with pStruct
delete [] pBytes ;
Вы можете привести обратно к байту * и удалить:
delete[] (BYTE*)pStruct;
@ericmayo - крипы. Что ж, экспериментируя с VS2005, я не могу получить честную утечку из скалярного удаления памяти, которое было сделано вектором new. Я полагаю, что поведение компилятора здесь не определено и является лучшей защитой, которую я могу найти.
Вы должны признать, хотя, это действительно паршивая практика делать то, что сказал оригинальный постер.
Если бы это было так, то C++ не был бы переносимым, как сегодня, и сбойное приложение никогда не было бы очищено ОС.
Эта логика на самом деле не справедлива. Я утверждаю, что среда выполнения компилятора может управлять памятью внутри блоков памяти, возвращаемых ей ОС. Именно так работает большинство виртуальных машин, поэтому ваш аргумент против переносимости в этом случае не имеет особого смысла.
@Matt Cruikshank
"Ну, экспериментируя с VS2005, я не могу получить честную утечку из скалярного удаления из памяти, которое было сделано вектором new. Я думаю, что поведение компилятора здесь" неопределено ", это лучшая защита, которую я могу найти".
Я не согласен, что это поведение компилятора или даже проблема компилятора. Ключевое слово "new" компилируется и связывается, как вы указали, с библиотеками времени выполнения. Эти библиотеки времени выполнения обрабатывают вызовы управления памятью для ОС в независимом согласованном синтаксисе ОС, и эти библиотеки времени выполнения отвечают за то, чтобы malloc и new работали согласованно между операционными системами, такими как Linux, Windows, Solaris, AIX и т. Д.... Это причина, по которой я упомянул аргумент переносимости; попытка доказать вам, что среда выполнения на самом деле не управляет памятью.
ОС управляет памятью.
Интерфейс libs среды выполнения к ОС. В Windows это библиотеки DLL диспетчера виртуальной памяти. Вот почему stdlib.h реализован в библиотеках GLIB-C, а не в источнике ядра Linux; если GLIB-C используется в других операционных системах, он вносит изменения в malloc для правильных вызовов операционной системы. В VS, Borland и т. Д. Вы никогда не найдете библиотек, которые поставляются со своими компиляторами, которые фактически управляют памятью. Однако вы найдете специальные определения ОС для malloc.
Поскольку у нас есть исходники для Linux, вы можете посмотреть, как там реализован malloc. Вы увидите, что malloc фактически реализован в компиляторе GCC, который, в свою очередь, в основном делает два системных вызова Linux в ядре для выделения памяти. Никогда, сам Malloc, фактически управляющий памятью!
И не принимай это от меня. Прочитайте исходный код ОС Linux или посмотрите, что K&R говорит об этом... Вот PDF-ссылка на K&R на C.
http://www.oberon2005.ru/paper/kr_c.pdf
Смотрите ближе к концу страницы: "Вызовы malloc и free могут происходить в любом порядке; malloc обращается к операционной системе, чтобы получить больше памяти по мере необходимости. Эти процедуры иллюстрируют некоторые соображения, связанные с написанием машинно-зависимого кода в относительно машинно-независимой Кстати, а также покажите в реальной жизни применение структур, союзов и typedef."
"Вы должны признать, что делать пародии на оригинальный постер - действительно паршивая практика".
О, я не согласен там. Я хотел сказать, что исходный код плаката не способствовал утечке памяти. Это все, что я говорил. Я не вмешивался в лучшие практики. Поскольку код вызывает удаление, память освобождается.
В вашу защиту я согласен, что если исходный код автора никогда не завершался или никогда не попадал в вызов delete, то в коде может быть утечка памяти, но, поскольку он заявляет, что позднее он увидит, что удаление вызывается."Позже, однако, память освобождается с помощью вызова удаления:"
Более того, моя причина для ответа, как я сделал, была из-за комментария ОП "Структуры переменной длины (TAPI), где размер структуры будет зависеть от строк переменной длины"
Этот комментарий звучал так, как будто он подверг сомнению динамический характер распределений в отношении выполняемого приведения и, следовательно, задавался вопросом, не вызовет ли это утечку памяти. Я читал между строк, если хотите;).
Вы как бы смешиваете C и C++ способы ведения дел. Зачем выделять больше, чем размер STRUCT? Почему не просто "новая СТРУКТУРА"? Если вы должны сделать это, тогда, возможно, было бы более понятно использовать malloc и free в этом случае, так как тогда вы или другие программисты могли бы с меньшей вероятностью делать предположения о типах и размерах выделенных объектов.
@Matt Cruikshank Вы должны обратить внимание и прочитать то, что я написал снова, потому что я никогда не предлагал не вызывать delete[] и просто позволить операционной системе очиститься. И вы ошибаетесь в отношении библиотек времени выполнения C++, управляющих кучей. Если бы это было так, то C++ не был бы переносимым, как сегодня, и сбойное приложение никогда не было бы очищено ОС. (Признавая, что существуют специфичные для ОС среды выполнения, которые делают C/C++ непереносимым). Я призываю вас найти stdlib.h в исходниках Linux на kernel.org. Новое ключевое слово в C++ фактически использует те же процедуры управления памятью, что и malloc.
Библиотеки времени выполнения C++ выполняют системные вызовы ОС, и именно ОС управляет кучами. Вы отчасти правы в том, что библиотеки времени выполнения указывают, когда освобождать память, однако, они на самом деле не обходят таблицы кучи напрямую. Другими словами, среда выполнения, с которой вы ссылаетесь, не добавляет код в ваше приложение для обхода или выделения. Это имеет место в Windows, Linux, Solaris, AIX и т. Д.... Это также причина, по которой вы не будете штрафовать malloc в любом исходном коде ядра Linux и не найдете stdlib.h в исходном коде Linux. Понимают, что у этих современных операционных систем есть диспетчеры виртуальной памяти, которые еще более усложняют ситуацию.
Вы когда-нибудь задумывались, почему вы можете сделать запрос malloc для 2G оперативной памяти на блоке 1G и при этом получить верный указатель памяти?
Управление памятью на процессорах x86 управляется в пространстве ядра с использованием трех таблиц. PAM (таблица размещения страниц), PD (каталоги страниц) и PT (таблицы страниц). Это на аппаратном уровне, о котором я говорю. Одна из вещей, которую делает диспетчер памяти ОС, а не ваше приложение C++, состоит в том, чтобы узнать, сколько физической памяти установлено на устройстве во время загрузки, с помощью вызовов BIOS. ОС также обрабатывает исключения, например, когда вы пытаетесь получить доступ к памяти, у вашего приложения тоже нет прав. (Ошибка общей защиты GPF).
Возможно, мы говорим то же самое, Мэтт, но я думаю, что вы можете немного сбить с толку функциональность "под капотом". Я использую, чтобы поддерживать компилятор C/C++ для жизни...
Я думаю, что нет утечки памяти.
STRUCT* pStruct = (STRUCT*)new BYTE [sizeof(STRUCT) + nPaddingSize];
Это преобразуется в вызов выделения памяти в операционной системе, на который возвращается указатель на эту память. Во время выделения памяти размер sizeof(STRUCT)
и размер nPaddingSize
было бы известно, чтобы выполнить любые запросы выделения памяти для базовой операционной системы.
Таким образом, выделенная память "записывается" в глобальные таблицы распределения памяти операционной системы. Таблицы памяти индексируются по указателям. Таким образом, при соответствующем вызове delete вся память, которая была первоначально выделена, свободна. (Фрагментация памяти популярный предмет и в этой области).
Видите ли, компилятор C/C++ не управляет памятью, а основной операционной системой.
Я согласен, что есть более чистые методы, но ОП сказал, что это был устаревший код.
Короче говоря, я не вижу утечки памяти, поскольку принятый ответ считает, что она есть.
Ответ Роба Уокера хорош.
Небольшое дополнение, если у вас нет конструктора и / или дистракторов, поэтому вам нужно выделить и освободить кусок сырой памяти, рассмотрите возможность использования пары free/malloc.
ericmayo.myopenid.com настолько неправ, что кто-то с достаточной репутацией должен понизить его рейтинг.
Библиотеки времени выполнения C или C++ управляют кучей, которая выделяется ей в блоках операционной системой, как вы указываете, Эрик. Но разработчик обязан указать компилятору, какие вызовы во время выполнения должны быть сделаны для освобождения памяти, и, возможно, уничтожить находящиеся там объекты. В этом случае необходимо удалить вектор (иначе удалить []), чтобы среда выполнения C++ оставила кучу в допустимом состоянии. Тот факт, что когда PROCESS завершается, ОС достаточно умна, чтобы освободить базовые блоки памяти, разработчики не должны полагаться на это. Это все равно что никогда не вызывать delete.