Будет ли этот код С++ вызывать утечку памяти (новый массив заливки)

Я работал над некоторым унаследованным кодом С++, который использует структуры переменной длины (TAPI), где размер структуры будет зависеть от строк переменной длины. Структуры выделяются массивом литья new, таким образом:

STRUCT* pStruct = (STRUCT*)new BYTE [sizeof(STRUCT) + nPaddingSize];

Позже, однако, память освобождается с помощью вызова delete:

delete pStruct;

Будет ли это сочетание массива new [] и non-array delete вызвать утечку памяти или будет зависеть от компилятора? Было бы лучше изменить этот код, чтобы вместо этого использовать malloc и free?

Ответ 1

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

Более важно, если STRUCT, где нужно (или когда-либо дано) деструктор, тогда он будет вызывать деструктор без вызова соответствующего конструктора.

Конечно, если вы знаете, откуда возникла pStruct, почему бы просто не отбросить ее на удаление в соответствии с распределением:

delete [] (BYTE*) pStruct;

Ответ 2

Я лично думаю, что вам лучше использовать 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, если вам нужно переместить право собственности.

Ответ 3

Поведение кода undefined. Возможно, вам повезет (или нет), и он может работать с вашим компилятором, но на самом деле это не правильный код. Там есть две проблемы:

  • delete должен быть массивом delete [].
  • delete должен быть вызван указателем на тот же тип, что и выделенный тип.

Итак, чтобы быть абсолютно правильным, вы хотите сделать что-то вроде этого:

delete [] (BYTE*)(pStruct);

Ответ 4

Да, это приведет к утечке памяти.

Смотрите это, кроме С++ 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 в конце концов;)

Ответ 5

В стандарте С++ четко указано:

delete-expression:
             ::opt delete cast-expression
             ::opt delete [ ] cast-expression

Первый вариант - для объектов без массива, а второй - для массивов. Операнд должен иметь тип указателя или тип класса, который имеет одну функцию преобразования (12.3.2) в тип указателя. Результат имеет тип void.

В первом альтернативе (удалить объект) значение операнда delete должно быть указателем на объект без массива [...] Если нет, то поведение undefined.

Значение операнда в delete pStruct является указателем на массив char, независимо от его статического типа (STRUCT*). Поэтому любое обсуждение утечек памяти совершенно бессмысленно, потому что код плохо сформирован, а компилятор С++ не требуется для создания разумного исполняемого файла в этом случае.

Это может привести к утечке памяти, она не может, или она может сделать что угодно, чтобы сбой вашей системы. Действительно, реализация С++, с которой я тестировал ваш код, прерывает выполнение программы в точке выражения delete.

Ответ 6

Как указано в других сообщениях:

1) Вызывает новое/удалить выделение памяти и может вызывать конструкторы/деструкторы (С++ '03 5.3.4/5.3.5)

2) Смешивание массива/не-массивные версии new и delete - это поведение undefined. (С++ '03 5.3.5/4)

Посмотрев на источник, выяснилось, что кто-то выполнил поиск и заменил для malloc и free, и это результат. С++ имеет прямую замену для этих функций, и это нужно прямое назначение функций распределения для 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;

Ответ 7

Если вы действительно должны это делать, вам следует, скорее всего, вызвать оператор new:

STRUCT* pStruct = operator new(sizeof(STRUCT) + nPaddingSize);

Я считаю, что его так называют, избегая вызова конструкторов/деструкторов.

Ответ 8

@eric - Спасибо за комментарии. Вы все время говорите что-то, что меня раздражает:

Эти библиотеки времени выполнения обрабатывают управление памятью вызывает ОС в Независимый от ОС синтаксис и эти библиотеки времени выполнения ответственный за создание malloc и новых последовательно работать между ОС, такими как Linux, Windows, Solaris, AIX и т.д.

Это неверно. Например, компилятор обеспечивает реализацию библиотек std, и они абсолютно бесплатны для реализации в OS зависимой. Например, они бесплатны, чтобы сделать один гигантский вызов malloc, а затем управлять памятью внутри блока, как бы они ни желали.

Совместимость обеспечивается, поскольку API-интерфейс std и т.д. одинаковый - не потому, что библиотеки времени выполнения все обходятся и вызывают одинаковые вызовы ОС.

Ответ 9

Различные возможные варианты использования ключевых слов new и delete, похоже, создают достаточную путаницу. В С++ всегда существуют два этапа построения динамических объектов: выделение необработанной памяти и построение нового объекта в выделенной области памяти. С другой стороны времени жизни объекта происходит уничтожение объекта и освобождение места памяти, в котором находился объект.

Часто эти два этапа выполняются одним оператором С++.

MyObject* ObjPtr = new MyObject;

//...

delete MyObject;

Вместо вышесказанного вы можете использовать функции выделения необработанной памяти С++ 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 выдает исключение. Хотя это можно поймать и решить с помощью обработчика исключений в точке выделения, возможно, более аккуратно предоставить пользовательский новый оператор, чтобы полная конструкция могла обрабатываться новым выражением размещения.

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 );
    }
};

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

