Будет ли этот код C ++ вызывать утечку памяти (приведение нового массива) - PullRequest
10 голосов
/ 16 сентября 2008

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

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

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

delete pStruct;

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

Ответы [ 24 ]

12 голосов
/ 16 сентября 2008

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

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

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

delete [] (BYTE*) pStruct;
7 голосов
/ 16 сентября 2008

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

6 голосов
/ 16 сентября 2008

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

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

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

delete [] (BYTE*)(pStruct);
6 голосов
/ 16 сентября 2008

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

См. Это, кроме C ++ Gotchas: http://www.informit.com/articles/article.aspx?p=30642 почему.

У Раймонда Чена есть объяснение того, как vector 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 в конце концов;)

4 голосов
/ 16 сентября 2008

Стандарт C ++ четко гласит:

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

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

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

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

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

3 голосов
/ 16 сентября 2008

Как выделено в других постах:

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, то вы можете рассмотреть вопрос о выделении памяти и затем использовать размещение new:

BYTE * pByteData = new BYTE[sizeof(STRUCT) + nPaddingSize];
STRUCT * pStruct = new (pByteData) STRUCT ();
// ...
pStruct->~STRUCT ();
delete[] pByteData;
2 голосов
/ 16 сентября 2008

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

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

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

2 голосов
/ 20 сентября 2008

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

Резюме!

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

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

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

2 голосов
/ 17 сентября 2008

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

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

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

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

1 голос
/ 16 сентября 2008

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

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

Пример кода приводит к неопределенному поведению, простому и понятному. Ответ SlicedLime является прямым и точным (с оговоркой, что слово «вектор» должно быть заменено на «массив», поскольку векторы - это STL).

Такие вещи довольно хорошо описаны в FAQ C ++ (разделы 16.12, 16.13 и 16.14):

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...