Вызовет ли этот код C ++ утечку памяти (создание нового массива)

StackOverflow https://stackoverflow.com/questions/73134

Вопрос

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

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

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

delete pStruct;

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

Это было полезно?

Решение

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

Что еще более важно, если 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 если вам нужно сменить владельца.

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

Смотрите это, за исключением ошибок на C ++: 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 в конце концов ;)

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

  1. Тот Самый delete должен быть массив delete [].
  2. Тот Самый delete должен вызываться по указателю на тот же тип, что и выделенный тип.

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

delete [] (BYTE*)(pStruct);

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

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

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

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

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

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

Как подчеркивалось в других сообщениях:

1) Вызовы для создания / удаления выделяют память и могут вызывать конструкторы / деструкторы (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, то вы могли бы рассмотреть возможность выделения памяти, а затем использовать placement new:

BYTE * pByteData = new BYTE[sizeof(STRUCT) + nPaddingSize];
STRUCT * pStruct = new (pByteData) STRUCT ();
// ...
pStruct->~STRUCT ();
delete[] pByteData;

@eric - Спасибо за комментарии.Но ты продолжаешь что-то говорить, и это сводит меня с ума:

Эти библиотеки времени выполнения обрабатывают вызовы управления памятью в операционной системе с помощью не зависящего от операционной системы согласованного синтаксиса. эти библиотеки времени выполнения отвечают за обеспечение согласованной работы malloc и new между операционными системами, такими как Linux, Windows, Solaris, AIX и т.д....

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

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

Различные возможные варианты использования ключевых слов 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, чтобы полная конструкция могла быть обработана выражением placement 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 );
    }
};

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

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

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

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

delete ObjectPtr;

Краткое содержание!

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

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

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

Если вы в самом деле если вам необходимо сделать что-то подобное, вам, вероятно, следует позвонить оператору new непосредственно:

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

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

В настоящее время я не могу голосовать, но ответ слайседлайма предпочтительнее, чтобы Ответ Роба Уокера, поскольку проблема не имеет ничего общего с распределителями или с тем, имеет ли СТРУКТУРА деструктор.

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

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

Такого рода вопросы довольно хорошо описаны в разделе часто задаваемых вопросов по C ++ (разделы 16.12, 16.13 и 16.14).:

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

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

Вы могли бы привести обратно к БАЙТУ * и удалить:

delete[] (BYTE*)pStruct;

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

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

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

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 ;

Лен:проблема в том, что pStruct - это STRUCT * , но выделенная память на самом деле представляет собой БАЙТ [] некоторого неизвестного размера.Таким образом, delete[] pStruct не будет перераспределять всю выделенную память.

Вы как бы смешиваете способы ведения дел на C и C ++.Зачем выделять больше, чем размер СТРУКТУРЫ?Почему бы просто не "новая СТРУКТУРА"?Если вы должны это сделать, то, возможно, было бы понятнее использовать 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 для получения 2 ГБ оперативной памяти в блоке 1 ГБ и при этом получить обратно действительный указатель на память?

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

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

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

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

Если бы это было так, то C ++ не был бы переносимым, как сегодня, и зависающее приложение никогда не было бы очищено операционной системой.

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

@Мэтт Крукшенк

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

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

Операционная система управляет памятью.

Интерфейс библиотек времени выполнения к операционной системе..В 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

Смотрите ближе к концу страницы 149:"Звонки в malloc и free могут осуществляться в любом порядке;malloc вызывает операционную систему для получения дополнительного объема памяти по мере необходимости.Эти процедуры иллюстрируют некоторые соображения, связанные с написанием машинно-зависимого кода относительно машинонезависимым способом, а также демонстрируют реальное применение структур, объединений и typedef ".

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

О, тут я не могу не согласиться.Я хотел сказать, что исходный код poster не способствовал утечке памяти.Это все, что я хотел сказать.Я не вмешивался в то, что касается наилучшей практики.Поскольку код вызывает delete, память освобождается.

Я согласен, в вашу защиту, если исходный код poster никогда не завершался или никогда не попадал в вызов delete, что в коде могла быть утечка памяти, но поскольку он заявляет, что позже он видит, что вызывается delete."Однако позже память освобождается с помощью вызова delete:"

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

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

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

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

Используйте оператор new и delete:

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

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

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

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

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

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

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

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

Роб Уокер Ответить это хорошо.

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top