// Called in one step like so:
MyObject* ObjectPtr = new (padding) MyObject;

Используя одно новое выражение, мы теперь гарантируем, что память не будет протекать, если какая-либо часть нового выражения выдает.

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

delete ObjectPtr;

Резюме

  • Посмотри, нет! operator new и operator delete имеют дело с необработанной памятью, размещение new может строить объекты в необработанной памяти. Явное приведение от void* к указателю объекта обычно является признаком чего-то логически неправильного, даже если оно "просто работает".

  • Мы полностью проигнорировали новый [] и удалили []. Эти объекты с переменным размером не будут работать в массивах в любом случае.

  • Размещение new позволяет новому выражению не течь, новое выражение по-прежнему оценивает указатель на объект, который нуждается в уничтожении, и память, которая требует освобождения. Использование определенного типа смарт-указателя может помочь предотвратить другие типы утечек. С положительной стороны мы разрешим простой delete быть правильным способом сделать это, чтобы большинство стандартных интеллектуальных указателей работали.

Ответ 10

В настоящее время я не могу голосовать, но ответ на slicedlime предпочтительнее ответ Роба Уокер, поскольку проблема не имеет ничего общего с распределителями или имеет ли STRUCT деструктор.

Также обратите внимание, что код примера не обязательно приводит к потере памяти - это поведение undefined. Довольно многое могло случиться (от ничего плохого до краха далеко, далеко).

Пример кода приводит к поведению undefined, простому и простому. Ответ на slicedlime является прямым и точным (с оговоркой, что слово "вектор" следует изменить на "массив", поскольку векторы - это вещь STL).

Этот вид материалов очень хорошо освещен в FAQ по С++ (разделы 16.12, 16.13 и 16.14):

http://www.parashift.com/c++-faq-lite/freestore-mgmt.html#faq-16.12

Ответ 11

Это массив delete ([]), на который вы ссылаетесь, а не удаление вектора. Вектор std::vector, и он заботится об удалении его элементов.

Ответ 12

Да, что может, поскольку вы выделяете новый [], но освобождаете delelte, да malloc/free здесь безопаснее, но в С++ вы не должны использовать их, поскольку они не будут обрабатывать конструкторы (de).

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

Лучше было бы сделать это правильно, так как это также правильно вызовет любые конструкторы и деконструкторы

STRUCT* pStruct = new STRUCT;
...
delete pStruct;

Ответ 13

Вы могли бы вернуться к BYTE * и удалить:

delete[] (BYTE*)pStruct;

Ответ 14

Всегда лучше сохранять/освобождать любой ресурс как можно более сбалансированным. Хотя утечка или нет, трудно сказать в этом случае. Это зависит от реализации компилятора выделения vector (de).

BYTE * pBytes = new BYTE [sizeof(STRUCT) + nPaddingSize];

STRUCT* pStruct = reinterpret_cast< STRUCT* > ( pBytes ) ;

 // do stuff with pStruct

delete [] pBytes ;

Ответ 15

Вы как бы смешиваете C и С++ способы делать что-то. Зачем выделять больше, чем размер STRUCT? Почему не просто "новая СТРУКТУРА"? Если вы это сделаете, то в этом случае может быть яснее использовать malloc и бесплатно, так как тогда вы или другие программисты могут быть немного менее склонны делать предположения о типах и размерах выделенных объектов.

Ответ 16

Len: проблема в том, что pStruct является STRUCT *, но выделенная память фактически является BYTE [] неизвестного размера. Таким образом, delete [] pStruct не будет выделять всю выделенную память.

Ответ 17

Использовать новый оператор и удалить:

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;
}

Ответ 18

Я думаю, что это не утечка памяти.

STRUCT* pStruct = (STRUCT*)new BYTE [sizeof(STRUCT) + nPaddingSize];

Это преобразуется в вызов выделения памяти в операционной системе, на который возвращается указатель на эту память. В то время, когда выделена память, размер sizeof(STRUCT) и размер nPaddingSize будут известны для выполнения любых запросов на распределение памяти в отношении базовой операционной системы.

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

Вы видите, что компилятор C/С++ не управляет памятью, базовая операционная система.

Я согласен, что есть более чистые методы, но OP действительно сказал, что это устаревший код.

Короче говоря, я не вижу утечки памяти, так как принятый ответ считает, что он один.

Ответ 19

@Matt Cruikshank Вы должны обратить внимание и прочитать то, что я написал снова, потому что я никогда не предлагал не вызывать delete [] и просто позволить ОС очистить. И вы ошибаетесь в библиотеках времени выполнения С++, управляющих кучей. Если бы это было так, то С++ не был бы переносимым, как сегодня, и сбойное приложение никогда не будет очищено ОС. (признавая, что существуют конкретные сроки выполнения ОС, которые делают C/С++ не переносимыми). Я предлагаю вам найти stdlib.h в источниках Linux из kernel.org. Новое ключевое слово в С++ на самом деле говорит с теми же программами управления памятью, что и malloc.

Библиотеки времени выполнения С++ создают системные вызовы ОС, и это ОС, которая управляет кучами. Вы отчасти правы в том, что библиотеки времени выполнения указывают, когда выпустить память, однако они фактически не перетаскивают таблицы кучи напрямую. Другими словами, время выполнения, на которое вы ссылаетесь, не добавляет код в ваше приложение, чтобы ходить в кучи для выделения или освобождения. Это происходит в Windows, Linux, Solaris, AIX и т.д. Это также причина, по которой вы не будете штрафовать malloc в любом источнике ядра Linux и не найдете stdlib.h в Linux-источнике. Понимать, что в этой современной операционной системе есть менеджеры виртуальной памяти, которые еще немного усложняют ситуацию.

Вы когда-нибудь задумывались, почему вы можете позвонить в malloc для 2G RAM в ящике 1G и вернуть верный указатель памяти?

Управление памятью на процессорах x86 управляется в пространстве Kernel с использованием трех таблиц. PAM (таблица распределения страниц), PD (страницы) и PT (таблицы страниц). Это на аппаратном уровне, о котором я говорю. Одна из вещей, которую менеджер OS памяти делает, а не ваше приложение на С++, заключается в том, чтобы узнать, сколько физической памяти установлено на ящике во время загрузки с помощью вызовов BIOS. ОС также обрабатывает исключения, например, когда вы пытаетесь получить доступ к памяти, у вашего приложения также нет прав. (Сбой общей защиты GPF).

Возможно, мы говорим то же самое, что и Мэтт, но я думаю, что вы можете немного запутать функциональность под капотом. Я использую для поддержки компилятора C/С++ для жизни...

Ответ 20

@ericmayo - преступники. Ну, экспериментируя с VS2005, я не могу получить честную утечку из скалярного удаления в памяти, созданную вектором new. Я думаю, что поведение компилятора "undefined" здесь, это лучшая защита, которую я могу собрать.

Вы должны признать, что это действительно паршивая практика делать то, что сказал оригинальный плакат.

Если бы это было так, то С++ не переносимы, как сегодня, и сбой приложения никогда не получится очищается ОС.

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

Ответ 21

@Matt Cruikshank

"Ну, экспериментируя с VS2005, я не могу получить честную утечку из скалярного удаления в памяти, созданную новым вектором. Я думаю, что поведение компилятора" undefined "здесь, это лучшая защита я может собрать".

Я не согласен с тем, что это поведение компилятора или даже проблема с компилятором. "Новое" ключевое слово компилируется и связывается, как вы указали, с библиотеками времени выполнения. Эти библиотеки времени выполнения обрабатывают вызовы управления памятью ОС в независимом от ОС синтаксисе, и эти библиотеки времени выполнения несут ответственность за постоянную работу malloc и новой работы между такими операционными системами, как Linux, Windows, Solaris, AIX и т.д..... Именно по этой причине я упомянул аргумент переносимости; попытка доказать вам, что время выполнения также не управляет памятью.

ОС управляет памятью.

Интерфейс run-time 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, что код может иметь утечку памяти, но поскольку он утверждает, что позже он видит, что вызов получает вызов. "Позже, однако, память освобождается с помощью вызова delete:"

Кроме того, моя причина для ответа, как и я, была связана с комментариями OP "структуры переменной длины (TAPI), где размер структуры будет зависеть от строк переменной длины"

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

Ответ 22

В дополнение к превосходным ответам выше, я также хотел бы добавить:

Если ваш код работает в Linux или если вы можете скомпилировать его в linux, я бы предложил запустить его через Valgrind. Это отличный инструмент, среди множества полезных предупреждений, которые он производит, он также расскажет вам, когда вы выделяете память как массив, а затем освобождаете ее как не-массив (и наоборот).

Ответ 23

Роб Уокер ответ хорош.

Просто небольшое добавление, если у вас нет конструктора или/или distructors, поэтому вам в основном нужно выделить и освободить кусок необработанной памяти, подумайте об использовании пары free/malloc.

Ответ 24

ericmayo.myopenid.com настолько ошибочен, что кто-то с достаточной репутацией должен понизить его.

Библиотеки времени выполнения C или С++ управляют кучей, которая предоставляется ей в блоках операционной системой, как вы указываете, Эрик. Однако разработчик должен указать компилятору, какие вызовы времени выполнения должны выполняться для освобождения памяти и, возможно, уничтожать объекты, которые там есть. В этом случае необходимо удалить вектор (aka delete []), чтобы среда выполнения С++ оставила кучу в допустимом состоянии. Тот факт, что, когда PROCESS завершается, ОС достаточно умна, чтобы освобождать лежащие в основе блоки памяти, не является чем-то, на что разработчики должны полагаться. Это будет похоже на то, что вы никогда не вызываете delete.